rewrite a bit how views and sections work; section's prepare function; website interfaces; section.as_view returns a view containing it and usable as it

This commit is contained in:
bkfox 2016-07-18 02:40:11 +02:00
parent 32a30004d6
commit 4e5d90fb1d
7 changed files with 306 additions and 202 deletions

View File

@ -34,21 +34,21 @@ class Route:
"""
@classmethod
def get_queryset(cl, model, request, **kwargs):
def get_queryset(cl, website, 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, request, **kwargs):
"""
Called by the view to get the object when it is needed
"""
pass
@classmethod
def get_title(cl, model, request, **kwargs):
def get_title(cl, website, request, **kwargs):
return ''
@classmethod
@ -56,8 +56,12 @@ class Route:
return name + '.' + cl.name
@classmethod
def as_url(cl, name, view, view_kwargs = None):
pattern = '^{}/{}'.format(name, cl.name)
def make_pattern(cl, prefix = ''):
"""
Make a url pattern using prefix as prefix and cl.params as
parameters.
"""
pattern = prefix
if cl.params:
pattern += ''.join([
'{pre}/(?P<{name}>{regexp}){post}'.format(
@ -68,13 +72,13 @@ class Route:
for name, regexp, *optional in cl.params
])
pattern += '/?$'
return pattern
kwargs = {
'route': cl,
}
if view_kwargs:
kwargs.update(view_kwargs)
@classmethod
def as_url(cl, name, view, kwargs = None):
pattern = cl.make_pattern('^{}/{}'.format(name, cl.name))
kwargs = kwargs.copy() if kwargs else {}
kwargs['route'] = cl
return url(pattern, view, kwargs = kwargs,
name = cl.make_view_name(name))

View File

@ -29,20 +29,19 @@ class Viewable:
@classmethod
def as_view (cl, *args, **kwargs):
"""
Similar to View.as_view, but instead, wrap a constructor of the
given class that is used as is.
Create a view containing the current viewable, using a subclass
of aircox.cms.views.BaseView.
All the arguments are passed to the view directly.
"""
def func(**kwargs_):
if kwargs_:
kwargs.update(kwargs_)
instance = cl(*args, **kwargs)
return instance
return func
from aircox.cms.views import PageView
kwargs['sections'] = cl
return PageView.as_view(*args, **kwargs)
@classmethod
def extends (cl, **kwargs):
"""
Return a sub class where the given attribute have been updated
using kwargs.
"""
class Sub(cl):
pass
@ -60,13 +59,20 @@ class Sections(Viewable, list):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def prepare(self, *args, **kwargs):
"""
prepare all children sections
"""
for i, section in enumerate(self):
if callable(section) or type(section) == type:
self[i] = section()
self[i].prepare(*args, **kwargs)
def render(self, *args, **kwargs):
if args:
self.prepare(*args, **kwargs)
return ''.join([
section.render(*args, **kwargs)
section.render()
for section in self
])
@ -127,6 +133,7 @@ class Section(Viewable, View):
its value is an empty string (prints an empty string).
"""
view = None
request = None
object = None
kwargs = None
@ -138,8 +145,8 @@ class Section(Viewable, View):
else:
self.css_class = css_class
def __init__ (self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__ (self, **kwargs):
super().__init__(**kwargs)
self.add_css_class('section')
if type(self) != Section:
@ -159,11 +166,7 @@ class Section(Viewable, View):
"""
return False
def get_context_data(self, request = None, object = None, **kwargs):
if request: self.request = request
if object: self.object = object
if kwargs: self.kwargs = kwargs
def get_context_data(self):
return {
'view': self,
'exp': (hasattr(self, '_exposure') and self._exposure) or None,
@ -178,8 +181,24 @@ class Section(Viewable, View):
'embed': True,
}
def render(self, request, object=None, **kwargs):
context = self.get_context_data(request=request, object=object, **kwargs)
def prepare(self, view, **kwargs):
"""
initialize the object with valuable informations.
"""
self.view = view
self.request = view.request
self.kwargs = view.kwargs
if hasattr(view, 'object'):
self.object = view.object
def render(self, *args, **kwargs):
"""
Render the section as a string. Use *args and **kwargs to prepare
the section, then get_context_data and render.
"""
if args and not self.view:
self.prepare(*args, **kwargs)
context = self.get_context_data()
is_empty = self.is_empty()
if not context or (is_empty and not self.message_empty):
@ -189,7 +208,9 @@ class Section(Viewable, View):
context['content'] = self.message_empty
context['embed'] = True
return render_to_string(self.template_name, context, request=request)
return render_to_string(
self.template_name, context, request=self.request
)
class Image(Section):
@ -336,11 +357,17 @@ class List(Section):
ListItem(item) for item in items
]
@classmethod
def as_view(cl, *args, **kwargs):
from aircox.cms.views import PostListView
kwargs['sections'] = cl
return PostListView.as_view(*args, **kwargs)
def is_empty(self):
return not self.object_list
def get_object_list(self):
return self.object_list
return self.object_list or []
def prepare_list(self, object_list):
"""
@ -349,8 +376,7 @@ class List(Section):
"""
return object_list
def get_context_data(self, request, object=None, object_list=None,
*args, **kwargs):
def get_context_data(self, *args, object_list=None, **kwargs):
"""
Return a context that is passed to the template at rendering, with
the following values:
@ -364,21 +390,20 @@ class List(Section):
Set `request`, `object`, `object_list` and `kwargs` in self.
"""
if request: self.request = request
if object: self.object = object
if kwargs: self.kwargs = kwargs
if args:
self.prepare(*args, **kwargs)
if object_list is None:
object_list = self.object_list or self.get_object_list()
if not object_list and not self.message_empty:
return
return {}
self.object_list = object_list
if object_list:
object_list = self.prepare_list(object_list)
Actions.make(request, object_list = object_list)
Actions.make(self.request, object_list = object_list)
context = super().get_context_data(request, object, *args, **kwargs)
context = super().get_context_data()
context.update({
'list': self,
'object_list': object_list[:self.paginate_by]
@ -510,13 +535,16 @@ class Menu(Section):
if not self.attrs:
self.attrs = {}
def get_context_data(self, *args, **kwargs):
super().get_context_data(*args, **kwargs)
def prepare(self, *args, **kwargs):
super().prepare(*args, **kwargs)
self.sections.prepare(*args, **kwargs)
def get_context_data(self):
return {
'tag': self.tag,
'css_class': self.css_class,
'attrs': self.attrs,
'content': self.sections.render(*args, **kwargs)
'content': self.sections.render()
}
@ -579,16 +607,18 @@ class Calendar(Section):
date = date.replace(day = 1)
first, count = calendar.monthrange(date.year, date.month)
def make_date(date, day):
date += tz.timedelta(days=day)
return (
date, self.model.reverse(
routes.DateRoute, year = date.year,
month = date.month, day = date.day
)
)
context.update({
'first_weekday': first,
'days': [
(date + tz.timedelta(days=day), self.model.reverse(
routes.DateRoute, year = date.year, month = date.month,
day = day
)
) for day in range(0, count)
],
'days': [ make_date(date, day) for day in range(0, count) ],
'today': datetime.date.today(),
'this_month': date,
'prev_month': date - tz.timedelta(days=10),

View File

@ -21,7 +21,7 @@ class BaseView:
# Request GET params:
* embed: view is embedded, render only the content of the view
"""
template_name = ''
template_name = 'aircox/cms/website.html'
"""it is set to "aircox/cms/detail.html" to render multiple sections"""
sections = None
"""sections used to render the page"""
@ -43,9 +43,6 @@ class BaseView:
self.sections = sections
super().__init__(*args, **kwargs)
def __section_is_single(self):
return not issubclass(type(self.sections), list)
def add_css_class(self, css_class):
"""
Add the given class to the current class list if not yet present.
@ -65,19 +62,15 @@ class BaseView:
# update from sections
if self.sections:
if self.__section_is_single():
if issubclass(type(self.sections), sections.Section):
self.template_name = self.sections.template_name
context.update(self.sections.get_context_data(
self.request,
object_list = hasattr(self, 'object_list') and \
self.object_list,
**self.kwargs
) or {})
self.sections.prepare(self)
context.update(self.sections.get_context_data())
else:
if not self.template_name:
self.template_name = 'aircox/cms/detail.html'
context.update({
'content': self.sections.render(self.request, **kwargs)
'content': self.sections.render(self)
})
context.update(super().get_context_data(**kwargs))
@ -98,7 +91,7 @@ class BaseView:
else None
if self.menus:
context['menus'] = {
k: v.render(self.request, **kwargs)
k: v.render(self)
for k, v in self.menus.items()
if v is not self
}
@ -148,6 +141,12 @@ class PostListView(BaseView, ListView):
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
default = self.prepare_list()
if default:
qs = self.list.get_object_list()
if qs:
return qs
if self.route:
qs = self.route.get_queryset(self.model, self.request,
**self.kwargs)
@ -166,15 +165,23 @@ class PostListView(BaseView, ListView):
return qs
def prepare_list(self):
"""
Prepare the list and return True if the list has been created using
defaults.
"""
if not self.list:
self.list = sections.List(
truncate = 32,
paginate_by = 0,
)
else:
self.list = sections.List(
truncate = 32,
paginate_by = 0,
)
default = True
elif type(self.list) == type:
self.list = self.list(paginate_by = 0)
self.template_name = self.list.template_name
self.css_class = self.list.css_class
default = False
self.list.prepare(self)
if self.request.GET.get('fields'):
self.list.fields = [
@ -184,12 +191,14 @@ class PostListView(BaseView, ListView):
# done in list
# Actions.make(self.request, object_list = self.object_list)
return default
def get_context_data(self, **kwargs):
self.prepare_list()
self.add_css_class('list')
context = super().get_context_data(**kwargs)
if not context.get('object_list'):
context['object_list'] = self.list.object_list
if self.route and not context.get('title'):
context['title'] = self.route.get_title(
@ -232,12 +241,14 @@ class PostDetailView(BaseView, DetailView):
"""
Handle new comments
"""
self.sections.prepare(self)
if not self.comments:
for section in self.sections:
if issubclass(type(section), sections.Comments):
self.comments = section
self.object = self.get_object()
self.comments.prepare(self)
self.comments.post(self, request, self.object)
return self.get(request, *args, **kwargs)
@ -249,8 +260,5 @@ class PageView(BaseView, TemplateView):
If sections is a list of sections, then render like a detail view;
If it is a single section, render it as website.html view;
"""
# dirty hack in order to accept a "model" kwargs, to allow "model=None"
# in routes. Cf. website.register (at if model / else)
model = None

View File

@ -9,6 +9,7 @@ import aircox.cms.routes as routes_
import aircox.cms.views as views
import aircox.cms.models as models
import aircox.cms.sections as sections
import aircox.cms.sections as sections_
class Website:
@ -37,7 +38,7 @@ class Website:
## components
Registration = namedtuple('Registration',
'name model routes as_default'
'name model routes default'
)
urls = []
@ -64,7 +65,7 @@ class Website:
if self.comments_routes:
self.register_comments()
def register_model(self, name, model, as_default):
def register_model(self, name, model, default):
"""
Register a model and update model's fields with few data:
- _website: back ref to self
@ -77,9 +78,9 @@ class Website:
if reg.model is model:
return reg
raise ValueError('A model has yet been registered under "{}"'
.format(name))
.format(reg.model, name))
reg = self.Registration(name, model, [], as_default)
reg = self.Registration(name, model, [], default)
self.registry[name] = reg
model._registration = reg
model._website = self
@ -97,74 +98,89 @@ class Website:
continue
self.exposures += section._exposure.gather(section)
def register(self, name, routes = [], view = views.PageView,
model = None, sections = None,
as_default = False, **view_kwargs):
"""
Register a view using given name and routes. If model is given,
register the views for it.
* name is used to register the routes as urls and the model if given
* routes: can be a path or a route used to generate urls for the view.
Can be a one item or a list of items.
* view: route that is registered for the given routes
* model: model being registrated. If given, register it in the website
under the given name, and make it available to the view.
* as_default: make the view available as a default view.
"""
if type(routes) not in (tuple, list):
routes = [ routes ]
def __route_to_url(self, name, route, view, sections, kwargs):
# route can be a tuple
if type(route) in (tuple,list):
route, view = route
view = view.as_view(
website = self, **kwargs
)
# model registration
if model:
reg = self.register_model(name, model, as_default)
reg.routes.extend(routes)
view_kwargs['model'] = model
else:
view_kwargs['model'] = None
# route can be a route or a string
if type(route) == type and issubclass(route, routes_.Route):
return route.as_url(name, view)
# init view
if not view_kwargs.get('menus'):
view_kwargs['menus'] = self.menus
if sections:
self.register_exposures(sections)
view_kwargs['sections'] = sections
view = view.as_view(
website = self,
**view_kwargs
return url(
slugify(name) if not route else str(route),
view = view, name = name, kwargs = kwargs
)
# url gen
def add_page(self, name, routes = [], view = views.PageView,
sections = None, default = False, **kwargs):
"""
Add a view and declare it on the given routes.
* routes: list of routes or patterns, or tuple (route/pattern, view)
to force a view to be used;
* view: view to use by default to render the page;
* sections: sections to display on the view;
* default: use this as a default view;
* kwargs: extra kwargs to pass to the view;
If view is a section, generate a PageView with this section as
child. Note: the kwargs are passed to the PageView constructor.
"""
if view and issubclass(type(view), sections_.Section):
sections, view = view, views.PageView
if not kwargs.get('menus'):
kwargs['menus'] = self.menus
if sections:
self.register_exposures(sections)
view = view.as_view(website = self, sections = sections, **kwargs)
if not hasattr(routes, '__iter__'):
routes = [routes]
self.urls += [
route.as_url(name, view)
if type(route) == type and issubclass(route, routes_.Route)
else url(slugify(name) if not route else route,
view = view, name = name)
self.__route_to_url(name, route, view, sections, kwargs)
for route in routes
]
def register_dl(self, name, model, sections = None, routes = None,
list_view = views.PostListView,
detail_view = views.PostDetailView,
list_kwargs = {}, detail_kwargs = {},
as_default = False):
def add_model(self, name, model, sections = None, routes = None,
default = False,
list_view = views.PostListView,
detail_view = views.PostDetailView,
**kwargs):
"""
Register a detail and list view for a given model, using
routes.
Add a model to the Website, register it and declare its routes.
Just a wrapper around `register`.
* model: model to register
* sections: sections to display in the *detail* view;
* routes: routes to use for the *list* view -- cf. add_page.routes;
* default: use as default route;
* list_view: use it as view for lists;
* detail_view: use it as view for details;
* kwargs: extra kwargs arguments to pass to the view;
"""
# register the model and the routes
reg = self.register_model(name, model, default)
reg.routes.extend([
route[0] if type(route) in (list,tuple) else route
for route in routes
])
reg.routes.append(routes_.DetailRoute)
kwargs['model'] = model
if sections:
self.register(name, [ routes_.DetailRoute ], view = detail_view,
model = model, sections = sections,
as_default = as_default,
**detail_kwargs)
self.add_page(name, view = detail_view, sections = sections,
routes = routes_.DetailRoute, default = default,
**kwargs)
if routes:
self.register(name, routes, view = list_view,
model = model, as_default = as_default,
**list_kwargs)
self.add_page(name, view = list_view, routes = routes,
default = default, **kwargs)
def register_comments(self):
"""
@ -173,16 +189,12 @@ class Website:
Just a wrapper around `register`.
"""
self.register(
self.add_model(
'comment',
view = views.PostListView,
routes = [routes.ThreadRoute],
model = models.Comment,
routes = [routes.ThreadRoute],
css_class = 'comments',
list = sections.Comments(
truncate = 30,
fields = ['content','author','date','time'],
)
list = sections.Comments
)
def set_menu(self, menu):
@ -204,7 +216,7 @@ class Website:
given route.
"""
for r in self.registry.values():
if r.as_default and route in r.routes:
if r.default and route in r.routes:
return r
def reverse(self, model, route, use_default = True, **kwargs):
@ -226,7 +238,7 @@ class Website:
return ''
for r in self.registry.values():
if r.as_default and route in r.routes:
if r.default and route in r.routes:
try:
name = route.make_view_name(r.name)
return reverse(name, kwargs = kwargs)

View File

@ -755,7 +755,7 @@ class Track(Related):
blank=True,
)
pos_in_secs = models.BooleanField(
_('use seconds'),
_('seconds'),
default = False,
help_text=_('position in the playlist is expressed in seconds')
)

