├── 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 | --------------------------------------------------------------------------------