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
+
+ {% for event in user_events %}
+ -
+ {% log_event event %}
+
+
+ {% endfor %}
+
+ {% if user_last_event and not user_events %}
{% trans 'Latest activity:' %}