work on comments, clean up a bit
This commit is contained in:
parent
f8f3beb124
commit
a989e53da4
|
@ -23,7 +23,8 @@ Later we would provide a package, but now we have other priorities.
|
|||
### settings.py
|
||||
* There must be `BASE_DIR` or `PROJECT_ROOT` defined in order to make liquidsoap working (that must call manage.py using an absolute path).
|
||||
* INSTALLED_APPS:
|
||||
- dependencies: `'taggit'`, `'easy_thumbnails'`
|
||||
- dependencies: `'taggit'` (*programs* and *cms* applications),
|
||||
`'easy_thumbnails'` (*cms*), `'honeypot'` (*cms*)
|
||||
- optional dependencies (in order to make users' life easier): `'autocomplete_light'`, `'suit'`
|
||||
- aircox: `'aircox.programs'`, `'aircox.liquidsoap'`, `'aircox.cms'`
|
||||
|
||||
|
|
148
cms/models.py
148
cms/models.py
|
@ -7,7 +7,8 @@ from django.utils.text import slugify
|
|||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django.db.models.signals import post_init, post_save, post_delete
|
||||
|
||||
from django.db.models.signals import Signal, post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
|
@ -43,6 +44,43 @@ class ProxyPost:
|
|||
self.detail_url = thread.detail_url()
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
thread_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.SET_NULL,
|
||||
blank = True, null = True
|
||||
)
|
||||
thread_id = models.PositiveIntegerField(
|
||||
blank = True, null = True
|
||||
)
|
||||
thread = GenericForeignKey('thread_type', 'thread_id')
|
||||
|
||||
published = models.BooleanField(
|
||||
verbose_name = _('public'),
|
||||
default = False
|
||||
)
|
||||
|
||||
author = models.CharField(
|
||||
verbose_name = _('author'),
|
||||
max_length = 32,
|
||||
)
|
||||
email = models.EmailField(
|
||||
verbose_name = _('email'),
|
||||
blank = True, null = True,
|
||||
)
|
||||
url = models.URLField(
|
||||
verbose_name = _('website'),
|
||||
blank = True, null = True,
|
||||
)
|
||||
date = models.DateTimeField(
|
||||
_('date'),
|
||||
default = timezone.datetime.now
|
||||
)
|
||||
content = models.TextField (
|
||||
_('comment'),
|
||||
)
|
||||
|
||||
|
||||
class Post (models.Model):
|
||||
"""
|
||||
Base model that can be used as is if wanted. Represent a generic
|
||||
|
@ -84,7 +122,7 @@ class Post (models.Model):
|
|||
)
|
||||
content = models.TextField (
|
||||
_('description'),
|
||||
blank = True, null = True
|
||||
default = '',
|
||||
)
|
||||
image = models.ImageField(
|
||||
blank = True, null = True
|
||||
|
@ -96,7 +134,7 @@ class Post (models.Model):
|
|||
|
||||
search_fields = [ 'title', 'content' ]
|
||||
|
||||
def get_proxy(self):
|
||||
def as_proxy(self):
|
||||
"""
|
||||
Return a ProxyPost instance using this post
|
||||
"""
|
||||
|
@ -111,14 +149,36 @@ class Post (models.Model):
|
|||
if not queryset:
|
||||
queryset = cl.objects
|
||||
thread_type = ContentType.objects.get_for_model(thread)
|
||||
qs = queryset.filter(thread_id = thread.pk,
|
||||
thread_type__pk = thread_type.id)
|
||||
qs = queryset.filter(
|
||||
thread_id = thread.pk,
|
||||
thread_type__pk = thread_type.id
|
||||
)
|
||||
return qs
|
||||
|
||||
def get_comments(self):
|
||||
"""
|
||||
Return comments pointing to this post
|
||||
"""
|
||||
type = ContentType.objects.get_for_model(self)
|
||||
qs = Comment.objects.filter(
|
||||
thread_id = self.pk,
|
||||
thread_type__pk = type.pk
|
||||
)
|
||||
return qs
|
||||
|
||||
def detail_url(self):
|
||||
return self.route_url(routes.DetailRoute,
|
||||
{ 'pk': self.pk, 'slug': slugify(self.title) })
|
||||
|
||||
|
||||
def get_object_list(self, request, object, **kwargs):
|
||||
type = ContentType.objects.get_for_model(object)
|
||||
qs = Comment.objects.filter(
|
||||
thread_id = object.pk,
|
||||
thread_type__pk = type.pk
|
||||
)
|
||||
return qs
|
||||
|
||||
@classmethod
|
||||
def route_url(cl, route, kwargs = None):
|
||||
name = cl._website.name_of_model(cl)
|
||||
|
@ -198,6 +258,24 @@ class RelatedPostBase (models.base.ModelBase):
|
|||
|
||||
return rel
|
||||
|
||||
@classmethod
|
||||
def make_auto_create(cl, model):
|
||||
if not model._relation.rel_to_post:
|
||||
return
|
||||
|
||||
def handler(sender, instance, created, *args, **kwargs):
|
||||
rel = model._relation
|
||||
post = model.objects.filter(related = instance)
|
||||
if post.count():
|
||||
post = post[0]
|
||||
elif rel.auto_create:
|
||||
post = model(related = instance)
|
||||
else:
|
||||
return
|
||||
post.rel_to_post()
|
||||
post.save(avoid_sync = True)
|
||||
post_save.connect(handler, model._relation.model, False)
|
||||
|
||||
def __new__ (cl, name, bases, attrs):
|
||||
# TODO: allow proxy models and better inheritance
|
||||
# TODO: check bindings
|
||||
|
@ -213,9 +291,11 @@ class RelatedPostBase (models.base.ModelBase):
|
|||
}.items() if not attrs.get(x) })
|
||||
|
||||
model = super().__new__(cl, name, bases, attrs)
|
||||
print(model, model.related)
|
||||
cl.register(rel.model, model)
|
||||
|
||||
# auto create and/or update
|
||||
cl.make_auto_create(model)
|
||||
|
||||
# name clashes
|
||||
name = rel.model._meta.object_name
|
||||
if name == model._meta.object_name:
|
||||
|
@ -275,6 +355,9 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
|||
model generated for the bindings.thread object.
|
||||
* field_args: dict of arguments to pass to the ForeignKey constructor,
|
||||
such as: ForeignKey(related_model, **field_args)
|
||||
* auto_create: automatically create a RelatedPost for each new item of
|
||||
the related object and init it with bounded values. Use signals
|
||||
'', ''.
|
||||
|
||||
Be careful with post_to_rel!
|
||||
* There is no check of permissions when related object is synchronised
|
||||
|
@ -288,6 +371,7 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
|||
rel_to_post = False
|
||||
thread_model = None
|
||||
field_args = None
|
||||
auto_create = False
|
||||
|
||||
def get_rel_attr(self, attr):
|
||||
attr = self._relation.bindings.get(attr)
|
||||
|
@ -306,7 +390,7 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
|||
Note: does not check if Relation.post_to_rel is True
|
||||
"""
|
||||
rel = self._relation
|
||||
if not rel.bindings:
|
||||
if not self.related or not rel.bindings:
|
||||
return
|
||||
|
||||
for attr, rel_attr in rel.bindings.items():
|
||||
|
@ -331,7 +415,7 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
|||
Note: does not check if Relation.post_to_rel is True
|
||||
"""
|
||||
rel = self._relation
|
||||
if not rel.bindings:
|
||||
if not self.related or not rel.bindings:
|
||||
return
|
||||
|
||||
has_changed = False
|
||||
|
@ -362,44 +446,16 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
|||
# we use this method for sync, in order to avoid intrusive code on other
|
||||
# applications, e.g. using signals.
|
||||
if self.pk and self._relation.rel_to_post:
|
||||
self.rel_to_post(save = False)
|
||||
self.rel_to_post(False)
|
||||
|
||||
def save (self, *args, **kwargs):
|
||||
# TODO handle when related change
|
||||
if not self.title and self.related:
|
||||
self.title = self.get_rel_attr('title')
|
||||
if self._relation.post_to_rel:
|
||||
self.post_to_rel(save = True)
|
||||
def save (self, avoid_sync = False, *args, **kwargs):
|
||||
"""
|
||||
If avoid_relation, do not synchronise the post/related object.
|
||||
"""
|
||||
if not avoid_sync:
|
||||
if not self.pk and self._relation.rel_to_post:
|
||||
self.rel_to_post(False)
|
||||
if self._relation.post_to_rel:
|
||||
self.post_to_rel(True)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
thread_type = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.SET_NULL,
|
||||
blank = True, null = True
|
||||
)
|
||||
thread_id = models.PositiveIntegerField(
|
||||
blank = True, null = True
|
||||
)
|
||||
thread = GenericForeignKey('thread_type', 'thread_id')
|
||||
|
||||
author = models.TextField(
|
||||
verbose_name = _('author'),
|
||||
blank = True, null = True,
|
||||
)
|
||||
email = models.EmailField(
|
||||
verbose_name = _('email'),
|
||||
blank = True, null = True,
|
||||
)
|
||||
date = models.DateTimeField(
|
||||
_('date'),
|
||||
default = timezone.datetime.now
|
||||
)
|
||||
content = models.TextField (
|
||||
_('description'),
|
||||
blank = True, null = True
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/** main layout **/
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -25,6 +26,7 @@ body {
|
|||
}
|
||||
|
||||
|
||||
/** detail and list content **/
|
||||
main .section {
|
||||
/* width: calc(50% - 2em);
|
||||
display: inline-block; */
|
||||
|
@ -49,3 +51,36 @@ main .section {
|
|||
}
|
||||
|
||||
|
||||
/** comments **/
|
||||
.comment-form label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-form input:not([type=checkbox]),
|
||||
.comment-form textarea {
|
||||
display: inline-block;
|
||||
width: calc(100% - 5em);
|
||||
max-height: 6em;
|
||||
margin: 0.2em 0em;
|
||||
}
|
||||
|
||||
.comment-form input[type=checkbox],
|
||||
.comment-form button[type=submit] {
|
||||
max-width: 4em;
|
||||
vertical-align:bottom;
|
||||
margin: 0.2em 0em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comment-form .extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-form input[type="checkbox"]:checked + .extra {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -29,6 +29,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{{ content|safe }}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -10,21 +10,21 @@
|
|||
{% endif %}
|
||||
|
||||
{% if header %}
|
||||
<header class="section_header">
|
||||
<header>
|
||||
{% block section_header %}
|
||||
{{ header }}
|
||||
{% endblock %}
|
||||
</header>
|
||||
{% endif %}
|
||||
|
||||
<div class="section_content">
|
||||
<div class="content">
|
||||
{% block section_content %}
|
||||
{{ content|safe }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if footer %}
|
||||
<footer class="section_footer">
|
||||
<footer>
|
||||
{% block section_footer %}
|
||||
{{ footer }}
|
||||
{% endblock %}
|
||||
|
|
49
cms/templates/aircox/cms/section_comments.html
Normal file
49
cms/templates/aircox/cms/section_comments.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends "aircox/cms/section.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load honeypot %}
|
||||
|
||||
{% block section_content %}
|
||||
{{ form.non_field_errors }}
|
||||
<form action="" method="POST" class="comment-form">
|
||||
{% csrf_token %}
|
||||
{% render_honeypot_field "hp_website" %}
|
||||
<div>
|
||||
{{ form.author.errors }}
|
||||
{{ form.author }}
|
||||
<input type="checkbox" value="1">
|
||||
<div class="extra">
|
||||
{{ form.email.errors }}
|
||||
{{ form.email }}
|
||||
{{ form.url.errors }}
|
||||
{{ form.url }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ form.content.errors }}
|
||||
{{ form.content }}
|
||||
<button type="submit">{% trans "Post!" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<ul style="padding:0; margin:0">
|
||||
{% for item in object_list %}
|
||||
<li id="comment-{{ item.id }}" class="{{item.css}}">
|
||||
{{ item.content }}
|
||||
|
||||
<div class="info">
|
||||
<a href="{% if item.url %}{{ item.url }}{% else %}#{% endif %}">{{ item.author }}</a>
|
||||
|
||||
<time datetime="{{ item.date }}">
|
||||
{{ item.date|date:'l d F Y' }},
|
||||
{{ item.date|time:'H\hi' }}
|
||||
</time>
|
||||
|
||||
<a href="#comment-{{ item.id }}">#{{ item.id }}</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% load thumbnail %}
|
||||
|
||||
% block section_content %}
|
||||
{% block section_content %}
|
||||
<ul style="padding:0; margin:0">
|
||||
{% for item in object_list %}
|
||||
<li class="{{item.css}}">
|
||||
|
|
86
cms/views.py
86
cms/views.py
|
@ -3,7 +3,9 @@ from django.template.loader import render_to_string
|
|||
from django.views.generic import ListView, DetailView
|
||||
from django.views.generic.base import View
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.http import Http404
|
||||
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
class PostBaseView:
|
||||
website = None # corresponding website
|
||||
|
@ -24,7 +26,7 @@ class PostBaseView:
|
|||
|
||||
if not self.embed:
|
||||
context['menus'] = {
|
||||
k: v.get(self.request, website = self.website, **kwargs)
|
||||
k: v.get(self.request, object = self.object, **kwargs)
|
||||
for k, v in {
|
||||
k: self.website.get_menu(k)
|
||||
for k in self.website.menu_layouts
|
||||
|
@ -48,7 +50,7 @@ class PostListView(PostBaseView, ListView):
|
|||
"""
|
||||
template_name = 'aircox/cms/list.html'
|
||||
allow_empty = True
|
||||
paginate_by = 50
|
||||
paginate_by = 25
|
||||
model = None
|
||||
|
||||
route = None
|
||||
|
@ -111,6 +113,9 @@ class PostListView(PostBaseView, ListView):
|
|||
return ''
|
||||
|
||||
|
||||
from honeypot.decorators import verify_honeypot_value
|
||||
from aircox.cms.forms import CommentForm
|
||||
|
||||
class PostDetailView(DetailView, PostBaseView):
|
||||
"""
|
||||
Detail view for posts and children
|
||||
|
@ -124,7 +129,7 @@ class PostDetailView(DetailView, PostBaseView):
|
|||
|
||||
def __init__(self, sections = None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.sections = Sections(sections = sections)
|
||||
self.sections = sections or []
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.GET.get('embed'):
|
||||
|
@ -143,44 +148,42 @@ class PostDetailView(DetailView, PostBaseView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(self.get_base_context())
|
||||
context['content'] = self.sections.get(request, object = self.object,
|
||||
**kwargs)
|
||||
|
||||
kwargs['object'] = self.object
|
||||
context['content'] = ''.join([
|
||||
section.get(request = self.request, **kwargs)
|
||||
for section in self.sections
|
||||
])
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle new comments
|
||||
"""
|
||||
self.object = self.get_object()
|
||||
if not self.object:
|
||||
raise Http404()
|
||||
|
||||
class Sections(View):
|
||||
comment_form = CommentForm(request.POST)
|
||||
if not comment_form.is_valid() or verify_honeypot_value(request, 'hp_website'):
|
||||
raise Http404()
|
||||
comment = comment_form.save(commit=False)
|
||||
comment.thread = self.object
|
||||
comment.save()
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Menu(View):
|
||||
template_name = 'aircox/cms/content_object.html'
|
||||
tag = 'div'
|
||||
tag = 'nav'
|
||||
classes = ''
|
||||
attrs = ''
|
||||
sections = None
|
||||
|
||||
def __init__ (self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.classes += ' sections'
|
||||
|
||||
def get_context_data(self, request, object = None, **kwargs):
|
||||
return {
|
||||
'tag': self.tag,
|
||||
'classes': self.classes,
|
||||
'attrs': self.attrs,
|
||||
'content': ''.join([
|
||||
section.get(request, object = object, **kwargs)
|
||||
for section in self.sections or []
|
||||
])
|
||||
}
|
||||
|
||||
def get(self, request, object = None, **kwargs):
|
||||
self.request = request
|
||||
context = self.get_context_data(request, object, **kwargs)
|
||||
return render_to_string(self.template_name, context)
|
||||
|
||||
|
||||
class Menu(Sections):
|
||||
name = ''
|
||||
tag = 'nav'
|
||||
enabled = True
|
||||
position = '' # top, left, bottom, right, header, footer, page_top, page_bottom
|
||||
sections = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -191,4 +194,23 @@ class Menu(Sections):
|
|||
self.attrs['name'] = self.name
|
||||
self.attrs['id'] = self.name
|
||||
|
||||
def get_context_data(self, request, object = None, **kwargs):
|
||||
kwargs['object'] = object
|
||||
|
||||
return {
|
||||
'tag': self.tag,
|
||||
'classes': self.classes,
|
||||
'attrs': self.attrs,
|
||||
'content': ''.join([
|
||||
section.get(request, **kwargs)
|
||||
for section in self.sections
|
||||
])
|
||||
}
|
||||
|
||||
def get(self, request, object = None, **kwargs):
|
||||
self.request = request
|
||||
context = self.get_context_data(request, object, **kwargs)
|
||||
return render_to_string(self.template_name, context)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class Sound(Nameable):
|
|||
|
||||
type = models.SmallIntegerField(
|
||||
verbose_name = _('type'),
|
||||
choices = [ (y, _(x)) for x,y in Type.__members__.items() ],
|
||||
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
|
||||
blank = True, null = True
|
||||
)
|
||||
path = models.FilePathField(
|
||||
|
@ -571,7 +571,7 @@ class Diffusion(models.Model):
|
|||
# specific
|
||||
type = models.SmallIntegerField(
|
||||
verbose_name = _('type'),
|
||||
choices = [ (y, _(x)) for x,y in Type.__members__.items() ],
|
||||
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
|
||||
)
|
||||
initial = models.ForeignKey (
|
||||
'self',
|
||||
|
|
Loading…
Reference in New Issue
Block a user