Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion feedback and accepting altered suggestions #4659

Merged
merged 26 commits into from
Apr 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
335baa2
Adjust suggestions UI for pluralform units
ta2-1 Apr 18, 2016
4060a85
Support rejecting/accepting suggestions with comments
ta2-1 Apr 18, 2016
350ac7f
Add suggestion feedback form React component
ta2-1 Apr 20, 2016
a99475c
Show suggestion feedback form
ta2-1 Apr 20, 2016
9250767
Add backend support for suggestion alterations via submit
ta2-1 Apr 19, 2016
e4a49a1
Close suggestion on click or esc button
ta2-1 Apr 19, 2016
96577ee
Prevent loss of unsaved changes in feedback form
ta2-1 Apr 19, 2016
64fd646
Show comments on accept/reject suggestions in timeline
ta2-1 Apr 18, 2016
c1b2c7d
Force cursor to pointer over collapsed suggestion
ta2-1 Apr 20, 2016
d7bf7a9
Use UnitForm for getting clean data for suggestion and comment
ta2-1 Apr 21, 2016
6726e3c
Get comment list via suggestion list (no GenericRelation used)
ta2-1 Apr 21, 2016
c053133
Add test for timeline with comments
ta2-1 Apr 21, 2016
526fc49
Set unit.state to TRANSLATED before altering accepted suggestion
ta2-1 Apr 21, 2016
4c3c615
Support rtl in suggestion altering area
ta2-1 Apr 21, 2016
3e8ab9a
Put altering of accepted suggestion in a separate timeline entry
ta2-1 Apr 21, 2016
c001c11
Add tests for accept/reject with comments
ta2-1 Apr 22, 2016
668d228
Avoid passing PTL.editor as parameter to component
ta2-1 Apr 26, 2016
ad3879e
Close suggestion before unit transitions
ta2-1 Apr 26, 2016
dfae55d
Cleanup: use DOM API instead of jQuery
ta2-1 Apr 26, 2016
2bd73b6
Cleanup: rename props accoding styleguide
ta2-1 Apr 26, 2016
22746b6
Use autoFocus attribute instead of direct call (autosize is supported…
ta2-1 Apr 26, 2016
ef12779
Avoid accessing to internal componet attributes
ta2-1 Apr 27, 2016
727c254
Use handlers instead of working methods
ta2-1 Apr 27, 2016
cedec4b
Unmount suggestion feedback component
ta2-1 Apr 27, 2016
dd4e1c4
Bind component event handlers
ta2-1 Apr 29, 2016
c099ab4
Force flushing of default django-cache
ta2-1 Apr 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 39 additions & 28 deletions pootle/apps/pootle_store/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from .form_fields import (
CategoryChoiceField, ISODateTimeField, MultipleArgsField,
CommaSeparatedCheckboxSelectMultiple)
from .models import Unit
from .models import Unit, Suggestion
from .util import FUZZY, OBSOLETE, TRANSLATED, UNTRANSLATED


Expand Down Expand Up @@ -259,6 +259,10 @@ class Meta(object):
)
similarity = forms.FloatField(required=False)
mt_similarity = forms.FloatField(required=False)
suggestion = forms.ModelChoiceField(
queryset=Suggestion.objects.all(),
required=False)
comment = forms.CharField(required=False)

def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
Expand All @@ -279,16 +283,42 @@ def clean_target_f(self):

return value

