1 from __future__
import unicode_literals
3 from django
.db
import models
4 from django
.contrib
.contenttypes
.models
import ContentType
5 from django
.contrib
.contenttypes
.fields
import GenericRelation
6 from django
.contrib
.contenttypes
import generic
8 from uuidfield
import UUIDField
11 class UUIDModel(models
.Model
):
12 """ Models that have an UUID as primary key """
14 id = UUIDField(primary_key
=True)
20 class TitleModel(models
.Model
):
21 """ Model that has a title """
23 title
= models
.CharField(max_length
=1000, null
=False, blank
=True,
25 subtitle
= models
.CharField(max_length
=1000, null
=False, blank
=True)
28 return self
.title
.encode('ascii', errors
='replace')
37 class DescriptionModel(models
.Model
):
38 """ Model that has a description """
40 description
= models
.TextField(null
=False, blank
=True)
46 class LinkModel(models
.Model
):
47 """ Model that has a link """
49 link
= models
.URLField(null
=True, max_length
=1000)
55 class LanguageModel(models
.Model
):
56 """ Model that has a language """
58 language
= models
.CharField(max_length
=10, null
=True, blank
=False,
65 class LastUpdateModel(models
.Model
):
66 """ Model with timestamp of last update from its source """
68 # date and time at which the model has last been updated from its source
69 # (eg a podcast feed). None means that the object has been created as a
70 # stub, without information from the source.
71 last_update
= models
.DateTimeField(null
=True)
77 class UpdateInfoModel(models
.Model
):
79 # this does not use "auto_now_add=True" so that data
80 # can be migrated with its creation timestamp intact; it can be
81 # switched on after the migration is complete
82 created
= models
.DateTimeField()
83 modified
= models
.DateTimeField(auto_now
=True)
89 class LicenseModel(models
.Model
):
90 # URL to a license (usually Creative Commons)
91 license
= models
.CharField(max_length
=100, null
=True, blank
=False,
98 class FlattrModel(models
.Model
):
99 # A Flattr payment URL
100 flattr_url
= models
.URLField(null
=True, blank
=False, max_length
=1000,
107 class ContentTypesModel(models
.Model
):
108 # contains a comma-separated values of content types, eg 'audio,video'
109 content_types
= models
.CharField(max_length
=20, null
=False, blank
=True)
115 class MergedIdsModel(models
.Model
):
121 class OutdatedModel(models
.Model
):
122 outdated
= models
.BooleanField(default
=False, db_index
=True)
128 class AuthorModel(models
.Model
):
129 author
= models
.CharField(max_length
=150, null
=True, blank
=True)
135 class UrlsMixin(models
.Model
):
136 """ Methods for working with URL objects """
138 urls
= GenericRelation('URL', related_query_name
='urls')
144 class SlugsMixin(models
.Model
):
145 """ Methods for working with Slug objects """
147 slugs
= GenericRelation('Slug', related_query_name
='slugs')
154 """ The main slug of the podcast
156 TODO: should be retrieved from a (materialized) view """
157 slug
= self
.slugs
.first()
163 class MergedUUIDsMixin(models
.Model
):
164 """ Methods for working with MergedUUID objects """
166 merged_uuids
= GenericRelation('MergedUUID',
167 related_query_name
='merged_uuids')
172 class TagsMixin(models
.Model
):
173 """ Methods for working with Tag objects """
175 tags
= GenericRelation('Tag', related_query_name
='tags')
181 class OrderedModel(models
.Model
):
182 """ A model that can be ordered
184 The implementing Model must make sure that 'order' is sufficiently unique
187 order
= models
.PositiveSmallIntegerField()
194 class PodcastGroup(UUIDModel
, TitleModel
, SlugsMixin
):
195 """ Groups multiple podcasts together """
198 class PodcastQuerySet(models
.QuerySet
):
199 """ Custom queries for Podcasts """
204 Excludes podcasts with missing title to guarantee some
205 minimum quality of the results """
206 return self
.exclude(title
='').order_by('?')
209 """ Podcasts providing Flattr information """
210 return self
.exclude(flattr_url__isnull
=True)
212 def license(self
, license_url
=None):
213 """ Podcasts with any / the given license """
215 return self
.filter(license
=license_url
)
217 return self
.exclude(license__isnull
=True)
221 class Podcast(UUIDModel
, TitleModel
, DescriptionModel
, LinkModel
,
222 LanguageModel
, LastUpdateModel
, UpdateInfoModel
, LicenseModel
,
223 FlattrModel
, ContentTypesModel
, MergedIdsModel
, OutdatedModel
,
224 AuthorModel
, UrlsMixin
, SlugsMixin
, TagsMixin
, MergedUUIDsMixin
):
227 logo_url
= models
.URLField(null
=True, max_length
=1000)
228 group
= models
.ForeignKey(PodcastGroup
, null
=True)
229 group_member_name
= models
.CharField(max_length
=30, null
=True, blank
=False)
231 # if p1 is related to p2, p2 is also related to p1
232 related_podcasts
= models
.ManyToManyField('self', symmetrical
=True)
234 #subscribers = SchemaListProperty(SubscriberData)
235 restrictions
= models
.CharField(max_length
=20, null
=True, blank
=True)
236 common_episode_title
= models
.CharField(max_length
=50, null
=False, blank
=True)
237 new_location
= models
.URLField(max_length
=1000, null
=True, blank
=False)
238 latest_episode_timestamp
= models
.DateTimeField(null
=True)
239 episode_count
= models
.PositiveIntegerField(default
=0)
240 hub
= models
.URLField(null
=True)
241 twitter
= models
.CharField(max_length
=15, null
=True, blank
=False)
243 objects
= PodcastQuerySet
.as_manager()
245 def subscriber_count(self
):
250 class Episode(UUIDModel
, TitleModel
, DescriptionModel
, LinkModel
,
251 LanguageModel
, LastUpdateModel
, UpdateInfoModel
, LicenseModel
,
252 FlattrModel
, ContentTypesModel
, MergedIdsModel
, OutdatedModel
,
253 AuthorModel
, UrlsMixin
, SlugsMixin
, MergedUUIDsMixin
):
256 guid
= models
.CharField(max_length
=200, null
=True)
257 content
= models
.TextField()
258 released
= models
.DateTimeField(null
=True)
259 duration
= models
.PositiveIntegerField(null
=True)
260 filesize
= models
.BigIntegerField(null
=True)
261 mimetypes
= models
.CharField(max_length
=50)
262 podcast
= models
.ForeignKey(Podcast
)
263 listeners
= models
.PositiveIntegerField(null
=True)
266 ordering
= ['-released']
269 class ScopedModel(models
.Model
):
271 # A slug / URL is unique within a scope; no two podcasts can have the same
272 # URL (scope None), and no two episdoes of the same podcast (scope =
273 # podcast-ID) can have the same URL
274 scope
= UUIDField(null
=True, db_index
=True)
280 class URL(OrderedModel
, ScopedModel
):
281 """ Podcasts and Episodes can have multiple URLs
283 URLs are ordered, and the first slug is considered the canonical one """
285 url
= models
.URLField(max_length
=2048)
287 # see https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#generic-relations
288 content_type
= models
.ForeignKey(ContentType
)
289 object_id
= UUIDField()
290 content_object
= generic
.GenericForeignKey('content_type', 'object_id')
294 # a URL is unique per scope
297 # URLs of an object must be ordered, so that no two slugs of one
298 # object have the same order key
299 ('content_type', 'object_id', 'order'),
303 verbose_name_plural
= 'URLs'
306 class Tag(models
.Model
):
307 """ Tags any kind of Model
309 See also :class:`TagsMixin`
318 (DELICIOUS
, 'delicious'),
322 tag
= models
.SlugField()
323 source
= models
.PositiveSmallIntegerField(choices
=SOURCE_CHOICES
)
324 #user = models.ForeignKey(null=True)
326 # see https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#generic-relations
327 content_type
= models
.ForeignKey(ContentType
)
328 object_id
= UUIDField()
329 content_object
= generic
.GenericForeignKey('content_type', 'object_id')
333 # a tag can only be assigned once from one source to one item
334 # TODO: add user to tuple
335 ('tag', 'source', 'content_type', 'object_id'),
339 class Slug(OrderedModel
, ScopedModel
):
340 """ Slug for any kind of Model
342 Slugs are ordered, and the first slug is considered the canonical one.
343 See also :class:`SlugsMixin`
346 slug
= models
.SlugField(max_length
=150, db_index
=True)
348 # see https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#generic-relations
349 content_type
= models
.ForeignKey(ContentType
)
350 object_id
= UUIDField()
351 content_object
= generic
.GenericForeignKey('content_type', 'object_id')
355 # a slug is unique per type; eg a podcast can have the same slug
356 # as an episode, but no two podcasts can have the same slug
359 # slugs of an object must be ordered, so that no two slugs of one
360 # object have the same order key
361 ('content_type', 'object_id', 'order'),
365 return '{cls}(slug={slug}, order={order}, content_object={obj}'.format(
366 cls
=self
.__class
__.__name
__,
369 obj
=self
.content_object
373 class MergedUUID(models
.Model
):
374 """ If objects are merged their UUIDs are stored for later reference
376 see also :class:`MergedUUIDsMixin`
379 uuid
= UUIDField(unique
=True)
381 # see https://docs.djangoproject.com/en/1.6/ref/contrib/contenttypes/#generic-relations
382 content_type
= models
.ForeignKey(ContentType
)
383 object_id
= UUIDField()
384 content_object
= generic
.GenericForeignKey('content_type', 'object_id')
387 verbose_name
= 'Merged UUID'
388 verbose_name_plural
= 'Merged UUIDs'