update suggestions when subscriptions change
authorStefan Kögl <stefan@skoegl.net>
Sat, 27 Apr 2013 17:50:32 +0000 (27 19:50 +0200)
committerStefan Kögl <stefan@skoegl.net>
Sat, 27 Apr 2013 17:50:32 +0000 (27 19:50 +0200)
this replaces the "update-suggestions" command that was previously run as
management command

mygpo/core/models.py
mygpo/core/signals.py
mygpo/users/management/commands/update-suggestions.py [deleted file]
mygpo/users/models.py
mygpo/users/signals.py [new file with mode: 0644]
mygpo/users/tasks.py
mygpo/web/views/__init__.py

index 8159780..6539a8c 100644 (file)
@@ -16,7 +16,6 @@ from mygpo.core.proxy import DocumentABCMeta
 from mygpo.core.slugs import SlugMixin
 from mygpo.core.oldid import OldIdMixin
 from mygpo.web.logo import CoverArt
-from mygpo.users.tasks import sync_user
 
 # make sure this code is executed at startup
 from mygpo.core.signals import *
@@ -321,7 +320,8 @@ class Podcast(Document, SlugMixin, OldIdMixin):
         state.subscribe(device)
         try:
             state.save()
-            sync_user.delay(user)
+            subscription_changed.send(sender=self, user=user, device=device,
+                                      subscribed=True)
         except Unauthorized as ex:
             raise SubscriptionException(ex)
 
@@ -333,7 +333,8 @@ class Podcast(Document, SlugMixin, OldIdMixin):
         state.unsubscribe(device)
         try:
             state.save()
-            sync_user.delay(user)
+            subscription_changed.send(sender=self, user=user, device=device,
+                                      subscribed=False)
         except Unauthorized as ex:
             raise SubscriptionException(ex)
 
index d7b38d2..9bdc187 100644 (file)
@@ -3,3 +3,9 @@ import django.dispatch
 # indicates that an incomplete object has been loaded from the database
 # the object needs to be updated from its source (eg the feed)
 incomplete_obj = django.dispatch.Signal()
+
+
+# indicates that a podcast was subscribed or unsubscribed
+# ``sender`` will equal the user. Additionally the parameters ``user`` and
+# ``subscribed`` will be provided
+subscription_changed = django.dispatch.Signal()
diff --git a/mygpo/users/management/commands/update-suggestions.py b/mygpo/users/management/commands/update-suggestions.py
deleted file mode 100644 (file)
index a389d48..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-from itertools import chain
-from optparse import make_option
-from operator import itemgetter
-from collections import Counter
-
-from django.core.management.base import BaseCommand
-
-from mygpo.users.models import Suggestions, User
-from mygpo.utils import progress
-from mygpo.decorators import repeat_on_conflict
-from mygpo.db.couchdb.user import suggestions_for_user
-
-
-class Command(BaseCommand):
-
-    option_list = BaseCommand.option_list + (
-        make_option('--max', action='store', type='int', dest='max', default=15, help="Maximum number of suggested podcasts per user."),
-        make_option('--max-users', action='store', type='int', dest='max_users', default=15, help="Maximum number of users to update."),
-        make_option('--outdated-only', action='store_true', dest='outdated', default=False, help="Update only users where the suggestions are not up-to-date"),
-        make_option('--user', action='store', type='string', dest='username', default='', help="Update a specific user"),
-        )
-
-
-    def handle(self, *args, **options):
-
-        max_suggestions = options.get('max')
-
-        if options.get('username'):
-            users = [User.get_user(options.get('username'))]
-
-        else:
-            users = User.all_users()
-            users = filter(lambda u: u.is_active, users)
-
-            if options.get('outdated'):
-                users = filter(lambda u: not u.suggestions_up_to_date, users)
-
-        if options.get('max_users'):
-            users = users[:int(options.get('max_users'))]
-
-        total = len(users)
-
-        for n, user in enumerate(users):
-            suggestion = suggestions_for_user(user)
-
-            subscribed_podcasts = list(set(user.get_subscribed_podcasts()))
-            subscribed_podcasts = filter(None, subscribed_podcasts)
-
-            subscribed_podcasts = filter(None, subscribed_podcasts)
-            related = chain.from_iterable([p.related_podcasts for p in subscribed_podcasts])
-            related = filter(lambda pid: not pid in suggestion.blacklist, related)
-
-            counter = Counter(related)
-            get_podcast_id = itemgetter(0)
-            suggested = map(get_podcast_id, counter.most_common(max_suggestions))
-            suggestion.podcasts = suggested
-
-            suggestion.save()
-
-            _update_user(user=user)
-
-            progress(n+1, total)
-
-
-@repeat_on_conflict(['user'])
-def _update_user(user):
-    user.suggestions_up_to_date = True
-    user.save()
index fed743d..428204e 100644 (file)
@@ -23,6 +23,8 @@ from mygpo.users.settings import FAV_FLAG, PUBLIC_SUB_PODCAST, SettingsMixin
 from mygpo.db.couchdb.podcast import podcasts_by_id, podcasts_to_dict
 from mygpo.db.couchdb.user import user_history, device_history
 
