diff --git a/cms/models.py b/cms/models.py index 4d921b5..ad2aa8b 100644 --- a/cms/models.py +++ b/cms/models.py @@ -7,35 +7,48 @@ from django.utils.text import slugify from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse -from django.db.models.signals import post_save +from django.db.models.signals import post_init, post_save, post_delete from django.dispatch import receiver +from taggit.managers import TaggableManager -# Using a separate thread helps for routing, by avoiding to specify an -# additional argument to get the second model that implies to find it by -# the name that can be non user-friendly, like /thread/relatedpost/id class Thread (models.Model): + """ + Object assigned to any Post and children that can be used to have parent and + children relationship between posts of different kind. + + We use this system instead of having directly a GenericForeignKey into the + Post because it avoids having to define the relationship with two models for + routing (one for the parent and one for the children). + """ 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) + def __get_query_set (cl, function, model, post, kwargs): + if post: + model = type(post) + kwargs['post_id'] = post.id + + kwargs['post_type'] = ContentType.objects.get_for_model(model) + return getattr(cl.objects, function)(**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) + def get (cl, model = None, post = None, **kwargs): + return cl.__get_query_set('get', model, post, 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 filter (cl, model = None, post = None, **kwargs): + return self.__get_query_set('filter', model, post, kwargs) + + @classmethod + def exclude (cl, model = None, post = None, **kwargs): + return self.__get_query_set('exclude', model, post, kwargs) + + def save (self, *args, **kwargs): + self.post = self.__initial_post + super().save(*args, **kwargs) def __str__ (self): return self.post_type.name + ': ' + str(self.post) @@ -57,16 +70,26 @@ class Post (models.Model): _('date'), default = timezone.datetime.now ) - public = models.BooleanField( + published = models.BooleanField( verbose_name = _('public'), default = True ) + + title = models.CharField ( + _('title'), + max_length = 128, + ) + content = models.TextField ( + _('description'), + blank = True, null = True + ) image = models.ImageField( blank = True, null = True ) - - title = '' - content = '' + tags = TaggableManager( + _('tags'), + blank = True, + ) def detail_url (self): return reverse(self._meta.verbose_name_plural.lower() + '_detail', @@ -77,28 +100,7 @@ class Post (models.Model): 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 Article (Post): - title = models.CharField( - _('title'), - max_length = 128, - blank = False, null = False - ) - content = models.TextField( - _('content'), - blank = False, null = False - ) static_page = models.BooleanField( _('static page'), default = False, @@ -125,18 +127,6 @@ class RelatedPostBase (models.base.ModelBase): if related_model: attrs['related'] = models.ForeignKey(related_model) - mapping = rel.get('mapping') - if mapping: - def get_prop (name, related_name): - return property(related_name) if callable(related_name) \ - else property(lambda self: - getattr(self.related, related_name)) - - attrs.update({ - name: get_prop(name, related_name) - for name, related_name in mapping.items() - }) - if not '__str__' in attrs: attrs['__str__'] = lambda self: str(self.related) @@ -144,11 +134,54 @@ class RelatedPostBase (models.base.ModelBase): class RelatedPost (Post, metaclass = RelatedPostBase): + related = None + class Meta: abstract = True class Relation: related_model = None - mapping = None + mapping = None # dict of related mapping values + bind_mapping = False # update fields of related data on save + def get_attribute (self, attr): + attr = self.Relation.mappings.get(attr) + return self.related.__dict__[attr] if attr else None + + def save (self, *args, **kwargs): + if not self.title and self.related: + self.title = self.get_attribute('title') + + if self.Relation.bind_mapping: + self.related.__dict__.update({ + rel_attr: self.__dict__[attr] + for attr, rel_attr in self.Relation.mapping + }) + + self.related.save() + + super().save(*args, **kwargs) + + +@receiver(post_init) +def on_thread_init (sender, instance, **kwargs): + if not issubclass(Thread, sender): + return + instance.__initial_post = instance.post + +@receiver(post_save) +def on_post_save (sender, instance, created, *args, **kwargs): + if not issubclass(sender, Post) or not created: + return + + thread = Thread(post = instance) + thread.save() + +@receiver(post_delete) +def on_post_delete (sender, instance, using, *args, **kwargs): + try: + Thread.get(sender, post = instance).delete() + except: + pass + diff --git a/cms/views.py b/cms/views.py index 64d790f..c6494b1 100644 --- a/cms/views.py +++ b/cms/views.py @@ -35,10 +35,12 @@ class PostListView (ListView): template_name = 'cms/list.html' allow_empty = True - model = None - query = None - classes = '' + website = None title = '' + classes = '' + + route = None + query = None embed = False fields = [ 'date', 'time', 'image', 'title', 'content' ] @@ -48,12 +50,11 @@ class PostListView (ListView): self.query = Query(self.query) def dispatch (self, request, *args, **kwargs): - self.route = self.kwargs.get('route') + self.route = self.kwargs.get('route') or self.route return super().dispatch(request, *args, **kwargs) def get_queryset (self): qs = self.route.get_queryset(self.model, self.request, **self.kwargs) - qs = qs.filter(public = True) query = self.query or PostListView.Query(self.request.GET) if query.exclude: @@ -92,6 +93,9 @@ class PostDetailView (DetailView): Detail view for posts and children """ template_name = 'cms/detail.html' + website = None + + embed = False sections = [] def __init__ (self, sections = None, *args, **kwargs): @@ -100,13 +104,13 @@ class PostDetailView (DetailView): def get_queryset (self, **kwargs): if self.model: - return super().get_queryset(**kwargs).filter(public = True) + return super().get_queryset(**kwargs).filter(published = True) return [] def get_object (self, **kwargs): if self.model: object = super().get_object(**kwargs) - if object.public: + if object.published: return object return None @@ -134,17 +138,19 @@ class ViewSet: detail_view = PostDetailView detail_sections = [] - def __init__ (self): + def __init__ (self, website = None): self.detail_sections = [ section() for section in self.detail_sections ] self.detail_view = self.detail_view.as_view( model = self.model, - sections = self.detail_sections + sections = self.detail_sections, + website = website, ) self.list_view = self.list_view.as_view( - model = self.model + model = self.model, + website = website, ) self.urls = [ route.as_url(self.model, self.list_view) @@ -152,6 +158,31 @@ class ViewSet: [ routes.DetailRoute.as_url(self.model, self.detail_view ) ] +class Menu (DetailView): + template_name = 'cms/menu.html' + + name = '' + enabled = True + classes = '' + position = '' # top, left, bottom, right + sections = None + + def get_context_data (self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'name': self.name, + 'classes': self.classes, + 'position': self.position, + 'sections': [ + section.get(self.request, object = self.object) + for section in self.sections + ] + }) + + def get (self, **kwargs): + context = self.get_context_data(**kwargs) + return render_to_string(self.template_name, context) + class Section (DetailView): """ @@ -159,6 +190,7 @@ class Section (DetailView): in order to have extra content about a post. """ template_name = 'cms/section.html' + require_object = False classes = '' title = '' content = '' @@ -177,11 +209,12 @@ class Section (DetailView): return context def get (self, request, **kwargs): - self.object = kwargs.get('object') + self.object = kwargs.get('object') or self.object context = self.get_context_data(**kwargs) return render_to_string(self.template_name, context) + class ListSection (Section): """ Section to render list. The context item 'object_list' is used as list of @@ -197,6 +230,8 @@ class ListSection (Section): self.title = title self.text = text + use_icons = True + icon_size = '32x32' template_name = 'cms/section_list.html' def get_object_list (self): @@ -204,11 +239,28 @@ class ListSection (Section): def get_context_data (self, **kwargs): context = super().get_context_data(**kwargs) - context['classes'] += ' section_list' - context['object_list'] = self.get_object_list() + context.update({ + 'classes': context.classes + ' section_list', + 'icon_size': self.icon_size, + 'object_list': self.get_object_list(), + }) return context +class UrlListSection (ListSection): + classes = 'section_urls' + targets = None + + def get_object_list (self, request, **kwargs): + return [ + ListSection.Item( + target.image or None, + '{}'.format(target.detail_url(), target.title) + ) + for target in self.targets + ] + + class PostListSection (PostListView): route = None model = None @@ -222,8 +274,8 @@ class PostListSection (PostListView): def dispatch (self, request, *args, **kwargs): kwargs = self.get_kwargs(kwargs) - # TODO: to_string - return super().dispatch(request, *args, **kwargs) + response = super().dispatch(request, *args, **kwargs) + return str(response.content) # TODO: # - get_title: pass object / queryset diff --git a/cms/website.py b/cms/website.py new file mode 100644 index 0000000..6b6bc5a --- /dev/null +++ b/cms/website.py @@ -0,0 +1,24 @@ +import cms.routes as routes + +class Website: + name = '' + logo = None + menus = None + router = None + + def __init__ (self, **kwargs): + self.__dict__.update(kwargs) + if not self.router: + self.router = routes.Router() + if not self.sets: + self.sets = [] + + def register_set (self, view_set): + view_set = view_set(website = self) + self.router.register_set(view_set) + + @property + def urls (self): + return self.router.get_urlpatterns() + + diff --git a/programs/admin.py b/programs/admin.py index 2da7c0c..6a7bf9b 100755 --- a/programs/admin.py +++ b/programs/admin.py @@ -39,22 +39,19 @@ class TrackInline (SortableTabularInline): extra = 10 -class DescriptionAdmin (admin.ModelAdmin): - fields = [ 'name', 'tags', 'description' ] +class NameableAdmin (admin.ModelAdmin): + fields = [ 'name' ] - def tags (obj): - return ', '.join(obj.tags.names()) - - list_display = ['id', 'name', tags] + list_display = ['id', 'name'] list_filter = [] search_fields = ['name',] @admin.register(Sound) -class SoundAdmin (DescriptionAdmin): +class SoundAdmin (NameableAdmin): fields = None fieldsets = [ - (None, { 'fields': DescriptionAdmin.fields + ['path' ] } ), + (None, { 'fields': NameableAdmin.fields + ['path' ] } ), (None, { 'fields': ['duration', 'date', 'fragment' ] } ) ] @@ -66,15 +63,15 @@ class StreamAdmin (SortableModelAdmin): @admin.register(Program) -class ProgramAdmin (DescriptionAdmin): - fields = DescriptionAdmin.fields + ['stream'] +class ProgramAdmin (NameableAdmin): + fields = NameableAdmin.fields + ['stream'] inlines = [ ScheduleInline ] @admin.register(Episode) -class EpisodeAdmin (DescriptionAdmin): - list_filter = ['program'] + DescriptionAdmin.list_filter - fields = DescriptionAdmin.fields + ['sounds'] +class EpisodeAdmin (NameableAdmin): + list_filter = ['program'] + NameableAdmin.list_filter + fields = NameableAdmin.fields + ['sounds'] inlines = (TrackInline, DiffusionInline) diff --git a/programs/models.py b/programs/models.py index b521bab..4767cf0 100755 --- a/programs/models.py +++ b/programs/models.py @@ -24,20 +24,11 @@ def date_or_default (date, date_only = False): return date -class Description (models.Model): +class Nameable (models.Model): name = models.CharField ( _('name'), max_length = 128, ) - description = models.TextField ( - _('description'), - max_length = 1024, - blank = True, null = True - ) - tags = TaggableManager( - _('tags'), - blank = True, - ) def get_slug_name (self): return slugify(self.name) @@ -51,7 +42,7 @@ class Description (models.Model): abstract = True -class Track (Description): +class Track (Nameable): # 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 @@ -68,6 +59,10 @@ class Track (Description): default = 0, help_text=_('position in the playlist'), ) + tags = TaggableManager( + _('tags'), + blank = True, + ) def __str__(self): return ' '.join([self.artist, ':', self.name ]) @@ -77,7 +72,7 @@ class Track (Description): verbose_name_plural = _('Tracks') -class Sound (Description): +class Sound (Nameable): """ A Sound is the representation of a sound, that can be: - An episode podcast/complete record @@ -134,7 +129,7 @@ class Sound (Description): if not self.pk: self.date = self.get_mtime() - super(Sound, self).save(*args, **kwargs) + super().save(*args, **kwargs) def __str__ (self): return '/'.join(self.path.split('/')[-3:]) @@ -393,7 +388,7 @@ class Stream (models.Model): return '#{} {}'.format(self.priority, self.name) -class Program (Description): +class Program (Nameable): stream = models.ForeignKey( Stream, verbose_name = _('stream'), @@ -419,7 +414,7 @@ class Program (Description): return schedule -class Episode (Description): +class Episode (Nameable): program = models.ForeignKey( Program, verbose_name = _('program'), diff --git a/website/admin.py b/website/admin.py index 1ef0cf0..4fe28b5 100644 --- a/website/admin.py +++ b/website/admin.py @@ -15,6 +15,15 @@ def add_inline (base_model, post_model, prepend = False): max_num = 1 verbose_name = _('Post') + fieldsets = [ + (None, { + 'fields': ['title', 'content', 'image', 'tags'] + }), + (None, { + 'fields': ['date', 'published', 'author', 'thread'] + }) + ] + registry = admin.site._registry if not base_model in registry: raise TypeError(str(base_model) + " not in admin registry") @@ -28,8 +37,8 @@ def add_inline (base_model, post_model, prepend = False): registry[base_model].inlines = inlines -add_inline(programs.Program, Program) -add_inline(programs.Episode, Episode) +add_inline(programs.Program, Program, True) +add_inline(programs.Episode, Episode, True) #class ArticleAdmin (DescriptionAdmin): diff --git a/website/urls.py b/website/urls.py index 3f3f6e8..2f5a2b1 100644 --- a/website/urls.py +++ b/website/urls.py @@ -4,7 +4,7 @@ from website.models import * from website.views import * from cms.models import Article -from cms.views import ViewSet +from cms.views import ViewSet, Menu from cms.routes import * class ProgramSet (ViewSet): @@ -38,11 +38,20 @@ class ArticleSet (ViewSet): DateRoute, ] -router = Router() -router.register_set(ProgramSet()) -router.register_set(EpisodeSet()) -router.register_set(ArticleSet()) -urlpatterns = router.get_urlpatterns() +website = Website( + name = 'RadioCampus', + menus = [ + Menu( + position = 'top', + sections = [] + ) + ], +}) +website.register_set(ProgramSet) +website.register_set(EpisodeSet) +website.register_set(ArticleSet) +urlpatterns = website.urls +