Skip to content

Instantly share code, notes, and snippets.

@alex-silva
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alex-silva/40313734b9f1cd37f204 to your computer and use it in GitHub Desktop.
Save alex-silva/40313734b9f1cd37f204 to your computer and use it in GitHub Desktop.
#!/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
{% 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)' %}">&harr; {% trans "Submit" %}</a></div>
<div class="submit"><a href="#" title="{% trans 'Switch to suggestion mode (Ctrl+Shift+Space)' %}">&harr; {% 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 }}">&nbsp;</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="&#8207;">RLM</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert LRM (Left-To-Right Mark) into the editor' %}"
data-entity="&#8206;">LRM</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert RLE (Right-To-Left Embedding) into the editor' %}"
data-entity="&#8235;">RLE</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert LRE (Left-To-Right Embedding) into the editor' %}"
data-entity="&#8234;">LRE</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert RLO (Right-To-Left Override) into the editor' %}"
data-entity="&#8238;">RLO</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert LRO (Left-To-Right Override) into the editor' %}"
data-entity="&#8237;">LRO</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert PDF (Pop Directional Format ) into the editor' %}"
data-entity="&#8236;">PDF</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert ZWJ (Zero Width Joiner) into the editor' %}"
data-entity="&#8205;">ZWJ</a>
<a class="editor-specialchar js-editor-copytext"
title="{% trans 'Insert ZWNJ (Zero Width Non Joiner) into the editor' %}"
data-entity="&#8204;">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 %}
#!/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
#!/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)
#!/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'),
)
#!/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