+# make sure this code is executed at startup
+from mygpo.users.signals import *
 
 
 RE_DEVICE_UID = re.compile(r'^[\w.-]+$')
diff --git a/mygpo/users/signals.py b/mygpo/users/signals.py
new file mode 100644 (file)
index 0000000..c3a9c0c
--- /dev/null
@@ -0,0 +1,22 @@
+from mygpo.core.signals import subscription_changed
+from mygpo.users.tasks import sync_user, update_suggestions
+
+
+def sync_user_on_subscription(sender, **kwargs):
+    """ synchronizes the user after one of his subscriptions has changed """
+    user = kwargs['user']
+    sync_user.delay(user)
+
+
+subscription_changed.connect(sync_user_on_subscription,
+                             dispatch_uid='sync_user_on_subscription')
+
+
+def update_suggestions_on_subscription(sender, **kwargs):
+    """ update a user's suggestions after one of his subscriptions change """
+    user = kwargs['user']
+    update_suggestions.delay(user)
+
+
+subscription_changed.connect(update_suggestions_on_subscription,
+                             dispatch_uid='update_suggestions_on_subscription')
index 78f22ec..b985032 100644 (file)
@@ -1,4 +1,11 @@
+from itertools import chain
+from operator import itemgetter
+from collections import Counter
+
+from couchdbkit import ResourceConflict
+
 from mygpo.cel import celery
+from mygpo.db.couchdb.user import suggestions_for_user
 
 
 @celery.task(max_retries=5, default_retry_delay=60)
@@ -9,3 +16,32 @@ def sync_user(user):
         if group.is_synced:
             device = group.devices[0]
             user.sync_group(device)
+
+
+@celery.task(max_retries=5, default_retry_delay=60)
+def update_suggestions(user, max_suggestions=15):
+    """ updates the suggestions of a user """
+
+    # get suggestions object
+    suggestion = suggestions_for_user(user)
+
+    # calculate possible suggestions
+    subscribed_podcasts = list(set(user.get_subscribed_podcasts()))
+    subscribed_podcasts = filter(None, subscribed_podcasts)
+    related = chain.from_iterable([p.related_podcasts for p in subscribed_podcasts])
+
+    # filter out blacklisted podcasts
+    related = filter(lambda pid: not pid in suggestion.blacklist, related)
+
+    # get most relevant
+    counter = Counter(related)
+    get_podcast_id = itemgetter(0)
+    suggested = map(get_podcast_id, counter.most_common(max_suggestions))
+    suggestion.podcasts = suggested
+
+    try:
+        # update suggestions object
+        suggestion.save()
+
+    except ResourceConflict as ex:
+        raise update_suggestions.retry(exc=ex)
index b0cb5a1..b13a342 100644 (file)
@@ -40,6 +40,7 @@ from mygpo.core.podcasts import PodcastSet
 from mygpo.directory.toplist import PodcastToplist
 from mygpo.users.models import Suggestions, History, HistoryEntry, \
          DeviceDoesNotExist
+from mygpo.users.tasks import update_suggestions
 from mygpo.web.utils import process_lang_params
 from mygpo.utils import parse_range
 from mygpo.web.views.podcast import slug_id_decorator
@@ -181,20 +182,17 @@ def history(request, count=15, uid=None):
 @login_required
 @slug_id_decorator
 def blacklist(request, blacklisted_podcast):
-    suggestion = suggestions_for_user(request.user)
+    user = request.user
+
+    suggestion = suggestions_for_user(user)
 
     @repeat_on_conflict(['suggestion'])
     def _update(suggestion, podcast_id):
         suggestion.blacklist.append(podcast_id)
         suggestion.save()
 
-    @repeat_on_conflict(['user'])
-    def _not_uptodate(user):
-        user.suggestions_up_to_date = False
-        user.save()
-
     _update(suggestion=suggestion, podcast_id=blacklisted_podcast.get_id())
-    _not_uptodate(user=request.user)
+    update_suggestions.delay(user)
 
     return HttpResponseRedirect(reverse('suggestions'))