work on exposure to support inheritance, start to work on calendar with dynamic loading
This commit is contained in:
parent
df65d310f5
commit
3d8abd9df8
134
cms/README.md
134
cms/README.md
|
@ -2,7 +2,9 @@
|
|||
Simple CMS generator used in Aircox. Main features includes:
|
||||
- website configuration and templating
|
||||
- articles and static pages
|
||||
- sections: embeddable views used to display related elements of an object
|
||||
- sections:
|
||||
- embeddable views used to display related elements of an object
|
||||
- load dynamic personnalized data
|
||||
- posts related to external models:
|
||||
- attributes binding, automatically updated
|
||||
- personalization of view rendering, using templates or sections
|
||||
|
@ -53,6 +55,42 @@ function will be called using arguments **(post, related_object)**.
|
|||
At rendering, the property *info* can be retrieved from the Post. It is however
|
||||
not a field.
|
||||
|
||||
|
||||
### Combine multiple models
|
||||
It is possible to render views that combine multiple models, with some
|
||||
limitation. In order to make it possible, the `GenericModel` has been created
|
||||
with its own query__set class `QCombine`.
|
||||
|
||||
It can be used to register views that must render list of different elements,
|
||||
such as for search. Once declared, you just need to register the class with
|
||||
a view to the website.
|
||||
|
||||
|
||||
```python
|
||||
import aircox.cms.qcombine as qcombine
|
||||
|
||||
class Article(RelatedPost):
|
||||
pass
|
||||
|
||||
class Program(RelatedPost):
|
||||
pass
|
||||
|
||||
class Diffusion(RelatedPost):
|
||||
pass
|
||||
|
||||
|
||||
class Publication(qcombine.GenericModel):
|
||||
models = [
|
||||
Program,
|
||||
Diffusion,
|
||||
Article,
|
||||
]
|
||||
|
||||
# website.register(... model = Publication ...)
|
||||
# qs = Publication.objects.filter(title__icontain == "Hi")
|
||||
```
|
||||
|
||||
|
||||
## Routes
|
||||
Routes are used to generate the URLs of the website. We provide some of the
|
||||
common routes: for the detail view of course, but also to select all posts or
|
||||
|
@ -60,8 +98,6 @@ by thread, date, search, tags.
|
|||
|
||||
It is of course possible to create your own routes.
|
||||
|
||||
Routes are registered to a router (FIXME: it might be possible that we remove
|
||||
this later)
|
||||
|
||||
## Sections
|
||||
Sections are used to render part of a publication, for example to render a
|
||||
|
@ -76,6 +112,60 @@ files (e.g. one for each type of publication), we prefer to declare these
|
|||
sections and configure them. This reduce the work, keep design coherent,
|
||||
and reduce the risk of bugs and so on.
|
||||
|
||||
|
||||
## Exposure
|
||||
Section can expose one or more methods whose result are accessible from the
|
||||
web. Such function is called an *Exposure*, and return a *string*(!).
|
||||
|
||||
The routes to the exposures are generated and registered at the same time
|
||||
a model is registered to the website, with the corresponding Section.
|
||||
|
||||
To expose a method, there is to steps:
|
||||
* decorate the parent class with `aircox.cms.decorators.expose`
|
||||
* decorate the method with `aircox.cms.decorators.expose`
|
||||
|
||||
An exposed method has an `_exposure` attribute, that is an instance of
|
||||
Exposure, that holds exposure's informations and offer some tools to
|
||||
work with exposure:
|
||||
|
||||
```python
|
||||
from aircox.cms.decorators import expose
|
||||
|
||||
@expose
|
||||
class MySection(sections.Section):
|
||||
@expose
|
||||
hi(self, request, *args, **kwargs):
|
||||
return "hi!"
|
||||
|
||||
@expose
|
||||
hello(self, request, name, *args, **kwargs):
|
||||
return "hello " + name
|
||||
|
||||
hello._exposure.pattern = "(?P<name>\w+)"
|
||||
```
|
||||
|
||||
The urls will be generated once the class decorator is called, using for each
|
||||
exposed item the following values:
|
||||
- name: `'exps.' + class._exposure.name + '.' + method._exposure.name`
|
||||
- url pattern: `'exps/' + class._exposure.pattern + '/' + method._exposure.name`
|
||||
- kwargs: `{'cl': class }`
|
||||
|
||||
Check also the javascript scripts where there are various utility to get the
|
||||
result of exposure and map them easily.
|
||||
|
||||
|
||||
### Using templates
|
||||
It is possible to render using templates, by setting `_exposure.template_name`.
|
||||
In this case, the function is wrapped using `aircox.cms.decorators.template`,
|
||||
and have this signature:
|
||||
|
||||
```python
|
||||
function(class, request) -> dict
|
||||
```
|
||||
|
||||
The returned dictionnary is used as context object to render the template.
|
||||
|
||||
|
||||
## Website
|
||||
This class is used to create the website itself and regroup all the needed
|
||||
informations to make it beautiful. There are different steps to create the
|
||||
|
@ -83,22 +173,33 @@ website, using instance of the Website class:
|
|||
|
||||
1. Create the Website instance with all basic information: name, tags,
|
||||
description, menus and so on.
|
||||
2. For each type of publication, register it using a Post model, a list of
|
||||
used sections, routes, and optional parameters. The given name is used
|
||||
for routing.
|
||||
2. Register the views: eventually linking with a model, a list of used
|
||||
sections, routes, and optional parameters. The given name is used for
|
||||
routing.
|
||||
3. Register website's URLs to Django.
|
||||
4. Change templates and css styles if needed.
|
||||
|
||||
It also offers various facilities, such as comments view registering.
|
||||
It also offers various facilities, such as comments view registering, menu
|
||||
initialization, url reversing.
|
||||
|
||||
|
||||
### Default view and reverse
|
||||
The Website class offers a `reverse` function that can be used to reverse
|
||||
a url using a Route, a Post and kwargs arguments.
|
||||
|
||||
It is possible to ask to reverse to a default route if the reversing process
|
||||
failed with the given model. In this case, it uses the registered views
|
||||
that have been registered as *default view* (register function with argument
|
||||
`as_default=True`).
|
||||
|
||||
|
||||
# Rendering
|
||||
## Views
|
||||
They are three kind of views, among which two are used to render related content (`PostListView`, `PostDetailView`), and one is used to set arbitrary content at given url pattern (`PageView`).
|
||||
|
||||
The `PostDetailView` and `PageView` use a list of sections to render their content. While `PostDetailView` is related to a model instance, `PageView` just render its sections.
|
||||
The `PostDetailView` and `PageView` use a list of sections to render their content. If there is only one section in the view, it is merged as the main content instead of being a block in the content; this has for consequence that the template's context are merged and rendering at the same time than the page instead of before.
|
||||
|
||||
`PostListView` uses the route that have been matched in order to render the list. Internally, it uses `sections.List` to render the list, if no section is given by the user. The context used to render the page is initialized using the list's rendering context.
|
||||
`PostListView` uses the route that have been matched in order to render the list. Internally, it uses `sections.List` to render the list, if no section is given by the user. The context used to render the page is initialized using the list's rendering context, then completed with its own context's stuff.
|
||||
|
||||
## Sections
|
||||
A Section behave similar to a view with few differences:
|
||||
|
@ -118,20 +219,19 @@ It is also possible to specify a list of fields that are rendered in the list, a
|
|||
|
||||
# Rendered content
|
||||
## Templates
|
||||
There are two base template that are extended by the others:
|
||||
* **section.html**: used to render a single section;
|
||||
* **website.html**: website page layout;
|
||||
All sections and pages uses the **website.html** view to be rendered. If `context['embed']` is True, it only render the content without any menu. Previously there was two distinct templates, but in order to reduce the amount of code, keep coherence between the templates, they have been merged.
|
||||
|
||||
These both define the following blocks, with their related container (declared *inside* the block):
|
||||
The following blocks are available, with their related container (declared *inside* the block):
|
||||
* *title*: the optional title in a `<h1>` tag;
|
||||
* *header*: the optional header in a `<header>` tag;
|
||||
* *content*: the content itself; for *section* there is not related container, for *website* container is declared *outside* as an element of class `.content`;
|
||||
* *content*: the content itself; by default there is no related container. By convention however, if there is a container it has the class `.content`;
|
||||
* *footer*: the footer in a `<footer>` tag;
|
||||
|
||||
The other templates Aircox.cms uses are:
|
||||
* **details.html**: used to render post details (extends *website.html*);
|
||||
* **list.html**: used to render lists, extends the given template `base_template` (*section.html* or *website.html*);
|
||||
* **comments.html**: used to render comments including a form (*list.html*)
|
||||
* **detail.html**: used to render post details (extends *website.html*);
|
||||
* **list.html**: used to render lists (extends *website.html*);
|
||||
* **list__item.html**: item in a list;
|
||||
* **comments.html**: used to render comments including a form (*list.html*);
|
||||
|
||||
|
||||
# CSS classes
|
||||
|
|
|
@ -1,32 +1,13 @@
|
|||
import uuid
|
||||
import inspect
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.conf.urls import url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
def template(name):
|
||||
"""
|
||||
the decorated function returns a context that is used to
|
||||
render a template value.
|
||||
|
||||
* template_name: name of the template to use
|
||||
* hide_empty: an empty context returns an empty string
|
||||
"""
|
||||
def template_(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if kwargs.get('cl'):
|
||||
context = func(kwargs.pop('cl'), request, *args, **kwargs)
|
||||
else:
|
||||
context = func(request, *args, **kwargs)
|
||||
if not context:
|
||||
return ''
|
||||
context['embed'] = True
|
||||
return render_to_string(name, context, request=request)
|
||||
return wrapper
|
||||
return template_
|
||||
|
||||
|
||||
class Exposure:
|
||||
"""
|
||||
Define an exposure. Look at @expose decorator.
|
||||
|
@ -35,8 +16,6 @@ class Exposure:
|
|||
"""generated view name"""
|
||||
pattern = None
|
||||
"""url pattern"""
|
||||
items = None
|
||||
"""for classes: list of url objects for exposed methods"""
|
||||
template_name = None
|
||||
"""
|
||||
for methods: exposed method return a context to be use with
|
||||
|
@ -44,24 +23,69 @@ class Exposure:
|
|||
"""
|
||||
item = None
|
||||
"""
|
||||
exposed item
|
||||
Back ref to the exposed item, can be used to detect inheritance of
|
||||
exposed classes.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def url(self, *args, **kwargs):
|
||||
@staticmethod
|
||||
def gather(cl):
|
||||
"""
|
||||
reverse url for this exposure
|
||||
Prepare all exposure declared in self.cl, create urls and return
|
||||
them. This is done at this place in order to allow sub-classing
|
||||
of exposed classes.
|
||||
"""
|
||||
return reverse(self.name, *args, **kwargs)
|
||||
def view(request, key, *args, fn = None, **kwargs):
|
||||
if not fn:
|
||||
if not hasattr(cl, key):
|
||||
raise Http404()
|
||||
|
||||
def prefix(self, parent):
|
||||
"""
|
||||
prefix exposure with the given parent
|
||||
"""
|
||||
self.name = parent.name + '.' + self.name
|
||||
self.pattern = parent.pattern + '/' + self.pattern
|
||||
fn = getattr(cl, key)
|
||||
if not hasattr(fn, '_exposure'):
|
||||
raise Http404()
|
||||
|
||||
exp = fn._exposure
|
||||
res = fn(request, *args, **kwargs)
|
||||
if res and exp.template_name:
|
||||
ctx = res or {}
|
||||
ctx.update({
|
||||
'embed': True,
|
||||
'exp': cl._exposure,
|
||||
})
|
||||
res = render_to_string(exp.template_name,
|
||||
v, request = request)
|
||||
return HttpResponse(res or '')
|
||||
|
||||
# id = str(uuid.uuid1())
|
||||
exp = cl._exposure
|
||||
exp.pattern = '{name}/{id}'.format(name = exp.name, id = id(cl))
|
||||
exp.name = 'exps.{name}.{id}'.format(name = exp.name, id = id(cl))
|
||||
|
||||
urls = []
|
||||
|
||||
for name, fn in inspect.getmembers(cl):
|
||||
if name.startswith('__') or not hasattr(fn, '_exposure'):
|
||||
continue
|
||||
fn_exp = fn._exposure
|
||||
if not fn_exp.pattern:
|
||||
continue
|
||||
|
||||
name = fn_exp.name or name
|
||||
pattern = exp.pattern + '/(?P<key>{name})/{pattern}'.format(
|
||||
name = name, pattern = fn_exp.pattern
|
||||
)
|
||||
|
||||
urls.append(url(
|
||||
pattern, name = exp.name, view = view,
|
||||
kwargs = { 'fn': fn }
|
||||
))
|
||||
|
||||
urls.append(url(
|
||||
exp.pattern + '(?P<key>\w+)', name = exp.name, view = view
|
||||
))
|
||||
return urls
|
||||
|
||||
|
||||
def expose(item):
|
||||
|
@ -74,7 +98,7 @@ def expose(item):
|
|||
|
||||
The exposed method has the following signature:
|
||||
|
||||
`func(cl, request, parent, *args, **kwargs) -> str`
|
||||
`func(cl, request, *args, **kwargs) -> str`
|
||||
|
||||
Data related to the exposure are put in the `_exposure` attribute,
|
||||
as instance of Exposure.
|
||||
|
@ -96,40 +120,6 @@ def expose(item):
|
|||
pattern = get_attr('pattern', item.__name__)
|
||||
|
||||
exp = Exposure(name = name, pattern = pattern, item = item)
|
||||
|
||||
# expose a class container: set _exposure attribute
|
||||
if type(item) == type:
|
||||
exp.name = 'exp.' + exp.name
|
||||
exp.items = []
|
||||
|
||||
for func in item.__dict__.values():
|
||||
if not hasattr(func, '_exposure'):
|
||||
continue
|
||||
|
||||
sub = func._exposure
|
||||
sub.prefix(exp)
|
||||
|
||||
# FIXME: template warping lose args
|
||||
if sub.template_name:
|
||||
sub.item = template(sub.template_name)(sub.item)
|
||||
|
||||
func = url(sub.pattern, name = sub.name,
|
||||
view = func, kwargs = {'cl': item})
|
||||
exp.items.append(func)
|
||||
|
||||
item._exposure = exp;
|
||||
return item
|
||||
# expose a method: wrap it
|
||||
else:
|
||||
if hasattr(item, '_exposure'):
|
||||
del item._exposure
|
||||
|
||||
def wrapper(request, as_str = False, *args, **kwargs):
|
||||
v = exp.item(request, *args, **kwargs)
|
||||
if as_str:
|
||||
return v
|
||||
return HttpResponse(v)
|
||||
wrapper._exposure = exp;
|
||||
return wrapper
|
||||
|
||||
item._exposure = exp;
|
||||
return item
|
||||
|
||||
|
|
|
@ -113,9 +113,10 @@ class QCombine:
|
|||
return list(it)
|
||||
|
||||
|
||||
|
||||
|
||||
class Manager(type):
|
||||
"""
|
||||
Metaclass used to generate the GenericModel.objects property
|
||||
"""
|
||||
models = []
|
||||
|
||||
@property
|
||||
|
@ -141,3 +142,12 @@ class GenericModel(metaclass=Manager):
|
|||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
@classmethod
|
||||
def reverse(cl, route, use_default = True, **kwargs):
|
||||
"""
|
||||
Reverse a url using a given route for the model - simple wrapper
|
||||
around cl._website.reverse
|
||||
"""
|
||||
return cl._website.reverse(cl, route, use_default, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ from django.db import models
|
|||
from django.utils import timezone as tz
|
||||
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
|
||||
|
||||
from taggit.models import Tag
|
||||
|
||||
import aircox.cms.qcombine as qcombine
|
||||
|
||||
|
||||
|
@ -14,7 +15,7 @@ class Route:
|
|||
type of route.
|
||||
|
||||
The generated url takes this form:
|
||||
name + '/' + route_name + '/' + '/'.join(route_url_args)
|
||||
name + '/' + route_name + '/' + '/'.join(params)
|
||||
|
||||
And their name (to use for reverse:
|
||||
name + '_' + route_name
|
||||
|
@ -22,8 +23,17 @@ class Route:
|
|||
By default name is the verbose name of the model. It is always in
|
||||
singular form.
|
||||
"""
|
||||
name = None # route name
|
||||
url_args = [] # arguments passed from the url [ (name : regex),... ]
|
||||
name = None
|
||||
"""
|
||||
Regular name of the route
|
||||
"""
|
||||
params = []
|
||||
"""
|
||||
Arguments passed by the url, it is a list of tupple with values:
|
||||
- name: (required) name of the argument
|
||||
- regex: (required) regular expression to append to the url
|
||||
- optional: (not required) if true, argument is optional
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cl, model, request, **kwargs):
|
||||
|
@ -50,15 +60,15 @@ class Route:
|
|||
@classmethod
|
||||
def as_url(cl, name, view, view_kwargs = None):
|
||||
pattern = '^{}/{}'.format(name, cl.name)
|
||||
if cl.url_args:
|
||||
url_args = '/'.join([
|
||||
'(?P<{}>{}){}'.format(
|
||||
arg, expr,
|
||||
(optional and optional[0] and '?') or ''
|
||||
if cl.params:
|
||||
pattern += ''.join([
|
||||
'{pre}/(?P<{name}>{regexp}){post}'.format(
|
||||
name = name, regexp = regexp,
|
||||
pre = (optional and optional[0] and '(?:') or '',
|
||||
post = (optional and optional[0] and ')?') or '',
|
||||
)
|
||||
for arg, expr, *optional in cl.url_args
|
||||
for name, regexp, *optional in cl.params
|
||||
])
|
||||
pattern += '/' + url_args
|
||||
pattern += '/?$'
|
||||
|
||||
kwargs = {
|
||||
|
@ -73,7 +83,7 @@ class Route:
|
|||
|
||||
class DetailRoute(Route):
|
||||
name = 'detail'
|
||||
url_args = [
|
||||
params = [
|
||||
('pk', '[0-9]+'),
|
||||
('slug', '(\w|-|_)+', True),
|
||||
]
|
||||
|
@ -106,7 +116,7 @@ class ThreadRoute(Route):
|
|||
- "pk" is the pk of the thread item.
|
||||
"""
|
||||
name = 'thread'
|
||||
url_args = [
|
||||
params = [
|
||||
('thread_model', '(\w|_|-)+'),
|
||||
('pk', '[0-9]+'),
|
||||
]
|
||||
|
@ -145,7 +155,7 @@ class DateRoute(Route):
|
|||
Select posts using a date with format yyyy/mm/dd;
|
||||
"""
|
||||
name = 'date'
|
||||
url_args = [
|
||||
params = [
|
||||
('year', '[0-9]{4}'),
|
||||
('month', '[0-1]?[0-9]'),
|
||||
('day', '[0-3]?[0-9]'),
|
||||
|
@ -170,11 +180,13 @@ class DateRoute(Route):
|
|||
|
||||
class SearchRoute(Route):
|
||||
"""
|
||||
Search post using request.GET['q']. It searches in fields designated by
|
||||
model.search_fields
|
||||
Search post using request.GET['q'] or q optional argument. It searches in
|
||||
fields designated by model.search_fields
|
||||
"""
|
||||
# TODO: q argument in url_args -> need to allow optional url_args
|
||||
name = 'search'
|
||||
params = [
|
||||
( 'q', '[^/]+', True)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def __search(cl, model, q):
|
||||
|
@ -187,8 +199,8 @@ class SearchRoute(Route):
|
|||
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cl, model, request, **kwargs):
|
||||
q = request.GET.get('q') or ''
|
||||
def get_queryset(cl, model, request, q = None, **kwargs):
|
||||
q = request.GET.get('q') or q or ''
|
||||
if issubclass(model, qcombine.GenericModel):
|
||||
models = model.models
|
||||
return qcombine.QCombine(
|
||||
|
@ -197,10 +209,10 @@ class SearchRoute(Route):
|
|||
return cl.__search(model, q)
|
||||
|
||||
@classmethod
|
||||
def get_title(cl, model, request, **kwargs):
|
||||
def get_title(cl, model, request, q = None, **kwargs):
|
||||
return _('Search <i>%(search)s</i> in %(model)s') % {
|
||||
'model': model._meta.verbose_name_plural,
|
||||
'search': request.GET.get('q') or '',
|
||||
'search': request.GET.get('q') or q or '',
|
||||
}
|
||||
|
||||
|
||||
|
@ -210,7 +222,7 @@ class TagsRoute(Route):
|
|||
by a '+'.
|
||||
"""
|
||||
name = 'tags'
|
||||
url_args = [
|
||||
params = [
|
||||
('tags', '(\w|-|_|\+)+')
|
||||
]
|
||||
|
||||
|
@ -221,10 +233,12 @@ class TagsRoute(Route):
|
|||
|
||||
@classmethod
|
||||
def get_title(cl, model, request, tags, **kwargs):
|
||||
import aircox.cms.utils as utils
|
||||
tags = Tag.objects.filter(slug__in = tags.split('+'))
|
||||
|
||||
# FIXME: get tag name instead of tag slug
|
||||
return _('%(model)s tagged with %(tags)s') % {
|
||||
'model': model._meta.verbose_name_plural,
|
||||
'tags': model.tags_to_html(model, tags = tags.split('+'))
|
||||
if '+' in tags else tags
|
||||
'tags': utils.tags_to_html(model, tags = tags)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ Define different Section css_class that can be used by views.Sections;
|
|||
import re
|
||||
from random import shuffle
|
||||
|
||||
from django.utils import timezone as tz
|
||||
from django.template.loader import render_to_string
|
||||
from django.views.generic.base import View
|
||||
from django.templatetags.static import static
|
||||
|
@ -15,6 +16,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
|||
from honeypot.decorators import check_honeypot
|
||||
|
||||
from aircox.cms.forms import CommentForm
|
||||
import aircox.cms.decorators as decorators
|
||||
|
||||
|
||||
class Viewable:
|
||||
|
@ -35,6 +37,22 @@ class Viewable:
|
|||
return instance
|
||||
return func
|
||||
|
||||
@classmethod
|
||||
def extends (cl, **kwargs):
|
||||
"""
|
||||
Return a sub class where the given attribute have been updated
|
||||
"""
|
||||
class Sub(cl):
|
||||
pass
|
||||
Sub.__name__ = cl.__name__
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(Sub, k, v)
|
||||
|
||||
if hasattr(cl, '_exposure'):
|
||||
return decorators.expose(Sub)
|
||||
return Sub
|
||||
|
||||
|
||||
class Sections(Viewable, list):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -122,6 +140,7 @@ class Section(Viewable, View):
|
|||
|
||||
return {
|
||||
'view': self,
|
||||
'exp': (hasattr(self, '_exposure') and self._exposure) or None,
|
||||
'tag': self.tag,
|
||||
'css_class': self.css_class,
|
||||
'attrs': self.attrs,
|
||||
|
@ -445,5 +464,85 @@ class Menu(Section):
|
|||
}
|
||||
|
||||
|
||||
class Search(Section):
|
||||
"""
|
||||
Implement a search form that can be used in menus. Model must be set,
|
||||
even if it is a generic model, in order to be able to reverse urls.
|
||||
"""
|
||||
model = None
|
||||
"""
|
||||
Model to search on
|
||||
"""
|
||||
placeholder = _('Search %(name_plural)s')
|
||||
"""
|
||||
Placeholder in the input text. The string is formatted before rendering.
|
||||
- name: model's verbose name
|
||||
- name_plural: model's verbose name in plural form
|
||||
"""
|
||||
no_button = True
|
||||
"""
|
||||
Hide submit button if true
|
||||
"""
|
||||
# TODO: (later) autocomplete using exposures -> might need templates
|
||||
|
||||
def get_content(self):
|
||||
import aircox.cms.routes as routes
|
||||
url = self.model.reverse(routes.SearchRoute)
|
||||
return """
|
||||
<form action="{url}" method="get">
|
||||
<input type="text" name="q" placeholder="{placeholder}"/>
|
||||
<input type="submit" {submit_style}/>
|
||||
</form>
|
||||
""".format(
|
||||
url = url, placeholder = self.placeholder % {
|
||||
'name': self.model._meta.verbose_name,
|
||||
'name_plural': self.model._meta.verbose_name_plural,
|
||||
},
|
||||
submit_style = (self.no_button and 'style="display: none;"') or '',
|
||||
)
|
||||
|
||||
|
||||
@decorators.expose
|
||||
class Calendar(Section):
|
||||
model = None
|
||||
template_name = "aircox/cms/calendar.html"
|
||||
|
||||
def get_context_data(self, year = None, month = None, *args, **kwargs):
|
||||
import calendar
|
||||
import aircox.cms.routes as routes
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
||||
date = tz.datetime.today()
|
||||
if year:
|
||||
date = date.replace(year = year)
|
||||
if month:
|
||||
date = date.replace(month = month)
|
||||
date = date.replace(day = 1)
|
||||
|
||||
first, count = calendar.monthrange(date.year, date.month)
|
||||
context.update({
|
||||
'first_weekday': first,
|
||||
'days': [
|
||||
(day, self.model.reverse(
|
||||
routes.DateRoute, year = date.year, month = date.month,
|
||||
day = day
|
||||
)
|
||||
) for day in range(1, count+1)
|
||||
],
|
||||
|
||||
'month': date,
|
||||
'prev_month': date - tz.timedelta(days=10),
|
||||
'next_month': date + tz.timedelta(days=31),
|
||||
})
|
||||
return context
|
||||
|
||||
@decorators.expose
|
||||
def render_exp(cl, *args, year, month, **kwargs):
|
||||
year = int(year)
|
||||
month = int(month)
|
||||
return cl.render(*args, year = year, month = month, **kwargs)
|
||||
render_exp._exposure.name = 'render'
|
||||
render_exp._exposure.pattern = '(?P<year>[0-9]{4})/(?P<month>[0-1]?[0-9])'
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
|
||||
function Part(url = '', params = '') {
|
||||
return new Part_(url, params);
|
||||
}
|
||||
|
||||
// Small utility used to make XMLHttpRequests, and map results to other
|
||||
// objects
|
||||
function Part_(url = '', params = '') {
|
||||
/// Small utility used to make XMLHttpRequests, and map results on objects.
|
||||
/// This is intended to dynamically retrieve Section and exposed data.
|
||||
///
|
||||
/// Request.Selector is a utility class that can be used to map data using
|
||||
/// selectors.
|
||||
///
|
||||
/// Since there is no a common method to render items in JS and Django, we
|
||||
/// retrieve items yet rendered, and select data over it.
|
||||
function Request_(url = '', params = '') {
|
||||
this.url = url;
|
||||
this.params = params;
|
||||
this.selectors = [];
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
Part_.prototype = {
|
||||
// XMLHttpRequest object used to retrieve data
|
||||
Request_.prototype = {
|
||||
/// XMLHttpRequest object used to retrieve data
|
||||
xhr: null,
|
||||
|
||||
// delayed actions that have been registered
|
||||
/// delayed actions that have been registered
|
||||
actions: null,
|
||||
|
||||
// registered selectors
|
||||
/// registered selectors
|
||||
selectors: null,
|
||||
|
||||
/// parse request result and save in this.stanza
|
||||
|
@ -32,7 +35,7 @@ Part_.prototype = {
|
|||
this.stanza = doc;
|
||||
},
|
||||
|
||||
// make an xhr request, and call callback(err, xhr) if given
|
||||
/// make an xhr request, and call callback(err, xhr) if given
|
||||
get: function() {
|
||||
var self = this;
|
||||
|
||||
|
@ -58,23 +61,23 @@ Part_.prototype = {
|
|||
return this;
|
||||
},
|
||||
|
||||
// send request
|
||||
/// send request
|
||||
send: function() {
|
||||
this.xhr.send();
|
||||
return this;
|
||||
},
|
||||
|
||||
// set selectors.
|
||||
// * callback: if set, call it once data are downloaded with an object
|
||||
// of elements matched with the given selectors only. The object is
|
||||
// made of `selector_name: select_result` items.
|
||||
/// set selectors.
|
||||
/// * callback: if set, call it once data are downloaded with an object
|
||||
/// of elements matched with the given selectors only. The object is
|
||||
/// made of `selector_name: select_result` items.
|
||||
select: function(selectors, callback = undefined) {
|
||||
for(var i in selectors) {
|
||||
selector = selectors[i];
|
||||
if(!selector.sort)
|
||||
selector = [selector]
|
||||
|
||||
selector = new Part_.Selector(i, selector[0], selector[1], selector[2])
|
||||
selector = new Request_.Selector(i, selector[0], selector[1], selector[2])
|
||||
selectors[i] = selector;
|
||||
this.selectors.push(selector)
|
||||
}
|
||||
|
@ -94,7 +97,7 @@ Part_.prototype = {
|
|||
return this;
|
||||
},
|
||||
|
||||
// map data using this.selectors on xhr result *and* dest
|
||||
/// map data using this.selectors on xhr result *and* dest
|
||||
map: function(dest) {
|
||||
var self = this;
|
||||
this.actions.push(function() {
|
||||
|
@ -108,7 +111,7 @@ Part_.prototype = {
|
|||
return this;
|
||||
},
|
||||
|
||||
// add an action to the list of actions
|
||||
/// add an action to the list of actions
|
||||
on: function(callback) {
|
||||
this.actions.push(callback)
|
||||
return this;
|
||||
|
@ -116,14 +119,14 @@ Part_.prototype = {
|
|||
};
|
||||
|
||||
|
||||
Part_.Selector = function(name, selector, attribute = null, all = false) {
|
||||
Request_.Selector = function(name, selector, attribute = null, all = false) {
|
||||
this.name = name;
|
||||
this.selector = selector;
|
||||
this.attribute = attribute;
|
||||
this.all = all;
|
||||
}
|
||||
|
||||
Part_.Selector.prototype = {
|
||||
Request_.Selector.prototype = {
|
||||
select: function(obj, use_attr = true) {
|
||||
if(!this.all) {
|
||||
obj = obj.querySelector(this.selector)
|
||||
|
@ -166,3 +169,59 @@ Part_.Selector.prototype = {
|
|||
}
|
||||
|
||||
|
||||
/// Just to by keep same convention between utilities
|
||||
/// Create an instance of Request_
|
||||
function Request(url = '', params = '') {
|
||||
return new Request_(url, params);
|
||||
}
|
||||
|
||||
|
||||
var Section = {
|
||||
/// Return the parent section of a DOM element. Compare it using its
|
||||
/// className. If not class_name is given, use "section"
|
||||
get_section: function(item, class_name) {
|
||||
class_name = class_name || 'section';
|
||||
while(item) {
|
||||
if(item.className && item.className.indexOf(class_name) != -1)
|
||||
break
|
||||
item = item.parentNode;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
|
||||
|
||||
/// Load a section using the given url and parameters, update the header,
|
||||
/// the content and the footer automatically. No need to add "embed" in
|
||||
/// url params.
|
||||
///
|
||||
/// If find_section is true, item is a child of the wanted section.
|
||||
///
|
||||
/// Return a Request.Selector
|
||||
load: function(item, url, params, find_section) {
|
||||
if(find_section)
|
||||
item = this.get_section(item);
|
||||
|
||||
var rq = Request(url, 'embed&' + (params || ''))
|
||||
return rq.get().select({
|
||||
'header': ['header', 'innerHTML', true],
|
||||
'content': ['.content', 'innerHTML', true],
|
||||
'footer': ['footer', 'innerHTML', true]
|
||||
}).map(item).send();
|
||||
},
|
||||
|
||||
|
||||
/// Load a Section on event, and handle it.
|
||||
load_event: function(event, params) {
|
||||
var item = event.currentTarget;
|
||||
var url = item.getAttribute('href');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.load(item, url, params, true);
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
25
cms/templates/aircox/cms/calendar.html
Normal file
25
cms/templates/aircox/cms/calendar.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "aircox/cms/website.html" %}
|
||||
|
||||
{% block header %}
|
||||
<header>
|
||||
<a href="{% url exp.name key="render" year=prev_month.year month=prev_month.month %}"
|
||||
onclick="return Section.load_event(event);"><</a>
|
||||
|
||||
<time>{{ month|date:'F Y' }}</time>
|
||||
|
||||
<a href="{% url exp.name key="render" year=next_month.year month=next_month.month %}"
|
||||
onclick="return Section.load_event(event);">></a>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content">
|
||||
<div first_weekday="{{ first_weekday }}">
|
||||
{% for day, url in days %}
|
||||
<a href="{{ url }}">{{ day }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class Website:
|
|||
"""
|
||||
self.registry = {}
|
||||
self.exposures = []
|
||||
self.urls = [ url(r'^exp/', include(self.exposures)) ]
|
||||
self.urls = [ url(r'^exps/', include(self.exposures)) ]
|
||||
self.menus = {}
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
@ -95,10 +95,7 @@ class Website:
|
|||
for section in sections:
|
||||
if not hasattr(section, '_exposure'):
|
||||
continue
|
||||
self.exposures += [
|
||||
url for url in section._exposure.items
|
||||
if url not in self.urls
|
||||
]
|
||||
self.exposures += section._exposure.gather(section)
|
||||
|
||||
def register(self, name, routes = [], view = views.PageView,
|
||||
model = None, sections = None,
|
||||
|
|
14
notes.md
14
notes.md
|
@ -19,23 +19,20 @@
|
|||
- config generation and sound diffusion
|
||||
|
||||
- cms:
|
||||
- switch to abstract class and remove qcombine (or keep it smw else)?
|
||||
- empty content -> empty string
|
||||
- update documentation:
|
||||
- cms.views
|
||||
- cms.exposure
|
||||
- cms.script
|
||||
- cms.qcombine
|
||||
- routes
|
||||
- tag name instead of tag slug for the title
|
||||
- optional url args
|
||||
- cms.exposure; make it right, see nomenclature, + docstring
|
||||
- admin cms
|
||||
- content management -> do we use a markup language?
|
||||
- sections:
|
||||
- calendar
|
||||
- article list with the focus
|
||||
-> set html attribute based on values that are public
|
||||
|
||||
- website:
|
||||
- strftime on diffusion default name from related model
|
||||
- render schedule does not get the correct list
|
||||
- diffusions:
|
||||
- filter sounds for undiffused diffusions
|
||||
- print sounds of diffusions
|
||||
|
@ -46,9 +43,6 @@
|
|||
- seek bar
|
||||
- load complete week for a schedule?
|
||||
- list of played diffusions and tracks when non-stop;
|
||||
- search input in a section
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class Player(sections.Section):
|
|||
)
|
||||
|
||||
if not qs or not qs[0].is_date_in_my_range():
|
||||
return ''
|
||||
return {}
|
||||
|
||||
qs = qs[0]
|
||||
post = models.Diffusion.objects.filter(related = qs) or \
|
||||
|
|
|
@ -559,7 +559,7 @@ player = {
|
|||
/** utility & actions **/
|
||||
/// update on air informations
|
||||
update_on_air: function() {
|
||||
part = Part('{% url "exp.player.on_air" %}').get()
|
||||
rq = Request('{% url exp.name key="on_air" %}').get()
|
||||
.select({
|
||||
title: '.title',
|
||||
url: ['.url', 'href'],
|
||||
|
|
|
@ -3,43 +3,17 @@
|
|||
{% block header %}
|
||||
<header>
|
||||
<script>
|
||||
function update_schedule(event) {
|
||||
var target = event.currentTarget;
|
||||
var url = target.getAttribute('href');
|
||||
var schedule = target;
|
||||
|
||||
// prevent event
|
||||
event.preventDefault();
|
||||
|
||||
// get schedule
|
||||
while(schedule) {
|
||||
if (schedule.className &&
|
||||
schedule.className.indexOf('section_schedule') != -1)
|
||||
break;
|
||||
schedule = schedule.parentNode;
|
||||
}
|
||||
if(!schedule)
|
||||
return;
|
||||
console.log(schedule.className)
|
||||
|
||||
fields = [ {% for field in list.fields %}"fields={{ field }}",{% endfor %} ];
|
||||
fields = fields.join('&');
|
||||
|
||||
part = new Part(url, 'embed&' + fields);
|
||||
part.get().select({
|
||||
'header': ['header', 'innerHTML', true],
|
||||
'content': ['.content', 'innerHTML', true],
|
||||
}).map(schedule).send();
|
||||
}
|
||||
sched_fields = [ {% for field in list.fields %}"fields={{ field }}",{% endfor %} ];
|
||||
sched_fields = sched_fields.join('&');
|
||||
</script>
|
||||
<a href="{{ prev_week }}" onclick="update_schedule(event); return true;"><</a>
|
||||
<a href="{{ prev_week }}" onclick="return Section.load_event(event, sched_fields);"><</a>
|
||||
{% for curr, url in dates %}
|
||||
<a href="{{ url }}" {% if curr == date %}class="selected" {% endif %}
|
||||
onclick="update_schedule(event); return true;">
|
||||
onclick="return Section.load_event(event);">
|
||||
{{ curr|date:'D. d' }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
<a href="{{ next_week }}" onclick="update_schedule(event); return true;">></a>
|
||||
<a href="{{ next_week }}" onclick="return Section.load_event(event, sched_fields);">></a>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user