diff --git a/pootle/apps/pootle_log/formatters.py b/pootle/apps/pootle_log/formatters.py new file mode 100644 index 00000000000..10f10499d88 --- /dev/null +++ b/pootle/apps/pootle_log/formatters.py @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from django.utils.functional import cached_property +from django.utils.html import format_html +from django.utils.safestring import mark_safe + +from pootle.core.delegate import event_formatters, profile +from pootle.i18n.gettext import ugettext as _ +from pootle_store.constants import STATES_MAP +from pootle_store.fields import to_python + + +class FormattedEvent(object): + + def __init__(self, event): + self.event = event + + @property + def event_types(self): + return { + 1: mark_safe( + '' + % _("Web update")), + 5: mark_safe( + '' + % _("File sync"))} + + @property + def avatar(self): + return self.user_profile.avatar + + @property + def timestamp(self): + return self.event.timestamp + + @cached_property + def user_profile(self): + return profile.get(self.user.__class__)(self.user) + + @property + def user(self): + return self.event.user + + @property + def formatted_unit(self): + return mark_safe('%s' % self.event.value.unit.id) + + @property + def change(self): + return "" + + @property + def formatted_revision(self): + return "" + + @property + def changes(self): + return ((self.message, self.change), ) + + @property + def revision(self): + return None + + @property + def css_class(self): + return "" + + +class FormattedSubmissionEvent(FormattedEvent): + + @property + def formatted_revision(self): + if self.revision: + return " (r%s)" % self.revision + else: + return "" + + @property + def method(self): + return self.event_types[self.event.value.type] + + @property + def revision(self): + return self.event.value.revision + + +class UnitCreatedEvent(FormattedEvent): + + @property + def action(self): + return _("Unit created") + + @property + def method(self): + if self.event.value.created_with: + return self.event_types[self.event.value.created_with] + return self.event_types[5] + + @property + def message(self): + return mark_safe( + _("Unit %s created" + % (self.formatted_unit))) + + +class UnitStateChangedEvent(FormattedSubmissionEvent): + + @property + def css_class(self): + return "evt-state-changed" + + @property + def action(self): + return _("State changed") + + @property + def message(self): + if self.event.value.revision: + revision = " (r%s)" % self.event.value.revision + else: + revision = "" + return mark_safe( + _("State changed for %s%s" + % (self.formatted_unit, + revision))) + + @property + def change(self): + message = format_html( + u"{} {}", + STATES_MAP[int(to_python(self.event.value.old_value))], + STATES_MAP[int(to_python(self.event.value.new_value))]) + return message + + +class UnitSourceUpdatedEvent(FormattedSubmissionEvent): + + @property + def action(self): + return _("Source updated") + + @property + def message(self): + return mark_safe( + _("Translation updated for %s at revision %s" + % (self.formatted_unit, self.event.value.revision))) + + @property + def change(self): + return self.event.value.new_value + + +class UnitTargetUpdatedEvent(FormattedSubmissionEvent): + + @property + def action(self): + return _("Translation updated") + + @property + def message(self): + return mark_safe( + _("%s for %s%s" + % (self.action, + self.formatted_unit, + self.formatted_revision))) + + @property + def change(self): + return self.event.value.new_value + + +class SuggestionAddedEvent(FormattedEvent): + + @property + def action(self): + return _("Suggestion added") + + @property + def message(self): + return mark_safe( + _("Suggestion (%s) added for %s" + % (self.event.value.id, self.formatted_unit))) + + @property + def method(self): + return self.event_types[1] + + @property + def change(self): + return self.event.value.target + + +class SuggestionAcceptedEvent(FormattedEvent): + + @property + def action(self): + return _("Suggestion (%s) accepted" % self.event.value.id) + + @property + def message(self): + return mark_safe( + _("%s for %s" + % (self.action, self.formatted_unit))) + + @property + def method(self): + return self.event_types[1] + + +class SuggestionRejectedEvent(FormattedEvent): + + @property + def action(self): + return _("Suggestion (%s) rejected" % self.event.value.id) + + @property + def message(self): + return mark_safe( + _("%s for %s" + % (self.action, self.formatted_unit))) + + @property + def method(self): + return self.event_types[1] + + +class CheckMutedEvent(FormattedEvent): + + @property + def message(self): + return mark_safe( + _('Check "%s" muted for %s' + % (self.event.value.quality_check.name, self.formatted_unit))) + + @property + def method(self): + return self.event_types[1] + + +class CheckUnmutedEvent(FormattedEvent): + + @property + def message(self): + return mark_safe( + _('Check "%s" unmuted for %s' + % (self.event.value.quality_check.name, self.formatted_unit))) + + @property + def method(self): + return self.event_types[1] + + +class CommentUpdatedEvent(FormattedEvent): + + @property + def message(self): + return mark_safe( + _('Comment updated for %s' % self.formatted_unit)) + + @property + def method(self): + return "" + + +class GroupedRejectionEvent(FormattedEvent): + + def __init__(self, context, events): + self.context = context + self.events = events + + @property + def action(self): + return _("%s suggestions rejected " % len(self.events)) + + @property + def formatted_unit(self): + formatted = ( + "(%s)" + % (", ".join(str(ev.value.unit.id) for ev in self.events))) + return formatted + + @property + def method(self): + return "" + + @property + def user(self): + for event in self.events: + return event.user + + @property + def changes(self): + return ((self.action, "")) + + +class GroupedCreationEvent(FormattedEvent): + + def __init__(self, context, events): + self.context = context + self.events = events + + @property + def action(self): + return _("%s units created" % len(self.events)) + + @property + def formatted_unit(self): + formatted = ( + "(%s)" + % (", ".join(str(ev.value.unit.id) for ev in self.events))) + return formatted + + @property + def method(self): + return "" + + @property + def user(self): + for event in self.events: + return event.user + + @property + def changes(self): + return ((self.action, "")) + + +class GroupedEvent(FormattedEvent): + + def __init__(self, context, events): + self.context = context + self.events = events + + @property + def formatters(self): + return event_formatters.gather(self.context.__class__) + + @cached_property + def event_group(self): + from itertools import groupby + groups = [] + creation_group = [] + reject_groups = {} + grouped_events = groupby( + self.events, + key=lambda x: (x.timestamp, x.unit)) + for (unit, timestamp), events in grouped_events: + events = list(events) + actions = [ev.action for ev in events] + event_group = [] + if "suggestion_created" in actions: + for event in events: + if event.action == "suggestion_created": + event_group.append( + self.formatters["suggestion_created"](event)) + if "suggestion_accepted" in actions: + for event in events: + if event.action == "suggestion_accepted": + event_group.append( + self.formatters["suggestion_accepted"](event)) + if "suggestion_rejected" in actions: + for event in events: + if event.action == "suggestion_rejected": + reject_groups[str(event.value.unit.id)] = reject_groups.get( + str(event.value.unit.id), []) + reject_groups[str(event.value.unit.id)].append(event) + for event in events: + if event.action == "unit_created": + creation_group.append(event) + for event in events: + if event.action == "source_updated": + event_group.append(self.formatters["source_updated"](event)) + for event in events: + if event.action == "target_updated": + event_group.append(self.formatters["target_updated"](event)) + for event in events: + if event.action == "state_changed": + event_group.append(self.formatters["state_changed"](event)) + groups.append(event_group) + if creation_group: + _creation_group = [] + if len(creation_group) == 1: + _creation_group.append( + self.formatters["unit_created"](event)) + else: + _creation_group.append( + self.formatters["grouped_creation"]( + self.context, creation_group)) + groups.append(_creation_group) + _reject_groups = [] + for k, v in reject_groups.items(): + _reject_groups.append( + self.formatters["grouped_rejection"](self.context, v)) + if _reject_groups: + groups.append(_reject_groups) + return groups + + @property + def message(self): + return mark_safe( + "" + % ", ".join(ev.action for ev in self.event_group)) + + @property + def change(self): + return mark_safe( + "" + % "".join( + "
  • %s
  • " + % ev.change for ev in self.event_group)) + + @property + def changes(self): + _changes = [] + for group in self.event_group: + if not group: + continue + revision = max(ev.revision or 0 for ev in group) + if revision: + revision = " (r%s)" % revision + else: + revision = "" + message = (", ".join(ev.action for ev in group)).lower().capitalize() + message = mark_safe( + "%s for %s%s" + % (message, + group[0].formatted_unit, revision)) + changes = "".join( + ('
  • %s
  • ' + % (ev.css_class, ev.change)) + for ev in group if ev.change) + if changes: + change = mark_safe("" % changes) + else: + change = "" + _changes.append((message, change)) + return _changes + + @property + def method(self): + for group in self.event_group: + if group: + return group[0].method + return "" + + @property + def avatar(self): + return self.user_profile.avatar + + @property + def timestamp(self): + for event in self.events: + if event.timestamp: + return event.timestamp.replace(second=0) + + @cached_property + def user_profile(self): + return profile.get(self.user.__class__)(self.user) + + @property + def user(self): + for event in self.events: + return ( + event.value.user + if event.action == "suggestion_accepted" + else event.user) + + @property + def committer_avatar(self): + return self.committer_profile.tiny_avatar + + @cached_property + def committer_profile(self): + return profile.get(self.committer.__class__)(self.committer) + + @property + def committer(self): + for event in self.events: + if event.action == "suggestion_accepted": + return event.user + + @property + def formatted_unit(self): + return mark_safe('%s' % self.event.value.unit.id) + + +base_formatters = dict( + group=GroupedEvent, + grouped_creation=GroupedCreationEvent, + grouped_rejection=GroupedRejectionEvent, + unit_created=UnitCreatedEvent, + state_changed=UnitStateChangedEvent, + source_updated=UnitSourceUpdatedEvent, + target_updated=UnitTargetUpdatedEvent, + suggestion_created=SuggestionAddedEvent, + suggestion_accepted=SuggestionAcceptedEvent, + suggestion_rejected=SuggestionRejectedEvent, + comment_updated=CommentUpdatedEvent, + check_muted=CheckMutedEvent, + check_unmuted=CheckUnmutedEvent) diff --git a/pootle/apps/pootle_log/templatetags/__init__.py b/pootle/apps/pootle_log/templatetags/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pootle/apps/pootle_log/templatetags/log_events.py b/pootle/apps/pootle_log/templatetags/log_events.py new file mode 100644 index 00000000000..5fd0333a01f --- /dev/null +++ b/pootle/apps/pootle_log/templatetags/log_events.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from django import template + + +register = template.Library() + + +@register.inclusion_tag("includes/log_event.html") +def log_event(event): + return dict(event=event) diff --git a/pootle/apps/pootle_log/utils.py b/pootle/apps/pootle_log/utils.py index d5fef1ca25a..f1e21b5665e 100644 --- a/pootle/apps/pootle_log/utils.py +++ b/pootle/apps/pootle_log/utils.py @@ -35,13 +35,6 @@ class ComparableLogEvent(BaseProxy): if x not in ["__lt__", "__gt__", "__call__"]) def __cmp__(self, other): - # valuable revisions are authoritative - if self.revision is not None and other.revision is not None: - if self.revision > other.revision: - return 1 - elif self.revision < other.revision: - return -1 - # timestamps have the next priority if self.timestamp and other.timestamp: if self.timestamp > other.timestamp: @@ -86,7 +79,7 @@ def submission_qs(self): @property def created_units(self): - return self.source_qs.select_related("unit", "created_by") + return self.source_qs.select_related("unit", "created_by", "unit__store") @property def suggestions(self): @@ -96,7 +89,7 @@ def suggestions(self): @property def submissions(self): return self.submission_qs.select_related( - "unit", "submitter", "unit__unit_source") + "unit", "submitter", "unit__unit_source", "quality_check") @cached_property def event(self): @@ -302,6 +295,25 @@ def get_events(self, **kwargs): for event in self.get_submission_events(**kwargs): yield event + def get_contributors(self, **kwargs): + event_sources = kwargs.pop( + "event_sources", + ("submission", "suggestion", "unit_source")) + users = set() + if "unit_source" in event_sources: + users |= set( + self.filtered_created_units().order_by().values_list( + "created_by", flat=True).distinct()) + if "suggestion" in event_sources: + users |= set( + self.filtered_suggestions().order_by().values_list( + "reviewer", flat=True).distinct()) + if "submission" in event_sources: + users |= set( + self.filtered_submissions().order_by().values_list( + "submitter", flat=True).distinct()) + return get_user_model().objects.filter(id__in=users) + class StoreLog(Log): include_meta = True diff --git a/pootle/apps/pootle_misc/templatetags/common_tags.py b/pootle/apps/pootle_misc/templatetags/common_tags.py index aad6e1f27a7..ecbb46bcbe6 100644 --- a/pootle/apps/pootle_misc/templatetags/common_tags.py +++ b/pootle/apps/pootle_misc/templatetags/common_tags.py @@ -29,11 +29,12 @@ def time_since(timestamp): @register.inclusion_tag('includes/avatar.html') -def avatar(username, email_hash, size): +def avatar(username, email_hash, size, css_class=""): # TODO: return sprite if its a system user if username == "system": - return dict(icon="icon-pootle") + return dict(icon="icon-pootle", css_class=css_class) return dict( + css_class=css_class, avatar_url=( 'https://secure.gravatar.com/avatar/%s?s=%d&d=mm' % (email_hash, size))) diff --git a/pootle/apps/pootle_profile/apps.py b/pootle/apps/pootle_profile/apps.py index 466fc6a6b91..79c89be8c88 100644 --- a/pootle/apps/pootle_profile/apps.py +++ b/pootle/apps/pootle_profile/apps.py @@ -18,3 +18,4 @@ class PootleProfileConfig(AppConfig): def ready(self): importlib.import_module("pootle_profile.getters") + importlib.import_module("pootle_profile.providers") diff --git a/pootle/apps/pootle_profile/providers.py b/pootle/apps/pootle_profile/providers.py new file mode 100644 index 00000000000..30ff1909a07 --- /dev/null +++ b/pootle/apps/pootle_profile/providers.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from django.contrib.auth import get_user_model + +from pootle.core.delegate import event_formatters +from pootle.core.plugin import provider +from pootle_log.formatters import base_formatters + + +@provider(event_formatters, sender=get_user_model()) +def gather_user_event_formatters(**kwargs_): + return base_formatters diff --git a/pootle/apps/pootle_profile/templatetags/profile_tags.py b/pootle/apps/pootle_profile/templatetags/profile_tags.py index b789c5d439c..11805f6f524 100644 --- a/pootle/apps/pootle_profile/templatetags/profile_tags.py +++ b/pootle/apps/pootle_profile/templatetags/profile_tags.py @@ -146,6 +146,8 @@ def profile_activity(profile, request_lang=None): context = dict(profile=profile) if profile.user.is_meta: return context - context["user_last_event"] = ( - context["profile"].user.last_event(locale=request_lang)) + context["user_events"] = context["profile"].get_events(n=5) + if not context["user_events"]: + context["user_last_event"] = ( + context["profile"].user.last_event(locale=request_lang)) return context diff --git a/pootle/apps/pootle_profile/utils.py b/pootle/apps/pootle_profile/utils.py index d07be599231..b527de850da 100644 --- a/pootle/apps/pootle_profile/utils.py +++ b/pootle/apps/pootle_profile/utils.py @@ -7,12 +7,13 @@ # AUTHORS file for copyright and authorship information. from datetime import timedelta +from itertools import groupby from django.utils import timezone from django.utils.functional import cached_property from pootle.core.delegate import ( - comparable_event, log, membership, scores, site_languages) + comparable_event, event_formatters, log, membership, scores, site_languages) from pootle.core.utils.templates import render_as_template from pootle.i18n.gettext import ugettext_lazy as _ @@ -30,6 +31,14 @@ def avatar(self): username=self.user.username, email_hash=self.user.email_hash)) + @cached_property + def tiny_avatar(self): + return render_as_template( + "{% load common_tags %}{% avatar username email_hash 13 %}", + context=dict( + username=self.user.username, + email_hash=self.user.email_hash)) + @cached_property def log(self): return log.get(self.user.__class__)(self.user) @@ -52,13 +61,23 @@ def display_name(self): def get_events(self, start=None, n=None): sortable = comparable_event.get(self.log.__class__) start = start or (timezone.now() - timedelta(days=30)) - events = sorted( - sortable(ev) - for ev - in self.log.get_events(start=start)) + events = groupby( + sorted( + sortable(ev) + for ev + in self.log.get_events(start=start)), + lambda event: event.timestamp.replace(second=0)) + _events = [] + formatters = event_formatters.gather(self.user.__class__) + for timestamp, evts in events: + evs = list(evts) + if len(evs) == 1: + _events.append(formatters.get(evs[0].action)(evs[0])) + else: + _events.append(formatters.get("group")(self.user, evs)) if n is not None: - events = events[-n:] - return reversed(events) + _events = _events[-n:] + return reversed(_events) class UserMembership(object): diff --git a/pootle/apps/pootle_store/formatters.py b/pootle/apps/pootle_store/formatters.py new file mode 100644 index 00000000000..ad34f30dee8 --- /dev/null +++ b/pootle/apps/pootle_store/formatters.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from pootle.i18n.gettext import ugettext as _ +from pootle_log.formatters import ( + FormattedEvent, FormattedSubmissionEvent, UnitCreatedEvent) + + +class StoreUnitCreatedEvent(UnitCreatedEvent): + pass + + +class StoreUnitStateChangedEvent(FormattedSubmissionEvent): + + @property + def message(self): + return _("State changed") + + +class StoreUnitTargetUpdatedEvent(FormattedSubmissionEvent): + + @property + def message(self): + return _("Translation updated") + + +class StoreSuggestionAddedEvent(FormattedEvent): + + @property + def message(self): + return _("Suggestion added") + + @property + def method(self): + return "" + + +class StoreSuggestionAcceptedEvent(FormattedEvent): + + @property + def message(self): + return _("Suggestion accepted") + + @property + def method(self): + return "" + + +class StoreSuggestionRejectedEvent(FormattedEvent): + + @property + def message(self): + return _("Suggestion rejected") + + @property + def method(self): + return "" diff --git a/pootle/apps/pootle_store/panels.py b/pootle/apps/pootle_store/panels.py new file mode 100644 index 00000000000..87421b52546 --- /dev/null +++ b/pootle/apps/pootle_store/panels.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Pootle contributors. +# +# This file is a part of the Pootle project. It is distributed under the GPL3 +# or later license. See the LICENSE file for a copy of the license and the +# AUTHORS file for copyright and authorship information. + +from datetime import timedelta +from itertools import groupby + +from django.utils import timezone + +from pootle.core.delegate import event_formatters, log, profile +from pootle.core.views.panels import Panel +from pootle_log.utils import ComparableLogEvent + + +class StoreActivityPanel(Panel): + template_name = "browser/includes/store_activity.html" + panel_name = "store-activity" + + def get_context_data(self): + ctx = {} + formatters = event_formatters.gather(self.view.object.__class__) + ctx["contributors"] = set() + start = timezone.now() - timedelta(days=30) + # start = None + event_log = log.get(self.view.object.__class__)(self.view.object) + events = groupby( + sorted( + ComparableLogEvent(ev) + for ev + in event_log.get_events(start=start)), + lambda event: ( + event.timestamp and event.timestamp.replace(second=0), + (event.value.user + if event.action == "suggestion_accepted" + else event.user))) + _events = [] + for timestamp, evts in events: + evs = list(evts) + if len(evs) == 1: + evt = formatters.get(evs[0].action)(evs[0]) + else: + evt = formatters.get("group")(self.view.object, evs) + _events.append(evt) + ctx["contributors"] = [ + profile.get(contrib.__class__)(contrib) + for contrib + in event_log.get_contributors()] + n = 10 + if n is not None: + _events = _events[-n:] + ctx["events"] = reversed(_events) + return ctx diff --git a/pootle/apps/pootle_store/providers.py b/pootle/apps/pootle_store/providers.py index b59a41408cf..6c0af82e881 100644 --- a/pootle/apps/pootle_store/providers.py +++ b/pootle/apps/pootle_store/providers.py @@ -7,13 +7,17 @@ # AUTHORS file for copyright and authorship information. from pootle.core.delegate import ( - event_formatters, format_diffs, format_syncers, format_updaters, unitid) + event_formatters, format_diffs, format_syncers, format_updaters, + panels, unitid) from pootle.core.plugin import provider +from pootle_log.formatters import base_formatters from pootle_log.utils import LogEvent from pootle_store.unit import timeline +from pootle_translationproject.views import TPBrowseStoreView from .diff import DiffableStore -from .models import Unit +from .models import Store, Unit +from .panels import StoreActivityPanel from .syncer import StoreSyncer from .updater import StoreUpdater from .utils import DefaultUnitid @@ -52,3 +56,13 @@ def gather_event_formatters(**kwargs_): comment_updated=timeline.CommentUpdatedEvent, check_muted=timeline.CheckMutedEvent, check_unmuted=timeline.CheckUnmutedEvent) + + +@provider(panels, sender=TPBrowseStoreView) +def activity_panel_provider(**kwargs_): + return dict(activity=StoreActivityPanel) + + +@provider(event_formatters, sender=Store) +def gather_stores_event_formatters(**kwargs_): + return base_formatters diff --git a/pootle/apps/pootle_translationproject/views.py b/pootle/apps/pootle_translationproject/views.py index 614008908d9..3e71a370cff 100644 --- a/pootle/apps/pootle_translationproject/views.py +++ b/pootle/apps/pootle_translationproject/views.py @@ -215,7 +215,6 @@ class TPStoreMixin(TPMixin): browse_url_path = "pootle-tp-store-browse" translate_url_path = "pootle-tp-store-translate" is_store = True - panels = () @property def permission_context(self): @@ -292,10 +291,11 @@ def score_context(self): class TPBrowseStoreView(TPStoreMixin, TPBrowseBaseView): disabled_items = False + panel_names = ("activity", ) @property def cache_key(self): - return "" + return self.object.pootle_path class TPBrowseView(TPDirectoryMixin, TPBrowseBaseView): diff --git a/pootle/static/css/style.css b/pootle/static/css/style.css index 80d08251986..a84511f929a 100644 --- a/pootle/static/css/style.css +++ b/pootle/static/css/style.css @@ -2061,7 +2061,7 @@ html[dir="rtl"] #tabs li { display: inline-block; overflow: hidden; - border-radius: 50%; + border-radius: 10%; } .avatar-system @@ -2619,3 +2619,10 @@ form.formtable tfoot .formtable-actions td float: right; color: #c30; } + +.evt-state-changed +{ + font-style: italic; + color: #777; + font-size: 0.9em; +} diff --git a/pootle/templates/browser/includes/store_activity.html b/pootle/templates/browser/includes/store_activity.html new file mode 100644 index 00000000000..3a2da58bed5 --- /dev/null +++ b/pootle/templates/browser/includes/store_activity.html @@ -0,0 +1,26 @@ +{% load log_events %} + +
    +
    +

    Contributors

    +
      + {% for contributor in contributors %} +
    • + {{ contributor.avatar }} + {{ contributor.display_name }} +
    • + {% endfor %} +
    +
    +
    +

    Recent activity

    +
      + {% for event in events %} +
    • + {% log_event event %} +
      +
    • + {% endfor %} +
    +
    +
    diff --git a/pootle/templates/browser/index.html b/pootle/templates/browser/index.html index 6f2ea43a1cc..9052fd9bff3 100644 --- a/pootle/templates/browser/index.html +++ b/pootle/templates/browser/index.html @@ -166,7 +166,6 @@

    {% trans "Translations" %}

    pootlePath: "{{ pootle_path }}", hasDisabledItems: {{ has_disabled|yesno:"true,false" }}, isAdmin: {{ has_admin_access|yesno:"true,false" }}, - isInitiallyExpanded: {{ is_store|yesno:"true,false" }}, topContributorsData: {{ top_scorers|to_js }}, uiLocaleDir: '{{ LANGUAGE_BIDI|yesno:"rtl,ltr" }}', }); diff --git a/pootle/templates/includes/avatar.html b/pootle/templates/includes/avatar.html index 098f785b4f8..2924a6680a0 100644 --- a/pootle/templates/includes/avatar.html +++ b/pootle/templates/includes/avatar.html @@ -1,5 +1,5 @@ {% if avatar_url %} - + {% else %} - + {% endif %} diff --git a/pootle/templates/includes/log_event.html b/pootle/templates/includes/log_event.html new file mode 100644 index 00000000000..73d9164e72f --- /dev/null +++ b/pootle/templates/includes/log_event.html @@ -0,0 +1,24 @@ +
    + {{ event.avatar }} + {% if event.committer %} + {{ event.committer_avatar }} + {% endif %} +
    +
    +
    + {% if event.timestamp %}{{ event.timestamp }}{% endif %} + {{ event.method }} +
    + {% for message, change in event.changes %} +
    +
    + {{ message }} +
    + {% if change %} +
    + {{ change }} +
    + {% endif %} +
    + {% endfor %} +
    diff --git a/pootle/templates/user/includes/profile_activity.html b/pootle/templates/user/includes/profile_activity.html index ae711bff749..d73828945e7 100644 --- a/pootle/templates/user/includes/profile_activity.html +++ b/pootle/templates/user/includes/profile_activity.html @@ -1,6 +1,15 @@ -{% load i18n %} +{% load i18n log_events %}
    - {% if user_last_event %} +

    Recent activity

    + + {% if user_last_event and not user_events %}

    {% trans 'Latest activity:' %}