diff --git a/README.md b/README.md
index 08d9f75..a8a5e7c 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@ Platform to manage radio programs, schedules, cms, etc. -- main test repo
# Applications
* **programs**: programs, episodes, schedules, sounds and tracks;
-* **streams**: streams and diffusions, links with LiquidSoap;
-* **website**: website rendering, using models defined by the previous apps;
+* **cms**: cms renderer
+* **website**: the website using the cms and the programs
# Code and names conventions and uses
diff --git a/aircox_cms/models.py b/aircox_cms/models.py
index b690bde..820809f 100644
--- a/aircox_cms/models.py
+++ b/aircox_cms/models.py
@@ -12,57 +12,18 @@ from django.dispatch import receiver
from taggit.managers import TaggableManager
-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')
-
- __initial_post = None
-
- @classmethod
- 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 get (cl, model = None, post = None, **kwargs):
- return cl.__get_query_set('get', model, post, kwargs)
-
- @classmethod
- 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 or self.post
- super().save(*args, **kwargs)
-
- def __str__ (self):
- return self.post_type.name + ': ' + str(self.post)
-
class Post (models.Model):
- thread = models.ForeignKey(
- Thread,
+ thread_type = models.ForeignKey(
+ ContentType,
on_delete=models.SET_NULL,
- blank = True, null = True,
- help_text = _('the publication is posted on this thread'),
+ blank = True, null = True
)
+ thread_pk = models.PositiveIntegerField(
+ blank = True, null = True
+ )
+ thread = GenericForeignKey('thread_type', 'thread_pk')
+
author = models.ForeignKey(
User,
verbose_name = _('author'),
@@ -94,7 +55,7 @@ class Post (models.Model):
)
def detail_url (self):
- return reverse(self._meta.verbose_name_plural.lower() + '_detail',
+ return reverse(self._meta.verbose_name.lower() + '_detail',
kwargs = { 'pk': self.pk,
'slug': slugify(self.title) })
@@ -151,7 +112,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
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
@@ -163,32 +123,10 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
if self._relation.bind_mapping:
self.related.__dict__.update({
rel_attr: self.__dict__[attr]
- for attr, rel_attr in self.Relation.mapping
+ for attr, rel_attr in self.Relation.mapping.items()
})
-
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/aircox_cms/requirements.txt b/aircox_cms/requirements.txt
new file mode 100644
index 0000000..1c94d31
--- /dev/null
+++ b/aircox_cms/requirements.txt
@@ -0,0 +1,4 @@
+Django>=1.9.0
+django-taggit>=0.12.1
+easy_thumbnails
+
diff --git a/aircox_cms/routes.py b/aircox_cms/routes.py
index e41af0c..c31f2d3 100644
--- a/aircox_cms/routes.py
+++ b/aircox_cms/routes.py
@@ -1,4 +1,5 @@
from django.conf.urls import url
+from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_lazy
@@ -27,9 +28,9 @@ 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)
+ model_name + '/' + route_name + '/' + '/'.join(route_url_args)
- Where base_name by default is the given model's verbose_name (uses plural if
+ Where model_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_
@@ -38,14 +39,14 @@ class Route:
url_args = [] # arguments passed from the url [ (name : regex),... ]
@classmethod
- def get_queryset (cl, model, request, **kwargs):
+ def get_queryset (cl, website, model, request, **kwargs):
"""
Called by the view to get the queryset when it is needed
"""
pass
@classmethod
- def get_object (cl, model, request, **kwargs):
+ def get_object (cl, website, model, request, **kwargs):
"""
Called by the view to get the object when it is needed
"""
@@ -56,10 +57,8 @@ class Route:
return ''
@classmethod
- def as_url (cl, model, view, view_kwargs = None):
- base_name = model._meta.verbose_name_plural.lower()
-
- pattern = '^{}/{}'.format(base_name, cl.name)
+ def as_url (cl, model, model_name, view, view_kwargs = None):
+ pattern = '^{}/{}'.format(model_name, cl.name)
if cl.url_args:
url_args = '/'.join([
'(?P<{}>{}){}'.format(
@@ -78,7 +77,7 @@ class Route:
kwargs.update(view_kwargs)
return url(pattern, view, kwargs = kwargs,
- name = base_name + '_' + cl.name)
+ name = model_name + '_' + cl.name)
class DetailRoute (Route):
@@ -89,7 +88,7 @@ class DetailRoute (Route):
]
@classmethod
- def get_object (cl, model, request, pk, **kwargs):
+ def get_object (cl, website, model, request, pk, **kwargs):
return model.objects.get(pk = int(pk))
@@ -97,7 +96,7 @@ class AllRoute (Route):
name = 'all'
@classmethod
- def get_queryset (cl, model, request, **kwargs):
+ def get_queryset (cl, website, model, request, **kwargs):
return model.objects.all()
@classmethod
@@ -108,14 +107,32 @@ class AllRoute (Route):
class ThreadRoute (Route):
+ """
+ Select posts using by their assigned thread.
+
+ - "thread_model" can be a string with the name of a registered item on
+ website or a model.
+ - "pk" is the pk of the thread item.
+ """
name = 'thread'
url_args = [
+ ('thread_model', '(\w|_|-)+'),
('pk', '[0-9]+'),
]
@classmethod
- def get_queryset (cl, model, request, pk, **kwargs):
- return model.objects.filter(thread__pk = int(pk))
+ def get_queryset (cl, website, model, request, thread_model, pk, **kwargs):
+ if type(thread_model) is str:
+ thread_model = website.registry.get(thread_model).model
+
+ if not thread_model:
+ return
+
+ thread_model = ContentType.objects.get_for_model(thread_model)
+ return model.objects.filter(
+ thread_type = thread_model,
+ thread_pk = int(pk)
+ )
class DateRoute (Route):
@@ -127,7 +144,7 @@ class DateRoute (Route):
]
@classmethod
- def get_queryset (cl, model, request, year, month, day, **kwargs):
+ def get_queryset (cl, website, model, request, year, month, day, **kwargs):
return model.objects.filter(
date__year = int(year),
date__month = int(month),
@@ -139,7 +156,7 @@ class SearchRoute (Route):
name = 'search'
@classmethod
- def get_queryset (cl, model, request, **kwargs):
+ def get_queryset (cl, website, model, request, **kwargs):
q = request.GET.get('q') or ''
qs = model.objects
for search_field in model.search_fields or []:
@@ -151,4 +168,3 @@ class SearchRoute (Route):
return qs
-
diff --git a/aircox_cms/static/aircox_cms/styles.css b/aircox_cms/static/aircox_cms/styles.css
index db2fe27..c3d9790 100644
--- a/aircox_cms/static/aircox_cms/styles.css
+++ b/aircox_cms/static/aircox_cms/styles.css
@@ -20,9 +20,13 @@ body {
}
+.section {
+ vertical-align: top;
+}
+
main .section {
- width: calc(50% - 1em);
- float: left;
+ width: calc(50% - 2em);
+ display: inline-block;
padding: 0.5em;
}
@@ -39,3 +43,8 @@ main .section {
}
+.post_item {
+ display: block;
+}
+
+
diff --git a/aircox_cms/templates/aircox_cms/list.html b/aircox_cms/templates/aircox_cms/list.html
index de33a17..6dfec85 100644
--- a/aircox_cms/templates/aircox_cms/list.html
+++ b/aircox_cms/templates/aircox_cms/list.html
@@ -12,23 +12,23 @@
{% endif %}
{% if 'image' in view.fields %}
-
+
{% endif %}
{% if 'title' in view.fields %}
-
{{ post.title }}
+ {{ post.title }}
{% endif %}
{% if 'content' in view.fields %}
diff --git a/aircox_cms/templates/aircox_cms/section_list.html b/aircox_cms/templates/aircox_cms/section_list.html
index 46479af..35589ee 100644
--- a/aircox_cms/templates/aircox_cms/section_list.html
+++ b/aircox_cms/templates/aircox_cms/section_list.html
@@ -6,6 +6,9 @@
diff --git a/aircox_cms/views.py b/aircox_cms/views.py
index 30f681a..c579d8d 100644
--- a/aircox_cms/views.py
+++ b/aircox_cms/views.py
@@ -1,6 +1,5 @@
import re
-
from django.templatetags.static import static
from django.shortcuts import render
from django.template.loader import render_to_string
@@ -19,7 +18,7 @@ class PostBaseView:
embed = False # page is embed (if True, only post content is printed
classes = '' # extra classes for the content
- def get_base_context (self):
+ def get_base_context (self, **kwargs):
"""
Return a context with all attributes of this classe plus 'view' set
to self.
@@ -32,7 +31,7 @@ class PostBaseView:
if not self.embed:
context['menus'] = {
- k: v.get(self.request)
+ k: v.get(self.request, **kwargs)
for k, v in {
k: self.website.get_menu(k)
for k in self.website.menu_layouts
@@ -70,10 +69,12 @@ class PostListView (PostBaseView, ListView):
template_name = 'aircox_cms/list.html'
allow_empty = True
+ model = None
route = None
query = None
fields = [ 'date', 'time', 'image', 'title', 'content' ]
+ icon_size = '64x64'
def __init__ (self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -84,7 +85,11 @@ class PostListView (PostBaseView, ListView):
return super().dispatch(request, *args, **kwargs)
def get_queryset (self):
- qs = self.route.get_queryset(self.model, self.request, **self.kwargs)
+ if self.route:
+ qs = self.route.get_queryset(self.website, self.model, self.request,
+ **self.kwargs)
+ else:
+ qs = self.queryset or self.model.objects.all()
query = self.query
query.update(self.request.GET)
@@ -156,7 +161,7 @@ class PostDetailView (DetailView, PostBaseView):
context.update(self.get_base_context())
context.update({
'sections': [
- section.get(self.request, object = self.object)
+ section.get(self.request, **kwargs)
for section in self.sections
]
})
@@ -184,7 +189,7 @@ class Menu (View):
'classes': self.classes,
'position': self.position,
'sections': [
- section.get(self.request, object = None)
+ section.get(self.request, object = None, **kwargs)
for section in self.sections
]
}
@@ -202,36 +207,48 @@ class BaseSection (View):
in order to have extra content about a post, or in menus.
"""
template_name = 'aircox_cms/base_section.html'
- tag = 'div' # container tags
+ kwargs = None # kwargs argument passed to get
+ tag = 'div' # container tags
classes = '' # container classes
attrs = '' # container extra attributes
content = '' # content
+ visible = True # if false renders an empty string
- def get_context_data (self, **kwargs):
+ def get_context_data (self):
return {
+ 'view': self,
'tag': self.tag,
'classes': self.classes,
'attrs': self.attrs,
+ 'visible': self.visible,
'content': self.content,
}
def get (self, request, **kwargs):
self.request = request
- context = self.get_context_data(**kwargs)
- return render_to_string(self.template_name, context)
+ self.kwargs = kwargs
+
+ context = self.get_context_data()
+ # get_context_data may call extra function that can change visibility
+ if self.visible:
+ return render_to_string(self.template_name, context)
+ return ''
class Section (BaseSection):
+ """
+ A Section that can be related to an object.
+ """
template_name = 'aircox_cms/section.html'
- require_object = False
object = None
+ object_required = False
title = ''
header = ''
bottom = ''
- def get_context_data (self, **kwargs):
- context = super().get_context_data(**kwargs)
+ def get_context_data (self):
+ context = super().get_context_data()
context.update({
'title': self.title,
'header': self.header,
@@ -239,14 +256,20 @@ class Section (BaseSection):
})
return context
- def get (self, request, **kwargs):
- self.object = kwargs.get('object') or self.object
+ def get (self, request, object = None, **kwargs):
+ self.object = object or self.object
+ if self.object_required and not self.object:
+ raise ValueError('object is required by this Section but not given')
+
return super().get(request, **kwargs)
class Sections:
class Image (BaseSection):
- url = None # relative url to the image
+ """
+ Render an image with the given relative url.
+ """
+ url = None
@property
def content (self):
@@ -256,6 +279,10 @@ class Sections:
class PostContent (Section):
+ """
+ Render the content of the Post (format the text a bit and escape HTML
+ tags).
+ """
@property
def content (self):
content = escape(self.object.content)
@@ -265,6 +292,9 @@ class Sections:
class PostImage (Section):
+ """
+ Render the image of the Post
+ """
@property
def content (self):
return '
'.format(
@@ -281,59 +311,78 @@ class Sections:
icon = None
title = None
text = None
+ url = None
- def __init__ (self, icon, title = None, text = None):
+ def __init__ (self, icon, title = None, text = None, url = None):
self.icon = icon
self.title = title
self.text = text
- use_icons = True
- icon_size = '32x32'
+ hide_empty = False # hides the section if the list is empty
+ use_icons = True # print icons
+ icon_size = '32x32' # icons size
template_name = 'aircox_cms/section_list.html'
def get_object_list (self):
return []
def get_context_data (self, **kwargs):
+ object_list = self.get_object_list()
+ self.visibility = True
+ if not object_list and hide_empty:
+ self.visibility = False
+
context = super().get_context_data(**kwargs)
context.update({
'classes': context.get('classes') + ' section_list',
'icon_size': self.icon_size,
- 'object_list': self.get_object_list(),
+ 'object_list': object_list,
})
return context
- class UrlList (List):
+ class Urls (List):
+ """
+ Render a list of urls of targets that are Posts
+ """
classes = 'section_urls'
targets = None
- def get_object_list (self, request, **kwargs):
+ def get_object_list (self):
return [
List.Item(
target.image or None,
- '{}'.format(target.detail_url(), target.title)
+ target.title,
+ url = target.detail_url(),
)
for target in self.targets
]
-
- class PostList (PostListView):
- route = None
- model = None
+ class Posts (PostBaseView, Section):
+ """
+ Render a list using PostListView's template.
+ """
embed = True
+ icon_size = '64x64'
+ fields = [ 'date', 'time', 'image', 'title', 'content' ]
- def __init__ (self, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ def get_object_list (self):
+ return []
- def get_kwargs (self, request, **kwargs):
- return kwargs
-
- def dispatch (self, request, *args, **kwargs):
- kwargs = self.get_kwargs(kwargs)
- response = super().dispatch(request, *args, **kwargs)
- return str(response.content)
+ def render_list (self):
+ self.embed = True
+ context = self.get_base_context(**self.kwargs)
+ context.update({
+ 'object_list': self.get_object_list(),
+ 'embed': True,
+ })
+ print(context['object_list'][0].image)
+ return render_to_string(PostListView.template_name, context)
+ def get_context_data (self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['content'] = self.render_list()
+ return context
class ViewSet:
@@ -348,15 +397,25 @@ class ViewSet:
detail_view = PostDetailView
detail_sections = [
- Sections.PostContent,
- Sections.PostImage,
+ Sections.PostContent(),
+ Sections.PostImage(),
]
- def __init__ (self, website = None):
- self.detail_sections = [
- section()
- for section in self.detail_sections
- ]
+
+ def get_list_name (self):
+ """
+ Return a string with the name to use in the route for the lists
+ """
+ return self.model._meta.verbose_name_plural.lower()
+
+ @classmethod
+ def get_detail_name (cl):
+ """
+ Return a string with the name to use in the route for the details
+ """
+ return cl.model._meta.verbose_name.lower()
+
+ def connect (self, website = None):
self.detail_view = self.detail_view.as_view(
model = self.model,
sections = self.detail_sections,
@@ -367,8 +426,9 @@ class ViewSet:
website = website,
)
- self.urls = [ route.as_url(self.model, self.list_view)
- for route in self.list_routes ] + \
- [ routes.DetailRoute.as_url(self.model, self.detail_view ) ]
+ self.urls = [ route.as_url(self.model, self.get_list_name(),
+ self.list_view) for route in self.list_routes ] + \
+ [ routes.DetailRoute.as_url(self.model,
+ self.get_detail_name(), self.detail_view ) ]
diff --git a/aircox_cms/website.py b/aircox_cms/website.py
index 45a4a6c..dd35c70 100644
--- a/aircox_cms/website.py
+++ b/aircox_cms/website.py
@@ -12,6 +12,7 @@ class Website:
'right', 'bottom',
'header', 'footer']
router = None
+ registry = {}
@property
def urls (self):
@@ -27,7 +28,9 @@ class Website:
Register a ViewSet (or subclass) to the router,
and connect it to self.
"""
- view_set = view_set(website = self)
+ view_set = view_set()
+ view_set.connect(website = self)
+ self.registry[view_set.get_detail_name()] = view_set
self.router.register_set(view_set)
def get_menu (self, position):
diff --git a/aircox_programs/admin.py b/aircox_programs/admin.py
index 65892fd..54d67dd 100755
--- a/aircox_programs/admin.py
+++ b/aircox_programs/admin.py
@@ -71,7 +71,7 @@ class ProgramAdmin (NameableAdmin):
@admin.register(Episode)
class EpisodeAdmin (NameableAdmin):
list_filter = ['program'] + NameableAdmin.list_filter
- fields = NameableAdmin.fields + ['sounds']
+ fields = NameableAdmin.fields + ['sounds', 'program']
inlines = (TrackInline, DiffusionInline)
diff --git a/website/admin.py b/website/admin.py
index b0f42b0..cad2fc2 100644
--- a/website/admin.py
+++ b/website/admin.py
@@ -20,7 +20,7 @@ def add_inline (base_model, post_model, prepend = False):
'fields': ['title', 'content', 'image', 'tags']
}),
(None, {
- 'fields': ['date', 'published', 'author', 'thread']
+ 'fields': ['date', 'published', 'author', 'thread_pk', 'thread_type']
})
]
@@ -40,6 +40,8 @@ def add_inline (base_model, post_model, prepend = False):
add_inline(programs.Program, Program, True)
add_inline(programs.Episode, Episode, True)
+admin.site.register(Program)
+admin.site.register(Episode)
#class ArticleAdmin (DescriptionAdmin):
# fieldsets = copy.deepcopy(DescriptionAdmin.fieldsets)
diff --git a/website/models.py b/website/models.py
index 56428ea..1551a04 100644
--- a/website/models.py
+++ b/website/models.py
@@ -6,6 +6,7 @@ import aircox_programs.models as programs
class Program (RelatedPost):
class Relation:
related_model = programs.Program
+ bind_mapping = True
mapping = {
'title': 'name',
'content': 'description',
@@ -14,6 +15,7 @@ class Program (RelatedPost):
class Episode (RelatedPost):
class Relation:
related_model = programs.Episode
+ bind_mapping = True
mapping = {
'title': 'name',
'content': 'description',
diff --git a/website/static/website/styles.css b/website/static/website/styles.css
index 7117ce0..cc758a0 100644
--- a/website/static/website/styles.css
+++ b/website/static/website/styles.css
@@ -9,36 +9,87 @@ h1, h2, h3 {
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif
}
+time {
+ font-size: 0.9em;
+ color: #616161;
+}
+
+a {
+ text-decoration: none;
+ color: #616161;
+}
+
+a:hover {
+ color: #818181;
+}
+
nav.menu {
padding: 0.5em;
}
-nav.menu_top {
- background-color: #212121;
- color: #007EDF;
- font-size: 1.1em;
-}
+ nav.menu_top {
+ background-color: #212121;
+ color: #007EDF;
+ font-size: 1.1em;
+ box-shadow: 0em 0.2em 0.5em 0.1em black
+ }
- header {
+ header.menu {
+ padding: 0.2em;
height: 9em;
}
- header img {
- height: 100%;
- float: left;
- }
+ header.menu img {
+ height: 100%;
+ float: left;
+ }
- header .colony {
- position: fixed;
- top: 0em;
- right: 0;
- z-index: -1;
- }
+ #colony img {
+ height: auto;
+ position: fixed;
+ top: 1em;
+ right: 0;
+ z-index: -1;
+ }
.page {
width: 100%;
+ padding: 1.5em 0em;
background-color: rgba(255, 255, 255, 0.8);
}
+ .page img {
+ box-shadow: 0em 0em 0.2em 0.01em black;
+ border-radius: 0.2em;
+ }
+
+
+.post_list {
+ background-color: #F2F2F2;
+ box-shadow: inset 0.2em 0.2em 0.2em 0.01em black;
+}
+
+ .post_list .post_item {
+ min-height: 64px;
+ padding: 0.2em;
+ }
+
+ .post_list .post_item:hover {
+ }
+
+ .post_list h3 {
+ margin: 0.2em;
+ }
+
+ .post_list time {
+ float: right;
+ }
+
+ .post_list img {
+ float: left;
+ margin-right: 0.5em;
+ }
+
+
diff --git a/website/urls.py b/website/urls.py
index 8026014..71db994 100644
--- a/website/urls.py
+++ b/website/urls.py
@@ -18,7 +18,8 @@ class ProgramSet (ViewSet):
]
detail_sections = ViewSet.detail_sections + [
- ScheduleSection,
+ ScheduleSection(),
+ EpisodesSection(),
]
class EpisodeSet (ViewSet):
@@ -49,7 +50,7 @@ website = Website(
position = 'header',
sections = [
Sections.Image(url = 'website/logo.png'),
- Sections.Image(url = 'website/colony.png', classes='colony'),
+ Sections.Image(url = 'website/colony.png', attrs = { 'id': 'colony' }),
]
),
diff --git a/website/views.py b/website/views.py
index 9aa4676..b7acda8 100644
--- a/website/views.py
+++ b/website/views.py
@@ -8,6 +8,9 @@ from django.utils.translation import ugettext as _, ugettext_lazy
import aircox_programs.models as programs
from aircox_cms.views import Sections
+from website.models import *
+
+
class PlayListSection (Sections.List):
title = _('Playlist')
@@ -34,4 +37,10 @@ class ScheduleSection (Sections.List):
]
+class EpisodesSection (Sections.Posts):
+ title = _('Episodes')
+
+ def get_object_list (self):
+ return Episode.objects.filter(related__program = self.object.pk)
+