From 6e4ff7d016ca5e0367561284cc56be03a0712f29 Mon Sep 17 00:00:00 2001 From: James Socol Date: Thu, 10 Mar 2011 15:38:51 -0500 Subject: [PATCH] New method for storing and formatting actions. * Drop the `Activity` class and table, replace with `Action`. * And associated renaming. * Define the concept of an `ActionFormatter` and a base class. * Add a README. --- apps/activity/README.rst | 68 +++++++++++++++++++++++++++ apps/activity/__init__.py | 13 +++++ apps/activity/models.py | 33 +++++++++---- migrations/92-activity-formatters.sql | 29 ++++++++++++ 4 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 apps/activity/README.rst create mode 100644 migrations/92-activity-formatters.sql diff --git a/apps/activity/README.rst b/apps/activity/README.rst new file mode 100644 index 000000000..43c3fb82e --- /dev/null +++ b/apps/activity/README.rst @@ -0,0 +1,68 @@ +================ +Logging Activity +================ + +The **activity** app provides a way to log arbitrary activity for interested +users (c.f. your Github dashboard). This activity can appear on a user's +profile, on their personal dashboard, or other places. + + +Logging What Now? +================= + +Each bit of activity is represented in the database by an ``Action`` object. +It's linked to relevant users by a ``ManyToManyField``. To add a new action to +users' activity logs, create a new ``Action`` object and add the relevant users +to the ``action.users`` manager. + + +Formatting Actions +================== + +``Action`` objects require a **formatter** class that determines how to render +the action in the log. For example, a formatter for a forum reply might decide +to render the title of the action like this:: + + _('{user} replied to {thread}').format(user=, thread=) + +Formatters have access to the entire action object, so they can look at any +attached objects including, potentially, the creator (of the action), or the +relevant content object (a ``GenericForeignKey``). + +Formatters should probably subclass ``activity.ActionFormatter``, though that's +not strictly required at the moment. They need to accept an ``Action`` object +to their constructors and implement the following properties: + +``title``: + a title for the action +``content``: + text content, may be blank +``__unicode__()``: + probably the same as ``title`` + +An fuller example:: + + class ForumReplyFormatter(ActionFormatter): + def __init__(self, action): + self.action = action + self.post = action.content_object + title = _('{user} replied to {thread}') + self.title = title.format(user=action.creator, + thread=self.post.thread.title) + self.content = self.post.content[0:225] + + def __unicode__(self): + return self.title + + +Saving the Formatter +-------------------- + +When creating an ``Action``, you need to save a Python route to a formatter +class. For example, assuming the formatter above was in ``forums.tasks``, you +might store:: + + action = Action() + action.formatter = 'forums.tasks.ForumReplyFormatter' + +It should be a path you can import from the Django shell. diff --git a/apps/activity/__init__.py b/apps/activity/__init__.py index e69de29bb..061aee5d2 100644 --- a/apps/activity/__init__.py +++ b/apps/activity/__init__.py @@ -0,0 +1,13 @@ +class ActionFormatter(object): + """A base class for action formatters. + + Subclasses must implement all properties, optionally with @property.""" + + title = 'Something Happened!' + content = '' + + def __init__(self, action): + self.action = action + + def __unicode__(self): + return self.title diff --git a/apps/activity/models.py b/apps/activity/models.py index 8a225b279..e4b058ea3 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -8,29 +8,46 @@ from django.db import models from sumo.models import ModelBase -class Activity(ModelBase): +class Action(ModelBase): """Represents a unit of activity in a user's 'inbox.'""" - user = models.ForeignKey(User, related_name='activity_inbox') + users = models.ManyToManyField(User, related_name='action_inbox') creator = models.ForeignKey(User, null=True, blank=True, - related_name='activity') + related_name='actions') created = models.DateTimeField(default=datetime.now, db_index=True) - title = models.CharField(max_length=120) - content = models.CharField(max_length=400, blank=True) + data = models.CharField(max_length=400, blank=True) url = models.URLField(null=True, blank=True) content_type = models.ForeignKey(ContentType, null=True, blank=True) object_id = models.PositiveIntegerField(null=True, blank=True) content_object = generic.GenericForeignKey() + formatter_cls = models.CharField(max_length=200, + default='activity.ActionFormatter') class Meta(object): ordering = ['-created'] + @property + def formatter(self): + if not hasattr(self, 'fmt'): + mod, _, cls = self.formatter_cls.rpartition('.') + fmt_cls = getattr(__import__(mod, fromlist=[cls]), cls) + self.fmt = fmt_cls(self) + return self.fmt + def __unicode__(self): - return self.title + return unicode(self.formatter) + + @property + def title(self): + return self.formatter.title + + @property + def content(self): + return self.formatter.content def get_absolute_url(self): return self.url -class ActivityMixin(object): +class ActionMixin(object): """Add a GenericRelation to a model.""" - activity = generic.GenericRelation(Activity) + actions = generic.GenericRelation(Action) diff --git a/migrations/92-activity-formatters.sql b/migrations/92-activity-formatters.sql new file mode 100644 index 000000000..492a9821c --- /dev/null +++ b/migrations/92-activity-formatters.sql @@ -0,0 +1,29 @@ +DROP TABLE `activity_activity`; + +CREATE TABLE `activity_action_users` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `action_id` integer NOT NULL, + `user_id` integer NOT NULL, + UNIQUE (`action_id`, `user_id`) +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; + +ALTER TABLE `activity_action_users` ADD CONSTRAINT `user_id_refs_id_514426b8` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); + +CREATE TABLE `activity_action` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `creator_id` integer, + `created` datetime NOT NULL, + `data` varchar(400) NOT NULL, + `url` varchar(200), + `content_type_id` integer, + `object_id` integer UNSIGNED, + `formatter_cls` varchar(200) NOT NULL +) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci; + +ALTER TABLE `activity_action` ADD CONSTRAINT `creator_id_refs_id_4475b305` FOREIGN KEY (`creator_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `activity_action` ADD CONSTRAINT `content_type_id_refs_id_95f5c947` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +ALTER TABLE `activity_action_users` ADD CONSTRAINT `action_id_refs_id_52ad1f42` FOREIGN KEY (`action_id`) REFERENCES `activity_action` (`id`); +CREATE INDEX `activity_action_f97a5119` ON `activity_action` (`creator_id`); +CREATE INDEX `activity_action_3216ff68` ON `activity_action` (`created`); +CREATE INDEX `activity_action_e4470c6e` ON `activity_action` (`content_type_id`); +