def clean_state(self):
def clean_similarity(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see the clean_similarity() and clean_mt_similarity() methods have been moved with no change/intent. If there was a cleanup intention please consider doing so separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They were not just moved.
clean_state() functionality was moved to clean() because clean_A() shouldn't depend on clean_B().

value = self.cleaned_data['similarity']

if 0 <= value <= 1 or value is None:
return value

raise forms.ValidationError(
_('Value of `similarity` should be in in the [0..1] range')
)

def clean_mt_similarity(self):
value = self.cleaned_data['mt_similarity']

if 0 <= value <= 1 or value is None:
return value

raise forms.ValidationError(
_('Value of `mt_similarity` should be in in the [0..1] range')
)

def clean(self):
old_state = self.instance.state # Integer
is_fuzzy = self.cleaned_data['state'] # Boolean
new_target = self.cleaned_data['target_f']

# If suggestion is provided set `old_state` should be `TRANSLATED`.
if self.cleaned_data['suggestion']:
old_state = TRANSLATED

if (self.request is not None and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for me putting and or other operators at the end of lines is not a matter of format but logic.

I know its not the pep8 spec, but these blocks of logic really do need to be understood when read, and i find the spec unreadable.

ignore if your really really want to 8/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't changed that line.
Can we add cleanup later if it is necessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies, for sure

not check_permission('administrate', self.request) and
is_fuzzy):
raise forms.ValidationError(_('Needs work flag must be '
'cleared'))
self.add_error('state',
forms.ValidationError(
_('Needs work flag must be '
'cleared')))

if new_target:
if old_state == UNTRANSLATED:
Expand Down Expand Up @@ -320,31 +350,12 @@ def clean_state(self):
self.updated_fields.append((SubmissionFields.STATE,
old_state, new_state))

return new_state

self.instance._state_updated = False

return old_state

def clean_similarity(self):
value = self.cleaned_data['similarity']

if 0 <= value <= 1 or value is None:
return value

raise forms.ValidationError(
_('Value of `similarity` should be in in the [0..1] range')
)

def clean_mt_similarity(self):
value = self.cleaned_data['mt_similarity']

if 0 <= value <= 1 or value is None:
return value
self.cleaned_data['state'] = new_state
else:
self.instance._state_updated = False
self.cleaned_data['state'] = old_state

raise forms.ValidationError(
_('Value of `mt_similarity` should be in in the [0..1] range')
)
return super(UnitForm, self).clean()

return UnitForm

Expand Down
20 changes: 20 additions & 0 deletions pootle/apps/pootle_store/migrations/0008_flush_django_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.core.management import call_command
from django.db import migrations


def flush_django_cache(apps, schema_editor):
call_command('flush_cache', '--django-cache')


class Migration(migrations.Migration):

dependencies = [
('pootle_store', '0007_case_sensitive_schema'),
]

operations = [
migrations.RunPython(flush_django_cache),
]
82 changes: 73 additions & 9 deletions pootle/apps/pootle_store/unit/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _

from pootle_comment import get_model as get_comment_model
from pootle_misc.checks import check_names
from pootle_statistics.models import (
Submission, SubmissionFields, SubmissionTypes)

from pootle_store.fields import to_python
from pootle_store.models import Suggestion
from pootle_store.util import STATES_MAP


Expand Down Expand Up @@ -60,24 +62,46 @@ def gravatar_url(self, size=80):

class SuggestionEvent(object):

def __init__(self, submission_type, username, full_name):
def __init__(self, submission_type, username, full_name, comment):
self.submission_type = submission_type
self.username = username
self.full_name = full_name
self.comment = comment

@cached_property
def user(self):
return DisplayUser(self.username, self.full_name)

Copy link
Member

@phlax phlax Apr 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im still finding this kinda hard to follow.

Can we put all of the statements as class attrs ie

Class SuggestionEvent(object):
   msg_accepted_suggestion = _(u'Accepted suggestion from %(author)s', params)
   msg_accepted_suggestion_comment = _(u'Accepted suggestion from %(author)s with comment: %(comment)s'

etc
then do away with description_dict, and just use normal conditionals

if self.submission_type == SubmissionTypes.SUGG_ADD:
   message = self.msg_add_suggestion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually im not sure if this will work properly in terms of gettext. just trying to think how to make this a bit tidier

@property
def description(self):
author_link = self.user.author_link
params = {
'author': self.user.author_link
}
sugg_accepted_desc = _(u'Accepted suggestion from %(author)s', params)
sugg_rejected_desc = _(u'Rejected suggestion from %(author)s', params)

if self.comment:
params.update({
'comment': format_html(u'<span class="comment">{}</span>',
self.comment),
})
sugg_accepted_desc = _(
u'Accepted suggestion from %(author)s '
u'with comment: %(comment)s',
params
)
sugg_rejected_desc = _(
u'Rejected suggestion from %(author)s '
u'with comment: %(comment)s',
params
)

description_dict = {
SubmissionTypes.SUGG_ADD: _(u'Added suggestion'),
SubmissionTypes.SUGG_ACCEPT: _(u'Accepted suggestion from %s',
author_link),
SubmissionTypes.SUGG_REJECT: _(u'Rejected suggestion from %s',
author_link)}
SubmissionTypes.SUGG_ACCEPT: sugg_accepted_desc,
SubmissionTypes.SUGG_REJECT: sugg_rejected_desc,
}

return description_dict.get(self.submission_type, None)


Expand Down Expand Up @@ -126,6 +150,10 @@ def suggestion_username(self):
def suggestion_target(self):
return self.values['suggestion__target_f']

@property
def suggestion_comment(self):
return self.values['comment']


class TimelineEntry(object):

Expand Down Expand Up @@ -165,11 +193,15 @@ def state_change_entry(self):

def suggestion_entry(self):
entry = self.entry_dict

suggestion_description = mark_safe(
SuggestionEvent(
self.submission.type,
self.submission.suggestion_username,
self.submission.suggestion_full_name).description)
self.submission.suggestion_full_name,
self.submission.suggestion_comment,
).description
)
entry.update(
{'suggestion_text': self.submission.suggestion_target,
'suggestion_description': suggestion_description})
Expand Down Expand Up @@ -219,6 +251,29 @@ def submissions(self):
creation_time=self.object.commented_on)
.order_by("id"))

