update sections, work a bit on style

This commit is contained in:
bkfox 2015-10-06 18:01:19 +02:00
parent cfdc9b6de0
commit cde58334bd
15 changed files with 264 additions and 163 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
Django>=1.9.0
django-taggit>=0.12.1
easy_thumbnails

View File

@ -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

View File

@ -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;
}

View File

@ -12,23 +12,23 @@
<time datetime="{{ post.date }}" class="post_datetime">
{% if 'date' in view.fields %}
<span class="post_date">
{{ post.date|date:'D. d F' }},
{{ post.date|date:'D. d F' }}
</span>
{% endif %}
{% if 'time' in view.fields %}
<span class="post_time">
{{ post.date|date:'H:i' }},
{{ post.date|date:'H:i' }}
</span>
{% endif %}
</time>
{% endif %}
{% if 'image' in view.fields %}
<img src="{% thumbnail post.image "64x64" crop %}" class="post_image">
<img src="{% thumbnail post.image view.icon_size crop %}" class="post_image">
{% endif %}
{% if 'title' in view.fields %}
<h4 class="post_title">{{ post.title }}</h4>
<h3 class="post_title">{{ post.title }}</h3>
{% endif %}
{% if 'content' in view.fields %}

View File

@ -6,6 +6,9 @@
<ul style="padding:0; margin:0">
{% for item in object_list %}
<li>
{% if item.url %}
<a href="{{item.url}}">
{% endif %}
{% if use_icons and item.icon %}
<img src="{% thumbnail item.icon icon_size crop %}" class="icon">
{% endif %}
@ -15,6 +18,9 @@
{% if item.text %}
<small>{{ item.text }}</small>
{% endif %}
{% if item.url %}
</a>
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -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 '<img src="{}" class="post_image">'.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,
'<a href="{}">{}</a>'.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 ) ]

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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',

View File

@ -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;
}

View File

@ -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' }),
]
),

View File

@ -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)