diff --git a/cms/routes.py b/cms/routes.py index 9f10153..c1f978e 100644 --- a/cms/routes.py +++ b/cms/routes.py @@ -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)) diff --git a/cms/sections.py b/cms/sections.py index 87f6e3e..a8dbb9f 100644 --- a/cms/sections.py +++ b/cms/sections.py @@ -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), diff --git a/cms/views.py b/cms/views.py index 4645977..51ec7b3 100644 --- a/cms/views.py +++ b/cms/views.py @@ -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 diff --git a/cms/website.py b/cms/website.py index e813fcc..2c72955 100644 --- a/cms/website.py +++ b/cms/website.py @@ -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) diff --git a/programs/models.py b/programs/models.py index ea93bd1..3809958 100755 --- a/programs/models.py +++ b/programs/models.py @@ -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') ) diff --git a/website/sections.py b/website/sections.py index 56b3224..e86eade 100644 --- a/website/sections.py +++ b/website/sections.py @@ -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} — {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 diff --git a/website/templates/aircox/website/schedule.html b/website/templates/aircox/website/schedule.html deleted file mode 100644 index 640c068..0000000 --- a/website/templates/aircox/website/schedule.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'aircox/cms/list.html' %} - -{% block header %} -
- -< -{% for curr, url in dates %} - - {{ curr|date:'D. d' }} - -{% endfor %} -> -
-{% endblock %} -