@cached_property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not sure there is much point using a cached_property if we dont do list() on the result

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @phlax, it should be cached because we call it twice otherwise. but probs I misunderstood.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking about it you are right, i suppose i just wonder if we should turn it into a list

def submissions_values(self):
return list(self.submissions.values(*self.fields))

@property
def suggestion_ids(self):
return list(set([
x["suggestion_id"]
for x in self.submissions_values
if x["suggestion_id"]
]))

@cached_property
def comment_dict(self):
Comment = get_comment_model()
return dict([
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this comment_dict or similar

# we need convert `object_pk` because it is TextField
(int(x[0]), x[1])
for x in Comment.objects.for_model(Suggestion)
.filter(object_pk__in=self.suggestion_ids)
.values_list("object_pk", "comment")
])

def add_creation_entry(self, grouped_entries):
User = get_user_model()
has_creation_entry = (
Expand All @@ -238,8 +293,16 @@ def add_creation_entry(self, grouped_entries):
def get_grouped_entries(self):
grouped_entries = []
grouped_timeline = groupby(
self.submissions.values(*self.fields),
key=lambda x: ("%d\001%s" % (x['submitter_id'], x['creation_time'])))
self.submissions_values,
key=lambda item: "\001".join([
str(x) for x in
[
item['submitter_id'],
item['creation_time'],
item['suggestion_id'],
]
])
)
# Group by submitter id and creation_time because
# different submissions can have same creation time
for key, values in grouped_timeline:
Expand All @@ -257,4 +320,5 @@ def get_grouped_entries(self):
return grouped_entries

def get_entry(self, item):
item["comment"] = self.comment_dict.get(item["suggestion_id"])
return self.entry_class(ProxySubmission(item)).entry
38 changes: 37 additions & 1 deletion pootle/apps/pootle_store/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
# or later license. See the LICENSE file for a copy of the license and the
# AUTHORS file for copyright and authorship information.

import copy

from translate.lang import data

from django import forms
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import Http404
from django.http import Http404, QueryDict
from django.shortcuts import redirect
from django.template import RequestContext, loader
from django.utils import timezone
Expand All @@ -30,6 +32,7 @@
from pootle_app.models.directory import Directory
from pootle_app.models.permissions import (check_permission,
check_user_permission)
from pootle_comment.forms import UnsecuredCommentForm
from pootle_misc.util import ajax_required
from pootle_statistics.models import (Submission, SubmissionFields,
SubmissionTypes)
Expand Down Expand Up @@ -505,6 +508,7 @@ def submit(request, unit):

translation_project = request.translation_project
language = translation_project.language
old_unit = copy.copy(unit)

if unit.hasplural():
snplurals = len(unit.source.strings)
Expand All @@ -518,8 +522,23 @@ def submit(request, unit):
form = form_class(request.POST, instance=unit, request=request)

if form.is_valid():
suggestion = form.cleaned_data['suggestion']
if suggestion:
old_unit.accept_suggestion(suggestion,
request.translation_project, request.user)
if form.cleaned_data['comment']:
kwargs = dict(
comment=form.cleaned_data['comment'],
user=request.user,
)
comment_form = UnsecuredCommentForm(suggestion, kwargs)
if comment_form.is_valid():
comment_form.save()

if form.updated_fields:
for field, old_value, new_value in form.updated_fields:
if field == SubmissionFields.TARGET and suggestion:
old_value = str(suggestion.target_f)
sub = Submission(
creation_time=current_time,
translation_project=translation_project,
Expand Down Expand Up @@ -628,6 +647,15 @@ def reject_suggestion(request, unit, suggid):
raise PermissionDenied(_('Insufficient rights to access review mode.'))

unit.reject_suggestion(sugg, request.translation_project, request.user)
r_data = QueryDict(request.body)
if "comment" in r_data and r_data["comment"]:
kwargs = dict(
comment=r_data["comment"],
user=request.user,
)
comment_form = UnsecuredCommentForm(sugg, kwargs)
if comment_form.is_valid():
comment_form.save()

json['user_score'] = request.user.public_score

Expand All @@ -647,6 +675,14 @@ def accept_suggestion(request, unit, suggid):
raise Http404

unit.accept_suggestion(suggestion, request.translation_project, request.user)
if "comment" in request.POST and request.POST["comment"]:
kwargs = dict(
comment=request.POST["comment"],
user=request.user,
)
comment_form = UnsecuredCommentForm(suggestion, kwargs)
if comment_form.is_valid():
comment_form.save()

json['user_score'] = request.user.public_score
json['newtargets'] = [highlight_whitespace(target)
Expand Down
Loading