From 74e5d75165e798d2d2de9a6e5853be4d88b33247 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stefan=20K=C3=B6gl?= Date: Tue, 29 Jul 2014 22:20:39 +0200 Subject: [PATCH] [Subscriptions] add Subscription, HistoryEntry --- mygpo/core/models.py | 19 ++ mygpo/history/__init__.py | 0 mygpo/history/admin.py | 18 ++ mygpo/history/migrations/0001_initial.py | 36 +++ mygpo/history/migrations/0002_pluralname.py | 18 ++ mygpo/history/migrations/__init__.py | 0 mygpo/history/models.py | 46 +++ mygpo/history/tests.py | 3 + mygpo/history/views.py | 3 + mygpo/maintenance/migrate.py | 316 ++++++++++++--------- mygpo/podcasts/admin.py | 11 + .../podcasts/migrations/0023_auto_20140729_1711.py | 36 +++ mygpo/podcasts/models.py | 27 +- mygpo/settings.py | 2 + mygpo/subscriptions/__init__.py | 0 mygpo/subscriptions/admin.py | 30 ++ mygpo/subscriptions/migrations/0001_initial.py | 57 ++++ mygpo/subscriptions/migrations/__init__.py | 0 mygpo/subscriptions/models.py | 67 +++++ mygpo/subscriptions/tests.py | 3 + mygpo/subscriptions/views.py | 3 + mygpo/users/models.py | 7 +- 22 files changed, 542 insertions(+), 160 deletions(-) create mode 100644 mygpo/history/__init__.py create mode 100644 mygpo/history/admin.py create mode 100644 mygpo/history/migrations/0001_initial.py create mode 100644 mygpo/history/migrations/0002_pluralname.py create mode 100644 mygpo/history/migrations/__init__.py create mode 100644 mygpo/history/models.py create mode 100644 mygpo/history/tests.py create mode 100644 mygpo/history/views.py rewrite mygpo/maintenance/migrate.py (63%) create mode 100644 mygpo/podcasts/migrations/0023_auto_20140729_1711.py create mode 100644 mygpo/subscriptions/__init__.py create mode 100644 mygpo/subscriptions/admin.py create mode 100644 mygpo/subscriptions/migrations/0001_initial.py create mode 100644 mygpo/subscriptions/migrations/__init__.py create mode 100644 mygpo/subscriptions/models.py create mode 100644 mygpo/subscriptions/tests.py create mode 100644 mygpo/subscriptions/views.py diff --git a/mygpo/core/models.py b/mygpo/core/models.py index 00f02f9d..6bc2f5de 100644 --- a/mygpo/core/models.py +++ b/mygpo/core/models.py @@ -68,3 +68,22 @@ class GenericManager(models.Manager): self.model._meta.db_table) row = cursor.fetchone() return int(row[0]) + + +class UpdateInfoModel(models.Model): + """ Model that keeps track of when it was created and updated """ + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class DeleteableModel(models.Model): + """ A model that can be marked as deleted """ + + # indicates that the object has been deleted + deleted = models.BooleanField(default=False) + + class Meta: + abstract = True diff --git a/mygpo/history/__init__.py b/mygpo/history/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mygpo/history/admin.py b/mygpo/history/admin.py new file mode 100644 index 00000000..9ff428aa --- /dev/null +++ b/mygpo/history/admin.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals + +from django.contrib import admin + +from mygpo.history.models import HistoryEntry + + +@admin.register(HistoryEntry) +class HistoryEntryAdmin(admin.ModelAdmin): + """ Admin page for history entries """ + + # configuration for the list view + list_display = ('user', 'timestamp', 'podcast', 'action', 'client', ) + + # fetch the related objects for the fields in list_display + list_select_related = ('user', 'podcast', 'client', ) + + raw_id_fields = ('user', 'podcast', 'client', ) diff --git a/mygpo/history/migrations/0001_initial.py b/mygpo/history/migrations/0001_initial.py new file mode 100644 index 00000000..60e1b6d2 --- /dev/null +++ b/mygpo/history/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_syncgroup_protect'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('podcasts', '0023_auto_20140729_1711'), + ] + + operations = [ + migrations.CreateModel( + name='HistoryEntry', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('timestamp', models.DateTimeField()), + ('action', models.CharField(max_length=11, choices=[(b'subscribe', b'subscribed'), (b'unsubscribe', b'unsubscribed')])), + ('client', models.ForeignKey(to='users.Client')), + ('podcast', models.ForeignKey(to='podcasts.Podcast')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': [b'timestamp'], + }, + bases=(models.Model,), + ), + migrations.AlterIndexTogether( + name='historyentry', + index_together=set([(b'user', b'podcast'), (b'user', b'client')]), + ), + ] diff --git a/mygpo/history/migrations/0002_pluralname.py b/mygpo/history/migrations/0002_pluralname.py new file mode 100644 index 00000000..7c11ed5c --- /dev/null +++ b/mygpo/history/migrations/0002_pluralname.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='historyentry', + options={'ordering': [b'timestamp'], 'verbose_name_plural': b'History Entries'}, + ), + ] diff --git a/mygpo/history/migrations/__init__.py b/mygpo/history/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mygpo/history/models.py b/mygpo/history/models.py new file mode 100644 index 00000000..219377c1 --- /dev/null +++ b/mygpo/history/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.conf import settings + +from mygpo.podcasts.models import Podcast, Episode +from mygpo.users.models import Client + + +class HistoryEntry(models.Model): + """ A entry in the history """ + + SUBSCRIBE = 'subscribe' + UNSUBSCRIBE = 'unsubscribe' + PODCAST_ACTIONS = ( + (SUBSCRIBE, 'subscribed'), + (UNSUBSCRIBE, 'unsubscribed'), + ) + + # the timestamp at which the event happened + timestamp = models.DateTimeField() + + # the podcast which was involved in the event + podcast = models.ForeignKey(Podcast, db_index=True, + on_delete=models.CASCADE) + + # the user which caused / triggered the event + user = models.ForeignKey(settings.AUTH_USER_MODEL, db_index=True, + on_delete=models.CASCADE) + + # the client on / for which the event happened + client = models.ForeignKey(Client, on_delete=models.CASCADE) + + # the action that happened + action = models.CharField( + max_length=max(map(len, [action for action, name in PODCAST_ACTIONS])), + choices=PODCAST_ACTIONS, + ) + + class Meta: + index_together = [ + ['user', 'client'], + ['user', 'podcast'], + ] + + ordering = ['timestamp'] + + verbose_name_plural = "History Entries" diff --git a/mygpo/history/tests.py b/mygpo/history/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/mygpo/history/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mygpo/history/views.py b/mygpo/history/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/mygpo/history/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/mygpo/maintenance/migrate.py b/mygpo/maintenance/migrate.py dissimilarity index 63% index 7f86c7b0..a17e062b 100644 --- a/mygpo/maintenance/migrate.py +++ b/mygpo/maintenance/migrate.py @@ -1,139 +1,177 @@ -from __future__ import unicode_literals - -import json -from datetime import datetime - -from django.contrib.auth.models import User - -from mygpo.users.models import User as U, UserProfile, Client, SyncGroup -from mygpo.podcasts.models import Podcast -from mygpo.publisher.models import PublishedPodcast - -import logging -logger = logging.getLogger(__name__) - - -def to_maxlength(cls, field, val): - """ Cut val to the maximum length of cls's field """ - max_length = cls._meta.get_field(field).max_length - orig_length = len(val) - if orig_length > max_length: - val = val[:max_length] - logger.warn('%s.%s length reduced from %d to %d', - cls.__name__, field, orig_length, max_length) - - return val - - -def migrate_user(u): - - # no need in migrating already deleted users - if u.deleted: - return - - user, created = User.objects.update_or_create(username=u.username, - defaults = { - 'email': u.email, - 'is_active': u.is_active, - 'is_staff': u.is_staff, - 'is_superuser': u.is_superuser, - 'last_login': u.last_login or datetime(1970, 01, 01), - 'date_joined': u.date_joined, - 'password': u.password, - } - ) - - profile = user.profile - profile.uuid = u._id - profile.suggestions_up_to_date = u.suggestions_up_to_date - profile.about = u.about or '' - profile.google_email = u.google_email - profile.subscriptions_token = u.subscriptions_token - profile.favorite_feeds_token = u.favorite_feeds_token - profile.publisher_update_token = u.publisher_update_token - profile.userpage_token = u.userpage_token - profile.twitter = to_maxlength(UserProfile, 'twitter', u.twitter) if u.twitter is not None else None - profile.activation_key = u.activation_key - profile.settings = json.dumps(u.settings) - profile.save() - - for podcast_id in u.published_objects: - try: - podcast = Podcast.objects.all().get_by_any_id(podcast_id) - except Podcast.DoesNotExist: - logger.warn("Podcast with ID '%s' does not exist", podcast_id) - continue - - PublishedPodcast.objects.get_or_create(publisher=user, podcast=podcast) - - for device in u.devices: - client = Client.objects.get_or_create(user=user, - uid=device.uid, - defaults = { - 'id': device.id, - 'name': device.name, - 'type': device.type, - 'deleted': device.deleted, - 'user_agent': device.user_agent, - } - ) - - logger.info('Migrading %d sync groups', len(getattr(u, 'sync_groups', []))) - groups = list(SyncGroup.objects.filter(user=user)) - for group_ids in getattr(u, 'sync_groups', []): - try: - group = groups.pop() - except IndexError: - group = SyncGroup.objects.create(user=user) - - # remove all clients from the group - Client.objects.filter(sync_group=group).update(sync_group=None) - - for client_id in group_ids: - client = Client.objects.get(id=client_id) - assert client.user == user - client.sync_group = group - client.save() - - SyncGroup.objects.filter(pk__in=[g.pk for g in groups]).delete() - - -from couchdbkit import Database -db = Database('http://127.0.0.1:6984/mygpo_users') -from couchdbkit.changes import ChangesStream, fold, foreach - - -MIGRATIONS = { - 'User': (U, migrate_user), - 'Suggestions': (None, None), -} - -def migrate_change(c): - logger.info('Migrate seq %s', c['seq']) - doc = c['doc'] - - if not 'doc_type' in doc: - logger.warn('Document contains no doc_type: %r', doc) - return - - doctype = doc['doc_type'] - - cls, migrate = MIGRATIONS[doctype] - - if cls is None: - logger.warn("Skipping '%s'", doctype) - return - - obj = cls.wrap(doc) - migrate(obj) - - -def migrate(since=0): - with ChangesStream(db, - feed="continuous", - heartbeat=True, - include_docs=True, - since=since, - ) as stream: - for change in stream: - migrate_change(change) +from __future__ import unicode_literals + +import json +from datetime import datetime + +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User + +from mygpo.podcasts.models import Tag +from mygpo.users.models import UserProfile, Client, SyncGroup, PodcastUserState +from mygpo.subscriptions.models import Subscription, PodcastConfig +from mygpo.history.models import HistoryEntry +from mygpo.podcasts.models import Podcast + +import logging +logger = logging.getLogger(__name__) + + +def to_maxlength(cls, field, val): + """ Cut val to the maximum length of cls's field """ + max_length = cls._meta.get_field(field).max_length + orig_length = len(val) + if orig_length > max_length: + val = val[:max_length] + logger.warn('%s.%s length reduced from %d to %d', + cls.__name__, field, orig_length, max_length) + + return val + + +def migrate_pstate(state): + """ migrate a podcast state """ + + try: + user = User.objects.get(profile__uuid=state.user) + except User.DoesNotExist: + logger.warn("User with ID '{id}' does not exist".format( + id=state.user)) + return + + try: + podcast = Podcast.objects.all().get_by_any_id(state.podcast) + except Podcast.DoesNotExist: + logger.warn("Podcast with ID '{id}' does not exist".format( + id=state.podcast)) + return + + logger.info('Updating podcast state for user {user} and podcast {podcast}' + .format(user=user, podcast=podcast)) + + # move all tags + for tag in state.tags: + ctype = ContentType.objects.get_for_model(podcast) + tag, created = Tag.objects.get_or_create(tag=tag, + source=Tag.USER, + user=user, + content_type=ctype, + object_id=podcast.id + ) + if created: + logger.info("Created tag '{}' for user {} and podcast {}", + tag, user, podcast) + + # create all history entries + history = HistoryEntry.objects.filter(user=user, podcast=podcast) + for action in state.actions: + timestamp = action.timestamp + client = user.client_set.get(id=action.device) + action = action.action + he_data = { + 'timestamp': timestamp, + 'podcast': podcast, + 'user': user, + 'client': client, + 'action': action, + } + he, created = HistoryEntry.objects.get_or_create(**he_data) + + if created: + logger.info('History Entry created: {user} {action} {podcast} ' + 'on {client} @ {timestamp}'.format(**he_data)) + + # check which clients are currently subscribed + subscribed_devices = get_subscribed_devices(state) + subscribed_ids = subscribed_devices.keys() + subscribed_clients = user.client_set.filter(id__in=subscribed_ids) + unsubscribed_clients = user.client_set.exclude(id__in=subscribed_ids) + + # create subscriptions for subscribed clients + for client in subscribed_clients: + ts = subscribed_devices[client.id.hex] + sub_data = { + 'user': user, + 'client': client, + 'podcast': podcast, + 'ref_url': state.ref_url, + 'created': ts, + 'modified': ts, + 'deleted': client.id.hex in state.disabled_devices, + } + subscription, created = Subscription.objects.get_or_create(**sub_data) + + if created: + logger.info('Subscription created: {user} subscribed to {podcast} ' + 'on {client} @ {created}'.format(**sub_data)) + + # delete all other subscriptions + Subscription.objects.filter(user=user, podcast=podcast, + client__in=unsubscribed_clients).delete() + + # only create the PodcastConfig obj if there are any settings + if state.settings: + logger.info('Updating {num} settings'.format(num=len(state.settings))) + PodcastConfig.objects.update_or_create(user=user, podcast=podcast, + defaults = { + 'settings': json.dumps(state.settings), + } + ) + + +def get_subscribed_devices(state): + """ device Ids on which the user subscribed to the podcast """ + devices = {} + + for action in state.actions: + if action.action == "subscribe": + if not action.device in state.disabled_devices: + devices[action.device] = action.timestamp + else: + if action.device in devices: + devices.pop(action.device) + + return devices + + + +from couchdbkit import Database +db = Database('http://127.0.0.1:5984/mygpo_userdata_copy') +from couchdbkit.changes import ChangesStream, fold, foreach + + +MIGRATIONS = { + 'PodcastUserState': (PodcastUserState, migrate_pstate), + 'User': (None, None), + 'Suggestions': (None, None), + 'EpisodeUserState': (None, None), +} + +def migrate_change(c): + logger.info('Migrate seq %s', c['seq']) + doc = c['doc'] + + if not 'doc_type' in doc: + logger.warn('Document contains no doc_type: %r', doc) + return + + doctype = doc['doc_type'] + + cls, migrate = MIGRATIONS[doctype] + + if cls is None: + logger.warn("Skipping '%s'", doctype) + return + + obj = cls.wrap(doc) + migrate(obj) + + +def migrate(since=0): + with ChangesStream(db, + feed="continuous", + heartbeat=True, + include_docs=True, + since=since, + ) as stream: + for change in stream: + migrate_change(change) diff --git a/mygpo/podcasts/admin.py b/mygpo/podcasts/admin.py index f0de4fe4..c284fcfd 100644 --- a/mygpo/podcasts/admin.py +++ b/mygpo/podcasts/admin.py @@ -187,3 +187,14 @@ class PodcastGroupAdmin(admin.ModelAdmin): inlines = [ PodcastInline, ] + + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + """ Admin page for tags """ + + list_display = ('tag', 'content_object', 'source', 'user', ) + + list_select_related = ('user', ) + + list_filter = ('source', ) diff --git a/mygpo/podcasts/migrations/0023_auto_20140729_1711.py b/mygpo/podcasts/migrations/0023_auto_20140729_1711.py new file mode 100644 index 00000000..4382fac6 --- /dev/null +++ b/mygpo/podcasts/migrations/0023_auto_20140729_1711.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('podcasts', '0022_index_episode_listeners'), + ('auth', '__first__'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='user', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='episode', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='podcast', + name='created', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together=set([('tag', 'source', 'user', 'content_type', 'object_id')]), + ), + ] diff --git a/mygpo/podcasts/models.py b/mygpo/podcasts/models.py index 551a8ab4..7ef3ed64 100644 --- a/mygpo/podcasts/models.py +++ b/mygpo/podcasts/models.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import re from datetime import datetime +from django.conf import settings from django.db import models, transaction, IntegrityError from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericRelation @@ -11,7 +12,8 @@ from django.contrib.contenttypes import generic from uuidfield import UUIDField from mygpo import utils -from mygpo.core.models import TwitterModel, UUIDModel, GenericManager +from mygpo.core.models import (TwitterModel, UUIDModel, GenericManager, + UpdateInfoModel) import logging logger = logging.getLogger(__name__) @@ -84,18 +86,6 @@ class LastUpdateModel(models.Model): abstract = True -class UpdateInfoModel(models.Model): - - # this does not use "auto_now_add=True" so that data - # can be migrated with its creation timestamp intact; it can be - # switched on after the migration is complete - created = models.DateTimeField(default=datetime.utcnow) - modified = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True - - class LicenseModel(models.Model): # URL to a license (usually Creative Commons) license = models.CharField(max_length=100, null=True, blank=False, @@ -720,8 +710,14 @@ class Tag(models.Model): ) tag = models.SlugField() + + # indicates where the tag came from source = models.PositiveSmallIntegerField(choices=SOURCE_CHOICES) - #user = models.ForeignKey(null=True) + + # the user that created the tag (if it was created by a user, + # null otherwise) + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, + on_delete=models.CASCADE) # see https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#generic-relations content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT) @@ -731,8 +727,7 @@ class Tag(models.Model): class Meta: unique_together = ( # a tag can only be assigned once from one source to one item - # TODO: add user to tuple - ('tag', 'source', 'content_type', 'object_id'), + ('tag', 'source', 'user', 'content_type', 'object_id'), ) diff --git a/mygpo/settings.py b/mygpo/settings.py index d0ea684a..e4c94474 100644 --- a/mygpo/settings.py +++ b/mygpo/settings.py @@ -136,6 +136,8 @@ INSTALLED_APPS = ( 'mygpo.api', 'mygpo.web', 'mygpo.publisher', + 'mygpo.subscriptions', + 'mygpo.history', 'mygpo.data', 'mygpo.userfeeds', 'mygpo.directory', diff --git a/mygpo/subscriptions/__init__.py b/mygpo/subscriptions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mygpo/subscriptions/admin.py b/mygpo/subscriptions/admin.py new file mode 100644 index 00000000..cf569c71 --- /dev/null +++ b/mygpo/subscriptions/admin.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +from django.contrib import admin + +from mygpo.subscriptions.models import Subscription, PodcastConfig + + +@admin.register(Subscription) +class SubscriptionAdmin(admin.ModelAdmin): + """ Admin page for subscriptions """ + + # configuration for the list view + list_display = ('user', 'podcast', 'client', ) + + # fetch the related objects for the fields in list_display + list_select_related = ('user', 'podcast', 'client', ) + + raw_id_fields = ('user', 'podcast', 'client', ) + + +@admin.register(PodcastConfig) +class PodcastConfigAdmin(admin.ModelAdmin): + + # configuration for the list view + list_display = ('user', 'podcast', ) + + # fetch the related objects for the fields in list_display + list_select_related = ('user', 'podcast', ) + + raw_id_fields = ('user', 'podcast', ) diff --git a/mygpo/subscriptions/migrations/0001_initial.py b/mygpo/subscriptions/migrations/0001_initial.py new file mode 100644 index 00000000..9cc6a24f --- /dev/null +++ b/mygpo/subscriptions/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_syncgroup_protect'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('podcasts', '0023_auto_20140729_1711'), + ] + + operations = [ + migrations.CreateModel( + name='PodcastConfig', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('settings', models.TextField(default=b'{}')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('podcast', models.ForeignKey(to='podcasts.Podcast', on_delete=django.db.models.deletion.PROTECT)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('deleted', models.BooleanField(default=False)), + ('ref_url', models.URLField(max_length=2048)), + ('created', models.DateTimeField()), + ('modified', models.DateTimeField()), + ('client', models.ForeignKey(to='users.Client')), + ('podcast', models.ForeignKey(to='podcasts.Podcast', on_delete=django.db.models.deletion.PROTECT)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='subscription', + unique_together=set([(b'user', b'client', b'podcast')]), + ), + migrations.AlterIndexTogether( + name='subscription', + index_together=set([(b'user', b'client')]), + ), + ] diff --git a/mygpo/subscriptions/migrations/__init__.py b/mygpo/subscriptions/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mygpo/subscriptions/models.py b/mygpo/subscriptions/models.py new file mode 100644 index 00000000..e6673417 --- /dev/null +++ b/mygpo/subscriptions/models.py @@ -0,0 +1,67 @@ +from django.db import models +from django.conf import settings + +from mygpo.core.models import UpdateInfoModel, DeleteableModel, SettingsModel +from mygpo.users.models import Client +from mygpo.podcasts.models import Podcast + + +class SubscriptionManager(models.Manager): + """ Manages subscriptions """ + + def subscribe(self, device, podcast): + # create subscription, add history + pass + + def unsubscribe(self, device, podcast): + # delete subscription, add history + pass + + +class Subscription(DeleteableModel): + """ A subscription to a podcast on a specific client """ + + # the user that subscribed to a podcast + user = models.ForeignKey(settings.AUTH_USER_MODEL, db_index=True, + on_delete=models.CASCADE) + + # the client on which the user subscribed to the podcast + client = models.ForeignKey(Client, db_index=True, + on_delete=models.CASCADE) + + # the podcast to which the user subscribed to + podcast = models.ForeignKey(Podcast, db_index=True, + on_delete=models.PROTECT) + + # the URL that the user subscribed to; a podcast might have multiple URLs, + # the we want to return the users the ones they know + ref_url = models.URLField(max_length=2048) + + # the following fields do not use auto_now[_add] for the time of the + # migration, in order to store historically accurate data; once the + # migration is complete, this model should inherit from UpdateInfoModel + created = models.DateTimeField() + modified = models.DateTimeField() + + objects = SubscriptionManager() + + class Meta: + unique_together = [ + ['user', 'client', 'podcast'], + ] + + index_together = [ + ['user', 'client'] + ] + + +class PodcastConfig(SettingsModel, UpdateInfoModel): + """ Settings for a podcast, independant of a device / subscription """ + + # the user for which the config is stored + user = models.ForeignKey(settings.AUTH_USER_MODEL, db_index=True, + on_delete=models.CASCADE) + + # the podcast for which the config is stored + podcast = models.ForeignKey(Podcast, db_index=True, + on_delete=models.PROTECT) diff --git a/mygpo/subscriptions/tests.py b/mygpo/subscriptions/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/mygpo/subscriptions/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/mygpo/subscriptions/views.py b/mygpo/subscriptions/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/mygpo/subscriptions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/mygpo/users/models.py b/mygpo/users/models.py index ad0c9866..97883f9f 100644 --- a/mygpo/users/models.py +++ b/mygpo/users/models.py @@ -22,7 +22,7 @@ from django.core.cache import cache from django.contrib import messages from mygpo.core.models import (TwitterModel, UUIDModel, SettingsModel, - GenericManager, ) + GenericManager, DeleteableModel, ) from mygpo.podcasts.models import Podcast, Episode from mygpo.utils import random_token from mygpo.core.proxy import DocumentABCMeta, proxy_object @@ -568,7 +568,7 @@ class SyncGroup(models.Model): -class Client(UUIDModel): +class Client(UUIDModel, DeleteableModel): """ A client application """ DESKTOP = 'desktop' @@ -601,9 +601,6 @@ class Client(UUIDModel): type = models.CharField(max_length=max(len(k) for k, v in TYPES), choices=TYPES, default=OTHER) - # indicates if the user has deleted the client - deleted = models.BooleanField(default=False) - # user-agent string from which the Client was last accessed (for writing) user_agent = models.CharField(max_length=300, null=True, blank=True) -- 2.11.4.GIT