diff --git a/programs/admin.py b/programs/admin.py index d7f3b6c..2da7c0c 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -32,72 +32,49 @@ class DiffusionInline (admin.TabularInline): class TrackInline (SortableTabularInline): - fields = ['artist', 'title', 'tags', 'position'] + fields = ['artist', 'name', 'tags', 'position'] form = TrackForm model = Track sortable = 'position' extra = 10 -class MetadataAdmin (admin.ModelAdmin): - fieldsets = [ - ( None, { - 'fields': [ 'title', 'tags' ] - }), - ( None, { - 'fields': [ 'date', 'public' ], - }), - ] +class DescriptionAdmin (admin.ModelAdmin): + fields = [ 'name', 'tags', 'description' ] - def save_model (self, request, obj, form, change): - # FIXME: if request.data.author? - if not obj.author: - obj.author = request.user - obj.save() + def tags (obj): + return ', '.join(obj.tags.names()) - -class PublicationAdmin (MetadataAdmin): - fieldsets = copy.deepcopy(MetadataAdmin.fieldsets) - - list_display = ('id', 'title', 'date', 'public', 'parent') - list_filter = ['date', 'public', 'parent', 'author'] - list_editable = ('public',) - search_fields = ['title', 'content'] - - fieldsets[0][1]['fields'].insert(1, 'subtitle') - fieldsets[0][1]['fields'] += [ 'img', 'content' ] - fieldsets[1][1]['fields'] += [ 'parent' ] #, 'meta' ], + list_display = ['id', 'name', tags] + list_filter = [] + search_fields = ['name',] @admin.register(Sound) -class SoundAdmin (MetadataAdmin): +class SoundAdmin (DescriptionAdmin): + fields = None fieldsets = [ - (None, { 'fields': ['title', 'tags', 'path' ] } ), + (None, { 'fields': DescriptionAdmin.fields + ['path' ] } ), (None, { 'fields': ['duration', 'date', 'fragment' ] } ) ] @admin.register(Stream) class StreamAdmin (SortableModelAdmin): - list_display = ('id', 'title', 'type', 'public', 'priority') - list_editable = ('public',) + list_display = ('id', 'name', 'type', 'priority') sortable = "priority" @admin.register(Program) -class ProgramAdmin (PublicationAdmin): - fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) +class ProgramAdmin (DescriptionAdmin): + fields = DescriptionAdmin.fields + ['stream'] inlines = [ ScheduleInline ] - fieldsets[1][1]['fields'] += ['email', 'url'] - @admin.register(Episode) -class EpisodeAdmin (PublicationAdmin): - fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) - list_filter = ['parent'] + PublicationAdmin.list_filter - - fieldsets[0][1]['fields'] += ['sounds'] +class EpisodeAdmin (DescriptionAdmin): + list_filter = ['program'] + DescriptionAdmin.list_filter + fields = DescriptionAdmin.fields + ['sounds'] inlines = (TrackInline, DiffusionInline) diff --git a/programs/autocomplete_light_registry.py b/programs/autocomplete_light_registry.py index fd75aac..0579c41 100644 --- a/programs/autocomplete_light_registry.py +++ b/programs/autocomplete_light_registry.py @@ -33,12 +33,12 @@ class TrackArtistAutocomplete(OneFieldAutocomplete): al.register(TrackArtistAutocomplete) -class TrackTitleAutocomplete(OneFieldAutocomplete): - search_fields = ['title'] +class TrackNameAutocomplete(OneFieldAutocomplete): + search_fields = ['name'] model = Track -al.register(TrackTitleAutocomplete) +al.register(TrackNameAutocomplete) #class DiffusionAutocomplete(OneFieldAutocomplete): diff --git a/programs/forms.py b/programs/forms.py index 828d541..b4df14e 100644 --- a/programs/forms.py +++ b/programs/forms.py @@ -10,10 +10,10 @@ from programs.models import * class TrackForm (forms.ModelForm): class Meta: model = Track - fields = ['artist', 'title', 'tags', 'position'] + fields = ['artist', 'name', 'tags', 'position'] widgets = { 'artist': al.TextWidget('TrackArtistAutocomplete'), - 'title': al.TextWidget('TrackTitleAutocomplete'), + 'name': al.TextWidget('TrackNameAutocomplete'), 'tags': TaggitWidget('TagAutocomplete'), } diff --git a/programs/management/commands/diffusions_monitor.py b/programs/management/commands/diffusions_monitor.py index 60f8eaf..06833a1 100644 --- a/programs/management/commands/diffusions_monitor.py +++ b/programs/management/commands/diffusions_monitor.py @@ -24,7 +24,7 @@ class Actions: @staticmethod def update (date): items = [] - for schedule in Schedule.objects.filter(parent__active = True): + for schedule in Schedule.objects.filter(program__active = True): items += schedule.diffusions_of_month(date, exclude_saved = True) print('> {} new diffusions for schedule #{} ({})'.format( len(items), schedule.id, str(schedule) @@ -47,7 +47,7 @@ class Actions: date__gt = date) items = [] for diffusion in qs: - schedules = Schedule.objects.filter(parent = diffusion.program) + schedules = Schedule.objects.filter(program = diffusion.program) for schedule in schedules: if schedule.match(diffusion.date): break diff --git a/programs/models.py b/programs/models.py index 9a50878..5ed62ca 100755 --- a/programs/models.py +++ b/programs/models.py @@ -1,10 +1,7 @@ import os from django.db import models -from django.contrib.auth.models import User from django.template.defaultfilters import slugify -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _, ugettext_lazy from django.utils import timezone as tz from django.utils.html import strip_tags @@ -27,27 +24,15 @@ def date_or_default (date, date_only = False): return date -class Metadata (models.Model): - """ - meta is used to extend a model for future needs - """ - author = models.ForeignKey ( - User, - verbose_name = _('author'), - blank = True, null = True, - ) - title = models.CharField( - _('title'), +class Description (models.Model): + name = models.CharField ( + _('name'), max_length = 128, ) - date = models.DateTimeField( - _('date'), - default = tz.datetime.now, - ) - public = models.BooleanField( - _('public'), - default = True, - help_text = _('publication is public'), + description = models.TextField ( + _('description'), + max_length = 1024, + blank = True, null = True ) tags = TaggableManager( _('tags'), @@ -55,68 +40,18 @@ class Metadata (models.Model): ) def get_slug_name (self): - return slugify(self.title) - - class Meta: - abstract = True - - -class Publication (Metadata): - subtitle = models.CharField( - _('subtitle'), - max_length = 128, - blank = True, - ) - img = models.ImageField( - _('image'), - upload_to = "images", - blank = True, - ) - content = models.TextField( - _('content'), - blank = True, - ) - commentable = models.BooleanField( - _('enable comments'), - default = True, - help_text = _('comments are enabled on this publication'), - ) - - @staticmethod - def _exclude_args (allow_unpublished = False, prefix = ''): - if allow_unpublished: - return {} - - res = {} - res[prefix + 'public'] = False - res[prefix + 'date__gt'] = tz.now() - return res - - @classmethod - def get_available (cl, first = False, **kwargs): - """ - Return the result of filter(kargs) if the resulting publications - is published and public - - Otherwise, return None - """ - kwargs['public'] = True - kwargs['date__lte'] = tz.now() - - e = cl.objects.filter(**kwargs) - - if first: - return (e and e[0]) or None - return e or None + return slugify(self.name) def __str__ (self): - return self.title + ' (' + str(self.id) + ')' + if self.pk: + return '#{} {}'.format(self.pk, self.name) + return '{}'.format(self.name) class Meta: abstract = True -class Track (models.Model): +class Track (Description): # There are no nice solution for M2M relations ship (even without # through) in django-admin. So we unfortunately need to make one- # to-one relations and add a position argument @@ -127,13 +62,8 @@ class Track (models.Model): _('artist'), max_length = 128, ) - title = models.CharField( - _('title'), - max_length = 128, - ) - tags = TaggableManager( blank = True ) - # position can be used to specify a position in seconds for non-stop - # programs or a position in the playlist + # position can be used to specify a position in seconds for non- + # stop programs or a position in the playlist position = models.SmallIntegerField( default = 0, help_text=_('position in the playlist'), @@ -147,7 +77,7 @@ class Track (models.Model): verbose_name_plural = _('Tracks') -class Sound (Metadata): +class Sound (Description): """ A Sound is the representation of a sound, that can be: - An episode podcast/complete record @@ -158,7 +88,7 @@ class Sound (Metadata): public, then we can podcast it. If a Sound is a fragment, then it is not usable for diffusion. - Each sound file can be associated to a filesystem's file or an embedded + Each sound can be associated to a filesystem's file or an embedded code (for external podcasts). """ path = models.FilePathField( @@ -177,10 +107,15 @@ class Sound (Metadata): _('duration'), blank = True, null = True, ) + public = models.BooleanField( + _('public'), + default = False, + help_text = _("the element is public"), + ) fragment = models.BooleanField( _('incomplete sound'), default = False, - help_text = _("the file has been cut"), + help_text = _("the file is a cut"), ) removed = models.BooleanField( default = False, @@ -198,6 +133,7 @@ class Sound (Metadata): def save (self, *args, **kwargs): if not self.pk: self.date = self.get_mtime() + super(Sound, self).save(*args, **kwargs) def __str__ (self): @@ -228,7 +164,7 @@ class Schedule (models.Model): for key, value in Frequency.items(): ugettext_lazy(key) - parent = models.ForeignKey( + program = models.ForeignKey( 'Program', blank = True, null = True, ) @@ -243,7 +179,7 @@ class Schedule (models.Model): rerun = models.ForeignKey( 'self', blank = True, null = True, - help_text = "Schedule of a rerun", + help_text = "Schedule of a rerun of this one", ) def match (self, date = None, check_time = True): @@ -373,7 +309,7 @@ class Diffusion (models.Model): 'default': 0x00, # simple diffusion (done/planed) 'unconfirmed': 0x01, # scheduled by the generator but not confirmed for diffusion 'cancel': 0x02, # cancellation happened; used to inform users - 'restart': 0x03, # manual restart; used to remix/give up antenna + # 'restart': 0x03, # manual restart; used to remix/give up antenna 'stop': 0x04, # diffusion has been forced to stop } for key, value in Type.items(): @@ -424,12 +360,17 @@ class Stream (models.Model): for key, value in Type.items(): ugettext_lazy(key) - title = models.CharField( - _('title'), + name = models.CharField( + _('name'), max_length = 32, blank = True, null = True, ) + public = models.BooleanField( + _('public'), + default = True, + help_text = _('program list is public'), + ) type = models.SmallIntegerField( verbose_name = _('type'), choices = [ (y, x) for x,y in Type.items() ], @@ -439,11 +380,6 @@ class Stream (models.Model): default = 0, help_text = _('priority of the stream') ) - public = models.BooleanField( - _('public'), - default = True, - help_text = _('program list is public'), - ) # get info for: # - random lists @@ -453,23 +389,14 @@ class Stream (models.Model): # - stream/pgm def __str__ (self): - return '#{} {}'.format(self.priority, self.title) + return '#{} {}'.format(self.priority, self.name) -class Program (Publication): - parent = models.ForeignKey( +class Program (Description): + stream = models.ForeignKey( Stream, verbose_name = _('stream'), ) - email = models.EmailField( - _('email'), - max_length = 128, - null = True, blank = True, - ) - url = models.URLField( - _('website'), - blank = True, null = True, - ) active = models.BooleanField( _('inactive'), default = True, @@ -491,8 +418,8 @@ class Program (Publication): return schedule -class Episode (Publication): - parent = models.ForeignKey( +class Episode (Description): + program = models.ForeignKey( Program, verbose_name = _('parent'), help_text = _('parent program'), diff --git a/website/admin.py b/website/admin.py index 6a94c6e..bc516fa 100644 --- a/website/admin.py +++ b/website/admin.py @@ -1,15 +1,42 @@ import copy from django.contrib import admin +from django.utils.translation import ugettext as _, ugettext_lazy +from django.contrib.contenttypes.admin import GenericStackedInline -from programs.admin import PublicationAdmin +import programs.models as programs from website.models import * -@admin.register(Article) -class ArticleAdmin (PublicationAdmin): - fieldsets = copy.deepcopy(PublicationAdmin.fieldsets) - fieldsets[1][1]['fields'] += ['static_page'] +def add_inline (base_model, post_model, prepend = False): + class InlineModel (GenericStackedInline): + model = post_model + extra = 1 + max_num = 1 + ct_field = 'object_type' + verbose_name = _('Post') + + registry = admin.site._registry + if not base_model in registry: + raise TypeError(str(base_model) + " not in admin registry") + + inlines = list(registry[base_model].inlines) or [] + if prepend: + inlines.insert(0, InlineModel) + else: + inlines.append(InlineModel) + + registry[base_model].inlines = inlines + + +add_inline(Program, ObjectDescription) +add_inline(Episode, ObjectDescription) + + +#class ArticleAdmin (DescriptionAdmin): +# fieldsets = copy.deepcopy(DescriptionAdmin.fieldsets) +# +# fieldsets[1][1]['fields'] += ['static_page'] diff --git a/website/models.py b/website/models.py index 22cc276..90fa561 100644 --- a/website/models.py +++ b/website/models.py @@ -1,15 +1,120 @@ from django.db import models +from django.contrib.auth.models import User +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _, ugettext_lazy +from django.utils import timezone -from programs.models import Publication +from django.db.models.signals import post_save +from django.dispatch import receiver + +from programs.models import * -class Article (Publication): - parent = models.ForeignKey( - 'self', - verbose_name = _('parent'), +class Thread (models.Model): + post_type = models.ForeignKey(ContentType) + post_id = models.PositiveIntegerField() + post = GenericForeignKey('post_type', 'post_id') + + @classmethod + def get (cl, model, **kwargs): + post_type = ContentType.objects.get_for_model(model) + return cl.objects.get(post_type__pk = post_type.id, + **kwargs) + + @classmethod + def filter (cl, model, **kwargs): + post_type = ContentType.objects.get_for_model(model) + return cl.objects.filter(post_type__pk = post_type.id, + **kwargs) + + @classmethod + def exclude (cl, model, **kwargs): + post_type = ContentType.objects.get_for_model(model) + return cl.objects.exclude(post_type__pk = post_type.id, + **kwargs) + + def __str__ (self): + return str(self.post) + + +class Post (models.Model): + thread = models.ForeignKey( + Thread, + on_delete=models.SET_NULL, blank = True, null = True, - help_text = _('parent article'), + help_text = _('the publication is posted on this thread'), + ) + author = models.ForeignKey( + User, + verbose_name = _('author'), + blank = True, null = True, + ) + date = models.DateTimeField( + _('date'), + default = timezone.datetime.now + ) + public = models.BooleanField( + verbose_name = _('public'), + default = True + ) + image = models.ImageField( + blank = True, null = True + ) + + + def as_dict (self): + d = {} + d.update(self.__dict__) + d.update({ + 'title': self.get_title(), + 'image': self.get_image(), + 'date': self.get_date(), + 'content': self.get_content() + }) + + def get_detail_url (self): + pass + + def get_image (self): + return self.image + + def get_date (self): + return self.date + + def get_title (self): + pass + + def get_content (self): + pass + + class Meta: + abstract = True + + +@receiver(post_save) +def on_new_post (sender, instance, created, *args, **kwargs): + """ + Signal handler to create a thread that is attached to the newly post + """ + if not issubclass(sender, Post) or not created: + return + + thread = Thread(post = instance) + thread.save() + + +class ObjectDescription (Post): + object_type = models.ForeignKey(ContentType, blank = True, null = True) + object_id = models.PositiveIntegerField(blank = True, null = True) + object = GenericForeignKey('object_type', 'object_id') + + + +class Article (Post): + title = models.CharField( + _('title'), + max_length = 128, ) static_page = models.BooleanField( _('static page'), @@ -19,17 +124,25 @@ class Article (Publication): _('article is focus'), default = False, ) - referring_tag = models.CharField( - _('referring tag'), - max_length = 32, - blank = True, null = True, - help_text = _('tag used by other to refers to this article'), - ) class Meta: verbose_name = _('Article') verbose_name_plural = _('Articles') - +#class MenuItem (): +# Menu = { +# 'top': 0x00, +# 'sidebar': 0x01, +# 'bottom': 0x02, +# } +# for key, value in Type.items(): +# ugettext_lazy(key) +# +# parent = models.ForeignKey( +# 'self', +# blank = True, null = True +# ) +# menu = models.SmallIntegerField( +# ) diff --git a/website/routes.py b/website/routes.py new file mode 100644 index 0000000..18d7f15 --- /dev/null +++ b/website/routes.py @@ -0,0 +1,139 @@ +from django.conf.urls import url +from django.utils import timezone + +from website.models import * + +class Routes: + registry = [] + + def register (self, route): + if not route in self.registry: + self.registry.append(route) + + def unregister (self, route): + self.registry.remove(route) + + def get_urlpatterns (self): + patterns = [] + for route in self.registry: + patterns += route.get_urlpatterns() or [] + return patterns + + +class Route: + """ + Base class for routing. Given a model, we generate url specific for each + route type. The generated url takes this form: + base_name + '/' + route_name + '/' + '/'.join(route_url_args) + + Where base_name by default is the given model's verbose_name (uses plural if + Route is for a list). + + The given view is considered as a django class view, and has view_ + """ + model = None # model routed here + view = None # view class to call + view_kwargs = None # arguments passed to view at creation of the urls + + class Meta: + name = None # route name + is_list = False # route is for a list + url_args = [] # arguments passed from the url [ (name : regex),... ] + + def __init__ (self, model, view, view_kwargs = None, + base_name = None): + self.model = model + self.view = view + self.view_kwargs = view_kwargs + self.embed = False + + _meta = {} + _meta.update(Route.Meta.__dict__) + _meta.update(self.Meta.__dict__) + self._meta = _meta + + if not base_name: + base_name = model._meta.verbose_name_plural if _meta['is_list'] \ + else model._meta.verbose_name + base_name = base_name.title().lower() + self.base_name = base_name + + def get_queryset (self, request, **kwargs): + """ + Called by the view to get the queryset when it is needed + """ + + def get_urlpatterns (self): + view_kwargs = self.view_kwargs or {} + + pattern = '^{}/{}'.format(self.base_name, self.Meta.name) + + if self.view.Meta.formats: + pattern += '(/(?P{}))?'.format('|'.join(self.view.Meta.formats)) + + if self._meta['url_args']: + url_args = '/'.join([ '(?P<{}>{})'.format(arg, expr) \ + for arg, expr in self._meta['url_args'] + ]) + pattern += '/' + url_args + pattern += '/?$' + + return [ url( + pattern, + self.view and self.view.as_view( + route = self, + model = self.model, + **view_kwargs + ), + name = '{}' + ) ] + + +class SearchRoute (Route): + class Meta: + name = 'search' + is_list = True + + def get_queryset (self, request, **kwargs): + q = request.GET.get('q') or '' + qs = self.model.objects + for search_field in model.search_fields or []: + r = self.model.objects.filter(**{ search_field + '__icontains': q }) + if qs: qs = qs | r + else: qs = r + + qs.distinct() + return qs + + +class ThreadRoute (Route): + class Meta: + name = 'thread' + is_list = True + url_args = [ + ('pk', '[0-9]+') + ] + + def get_queryset (self, request, **kwargs): + return self.model.objects.filter(thread__id = int(kwargs['pk'])) + + +class DateRoute (Route): + class Meta: + name = 'date' + is_list = True + url_args = [ + ('year', '[0-9]{4}'), + ('month', '[0-9]{2}'), + ('day', '[0-9]{1,2}'), + ] + + def get_queryset (self, request, **kwargs): + return self.model.objects.filter( + date__year = int(kwargs['year']), + date__month = int(kwargs['month']), + date__day = int(kwargs['day']), + ) + + + diff --git a/website/templates/website/list.html b/website/templates/website/list.html new file mode 100644 index 0000000..baac919 --- /dev/null +++ b/website/templates/website/list.html @@ -0,0 +1,59 @@ +{# Parameters are: #} +{# * pub: publication itself; pub.meta must have been eval() #} +{# * threads: list of parent, from top to bottom, including itself #} +{# #} +{# * views: a view object used to know which view to use for links #} +{# #} +{# {% extends embed|yesno:"website/single.html,website/base.html" %} #} + +{% load i18n %} +{% load thumbnail %} +{# {% load website_views %} #} + + +
+{% for post in object_list %} + + + {% if 'date' in list.fields or 'time' in list.fields %} + {% with post_date=post.get_date %} + + {% endwith %} + {% endif %} + + {% if 'image' in list.fields %} + {% with post_image=post.get_image %} + + {% endwith %} + {% endif %} + + {% if 'title' in list.fields %} + {% with post_title=post.get_title %} +

