weblate/trans/views/edit.py
2013-09-18 16:04:14 +02:00

695 lines
22 KiB
Python
Vendored

# -*- coding: utf-8 -*-
#
# Copyright © 2012 - 2013 Michal Čihař <michal@cihar.com>
#
# This file is part of Weblate <http://weblate.org/>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from django.shortcuts import render_to_response, get_object_or_404
from django.utils.translation import ugettext as _
from django.template import RequestContext
from django.http import HttpResponseRedirect, HttpResponse
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.utils import formats
import uuid
import time
from trans.models import SubProject, Unit, Change
from trans.models.unitdata import Comment, Suggestion
from trans.forms import (
TranslationForm, SearchForm,
MergeForm, AutoForm, ReviewForm,
AntispamForm, CommentForm, RevertForm
)
from trans.views.helper import get_translation
from trans.checks import CHECKS
from trans.util import join_plural
def get_filter_name(rqtype):
'''
Returns name of current filter.
'''
if rqtype == 'fuzzy':
return _('Fuzzy strings')
elif rqtype == 'untranslated':
return _('Untranslated strings')
elif rqtype == 'suggestions':
return _('Strings with suggestions')
elif rqtype == 'allchecks':
return _('Strings with any failing checks')
elif rqtype in CHECKS:
return CHECKS[rqtype].name
def get_search_name(search_type, search_query):
'''
Returns name for search.
'''
if search_type == 'ftx':
return _('Fulltext search for "%s"') % search_query
elif search_type == 'exact':
return _('Search for exact string "%s"') % search_query
else:
return _('Substring search for "%s"') % search_query
def cleanup_session(session):
'''
Deletes old search results from session storage.
'''
now = int(time.time())
for key in session.keys():
if key.startswith('search_') and session[key]['ttl'] < now:
del session[key]
def show_form_errors(request, form):
'''
Shows all form errors as a message.
'''
for error in form.non_field_errors():
messages.error(request, error)
for field in form:
for error in field.errors:
messages.error(
request,
_('Error in parameter %(field)s: %(error)s') % {
'field': field.name,
'error': error
}
)
def search(translation, request):
'''
Performs search or returns cached search results.
'''
# Already performed search
if 'sid' in request.GET:
# Grab from session storage
search_id = 'search_%s' % request.GET['sid']
# Check if we know the search
if search_id not in request.session:
messages.error(request, _('Invalid search string!'))
return HttpResponseRedirect(translation.get_absolute_url())
return request.session[search_id]
# Possible new search
rqtype = request.GET.get('type', 'all')
search_form = SearchForm(request.GET)
review_form = ReviewForm(request.GET)
search_query = None
if review_form.is_valid():
# Review
allunits = translation.unit_set.review(
review_form.cleaned_data['date'],
request.user
)
formatted_date = formats.date_format(
review_form.cleaned_data['date'],
'SHORT_DATE_FORMAT'
)
name = _('Review of translations since %s') % formatted_date
elif search_form.is_valid():
# Apply search conditions
allunits = translation.unit_set.search(
search_form.cleaned_data['search'],
search_form.cleaned_data['q'],
search_form.cleaned_data['src'],
search_form.cleaned_data['ctx'],
search_form.cleaned_data['tgt'],
)
search_query = search_form.cleaned_data['q']
name = get_search_name(
search_form.cleaned_data['search'],
search_query,
)
else:
# Error reporting
if 'date' in request.GET:
show_form_errors(request, review_form)
elif 'q' in request.GET:
show_form_errors(request, search_form)
# Filtering by type
allunits = translation.unit_set.filter_type(
rqtype,
translation,
ignored='ignored' in request.GET
)
name = get_filter_name(rqtype)
# Grab unit IDs
unit_ids = list(allunits.values_list('id', flat=True))
# Check empty search results
if len(unit_ids) == 0:
messages.warning(request, _('No string matched your search!'))
return HttpResponseRedirect(translation.get_absolute_url())
# Checksum unit access
offset = 0
if 'checksum' in request.GET:
try:
unit = allunits.filter(checksum=request.GET['checksum'])[0]
offset = unit_ids.index(unit.id)
except (Unit.DoesNotExist, IndexError):
messages.warning(request, _('No string matched your search!'))
return HttpResponseRedirect(translation.get_absolute_url())
# Remove old search results
cleanup_session(request.session)
# Store in cache and return
search_id = str(uuid.uuid1())
search_result = {
'query': search_query,
'name': name,
'ids': unit_ids,
'search_id': search_id,
'ttl': int(time.time()) + 86400,
'offset': offset,
}
request.session['search_%s' % search_id] = search_result
return search_result
def handle_translate_suggest(unit, form, request,
this_unit_url, next_unit_url):
'''
Handle suggesion saving.
'''
if form.cleaned_data['target'][0] == '':
messages.error(request, _('Your suggestion is empty!'))
# Stay on same entry
return HttpResponseRedirect(this_unit_url)
# Invite user to become translator if there is nobody else
recent_changes = Change.objects.content(True).filter(
translation=unit.translation,
).exclude(
user=None
)
if not recent_changes.exists():
messages.info(request, _(
'There is currently no active translator for this '
'translation, please consider becoming a translator '
'as your suggestion might otherwise remain unreviewed.'
))
# Create the suggestion
Suggestion.objects.add(
unit,
join_plural(form.cleaned_data['target']),
request,
)
return HttpResponseRedirect(next_unit_url)
def handle_translate(obj, request, user_locked, this_unit_url, next_unit_url):
'''
Saves translation or suggestion to database and backend.
'''
# Antispam protection
antispam = AntispamForm(request.POST)
if not request.user.is_authenticated() and not antispam.is_valid():
# Silently redirect to next entry
return HttpResponseRedirect(next_unit_url)
form = TranslationForm(request.POST)
if not form.is_valid():
return
# Check whether translation is not outdated
obj.check_sync()
try:
unit = Unit.objects.get_checksum(
request,
obj,
form.cleaned_data['checksum'],
)
except (Unit.DoesNotExist, IndexError):
return
if 'suggest' in request.POST:
return handle_translate_suggest(
unit, form, request, this_unit_url, next_unit_url
)
elif not request.user.is_authenticated():
# We accept translations only from authenticated
messages.error(
request,
_('You need to log in to be able to save translations!')
)
elif not request.user.has_perm('trans.save_translation'):
# Need privilege to save
messages.error(
request,
_('You don\'t have privileges to save translations!')
)
elif (unit.only_vote_suggestions()
and not request.user.has_perm('trans.save_translation')):
messages.error(
request,
_('Only suggestions are allowed in this translation!')
)
elif not user_locked:
# Remember old checks
oldchecks = set(
unit.active_checks().values_list('check', flat=True)
)
# Custom commit message
if 'commit_message' in request.POST and request.POST['commit_message']:
unit.translation.commit_message = request.POST['commit_message']
unit.translation.save()
# Save
saved, fixups = unit.translate(
request,
form.cleaned_data['target'],
form.cleaned_data['fuzzy']
)
# Warn about applied fixups
if len(fixups) > 0:
messages.info(
request,
_('Following fixups were applied to translation: %s') %
', '.join([unicode(f) for f in fixups])
)
# Get new set of checks
newchecks = set(
unit.active_checks().values_list('check', flat=True)
)
# Did we introduce any new failures?
if saved and newchecks > oldchecks:
# Show message to user
messages.error(
request,
_('Some checks have failed on your translation!')
)
# Stay on same entry
return HttpResponseRedirect(this_unit_url)
# Redirect to next entry
return HttpResponseRedirect(next_unit_url)
def handle_merge(obj, request, next_unit_url):
'''
Handles unit merging.
'''
if not request.user.has_perm('trans.save_translation'):
# Need privilege to save
messages.error(
request,
_('You don\'t have privileges to save translations!')
)
return
mergeform = MergeForm(request.GET)
if not mergeform.is_valid():
return
try:
unit = Unit.objects.get_checksum(
request,
obj,
mergeform.cleaned_data['checksum'],
)
except Unit.DoesNotExist:
return
merged = Unit.objects.get(
pk=mergeform.cleaned_data['merge']
)
if unit.checksum != merged.checksum:
messages.error(
request,
_('Can not merge different messages!')
)
else:
# Store unit
unit.target = merged.target
unit.fuzzy = merged.fuzzy
saved = unit.save_backend(request)
# Update stats if there was change
if saved:
profile = request.user.get_profile()
profile.translated += 1
profile.save()
# Redirect to next entry
return HttpResponseRedirect(next_unit_url)
def handle_revert(obj, request, next_unit_url):
if not request.user.has_perm('trans.save_translation'):
# Need privilege to save
messages.error(
request,
_('You don\'t have privileges to save translations!')
)
return
revertform = RevertForm(request.GET)
if not revertform.is_valid():
return
try:
unit = Unit.objects.get_checksum(
request,
obj,
revertform.cleaned_data['checksum'],
)
except Unit.DoesNotExist:
return
change = Change.objects.get(
pk=revertform.cleaned_data['revert']
)
if unit.checksum != change.unit.checksum:
messages.error(
request,
_('Can not revert to different unit!')
)
elif change.target == "":
messages.error(
request,
_('Can not revert to empty translation!')
)
else:
# Store unit
unit.target = change.target
unit.save_backend(request, change_action=Change.ACTION_REVERT)
# Redirect to next entry
return HttpResponseRedirect(next_unit_url)
def handle_suggestions(obj, request, this_unit_url):
'''
Handles suggestion deleting/accepting.
'''
# Check for authenticated users
if not request.user.is_authenticated():
messages.error(
request,
_('You need to log in to be able to manage suggestions!')
)
return HttpResponseRedirect(this_unit_url)
sugid = ''
# Parse suggestion ID
for param in ('accept', 'delete', 'upvote', 'downvote'):
if param in request.POST:
sugid = request.POST[param]
break
try:
sugid = int(sugid)
suggestion = Suggestion.objects.get(pk=sugid)
if 'accept' in request.POST:
# Accept suggesion
if not request.user.has_perm('trans.accept_suggestion'):
messages.error(
request,
_('You do not have privilege to accept suggestions!')
)
return HttpResponseRedirect(this_unit_url)
suggestion.accept(obj, request)
elif 'delete' in request.POST:
# Delete suggestion
if not request.user.has_perm('trans.delete_suggestion'):
messages.error(
request,
_('You do not have privilege to delete suggestions!')
)
return HttpResponseRedirect(this_unit_url)
suggestion.delete()
elif 'upvote' in request.POST:
if not request.user.has_perm('trans.vote_suggestion'):
messages.error(
request,
_('You do not have privilege to vote for suggestions!')
)
return HttpResponseRedirect(this_unit_url)
suggestion.add_vote(obj, request, True)
elif 'downvote' in request.POST:
if not request.user.has_perm('trans.vote_suggestion'):
messages.error(
request,
_('You do not have privilege to vote for suggestions!')
)
return HttpResponseRedirect(this_unit_url)
suggestion.add_vote(obj, request, False)
except (Suggestion.DoesNotExist, ValueError):
messages.error(request, _('Invalid suggestion!'))
# Redirect to same entry for possible editing
return HttpResponseRedirect(this_unit_url)
def translate(request, project, subproject, lang):
'''
Generic entry point for translating, suggesting and searching.
'''
obj = get_translation(request, project, subproject, lang)
# Check locks
project_locked, user_locked, own_lock = obj.is_locked(request, True)
locked = project_locked or user_locked
# Search results
search_result = search(obj, request)
# Handle redirects
if isinstance(search_result, HttpResponse):
return search_result
# Get numer of results
num_results = len(search_result['ids'])
# Search offset
try:
offset = int(request.GET.get('offset', search_result.get('offset', 0)))
except ValueError:
offset = 0
# Check boundaries
if offset < 0 or offset >= num_results:
messages.info(request, _('You have reached end of translating.'))
# Delete search
del request.session['search_%s' % search_result['search_id']]
# Redirect to translation
return HttpResponseRedirect(obj.get_absolute_url())
# Some URLs we will most likely use
base_unit_url = '%s?sid=%s&offset=' % (
obj.get_translate_url(),
search_result['search_id'],
)
this_unit_url = base_unit_url + str(offset)
next_unit_url = base_unit_url + str(offset + 1)
response = None
# Any form submitted?
if request.method == 'POST' and not project_locked:
# Handle accepting/deleting suggestions
if ('accept' not in request.POST
and 'delete' not in request.POST
and 'upvote' not in request.POST
and 'downvote' not in request.POST):
response = handle_translate(
obj, request, user_locked, this_unit_url, next_unit_url
)
elif not locked:
response = handle_suggestions(obj, request, this_unit_url)
# Handle translation merging
elif 'merge' in request.GET and not locked:
response = handle_merge(obj, request, next_unit_url)
# Handle reverting
elif 'revert' in request.GET and not locked:
response = handle_revert(obj, request, this_unit_url)
# Pass possible redirect further
if response is not None:
return response
# Grab actual unit
try:
unit = obj.unit_set.get(pk=search_result['ids'][offset])
except Unit.DoesNotExist:
# Can happen when using SID for other translation
messages.error(request, _('Invalid search string!'))
return HttpResponseRedirect(obj.get_absolute_url())
# Show secondary languages for logged in users
if request.user.is_authenticated():
secondary = request.user.get_profile().get_secondary_units(unit)
antispam = None
else:
secondary = None
antispam = AntispamForm()
# Prepare form
form = TranslationForm(initial={
'checksum': unit.checksum,
'target': (unit.translation.language, unit.get_target_plurals()),
'fuzzy': unit.fuzzy,
})
return render_to_response(
'translate.html',
RequestContext(
request,
{
'this_unit_url': this_unit_url,
'first_unit_url': base_unit_url + '0',
'last_unit_url': base_unit_url + str(num_results - 1),
'next_unit_url': next_unit_url,
'prev_unit_url': base_unit_url + str(offset - 1),
'object': obj,
'unit': unit,
'total': obj.unit_set.all().count(),
'search_id': search_result['search_id'],
'search_query': search_result['query'],
'offset': offset,
'filter_name': search_result['name'],
'filter_count': num_results,
'filter_pos': offset + 1,
'form': form,
'antispam': antispam,
'comment_form': CommentForm(),
'search_form': SearchForm(),
'update_lock': own_lock,
'secondary': secondary,
'locked': locked,
'user_locked': user_locked,
'project_locked': project_locked,
},
)
)
@login_required
@permission_required('trans.automatic_translation')
def auto_translation(request, project, subproject, lang):
obj = get_translation(request, project, subproject, lang)
obj.commit_pending(request)
autoform = AutoForm(obj, request.POST)
change = None
if not obj.subproject.locked and autoform.is_valid():
if autoform.cleaned_data['inconsistent']:
units = obj.unit_set.filter_type('inconsistent', obj)
elif autoform.cleaned_data['overwrite']:
units = obj.unit_set.all()
else:
units = obj.unit_set.filter(translated=False)
sources = Unit.objects.filter(
translation__language=obj.language,
translated=True
)
if autoform.cleaned_data['subproject'] == '':
sources = sources.filter(
translation__subproject__project=obj.subproject.project
).exclude(
translation=obj
)
else:
subprj = SubProject.objects.get(
project=obj.subproject.project,
slug=autoform.cleaned_data['subproject']
)
sources = sources.filter(translation__subproject=subprj)
for unit in units.iterator():
update = sources.filter(checksum=unit.checksum)
if update.exists():
# Get first entry
update = update[0]
# No save if translation is same
if unit.fuzzy == update.fuzzy and unit.target == update.target:
continue
# Copy translation
unit.fuzzy = update.fuzzy
unit.target = update.target
# Create signle change object for whole merge
if change is None:
change = Change.objects.create(
action=Change.ACTION_AUTO,
translation=unit.translation,
user=request.user,
author=request.user
)
# Save unit to backend
unit.save_backend(request, False, False)
messages.info(request, _('Automatic translation completed.'))
else:
messages.error(request, _('Failed to process form!'))
return HttpResponseRedirect(obj.get_absolute_url())
@login_required
def comment(request, pk):
'''
Adds new comment.
'''
obj = get_object_or_404(Unit, pk=pk)
obj.check_acl(request)
if request.POST.get('type', '') == 'source':
lang = None
else:
lang = obj.translation.language
form = CommentForm(request.POST)
if form.is_valid():
Comment.objects.add(
obj,
request.user,
lang,
form.cleaned_data['comment']
)
messages.info(request, _('Posted new comment'))
else:
messages.error(request, _('Failed to add comment!'))
return HttpResponseRedirect(obj.get_absolute_url())