Browse files

ActionLog page for Projects.

- There is a new page showing the past actions which affect a particular
  Project under '/projects/log/'.
- Currently access is limited to project maintainers.
- The changeset includes support for pagination, filtering and sorting for
  the results, which can be re-used by other applications.
- The Project Maintainer can see a link at the bottom of the History
  section to view the full log.
- Adds two new dependencies, `django-filter` and `django-sorting`.
* * *
Some more changes.
* * *
temp
  • Loading branch information...
1 parent 5665df3 commit 471aa583a80e22d0e4ad9b2203f40c41c1e2bfbe @glezos glezos committed Aug 18, 2009
View
20 docs/releases/0.8.txt
@@ -15,4 +15,22 @@ To be released:
This is Transifex's current development release. It might eat your babies.
-Transifex 0.7 is intended for development use.
+Transifex 0.8 is intended for development use.
+
+
+What's new in Transifex 0.8
+---------------------------
+
+The following major features were introduced in this release.
+
+
+Upgrading to 0.8
+----------------
+
+Dependencies
+::::::::::::
+
+The following dependencies were *added* in this version:
+
+- django-filter
+- django-sorting
View
6 setup.py
@@ -67,11 +67,13 @@ def run(self):
"Pygments >= 0.9",
"PIL == 1.1.6",
"contact_form >= 0.3", # hg 97559a887345 or newer
+ "django-authority",
+ "django-filter >= 0.1",
"django-notification >= 0.1.2",
"django-pagination >= 1.0.5",
- "tagging >= 0.3_pre",
- "django-authority",
"django-piston",
+ "django-sorting >= 0.1.2",
+ "tagging >= 0.3_pre",
"South >= 0.6-rc1",
"django-ajax-selects",
],
View
7 transifex/actionlog/filters.py
@@ -0,0 +1,7 @@
+import django_filters
+from actionlog.models import LogEntry
+
+class LogEntryFilter(django_filters.FilterSet):
+ class Meta:
+ model = LogEntry
+ fields = ['user', 'action_type']
View
29 transifex/actionlog/models.py
@@ -33,6 +33,12 @@ def _get_formatted_message(label, context):
return msg
+class LogEntryManager(models.Manager):
+ def by_object(self, obj):
+ """Return LogEntries for a related object."""
+ ctype = ContentType.objects.get_for_model(obj)
+ return self.filter(content_type__pk=ctype.pk, object_id=obj.pk)
+
class LogEntry(models.Model):
"""A Entry in an object's log."""
user = models.ForeignKey(User, blank=True, null=True,
@@ -47,6 +53,9 @@ class LogEntry(models.Model):
object_name = models.CharField(blank=True, max_length=200)
message = models.TextField(blank=True, null=True)
+ # Managers
+ objects = LogEntryManager()
+
class Meta:
verbose_name = _('log entry')
verbose_name_plural = _('log entries')
@@ -56,14 +65,28 @@ def __unicode__(self):
return u'%s.%s.%s' % (self.action_type, self.object_name, self.user)
def __repr__(self):
- return smart_unicode(self.action_time)
-
+ return smart_unicode("<LogEntry %d (%s)>" % (self.id,
+ self.action_type.label))
def save(self, *args, **kwargs):
"""Save the object in the database."""
if self.action_time is None:
self.action_time = datetime.datetime.now()
super(LogEntry, self).save(*args, **kwargs)
+
+ @property
+ def action_type_short(self):
+ """
+ Return a shortened, generalized version of an action type.
+
+ Useful for presenting an image signifying an action type. Example::
+
+ >>> print l.action_type
+ <NoticeType: project_component_added>
+ >>> print l.action_type_short
+ u'added'
+ """
+ return self.action_type.label.split('_')[-1]
def action_logging(user, object_list, action_type, message=None, context=None):
"""
@@ -79,7 +102,7 @@ def action_logging(user, object_list, action_type, message=None, context=None):
A message to be included at the actionlog. If no message is passed
it will try do render a message using the notice.html from the
notification application.
- contex:
+ context:
To render the message using the notification files, sometimes it is
necessary to pass some vars by using a context.
View
6 transifex/projects/permissions/__init__.py
@@ -15,6 +15,10 @@
('general', 'projects.delete_project'),
)
+pr_project_view_log = (
+ ('granular', 'project_perm.maintain'),
+)
+
pr_project_add_perm = (
('granular', 'project_perm.maintain'),
('general', 'authority.add_permission'),
@@ -69,4 +73,4 @@
('granular', 'project_perm.submit_file'),
('general', 'repowatch.add_watch'),
('general', 'repowatch.delete_watch'),
-)
+)
View
7 transifex/projects/urls.py
@@ -105,6 +105,13 @@
dict(queryset_or_model=Project, allow_empty=True,
template_object_name='project'),
name='project_tag_list'),
+ url(
+ regex = '^(?P<slug>[-\w]+)/log/$',
+ view = project_log,
+ name = 'project_log',
+ kwargs = {'queryset': Project.objects.all(),
+ 'template_object_name': 'project',
+ 'template_name': 'projects/project_log.html'},),
)
View
23 transifex/projects/views/project.py
@@ -6,9 +6,11 @@
from django.dispatch import Signal
from django.utils.translation import ugettext as _
from django.conf import settings
+from django.views.generic import list_detail
from django.contrib.auth.decorators import login_required
-from actionlog.models import action_logging
+from actionlog.models import action_logging, LogEntry
+from actionlog.filters import LogEntryFilter
from notification import models as notification
from projects.models import Project
from projects.forms import ProjectAccessSubForm, ProjectForm
@@ -104,6 +106,25 @@ def project_update(request, project_slug):
return _project_create_update(request, project_slug)
@login_required
+@one_perm_required_or_403(pr_project_view_log,
+ (Project, 'slug__exact', 'project_slug'))
+def project_log(request, *args, **kwargs):
+ """
+ Present a log of the latest actions on the project.
+
+ The view limits the results and uses filters to allow the user to even
+ further refine the set.
+ """
+ project = get_object_or_404(Project, slug=kwargs['slug'])
+ log_entries = LogEntry.objects.by_object(project)
+ f = LogEntryFilter(request.GET, queryset=log_entries)
+ # The template needs both these variables. The first is used in filtering,
+ # the second is used for pagination and sorting.
+ kwargs.setdefault('extra_context', {}).update({'f': f,
+ 'actionlog': f.qs})
+ return list_detail.object_detail(request, *args, **kwargs)
+
+@login_required
@one_perm_required_or_403(pr_project_delete,
(Project, 'slug__exact', 'project_slug'))
def project_delete(request, project_slug):
View
4 transifex/settings/50-project.conf
@@ -30,6 +30,7 @@ MIDDLEWARE_CLASSES = [
'django.middleware.locale.LocaleMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
+ 'django_sorting.middleware.SortingMiddleware',
'pagination.middleware.PaginationMiddleware',
]
@@ -50,7 +51,8 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.admindocs',
'notification',
- # OpenId support (needs middleware class above too)
+ 'django_filters',
+ 'django_sorting',
'south',
'tagging',
'pagination',
View
8 transifex/site_media/css/icons.css
@@ -44,19 +44,23 @@ input.i16 {
.i16.action_go { background-image: url('../images/icons/lightning_go.png'); }
.i16.actionlog { background-image: url('../images/icons/time.png'); }
.i16.admin { background-image: url('../images/icons/award_star_gold_1.png'); }
+.i16.added,
.i16.add { background-image: url('../images/icons/add.png'); }
.i16.allow_file { background-image: url('../images/icons/database_edit.png'); }
.i16.bell { background-image: url('../images/icons/bell.png'); }
.i16.branch { background-image: url('../images/icons/arrow_branch.png'); }
.i16.bug { background-image: url('../images/icons/bug.png'); }
+.i16.clock { background-image: url('../images/icons/clock.png'); }
.i16.cog { background-image: url('../images/icons/cog.png'); }
.i16.collection { background-image: url('../images/icons/basket.png'); }
.i16.component { background-image: url('../images/icons/brick.png'); }
.i16.compress { background-image: url('../images/icons/compress.png'); }
.i16.coordinators { background-image: url('../images/icons/user_suit.png'); }
+.i16.deleted,
.i16.delete { background-image: url('../images/icons/cross.png'); }
.i16.delete_circle { background-image: url('../images/icons/cancel.png'); }
.i16.drink { background-image: url('../images/icons/drink.png'); }
+.i16.changed,
.i16.edit { background-image: url('../images/icons/pencil.png'); }
.i16.edit_file { background-image: url('../images/icons/page_white_edit.png'); }
.i16.email { background-image: url('../images/icons/email.png'); }
@@ -76,12 +80,14 @@ input.i16 {
.i16.logout { background-image: url('../images/icons/door_out.png'); }
.i16.maintainer { background-image: url('../images/icons/user_gray.png'); }
.i16.next { background-image: url('../images/icons/next.png'); }
+.i16.error,
.i16.notice { background-image: url('../images/icons/exclamation.png'); }
.i16.language { background-image: url('../images/icons/comment.png'); }
.i16.link { background-image: url('../images/icons/world_link.png'); }
.i16.multifile { background-image: url('../images/icons/page_white_stack.png'); }
.i16.project { background-image: url('../images/icons/package.png'); }
.i16.previous { background-image: url('../images/icons/previous.png'); }
+.i16.date,
.i16.release { background-image: url('../images/icons/date.png'); }
.i16.release_date { background-image: url('../images/icons/date_go.png'); }
.i16.repository { background-image: url('../images/icons/drive_network.png'); }
@@ -91,12 +97,14 @@ input.i16 {
.i16.stats_edit { background-image: url('../images/icons/chart_bar_edit.png'); }
.i16.stop { background-image: url('../images/icons/delete.png'); }
.i16.submit { background-image: url('../images/icons/tick.png'); }
+.i16.submitted,
.i16.submit_file { background-image: url('../images/icons/page_white_put.png'); }
.i16.tag { background-image: url('../images/icons/tag_blue.png'); }
.i16.team { background-image: url('../images/icons/group.png'); }
.i16.team_join { background-image: url('../images/icons/group_add.png'); }
.i16.team_user_request { background-image: url('../images/icons/user_red.png'); }
.i16.team_request { background-image: url('../images/icons/group_gear.png'); }
+.i16.text { background-image: url('../images/icons/page_white_text.png'); }
.i16.tick { background-image: url('../images/icons/tick.png'); }
.i16.tick_circle { background-image: url('../images/icons/accept.png'); }
.i16.tip { background-image: url('../images/icons/lightbulb.png'); }
View
5 transifex/site_media/css/tablesorter.css
@@ -48,6 +48,11 @@ table.tablesorter td {
height: 24px;
}
+table.tablesorter.withair td {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
/* Pager support for client-side pagination. */
div.pager {
View
BIN transifex/site_media/images/icons/clock.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN transifex/site_media/images/icons/page_white_text.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
8 transifex/templates/projects/project_detail.html
@@ -112,16 +112,22 @@ <h2 class="name">{{ project.name }}</h2>
{% load tx_action_log %}
{% get_log 5 as action_log for_object project %}
{% if not action_log %}
+
<p>{% trans 'None available' %}</p>
+
{% else %}
+
<ul class="actionlist simple">
{% for entry in action_log %}
<li class="i16 actionlog">
{{ entry.message|safe }} by {{ entry.user }} {{ entry.action_time|timesince }} ago.
</li>
{% endfor %}
-
</ul>
+{% if perms.projects.change_project or is_maintainer %}
+ <p><span class="i16 date"><a href="{% url project_log slug=project.slug %}">{% trans "View complete action log" %}</a></span></p>
+{% endif %}
+
{% endif %}
{% endblock %}
View
70 transifex/templates/projects/project_log.html
@@ -0,0 +1,70 @@
+{% extends "projects/project_menu.html" %}
+{% load i18n %}
+{% load pagination_tags %}
+{% load sorting_tags %}
+{% load txcommontags %}
+
+{% block extra_head %}
+{# <link rel="alternate" type="application/rss+xml" title="RSS" href="{% url project_submission_feed param=project.slug %}" /> #}
+
+<link media="screen" href="{{ MEDIA_URL }}css/tablesorter.css" type="text/css" rel="stylesheet" />
+<script type="text/javascript" src="{{ MEDIA_URL }}js/tablesorted.js"></script>
+{% endblock %}
+
+{% block title %}{{ block.super }} | {% trans "Action log" %}{% endblock %}
+
+{% block breadcrumb %}{{ block.super }} &raquo; {% trans "Action log" %}{% endblock %}
+
+{% block content_main %}
+ <div class="obj_bigdetails">
+ <h2 class="name">{{ project.name }} &raquo; {% trans "Action log" %}</h2>
+
+{% autosort actionlog %}
+{% autopaginate actionlog 30 %}
+
+<fieldset style="margin: inherit auto; width: 50%;" class="compact">
+ <legend><span class="i16 filter">{% trans "Filter results" %}</span></legend>
+ <form action="" method="get">
+ <table class="definition">
+ {{ f.form.as_table }}
+ <tr><td></td><td><input type="submit" class="i16 submit" value="{% trans "Go" %}"/></td></tr>
+ </table>
+ </form>
+</fieldset>
+
+{% if not actionlog %}
+<p>{% trans 'None available' %}</p>
+{% else %}
+
+ <div class="pagination">{% paginate %}</div>
+
+ <table class="tablesorter compact withair">
+ <thead>
+ <tr>
+ <th><span class="i16 action">{% anchor action_type Action %}</span></th>
+ <th><span class="i16 user">{% anchor user User %}</span></th>
+ <th><span class="i16 clock">{% anchor action_time "Time" %}</span></th>
+ <th><span class="i16 text">{% trans "Description" %}</span></th>
+ </tr>
+ </thead>
+ <tbody>
+{% for entry in actionlog %}
+ <tr>
+ <td><span class="i16 {{ entry.action_type_short }}">{{ entry.action_type_short }}</span></td>
+ <td>{{ entry.user }}</td>
+ <td>{{ entry.action_time|timesince }}</td>
+ <td>{{ entry.message|safe }}</td>
+ </tr>
+{% endfor %}
+ </tbody>
+ </table>
+
+ <div class="pagination">{% paginate %}</div>
+
+{% endif %}
+
+</div>
+
+{% endblock %}
+
+{% block content_footer %}{% endblock %}
View
8 transifex/templates/projects/project_menu.html
@@ -55,6 +55,14 @@
</a>
</li>
+ {% if request.user.is_authenticated %}
+ <li class="ui-state-default ui-corner-top {% if project_log %}ui-tabs-selected ui-state-active{% endif %}">
+ <a id="tab-link-3" href="{% url project_log slug=project.slug %}">
+ <span>{% trans "Timeline" %}</span>
+ </a>
+ </li>
+ {% endif %}
+
</ul>
</div>
{% endif %}

0 comments on commit 471aa58

Please sign in to comment.