post_title

+ {% endwith %} + {% endif %} + + {% if 'content' in list.fields %} + {% with post_content=post.get_content %} +
+ {{ post_content|safe|striptags|truncatechars:"64" }} +
+ {% endwith %} + {% endif %} +
+{% endfor %} +
+ + diff --git a/website/urls.py b/website/urls.py new file mode 100644 index 0000000..b477c29 --- /dev/null +++ b/website/urls.py @@ -0,0 +1,24 @@ +from django.conf.urls import url, include + +from website.models import * +from website.views import * +from website.routes import * + + +routes = Routes() + +routes.register( SearchRoute(Article, PostListView) ) +#routes.register( SearchRoute(ProgramPost, PostListView, base_name = 'programs') ) +#routes.register( SearchRoute(EpisodePost, PostListView, base_name = 'episodes') ) + +routes.register( ThreadRoute(Article, PostListView) ) +#routes.register( ThreadRoute(ProgramPost, PostListView, base_name = 'programs') ) +#routes.register( ThreadRoute(EpisodePost, PostListView, base_name = 'episodes') ) + +routes.register( DateRoute(Article, PostListView) ) +#routes.register( DateRoute(ProgramPost, PostListView, base_name = 'programs') ) +#routes.register( DateRoute(EpisodePost, PostListView, base_name = 'episodes') ) + +urlpatterns = routes.get_urlpatterns() + + diff --git a/website/utils.py b/website/utils.py index ea77a5b..a31590c 100644 --- a/website/utils.py +++ b/website/utils.py @@ -36,7 +36,7 @@ class ListQueries: if not q: q = timezone.datetime.today() if type(q) is str: - q = timezone.datetime.strptime(q, '%Y/%m/%d').date() + q = timezone.datetime.strptime(q, '%Y%m%d').date() return qs.filter(date__startswith = q) diff --git a/website/views.py b/website/views.py index 91ea44a..602a128 100644 --- a/website/views.py +++ b/website/views.py @@ -1,3 +1,77 @@ from django.shortcuts import render +from django.utils import timezone +from django.views.generic import ListView +from django.views.generic import DetailView +from django.core import serializers +from django.utils.translation import ugettext as _, ugettext_lazy + +from website.models import * + + +class PostListView (ListView): + class Query: + """ + Request availables parameters + """ + exclude = None + order = 'desc' + reverse = False + format = 'normal' + + def __init__ (self, query): + my_class = self.__class__ + if type(query) is my_class: + self.__dict__.update(query.__dict__) + return + + if type(query) is not dict: + query = query.__dict__ + + self.__dict__ = { k: v for k,v in query.items() } + + template_name = 'website/list.html' + allow_empty = True + + query = None + format = None + fields = [ 'date', 'time', 'image', 'title', 'content' ] + + route = None + model = None + + class Meta: + # FIXME + formats = ['normal', 'embed', 'json', 'yaml', 'xml'] + + def __init__ (self, *args, **kwargs): + super(PostListView, self).__init__(*args, **kwargs) + + if self.query: + self.query = Query(self.query) + + def get_queryset (self): + qs = self.route.get_queryset(self.request, **self.kwargs) + qs = qs.filter(public = True) + + query = self.query or PostListView.Query(self.request.GET) + if query.exclude: + qs = qs.exclude(id = int(exclude)) + + if query.order == 'asc': + qs.order_by('date', 'id') + else: + qs.order_by('-date', '-id') + + return qs + + + def get_context_data (self, **kwargs): + context = super(PostListView, self).get_context_data(**kwargs) + context.update({ + 'list': self + }) + + return context + + -# Create your views here.