├── README.md
├── setup.py
└── subscription
├── __init__.py
├── backends.py
├── base.py
├── client.py
├── cluster.py
├── context_processors.py
├── emitter.py
├── management
├── __init__.py
└── commands
│ ├── __init__.py
│ └── delete_empty_subscriptions.py
├── migrations
├── 0001_initial.py
└── __init__.py
├── models.py
├── south_migrations
├── 0002_remove_dupes.py
├── 0003_auto__add_unique_subscription_user_content_type_object_id.py
├── 0004_initial.py
├── 0005_simpler_streams.py
├── 0006_limit_to_200.py
└── __init__.py
├── stream.py
├── templates
└── comments
│ └── email_comment_template.html
├── templatetags
├── __init__.py
└── subscription_tags.py
├── urls.py
└── views.py
/README.md:
--------------------------------------------------------------------------------
1 | # Adopted Spec Format
2 |
3 | http://activitystrea.ms/head/json-activity.html
4 |
5 | # Subscribing
6 |
7 | Subscription.objects.subscribe(user, content_object) # Subscribes a user
8 |
9 | # Emitting
10 |
11 | ## Just one person, but not if its the author of the comment
12 |
13 | Subscription.objects.to(comment_obj.content_object.user).not_to(comment_obj.user).emit("comment.create", comment_spec)
14 |
15 | ## All subscribers of the content objects except the author of the comment
16 |
17 | Subscription.objects.of(comment_obj).not_to(comment_obj.user).emit(
18 | "comment.create", comment_obj,
19 | emitter_class=ModelEmitter)
20 |
21 | # Settings
22 |
23 | The args/kwargs to emit() are more or less shuttled straight to the SUBSCRIPTION_BACKEND(s),
24 | which is a dict in your settings.py like:
25 |
26 | SUBSCRIPTION_BACKENDS = {
27 | 'email': 'myproject.subscription_backends.Email',
28 | 'redis': 'myproject.subscription_backends.Redis',
29 | }
30 |
31 | SUBSCRIPTION_ACTSTREAM_PROPERTIES = [
32 | 'contentOwner'
33 | ]
34 | # Use this if you wanna say fuckit to the 'official' activitystream properties
35 |
36 |
37 | # Writing a backend
38 |
39 | You can subclass subscription.backends.BaseBackend. Right now the options are:
40 |
41 | SomeBackend.emit(recipient, 'some.verb', **activity_stream_spec_plus_whatever)
42 |
43 | * recipient (auth.User)
44 | * verb
45 | * activity stream spec attrs (published, target, object, actor)
46 | * **kwargs - Passed onto your backend subclass in case you need more info
47 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup, find_packages
4 |
5 | setup(
6 | name='django-subscription',
7 | version="0.4",
8 | author='Steve Yeago',
9 | author_email='yeago999@gmail.com',
10 | description='Managing subscriptions in Django',
11 | url='http://github.com/yeago/django-subscription',
12 | packages=find_packages(),
13 | include_package_data=True,
14 | classifiers=[
15 | "Framework :: Django",
16 | "Intended Audience :: Developers",
17 | "Intended Audience :: System Administrators",
18 | "Operating System :: OS Independent",
19 | "Topic :: Software Development"
20 | ],
21 | )
22 |
--------------------------------------------------------------------------------
/subscription/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/__init__.py
--------------------------------------------------------------------------------
/subscription/backends.py:
--------------------------------------------------------------------------------
1 | import time
2 | import datetime
3 | import json
4 | from django.core.mail import send_mail
5 | from django.contrib.contenttypes.models import ContentType
6 | from .client import get_cache_client
7 | from .cluster import cluster_specs
8 |
9 | from subscription.models import Subscription
10 | from django.conf import settings
11 |
12 | DEFAULT_ACTSTREAM_PROPERTIES = [
13 | 'published',
14 | 'actor',
15 | 'target',
16 | 'object',
17 | ]
18 |
19 | class BaseBackend(object):
20 | def __call__(obj, *args, **kwargs):
21 | return obj(*args, **kwargs)
22 |
23 | def __init__(self, user, verb, emitter_class=None, spec=None, **kwargs):
24 | """
25 | Verb, Spec: http://activitystrea.ms/head/json-activity.html
26 | - **kwargs - Maybe you wrote a backend that wants more stuff than the above!!
27 |
28 | CAREFUL: If you send a typo-kwarg it will just be sent to emit(), so no error will raise =(
29 | """
30 | spec = spec or {}
31 | spec['verb'] = verb
32 | for prop in DEFAULT_ACTSTREAM_PROPERTIES:
33 | if kwargs.get(prop):
34 | spec[prop] = kwargs[prop]
35 |
36 | if hasattr(settings, 'SUBSCRIPTION_ACTSTREAM_PROPERTIES'):
37 | ## The user wanted to add more things to the spec
38 | for prop in settings.SUBSCRIPTION_ACTSTREAM_PROPERTIES:
39 | if kwargs.get(prop):
40 | spec[prop] = kwargs[prop]
41 |
42 |
43 | if emitter_class:
44 | emitter = emitter_class(spec)
45 | for prop in DEFAULT_ACTSTREAM_PROPERTIES:
46 | if getattr(emitter, prop, None):
47 | spec[prop] = getattr(emitter, prop)
48 |
49 | self.kwargs = kwargs
50 | cluster_specs([spec]) # Try it out. If shit goes wrong it wasn't meant to be.
51 | self.emit(user, spec, **kwargs)
52 |
53 | def emit(self, user, spec, **kwargs):
54 | raise NotImplementedError("Override this!")
55 |
56 |
57 | class UserStream(BaseBackend):
58 | def emit(self, user, spec, **kwargs):
59 | conn = get_cache_client()
60 | if not spec.get("published"):
61 | spec['published'] = int(time.mktime(datetime.datetime.now().timetuple()))
62 | conn.lpush("actstream::%s" % user.pk, json.dumps(spec))
63 |
64 |
65 | class SimpleEmailBackend(BaseBackend):
66 | def emit(self, user, text, **kwargs):
67 | if not user.email:
68 | return
69 |
70 | send_mail(self.get_subject(),text,None,[user.email])
71 |
72 | def get_subject(self):
73 | return "Here's a subject!"
74 |
--------------------------------------------------------------------------------
/subscription/base.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import ImproperlyConfigured
2 | from django.conf import settings
3 |
4 | try:
5 | from django.utils.module_loading import import_by_path
6 | except ImportError:
7 | from importlib import import_module
8 |
9 | def import_by_path(path):
10 | i = path.rfind('.')
11 | module, attr = path[:i], path[i + 1:]
12 | try:
13 | mod = import_module(module)
14 | except ImportError as e:
15 | raise ImproperlyConfigured('Error importing subscription backend %s: "%s"' % (path, e))
16 | except ValueError as e:
17 | raise ImproperlyConfigured('Error importing subscription backends. Is SUBSCRIPTION_BACKENDS a correctly defined dictionary?')
18 | try:
19 | return getattr(mod, attr)
20 | except AttributeError:
21 | raise ImproperlyConfigured('Module "%s" does not define a "%s" subscription backend' % (module, attr))
22 |
23 |
24 | def get_backends():
25 | backends = {}
26 | for backend_name, backend_path in settings.SUBSCRIPTION_BACKENDS.items():
27 | backends[backend_name] = import_by_path(backend_path)
28 | if not backends:
29 | raise ImproperlyConfigured('No subscription backends have been defined. Does SUBSCRIPTION_BACKENDS contain anything?')
30 | return backends
31 |
32 |
33 | def get_profile(user):
34 | try:
35 | return getattr(user, settings.SUBSCRIPTION_USERPROFILE)
36 | except AttributeError:
37 | raise ImproperlyConfigured('Please set the SUBSCRIPTION_USERPROFILE setting with the name of your userprofile')
38 |
--------------------------------------------------------------------------------
/subscription/client.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | try:
3 | from redis import Redis as RedisBase
4 | except ImportError:
5 | class RedisBase(object):
6 | pass
7 |
8 | class Redis(RedisBase):
9 | def __init__(self, **kwargs):
10 | kwargs = kwargs or {}
11 | if not 'port' in kwargs:
12 | kwargs['port'] = settings.REDIS_PORT
13 | if not 'host' in kwargs:
14 | kwargs['host'] = settings.REDIS_HOST
15 | if not 'db' in kwargs:
16 | kwargs['db'] = settings.REDIS_DB
17 | if getattr(settings, 'REDIS_PASSWORD', None):
18 | kwargs['password'] = settings.REDIS_PASSWORD
19 | super(Redis, self).__init__(**kwargs)
20 |
21 | def get_cache_client(**kwargs):
22 | return Redis(**kwargs)
23 |
--------------------------------------------------------------------------------
/subscription/cluster.py:
--------------------------------------------------------------------------------
1 | from django.template.defaultfilters import pluralize
2 | from django.conf import settings
3 |
4 |
5 | def render_actors(actors):
6 | """
7 | Gives the following experience
8 |
9 | 1 actor - SomeBody commented on X
10 | 2 actors - SomeBody and AnotherPerson commented on X
11 | >2 actors - SomeBody, AnotherPerson and 3 others commented on X
12 | """
13 | # http://stackoverflow.com/questions/11092511/python-list-of-unique-dictionaries
14 | unique_actors = map(dict, set(tuple(sorted(d.items())) for d in actors))
15 | # But we need them in the original order since its timestamped
16 | unique_redux = []
17 | for actor in actors:
18 | for unique in unique_actors:
19 | if unique == actor and unique not in unique_redux:
20 | unique_redux.append(unique)
21 | break
22 | actors = unique_redux
23 | join_on = ", "
24 | if len(actors) == 2:
25 | join_on = " and "
26 | string = join_on.join(i['displayName'] for i in actors[:2])
27 | if len(actors) > 2:
28 | string = "%s and %s other%s" % (string, len(actors) - 2, pluralize(len(actors) - 2))
29 | return string
30 |
31 |
32 | def cluster_specs(specs):
33 | clusters = {}
34 |
35 | for spec in specs:
36 | try:
37 | object_id = int(spec['target']['objectId'])
38 | except ValueError:
39 | object_id = spec['target']['objectId']
40 | key = (spec['target']['objectType'], object_id, spec['verb'])
41 | if key not in clusters:
42 | clusters[key] = []
43 | clusters[key].append(spec)
44 | return clusters
45 |
46 |
47 | def render_clusters(specs):
48 | for cluster, items in specs.items():
49 | items = sorted(items, key=lambda x: x['published'], reverse=True)
50 | formatting = {}
51 | if cluster[2] in settings.NON_CLUSTER_SUBSCRIPTION_VERBS:
52 | for item in items:
53 | if item.get('actor'):
54 | formatting['actor'] = item['actor'].get('displayName') or ''
55 | if item.get('target'):
56 | formatting['target'] = item['target'].get('displayName') or ''
57 | try:
58 | yield item['published'], settings.SUBSCRIPTION_VERB_RENDER_MAP[item['verb']] % formatting
59 | except KeyError:
60 | continue
61 | else:
62 | try:
63 | formatting['actor'] = render_actors([i['actor'] for i in items if i['actor'].get('displayName')])
64 | except KeyError:
65 | continue
66 | formatting['target'] = items[0]['target']['displayName']
67 | verbage = settings.SUBSCRIPTION_VERB_RENDER_MAP[items[0]['verb']] % formatting
68 | yield max(i["published"] for i in items), verbage
69 |
--------------------------------------------------------------------------------
/subscription/context_processors.py:
--------------------------------------------------------------------------------
1 | import time
2 | from .stream import user_stream
3 | from .base import get_profile
4 |
5 |
6 | def get_notifications(request):
7 | if not request.user.is_authenticated():
8 | return {}
9 | """
10 | If we have 'undelivered' items, we deliver them to the unacknowledged list
11 | """
12 | stream = user_stream(request.user)
13 |
14 | last_ack = get_profile(request.user).get_stream_acknowledged()
15 | unacknowledged = user_stream(request.user, newer_than=last_ack)
16 |
17 | return {'notifications': stream,
18 | 'notifications_unacknowledged': unacknowledged}
19 |
--------------------------------------------------------------------------------
/subscription/emitter.py:
--------------------------------------------------------------------------------
1 | import time
2 | from django.db.models import Model
3 | from django.contrib.contenttypes.models import ContentType
4 | from django.utils.html import strip_tags
5 |
6 | def model_to_spec(obj):
7 | spec = {
8 | 'objectId': obj.pk,
9 | 'objectType': ContentType.objects.get_for_model(obj).pk,
10 | 'displayName': u"%s" % obj,
11 | }
12 | try:
13 | url = obj.get_absolute_url()
14 | spec['url'] = url
15 | spec['displayName'] = u"%s" % (url, strip_tags(u'%s' % obj))
16 | except AttributeError:
17 | pass
18 | return spec
19 |
20 |
21 | class ModelEmitter(object):
22 | """
23 | Example emitter object which loosely converts django
24 | models into specs. Probably won't work perfectly out of
25 | the box, but that's why we subclass.
26 | """
27 | def __init__(self, kwargs):
28 | self.kwargs = kwargs
29 |
30 | def _generic(self, obj):
31 | if isinstance(obj, Model):
32 | return model_to_spec(obj)
33 | return obj
34 |
35 | @property
36 | def target(self):
37 | if self.kwargs.get('target'):
38 | return self._generic(self.kwargs['target'])
39 |
40 | @property
41 | def object(self):
42 | if self.kwargs.get('object'):
43 | return self._generic(self.kwargs['object'])
44 |
45 | @property
46 | def actor(self):
47 | if self.kwargs.get('actor'):
48 | return self._generic(self.kwargs['actor'])
49 |
50 |
51 | class CommentEmitter(ModelEmitter):
52 | """
53 | Fun sample which you can use to beam a django.contrib.comments Comment
54 | """
55 | @property
56 | def published(self):
57 | time.mktime(self.kwargs['object'].submit_date.timetuple())
58 |
59 | @property
60 | def target(self):
61 | instance = self.kwargs['object']
62 | return self._generic(instance.content_object)
63 |
64 | @property
65 | def actor(self):
66 | return self._generic(self.kwargs['object'].user)
67 |
--------------------------------------------------------------------------------
/subscription/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/management/__init__.py
--------------------------------------------------------------------------------
/subscription/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/management/commands/__init__.py
--------------------------------------------------------------------------------
/subscription/management/commands/delete_empty_subscriptions.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 |
3 | from subscription.models import Subscription
4 |
5 |
6 | def queryset_iterator(queryset, chunksize=1000):
7 | '''''
8 | Iterate over a Django Queryset ordered by the primary key
9 | This method loads a maximum of chunksize (default: 1000) rows in it's
10 | memory at the same time while django normally would load all rows in it's
11 | memory. Using the iterator() method only causes it to not preload all the
12 | classes.
13 | Note that the implementation of the iterator does not support ordered query sets.
14 | '''
15 | pk = 0
16 | try:
17 | last_pk = queryset.order_by('-pk')[0].pk
18 | except IndexError:
19 | pass
20 | else:
21 | queryset = queryset.order_by('pk')
22 | while pk < last_pk:
23 | for row in queryset.filter(pk__gt=pk)[:chunksize]:
24 | pk = row.pk
25 | yield row
26 |
27 |
28 | class Command(BaseCommand):
29 | args = ""
30 | help = "Deletes all subscriptions with None as content_object"
31 |
32 | def handle(self, *args, **options):
33 | deleted_subscriptions_counter = 0
34 | total_subscriptions_counter = 0
35 | subscription_iterator = queryset_iterator(Subscription.objects.all())
36 | for subscription in subscription_iterator:
37 | total_subscriptions_counter += 1
38 | try:
39 | if subscription.content_object is None:
40 | subscription.delete()
41 | deleted_subscriptions_counter += 1
42 | except AttributeError:
43 | subscription.delete()
44 | deleted_subscriptions_counter += 1
45 |
46 | self.stdout.write('Total subscriptions: %s\nSuccessfully deleted subscriptions: %s\n' %
47 | (total_subscriptions_counter, deleted_subscriptions_counter))
48 |
--------------------------------------------------------------------------------
/subscription/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.9.6 on 2016-08-10 16:17
3 | from __future__ import unicode_literals
4 |
5 | import datetime
6 | from django.conf import settings
7 | from django.db import migrations, models
8 | import django.db.models.deletion
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | initial = True
14 |
15 | dependencies = [
16 | ('contenttypes', '0002_remove_content_type_name'),
17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18 | ]
19 |
20 | operations = [
21 | migrations.CreateModel(
22 | name='Subscription',
23 | fields=[
24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25 | ('object_id', models.PositiveIntegerField()),
26 | ('timestamp', models.DateTimeField(default=datetime.datetime.now, editable=False)),
27 | ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
29 | ],
30 | options={
31 | 'db_table': 'subscription',
32 | },
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/subscription/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/migrations/__init__.py
--------------------------------------------------------------------------------
/subscription/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from django.db import models
3 | from django.contrib.contenttypes.fields import GenericForeignKey
4 | from django.contrib.contenttypes.models import ContentType
5 | from subscription.base import get_backends
6 | from django.db.models.query import QuerySet
7 |
8 | class StreamAcknowledgeProfileMixin(object):
9 | """
10 | You must add this to your userprofile:
11 | stream_last_acknowledged = models.DateTimeField(null=True, blank=True)
12 | """
13 | def get_stream_acknowledged(self):
14 | """
15 | Last time they checked their notifications
16 |
17 | This assumes a datetime field on the userprofile
18 | """
19 | return self.stream_last_acknowledged
20 |
21 | def stream_pending_acknowledgements(self, date):
22 | if not self.stream_last_acknowledged or self.stream_last_acknowledged <= date:
23 | return True
24 | return False
25 |
26 |
27 | class SubscriptionQuerySet(QuerySet):
28 | def __init__(self, *args, **kwargs):
29 | super(SubscriptionQuerySet, self).__init__(*args, **kwargs)
30 | self._subscription_exclude = []
31 | self._subscription_to = []
32 | self._subscription_of = None
33 |
34 |
35 | def _clone(self, **kwargs):
36 | clone = super(SubscriptionQuerySet, self)._clone(**kwargs)
37 | clone._subscription_to = self._subscription_to
38 | clone._subscription_exclude = self._subscription_exclude
39 | clone._subscription_of = self._subscription_of
40 | clone.__dict__.update(kwargs)
41 | return clone
42 |
43 | def of(self, instance):
44 | clone = self._clone()
45 | ct = ContentType.objects.get_for_model(instance)
46 | clone._subscription_of = Subscription.objects.filter(content_type=ct.pk, object_id=instance.pk)
47 | return clone
48 |
49 | def to(self, user):
50 | clone = self._clone()
51 | try:
52 | iter(user)
53 | clone._subscription_to.extend([i for i in user if i.is_active])
54 | except TypeError:
55 | if user.is_active:
56 | clone._subscription_to.append(user)
57 | return clone
58 |
59 | def not_to(self, user):
60 | clone = self._clone()
61 | try:
62 | iter(user)
63 | clone._subscription_exclude.extend(user)
64 | except TypeError:
65 | clone._subscription_exclude.append(user)
66 | return clone
67 |
68 | def subscribe(self, user, obj):
69 | ct = ContentType.objects.get_for_model(obj)
70 | Subscription.objects.get_or_create(content_type=ct,object_id=obj.pk,user=user)
71 |
72 | def emit(self, *args, **kwargs):
73 | clone = self._clone()
74 |
75 | backend = kwargs.pop('backend',None) or None
76 | for backend_module in get_backends().values():
77 | if backend and not backend_module == backend:
78 | continue
79 | for item in clone._subscription_to:
80 | if item in clone._subscription_exclude:
81 | continue
82 | backend_module(item, *args, **kwargs)
83 | for item in clone._subscription_of or []:
84 | if not item.user.is_active or item.user in clone._subscription_to or item.user in clone._subscription_exclude:
85 | continue
86 | backend_module(item.user, *args, **kwargs)
87 |
88 |
89 | class Subscription(models.Model):
90 | user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
91 | content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.PROTECT)
92 | object_id = models.PositiveIntegerField()
93 | content_object = GenericForeignKey()
94 | timestamp = models.DateTimeField(editable=False, default=datetime.datetime.now)
95 | objects = SubscriptionQuerySet.as_manager()
96 |
97 | class Meta:
98 | db_table = "subscription"
99 |
100 | """
101 | Seems sensible to auto-subscribe people to objects they comment on.
102 |
103 | Just send this with the comment_was_posted signal
104 |
105 | def auto_subscribe(**kwargs):
106 | comment = kwargs.pop('comment')
107 | auto_subscribe_field = getattr(settings,'SUBSCRIPTION_AUTOSUBSCRIBE_PROFILE_FIELD','auto_subscribe')
108 | if getattr(get_profile(comment.user),auto_subscribe_field,True):
109 | Subscription.objects.subscribe(user,comment.content_object)
110 |
111 | comment_was_posted.connect(auto_subscribe, sender=CommentModel)
112 | """
113 |
114 | """
115 | Make abstract, turn into emit backend
116 | #comment_was_posted.connect(email_comment, sender=CommentModel)
117 |
118 | def email_comment(**kwargs):
119 | comment = kwargs.pop('comment')
120 | request = kwargs.pop('request')
121 |
122 | site = Site.objects.get(id=settings.SITE_ID)
123 |
124 | supress_email_field = getattr(settings,'SUBSCRIPTION_EMAIL_SUPRESS_PROFILE_FIELD','no_email')
125 | t = loader.get_template('comments/email_comment_template.html')
126 |
127 | subscriptions = Subscription.objects.filter(content_type=comment.content_type,\
128 | object_id=comment.object_pk).exclude(user=comment.user)
129 |
130 | for i in subscriptions:
131 | if getattr(get_profile(i.user),supress_email_field,False):
132 | continue
133 |
134 | c = {
135 | 'domain': site.domain,
136 | 'site_name': site.name,
137 | 'c': comment,
138 | 'delete': i.user.has_perm('comments.delete_comment'),
139 | 'subscription': i,
140 | }
141 | if not i.user.email:
142 | continue
143 |
144 | send_mail(("%s - Comment on %s") % (site.name,comment.content_object), \
145 | t.render(RequestContext(request,c)), None, [i.user.email])
146 | """
147 |
--------------------------------------------------------------------------------
/subscription/south_migrations/0002_remove_dupes.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | import datetime
3 | from south.db import db
4 | from south.v2 import DataMigration
5 | from django.db import models
6 |
7 | class Migration(DataMigration):
8 |
9 | def forwards(self, orm):
10 | from django.db import connection
11 |
12 | cursor = connection.cursor()
13 | cursor.execute("create table subscriptiontmp like subscription;")
14 | cursor.execute("insert into subscriptiontmp (user_id, content_type_id, object_id, timestamp) select user_id, content_type_id, object_id, MAX(timestamp) as timestamp from subscription GROUP BY user_id, content_type_id, object_id;")
15 | cursor.execute("drop table subscription;")
16 | cursor.execute("alter table subscriptiontmp rename to subscription;")
17 | cursor.close()
18 |
19 | def backwards(self, orm):
20 | pass
21 |
22 | models = {
23 | 'auth.group': {
24 | 'Meta': {'object_name': 'Group'},
25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
26 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
27 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
28 | },
29 | 'auth.permission': {
30 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
31 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
33 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
35 | },
36 | 'auth.user': {
37 | 'Meta': {'object_name': 'User'},
38 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
39 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
40 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
41 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
43 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
44 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
46 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
47 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
48 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
49 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
50 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
51 | },
52 | 'contenttypes.contenttype': {
53 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
54 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
55 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
56 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
57 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
58 | },
59 | 'subscription.subscription': {
60 | 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
61 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
62 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
63 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
64 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
65 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
66 | }
67 | }
68 |
69 | complete_apps = ['subscription']
70 |
--------------------------------------------------------------------------------
/subscription/south_migrations/0003_auto__add_unique_subscription_user_content_type_object_id.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | import datetime
3 | from south.db import db
4 | from south.v2 import SchemaMigration
5 | from django.db import models
6 |
7 | class Migration(SchemaMigration):
8 |
9 | def forwards(self, orm):
10 |
11 | # Adding unique constraint on 'Subscription', fields ['user', 'content_type', 'object_id']
12 | db.create_unique('subscription', ['user_id', 'content_type_id', 'object_id'])
13 |
14 |
15 | def backwards(self, orm):
16 |
17 | # Removing unique constraint on 'Subscription', fields ['user', 'content_type', 'object_id']
18 | db.delete_unique('subscription', ['user_id', 'content_type_id', 'object_id'])
19 |
20 |
21 | models = {
22 | 'auth.group': {
23 | 'Meta': {'object_name': 'Group'},
24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
27 | },
28 | 'auth.permission': {
29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
34 | },
35 | 'auth.user': {
36 | 'Meta': {'object_name': 'User'},
37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
50 | },
51 | 'contenttypes.contenttype': {
52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
57 | },
58 | 'subscription.subscription': {
59 | 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'Subscription', 'db_table': "'subscription'"},
60 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
63 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
64 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
65 | }
66 | }
67 |
68 | complete_apps = ['subscription']
69 |
--------------------------------------------------------------------------------
/subscription/south_migrations/0004_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from south.db import db
4 | from south.v2 import SchemaMigration
5 | from django.db import models
6 |
7 |
8 | class Migration(SchemaMigration):
9 |
10 | def forwards(self, orm):
11 | # Adding model 'Subscription'
12 | db.create_table('subscription', (
13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
15 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
16 | ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
17 | ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
18 | ))
19 | db.send_create_signal('subscription', ['Subscription'])
20 |
21 |
22 | def backwards(self, orm):
23 | # Deleting model 'Subscription'
24 | db.delete_table('subscription')
25 |
26 |
27 | models = {
28 | 'auth.group': {
29 | 'Meta': {'object_name': 'Group'},
30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
31 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
32 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
33 | },
34 | 'auth.permission': {
35 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
36 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
37 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
38 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
40 | },
41 | 'auth.user': {
42 | 'Meta': {'object_name': 'User'},
43 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
44 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
45 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
46 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
47 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
48 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
49 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
50 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
51 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
52 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
53 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
54 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
55 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
56 | },
57 | 'contenttypes.contenttype': {
58 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
59 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
63 | },
64 | 'subscription.subscription': {
65 | 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
66 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
67 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
69 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
70 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
71 | }
72 | }
73 |
74 | complete_apps = ['subscription']
--------------------------------------------------------------------------------
/subscription/south_migrations/0005_simpler_streams.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from south.db import db
4 | from south.v2 import DataMigration
5 | from django.db import models
6 |
7 | class Migration(DataMigration):
8 |
9 | def forwards(self, orm):
10 | from subscription.client import get_cache_client
11 | conn = get_cache_client()
12 | for key in conn.keys("actstream::*::*"):
13 | user_id = key.split("::")[1]
14 | for item in conn.lrange(key, 0, -1):
15 | conn.rpoplpush(key, "actstream::%s" % user_id)
16 |
17 | "Write your forwards methods here."
18 | # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
19 |
20 | def backwards(self, orm):
21 | "Write your backwards methods here."
22 |
23 | models = {
24 | 'auth.group': {
25 | 'Meta': {'object_name': 'Group'},
26 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
27 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
28 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
29 | },
30 | 'auth.permission': {
31 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
32 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
33 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
34 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
35 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
36 | },
37 | 'auth.user': {
38 | 'Meta': {'object_name': 'User'},
39 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
40 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
41 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
42 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
43 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
44 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
45 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
46 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
47 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
48 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
49 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
50 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
51 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
52 | },
53 | 'contenttypes.contenttype': {
54 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
55 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
56 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
57 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
58 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
59 | },
60 | 'subscription.subscription': {
61 | 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
62 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
63 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
64 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
65 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
66 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
67 | }
68 | }
69 |
70 | complete_apps = ['subscription']
71 | symmetrical = True
72 |
--------------------------------------------------------------------------------
/subscription/south_migrations/0006_limit_to_200.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import datetime
3 | from south.db import db
4 | from south.v2 import DataMigration
5 | from django.db import models
6 |
7 | class Migration(DataMigration):
8 |
9 | def forwards(self, orm):
10 | from subscription.client import get_cache_client
11 | conn = get_cache_client()
12 | for item in conn.keys("actstream::*"):
13 | conn.ltrim(item, 0, 200)
14 |
15 | "Write your forwards methods here."
16 | # Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
17 |
18 | def backwards(self, orm):
19 | "Write your backwards methods here."
20 |
21 | models = {
22 | 'auth.group': {
23 | 'Meta': {'object_name': 'Group'},
24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
27 | },
28 | 'auth.permission': {
29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
34 | },
35 | 'auth.user': {
36 | 'Meta': {'object_name': 'User'},
37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
50 | },
51 | 'contenttypes.contenttype': {
52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
57 | },
58 | 'subscription.subscription': {
59 | 'Meta': {'object_name': 'Subscription', 'db_table': "'subscription'"},
60 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
62 | 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
63 | 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
64 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
65 | }
66 | }
67 |
68 | complete_apps = ['subscription']
69 | symmetrical = True
70 |
--------------------------------------------------------------------------------
/subscription/south_migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/south_migrations/__init__.py
--------------------------------------------------------------------------------
/subscription/stream.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | from .client import get_cache_client
4 | from .cluster import cluster_specs, render_clusters
5 |
6 |
7 | def clear_stream(user, conn=None, key='actstream::%s'):
8 | conn = conn or get_cache_client()
9 | conn.delete(key % (user.pk))
10 |
11 |
12 | def compile_stream(stream, newer_than=None, self=None, render=True):
13 | stream_redux = []
14 | if not stream:
15 | return stream_redux
16 | """
17 | Things are gonna get gross here. Basically, I want to transition to the new
18 | system of consuming specs without destroying all the old messages.
19 |
20 | We can considered the old ones already rendered. The new ones we can consolidate.
21 |
22 | So far we've been going with (datetime, textblob) so we're gonna stick with that
23 | for the new ones, too
24 | """
25 | neostream, legacy_stream = [], []
26 | for item in stream:
27 | if isinstance(item, dict):
28 | neostream.append(item)
29 | continue
30 | try:
31 | timestamp, text = json.loads(item)
32 | try:
33 | if newer_than and timestamp < newer_than:
34 | continue
35 | except TypeError:
36 | continue
37 | legacy_stream.append((timestamp, text))
38 | except ValueError:
39 | item = json.loads(item)
40 | neostream.append(item)
41 | if newer_than:
42 | neostream = [
43 | i for i in neostream if not i.get('published') or
44 | datetime.datetime.fromtimestamp(i['published']) > newer_than]
45 | specs = cluster_specs(neostream)
46 | if render:
47 | specs = render_clusters(specs)
48 | stream_redux.extend(specs)
49 | stream_redux.extend(legacy_stream)
50 | if render:
51 | stream_redux = sorted(stream_redux, key=lambda x: int(x[0]), reverse=True)
52 | return stream_redux
53 |
54 |
55 | def get_stream(user_id, conn=None, limit=None, renderer=None, newer_than=None, key='actstream::%s'):
56 | """
57 | * limit - The redis limit to apply to the list (maybe you only want the last 20)
58 | * newer_than - Epoch of the earliest messages you want.
59 | """
60 | limit = limit or -1
61 | conn = conn or get_cache_client()
62 | user_id = user_id or '*'
63 | redis_list = conn.lrange(key % (user_id), 0, limit)
64 | if renderer:
65 | return renderer(redis_list, newer_than=newer_than, self=user_id)
66 | return redis_list
67 |
68 |
69 | def user_stream(user, limit=None, newer_than=None):
70 | """
71 | * limit_labmda - Cheesy as fuck hook to pass limit to a list
72 | * newer_than - Stream items greater than this
73 |
74 | BACKSTORY -- You probably want to deliver all
75 | unacknowledged but you might not want
76 | the entire history of everything they've already seen.
77 | """
78 | conn = get_cache_client()
79 | return get_stream(
80 | user.pk, conn,
81 | limit=limit, newer_than=newer_than, renderer=compile_stream)
82 |
--------------------------------------------------------------------------------
/subscription/templates/comments/email_comment_template.html:
--------------------------------------------------------------------------------
1 | {% autoescape off %}
2 | New comment at http://{{ domain }}{{ c.get_absolute_url }}
3 |
4 | {{ c.user_name }}{% if delete %} ({{ c.user_email }} / {{ c.user_url }} / {{ c.ip_address}} ) {% endif %} says:
5 |
6 | {{ c.comment }}
7 |
8 | =====
9 | {% if delete %}delete - http://{{ domain }}/comments/delete/{{ c.id }}/{% endif %}
10 | unsubscribe - http://{{ domain }}{% url subscription_unsubscribe subscription.content_type_id subscription.object_id %}
11 | {% endautoescape %}
12 |
--------------------------------------------------------------------------------
/subscription/templatetags/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yeago/django-subscription/250881f1f97126488d0672e2610c4e9f8e4f3dbc/subscription/templatetags/__init__.py
--------------------------------------------------------------------------------
/subscription/templatetags/subscription_tags.py:
--------------------------------------------------------------------------------
1 | from django.template import Library
2 | from django.utils.safestring import mark_safe
3 | from django.urls import reverse
4 |
5 | from django.contrib.contenttypes.models import ContentType
6 | from subscription.models import Subscription
7 |
8 | register = Library()
9 |
10 |
11 | @register.simple_tag
12 | def unsubscribe_url(instance):
13 | ct = ContentType.objects.get_for_model(instance.__class__)
14 | return reverse("subscribe", [ct.pk, instance.pk])
15 |
16 |
17 | @register.simple_tag
18 | def subscribe_url(instance):
19 | ct = ContentType.objects.get_for_model(instance.__class__)
20 | return reverse("unsubscribe", [ct.pk, instance.pk])
21 |
22 |
23 | @register.simple_tag
24 | def subscription_toggle_link(object, user, return_url=None):
25 | if not user.is_authenticated:
26 | return ''
27 |
28 | ct = ContentType.objects.get_for_model(object.__class__)
29 | try:
30 | Subscription.objects.get(content_type=ct, object_id=object.pk, user=user)
31 | url = "subscription_unsubscribe"
32 | verbage = "Unsubscribe"
33 | except Subscription.DoesNotExist:
34 | url = "subscription_subscribe"
35 | verbage = "Subscribe"
36 |
37 | if return_url:
38 | return mark_safe("%s" % (
39 | reverse(url, args=[ct.pk, object.pk]), return_url, verbage))
40 | return mark_safe("%s" % (
41 | reverse(url, args=[ct.pk, object.pk]), verbage))
42 |
--------------------------------------------------------------------------------
/subscription/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 | from subscription import views
3 |
4 | urlpatterns = [
5 | re_path(r'^$', views.SubscriptionView.as_view(), name="subscriptions"),
6 | re_path(r'^notifications/ping/$', views.notifications_ping, name="notifications_ping"),
7 | re_path('unsubscribe/(?P\d+)/(?P\d+)/', views.unsubscribe, name="subscription_unsubscribe"),
8 | re_path('subscribe/(?P\d+)/(?P\d+)/', views.subscribe, name="subscription_subscribe"),
9 | ]
10 |
--------------------------------------------------------------------------------
/subscription/views.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from django.http import Http404, HttpResponse
4 | from django.shortcuts import redirect, get_object_or_404
5 | from django.contrib.contenttypes.models import ContentType
6 | from django.utils.decorators import method_decorator
7 | from django.views.generic import ListView
8 | from django.contrib.auth.decorators import login_required
9 | from django.contrib import messages
10 |
11 | from subscription.models import Subscription
12 | from subscription.base import get_profile
13 |
14 |
15 | @login_required
16 | def notifications_ping(request): # Move unacknowledged items to acknowledged
17 | profile = get_profile(request.user)
18 | profile.stream_last_acknowledged = datetime.datetime.now()
19 | profile.save()
20 | return HttpResponse("")
21 |
22 |
23 | @login_required
24 | def subscribe(request, content_type, object_id, success_message="Subscription added"):
25 | content_type = get_object_or_404(ContentType, pk=content_type)
26 | Subscription.objects.get_or_create(content_type=content_type, object_id=object_id, user=request.user)
27 | messages.success(request, success_message)
28 | return redirect(request.GET.get('return_url', '/'))
29 |
30 | @login_required
31 | def unsubscribe(request, content_type, object_id, success_message="You have been unsubscribed"):
32 | content_type = get_object_or_404(ContentType, pk=content_type)
33 | subscription = get_object_or_404(Subscription, content_type=content_type, object_id=object_id, user=request.user)
34 | subscription.delete()
35 | messages.success(request, success_message)
36 | return redirect(request.GET.get('return_url', '/'))
37 |
38 |
39 | class SubscriptionView(ListView):
40 | paginate_by = 25
41 | ordering = ['timestamp']
42 |
43 | @method_decorator(login_required)
44 | def dispatch(self, *args, **kwargs):
45 | return super(SubscriptionView, self).dispatch(*args, **kwargs)
46 |
47 | def get_queryset(self):
48 | return Subscription.objects.filter(user=self.request.user).order_by('-timestamp')
49 |
--------------------------------------------------------------------------------