Repository: translate/pootle

Issue:
Display suggestion-related events in the timeline
We're missing suggestion-related events in the timeline: 'suggestion added', 'suggestion accepted' and 'suggestion rejected'. These can help understand the history of the unit edits better, especially when it comes to understanding scores and similarity.

Proposed format:

```
[ Author ]   <suggestion_text>
[ Avatar ]   Added suggestion
```

```
[Reviewer]   <suggestion_text>
[ Avatar ]   Rejected suggestion from <suggestion_author>
```

```
[Reviewer]   <suggestion_text>
[ Avatar ]   Accepted suggestion from <suggestion_author>
```



Discussion:

Resolving patch:
diff --git a/pootle/apps/pootle_statistics/models.py b/pootle/apps/pootle_statistics/models.py
--- a/pootle/apps/pootle_statistics/models.py
+++ b/pootle/apps/pootle_statistics/models.py
@@ -13,6 +13,7 @@
 from django.db import models
 from django.template.defaultfilters import truncatechars
 from django.utils.functional import cached_property
+from django.utils.html import format_html
 from django.utils.translation import ugettext_lazy as _
 
 from pootle.core.log import log, SCORE_CHANGED
@@ -49,6 +50,7 @@ class SubmissionTypes(object):
     # types that are stored in the DB
     EDIT_TYPES = [NORMAL, SYSTEM]
     CONTRIBUTION_TYPES = [NORMAL, SYSTEM, SUGG_ADD]
+    SUGGESTION_TYPES = [SUGG_ACCEPT, SUGG_ADD, SUGG_REJECT]
 
 
 #: Values for the 'field' field of Submission
@@ -247,6 +249,24 @@ def get_submission_info(self):
 
         return result
 
+    def get_suggestion_description(self):
+        """Returns a suggestion-related descriptive message for the submission.
+
+        If there's no suggestion activity linked with the submission, `None` is
+        returned instead.
+        """
+        if self.type not in SubmissionTypes.SUGGESTION_TYPES:
+            return None
+
+        sugg_user = self.suggestion.user
+        author = format_html('<a href="{}">{}</a>', sugg_user.get_absolute_url(),
+                                                    sugg_user.display_name)
+        return {
+            SubmissionTypes.SUGG_ADD: _('Added suggestion'),
+            SubmissionTypes.SUGG_ACCEPT: _('Accepted suggestion from %s' % author),
+            SubmissionTypes.SUGG_REJECT: _('Rejected suggestion from %s' % author),
+        }.get(self.type, None)
+
     def save(self, *args, **kwargs):
         super(Submission, self).save(*args, **kwargs)
 
diff --git a/pootle/apps/pootle_store/views.py b/pootle/apps/pootle_store/views.py
--- a/pootle/apps/pootle_store/views.py
+++ b/pootle/apps/pootle_store/views.py
@@ -17,6 +17,7 @@
 from django.http import Http404
 from django.shortcuts import redirect
 from django.template import loader, RequestContext
+from django.utils.safestring import mark_safe
 from django.utils.translation import to_locale, ugettext as _
 from django.utils.translation.trans_real import parse_accept_lang_header
 from django.utils import timezone
@@ -579,10 +580,15 @@ 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, SubmissionFields.NONE
-    ]).exclude(
+    timeline = Submission.objects.filter(
+        unit=unit,
+    ).filter(
+        Q(field__in=[
+            SubmissionFields.TARGET, SubmissionFields.STATE,
+            SubmissionFields.COMMENT, SubmissionFields.NONE
+        ]) |
+        Q(type__in=SubmissionTypes.SUGGESTION_TYPES)
+    ).exclude(
         field=SubmissionFields.COMMENT,
         creation_time=unit.commented_on
     ).order_by("id")
@@ -614,12 +620,18 @@ def timeline(request, unit):
 
             entry = {
                 'field': item.field,
-                'field_name': SubmissionFields.NAMES_MAP[item.field],
+                'field_name': SubmissionFields.NAMES_MAP.get(item.field, None),
+                'type': item.type,
             }
 
             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))]
+            elif item.suggestion:
+                entry.update({
+                    'suggestion_text': item.suggestion.target,
+                    'suggestion_description': mark_safe(item.get_suggestion_description()),
+                })
             elif item.quality_check:
                 check_name = item.quality_check.name
                 entry.update({


Test patch: