forked from rc/aircox
admin; section.get -> section.render; templates fix; menu are now per view; doc
This commit is contained in:
parent
b99dec05e3
commit
21f3e89101
|
@ -26,8 +26,8 @@ A **Website** holds all required informations to run the server instance. It
|
||||||
is used to register all kind of posts, routes to the views, menus, etc.
|
is used to register all kind of posts, routes to the views, menus, etc.
|
||||||
|
|
||||||
Basically, for each type of publication, the user declare the corresponding
|
Basically, for each type of publication, the user declare the corresponding
|
||||||
model, the routes, the sections used for rendering, and register them using
|
model, the routes, the views used to render it, using `website.register`.
|
||||||
website.register.
|
|
||||||
|
|
||||||
## Posts
|
## Posts
|
||||||
**Post** is the base model for a publication. **Article** is the provided model
|
**Post** is the base model for a publication. **Article** is the provided model
|
||||||
|
@ -50,7 +50,6 @@ class MyModelPost(RelatedPost):
|
||||||
Note: it is possible to assign a function as a bounded value; in such case, the
|
Note: it is possible to assign a function as a bounded value; in such case, the
|
||||||
function will be called using arguments **(post, related_object)**.
|
function will be called using arguments **(post, related_object)**.
|
||||||
|
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
Routes are used to generate the URLs of the website. We provide some of the
|
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
|
common routes: for the detail view of course, but also to select all posts or
|
||||||
|
@ -61,7 +60,6 @@ It is of course possible to create your own routes.
|
||||||
Routes are registered to a router (FIXME: it might be possible that we remove
|
Routes are registered to a router (FIXME: it might be possible that we remove
|
||||||
this later)
|
this later)
|
||||||
|
|
||||||
|
|
||||||
## Sections
|
## Sections
|
||||||
Sections are used to render part of a publication, for example to render a
|
Sections are used to render part of a publication, for example to render a
|
||||||
playlist related to the diffusion of a program.
|
playlist related to the diffusion of a program.
|
||||||
|
@ -75,7 +73,6 @@ 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,
|
sections and configure them. This reduce the work, keep design coherent,
|
||||||
and reduce the risk of bugs and so on.
|
and reduce the risk of bugs and so on.
|
||||||
|
|
||||||
|
|
||||||
## Website
|
## Website
|
||||||
This class is used to create the website itself and regroup all the needed
|
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
|
informations to make it beautiful. There are different steps to create the
|
||||||
|
@ -89,9 +86,52 @@ website, using instance of the Website class:
|
||||||
3. Register website's URLs to Django.
|
3. Register website's URLs to Django.
|
||||||
4. Change templates and css styles if needed.
|
4. Change templates and css styles if needed.
|
||||||
|
|
||||||
|
It also offers various facilities, such as comments view registering.
|
||||||
|
|
||||||
# Generated content
|
|
||||||
## CSS
|
# 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.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
A Section behave similar to a view with few differences:
|
||||||
|
* it renders its content to a string, using the function `render`;
|
||||||
|
* the method `as_view` return an instance of the section rather than a function, in order to keep possible to access section's data;
|
||||||
|
|
||||||
|
## Menus
|
||||||
|
`Menu` is a section containing others sections, and are used to render the website's menus. By default they are the ones of the parent website, but it is also possible to change the menus per view.
|
||||||
|
|
||||||
|
It is possible to render only the content of a view without any menu, by adding the parameter `embed` in the request's url. This has been done in order to allow XMLHttpRequests proper.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
Lists in `PostListView` and as a section in another view always uses the **list.html** template. It extends another template, that is configurable using `base_template` template argument; this has been used to render the list either in a section or as a page.
|
||||||
|
|
||||||
|
It is also possible to specify a list of fields that are rendered in the list, at the list initialisation or using request parameter `fields` (in this case it must be a subset of the list.fields).
|
||||||
|
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
|
||||||
|
These both define the following blocks, 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`;
|
||||||
|
* *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*)
|
||||||
|
|
||||||
|
|
||||||
|
# CSS classes
|
||||||
* **.meta**: metadata of any item (author, date, info, tags...)
|
* **.meta**: metadata of any item (author, date, info, tags...)
|
||||||
* **.info**: used to render extra information, usually in lists
|
* **.info**: used to render extra information, usually in lists
|
||||||
|
|
||||||
|
|
44
cms/admin.py
44
cms/admin.py
|
@ -1,8 +1,48 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
|
|
||||||
import aircox.cms.models as models
|
import aircox.cms.models as models
|
||||||
|
|
||||||
admin.site.register(models.Article)
|
|
||||||
admin.site.register(models.Comment)
|
|
||||||
|
class PostAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [ 'title', 'date', 'author', 'published', 'post_tags']
|
||||||
|
list_editable = [ 'published' ]
|
||||||
|
list_filter = ['date', 'author', 'published']
|
||||||
|
|
||||||
|
def post_tags(self, post):
|
||||||
|
tags = []
|
||||||
|
for tag in post.tags.all():
|
||||||
|
tags.append(str(tag))
|
||||||
|
return ', '.join(tags)
|
||||||
|
post_tags.short_description = _('tags')
|
||||||
|
|
||||||
|
def post_image(self, post):
|
||||||
|
if not post.image:
|
||||||
|
return
|
||||||
|
|
||||||
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
|
options = {'size': (48, 48), 'crop': True}
|
||||||
|
url = get_thumbnailer(post.image).get_thumbnail(options).url
|
||||||
|
return u'<img src="{url}">'.format(url=url)
|
||||||
|
post_image.short_description = _('image')
|
||||||
|
post_image.allow_tags = True
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedPostAdmin(PostAdmin):
|
||||||
|
list_display = ['title', 'date', 'published', 'post_tags', 'post_image' ]
|
||||||
|
|
||||||
|
|
||||||
|
class CommentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [ 'date', 'author', 'published', 'content_slice' ]
|
||||||
|
list_editable = [ 'published' ]
|
||||||
|
list_filter = ['date', 'author', 'published']
|
||||||
|
|
||||||
|
def content_slice(self, post):
|
||||||
|
return post.content[:256]
|
||||||
|
content_slice.short_description = _('content')
|
||||||
|
|
||||||
|
admin.site.register(models.Article, PostAdmin)
|
||||||
|
admin.site.register(models.Comment, CommentAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,44 @@ from honeypot.decorators import check_honeypot
|
||||||
from aircox.cms.forms import CommentForm
|
from aircox.cms.forms import CommentForm
|
||||||
|
|
||||||
|
|
||||||
class Section(View):
|
class Viewable:
|
||||||
|
"""
|
||||||
|
Describe a view that is still usable as a class after as_view() has
|
||||||
|
been called.
|
||||||
|
"""
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
def func(**kwargs_):
|
||||||
|
if kwargs_:
|
||||||
|
kwargs.update(kwargs_)
|
||||||
|
instance = cl(*args, **kwargs)
|
||||||
|
return instance
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class Sections(Viewable, list):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
for i, section in enumerate(self):
|
||||||
|
if callable(section) or type(section) == type:
|
||||||
|
self[i] = section()
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
return ''.join([
|
||||||
|
section.render(*args, **kwargs)
|
||||||
|
for section in self
|
||||||
|
])
|
||||||
|
|
||||||
|
def filter(self, predicate):
|
||||||
|
return [ section for section in self if predicate(section) ]
|
||||||
|
|
||||||
|
|
||||||
|
class Section(Viewable, View):
|
||||||
"""
|
"""
|
||||||
On the contrary to Django's views, we create an instance of the view
|
On the contrary to Django's views, we create an instance of the view
|
||||||
only once, when the server is run.
|
only once, when the server is run.
|
||||||
|
@ -57,19 +94,6 @@ class Section(View):
|
||||||
object = None
|
object = None
|
||||||
kwargs = None
|
kwargs = None
|
||||||
|
|
||||||
@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.
|
|
||||||
"""
|
|
||||||
def func(**kwargs_):
|
|
||||||
if kwargs_:
|
|
||||||
kwargs.update(kwargs_)
|
|
||||||
instance = cl(*args, **kwargs)
|
|
||||||
return instance
|
|
||||||
return func
|
|
||||||
|
|
||||||
def add_css_class(self, css_class):
|
def add_css_class(self, css_class):
|
||||||
if self.css_class:
|
if self.css_class:
|
||||||
if css_class not in self.css_class:
|
if css_class not in self.css_class:
|
||||||
|
@ -110,9 +134,9 @@ class Section(View):
|
||||||
'object': self.object,
|
'object': self.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, object=None, return_context=False, **kwargs):
|
def render(self, request, object=None, context_only=False, **kwargs):
|
||||||
context = self.get_context_data(request=request, object=object, **kwargs)
|
context = self.get_context_data(request=request, object=object, **kwargs)
|
||||||
if return_context:
|
if context_only:
|
||||||
return context
|
return context
|
||||||
if not context:
|
if not context:
|
||||||
return ''
|
return ''
|
||||||
|
@ -153,7 +177,6 @@ class Content(Section):
|
||||||
|
|
||||||
def get_content(self):
|
def get_content(self):
|
||||||
if self.content is None:
|
if self.content is None:
|
||||||
# FIXME: markdown?
|
|
||||||
content = getattr(self.object, self.rel_attr)
|
content = getattr(self.object, self.rel_attr)
|
||||||
content = escape(content)
|
content = escape(content)
|
||||||
content = re.sub(r'(^|\n\n)((\n?[^\n])+)', r'<p>\2</p>', content)
|
content = re.sub(r'(^|\n\n)((\n?[^\n])+)', r'<p>\2</p>', content)
|
||||||
|
@ -161,7 +184,8 @@ class Content(Section):
|
||||||
|
|
||||||
if self.rel_image_attr and hasattr(self.object, self.rel_image_attr):
|
if self.rel_image_attr and hasattr(self.object, self.rel_image_attr):
|
||||||
image = getattr(self.object, self.rel_image_attr)
|
image = getattr(self.object, self.rel_image_attr)
|
||||||
content = '<img src="{}">'.format(image.url) + content
|
if image:
|
||||||
|
content = '<img src="{}">'.format(image.url) + content
|
||||||
return content
|
return content
|
||||||
return str(self.content)
|
return str(self.content)
|
||||||
|
|
||||||
|
@ -330,12 +354,9 @@ class Comments(List):
|
||||||
messages.error(request, self.error_message, fail_silently=True)
|
messages.error(request, self.error_message, fail_silently=True)
|
||||||
self.comment_form = comment_form
|
self.comment_form = comment_form
|
||||||
|
|
||||||
|
|
||||||
class Menu(Section):
|
class Menu(Section):
|
||||||
template_name = 'aircox/cms/section.html'
|
|
||||||
tag = 'nav'
|
tag = 'nav'
|
||||||
classes = ''
|
|
||||||
attrs = ''
|
|
||||||
name = ''
|
|
||||||
position = '' # top, left, bottom, right, header, footer, page_top, page_bottom
|
position = '' # top, left, bottom, right, header, footer, page_top, page_bottom
|
||||||
sections = None
|
sections = None
|
||||||
|
|
||||||
|
@ -343,8 +364,8 @@ class Menu(Section):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.add_css_class('menu')
|
self.add_css_class('menu')
|
||||||
self.add_css_class('menu_' + str(self.name or self.position))
|
self.add_css_class('menu_' + str(self.name or self.position))
|
||||||
self.sections = [ section() if callable(section) else section
|
self.sections = Sections(self.sections)
|
||||||
for section in self.sections ]
|
|
||||||
if not self.attrs:
|
if not self.attrs:
|
||||||
self.attrs = {}
|
self.attrs = {}
|
||||||
|
|
||||||
|
@ -354,10 +375,7 @@ class Menu(Section):
|
||||||
'tag': self.tag,
|
'tag': self.tag,
|
||||||
'css_class': self.css_class,
|
'css_class': self.css_class,
|
||||||
'attrs': self.attrs,
|
'attrs': self.attrs,
|
||||||
'content': ''.join([
|
'content': self.sections.render(*args, **kwargs)
|
||||||
section.get(request=self.request, object=self.object)
|
|
||||||
for section in self.sections
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
{% if header %}
|
||||||
<header>
|
<header>
|
||||||
{{ header|safe }}
|
{{ header|safe }}
|
||||||
</header>
|
</header>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
{% if website.styles %}
|
{% if website.styles %}
|
||||||
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<title>{{ website.name }} {% if title %}- {{ title }} {% endif %}</title>
|
<title>{% if title %}{{ title }} - {% endif %}{{ website.name }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block page_header %}
|
{% block page_header %}
|
||||||
|
@ -65,6 +65,15 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{% if footer %}
|
||||||
|
<footer>
|
||||||
|
{{ footer|safe }}
|
||||||
|
</footer>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% if not embed %}
|
{% if not embed %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
@ -78,7 +87,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block footer %}
|
{% block page_footer %}
|
||||||
{% if menus.footer %}
|
{% if menus.footer %}
|
||||||
{{ menus.footer|safe }}
|
{{ menus.footer|safe }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
63
cms/views.py
63
cms/views.py
|
@ -10,13 +10,27 @@ import aircox.cms.sections as sections
|
||||||
|
|
||||||
|
|
||||||
class PostBaseView:
|
class PostBaseView:
|
||||||
website = None # corresponding website
|
"""
|
||||||
title = '' # title of the page
|
Base class for views.
|
||||||
embed = False # page is embed (if True, only post content is printed
|
|
||||||
|
# Request GET params:
|
||||||
|
* embed: view is embedded, render only the content of the view
|
||||||
|
"""
|
||||||
|
website = None
|
||||||
|
"""website that uses the view"""
|
||||||
|
menus = None
|
||||||
|
"""menus used to render the view page"""
|
||||||
|
title = ''
|
||||||
|
"""title of the page (used in <title> tags and page <h1>)"""
|
||||||
attrs = '' # attr for the HTML element of the content
|
attrs = '' # attr for the HTML element of the content
|
||||||
|
"""attributes to set in the HTML element containing the view"""
|
||||||
css_class = '' # css classes for the HTML element of the content
|
css_class = '' # css classes for the HTML element of the content
|
||||||
|
"""css classes used for the HTML element containing the view"""
|
||||||
|
|
||||||
def add_css_class(self, css_class):
|
def add_css_class(self, css_class):
|
||||||
|
"""
|
||||||
|
Add the given class to the current class list if not yet present.
|
||||||
|
"""
|
||||||
if self.css_class:
|
if self.css_class:
|
||||||
if css_class not in self.css_class:
|
if css_class not in self.css_class:
|
||||||
self.css_class += ' ' + css_class
|
self.css_class += ' ' + css_class
|
||||||
|
@ -29,17 +43,21 @@ class PostBaseView:
|
||||||
to self.
|
to self.
|
||||||
"""
|
"""
|
||||||
context = {
|
context = {
|
||||||
k: getattr(self, k)
|
key: getattr(self, key)
|
||||||
for k, v in PostBaseView.__dict__.items()
|
for key in PostBaseView.__dict__.keys()
|
||||||
if not k.startswith('__')
|
if not key.startswith('__')
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.embed:
|
if 'embed' not in self.request.GET:
|
||||||
object = self.object if hasattr(self, 'object') else None
|
object = self.object if hasattr(self, 'object') else None
|
||||||
context['menus'] = {
|
if self.menus:
|
||||||
k: v.get(self.request, object = object, **kwargs)
|
context['menus'] = {
|
||||||
for k, v in self.website.menus.items()
|
k: v.render(self.request, object = object, **kwargs)
|
||||||
}
|
for k, v in self.menus.items()
|
||||||
|
}
|
||||||
|
context['embed'] = False
|
||||||
|
else:
|
||||||
|
context['embed'] = True
|
||||||
context['view'] = self
|
context['view'] = self
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -56,7 +74,6 @@ class PostListView(PostBaseView, ListView):
|
||||||
route.get_queryset or self.model.objects.all()
|
route.get_queryset or self.model.objects.all()
|
||||||
|
|
||||||
Request.GET params:
|
Request.GET params:
|
||||||
* embed: view is embedded, render only the list
|
|
||||||
* exclude: exclude item of the given id
|
* exclude: exclude item of the given id
|
||||||
* order: 'desc' or 'asc'
|
* order: 'desc' or 'asc'
|
||||||
* page: page number
|
* page: page number
|
||||||
|
@ -67,16 +84,16 @@ class PostListView(PostBaseView, ListView):
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
route = None
|
route = None
|
||||||
|
"""route used to render this list"""
|
||||||
list = None
|
list = None
|
||||||
css_class = None
|
"""list section to use to render the list and get base context.
|
||||||
|
By default it is sections.List"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.route = self.kwargs.get('route') or self.route
|
self.route = self.kwargs.get('route') or self.route
|
||||||
if request.GET.get('embed'):
|
|
||||||
self.embed = True
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -140,9 +157,6 @@ class PostListView(PostBaseView, ListView):
|
||||||
class PostDetailView(DetailView, PostBaseView):
|
class PostDetailView(DetailView, PostBaseView):
|
||||||
"""
|
"""
|
||||||
Detail view for posts and children
|
Detail view for posts and children
|
||||||
|
|
||||||
Request.GET params:
|
|
||||||
* embed: view is embedded, only render the content
|
|
||||||
"""
|
"""
|
||||||
template_name = 'aircox/cms/detail.html'
|
template_name = 'aircox/cms/detail.html'
|
||||||
|
|
||||||
|
@ -155,8 +169,6 @@ class PostDetailView(DetailView, PostBaseView):
|
||||||
self.sections = [ section() for section in (sections or []) ]
|
self.sections = [ section() for section in (sections or []) ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.GET.get('embed'):
|
|
||||||
self.embed = True
|
|
||||||
if self.model:
|
if self.model:
|
||||||
return super().get_queryset().filter(published = True)
|
return super().get_queryset().filter(published = True)
|
||||||
return []
|
return []
|
||||||
|
@ -174,9 +186,9 @@ class PostDetailView(DetailView, PostBaseView):
|
||||||
|
|
||||||
kwargs['object'] = self.object
|
kwargs['object'] = self.object
|
||||||
context.update({
|
context.update({
|
||||||
'title': self.object.title,
|
'title': self.title or self.object.title,
|
||||||
'content': ''.join([
|
'content': ''.join([
|
||||||
section.get(request = self.request, **kwargs)
|
section.render(request = self.request, **kwargs)
|
||||||
for section in self.sections
|
for section in self.sections
|
||||||
]),
|
]),
|
||||||
'css_class': self.css_class,
|
'css_class': self.css_class,
|
||||||
|
@ -208,17 +220,14 @@ class PageView(TemplateView, PostBaseView):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.sections = sections.Sections(self.sections)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context.update(self.get_base_context())
|
context.update(self.get_base_context())
|
||||||
|
|
||||||
context.update({
|
context.update({
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'content': ''.join([
|
'content': self.sections.render(request=self.request,**kwargs)
|
||||||
section.get(request = self.request, **kwargs)
|
|
||||||
for section in self.sections
|
|
||||||
]),
|
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -3,29 +3,39 @@ from django.conf.urls import url
|
||||||
|
|
||||||
import aircox.cms.routes as routes
|
import aircox.cms.routes as routes
|
||||||
import aircox.cms.views as views
|
import aircox.cms.views as views
|
||||||
|
import aircox.cms.models as models
|
||||||
|
import aircox.cms.sections as sections
|
||||||
|
|
||||||
|
|
||||||
class Website:
|
class Website:
|
||||||
"""
|
"""
|
||||||
Describe a website and all its settings that are used for its rendering.
|
Describe a website and all its settings that are used for its rendering.
|
||||||
"""
|
"""
|
||||||
# metadata
|
## metadata
|
||||||
name = ''
|
name = ''
|
||||||
domain = ''
|
domain = ''
|
||||||
description = 'An aircox website'
|
description = 'An aircox website'
|
||||||
tags = 'aircox,radio,music'
|
tags = 'aircox,radio,music'
|
||||||
|
|
||||||
# rendering
|
## rendering
|
||||||
styles = ''
|
styles = ''
|
||||||
|
"""extra css style file"""
|
||||||
menus = None
|
menus = None
|
||||||
|
"""dict of default menus used to render website pages"""
|
||||||
|
|
||||||
# user interaction
|
## user interaction
|
||||||
allow_comments = True
|
allow_comments = True
|
||||||
|
"""allow comments on the website"""
|
||||||
auto_publish_comments = False
|
auto_publish_comments = False
|
||||||
|
"""publish comment without human approval"""
|
||||||
comments_routes = True
|
comments_routes = True
|
||||||
|
"""register list routes for the Comment model"""
|
||||||
|
|
||||||
# components
|
## components
|
||||||
urls = []
|
urls = []
|
||||||
|
"""list of urls generated thourgh registrations"""
|
||||||
registry = {}
|
registry = {}
|
||||||
|
"""dict of registered models by their name"""
|
||||||
|
|
||||||
def __init__(self, menus = None, **kwargs):
|
def __init__(self, menus = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -45,18 +55,18 @@ class Website:
|
||||||
|
|
||||||
|
|
||||||
def name_of_model(self, model):
|
def name_of_model(self, model):
|
||||||
|
"""
|
||||||
|
Return the registered name for a given model if found.
|
||||||
|
"""
|
||||||
for name, _model in self.registry.items():
|
for name, _model in self.registry.items():
|
||||||
if model is _model:
|
if model is _model:
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def register_comments_routes(self):
|
def register_comments_routes(self):
|
||||||
"""
|
"""
|
||||||
Register routes for comments, for the moment, only:
|
Register routes for comments, for the moment, only
|
||||||
* ThreadRoute
|
ThreadRoute
|
||||||
"""
|
"""
|
||||||
import aircox.cms.models as models
|
|
||||||
import aircox.cms.sections as sections
|
|
||||||
|
|
||||||
self.register_list(
|
self.register_list(
|
||||||
'comment', models.Comment,
|
'comment', models.Comment,
|
||||||
routes = [routes.ThreadRoute],
|
routes = [routes.ThreadRoute],
|
||||||
|
@ -72,7 +82,9 @@ class Website:
|
||||||
Register a model and return the name under which it is registered.
|
Register a model and return the name under which it is registered.
|
||||||
Raise a ValueError if another model is yet associated under this name.
|
Raise a ValueError if another model is yet associated under this name.
|
||||||
"""
|
"""
|
||||||
if name in self.registry and self.registry[name] is not model:
|
if name in self.registry:
|
||||||
|
if self.registry[name] is model:
|
||||||
|
return name
|
||||||
raise ValueError('A model has yet been registered under "{}"'
|
raise ValueError('A model has yet been registered under "{}"'
|
||||||
.format(name))
|
.format(name))
|
||||||
self.registry[name] = model
|
self.registry[name] = model
|
||||||
|
@ -85,11 +97,15 @@ class Website:
|
||||||
Register a model and the detail view
|
Register a model and the detail view
|
||||||
"""
|
"""
|
||||||
name = self.register_model(name, model)
|
name = self.register_model(name, model)
|
||||||
|
if not view_kwargs.get('menus'):
|
||||||
|
view_kwargs['menus'] = self.menus
|
||||||
|
|
||||||
view = view.as_view(
|
view = view.as_view(
|
||||||
website = self,
|
website = self,
|
||||||
model = model,
|
model = model,
|
||||||
**view_kwargs
|
**view_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
self.urls.append(routes.DetailRoute.as_url(name, view))
|
self.urls.append(routes.DetailRoute.as_url(name, view))
|
||||||
self.registry[name] = model
|
self.registry[name] = model
|
||||||
|
|
||||||
|
@ -99,11 +115,15 @@ class Website:
|
||||||
Register a model and the given list view using the given routes
|
Register a model and the given list view using the given routes
|
||||||
"""
|
"""
|
||||||
name = self.register_model(name, model)
|
name = self.register_model(name, model)
|
||||||
|
if not 'menus' in view_kwargs:
|
||||||
|
view_kwargs['menus'] = self.menus
|
||||||
|
|
||||||
view = view.as_view(
|
view = view.as_view(
|
||||||
website = self,
|
website = self,
|
||||||
model = model,
|
model = model,
|
||||||
**view_kwargs
|
**view_kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
self.urls += [ route.as_url(name, view) for route in routes ]
|
self.urls += [ route.as_url(name, view) for route in routes ]
|
||||||
self.registry[name] = model
|
self.registry[name] = model
|
||||||
|
|
||||||
|
@ -113,6 +133,9 @@ class Website:
|
||||||
Register a page that is accessible to the given path. If path is None,
|
Register a page that is accessible to the given path. If path is None,
|
||||||
use a slug of the name.
|
use a slug of the name.
|
||||||
"""
|
"""
|
||||||
|
if not 'menus' in view_kwargs:
|
||||||
|
view_kwargs['menus'] = self.menus
|
||||||
|
|
||||||
view = view.as_view(
|
view = view.as_view(
|
||||||
website = self,
|
website = self,
|
||||||
**view_kwargs
|
**view_kwargs
|
||||||
|
|
|
@ -1,8 +1,25 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from suit.admin import SortableTabularInline, SortableModelAdmin
|
||||||
|
|
||||||
|
import aircox.programs.models as programs
|
||||||
|
import aircox.cms.admin as cms
|
||||||
import aircox.website.models as models
|
import aircox.website.models as models
|
||||||
|
import aircox.website.forms as forms
|
||||||
admin.site.register(models.Program)
|
|
||||||
admin.site.register(models.Diffusion)
|
|
||||||
|
class TrackInline (SortableTabularInline):
|
||||||
|
fields = ['artist', 'name', 'tags', 'position']
|
||||||
|
form = forms.TrackForm
|
||||||
|
model = programs.Track
|
||||||
|
sortable = 'position'
|
||||||
|
extra = 10
|
||||||
|
|
||||||
|
|
||||||
|
class DiffusionPostAdmin(cms.RelatedPostAdmin):
|
||||||
|
inlines = [TrackInline]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(models.Program, cms.RelatedPostAdmin)
|
||||||
|
admin.site.register(models.Diffusion, DiffusionPostAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
|
40
website/autocomplete_light_registry.py
Normal file
40
website/autocomplete_light_registry.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import autocomplete_light.shortcuts as al
|
||||||
|
import aircox.programs.models as programs
|
||||||
|
|
||||||
|
from taggit.models import Tag
|
||||||
|
al.register(Tag)
|
||||||
|
|
||||||
|
|
||||||
|
class OneFieldAutocomplete(al.AutocompleteModelBase):
|
||||||
|
choice_html_format = u'''
|
||||||
|
<span class="block" data-value="%s">%s</span>
|
||||||
|
'''
|
||||||
|
|
||||||
|
def choice_html (self, choice):
|
||||||
|
value = choice[self.search_fields[0]]
|
||||||
|
return self.choice_html_format % (self.choice_label(choice),
|
||||||
|
self.choice_label(value))
|
||||||
|
|
||||||
|
|
||||||
|
def choices_for_request(self):
|
||||||
|
#if not self.request.user.is_staff:
|
||||||
|
# self.choices = self.choices.filter(private=False)
|
||||||
|
filter_args = { self.search_fields[0] + '__icontains': self.request.GET['q'] }
|
||||||
|
|
||||||
|
self.choices = self.choices.filter(**filter_args)
|
||||||
|
self.choices = self.choices.values(self.search_fields[0]).distinct()
|
||||||
|
return self.choices
|
||||||
|
|
||||||
|
|
||||||
|
class TrackArtistAutocomplete(OneFieldAutocomplete):
|
||||||
|
search_fields = ['artist']
|
||||||
|
model = programs.Track
|
||||||
|
al.register(TrackArtistAutocomplete)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackNameAutocomplete(OneFieldAutocomplete):
|
||||||
|
search_fields = ['name']
|
||||||
|
model = programs.Track
|
||||||
|
al.register(TrackNameAutocomplete)
|
||||||
|
|
||||||
|
|
19
website/forms.py
Normal file
19
website/forms.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
import autocomplete_light.shortcuts as al
|
||||||
|
from autocomplete_light.contrib.taggit_field import TaggitWidget
|
||||||
|
|
||||||
|
import aircox.programs.models as programs
|
||||||
|
|
||||||
|
|
||||||
|
class TrackForm (forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = programs.Track
|
||||||
|
fields = ['artist', 'name', 'tags', 'position']
|
||||||
|
widgets = {
|
||||||
|
'artist': al.TextWidget('TrackArtistAutocomplete'),
|
||||||
|
'name': al.TextWidget('TrackNameAutocomplete'),
|
||||||
|
'tags': TaggitWidget('TagAutocomplete'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Diffusion (RelatedPost):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.thread:
|
if self.thread:
|
||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = _('{name} on {first_diff}').format(
|
self.title = _('{name} // {first_diff}').format(
|
||||||
self.related.program.name,
|
self.related.program.name,
|
||||||
self.related.start.strftime('%A %d %B')
|
self.related.start.strftime('%A %d %B')
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user