Last active
August 29, 2015 14:03
-
-
Save alex-silva/40313734b9f1cd37f204 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2013 Zuza Software Foundation | |
# Copyright 2013 Evernote Corporation | |
# | |
# This file is part of Pootle. | |
# | |
# Pootle 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 2 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 functools import wraps | |
from django.core.exceptions import PermissionDenied | |
from django.http import Http404, HttpResponse | |
from django.shortcuts import get_object_or_404 | |
from django.utils.translation import ugettext as _ | |
from pootle.core.exceptions import Http400 | |
from pootle.core.url_helpers import split_pootle_path | |
from pootle_app.models import Directory | |
from pootle_app.models.permissions import (check_permission, | |
get_matching_permissions) | |
from pootle_misc.util import jsonify | |
from pootle_profile.models import get_profile | |
from .models import Store, Unit | |
def _common_context(request, translation_project, permission_codes): | |
"""Adds common context to request object and checks permissions.""" | |
request.translation_project = translation_project | |
_check_permissions(request, translation_project.directory, | |
permission_codes) | |
def _check_permissions(request, directory, permission_codes): | |
"""Checks if the current user has enough permissions defined by | |
`permission_codes` in the current`directory`. | |
""" | |
request.profile = get_profile(request.user) | |
request.permissions = get_matching_permissions(request.profile, | |
directory) | |
if not permission_codes: | |
return | |
if isinstance(permission_codes, basestring): | |
permission_codes = [permission_codes] | |
for permission_code in permission_codes: | |
if not check_permission(permission_code, request): | |
raise PermissionDenied( | |
_("Insufficient rights to access this directory."), | |
) | |
def get_store_context(permission_codes): | |
def wrap_f(f): | |
@wraps(f) | |
def decorated_f(request, pootle_path, *args, **kwargs): | |
if pootle_path[0] != '/': | |
pootle_path = '/' + pootle_path | |
try: | |
store = Store.objects.select_related('translation_project', | |
'parent') \ | |
.get(pootle_path=pootle_path) | |
except Store.DoesNotExist: | |
raise Http404 | |
_common_context(request, store.translation_project, permission_codes) | |
request.store = store | |
request.directory = store.parent | |
return f(request, store, *args, **kwargs) | |
return decorated_f | |
return wrap_f | |
def get_unit_context(permission_codes): | |
def wrap_f(f): | |
@wraps(f) | |
def decorated_f(request, uid, *args, **kwargs): | |
unit = get_object_or_404( | |
Unit.objects.select_related("store__translation_project", | |
"store__parent"), | |
id=uid, | |
) | |
_common_context(request, unit.store.translation_project, | |
permission_codes) | |
request.unit = unit | |
request.store = unit.store | |
request.directory = unit.store.parent | |
return f(request, unit, *args, **kwargs) | |
return decorated_f | |
return wrap_f | |
def get_xhr_resource_context(permission_codes): | |
"""Gets the resource context for a XHR view. | |
Note that the pootle path string (which includes language, project and | |
resource information) will be read from the `request.GET` dictionary, | |
not from the decorated function arguments. | |
:param permission_codes: Permissions codes to optionally check. | |
""" | |
def wrap_f(f): | |
@wraps(f) | |
def decorated_f(request): | |
"""Loads :cls:`pootle_app.models.Directory` and | |
:cls:`pootle_store.models.Store` models and populates the | |
request object. | |
""" | |
pootle_path = request.GET.get('path', None) | |
if pootle_path is None: | |
raise Http400(_('Arguments missing.')) | |
lang, proj, dir_path, filename = split_pootle_path(pootle_path) | |
store = None | |
if filename: | |
try: | |
store = Store.objects.select_related( | |
'parent', | |
).get(pootle_path=pootle_path) | |
directory = store.parent | |
except Store.DoesNotExist: | |
raise Http404(_('File does not exist.')) | |
else: | |
directory = Directory.objects.get(pootle_path=pootle_path) | |
_check_permissions(request, directory, permission_codes) | |
path_obj = store or directory | |
request.pootle_path = pootle_path | |
return f(request, path_obj) | |
return decorated_f | |
return wrap_f |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% load url from future %} | |
{% load i18n baseurl store_tags cleanhtml cache profile_tags locale %} | |
{% get_current_language as LANGUAGE_CODE %} | |
{% get_current_language_bidi as LANGUAGE_BIDI %} | |
{% cache settings.CACHE_TIMEOUT unit_edit unit.id unit.mtime cantranslate cansuggest canreview report_target altsrcs profile.id LANGUAGE_CODE %} | |
<td colspan="3" rowspan="1" class="translate-full translate-focus{% if unit.isfuzzy %} fuzzy-unit{% endif %}" dir="{% locale_dir %}"> | |
<div class="translate-container"> | |
<div class="unit-path"> | |
<span class="pull-{% locale_align %}"> | |
<p class="sidetitle" title="{{ unit.store.path }}"> | |
{{ language.name }} / {{ project }} / {{ unit.store.path }} | |
</p> | |
</span> | |
<span class="pull-{% locale_reverse_align %}"> | |
<a href="{{ unit.get_translate_url }}">{% blocktrans with id=unit.id %}String {{ id }}{% endblocktrans %}</a> | |
</span> | |
</div> | |
<div class="translate-{% locale_align %}"> | |
{% if unit.getcontext and unit.locations != unit.context %} | |
<!-- Context information and comments --> | |
<div class="translate-context sidebar"> | |
<div class="sidetitle" lang="{{ LANGUAGE_CODE }}">{% trans "Context:" %}</div> | |
<div class="translate-context-value"> | |
{{ unit.getcontext }} | |
</div> | |
</div> | |
{% endif %} | |
{% if unit.developer_comment or unit.locations %} | |
<!-- Developer comments --> | |
<div class="comments sidebar"> | |
{% if unit.developer_comment %} | |
<div class="sidetitle" lang="{{ LANGUAGE_CODE }}">{% trans "Comments:" %}</div> | |
<div class="developer-comments" lang="{{ source_language.code }}" dir="{{ source_language.direction }}"{% if unit.locations %}title="{{ unit.locations }}"{% endif %}>{{ unit.developer_comment|urlize|url_target_blank|linebreaks }}</div> | |
{% with image_urls=unit.developer_comment|image_urls %} | |
{% if image_urls %} | |
<div class="developer-images hr"> | |
{% for image_url in image_urls %} | |
<a class="js-dev-img" href="{{ image_url }}"> | |
<img src="{{ image_url }}"/> | |
</a> | |
{% endfor %} | |
</div> | |
{% endif %} | |
{% endwith %} | |
{% endif %} | |
{% if unit.locations and not unit.developer_comment %} | |
<div class="sidetitle" lang="{{ LANGUAGE_CODE }}">{% trans "Locations:" %}</div> | |
<div class="translate-locations" lang="en" dir="ltr" title="{{ unit.locations }}">{{ unit.locations|truncatewords:3 }}</div> | |
{% endif %} | |
</div> | |
{% endif %} | |
<!-- Terminology suggestions --> | |
{% with unit.get_terminology as terms %} | |
{% if terms %} | |
<div id="tm" class="sidebar" dir="{% locale_dir %}"> | |
<div class="sidetitle" lang="{{ LANGUAGE_CODE }}">{% trans "Terminology:" %}</div> | |
{% for term in terms %} | |
<div class="tm-unit js-editor-copytext" title="{% trans 'Insert the translated term into the editor' %}"> | |
<span class="tm-original" dir="{{ source_language.direction }}" lang="{{ source_language.code }}">{{ term.source }}</span> | |
<span class="tm-translation" dir="{{ language.direction }}" lang="{{ language.code }}">{{ term.target }}</span> | |
</div> | |
{% endfor %} | |
</div> | |
{% endif %} | |
{% endwith %} | |
</div> | |
<div class="translate-{% if LANGUAGE_BIDI %}left{% else %}right{% endif %}"> | |
{% if unit.get_qualitychecks.count %} | |
<!-- Quality Checks --> | |
<div id="translate-checks-block" dir="{% locale_dir %}"> | |
<div class="sidetitle" lang="{{ LANGUAGE_CODE }}" title='{% trans "Possible issues with the translation" %}'>{% trans "Failing checks:" %}</div> | |
<ul class="checks"> | |
{% for check in unit.get_qualitychecks.iterator %} | |
<li class="check"> | |
<a href="http://docs.translatehouse.org/projects/translate-toolkit/en/latest/commands/pofilter_tests.html#{{ check.name }}" target="_blank">{{ check.display_name }}</a> | |
{% if canreview %} | |
<a title="{% trans "Remove quality check" %}" class="js-reject-check" | |
data-check-id="{{ check.id }}"><i class="icon-block"></i></a> | |
{% endif %} | |
</li> | |
{% endfor %} | |
</ul> | |
</div> | |
{% endif %} | |
</div> | |
<div class="translate-middle"> | |
<div id="target-item-content"> | |
<form action="" method="post" name="translate" id="translate"> | |
{{ form.id.as_hidden }} | |
{{ form.index.as_hidden }} | |
{{ form.source_f.as_hidden }} | |
<div class="sources"> | |
<!-- Alternative source language translations --> | |
{% for altunit in altsrcs %} | |
<div class="source-language alternative"> | |
<div class="translation-text-headers" lang="{{ LANGUAGE_CODE }}" dir="{% locale_dir %}"> | |
<div class="language-name">{{ altunit.store.translation_project.language.name }}</div> | |
{% if cansuggest or cantranslate %} | |
<div class="translate-toolbar"> | |
<a class="icon-copy js-copyoriginal" title="{% trans 'Copy into translation' %}" accesskey="c"></a> | |
</div> | |
{% endif %} | |
</div> | |
<div id="unit-{{ altunit.id }}" class="translate-original{% if unit.hasplural %} translate-plural{% endif %}"> | |
{% for i, target, title in altunit|pluralize_target %} | |
<div class="translation-text" lang="{{ altunit.store.translation_project.language.code }}" dir="{{ altunit.store.translation_project.language.direction }}"{% if title %} title="{{ title }}"{% endif %}>{{ target|fancy_highlight }}</div> | |
{% endfor %} | |
<div class="placeholder"></div> | |
</div> | |
</div> | |
{% endfor %} | |
<!-- Original --> | |
<div class="source-language original"> | |
<div class="translation-text-headers" lang="{{ LANGUAGE_CODE }}" dir="{% locale_dir %}"> | |
<div class="language-name">{{ source_language.name }}</div> | |
{% if cansuggest or cantranslate %} | |
<div class="translate-toolbar"> | |
{% if report_target %} | |
<a class="icon-report-bug" href="{{ report_target }}" title="{% trans 'Report a problem with the source string' %}" target="_blank"></a> | |
{% endif %} | |
<a class="icon-copy js-copyoriginal" title="{% trans 'Copy into translation' %}" accesskey="c"></a> | |
</div> | |
{% endif %} | |
</div> | |
<div class="translate-original{% if unit.hasplural %} translate-plural{% endif %}"> | |
{% for i, source, title in unit|pluralize_source %} | |
<div class="translation-text" lang="{{ source_language.code }}" dir="{{ source_language.direction }}"{% if title %} title="{{ title }}"{% endif %}>{{ source|fancy_highlight }}</div> | |
{% endfor %} | |
<div class="placeholder"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Buttons, resize links, special characters --> | |
<div class="buttons translate-buttons-block" lang="{{ LANGUAGE_CODE }}" dir="{% locale_dir %}"> | |
{% if cantranslate %} | |
<input type="submit" name="submit" class="submit" tabindex="11" accesskey="s" value="{% trans 'Submit' %}" title="{% trans 'Submit translation and go to the next string (Ctrl+Enter)' %}" /> | |
{% endif %} | |
{% if cantranslate %} | |
<!-- | |
ADD NEW TRANSLATION | |
<div id="add"> | |
<form id="add-form" action="{% url 'pootle-xhr-units-add' unit.id %}" method="post"> | |
<p>{{ comment_form.translator_comment }}</p> | |
<p><input type="submit" value="{% trans 'Submit' %}" /></p> | |
</form> | |
</div>--> | |
{% endif %} | |
{% if cansuggest %} | |
<input type="submit" name="suggest" class="suggest" tabindex="11" accesskey="s" value="{% trans 'Suggest' %}" title="{% trans 'Suggest translation and go to the next string (Ctrl+Enter)' %}" /> | |
{% endif %} | |
<input type="hidden" name="store" value="{{ store }}" /> | |
<input type="hidden" name="path" value="{{ store|l }}" /> | |
<input type="hidden" name="pootle_path" value="{{ store.pootle_path }}" /> | |
{% if cansuggest and cantranslate %} | |
<div class="switch-suggest-mode tiny" lang="{{ LANGUAGE_CODE }}"> | |
<div class="suggest"><a href="#" title="{% trans 'Switch to translation mode (Ctrl+Shift+Space)' %}">↔ {% trans "Submit" %}</a></div> | |
<div class="submit"><a href="#" title="{% trans 'Switch to suggestion mode (Ctrl+Shift+Space)' %}">↔ {% trans "Suggest" %}</a></div> | |
</div> | |
{% endif %} | |
<div class="translate-fuzzy-block" lang="{{ LANGUAGE_CODE }}" title="{% trans 'Mark this string as needing further work (Ctrl+Space)' %}"> | |
{{ form.state }} {{ form.state.label_tag }} | |
</div> | |
</div> | |
<!-- Translation --> | |
<div id="orig{{ unit.index }}" class="translate-translation" lang="{{ LANGUAGE_CODE }}" dir="{% locale_dir %}"> | |
{% if unit.submitted_by %} | |
<div id="target-item-gravatar"> | |
<a href="{{ unit.submitted_by.get_absolute_url }}"><img src="{{ unit.submitted_by|gravatar:24 }}" alt="{{ unit.submitted_by.username }}" width="24" height="24" title="{{ unit.submitted_by.user }}" /></a> | |
</div> | |
{% endif %} | |
{{ form.target_f }} | |
{% if unit.submitted_by and unit.submitted_on %} | |
<div class="unit-meta"> | |
<time class="js-relative-date" title="{{ unit.submitted_on }}" | |
datetime="{{ unit.submitted_on.isoformat }}"> </time> | |
{% with reviewer=unit.get_reviewer %} | |
{% if reviewer %} | |
<br> | |
<span>({% blocktrans with profile_url=reviewer.get_absolute_url%}reviewed by <a href="{{ profile_url }}">{{ reviewer }}</a>{% endblocktrans %})</span> | |
{% endif %} | |
{% endwith %} | |
</div> | |
{% endif %} | |
{% if language.specialchars or language.direction == "rtl" %} | |
{% if cantranslate or cansuggest %} | |
<div class="editor-specialchars-block" lang="{{ language.code }}"> | |
{% if language.direction == "rtl" %} | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert RLM (Right-To-Left Mark) into the editor' %}" | |
data-entity="‏">RLM</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert LRM (Left-To-Right Mark) into the editor' %}" | |
data-entity="‎">LRM</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert RLE (Right-To-Left Embedding) into the editor' %}" | |
data-entity="‫">RLE</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert LRE (Left-To-Right Embedding) into the editor' %}" | |
data-entity="‪">LRE</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert RLO (Right-To-Left Override) into the editor' %}" | |
data-entity="‮">RLO</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert LRO (Left-To-Right Override) into the editor' %}" | |
data-entity="‭">LRO</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert PDF (Pop Directional Format ) into the editor' %}" | |
data-entity="‬">PDF</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert ZWJ (Zero Width Joiner) into the editor' %}" | |
data-entity="‍">ZWJ</a> | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert ZWNJ (Zero Width Non Joiner) into the editor' %}" | |
data-entity="‌">ZWNJ</a> | |
{% endif %} | |
{% for specialchar in language.specialchars %} | |
{% if not specialchar.isspace %} | |
<a class="editor-specialchar js-editor-copytext" | |
title="{% trans 'Insert this symbol into the editor' %}">{{ specialchar }}</a> | |
{% else %} | |
<span class="extraspace"> </span> | |
{% endif %} | |
{% endfor %} | |
</div> | |
{% endif %} | |
{% endif %} | |
</div> | |
</form> | |
</div> | |
<div id="extras-bar"> | |
{% if cantranslate %}<a class="js-upload-image" >{% trans "Upload Image" %}</a>{% endif %} | |
{% if cantranslate %}<a class="js-editor-comment" tabindex="15">{% trans "Add Comment" %}</a>{% endif %} | |
{% if unit.submission_set.count %} | |
<a id="js-show-timeline" href="{% url 'pootle-xhr-units-timeline' unit.id %}">{% trans 'Show Timeline' %}</a> | |
<a id="js-hide-timeline" class="hide">{% trans 'Hide Timeline' %}</a> | |
{% endif %} | |
{% if cantranslate %} | |
<div id="invalidated-show"> | |
{% if unit.isInvalidated %}<p style='color:red'>String changed in base language, please review</p>{% endif %} | |
</div> | |
<div id="upload-image" class="hide"> | |
<form id="image-form" enctype='multipart/form-data' action="{% url 'pootle-xhr-units-image' unit.id %}" method="post"> | |
{% csrf_token %} | |
<input type="file" name="image" id="id_image" /> | |
<p><input type="submit" value="{% trans 'Upload' %}" /></p> | |
</form> | |
</div> | |
<div id="editor-comment" class="hide"> | |
<form id="comment-form" action="{% url 'pootle-xhr-units-comment' unit.id %}" method="post"> | |
<p>{{ comment_form.translator_comment }}</p> | |
<p><input type="submit" value="{% trans 'Submit' %}" /></p> | |
</form> | |
</div> | |
{% endif %} | |
</div> | |
<!-- Latest comment, [timeline of changes], suggestions from users | |
and Translation Memory --> | |
<div id="extras-container"> | |
{% if unit.translator_comment %} | |
<!-- Latest comment --> | |
<div id="translator-comment" lang="{{ LANGUAGE_CODE }}"> | |
{% include "unit/comment.html" %} | |
</div> | |
{% endif %} | |
{% if unit.image %} | |
<!-- Latest image --> | |
<div id="uploaded-image" lang="{{ LANGUAGE_CODE }}"> | |
{% include "unit/image.html" %} | |
</div> | |
{% endif %} | |
{% if suggestions %} | |
<div id="suggestions"> | |
<div class="extra-item-title">{% trans 'User suggestions' %}</div> | |
{% for sugg, score in suggestions %} | |
<div id="suggestion-{{ sugg.id }}" class="extra-item-block"> | |
{% for i, target, diff, title in sugg|pluralize_diff_sugg %} | |
<div class="extra-item-content"> | |
{% if sugg.user %} | |
<div class="extra-item-gravatar"> | |
<a href="{{ sugg.user.get_absolute_url }}"><img src="{{ sugg.user|gravatar:24 }}" | |
alt="{{ sugg.user.username }}" width="24" height="24" | |
title="{{ sugg.user }}" /></a> | |
</div> | |
{% endif %} | |
<div class="extra-item js-editor-copytext" data-action="overwrite"> | |
{% block votes %}{% endblock %} | |
{% if canreview %} | |
<a accesskey="a" class="suggestion-action js-suggestion-accept" | |
data-sugg-id="{{ sugg.id }}"> | |
<i class="icon-accept" dir="{% locale_dir %}" | |
title="{% trans 'Accept suggestion' %}"></i> | |
</a> | |
<a accesskey="r" class="suggestion-action js-suggestion-reject" | |
data-sugg-id="{{ sugg.id }}"> | |
<i class="icon-reject" dir="{% locale_dir %}" | |
title="{% trans 'Reject suggestion' %}"></i> | |
</a> | |
{% else %} | |
{% if user.is_authenticated and profile == sugg.user %} | |
<a accesskey="r" class="suggestion-action js-suggestion-reject" | |
data-sugg-id="{{ sugg.id }}"> | |
<i class="icon-reject" dir="{% locale_dir %}" | |
title="{% trans 'Remove suggestion' %}"></i> | |
</a> | |
{% endif %} | |
{% endif %} | |
<div id="suggdiff-{{sugg.id}}-{{i}}" class="suggestion-translation" lang="{{ language.code }}" {% if title %} title="{{ title }}"{% endif %} | |
><span class="suggestion-translation-body">{{ diff }}</span></div> | |
</div> | |
{% block suggestion_comment %}{% endblock %} | |
</div> | |
{% endfor %} | |
</div> | |
{% endfor %} | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
</td> | |
{% endcache %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2009-2012 Zuza Software Foundation | |
# | |
# This file is part of Pootle. | |
# | |
# 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 2 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/>. | |
"""Form fields required for handling translation files.""" | |
import re | |
from django import forms | |
from django.utils import timezone | |
from django.utils.translation import get_language, ugettext as _ | |
from translate.misc.multistring import multistring | |
from pootle_app.models.permissions import check_permission | |
from pootle_statistics.models import (Submission, SubmissionFields, | |
SubmissionTypes) | |
from pootle_store.models import Unit | |
from pootle_store.util import UNTRANSLATED, FUZZY, TRANSLATED | |
from pootle_store.fields import PLURAL_PLACEHOLDER, to_db | |
############## text cleanup and highlighting ######################### | |
FORM_RE = re.compile('\r\n|\r|\n|\t|\\\\') | |
def highlight_whitespace(text): | |
"""Make whitespace chars visible.""" | |
def replace(match): | |
submap = { | |
'\r\n': '\\r\\n\n', | |
'\r': '\\r\n', | |
'\n': '\\n\n', | |
'\t': '\\t', | |
'\\': '\\\\', | |
} | |
return submap[match.group()] | |
return FORM_RE.sub(replace, text) | |
FORM_UNRE = re.compile('\r|\n|\t|\\\\r|\\\\n|\\\\t|\\\\\\\\') | |
def unhighlight_whitespace(text): | |
"""Replace visible whitespace with proper whitespace.""" | |
def replace(match): | |
submap = { | |
'\t': '', | |
'\n': '', | |
'\r': '', | |
'\\t': '\t', | |
'\\n': '\n', | |
'\\r': '\r', | |
'\\\\': '\\', | |
} | |
return submap[match.group()] | |
return FORM_UNRE.sub(replace, text) | |
class MultiStringWidget(forms.MultiWidget): | |
"""Custom Widget for editing multistrings, expands number of text | |
area based on number of plural forms.""" | |
def __init__(self, attrs=None, nplurals=1, textarea=True): | |
if textarea: | |
widget = forms.Textarea | |
else: | |
widget = forms.TextInput | |
widgets = [widget(attrs=attrs) for i in xrange(nplurals)] | |
super(MultiStringWidget, self).__init__(widgets, attrs) | |
def format_output(self, rendered_widgets): | |
from django.utils.safestring import mark_safe | |
if len(rendered_widgets) == 1: | |
return mark_safe(rendered_widgets[0]) | |
output = '' | |
for i, widget in enumerate(rendered_widgets): | |
output += '<div lang="%s" title="%s">' % \ | |
(get_language(), _('Plural Form %d', i)) | |
output += widget | |
output += '</div>' | |
return mark_safe(output) | |
def decompress(self, value): | |
if value is None: | |
return [None] * len(self.widgets) | |
elif isinstance(value, multistring): | |
return [highlight_whitespace(string) for string in value.strings] | |
elif isinstance(value, list): | |
return [highlight_whitespace(string) for string in value] | |
elif isinstance(value, basestring): | |
return [highlight_whitespace(value)] | |
else: | |
raise ValueError | |
class HiddenMultiStringWidget(MultiStringWidget): | |
"""Uses hidden input instead of textareas.""" | |
def __init__(self, attrs=None, nplurals=1): | |
widgets = [forms.HiddenInput(attrs=attrs) for i in xrange(nplurals)] | |
super(MultiStringWidget, self).__init__(widgets, attrs) | |
def format_output(self, rendered_widgets): | |
return super(MultiStringWidget, self).format_output(rendered_widgets) | |
def __call__(self): | |
#HACKISH: Django is inconsistent in how it handles | |
# Field.widget and Field.hidden_widget, it expects widget to | |
# be an instantiated object and hidden_widget to be a class, | |
# since we need to specify nplurals at run time we can let | |
# django instantiate hidden_widget. | |
# | |
# making the object callable let's us get away with forcing an | |
# object where django expects a class | |
return self | |
class MultiStringFormField(forms.MultiValueField): | |
def __init__(self, nplurals=1, attrs=None, textarea=True, *args, **kwargs): | |
self.widget = MultiStringWidget(nplurals=nplurals, attrs=attrs, | |
textarea=textarea) | |
self.hidden_widget = HiddenMultiStringWidget(nplurals=nplurals) | |
fields = [forms.CharField() for i in range(nplurals)] | |
super(MultiStringFormField, self).__init__(fields=fields, | |
*args, **kwargs) | |
def compress(self, data_list): | |
return [unhighlight_whitespace(string) for string in data_list] | |
class UnitStateField(forms.BooleanField): | |
def to_python(self, value): | |
"""Returns a Python boolean object. | |
:return: ``False`` for any unknown :cls:`~pootle_store.models.Unit` | |
states and for the 'False' string. | |
""" | |
if (value in ('False',) or | |
value not in (str(s) for s in (UNTRANSLATED, FUZZY, TRANSLATED))): | |
value = False | |
else: | |
value = bool(value) | |
value = super(forms.BooleanField, self).to_python(value) | |
if not value and self.required: | |
raise forms.ValidationError(self.error_messages['required']) | |
return value | |
def unit_form_factory(language, snplurals=None, request=None): | |
if snplurals is not None: | |
tnplurals = language.nplurals | |
else: | |
tnplurals = 1 | |
action_disabled = False | |
if request is not None: | |
cantranslate = check_permission("translate", request) | |
cansuggest = check_permission("suggest", request) | |
if not (cansuggest or cantranslate): | |
action_disabled = True | |
target_attrs = { | |
'lang': language.code, | |
'dir': language.direction, | |
'class': 'translation expanding focusthis', | |
'rows': 5, | |
'tabindex': 10, | |
} | |
fuzzy_attrs = { | |
'accesskey': 'f', | |
'class': 'fuzzycheck', | |
'tabindex': 13, | |
} | |
if action_disabled: | |
target_attrs['disabled'] = 'disabled' | |
fuzzy_attrs['disabled'] = 'disabled' | |
class UnitForm(forms.ModelForm): | |
class Meta: | |
model = Unit | |
exclude = ['store', 'developer_comment', 'translator_comment', 'submitted_by', 'commented_by', | |
'image','uploaded_by', 'image2'] | |
id = forms.IntegerField(required=False) | |
source_f = MultiStringFormField(nplurals=snplurals or 1, | |
required=False, textarea=False) | |
target_f = MultiStringFormField(nplurals=tnplurals, required=False, | |
attrs=target_attrs) | |
state = UnitStateField(required=False, label=_('Needs work'), | |
widget=forms.CheckboxInput( | |
attrs=fuzzy_attrs, | |
check_test=lambda x: x == FUZZY)) | |
def __init__(self, *args, **argv): | |
super(UnitForm, self).__init__(*args, **argv) | |
self.updated_fields = [] | |
def clean_source_f(self): | |
value = self.cleaned_data['source_f'] | |
if self.instance.source.strings != value: | |
self.instance._source_updated = True | |
self.updated_fields.append((SubmissionFields.SOURCE, | |
to_db(self.instance.source), | |
to_db(value))) | |
if snplurals == 1: | |
# plural with single form, insert placeholder | |
value.append(PLURAL_PLACEHOLDER) | |
return value | |
def clean_target_f(self): | |
value = self.cleaned_data['target_f'] | |
if self.instance.target.strings != multistring(value or [u'']): | |
self.instance._target_updated = True | |
self.updated_fields.append((SubmissionFields.TARGET, | |
to_db(self.instance.target), | |
to_db(value))) | |
return value | |
def clean_state(self): | |
old_state = self.instance.state # Integer | |
value = self.cleaned_data['state'] # Boolean | |
new_target = self.cleaned_data['target_f'] | |
new_state = None | |
if new_target: | |
if value: | |
new_state = FUZZY | |
else: | |
new_state = TRANSLATED | |
else: | |
new_state = UNTRANSLATED | |
if old_state != new_state: | |
self.instance._state_updated = True | |
self.updated_fields.append((SubmissionFields.STATE, | |
old_state, new_state)) | |
else: | |
self.instance._state_updated = False | |
return new_state | |
return UnitForm | |
def unit_comment_form_factory(language): | |
comment_attrs = { | |
'lang': language.code, | |
'dir': language.direction, | |
'class': 'comments expanding focusthis', | |
'rows': 2, | |
'tabindex': 15, | |
} | |
class UnitCommentForm(forms.ModelForm): | |
class Meta: | |
fields = ('translator_comment',) | |
model = Unit | |
translator_comment = forms.CharField(required=True, | |
label=_("Translator comment"), | |
widget=forms.Textarea( | |
attrs=comment_attrs)) | |
def __init__(self, *args, **kwargs): | |
self.request = kwargs.pop('request', None) | |
super(UnitCommentForm, self).__init__(*args, **kwargs) | |
def save(self): | |
"""Registers the submission and saves the comment.""" | |
if self.has_changed(): | |
creation_time = timezone.now() | |
translation_project = self.request.translation_project | |
sub = Submission( | |
creation_time=creation_time, | |
translation_project=translation_project, | |
submitter=self.request.profile, | |
unit=self.instance, | |
field=SubmissionFields.COMMENT, | |
type=SubmissionTypes.NORMAL, | |
old_value=u"", | |
new_value=self.cleaned_data['translator_comment'] | |
) | |
sub.save() | |
super(UnitCommentForm, self).save() | |
return UnitCommentForm | |
def unit_image_form_factory(language): | |
image_attrs = { | |
'lang': language.code, | |
'dir': language.direction, | |
'class': 'images expanding focusthis', | |
'rows': 2, | |
'tabindex': 15, | |
} | |
class UnitImageForm(forms.ModelForm): | |
class Meta: | |
fields = ('image',) | |
model = Unit | |
#image = forms.CharField(required=True, | |
# label=_("Image"), | |
# widget=forms.Textarea( | |
# attrs=image_attrs)) | |
image= forms.FileField(required=True, label=_('Image'), | |
widget=forms.FileInput( | |
attrs=image_attrs)) | |
def __init__(self, *args, **kwargs): | |
self.request = kwargs.pop('request', None) | |
super(UnitImageForm, self).__init__(*args, **kwargs) | |
def save(self): | |
"""Registers the submission and saves the image.""" | |
#if self.has_changed(): | |
# creation_time = timezone.now() | |
# translation_project = self.request.translation_project | |
#sub = Submission( | |
# creation_time=creation_time, | |
# translation_project=translation_project, | |
# submitter=self.request.profile, | |
# unit=self.instance, | |
# field=SubmissionFields.COMMENT, | |
# type=SubmissionTypes.NORMAL, | |
# old_value=u"", | |
# new_value=self.cleaned_data['image2'] | |
#) | |
#sub.save() | |
super(UnitImageForm, self).save() | |
return UnitImageForm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2008-2013 Zuza Software Foundation | |
# | |
# This file is part of Pootle. | |
# | |
# Pootle 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 2 of the License, or (at your option) any later | |
# version. | |
# | |
# Pootle 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 | |
# Pootle; if not, see <http://www.gnu.org/licenses/>. | |
import datetime | |
import logging | |
import os | |
import re | |
import time | |
from hashlib import md5 | |
from itertools import chain | |
from translate.filters.decorators import Category | |
from translate.storage import base | |
from django.conf import settings | |
from django.core.exceptions import ObjectDoesNotExist | |
from django.core.files.storage import FileSystemStorage | |
from django.core.urlresolvers import reverse | |
from django.db import models, DatabaseError, IntegrityError | |
from django.db.models.signals import post_delete, post_save, pre_delete | |
from django.db.transaction import commit_on_success | |
from django.utils import timezone, tzinfo | |
from django.utils.translation import ugettext_lazy as _ | |
from taggit.managers import TaggableManager | |
from pootle.core.managers import RelatedManager | |
from pootle.core.url_helpers import get_editor_filter, split_pootle_path | |
from pootle_misc.aggregate import group_by_count_extra, max_column | |
from pootle_misc.baseurl import l | |
from pootle_misc.checks import check_names | |
from pootle_misc.util import (cached_property, getfromcache, deletefromcache, | |
datetime_min) | |
from pootle_statistics.models import SubmissionFields, SubmissionTypes | |
from pootle_store.fields import (TranslationStoreField, MultiStringField, | |
PLURAL_PLACEHOLDER, SEPARATOR) | |
from pootle_store.filetypes import factory_classes, is_monolingual | |
from pootle_store.util import (calculate_stats, empty_quickstats, | |
OBSOLETE, UNTRANSLATED, FUZZY, TRANSLATED) | |
from pootle_tagging.models import ItemWithGoal | |
# | |
# Store States | |
# | |
# Store being modified | |
LOCKED = -1 | |
# Store just created, not parsed yet | |
NEW = 0 | |
# Store just parsed, units added but no quality checks were run | |
PARSED = 1 | |
# Quality checks run | |
CHECKED = 2 | |
############### Quality Check ############# | |
class QualityCheck(models.Model): | |
"""Database cache of results of qualitychecks on unit.""" | |
name = models.CharField(max_length=64, db_index=True) | |
unit = models.ForeignKey("pootle_store.Unit", db_index=True) | |
category = models.IntegerField(null=False, default=Category.NO_CATEGORY) | |
message = models.TextField() | |
false_positive = models.BooleanField(default=False, db_index=True) | |
objects = RelatedManager() | |
@property | |
def display_name(self): | |
return check_names.get(self.name, self.name) | |
def __unicode__(self): | |
return self.name | |
################# Suggestion ################ | |
class SuggestionManager(RelatedManager): | |
def get_by_natural_key(self, target_hash, unitid_hash, pootle_path): | |
return self.get(target_hash=target_hash, unit__unitid_hash=unitid_hash, | |
unit__store__pootle_path=pootle_path) | |
class Suggestion(models.Model, base.TranslationUnit): | |
"""Suggested translation for a :cls:`~pootle_store.models.Unit`, provided | |
by users or automatically generated after a merge. | |
""" | |
target_f = MultiStringField() | |
target_hash = models.CharField(max_length=32, db_index=True) | |
unit = models.ForeignKey('pootle_store.Unit') | |
user = models.ForeignKey('pootle_profile.PootleProfile', null=True) | |
translator_comment_f = models.TextField(null=True, blank=True) | |
#image = models.TextField(null=True, blank=True) | |
image = models.ImageField(upload_to='/home/grove/') | |
objects = SuggestionManager() | |
class Meta: | |
unique_together = ('unit', 'target_hash') | |
def natural_key(self): | |
return (self.target_hash, self.unit.unitid_hash, | |
self.unit.store.pootle_path) | |
natural_key.dependencies = ['pootle_store.Unit', 'pootle_store.Store'] | |
############################ Properties ################################### | |
@property | |
def _target(self): | |
return self.target_f | |
@_target.setter | |
def _target(self, value): | |
self.target_f = value | |
self._set_hash() | |
@property | |
def _source(self): | |
return self.unit._source | |
@property | |
def translator_comment(self): | |
return self.translator_comment_f | |
@translator_comment.setter | |
def translator_comment(self, value): | |
self.translator_comment_f = value | |
self._set_hash() | |
@property | |
def image(self): | |
return self.image | |
@image.setter | |
def image(self, value): | |
self.image = value | |
self._set_hash() | |
############################ Methods ###################################### | |
def __unicode__(self): | |
return unicode(self.target) | |
def _set_hash(self): | |
string = self.translator_comment_f | |
if string: | |
string = self.target_f + SEPARATOR + string | |
else: | |
string = self.target_f | |
self.target_hash = md5(string.encode("utf-8")).hexdigest() | |
################################ Signal handlers ############################## | |
def delete_votes(sender, instance, **kwargs): | |
# Since votes are linked by ContentType and not foreign keys, referential | |
# integrity is not kept, and we have to ensure we remove any votes manually | |
# when a suggestion is removed | |
from voting.models import Vote | |
from django.contrib.contenttypes.models import ContentType | |
ctype = ContentType.objects.get_for_model(instance) | |
Vote.objects.filter(content_type=ctype, | |
object_id=instance._get_pk_val()).delete() | |
post_delete.connect(delete_votes, sender=Suggestion) | |
############### Unit #################### | |
def fix_monolingual(oldunit, newunit, monolingual=None): | |
"""Hackish workaround for monolingual files always having only source and | |
no target. | |
We compare monolingual unit with corresponding bilingual unit, if sources | |
differ assume monolingual source is actually a translation. | |
""" | |
if monolingual is None: | |
monolingual = is_monolingual(type(newunit._store)) | |
if monolingual and newunit.source != oldunit.source: | |
newunit.target = newunit.source | |
newunit.source = oldunit.source | |
def count_words(strings): | |
from translate.storage import statsdb | |
wordcount = 0 | |
for string in strings: | |
wordcount += statsdb.wordcount(string) | |
return wordcount | |
def stringcount(string): | |
try: | |
return len(string.strings) | |
except AttributeError: | |
return 1 | |
class UnitManager(RelatedManager): | |
def get_by_natural_key(self, unitid_hash, pootle_path): | |
return self.get(unitid_hash=unitid_hash, | |
store__pootle_path=pootle_path) | |
def get_for_path(self, pootle_path, profile): | |
"""Returns units that fall below the `pootle_path` umbrella. | |
:param pootle_path: An internal pootle path. | |
:param profile: The user profile who is accessing the units. | |
""" | |
lang, proj, dir_path, filename = split_pootle_path(pootle_path) | |
units_qs = super(UnitManager, self).get_query_set().filter( | |
state__gt=OBSOLETE, | |
) | |
# /projects/<project_code>/translate/* | |
if lang is None and proj is not None: | |
units_qs = units_qs.extra( | |
where=[ | |
'`pootle_store_store`.`pootle_path` LIKE %s', | |
'`pootle_store_store`.`pootle_path` NOT LIKE %s', | |
], params=[''.join(['/%/', proj ,'/%']), '/templates/%'] | |
) | |
# /<lang_code>/<project_code>/translate/* | |
# /<lang_code>/translate/* | |
# /translate/* | |
else: | |
units_qs = units_qs.filter( | |
store__pootle_path__startswith=pootle_path, | |
) | |
return units_qs | |
class Unit(models.Model, base.TranslationUnit): | |
store = models.ForeignKey("pootle_store.Store", db_index=True) | |
index = models.IntegerField(db_index=True) | |
unitid = models.TextField(editable=False) | |
unitid_hash = models.CharField(max_length=32, db_index=True, | |
editable=False) | |
source_f = MultiStringField(null=True) | |
source_hash = models.CharField(max_length=32, db_index=True, | |
editable=False) | |
source_wordcount = models.SmallIntegerField(default=0, editable=False) | |
source_length = models.SmallIntegerField(db_index=True, default=0, | |
editable=False) | |
target_f = MultiStringField(null=True, blank=True) | |
target_wordcount = models.SmallIntegerField(default=0, editable=False) | |
target_length = models.SmallIntegerField(db_index=True, default=0, | |
editable=False) | |
developer_comment = models.TextField(null=True, blank=True) | |
translator_comment = models.TextField(null=True, blank=True) | |
# New field to add context images | |
#image = models.TextField(null=True, blank=True) | |
image = models.FileField(upload_to=".") | |
locations = models.TextField(null=True, editable=False) | |
context = models.TextField(null=True, editable=False) | |
state = models.IntegerField(null=False, default=UNTRANSLATED, db_index=True) | |
# New field to verify changes | |
oldValue = MultiStringField(null=True, blank=True) | |
# New field to flag changes | |
isInvalidated = models.SmallIntegerField(default=0, editable=False) | |
# Metadata | |
mtime = models.DateTimeField(auto_now=True, auto_now_add=True, | |
db_index=True, editable=False) | |
submitted_by = models.ForeignKey('pootle_profile.PootleProfile', null=True, | |
db_index=True, related_name='submitted') | |
submitted_on = models.DateTimeField(auto_now_add=True, db_index=True, | |
null=True) | |
commented_by = models.ForeignKey('pootle_profile.PootleProfile', null=True, | |
db_index=True, related_name='commented') | |
commented_on = models.DateTimeField(auto_now_add=True, db_index=True, | |
null=True) | |
# New fields for uploaded image | |
uploaded_by = models.ForeignKey('pootle_profile.PootleProfile', null=True, | |
db_index=True, related_name='uploaded') | |
uploaded_on = models.DateTimeField(auto_now_add=True, db_index=True, | |
null=True) | |
objects = UnitManager() | |
class Meta: | |
ordering = ['store', 'index'] | |
unique_together = ('store', 'unitid_hash') | |
get_latest_by = 'mtime' | |
def natural_key(self): | |
return (self.unitid_hash, self.store.pootle_path) | |
natural_key.dependencies = ['pootle_store.Store'] | |
############################ Properties ################################### | |
@property | |
def _source(self): | |
return self.source_f | |
@_source.setter | |
def _source(self, value): | |
self.source_f = value | |
self._source_updated = True | |
@property | |
def _target(self): | |
return self.target_f | |
@_target.setter | |
def _target(self, value): | |
self.target_f = value | |
self._target_updated = True | |
@property | |
def _isInvalidated(self): | |
return self.isInvalidated | |
@_isInvalidated.setter | |
def _isInvalidated(self, value): | |
self.isInvalidated = value | |
self._isInvalidated_updated = True | |
@property | |
def _oldValue(self): | |
return self.oldValue | |
@_oldValue.setter | |
def _oldValue(self, value): | |
self.oldValue = value | |
self._oldValue_updated = True | |
############################ Methods ###################################### | |
def __unicode__(self): | |
# FIXME: consider using unit id instead? | |
return unicode(self.source) | |
def __str__(self): | |
unitclass = self.get_unit_class() | |
return str(self.convert(unitclass)) | |
def __init__(self, *args, **kwargs): | |
super(Unit, self).__init__(*args, **kwargs) | |
self._rich_source = None | |
self._source_updated = False | |
self._rich_target = None | |
self._target_updated = False | |
self._encoding = 'UTF-8' | |
def save(self, *args, **kwargs): | |
if self._source_updated: | |
# update source related fields | |
self.source_hash = md5(self.source_f.encode("utf-8")).hexdigest() | |
self.source_wordcount = count_words(self.source_f.strings) | |
self.source_length = len(self.source_f) | |
if self._target_updated: | |
# update target related fields | |
self.target_wordcount = count_words(self.target_f.strings) | |
self.target_length = len(self.target_f) | |
if filter(None, self.target_f.strings): | |
if self.state == UNTRANSLATED: | |
self.state = TRANSLATED | |
elif self.state > FUZZY: | |
self.state = UNTRANSLATED | |
super(Unit, self).save(*args, **kwargs) | |
if (settings.AUTOSYNC and self.store.file and | |
self.store.state >= PARSED and | |
(self._target_updated or self._source_updated)): | |
#FIXME: last translator information is lost | |
self.sync(self.getorig()) | |
self.store.update_store_header() | |
self.store.file.savestore() | |
if (self.store.state >= CHECKED and | |
(self._source_updated or self._target_updated)): | |
#FIXME: are we sure only source and target affect quality checks? | |
self.update_qualitychecks() | |
# done processing source/target update remove flag | |
self._source_updated = False | |
self._target_updated = False | |
if self.store.state >= PARSED: | |
# updated caches | |
store = self.store | |
deletefromcache(store, ["getquickstats", "getcompletestats", | |
"get_mtime", "get_suggestion_count"]) | |
def get_absolute_url(self): | |
return l(self.store.pootle_path) | |
def get_translate_url(self): | |
lang, proj, dir, fn = split_pootle_path(self.store.pootle_path) | |
return u''.join([ | |
reverse('pootle-tp-translate', args=[lang, proj, dir, fn]), | |
'#unit=', unicode(self.id), | |
]) | |
def get_mtime(self): | |
return self.mtime | |
def convert(self, unitclass): | |
"""Convert to a unit of type :param:`unitclass` retaining as much | |
information from the database as the target format can support.""" | |
newunit = unitclass(self.source) | |
newunit.target = self.target | |
newunit.markfuzzy(self.isfuzzy()) | |
locations = self.getlocations() | |
if locations: | |
newunit.addlocations(locations) | |
notes = self.getnotes(origin="developer") | |
if notes: | |
newunit.addnote(notes, origin="developer") | |
notes = self.getnotes(origin="translator") | |
if notes: | |
newunit.addnote(notes, origin="translator") | |
newunit.setid(self.getid()) | |
newunit.setcontext(self.getcontext()) | |
if hasattr(newunit, "addalttrans"): | |
for suggestion in self.get_suggestions().iterator(): | |
newunit.addalttrans(suggestion.target, | |
origin=unicode(suggestion.user)) | |
if self.isobsolete(): | |
newunit.makeobsolete() | |
return newunit | |
def get_unit_class(self): | |
try: | |
return self.store.get_file_class().UnitClass | |
except ObjectDoesNotExist: | |
from translate.storage import po | |
return po.pounit | |
def getorig(self): | |
unit = self.store.file.store.units[self.index] | |
if self.getid() == unit.getid(): | |
return unit | |
# FIXME: if we are here, file changed structure and we need to update | |
# indexes | |
logging.debug(u"Incorrect unit index %d for %s in file %s", | |
unit.index, unit, unit.store.file) | |
self.store.file.store.require_index() | |
unit = self.store.file.store.findid(self.getid()) | |
return unit | |
def sync(self, unit): | |
"""Sync in file unit with translations from the DB.""" | |
changed = False | |
if not self.isobsolete() and unit.isobsolete(): | |
unit.resurrect() | |
changed = True | |
if unit.target != self.target: | |
if unit.hasplural(): | |
nplurals = self.store.translation_project.language.nplurals | |
target_plurals = len(self.target.strings) | |
strings = self.target.strings | |
if target_plurals < nplurals: | |
strings.extend([u'']*(nplurals - target_plurals)) | |
if unit.target.strings != strings: | |
unit.target = strings | |
changed = True | |
else: | |
unit.target = self.target | |
changed = True | |
self_notes = self.getnotes(origin="translator") | |
if unit.getnotes(origin="translator") != self_notes or '': | |
unit.addnote(self_notes, origin="translator", position="replace") | |
changed = True | |
if unit.isfuzzy() != self.isfuzzy(): | |
unit.markfuzzy(self.isfuzzy()) | |
changed = True | |
if hasattr(unit, 'addalttrans') and self.get_suggestions().count(): | |
alttranslist = [alttrans.target for alttrans in unit.getalttrans()] | |
for suggestion in self.get_suggestions().iterator(): | |
if suggestion.target in alttranslist: | |
# don't add duplicate suggestion | |
continue | |
unit.addalttrans(suggestion.target, unicode(suggestion.user)) | |
changed = True | |
if self.isobsolete() and not unit.isobsolete(): | |
unit.makeobsolete() | |
changed = True | |
return changed | |
def update(self, unit): | |
"""Update in-DB translation from the given :param:`unit`. | |
:rtype: bool | |
:return: True if the new :param:`unit` differs from the current unit. | |
Two units differ when any of the fields differ (source, target, | |
translator/developer comments, locations, context, status...). | |
""" | |
changed = False | |
if (self.source != unit.source or | |
len(self.source.strings) != stringcount(unit.source) or | |
self.hasplural() != unit.hasplural()): | |
if unit.hasplural() and len(unit.source.strings) == 1: | |
self.source = [unit.source, PLURAL_PLACEHOLDER] | |
else: | |
self.source = unit.source | |
changed = True | |
if (self.target != unit.target or | |
len(self.target.strings) != stringcount(unit.target)): | |
notempty = filter(None, self.target_f.strings) | |
self.target = unit.target | |
if filter(None, self.target_f.strings) or notempty: | |
#FIXME: we need to do this cause we discard nplurals | |
# for empty plurals | |
changed = True | |
notes = unit.getnotes(origin="developer") | |
if (self.developer_comment != notes and | |
(self.developer_comment or notes)): | |
self.developer_comment = notes or None | |
changed = True | |
notes = unit.getnotes(origin="translator") | |
if (self.translator_comment != notes and | |
(self.translator_comment or notes)): | |
self.translator_comment = notes or None | |
changed = True | |
locations = "\n".join(unit.getlocations()) | |
if self.locations != locations and (self.locations or locations): | |
self.locations = locations or None | |
changed = True | |
context = unit.getcontext() | |
if self.context != unit.getcontext() and (self.context or context): | |
self.context = context or None | |
changed = True | |
if self.isfuzzy() != unit.isfuzzy(): | |
self.markfuzzy(unit.isfuzzy()) | |
changed = True | |
if self.isobsolete() != unit.isobsolete(): | |
if unit.isobsolete(): | |
self.makeobsolete() | |
else: | |
self.resurrect() | |
changed = True | |
if self.unitid != unit.getid(): | |
self.unitid = unicode(unit.getid()) or unicode(unit.source) | |
self.unitid_hash = md5(self.unitid.encode("utf-8")).hexdigest() | |
changed = True | |
if hasattr(unit, 'getalttrans'): | |
for suggestion in unit.getalttrans(): | |
if suggestion.source == self.source: | |
self.add_suggestion(suggestion.target, touch=False) | |
changed = True | |
return changed | |
def update_qualitychecks(self, created=False, keep_false_positives=False): | |
"""Run quality checks and store result in the database.""" | |
existing = [] | |
if not created: | |
checks = self.qualitycheck_set.all() | |
if keep_false_positives: | |
existing = set(checks.filter(false_positive=True) \ | |
.values_list('name', flat=True)) | |
checks = checks.filter(false_positive=False) | |
checks.delete() | |
if not self.target: | |
return | |
qc_failures = self.store.translation_project.checker \ | |
.run_filters(self, categorised=True) | |
for name in qc_failures.iterkeys(): | |
if name == 'isfuzzy' or name in existing: | |
continue | |
message = qc_failures[name]['message'] | |
category = qc_failures[name]['category'] | |
self.qualitycheck_set.create(name=name, message=message, | |
category=category) | |
def get_qualitychecks(self): | |
return self.qualitycheck_set.filter(false_positive=False) | |
# FIXME: This is a hackish implementation needed due to the underlying | |
# lame model definitions | |
def get_reviewer(self): | |
"""Retrieve reviewer information for the current unit. | |
:return: In case the current unit's status is an effect of accepting a | |
suggestion, the reviewer profile is returned. | |
Otherwise, returns ``None``, indicating that the current unit's | |
status is an effect of any other actions. | |
""" | |
if self.submission_set.count(): | |
# Find the latest submission changing either the target or the | |
# unit's state and return the reviewer attached to it in case the | |
# submission type was accepting a suggestion | |
last_submission = self.submission_set.filter( | |
field__in=[SubmissionFields.TARGET, SubmissionFields.STATE] | |
).latest() | |
if last_submission.type == SubmissionTypes.SUGG_ACCEPT: | |
return getattr(last_submission.from_suggestion, 'reviewer', | |
None) | |
return None | |
################# TranslationUnit ############################ | |
def getnotes(self, origin=None): | |
if origin is None: | |
notes = '' | |
if self.translator_comment is not None: | |
notes += self.translator_comment | |
if self.developer_comment is not None: | |
notes += self.developer_comment | |
return notes | |
elif origin == "translator": | |
return self.translator_comment or '' | |
elif origin in ["programmer", "developer", "source code"]: | |
return self.developer_comment or '' | |
else: | |
raise ValueError("Comment type not valid") | |
def getimages(self): | |
images = '' | |
if self.image is not None: | |
images += self.image | |
return images | |
def addnote(self, text, origin=None, position="append"): | |
if not (text and text.strip()): | |
return | |
if origin in ["programmer", "developer", "source code"]: | |
self.developer_comment = text | |
else: | |
self.translator_comment = text | |
def addimage(self, text, position="append"): | |
if not (text and text.strip()): | |
return | |
self.image = text | |
def getid(self): | |
return self.unitid | |
def setid(self, value): | |
self.unitid = value | |
self.unitid_hash = md5(self.unitid.encode("utf-8")).hexdigest() | |
def getlocations(self): | |
if self.locations is None: | |
return [] | |
return filter(None, self.locations.split('\n')) | |
def addlocation(self, location): | |
if self.locations is None: | |
self.locations = '' | |
self.locations += location + "\n" | |
def getcontext(self): | |
return self.context | |
def setcontext(self, value): | |
self.context = value | |
def isfuzzy(self): | |
return self.state == FUZZY | |
def markfuzzy(self, value=True): | |
if self.state <= OBSOLETE: | |
return | |
if value: | |
self.state = FUZZY | |
elif self.state <= FUZZY: | |
if filter(None, self.target_f.strings): | |
self.state = TRANSLATED | |
else: | |
self.state = UNTRANSLATED | |
def hasplural(self): | |
return (self.source is not None and | |
(len(self.source.strings) > 1 | |
or hasattr(self.source, "plural") and | |
self.source.plural)) | |
def isobsolete(self): | |
return self.state == OBSOLETE | |
def makeobsolete(self): | |
if self.state > OBSOLETE: | |
self.state = OBSOLETE | |
def resurrect(self): | |
if self.state > OBSOLETE: | |
return | |
if filter(None, self.target_f.strings): | |
self.state = TRANSLATED | |
else: | |
self.state = UNTRANSLATED | |
def istranslated(self): | |
if self._target_updated and not self.isfuzzy(): | |
return bool(filter(None, self.target_f.strings)) | |
return self.state >= TRANSLATED | |
@classmethod | |
def buildfromunit(cls, unit): | |
newunit = cls() | |
newunit.update(unit) | |
return newunit | |
def addalttrans(self, txt, origin=None): | |
self.add_suggestion(txt, user=origin) | |
def getalttrans(self): | |
return self.get_suggestions() | |
def delalttrans(self, alternative): | |
alternative.delete() | |
def fuzzy_translate(self, matcher): | |
candidates = matcher.matches(self.source) | |
if candidates: | |
match_unit = candidates[0] | |
changed = self.merge(match_unit, authoritative=True) | |
if changed: | |
return match_unit | |
def merge(self, merge_unit, overwrite=False, comments=True, | |
authoritative=False, images=True): | |
"""Merges :param:`merge_unit` with the current unit. | |
:param merge_unit: The unit that will be merged into the current unit. | |
:param overwrite: Whether to replace the existing translation or not. | |
:param comments: Whether to merge translator comments or not. | |
:param images: Whether to merge images or not. | |
:param authoritative: Not used. Kept for Toolkit API consistenty. | |
:return: True if the current unit has been changed. | |
""" | |
changed = False | |
if comments: | |
notes = merge_unit.getnotes(origin="translator") | |
if notes and self.translator_comment != notes: | |
self.translator_comment = notes | |
changed = True | |
if images: | |
images = merge_unit.getimages() | |
if images and self.image != images: | |
self.image = images | |
changed = True | |
# No translation in merge_unit: bail out | |
if not bool(merge_unit.target): | |
return changed | |
# Won't replace existing translation unless overwrite is True | |
if bool(self.target) and not overwrite: | |
return changed | |
# Current translation more trusted | |
if self.istranslated() and not merge_unit.istranslated(): | |
return changed | |
if self.target != merge_unit.target: | |
self.target = merge_unit.target | |
if self.source != merge_unit.source: | |
self.markfuzzy() | |
else: | |
self.markfuzzy(merge_unit.isfuzzy()) | |
changed = True | |
elif self.isfuzzy() != merge_unit.isfuzzy(): | |
self.markfuzzy(merge_unit.isfuzzy()) | |
changed = True | |
return changed | |
################# Suggestions ################################# | |
def get_suggestions(self): | |
return self.suggestion_set.select_related('user').all() | |
def add_suggestion(self, translation, user=None, touch=True): | |
if not filter(None, translation): | |
return None | |
if translation == self.target: | |
return None | |
suggestion = Suggestion(unit=self, user=user) | |
suggestion.target = translation | |
try: | |
suggestion.save() | |
if touch: | |
self.save() | |
except: | |
# probably duplicate suggestion | |
return None | |
return suggestion | |
def accept_suggestion(self, suggid): | |
try: | |
suggestion = self.suggestion_set.get(id=suggid) | |
except Suggestion.DoesNotExist: | |
return False | |
self.target = suggestion.target | |
self.state = TRANSLATED | |
self.submitted_by = suggestion.user | |
self.submitted_on = timezone.now() | |
# It is important to first delete the suggestion before calling | |
# ``save``, otherwise the quality checks won't be properly updated | |
# when saving the unit. | |
suggestion.delete() | |
self.save() | |
if settings.AUTOSYNC and self.file: | |
#FIXME: update alttrans | |
self.sync(self.getorig()) | |
self.store.update_store_header(profile=suggestion.user) | |
self.file.savestore() | |
return True | |
def reject_suggestion(self, suggid): | |
try: | |
suggestion = self.suggestion_set.get(id=suggid) | |
except Suggestion.DoesNotExist: | |
return False | |
suggestion.delete() | |
# Update timestamp | |
self.save() | |
return True | |
def get_terminology(self): | |
"""get terminology suggestions""" | |
matcher = self.store.translation_project.gettermmatcher() | |
if matcher is not None: | |
result = matcher.matches(self.source) | |
else: | |
result = [] | |
return result | |
###################### Store ########################### | |
# custom storage otherwise djago assumes all files are uploads headed to | |
# media dir | |
fs = FileSystemStorage(location=settings.PODIRECTORY) | |
# regexp to parse suggester name from msgidcomment | |
suggester_regexp = re.compile(r'suggested by (.*) \[[-0-9]+\]') | |
class StoreManager(RelatedManager): | |
def get_by_natural_key(self, pootle_path): | |
return self.get(pootle_path=pootle_path) | |
class Store(models.Model, base.TranslationStore): | |
"""A model representing a translation store (i.e. a PO or XLIFF file).""" | |
file = TranslationStoreField(upload_to="fish", max_length=255, storage=fs, | |
db_index=True, null=False, editable=False) | |
# Deprecated | |
pending = TranslationStoreField(ignore='.pending', upload_to="fish", | |
max_length=255, storage=fs, editable=False) | |
tm = TranslationStoreField(ignore='.tm', upload_to="fish", max_length=255, | |
storage=fs, editable=False) | |
parent = models.ForeignKey('pootle_app.Directory', | |
related_name='child_stores', db_index=True, editable=False) | |
translation_project_fk = 'pootle_translationproject.TranslationProject' | |
translation_project = models.ForeignKey(translation_project_fk, | |
related_name='stores', db_index=True, editable=False) | |
pootle_path = models.CharField(max_length=255, null=False, unique=True, | |
db_index=True, verbose_name=_("Path")) | |
name = models.CharField(max_length=128, null=False, editable=False) | |
sync_time = models.DateTimeField(default=datetime_min) | |
state = models.IntegerField(null=False, default=NEW, editable=False, | |
db_index=True) | |
tags = TaggableManager(blank=True, verbose_name=_("Tags"), | |
help_text=_("A comma-separated list of tags.")) | |
goals = TaggableManager(blank=True, verbose_name=_("Goals"), | |
through=ItemWithGoal, | |
help_text=_("A comma-separated list of goals.")) | |
UnitClass = Unit | |
Name = "Model Store" | |
is_dir = False | |
objects = StoreManager() | |
class Meta: | |
ordering = ['pootle_path'] | |
unique_together = ('parent', 'name') | |
def natural_key(self): | |
return (self.pootle_path,) | |
natural_key.dependencies = ['pootle_app.Directory'] | |
############################ Properties ################################### | |
@property | |
def tag_like_objects(self): | |
"""Return the tag like objects applied to this store. | |
Tag like objects can be either tags or goals. | |
""" | |
return list(chain(self.tags.all().order_by("name"), | |
self.goals.all().order_by("name"))) | |
@property | |
def abs_real_path(self): | |
if self.file: | |
return self.file.path | |
@property | |
def real_path(self): | |
return self.file.name | |
@property | |
def is_terminology(self): | |
"""Is this a project specific terminology store?""" | |
#TODO: Consider if this should check if the store belongs to a | |
# terminology project. Probably not, in case this might be called over | |
# several files in a project. | |
return self.name.startswith('pootle-terminology') | |
@property | |
def units(self): | |
if hasattr(self, '_units'): | |
return self._units | |
self.require_units() | |
return self.unit_set.filter(state__gt=OBSOLETE).order_by('index') \ | |
.select_related('store__translation_project') | |
@units.setter | |
def units(self, value): | |
"""Null setter to avoid tracebacks if :meth:`TranslationStore.__init__` | |
is called. | |
""" | |
pass | |
############################ Cached properties ############################ | |
@cached_property | |
def path(self): | |
"""Returns just the path part omitting language and project codes. | |
If the `pootle_path` of a :cls:`Store` object `store` is | |
`/af/project/dir1/dir2/file.po`, `store.path` will return | |
`dir1/dir2/file.po`. | |
""" | |
return u'/'.join(self.pootle_path.split(u'/')[3:]) | |
############################ Methods ###################################### | |
@classmethod | |
def _get_mtime_from_header(cls, store): | |
mtime = None | |
from translate.storage import poheader | |
if isinstance(store, poheader.poheader): | |
try: | |
_mtime = store.parseheader().get('X-POOTLE-MTIME', None) | |
if _mtime: | |
mtime = datetime.datetime.fromtimestamp(float(_mtime)) | |
if settings.USE_TZ: | |
# Africa/Johanesburg - pre-2.1 default | |
tz = tzinfo.FixedOffset(120) | |
mtime = timezone.make_aware(mtime, tz) | |
else: | |
mtime -= datetime.timedelta(hours=2) | |
except Exception as e: | |
logging.debug("failed to parse mtime: %s", e) | |
return mtime | |
def __unicode__(self): | |
return unicode(self.pootle_path) | |
def __str__(self): | |
storeclass = self.get_file_class() | |
store = self.convert(storeclass) | |
return str(store) | |
def save(self, *args, **kwargs): | |
self.pootle_path = self.parent.pootle_path + self.name | |
super(Store, self).save(*args, **kwargs) | |
if hasattr(self, '_units'): | |
index = self.max_index() + 1 | |
for i, unit in enumerate(self._units): | |
unit.store = self | |
unit.index = index + i | |
unit.save() | |
if self.state >= PARSED: | |
# new units, let's flush cache | |
deletefromcache(self, ["getquickstats", "getcompletestats", | |
"get_mtime", "get_suggestion_count"]) | |
def delete(self, *args, **kwargs): | |
super(Store, self).delete(*args, **kwargs) | |
deletefromcache(self, ["getquickstats", "getcompletestats", | |
"get_mtime", "get_suggestion_count"]) | |
def get_absolute_url(self): | |
return l(self.pootle_path) | |
def get_translate_url(self, **kwargs): | |
lang, proj, dir, fn = split_pootle_path(self.pootle_path) | |
return u''.join([ | |
reverse('pootle-tp-translate', args=[lang, proj, dir, fn]), | |
get_editor_filter(**kwargs), | |
]) | |
@getfromcache | |
def get_mtime(self): | |
return max_column(self.unit_set.all(), 'mtime', datetime_min) | |
def require_units(self): | |
"""Make sure file is parsed and units are created.""" | |
if self.state < PARSED and self.unit_set.count() == 0: | |
if (self.file and is_monolingual(type(self.file.store)) and | |
not self.translation_project.is_template_project): | |
self.translation_project \ | |
.update_against_templates(pootle_path=self.pootle_path) | |
else: | |
self.parse() | |
def require_dbid_index(self, update=False, obsolete=False): | |
"""build a quick mapping index between unit ids and database ids""" | |
if update or not hasattr(self, "dbid_index"): | |
units = self.unit_set.all() | |
if not obsolete: | |
units = units.filter(state__gt=OBSOLETE) | |
self.dbid_index = dict(units.values_list('unitid', 'id')) | |
def findid_bulk(self, ids): | |
chunks = 200 | |
for i in xrange(0, len(ids), chunks): | |
units = self.unit_set.filter(id__in=ids[i:i+chunks]) | |
for unit in units.iterator(): | |
yield unit | |
def get_matcher(self): | |
"""builds a TM matcher from current translations and obsolete units""" | |
from translate.search import match | |
#FIXME: should we cache this? | |
matcher = match.matcher( | |
self, | |
max_candidates=1, | |
max_length=settings.FUZZY_MATCH_MAX_LENGTH, | |
min_similarity=settings.FUZZY_MATCH_MIN_SIMILARITY, | |
usefuzzy=True | |
) | |
matcher.extendtm(self.unit_set.filter(state=OBSOLETE)) | |
matcher.addpercentage = False | |
return matcher | |
def clean_stale_lock(self): | |
if self.state != LOCKED: | |
return | |
mtime = max_column(self.unit_set.all(), 'mtime', None) | |
if mtime is None: | |
#FIXME: we can't tell stale locks if store has no units at all | |
return | |
delta = timezone.now() - mtime | |
if delta.days or delta.seconds > 2 * 60 * 60: | |
logging.warning("Found stale lock in %s, something went wrong " | |
"with a previous operation on the store", | |
self.pootle_path) | |
# lock been around for too long, assume it is stale | |
if QualityCheck.objects.filter(unit__store=self).exists(): | |
# there are quality checks, assume we are checked | |
self.state = CHECKED | |
else: | |
# there are units assumed we are parsed | |
self.state = PARSED | |
return True | |
return False | |
@commit_on_success | |
def parse(self, store=None): | |
self.clean_stale_lock() | |
if self.state == LOCKED: | |
# File currently being updated | |
# FIXME: shall we idle wait for lock to be released first? what | |
# about stale locks? | |
logging.info(u"Attemped to update %s while locked", | |
self.pootle_path) | |
return | |
if store is None: | |
store = self.file.store | |
if self.state < PARSED: | |
logging.debug(u"Parsing %s", self.pootle_path) | |
# no existing units in db, file hasn't been parsed before | |
# no point in merging, add units directly | |
old_state = self.state | |
self.state = LOCKED | |
self.save() | |
try: | |
for index, unit in enumerate(store.units): | |
if unit.istranslatable(): | |
try: | |
self.addunit(unit, index) | |
except IntegrityError as e: | |
logging.warning(u'Data integrity error while ' | |
u'importing unit %s:\n%s', | |
unit.getid(), e) | |
except: | |
# Something broke, delete any units that got created | |
# and return store state to its original value | |
self.unit_set.all().delete() | |
self.state = old_state | |
self.save() | |
raise | |
self.state = PARSED | |
self.sync_time = self.get_mtime() | |
self.save() | |
return | |
def _remove_obsolete(self, source): | |
"""Removes an obsolete unit from the DB. This will usually be used | |
after fuzzy matching. | |
""" | |
obsolete_unit = self.findunit(source, obsolete=True) | |
if obsolete_unit: | |
obsolete_unit.delete() | |
@commit_on_success | |
def update(self, update_structure=False, update_translation=False, | |
store=None, fuzzy=False, only_newer=False, modified_since=0,): | |
"""Update DB with units from file. | |
:param update_structure: Whether to update store's structure by marking | |
common DB units as obsolete and adding new units. | |
:param update_translation: Whether to update existing translations or | |
not. | |
:param store: The target :class:`~pootle_store.models.Store`. If unset, | |
the current file will be used as a target. | |
:param fuzzy: Whether to perform fuzzy matching or not. | |
:param only_newer: Whether to update only the files that changed on | |
disk after the last sync. | |
:param modified_since: Don't update translations that have been | |
modified since the given change ID. | |
""" | |
self.clean_stale_lock() | |
if self.state == LOCKED: | |
# File currently being updated | |
# FIXME: Shall we idle wait for lock to be released first? | |
# What about stale locks? | |
logging.info(u"Attempted to update %s while locked", | |
self.pootle_path) | |
return | |
elif self.state < PARSED: | |
# File has not been parsed before | |
logging.debug(u"Attempted to update unparsed file %s", | |
self.pootle_path) | |
self.parse(store=store) | |
return | |
if only_newer: | |
disk_mtime = datetime.datetime \ | |
.fromtimestamp(self.file.getpomtime()[0]) | |
if settings.USE_TZ: | |
tz = timezone.get_default_timezone() | |
disk_mtime = timezone.make_aware(disk_mtime, tz) | |
if disk_mtime <= self.sync_time: | |
# The file on disk wasn't changed since the last sync | |
logging.debug(u"File didn't change since last sync, skipping " | |
u"%s", self.pootle_path) | |
return | |
if store is None: | |
store = self.file.store | |
# Lock store | |
logging.debug(u"Updating %s", self.pootle_path) | |
old_state = self.state | |
self.state = LOCKED | |
self.save() | |
try: | |
if fuzzy: | |
matcher = self.get_matcher() | |
monolingual = is_monolingual(type(store)) | |
# Force a rebuild of the unit ID <-> DB ID index and get IDs for | |
# in-DB (old) and on-disk (new) stores | |
self.require_dbid_index(update=True, obsolete=True) | |
old_ids = set(self.dbid_index.keys()) | |
new_ids = set(store.getids()) | |
if update_structure: | |
# Remove old units or make them obsolete if they were already | |
# translated | |
obsolete_dbids = [self.dbid_index.get(uid) | |
for uid in old_ids - new_ids] | |
for unit in self.findid_bulk(obsolete_dbids): | |
if unit.istranslated(): | |
unit.makeobsolete() | |
unit.save() | |
else: | |
unit.delete() | |
# Add new units to the store | |
new_units = (store.findid(uid) for uid in new_ids - old_ids) | |
for unit in new_units: | |
newunit = self.addunit(unit, unit.index) | |
# Fuzzy match non-empty target strings | |
if fuzzy and not filter(None, newunit.target.strings): | |
match_unit = newunit.fuzzy_translate(matcher) | |
if match_unit: | |
newunit.save() | |
self._remove_obsolete(match_unit.source) | |
# Update quality checks for the new unit in case they were | |
# calculated for the store before | |
if old_state >= CHECKED: | |
newunit.update_qualitychecks(created=True) | |
if update_translation or modified_since: | |
modified_units = set() | |
if modified_since: | |
from pootle_statistics.models import Submission | |
self_unit_ids = set(self.dbid_index.values()) | |
try: | |
modified_units = set(Submission.objects.filter( | |
id__gt=modified_since, | |
unit__id__in=self_unit_ids, | |
).values_list('unit', flat=True).distinct()) | |
except DatabaseError as e: | |
# SQLite might barf with the IN operator over too many | |
# values | |
modified_units = set(Submission.objects.filter( | |
id__gt=modified_since, | |
).values_list('unit', flat=True).distinct()) | |
modified_units &= self_unit_ids | |
common_dbids = set(self.dbid_index.get(uid) \ | |
for uid in old_ids & new_ids) | |
# If some units have been modified since a given change ID, | |
# keep them safe and avoid overwrites | |
if modified_units: | |
common_dbids -= modified_units | |
common_dbids = list(common_dbids) | |
for unit in self.findid_bulk(common_dbids): | |
newunit = store.findid(unit.getid()) | |
if (monolingual and not | |
self.translation_project.is_template_project): | |
fix_monolingual(unit, newunit, monolingual) | |
changed = unit.update(newunit) | |
# Unit's index within the store might have changed | |
if update_structure and unit.index != newunit.index: | |
unit.index = newunit.index | |
changed = True | |
# Fuzzy match non-empty target strings | |
if fuzzy and not filter(None, unit.target.strings): | |
match_unit = unit.fuzzy_translate(matcher) | |
if match_unit: | |
changed = True | |
self._remove_obsolete(match_unit.source) | |
if changed: | |
do_checks = unit._source_updated or unit._target_updated | |
unit.save() | |
if do_checks and old_state >= CHECKED: | |
unit.update_qualitychecks() | |
finally: | |
# Unlock store | |
self.state = old_state | |
if (update_structure and | |
(update_translation or modified_since)): | |
self.sync_time = timezone.now() | |
self.save() | |
def require_qualitychecks(self): | |
"""make sure quality checks are run""" | |
if self.state < CHECKED: | |
self.update_qualitychecks() | |
# new qualitychecks, let's flush cache | |
deletefromcache(self, ["getcompletestats"]) | |
@commit_on_success | |
def update_qualitychecks(self): | |
logging.debug(u"Updating quality checks for %s", self.pootle_path) | |
for unit in self.units.iterator(): | |
unit.update_qualitychecks() | |
if self.state < CHECKED: | |
self.state = CHECKED | |
self.save() | |
def sync(self, update_structure=False, update_translation=False, | |
conservative=True, create=False, profile=None, skip_missing=False, | |
modified_since=0): | |
"""Sync file with translations from DB.""" | |
if skip_missing and not self.file.exists(): | |
return | |
if (not modified_since and conservative and | |
self.sync_time >= self.get_mtime()): | |
return | |
if not self.file: | |
if create: | |
# File doesn't exist let's create it | |
logging.debug(u"Creating file %s", self.pootle_path) | |
storeclass = self.get_file_class() | |
store_path = os.path.join( | |
self.translation_project.abs_real_path, self.name | |
) | |
store = self.convert(storeclass) | |
store.savefile(store_path) | |
self.file = store_path | |
self.update_store_header(profile=profile) | |
self.file.savestore() | |
self.sync_time = self.get_mtime() | |
self.save() | |
return | |
if conservative and self.translation_project.is_template_project: | |
# don't save to templates | |
return | |
logging.debug(u"Syncing %s", self.pootle_path) | |
self.require_dbid_index(update=True) | |
disk_store = self.file.store | |
old_ids = set(disk_store.getids()) | |
new_ids = set(self.dbid_index.keys()) | |
file_changed = False | |
if update_structure: | |
obsolete_units = (disk_store.findid(uid) \ | |
for uid in old_ids - new_ids) | |
for unit in obsolete_units: | |
if not unit.istranslated(): | |
del unit | |
elif not conservative: | |
unit.makeobsolete() | |
if not unit.isobsolete(): | |
del unit | |
file_changed = True | |
new_dbids = [self.dbid_index.get(uid) for uid in new_ids - old_ids] | |
for unit in self.findid_bulk(new_dbids): | |
newunit = unit.convert(disk_store.UnitClass) | |
disk_store.addunit(newunit) | |
file_changed = True | |
monolingual = is_monolingual(type(disk_store)) | |
if update_translation: | |
modified_units = set() | |
if modified_since: | |
from pootle_statistics.models import Submission | |
self_unit_ids = set(self.dbid_index.values()) | |
try: | |
modified_units = set(Submission.objects.filter( | |
id__gt=modified_since, | |
unit__id__in=self_unit_ids, | |
).values_list('unit', flat=True).distinct()) | |
except DatabaseError as e: | |
# SQLite might barf with the IN operator over too many | |
# values | |
modified_units = set(Submission.objects.filter( | |
id__gt=modified_since, | |
).values_list('unit', flat=True).distinct()) | |
modified_units &= self_unit_ids | |
common_dbids = set(self.dbid_index.get(uid) \ | |
for uid in old_ids & new_ids) | |
if modified_units: | |
common_dbids &= modified_units | |
common_dbids = list(common_dbids) | |
for unit in self.findid_bulk(common_dbids): | |
# FIXME: use a better mechanism for handling states and | |
# different formats | |
if monolingual and not unit.istranslated(): | |
continue | |
match = disk_store.findid(unit.getid()) | |
if match is not None: | |
changed = unit.sync(match) | |
if changed: | |
file_changed = True | |
if file_changed: | |
self.update_store_header(profile=profile) | |
self.file.savestore() | |
self.sync_time = timezone.now() | |
self.save() | |
def get_file_class(self): | |
try: | |
return self.translation_project.project.get_file_class() | |
except ObjectDoesNotExist: | |
if self.name: | |
name, ext = os.path.splitext(self.name) | |
return factory_classes[ext] | |
return factory_classes['po'] | |
def convert(self, fileclass): | |
"""export to fileclass""" | |
logging.debug(u"Converting %s to %s", self.pootle_path, fileclass) | |
output = fileclass() | |
try: | |
output.settargetlanguage(self.translation_project.language.code) | |
except ObjectDoesNotExist: | |
pass | |
#FIXME: we should add some headers | |
for unit in self.units.iterator(): | |
output.addunit(unit.convert(output.UnitClass)) | |
return output | |
#################### TranslationStore ######################### | |
suggestions_in_format = True | |
def max_index(self): | |
"""Largest unit index""" | |
return max_column(self.unit_set.all(), 'index', -1) | |
def addunit(self, unit, index=None): | |
if index is None: | |
index = self.max_index() + 1 | |
newunit = self.UnitClass(store=self, index=index) | |
newunit.update(unit) | |
if self.id: | |
newunit.save() | |
else: | |
# We can't save the unit if the store is not in the | |
# database already, so let's keep it in temporary list | |
if not hasattr(self, '_units'): | |
class FakeQuerySet(list): | |
def iterator(self): | |
return self.__iter__() | |
self._units = FakeQuerySet() | |
self._units.append(newunit) | |
return newunit | |
def findunits(self, source, obsolete=False): | |
if not obsolete and hasattr(self, "sourceindex"): | |
return super(Store, self).findunits(source) | |
# find using hash instead of index | |
source_hash = md5(source.encode("utf-8")).hexdigest() | |
units = self.unit_set.filter(source_hash=source_hash) | |
if obsolete: | |
units = units.filter(state=OBSOLETE) | |
else: | |
units = units.filter(state__gt=OBSOLETE) | |
if units.count(): | |
return units | |
def findunit(self, source, obsolete=False): | |
units = self.findunits(source, obsolete) | |
if units: | |
return units[0] | |
def findid(self, id): | |
if hasattr(self, "id_index"): | |
return self.id_index.get(id, None) | |
unitid_hash = md5(id.encode("utf-8")).hexdigest() | |
try: | |
return self.units.get(unitid_hash=unitid_hash) | |
except Unit.DoesNotExist: | |
return None | |
def getids(self, filename=None): | |
if hasattr(self, "_units"): | |
self.makeindex() | |
if hasattr(self, "id_index"): | |
return self.id_index.keys() | |
elif hasattr(self, "dbid_index"): | |
return self.dbid_index.values() | |
else: | |
return self.units.values_list('unitid', flat=True) | |
def header(self): | |
#FIXME: we should store some metadata in db | |
if self.file and hasattr(self.file.store, 'header'): | |
return self.file.store.header() | |
########################### Stats ############################ | |
@getfromcache | |
def getquickstats(self): | |
"""calculate translation statistics""" | |
try: | |
return calculate_stats(self.units) | |
except IntegrityError: | |
logging.info(u"Duplicate IDs in %s", self.abs_real_path) | |
except base.ParseError as e: | |
logging.info(u"Failed to parse %s\n%s", self.abs_real_path, e) | |
except (IOError, OSError) as e: | |
logging.info(u"Can't access %s\n%s", self.abs_real_path, e) | |
stats = {} | |
stats.update(empty_quickstats) | |
stats['errors'] += 1 | |
return stats | |
@getfromcache | |
def getcompletestats(self): | |
"""report result of quality checks""" | |
try: | |
self.require_qualitychecks() | |
queryset = QualityCheck.objects.filter(unit__store=self, | |
unit__state__gt=UNTRANSLATED, | |
false_positive=False) | |
return group_by_count_extra(queryset, 'name', 'category') | |
except Exception as e: | |
logging.info(u"Error getting quality checks for %s\n%s", | |
self.name, e) | |
return {} | |
@getfromcache | |
def get_suggestion_count(self): | |
"""Check if any unit in the store has suggestions""" | |
return Suggestion.objects.filter(unit__store=self, | |
unit__state__gt=OBSOLETE).count() | |
############################ Translation ############################# | |
def getitem(self, item): | |
"""Returns a single unit based on the item number.""" | |
return self.units[item] | |
@commit_on_success | |
def mergefile(self, newfile, profile, allownewstrings, suggestions, | |
notranslate, obsoletemissing): | |
"""Merges :param:`newfile` with the current store. | |
:param newfile: The file that will be merged into the current store. | |
:param profile: A :cls:`~pootle_profile.models.PootleProfile` user | |
profile. | |
:param allownewstrings: Whether to add or not units from | |
:param:`newfile` not present in the current store. | |
:param suggestions: Try to add conflicting units as suggestions in case | |
the new file's modified time is unknown or older that the in-DB | |
unit). | |
:param notranslate: Don't translate/merge in-DB units but rather add | |
them as suggestions. | |
:param obsoletemissing: Whether to remove or not units present in the | |
current store but not in :param:`newfile`. | |
""" | |
if not newfile.units: | |
return | |
monolingual = is_monolingual(type(newfile)) | |
self.clean_stale_lock() | |
# Must be done before locking the file in case it wasn't already parsed | |
self.require_units() | |
if self.state == LOCKED: | |
# File currently being updated | |
# FIXME: shall we idle wait for lock to be released first? what | |
# about stale locks? | |
logging.info(u"Attemped to merge %s while locked", self.pootle_path) | |
return | |
logging.debug(u"Merging %s", self.pootle_path) | |
# Lock store | |
old_state = self.state | |
self.state = LOCKED | |
self.save() | |
if suggestions: | |
mtime = self._get_mtime_from_header(newfile) | |
else: | |
mtime = None | |
try: | |
self.require_dbid_index(update=True, obsolete=True) | |
old_ids = set(self.dbid_index.keys()) | |
if issubclass(self.translation_project.project.get_file_class(), | |
newfile.__class__): | |
new_ids = set(newfile.getids()) | |
else: | |
new_ids = set(newfile.getids(self.name)) | |
if ((not monolingual or | |
self.translation_project.is_template_project) and | |
allownewstrings): | |
new_units = (newfile.findid(uid) for uid in new_ids - old_ids) | |
for unit in new_units: | |
newunit = self.addunit(unit) | |
if old_state >= CHECKED: | |
newunit.update_qualitychecks(created=True) | |
if obsoletemissing: | |
obsolete_dbids = [self.dbid_index.get(uid) | |
for uid in old_ids - new_ids] | |
for unit in self.findid_bulk(obsolete_dbids): | |
if unit.istranslated(): | |
unit.makeobsolete() | |
unit.save() | |
else: | |
unit.delete() | |
common_dbids = [self.dbid_index.get(uid) | |
for uid in old_ids & new_ids] | |
for oldunit in self.findid_bulk(common_dbids): | |
newunit = newfile.findid(oldunit.getid()) | |
if (monolingual and | |
not self.translation_project.is_template_project): | |
fix_monolingual(oldunit, newunit, monolingual) | |
if newunit.istranslated(): | |
if (notranslate or suggestions and | |
oldunit.istranslated() and | |
(not mtime or mtime < oldunit.mtime)): | |
oldunit.add_suggestion(newunit.target, profile) | |
else: | |
changed = oldunit.merge(newunit, overwrite=True) | |
if changed: | |
do_checks = (oldunit._source_updated or | |
oldunit._target_updated) | |
oldunit.save() | |
if do_checks and old_state >= CHECKED: | |
oldunit.update_qualitychecks() | |
if allownewstrings or obsoletemissing: | |
self.sync(update_structure=True, update_translation=True, | |
conservative=False, create=False, profile=profile) | |
finally: | |
# Unlock store | |
self.state = old_state | |
self.save() | |
def update_store_header(self, profile=None): | |
language = self.translation_project.language | |
source_language = self.translation_project.project.source_language | |
disk_store = self.file.store | |
disk_store.settargetlanguage(language.code) | |
disk_store.setsourcelanguage(source_language.code) | |
from translate.storage import poheader | |
if isinstance(disk_store, poheader.poheader): | |
mtime = self.get_mtime() | |
if mtime is None: | |
mtime = timezone.now() | |
if profile is None: | |
try: | |
submission = self.translation_project.submission_set \ | |
.filter(creation_time=mtime).latest() | |
submitter = submission.submitter | |
if submitter is not None: | |
if submitter.user.username != 'nobody': | |
profile = submitter | |
except ObjectDoesNotExist: | |
try: | |
submission = self.translation_project.submission_set \ | |
.latest() | |
mtime = min(submission.creation_time, mtime) | |
submitter = submission.submitter | |
if submitter is not None: | |
if submitter.user.username != 'nobody': | |
profile = submitter | |
except ObjectDoesNotExist: | |
pass | |
po_revision_date = mtime.strftime('%Y-%m-%d %H:%M') + \ | |
poheader.tzstring() | |
from pootle.__version__ import sver as pootle_version | |
x_generator = "Pootle %s" % pootle_version | |
headerupdates = { | |
'PO_Revision_Date': po_revision_date, | |
'X_Generator': x_generator, | |
'X_POOTLE_MTIME': ('%s.%06d' % | |
(int(time.mktime(mtime.timetuple())), | |
mtime.microsecond)), | |
} | |
if profile is not None and profile.user.is_authenticated(): | |
headerupdates['Last_Translator'] = '%s <%s>' % \ | |
(profile.user.first_name or profile.user.username, | |
profile.user.email) | |
else: | |
#FIXME: maybe insert settings.TITLE or domain here? | |
headerupdates['Last_Translator'] = 'Anonymous Pootle User' | |
disk_store.updateheader(add=True, **headerupdates) | |
if language.nplurals and language.pluralequation: | |
disk_store.updateheaderplural(language.nplurals, | |
language.pluralequation) | |
########################## Pending Files ################################# | |
# The .pending files are deprecated since Pootle 2.1.0, but support for | |
# them are kept here to be able to do migrations from older Pootle | |
# versions. | |
def init_pending(self): | |
"""initialize pending translations file if needed""" | |
if self.pending: | |
# pending file already referenced in db, but does it | |
# really exist | |
if os.path.exists(self.pending.path): | |
# pending file exists | |
return | |
else: | |
# pending file doesn't exist anymore | |
self.pending = None | |
self.save() | |
pending_name = os.extsep.join(self.file.name.split(os.extsep)[:-1] + \ | |
['po', 'pending']) | |
pending_path = os.path.join(settings.PODIRECTORY, pending_name) | |
# check if pending file already exists, just in case it was | |
# added outside of pootle | |
if os.path.exists(pending_path): | |
self.pending = pending_name | |
self.save() | |
@commit_on_success | |
def import_pending(self): | |
"""import suggestions from legacy .pending files, into database""" | |
self.init_pending() | |
if not self.pending: | |
return | |
for sugg in [sugg for sugg in self.pending.store.units | |
if sugg.istranslatable() and sugg.istranslated()]: | |
if not sugg.istranslatable() or not sugg.istranslated(): | |
continue | |
unit = self.findunit(sugg.source) | |
if unit: | |
suggester = self.getsuggester_from_pending(sugg) | |
unit.add_suggestion(sugg.target, suggester, touch=False) | |
self.pending.store.units.remove(sugg) | |
if len(self.pending.store.units) > 1: | |
self.pending.savestore() | |
else: | |
self.pending.delete() | |
self.pending = None | |
self.save() | |
def getsuggester_from_pending(self, unit): | |
"""returns who suggested the given item's suggitem if | |
recorded, else None""" | |
suggestedby = suggester_regexp.search(unit.msgidcomment) | |
if suggestedby: | |
username = suggestedby.group(1) | |
from pootle_profile.models import PootleProfile | |
try: | |
return PootleProfile.objects.get(user__username=username) | |
except PootleProfile.DoesNotExist: | |
pass | |
return None | |
################################ Signal handlers ############################## | |
# NOTE: for some strange reason it was impossible to use m2m_changed signal. | |
def flush_goal_stats_for_tp_cache(sender, instance, **kwargs): | |
"""Flush goal stats for a TP if the goal is (un)applied to a store.""" | |
# Make sure that the signal was sent when (un)applying a goal to a store. | |
if isinstance(instance.content_object, Store): | |
goal = instance.tag | |
store_path = instance.content_object.pootle_path | |
goal.delete_cache_for_path(store_path) | |
post_save.connect(flush_goal_stats_for_tp_cache, sender=Store.goals.through) | |
pre_delete.connect(flush_goal_stats_for_tp_cache, sender=Store.goals.through) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2008-2013 Zuza Software Foundation | |
# Copyright 2013 Evernote Corporation | |
# | |
# This file is part of Pootle. | |
# | |
# Pootle 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 2 of the License, or (at your option) any later | |
# version. | |
# | |
# Pootle 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 | |
# Pootle; if not, see <http://www.gnu.org/licenses/>. | |
from django.conf.urls import patterns, url | |
urlpatterns = patterns('pootle_store.views', | |
# Download and export - key to key | |
(r'^export-file/xmlKey/(?P<pootle_path>.*)/?$', | |
'export_as_xmlKey'), | |
(r'^export-file/stringsKey/(?P<pootle_path>.*)/?$', | |
'export_as_stringsKey'), | |
(r'^export-file/propertiesKey/(?P<pootle_path>.*)/?$', | |
'export_as_propertiesKey'), | |
# Download and export - normal | |
url(r'^download/(?P<pootle_path>.*)/?$', | |
'download'), | |
(r'^export-file/xlf/(?P<pootle_path>.*)/?$', | |
'export_as_xliff'), | |
(r'^export-file/xml/(?P<pootle_path>.*)/?$', | |
'export_as_xml'), | |
(r'^export-file/strings/(?P<pootle_path>.*)/?$', | |
'export_as_strings'), | |
(r'^export-file/properties/(?P<pootle_path>.*)/?$', | |
'export_as_properties'), | |
(r'^export-file/(?P<filetype>.*)/(?P<pootle_path>.*)/?$', | |
'export_as_type'), | |
# XHR | |
url(r'^xhr/checks/?$', | |
'get_failing_checks', | |
name='pootle-xhr-checks'), | |
url(r'^xhr/units/?$', | |
'get_units', | |
name='pootle-xhr-units'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/comment/?$', | |
'comment', | |
name='pootle-xhr-units-add'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/?$', | |
'submit', | |
name='pootle-xhr-units-submit'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/comment/?$', | |
'comment', | |
name='pootle-xhr-units-comment'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/image/?$', | |
'image', | |
name='pootle-xhr-units-image'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/context/?$', | |
'get_more_context', | |
name='pootle-xhr-units-context'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/edit/?$', | |
'get_edit_unit', | |
name='pootle-xhr-units-edit'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/timeline/?$', | |
'timeline', | |
name='pootle-xhr-units-timeline'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/suggestions/?$', | |
'suggest', | |
name='pootle-xhr-units-suggest'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/suggestions/(?P<suggid>[0-9]+)/accept/?$', | |
'accept_suggestion', | |
name='pootle-xhr-units-suggestion-accept'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/suggestions/(?P<suggid>[0-9]+)/reject/?$', | |
'reject_suggestion', | |
name='pootle-xhr-units-suggestion-reject'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/suggestions/(?P<suggid>[0-9]+)/votes/?$', | |
'vote_up', | |
name='pootle-xhr-units-suggestions-votes-up'), | |
# FIXME: unify voting URLs | |
url(r'^xhr/votes/(?P<voteid>[0-9]+)/clear/?$', | |
'clear_vote', | |
name='pootle-xhr-votes-clear'), | |
url(r'^xhr/units/(?P<uid>[0-9]+)/checks/(?P<checkid>[0-9]+)/reject/?$', | |
'reject_qualitycheck', | |
name='pootle-xhr-units-checks-reject'), | |
# XHR for tags. | |
url(r'^ajax/tags/add/store/(?P<store_pk>[0-9]+)?$', | |
'ajax_add_tag_to_store', | |
name='pootle-store-ajax-add-tag'), | |
url(r'^ajax/tags/remove/(?P<tag_slug>[a-z0-9-]+)/store/' | |
r'(?P<store_pk>[0-9]+)?$', | |
'ajax_remove_tag_from_store', | |
name='pootle-store-ajax-remove-tag'), | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2010-2013 Zuza Software Foundation | |
# Copyright 2013 Evernote Corporation | |
# | |
# This file is part of Pootle. | |
# | |
# Pootle 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 2 of the License, or (at your option) any later | |
# version. | |
# | |
# Pootle 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 | |
# Pootle; if not, see <http://www.gnu.org/licenses/>. | |
import logging | |
import os | |
from itertools import groupby | |
from translate.lang import data | |
from django.conf import settings | |
from django.contrib.auth.models import User | |
from django.core.cache import cache | |
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist | |
from django.core.urlresolvers import reverse | |
from django.db.models import Q | |
from django.http import HttpResponse, Http404 | |
from django.shortcuts import get_object_or_404, render_to_response | |
from django.template import loader, RequestContext | |
from django.utils.translation import to_locale, ugettext as _ | |
from django.utils.translation.trans_real import parse_accept_lang_header | |
from django.utils import simplejson, timezone | |
from django.utils.encoding import iri_to_uri | |
from django.views.decorators.cache import never_cache | |
from django.views.decorators.http import require_POST | |
from taggit.models import Tag | |
from pootle.core.exceptions import Http400 | |
from pootle.core.url_helpers import split_pootle_path | |
from pootle_app.models import Suggestion as SuggestionStat | |
from pootle_app.models.permissions import (check_permission, | |
check_profile_permission) | |
from pootle_language.models import Language | |
from pootle_misc.baseurl import redirect | |
from pootle_misc.checks import get_quality_check_failures | |
from pootle_misc.forms import make_search_form | |
from pootle_misc.stats import get_raw_stats | |
from pootle_misc.url_manip import ensure_uri | |
from pootle_misc.util import paginate, ajax_required, jsonify | |
from pootle_profile.models import get_profile | |
from pootle_project.models import Project | |
from pootle_statistics.models import (Submission, SubmissionFields, | |
SubmissionTypes) | |
from pootle_tagging.forms import TagForm | |
from pootle_tagging.models import Goal | |
from pootle_translationproject.models import TranslationProject | |
from .decorators import (get_store_context, get_unit_context, | |
get_xhr_resource_context) | |
from .forms import (unit_comment_form_factory, unit_image_form_factory, unit_form_factory, | |
highlight_whitespace) | |
from .models import Store, Unit | |
from pootle_translationproject.models import TranslationProject | |
from pootle_language.models import Language | |
from .signals import translation_submitted | |
from .templatetags.store_tags import (highlight_diffs, pluralize_source, | |
pluralize_target) | |
from .util import (UNTRANSLATED, FUZZY, TRANSLATED, STATES_MAP, | |
absolute_real_path, find_altsrcs, get_sugg_list) | |
from convert import getFileType | |
@get_store_context('view') | |
def export_as_strings(request, store): | |
"""Export given file to strings for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'strings']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_strings" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "strings") | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_stringsKey(request, store): | |
"""Export given file to strings for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'strings']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_strings" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "strings", True) | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_properties(request, store): | |
"""Export given file to properties for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'properties']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_properties" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "properties") | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_propertiesKey(request, store): | |
"""Export given file to properties for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'properties']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_properties" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "properties", True) | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_xml(request, store): | |
"""Export given file to xml for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'xml']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_xml" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "xml") | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_xmlKey(request, store): | |
"""Export given file to xml for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'xml']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_xml" % store.pootle_path) | |
last_export = cache.get(key) | |
from pootle_app.project_tree import ensure_target_dir_exists | |
ensure_target_dir_exists(abs_export_path) | |
store.sync(update_translation=True) | |
getFileType(store.file, abs_export_path, "xml", True) | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_xliff(request, store): | |
"""Export given file to xliff for offline translation.""" | |
path = store.real_path | |
if not path: | |
# bug 2106 | |
project = request.translation_project.project | |
if project.get_treestyle() == "gnu": | |
path = "/".join(store.pootle_path.split(os.path.sep)[2:]) | |
else: | |
parts = store.pootle_path.split(os.path.sep)[1:] | |
path = "%s/%s/%s" % (parts[1], parts[0], "/".join(parts[2:])) | |
path, ext = os.path.splitext(path) | |
export_path = "/".join(['POOTLE_EXPORT', path + os.path.extsep + 'xlf']) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_xliff" % store.pootle_path) | |
last_export = cache.get(key) | |
store.sync(update_translation=True) | |
if (not (last_export and last_export == store.get_mtime() and | |
os.path.isfile(abs_export_path))): | |
from pootle_app.project_tree import ensure_target_dir_exists | |
from translate.storage.poxliff import PoXliffFile | |
from pootle_misc import ptempfile as tempfile | |
import shutil | |
ensure_target_dir_exists(abs_export_path) | |
outputstore = store.convert(PoXliffFile) | |
outputstore.switchfile(store.name, createifmissing=True) | |
fd, tempstore = tempfile.mkstemp(prefix=store.name, suffix='.xlf') | |
os.close(fd) | |
outputstore.savefile(tempstore) | |
shutil.move(tempstore, abs_export_path) | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def export_as_type(request, store, filetype): | |
"""Export given file to xliff for offline translation.""" | |
from pootle_store.filetypes import factory_classes, is_monolingual | |
klass = factory_classes.get(filetype, None) | |
if (not klass or is_monolingual(klass) or | |
store.pootle_path.endswith(filetype)): | |
raise ValueError | |
path, ext = os.path.splitext(store.real_path) | |
export_path = os.path.join('POOTLE_EXPORT', | |
path + os.path.extsep + filetype) | |
abs_export_path = absolute_real_path(export_path) | |
key = iri_to_uri("%s:export_as_%s" % (store.pootle_path, filetype)) | |
last_export = cache.get(key) | |
if (not (last_export and last_export == store.get_mtime() and | |
os.path.isfile(abs_export_path))): | |
from pootle_app.project_tree import ensure_target_dir_exists | |
from pootle_misc import ptempfile as tempfile | |
import shutil | |
ensure_target_dir_exists(abs_export_path) | |
outputstore = store.convert(klass) | |
fd, tempstore = tempfile.mkstemp(prefix=store.name, | |
suffix=os.path.extsep + filetype) | |
os.close(fd) | |
outputstore.savefile(tempstore) | |
shutil.move(tempstore, abs_export_path) | |
cache.set(key, store.get_mtime(), settings.OBJECT_CACHE_TIMEOUT) | |
return redirect('/export/' + export_path) | |
@get_store_context('view') | |
def download(request, store): | |
store.sync(update_translation=True) | |
return redirect('/export/' + store.real_path) | |
####################### Translate Page ############################## | |
def get_alt_src_langs(request, translation_project): | |
language = translation_project.language | |
transProj = TranslationProject.objects.exclude(language_id=translation_project.language_id).filter(project_id=translation_project.project_id) | |
transProj = transProj.values_list('language_id', flat=True).distinct() | |
language_id = [] | |
for proj in transProj: | |
language_id.append(proj) | |
langs = Language.objects.filter(id__in=language_id) | |
return langs | |
def get_non_indexed_search_step_query(form, units_queryset): | |
words = form.cleaned_data['search'].split() | |
result = units_queryset.none() | |
if 'source' in form.cleaned_data['sfields']: | |
subresult = units_queryset | |
for word in words: | |
subresult = subresult.filter(source_f__icontains=word) | |
result = result | subresult | |
if 'target' in form.cleaned_data['sfields']: | |
subresult = units_queryset | |
for word in words: | |
subresult = subresult.filter(target_f__icontains=word) | |
result = result | subresult | |
if 'notes' in form.cleaned_data['sfields']: | |
translator_subresult = units_queryset | |
developer_subresult = units_queryset | |
for word in words: | |
translator_subresult = translator_subresult.filter( | |
translator_comment__icontains=word, | |
) | |
developer_subresult = developer_subresult.filter( | |
developer_comment__icontains=word, | |
) | |
result = result | translator_subresult | developer_subresult | |
if 'images' in form.cleaned_data['sfields']: | |
images_subresult = units_queryset | |
for word in words: | |
images_subresult = images_subresult.filter( | |
images__icontains=word, | |
) | |
developer_subresult = developer_subresult.filter( | |
developer_comment__icontains=word, | |
) | |
result = result | images_subresult | |
if 'locations' in form.cleaned_data['sfields']: | |
subresult = units_queryset | |
for word in words: | |
subresult = subresult.filter(locations__icontains=word) | |
result = result | subresult | |
return result | |
def get_non_indexed_search_exact_query(form, units_queryset): | |
phrase = form.cleaned_data['search'] | |
result = units_queryset.none() | |
if 'source' in form.cleaned_data['sfields']: | |
subresult = units_queryset.filter(source_f__contains=phrase) | |
result = result | subresult | |
if 'target' in form.cleaned_data['sfields']: | |
subresult = units_queryset.filter(target_f__contains=phrase) | |
result = result | subresult | |
if 'notes' in form.cleaned_data['sfields']: | |
translator_subresult = units_queryset | |
developer_subresult = units_queryset | |
translator_subresult = translator_subresult.filter( | |
translator_comment__contains=phrase, | |
) | |
developer_subresult = developer_subresult.filter( | |
developer_comment__contains=phrase, | |
) | |
result = result | translator_subresult | developer_subresult | |
if 'notes' in form.cleaned_data['sfields']: | |
images_subresult = units_queryset | |
images_subresult = images_subresult.filter( | |
image__contains=phrase, | |
) | |
result = result | images_subresult | |
if 'locations' in form.cleaned_data['sfields']: | |
subresult = units_queryset.filter(locations__contains=phrase) | |
result = result | subresult | |
return result | |
def get_search_step_query(request, form, units_queryset): | |
"""Narrows down units query to units matching search string.""" | |
if 'exact' in form.cleaned_data['soptions']: | |
logging.debug(u"Using exact database search") | |
return get_non_indexed_search_exact_query(form, units_queryset) | |
path = request.GET.get('path', None) | |
if path is not None: | |
lang, proj, dir_path, filename = split_pootle_path(path) | |
translation_projects = [] | |
# /<language_code>/<project_code>/ | |
if lang is not None and proj is not None: | |
project = get_object_or_404(Project, code=proj) | |
language = get_object_or_404(Language, code=lang) | |
translation_projects = \ | |
TranslationProject.objects.filter(project=project, | |
language=language) | |
# /projects/<project_code>/ | |
elif lang is None and proj is not None: | |
project = get_object_or_404(Project, code=proj) | |
translation_projects = \ | |
TranslationProject.objects.filter(project=project) | |
# /<language_code>/ | |
elif lang is not None and proj is None: | |
language = get_object_or_404(Language, code=lang) | |
translation_projects = \ | |
TranslationProject.objects.filter(language=language) | |
# / | |
elif lang is None and proj is None: | |
translation_projects = TranslationProject.objects.all() | |
has_indexer = True | |
for translation_project in translation_projects: | |
if translation_project.indexer is None: | |
has_indexer = False | |
if not has_indexer: | |
logging.debug(u"No indexer for one or more translation project," | |
u" using database search") | |
return get_non_indexed_search_step_query(form, units_queryset) | |
else: | |
alldbids = [] | |
for translation_project in translation_projects: | |
logging.debug(u"Found %s indexer for %s, using indexed search", | |
translation_project.indexer.INDEX_DIRECTORY_NAME, | |
translation_project) | |
word_querylist = [] | |
words = form.cleaned_data['search'] | |
fields = form.cleaned_data['sfields'] | |
paths = units_queryset.order_by() \ | |
.values_list('store__pootle_path', | |
flat=True) \ | |
.distinct() | |
path_querylist = [('pofilename', pootle_path) | |
for pootle_path in paths.iterator()] | |
cache_key = "search:%s" % str(hash((repr(path_querylist), | |
translation_project.get_mtime(), | |
repr(words), | |
repr(fields)))) | |
dbids = cache.get(cache_key) | |
if dbids is None: | |
searchparts = [] | |
word_querylist = [(field, words) for field in fields] | |
textquery = \ | |
translation_project.indexer.make_query(word_querylist, | |
False) | |
searchparts.append(textquery) | |
pathquery = \ | |
translation_project.indexer.make_query(path_querylist, | |
False) | |
searchparts.append(pathquery) | |
limitedquery = \ | |
translation_project.indexer.make_query(searchparts, | |
True) | |
result = translation_project.indexer.search(limitedquery, | |
['dbid']) | |
dbids = [int(item['dbid'][0]) for item in result[:999]] | |
cache.set(cache_key, dbids, settings.OBJECT_CACHE_TIMEOUT) | |
alldbids.extend(dbids) | |
return units_queryset.filter(id__in=alldbids) | |
def get_step_query(request, units_queryset): | |
"""Narrows down unit query to units matching conditions in GET.""" | |
if 'filter' in request.GET: | |
unit_filter = request.GET['filter'] | |
username = request.GET.get('user', None) | |
profile = request.profile | |
if username is not None: | |
try: | |
user = User.objects.get(username=username) | |
profile = user.get_profile() | |
except User.DoesNotExist: | |
pass | |
if unit_filter: | |
match_queryset = units_queryset.none() | |
if unit_filter == 'all': | |
match_queryset = units_queryset | |
elif unit_filter == 'translated': | |
match_queryset = units_queryset.filter(state=TRANSLATED) | |
elif unit_filter == 'untranslated': | |
match_queryset = units_queryset.filter(state=UNTRANSLATED) | |
elif unit_filter == 'fuzzy': | |
match_queryset = units_queryset.filter(state=FUZZY) | |
elif unit_filter == 'incomplete': | |
match_queryset = units_queryset.filter( | |
Q(state=UNTRANSLATED) | Q(state=FUZZY), | |
) | |
elif unit_filter == 'suggestions': | |
#FIXME: is None the most efficient query | |
match_queryset = units_queryset.exclude(suggestion=None) | |
elif unit_filter == 'user-suggestions': | |
match_queryset = units_queryset.filter( | |
suggestion__user=profile, | |
).distinct() | |
elif unit_filter == 'user-suggestions-accepted': | |
# FIXME: Oh, this is pretty lame, we need a completely | |
# different way to model suggestions | |
unit_ids = SuggestionStat.objects.filter( | |
suggester=profile, | |
state='accepted', | |
).values_list('unit', flat=True) | |
match_queryset = units_queryset.filter( | |
id__in=unit_ids, | |
).distinct() | |
elif unit_filter == 'user-suggestions-rejected': | |
# FIXME: Oh, this is as lame as above | |
unit_ids = SuggestionStat.objects.filter( | |
suggester=profile, | |
state='rejected', | |
).values_list('unit', flat=True) | |
match_queryset = units_queryset.filter( | |
id__in=unit_ids, | |
).distinct() | |
elif unit_filter == 'user-submissions': | |
match_queryset = units_queryset.filter( | |
submission__submitter=profile, | |
).distinct() | |
elif unit_filter == 'user-submissions-overwritten': | |
match_queryset = units_queryset.filter( | |
submission__submitter=profile, | |
).exclude(submitted_by=profile).distinct() | |
elif unit_filter == 'checks' and 'checks' in request.GET: | |
checks = request.GET['checks'].split(',') | |
if checks: | |
match_queryset = units_queryset.filter( | |
qualitycheck__false_positive=False, | |
qualitycheck__name__in=checks | |
).distinct() | |
units_queryset = match_queryset | |
if 'goal' in request.GET: | |
try: | |
goal = Goal.objects.get(slug=request.GET['goal']) | |
except Goal.DoesNotExist: | |
pass | |
else: | |
pootle_path = (request.GET.get('path', '') or | |
request.path.replace("/export-view/", "/", 1)) | |
goal_stores = goal.get_stores_for_path(pootle_path) | |
units_queryset = units_queryset.filter(store__in=goal_stores) | |
if 'search' in request.GET and 'sfields' in request.GET: | |
# use the search form for validation only | |
search_form = make_search_form(request.GET) | |
if search_form.is_valid(): | |
units_queryset = get_search_step_query(request, search_form, | |
units_queryset) | |
return units_queryset | |
# | |
# Views used with XMLHttpRequest requests. | |
# | |
def _filter_ctx_units(units_qs, unit, how_many, gap=0): | |
"""Returns ``how_many``*2 units that are before and after ``index``.""" | |
result = {'before': [], 'after': []} | |
if how_many and unit.index - gap > 0: | |
before = units_qs.filter(store=unit.store_id, index__lt=unit.index) \ | |
.order_by('-index')[gap:how_many+gap] | |
result['before'] = _build_units_list(before, reverse=True) | |
result['before'].reverse() | |
#FIXME: can we avoid this query if length is known? | |
if how_many: | |
after = units_qs.filter(store=unit.store_id, | |
index__gt=unit.index)[gap:how_many+gap] | |
result['after'] = _build_units_list(after) | |
return result | |
def _prepare_unit(unit): | |
"""Constructs a dictionary with relevant `unit` data.""" | |
return { | |
'id': unit.id, | |
'isfuzzy': unit.isfuzzy(), | |
'source': [source[1] for source in pluralize_source(unit)], | |
'target': [target[1] for target in pluralize_target(unit)], | |
} | |
def _path_units_with_meta(path, units): | |
"""Constructs a dictionary which contains a list of `units` | |
corresponding to `path` as well as its metadata. | |
""" | |
meta = None | |
units_list = [] | |
for unit in iter(units): | |
if meta is None: | |
# XXX: Watch out for the query count | |
store = unit.store | |
tp = store.translation_project | |
project = tp.project | |
meta = { | |
'source_lang': project.source_language.code, | |
'source_dir': project.source_language.direction, | |
'target_lang': tp.language.code, | |
'target_dir': tp.language.direction, | |
'project_code': project.code, | |
'project_style': project.checkstyle, | |
} | |
units_list.append(_prepare_unit(unit)) | |
return { | |
path: { | |
'meta': meta, | |
'units': units_list, | |
}, | |
} | |
def _build_units_list(units, reverse=False): | |
"""Given a list/queryset of units, builds a list with the unit data | |
contained in a dictionary ready to be returned as JSON. | |
:return: A list with unit id, source, and target texts. In case of | |
having plural forms, a title for the plural form is also provided. | |
""" | |
return_units = [] | |
for unit in iter(units): | |
return_units.append(_prepare_unit(unit)) | |
return return_units | |
@ajax_required | |
def get_units(request): | |
"""Gets source and target texts and its metadata. | |
:return: A JSON-encoded object containing the source and target texts | |
grouped by the store they belong to. | |
When the ``pager`` GET parameter is present, pager information | |
will be returned too. | |
""" | |
pootle_path = request.GET.get('path', None) | |
if pootle_path is None: | |
raise Http400(_('Arguments missing.')) | |
page = None | |
request.profile = get_profile(request.user) | |
limit = request.profile.get_unit_rows() | |
units_qs = Unit.objects.get_for_path(pootle_path, request.profile) | |
step_queryset = get_step_query(request, units_qs) | |
# Maybe we are trying to load directly a specific unit, so we have | |
# to calculate its page number. | |
uid = request.GET.get('uid', None) | |
if uid is not None: | |
try: | |
# XXX: Watch for performance, might want to drop into raw SQL | |
# at some stage. | |
uid_list = list(step_queryset.values_list('id', flat=True)) | |
preceding = uid_list.index(int(uid)) | |
page = preceding / limit + 1 | |
except ValueError: | |
pass # uid wasn't a number or not present in the results. | |
# XXX: Black magic going on here. See issue #4 on Evernote for details. | |
step_queryset.query.sql_with_params() | |
pager = paginate(request, step_queryset, items=limit, page=page) | |
unit_groups = [] | |
units_by_path = groupby(pager.object_list, lambda x: x.store.pootle_path) | |
for pootle_path, units in units_by_path: | |
unit_groups.append(_path_units_with_meta(pootle_path, units)) | |
response = { | |
'unit_groups': unit_groups, | |
} | |
if request.GET.get('pager', False): | |
response['pager'] = { | |
'count': pager.paginator.count, | |
'current': pager.number, | |
'numPages': pager.paginator.num_pages, | |
'perPage': pager.paginator.per_page, | |
} | |
return HttpResponse(jsonify(response), mimetype="application/json") | |
def _is_filtered(request): | |
"""Checks if unit list is filtered.""" | |
return ('filter' in request.GET or 'checks' in request.GET or | |
'user' in request.GET or | |
('search' in request.GET and 'sfields' in request.GET)) | |
@ajax_required | |
@get_unit_context('view') | |
def get_more_context(request, unit): | |
"""Retrieves more context units. | |
:return: An object in JSON notation that contains the source and target | |
texts for units that are in the context of unit ``uid``. | |
""" | |
store = request.store | |
json = {} | |
gap = int(request.GET.get('gap', 0)) | |
qty = int(request.GET.get('qty', 1)) | |
json["ctx"] = _filter_ctx_units(store.units, unit, qty, gap) | |
rcode = 200 | |
response = jsonify(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@never_cache | |
@get_unit_context('view') | |
def timeline(request, unit): | |
"""Returns a JSON-encoded string including the changes to the unit | |
rendered in HTML. | |
""" | |
timeline = Submission.objects.filter(unit=unit, field__in=[ | |
SubmissionFields.TARGET, SubmissionFields.STATE, | |
SubmissionFields.COMMENT | |
]) | |
timeline = timeline.select_related("submitter__user", | |
"translation_project__language") | |
context = {} | |
entries_group = [] | |
import locale | |
from pootle_store.fields import to_python | |
for key, values in groupby(timeline, key=lambda x: x.creation_time): | |
entry_group = { | |
'datetime': key, | |
'datetime_str': key.strftime(locale.nl_langinfo(locale.D_T_FMT)), | |
'entries': [], | |
} | |
for item in values: | |
# Only add submitter information for the whole entry group once | |
entry_group.setdefault('submitter', item.submitter) | |
context.setdefault('language', item.translation_project.language) | |
entry = { | |
'field': item.field, | |
'field_name': SubmissionFields.NAMES_MAP[item.field], | |
} | |
if item.field == SubmissionFields.STATE: | |
entry['old_value'] = STATES_MAP[int(to_python(item.old_value))] | |
entry['new_value'] = STATES_MAP[int(to_python(item.new_value))] | |
else: | |
entry['new_value'] = to_python(item.new_value) | |
entry_group['entries'].append(entry) | |
entries_group.append(entry_group) | |
# Let's reverse the chronological order | |
entries_group.reverse() | |
# Remove first timeline item if it's solely a change to the target | |
if (entries_group and len(entries_group[0]['entries']) == 1 and | |
entries_group[0]['entries'][0]['field'] == SubmissionFields.TARGET): | |
del entries_group[0] | |
context['entries_group'] = entries_group | |
if request.is_ajax(): | |
# The client will want to confirm that the response is relevant for | |
# the unit on screen at the time of receiving this, so we add the uid. | |
json = {'uid': unit.id} | |
t = loader.get_template('unit/xhr-timeline.html') | |
c = RequestContext(request, context) | |
json['timeline'] = t.render(c).replace('\n', '') | |
response = simplejson.dumps(json) | |
return HttpResponse(response, mimetype="application/json") | |
else: | |
return render_to_response('unit/timeline.html', context, | |
context_instance=RequestContext(request)) | |
@require_POST | |
@ajax_required | |
@get_unit_context('translate') | |
def comment(request, unit): | |
"""Stores a new comment for the given ``unit``. | |
:return: If the form validates, the cleaned comment is returned. | |
An error message is returned otherwise. | |
""" | |
# Update current unit instance's attributes | |
unit.commented_by = request.profile | |
unit.commented_on = timezone.now() | |
language = request.translation_project.language | |
form = unit_comment_form_factory(language)(request.POST, instance=unit, | |
request=request) | |
if form.is_valid(): | |
form.save() | |
context = { | |
'unit': unit, | |
'language': language, | |
} | |
t = loader.get_template('unit/comment.html') | |
c = RequestContext(request, context) | |
json = {'comment': t.render(c)} | |
rcode = 200 | |
else: | |
json = {'msg': _("Comment submission failed.")} | |
rcode = 400 | |
response = simplejson.dumps(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@require_POST | |
@ajax_required | |
@get_unit_context('translate') | |
def image(request, unit): | |
"""Stores a new image for the given ``unit``. | |
:return: If the form validates, the cleaned image is returned. | |
An error message is returned otherwise. | |
""" | |
# Update current unit instance's attributes | |
unit.uploaded_by = request.profile | |
unit.uploaded_on = timezone.now() | |
language = request.translation_project.language | |
form = unit_image_form_factory(language)(request.POST, request.FILES, instance=unit, | |
request=request) | |
if form.is_valid(): | |
form.save() | |
context = { | |
'unit': unit, | |
'language': language, | |
} | |
t = loader.get_template('unit/image.html') | |
c = RequestContext(request, context) | |
json = {'image': t.render(c)} | |
rcode = 200 | |
else: | |
json = {'msg': _("Image submission failed.")} | |
rcode = 400 | |
response = simplejson.dumps(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@never_cache | |
@ajax_required | |
@get_unit_context('view') | |
def get_edit_unit(request, unit): | |
"""Given a store path ``pootle_path`` and unit id ``uid``, gathers all the | |
necessary information to build the editing widget. | |
:return: A templatised editing widget is returned within the ``editor`` | |
variable and paging information is also returned if the page | |
number has changed. | |
""" | |
json = {} | |
translation_project = request.translation_project | |
language = translation_project.language | |
if unit.hasplural(): | |
snplurals = len(unit.source.strings) | |
else: | |
snplurals = None | |
form_class = unit_form_factory(language, snplurals, request) | |
form = form_class(instance=unit) | |
comment_form_class = unit_comment_form_factory(language) | |
comment_form = comment_form_class({}, instance=unit) | |
image_form_class = unit_image_form_factory(language) | |
image_form = image_form_class({}, instance=unit) | |
store = unit.store | |
directory = store.parent | |
profile = request.profile | |
# Get the alternative source languages for the project, not the user profile | |
alt_src_langs = get_alt_src_langs(request, translation_project) | |
project = translation_project.project | |
report_target = ensure_uri(project.report_target) | |
suggestions = get_sugg_list(unit) | |
template_vars = { | |
'unit': unit, | |
'form': form, | |
'comment_form': comment_form, | |
'image_form': image_form, | |
'store': store, | |
'directory': directory, | |
'profile': profile, | |
'user': request.user, | |
'project': project, | |
'language': language, | |
'source_language': translation_project.project.source_language, | |
'cantranslate': check_profile_permission(profile, "translate", | |
directory), | |
'cansuggest': check_profile_permission(profile, "suggest", directory), | |
'canreview': check_profile_permission(profile, "review", directory), | |
'altsrcs': find_altsrcs(unit, alt_src_langs, store=store, | |
project=project), | |
'report_target': report_target, | |
'suggestions': suggestions, | |
} | |
if translation_project.project.is_terminology or store.is_terminology: | |
t = loader.get_template('unit/term_edit.html') | |
else: | |
t = loader.get_template('unit/edit.html') | |
c = RequestContext(request, template_vars) | |
json['editor'] = t.render(c) | |
rcode = 200 | |
# Return context rows if filtering is applied but | |
# don't return any if the user has asked not to have it. | |
current_filter = request.GET.get('filter', 'all') | |
show_ctx = request.COOKIES.get('ctxShow', 'true') | |
if ((_is_filtered(request) or current_filter not in ('all',)) and | |
show_ctx == 'true'): | |
# TODO: review if this first 'if' branch makes sense. | |
if translation_project.project.is_terminology or store.is_terminology: | |
json['ctx'] = _filter_ctx_units(store.units, unit, 0) | |
else: | |
ctx_qty = int(request.COOKIES.get('ctxQty', 1)) | |
json['ctx'] = _filter_ctx_units(store.units, unit, ctx_qty) | |
response = jsonify(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@ajax_required | |
@get_xhr_resource_context('view') | |
def get_failing_checks(request, path_obj): | |
"""Gets a list of failing checks for the current object. | |
:return: JSON string with a list of failing check categories which | |
include the actual checks that are failing. | |
""" | |
if 'goal' in request.GET and request.GET['goal']: | |
try: | |
goal = Goal.objects.get(slug=request.GET['goal']) | |
except Goal.DoesNotExist: | |
raise Http404 | |
failures = goal.get_failing_checks_for_path(path_obj.pootle_path) | |
else: | |
stats = get_raw_stats(path_obj) | |
failures = get_quality_check_failures(path_obj, stats, | |
include_url=False) | |
response = jsonify(failures) | |
return HttpResponse(response, mimetype="application/json") | |
@require_POST | |
@ajax_required | |
@get_unit_context('') | |
def submit(request, unit): | |
"""Processes translation submissions and stores them in the database. | |
:return: An object in JSON notation that contains the previous and last | |
units for the unit next to unit ``uid``. | |
""" | |
json = {} | |
cantranslate = check_permission("translate", request) | |
if not cantranslate: | |
raise PermissionDenied(_("You do not have rights to access " | |
"translation mode.")) | |
translation_project = request.translation_project | |
language = translation_project.language | |
if unit.hasplural(): | |
snplurals = len(unit.source.strings) | |
else: | |
snplurals = None | |
# Store current time so that it is the same for all submissions | |
current_time = timezone.now() | |
# Update current unit instance's attributes | |
unit.submitted_by = request.profile | |
unit.submitted_on = current_time | |
form_class = unit_form_factory(language, snplurals, request) | |
form = form_class(request.POST, instance=unit) | |
if form.is_valid(): | |
if form.updated_fields: | |
for field, old_value, new_value in form.updated_fields: | |
sub = Submission( | |
creation_time=current_time, | |
translation_project=translation_project, | |
submitter=request.profile, | |
unit=unit, | |
field=field, | |
type=SubmissionTypes.NORMAL, | |
old_value=old_value, | |
new_value=new_value, | |
) | |
# value changed | |
if unit.target_f!=unit.oldValue: | |
currentSource = unit.source_f | |
store = get_object_or_404(Store, pk=unit.store_id) | |
currentTranslationProject = store.translation_project_id | |
translationProject = get_object_or_404(TranslationProject, pk=currentTranslationProject) | |
language = get_object_or_404(Language, pk=translationProject.language_id) | |
if "en" in language.code: # Base Language changed | |
projectLanguages = TranslationProject.objects.exclude(id=currentTranslationProject).filter(project_id=translationProject.project_id) | |
# invalidates field in another languages of the same project | |
for lang in projectLanguages: | |
language = get_object_or_404(Language, pk=lang.language_id) | |
storeLang = get_object_or_404(Store, translation_project_id=lang.id) | |
unitLang = Unit.objects.filter(store_id=storeLang.id).filter(source_f=currentSource) | |
for unitL in unitLang: | |
unitL.isInvalidated = 1 | |
# consider the string as untranslated | |
unitL.target_wordcount = 0 | |
unitL.target_length = 0 | |
unitL.state = 0 | |
unitL.save() | |
# after submitting a translation, revalidates the string | |
unit.isInvalidated = 0 | |
unit.oldValue = unit.target_f | |
sub.save() | |
form.save() | |
translation_submitted.send( | |
sender=translation_project, | |
unit=form.instance, | |
profile=request.profile, | |
) | |
rcode = 200 | |
else: | |
# Form failed | |
#FIXME: we should display validation errors here | |
rcode = 400 | |
json["msg"] = _("Failed to process submission.") | |
response = jsonify(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@require_POST | |
@ajax_required | |
@get_unit_context('') | |
def add(request, unit): | |
"""Processes translation submissions and stores them in the database. | |
:return: An object in JSON notation that contains the previous and last | |
units for the unit next to unit ``uid``. | |
""" | |
json = {} | |
cantranslate = check_permission("translate", request) | |
if not cantranslate: | |
raise PermissionDenied(_("You do not have rights to access " | |
"translation mode.")) | |
translation_project = request.translation_project | |
language = translation_project.language | |
if unit.hasplural(): | |
snplurals = len(unit.source.strings) | |
else: | |
snplurals = None | |
# Store current time so that it is the same for all submissions | |
current_time = timezone.now() | |
# Update current unit instance's attributes | |
unit.submitted_by = request.profile | |
unit.submitted_on = current_time | |
form_class = unit_form_factory(language, snplurals, request) | |
form = form_class(request.POST, instance=unit) | |
if form.is_valid(): | |
if form.updated_fields: | |
for field, old_value, new_value in form.updated_fields: | |
sub = Submission( | |
creation_time=current_time, | |
translation_project=translation_project, | |
submitter=request.profile, | |
unit=unit, | |
field=field, | |
type=SubmissionTypes.NORMAL, | |
old_value=old_value, | |
new_value=new_value, | |
) | |
sub.save() | |
form.save() | |
translation_submitted.send( | |
sender=translation_project, | |
unit=form.instance, | |
profile=request.profile, | |
) | |
rcode = 200 | |
else: | |
# Form failed | |
#FIXME: we should display validation errors here | |
rcode = 400 | |
json["msg"] = _("Failed to process submission.") | |
response = jsonify(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@require_POST | |
@ajax_required | |
@get_unit_context('') | |
def suggest(request, unit): | |
"""Processes translation suggestions and stores them in the database. | |
:return: An object in JSON notation that contains the previous and last | |
units for the unit next to unit ``uid``. | |
""" | |
json = {} | |
cansuggest = check_permission("suggest", request) | |
if not cansuggest: | |
raise PermissionDenied(_("You do not have rights to access " | |
"translation mode.")) | |
translation_project = request.translation_project | |
language = translation_project.language | |
if unit.hasplural(): | |
snplurals = len(unit.source.strings) | |
else: | |
snplurals = None | |
form_class = unit_form_factory(language, snplurals, request) | |
form = form_class(request.POST, instance=unit) | |
if form.is_valid(): | |
if form.instance._target_updated: | |
# TODO: Review if this hackish method is still necessary | |
#HACKISH: django 1.2 stupidly modifies instance on | |
# model form validation, reload unit from db | |
unit = Unit.objects.get(id=unit.id) | |
sugg = unit.add_suggestion(form.cleaned_data['target_f'], | |
request.profile) | |
if sugg: | |
SuggestionStat.objects.get_or_create( | |
translation_project=translation_project, | |
suggester=request.profile, state='pending', unit=unit.id | |
) | |
rcode = 200 | |
else: | |
# Form failed | |
#FIXME: we should display validation errors here | |
rcode = 400 | |
json["msg"] = _("Failed to process suggestion.") | |
response = jsonify(json) | |
return HttpResponse(response, status=rcode, mimetype="application/json") | |
@ajax_required | |
@get_unit_context('') | |
def reject_suggestion(request, unit, suggid): | |
json = {} | |
translation_project = request.translation_project | |
json["udbid"] = unit.id | |
json["sugid"] = suggid | |
if request.POST.get('reject'): | |
try: | |
sugg = unit.suggestion_set.get(id=suggid) | |
except ObjectDoesNotExist: | |
raise Http404 | |
if (not check_permission('review', request) and | |
(not request.user.is_authenticated() or sugg and | |
sugg.user != request.profile)): | |
raise PermissionDenied(_("You do not have rights to access " | |
"review mode.")) | |
success = unit.reject_suggestion(suggid) | |
if sugg is not None and success: | |
# FIXME: we need a totally different model for tracking stats, this | |
# is just lame | |
suggstat, created = SuggestionStat.objects.get_or_create( | |
translation_project=translation_project, | |
suggester=sugg.user, | |
state='pending', | |
unit=unit.id, | |
) | |
suggstat.reviewer = request.profile | |
suggstat.state = 'rejected' | |
suggstat.save() | |
response = jsonify(json) | |
return HttpResponse(response, mimetype="application/json") | |
@ajax_required | |
@get_unit_context('review') | |
def accept_suggestion(request, unit, suggid): | |
json = { | |
'udbid': unit.id, | |
'sugid': suggid, | |
} | |
translation_project = request.translation_project | |
if request.POST.get('accept'): | |
try: | |
suggestion = unit.suggestion_set.get(id=suggid) | |
except ObjectDoesNotExist: | |
raise Http404 | |
old_target = unit.target | |
success = unit.accept_suggestion(suggid) | |
json['newtargets'] = [highlight_whitespace(target) | |
for target in unit.target.strings] | |
json['newdiffs'] = {} | |
for sugg in unit.get_suggestions(): | |
json['newdiffs'][sugg.id] = \ | |
[highlight_diffs(unit.target.strings[i], target) | |
for i, target in enumerate(sugg.target.strings)] | |
if suggestion is not None and success: | |
if suggestion.user: | |
translation_submitted.send(sender=translation_project, | |
unit=unit, profile=suggestion.user) | |
# FIXME: we need a totally different model for tracking stats, this | |
# is just lame | |
suggstat, created = SuggestionStat.objects.get_or_create( | |
translation_project=translation_project, | |
suggester=suggestion.user, | |
state='pending', | |
unit=unit.id, | |
) | |
suggstat.reviewer = request.profile | |
suggstat.state = 'accepted' | |
suggstat.save() | |
# For now assume the target changed | |
# TODO: check all fields for changes | |
creation_time = timezone.now() | |
sub = Submission( | |
creation_time=creation_time, | |
translation_project=translation_project, | |
submitter=suggestion.user, | |
from_suggestion=suggstat, | |
unit=unit, | |
field=SubmissionFields.TARGET, | |
type=SubmissionTypes.SUGG_ACCEPT, | |
old_value=old_target, | |
new_value=unit.target, | |
) | |
sub.save() | |
response = jsonify(json) | |
return HttpResponse(response, mimetype="application/json") | |
@ajax_required | |
def clear_vote(request, voteid): | |
json = {} | |
json["voteid"] = voteid | |
if request.POST.get('clear'): | |
try: | |
from voting.models import Vote | |
vote = Vote.objects.get(pk=voteid) | |
if vote.user != request.user: | |
# No i18n, will not go to UI | |
raise PermissionDenied("Users can only remove their own votes") | |
vote.delete() | |
except ObjectDoesNotExist: | |
raise Http404 | |
response = jsonify(json) | |
return HttpResponse(response, mimetype="application/json") | |
@ajax_required | |
@get_unit_context('') | |
def vote_up(request, unit, suggid): | |
json = {} | |
json["suggid"] = suggid | |
if request.POST.get('up'): | |
try: | |
suggestion = unit.suggestion_set.get(id=suggid) | |
from voting.models import Vote | |
# Why can't it just return the vote object? | |
Vote.objects.record_vote(suggestion, request.user, +1) | |
json["voteid"] = Vote.objects.get_for_user(suggestion, | |
request.user).id | |
except ObjectDoesNotExist: | |
raise Http404(_("The suggestion or vote is not valid any more.")) | |
response = jsonify(json) | |
return HttpResponse(response, mimetype="application/json") | |
@ajax_required | |
@get_unit_context('review') | |
def reject_qualitycheck(request, unit, checkid): | |
json = {} | |
json["udbid"] = unit.id | |
json["checkid"] = checkid | |
if request.POST.get('reject'): | |
try: | |
check = unit.qualitycheck_set.get(id=checkid) | |
check.false_positive = True | |
check.save() | |
# update timestamp | |
unit.save() | |
except ObjectDoesNotExist: | |
raise Http404 | |
response = jsonify(json) | |
return HttpResponse(response, mimetype="application/json") | |
@require_POST | |
@ajax_required | |
def ajax_remove_tag_from_store(request, tag_slug, store_pk): | |
if not check_permission('administrate', request): | |
raise PermissionDenied(_("You do not have rights to remove tags.")) | |
store = get_object_or_404(Store, pk=store_pk) | |
if tag_slug.startswith("goal-"): | |
goal = get_object_or_404(Goal, slug=tag_slug) | |
store.goals.remove(goal) | |
else: | |
tag = get_object_or_404(Tag, slug=tag_slug) | |
store.tags.remove(tag) | |
return HttpResponse(status=201) | |
def _add_tag(request, store, tag_like_object): | |
if isinstance(tag_like_object, Tag): | |
store.tags.add(tag_like_object) | |
else: | |
store.goals.add(tag_like_object) | |
context = { | |
'store_tags': store.tag_like_objects, | |
'path_obj': store, | |
'can_edit': check_permission('administrate', request), | |
} | |
response = render_to_response('store/xhr_tags_list.html', context, | |
RequestContext(request)) | |
response.status_code = 201 | |
return response | |
@require_POST | |
@ajax_required | |
def ajax_add_tag_to_store(request, store_pk): | |
"""Return an HTML snippet with the failed form or blank if valid.""" | |
if not check_permission('administrate', request): | |
raise PermissionDenied(_("You do not have rights to add tags.")) | |
store = get_object_or_404(Store, pk=store_pk) | |
add_tag_form = TagForm(request.POST) | |
if add_tag_form.is_valid(): | |
new_tag_like_object = add_tag_form.save() | |
return _add_tag(request, store, new_tag_like_object) | |
else: | |
# If the form is invalid, perhaps it is because the tag already exists, | |
# so check if the tag exists. | |
try: | |
criteria = { | |
'name': add_tag_form.data['name'], | |
'slug': add_tag_form.data['slug'], | |
} | |
if len(store.tags.filter(**criteria)) == 1: | |
# If the tag is already applied to the store then avoid | |
# reloading the page. | |
return HttpResponse(status=204) | |
elif len(store.goals.filter(**criteria)) == 1: | |
# If the goal is already applied to the store then avoid | |
# reloading the page. | |
return HttpResponse(status=204) | |
else: | |
# Else add the tag (or goal) to the store. | |
if criteria['name'].startswith("goal:"): | |
tag_like_object = Goal.objects.get(**criteria) | |
else: | |
tag_like_object = Tag.objects.get(**criteria) | |
return _add_tag(request, store, tag_like_object) | |
except Exception: | |
# If the form is invalid and the tag doesn't exist yet then display | |
# the form with the error messages. | |
context = { | |
'add_tag_form': add_tag_form, | |
'add_tag_action_url': reverse('pootle-store-ajax-add-tag', | |
args=[store.pk]) | |
} | |
return render_to_response('common/xhr_add_tag_form.html', context, | |
RequestContext(request)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment