work on comments, clean up a bit

This commit is contained in:
bkfox 2016-05-26 00:51:23 +02:00
parent f8f3beb124
commit a989e53da4
9 changed files with 249 additions and 86 deletions

View File

@ -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'`

View File

@ -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
)

View File

@ -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;
}

View File

@ -29,6 +29,6 @@
{% endblock %}
{% block content %}
{{ content }}
{{ content|safe }}
{% endblock %}

View File

@ -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 %}

View 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 %}

View File

@ -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}}">

View File

@ -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)

View File

@ -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',