View File

@ -95,7 +95,6 @@ class Player(sections.Section):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update({
'base_template': 'aircox/cms/section.html',
'live_streams': self.live_streams,
@ -220,7 +219,7 @@ class Diffusions(sections.List):
return ' / \n'.join([str_sched(sched)
for sched in programs.Schedule.objects \
.filter(program = self.object.related.pk)
.filter(program = self.object and self.object.related.pk)
])
@ -256,27 +255,42 @@ class Sounds(sections.List):
]
class Schedule(Diffusions):
class ListByDate(sections.List):
"""
Render a list of diffusions in the form of a schedule
List that add a navigation by date in its header.
"""
template_name = 'aircox/website/schedule.html'
date = None
nav_date_format = '%a. %d'
fields = [ 'time', 'image', 'title']
template_name = 'aircox/website/list_by_date.html'
message_empty = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_css_class('schedule')
model = None
@staticmethod
def get_week_dates(date):
date = None
"""
date of the items to print
"""
nav_days = 7
"""
number of days to display in the header
"""
nav_date_format = '%a. %d'
"""
format of dates to display in the header
"""
nav_per_week = True
"""
if true, print days in header by week
"""
def nav_dates(self, date):
"""
Return a list of dates of the week of the given date.
"""
first = date - tz.timedelta(days=date.weekday())
return [ first + tz.timedelta(days=i) for i in range(0, 7) ]
first = int((self.nav_days - 1) / 2)
first = date - tz.timedelta(days=date.weekday()) \
if self.nav_per_week else \
date - tz.timedelta(days=first)
return [ first + tz.timedelta(days=i) for i in range(0, self.nav_days) ]
def date_or_default(self):
if self.date:
@ -287,40 +301,20 @@ class Schedule(Diffusions):
day = int(self.kwargs['day']),
hour = 0, minute = 0, second = 0,
microsecond = 0)
return tz.datetime.now()
def get_object_list(self):
date = self.date_or_default()
return routes.DateRoute.get_queryset(
models.Diffusion, self.request, date.year, date.month,
date.day
).order_by('date')
return tz.now()
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
date = self.date_or_default()
dates = [
(date, models.Diffusion.reverse(
routes.DateRoute,
year = date.year, month = date.month, day = date.day
))
for date in self.get_week_dates(date)
]
dates = [ (date, self.get_date_url(date))
for date in self.nav_dates(date) ]
next_week = dates[-1][0] + tz.timedelta(days=1)
next_week = models.Diffusion.reverse(
routes.DateRoute,
year = next_week.year, month = next_week.month,
day = next_week.day
)
next_week = self.get_date_url(next_week)
prev_week = dates[0][0] - tz.timedelta(days=1)
prev_week = models.Diffusion.reverse(
routes.DateRoute,
year = prev_week.year, month = prev_week.month,
day = prev_week.day
)
prev_week = self.get_date_url(prev_week)
context.update({
'date': date,
@ -330,15 +324,90 @@ class Schedule(Diffusions):
})
return context
@staticmethod
def get_date_url(date):
"""
return a url to the list for the given date
"""
@property
def url(self):
return None
class Logs(Schedule):
class Schedule(Diffusions,ListByDate):
"""
Render a list of diffusions in the form of a schedule
"""
fields = [ 'time', 'image', 'title', 'content', 'info', 'actions' ]
truncate = 30
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_css_class('schedule')
def get_object_list(self):
date = self.date_or_default()
return routes.DateRoute.get_queryset(
models.Diffusion, self.request, date.year, date.month,
date.day
).order_by('date')
@staticmethod
def get_date_url(date):
"""
return an url for the given date
"""
return models.Diffusion.reverse(
routes.DateRoute,
year = date.year, month = date.month, day = date.day,
)
class Logs(ListByDate):
"""
Return a list of played stream sounds and diffusions.
"""
template_name = 'aircox/website/schedule.html'
# HERE -- + rename aircox/website/schedule to dated_list
@staticmethod
def make_item(log):
"""
Return a list of items to add to the playlist.
"""
if issubclass(type(log.related), programs.Diffusion):
diff = log.related
post = models.Diffusion.objects.filter(related = diff).first() \
or models.Program.objects.filter(related = diff.program).first() \
or ListItem(title = diff.program.name)
post.date = diff.start
return post
if issubclass(type(log.related), programs.Track):
track = log.related
post = ListItem(
title = '{artist} &#8212; {name}'.format(
artist = track.artist,
name = track.name,
),
date = log.date,
content = track.info,
info = '',
)
return post
def get_object_list(self):
station = self.view._website.station
qs = station.get_played(
models = [ programs.Diffusion, programs.Track ],
).filter(
date__year = int(year), date__month = int(month),
date__day = int(day)
)
# TODO for each, exclude if there is a diffusion (that has not been logged)
return [ cl.make_item(log) for log in qs ]
@staticmethod
def get_date_url(date):
pass

View File

@ -1,19 +0,0 @@
{% extends 'aircox/cms/list.html' %}
{% block header %}
<header>
<script>
sched_fields = [ {% for field in list.fields %}"fields={{ field }}",{% endfor %} ];
sched_fields = sched_fields.join('&');
</script>
<a href="{{ prev_week }}" onclick="return Section.load_event(event, sched_fields);">&lt;</a>
{% for curr, url in dates %}
<a href="{{ url }}" {% if curr == date %}class="selected" {% endif %}
onclick="return Section.load_event(event);">
{{ curr|date:'D. d' }}
</a>
{% endfor %}
<a href="{{ next_week }}" onclick="return Section.load_event(event, sched_fields);">&gt;</a>
</header>
{% endblock %}