Merge branch 'wip' into 'master'

Forum improvements

See merge request !75
This commit is contained in:
Skia 2017-06-01 13:31:35 +02:00
commit 38622c98e9
28 changed files with 1016 additions and 403 deletions

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('club', '0007_auto_20170324_0917'),
]
operations = [
migrations.AlterField(
model_name='club',
name='id',
field=models.AutoField(primary_key=True, serialize=False, db_index=True),
),
]

View File

@ -39,6 +39,7 @@ class Club(models.Model):
""" """
The Club class, made as a tree to allow nice tidy organization The Club class, made as a tree to allow nice tidy organization
""" """
id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_('name'), max_length=64) name = models.CharField(_('name'), max_length=64)
parent = models.ForeignKey('Club', related_name='children', null=True, blank=True) parent = models.ForeignKey('Club', related_name='children', null=True, blank=True)
unix_name = models.CharField(_('unix name'), max_length=30, unique=True, unix_name = models.CharField(_('unix name'), max_length=30, unique=True,
@ -151,11 +152,21 @@ class Club(models.Model):
return False return False
return sub.is_subscribed return sub.is_subscribed
_memberships = {}
def get_membership_for(self, user): def get_membership_for(self, user):
""" """
Returns the current membership the given user Returns the current membership the given user
""" """
return self.members.filter(user=user.id).filter(end_date=None).first() try:
return Club._memberships[self.id][user.id]
except:
m = self.members.filter(user=user.id).filter(end_date=None).first()
try:
Club._memberships[self.id][user.id] = m
except:
Club._memberships[self.id] = {}
Club._memberships[self.id][user.id] = m
return m
class Membership(models.Model): class Membership(models.Model):
""" """

View File

@ -22,3 +22,4 @@
# #
# #
default_app_config = 'core.apps.SithConfig'

57
core/apps.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding:utf-8 -*
#
# Copyright 2017
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.apps import AppConfig
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, m2m_changed
class SithConfig(AppConfig):
name = 'core'
verbose_name = "Core app of the Sith"
def ready(self):
from core.models import User, Group
from club.models import Club, Membership
from forum.models import Forum
def clear_cached_groups(sender, **kwargs):
if kwargs['model'] == Group:
User._group_ids = {}
User._group_name = {}
def clear_cached_memberships(sender, **kwargs):
User._club_memberships = {}
Club._memberships = {}
Forum._club_memberships = {}
print("Connecting signals!")
m2m_changed.connect(clear_cached_groups, weak=False, dispatch_uid="clear_cached_groups")
post_save.connect(clear_cached_memberships, weak=False, sender=Membership, # Membership is cached
dispatch_uid="clear_cached_memberships_membership")
post_save.connect(clear_cached_memberships, weak=False, sender=Club, # Club has a cache of Membership
dispatch_uid="clear_cached_memberships_club")
post_save.connect(clear_cached_memberships, weak=False, sender=Forum, # Forum has a cache of Membership
dispatch_uid="clear_cached_memberships_forum")
# TODO: there may be a need to add more cache clearing

View File

@ -223,14 +223,25 @@ class User(AbstractBaseUser):
s = self.subscriptions.last() s = self.subscriptions.last()
return s.is_valid_now() if s is not None else False return s.is_valid_now() if s is not None else False
_club_memberships = {}
_group_names = {}
_group_ids = {}
def is_in_group(self, group_name): def is_in_group(self, group_name):
"""If the user is in the group passed in argument (as string or by id)""" """If the user is in the group passed in argument (as string or by id)"""
group_id = 0 group_id = 0
g = None g = None
if isinstance(group_name, int): # Handle the case where group_name is an ID if isinstance(group_name, int): # Handle the case where group_name is an ID
g = Group.objects.filter(id=group_name).first() if group_name in User._group_ids.keys():
g = User._group_ids[group_name]
else:
g = Group.objects.filter(id=group_name).first()
User._group_ids[group_name] = g
else: else:
g = Group.objects.filter(name=group_name).first() if group_name in User._group_names.keys():
g = User._group_names[group_name]
else:
g = Group.objects.filter(name=group_name).first()
User._group_names[group_name] = g
if g: if g:
group_name = g.name group_name = g.name
group_id = g.id group_id = g.id
@ -245,18 +256,26 @@ class User(AbstractBaseUser):
if group_name == settings.SITH_MAIN_MEMBERS_GROUP: # We check the subscription if asked if group_name == settings.SITH_MAIN_MEMBERS_GROUP: # We check the subscription if asked
return self.is_subscribed return self.is_subscribed
if group_name[-len(settings.SITH_BOARD_SUFFIX):] == settings.SITH_BOARD_SUFFIX: if group_name[-len(settings.SITH_BOARD_SUFFIX):] == settings.SITH_BOARD_SUFFIX:
from club.models import Club
name = group_name[:-len(settings.SITH_BOARD_SUFFIX)] name = group_name[:-len(settings.SITH_BOARD_SUFFIX)]
c = Club.objects.filter(unix_name=name).first() if name in User._club_memberships.keys():
mem = c.get_membership_for(self) mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem: if mem:
return mem.role > settings.SITH_MAXIMUM_FREE_ROLE return mem.role > settings.SITH_MAXIMUM_FREE_ROLE
return False return False
if group_name[-len(settings.SITH_MEMBER_SUFFIX):] == settings.SITH_MEMBER_SUFFIX: if group_name[-len(settings.SITH_MEMBER_SUFFIX):] == settings.SITH_MEMBER_SUFFIX:
from club.models import Club
name = group_name[:-len(settings.SITH_MEMBER_SUFFIX)] name = group_name[:-len(settings.SITH_MEMBER_SUFFIX)]
c = Club.objects.filter(unix_name=name).first() if name in User._club_memberships.keys():
mem = c.get_membership_for(self) mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem: if mem:
return True return True
return False return False

View File

@ -48,8 +48,8 @@ a {
.ib { .ib {
display: inline-block; display: inline-block;
padding: 2px; padding: 1px;
margin: 2px; margin: 1px;
} }
.w_big { .w_big {
@ -57,11 +57,11 @@ a {
} }
.w_medium { .w_medium {
width: 45%; width: 47%;
} }
.w_small { .w_small {
width: 20%; width: 23%;
} }
/*--------------------------------HEADER-------------------------------*/ /*--------------------------------HEADER-------------------------------*/
@ -271,11 +271,15 @@ code {
} }
blockquote { blockquote {
margin: 10px; margin: 5px;
padding: 5px; padding: 2px;
border: solid 1px $black-color; border: solid 1px $black-color;
} }
blockquote h5:first-child {
font-size: 100%;
}
.edit-bar { .edit-bar {
display: block; display: block;
margin: 4px; margin: 4px;
@ -498,86 +502,132 @@ textarea {
/*------------------------------FORUM----------------------------------*/ /*------------------------------FORUM----------------------------------*/
.topic a, .forum a, .category a { #forum {
color: $black-color;
}
.topic a:hover, .forum a:hover, .category a:hover {
color: #424242;
text-decoration: underline;
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 2px;
margin: 2px;
}
.forum {
background: $primary-neutral-light-color;
padding: 2px;
margin: 2px;
}
.category {
background: $secondary-color;
}
.message {
padding: 2px;
margin: 2px;
background: $white-color;
&:nth-child(odd) {
background: $primary-neutral-light-color;
}
h5 {
font-size: 100%;
}
&.unread {
background: #d8e7f3;
}
}
.msg_author.deleted {
background: #ffcfcf;
}
.msg_content {
&.deleted {
background: #ffefef;
}
display: inline-block;
width: 80%;
vertical-align: top;
}
.msg_author {
display: inline-block;
width: 19%;
text-align: center;
background: $primary-light-color;
img {
max-width: 70%;
margin: 0px auto;
}
}
.msg_meta {
font-size: small;
list-style-type: none;
li {
padding: 2px;
margin: 2px;
}
}
.forum_signature {
color: #C0C0C0;
border-top: 1px solid #C0C0C0;
a { a {
color: $black-color;
}
a:hover {
color: #424242;
text-decoration: underline;
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
}
.tools {
font-size: x-small;
border: none;
a {
padding: 1px;
}
}
.title {
font-size: small;
font-weight: bold;
padding: 2px;
}
.last_message date {
white-space: nowrap;
}
.last_message span {
white-space: nowrap;
text-overflow: ellipsis;
overflow:hidden;
width: 100%;
display: block;
}
.forum {
background: $primary-neutral-light-color;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
}
.category {
margin-top: 5px;
background: $secondary-color;
.title {
text-transform: uppercase;
}
}
.message {
padding: 1px;
margin: 1px;
background: $white-color;
&:nth-child(odd) {
background: $primary-neutral-light-color;
}
.title {
font-size: 100%;
}
&.unread {
background: #d8e7f3;
}
}
.msg_author.deleted {
background: #ffcfcf;
}
.msg_content {
&.deleted {
background: #ffefef;
}
display: inline-block;
width: 80%;
vertical-align: top;
}
.msg_author {
display: inline-block;
width: 19%;
text-align: center;
background: $primary-light-color;
img {
max-width: 70%;
margin: 0px auto;
}
}
.msg_header {
display: inline-block;
width: 100%;
font-size: small;
}
.msg_meta {
font-size: small;
list-style-type: none;
li {
padding: 1px;
margin: 1px;
}
}
.forum_signature {
color: #C0C0C0; color: #C0C0C0;
&:hover { border-top: 1px solid #C0C0C0;
text-decoration: underline; a {
color: #C0C0C0;
&:hover {
text-decoration: underline;
}
} }
} }
} }

View File

@ -2,6 +2,10 @@
<a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_display_name() }}</a> <a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_display_name() }}</a>
{%- endmacro %} {%- endmacro %}
{% macro user_profile_link_short_name(user) -%}
<a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_short_name() }}</a>
{%- endmacro %}
{% macro user_link_with_pict(user) -%} {% macro user_link_with_pict(user) -%}
<a href="{{ url("core:user_profile", user_id=user.id) }}" class="mini_profile_link" > <a href="{{ url("core:user_profile", user_id=user.id) }}" class="mini_profile_link" >
{{ user.get_mini_item()|safe }} {{ user.get_mini_item()|safe }}

View File

@ -1,12 +1,14 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title %} {% block title %}
{% trans %}Doku to Markdown{% endtrans %} {% trans %}To Markdown{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<input type="radio" name="syntax" value="doku" {% if request.POST['syntax'] != "bbcode" %}checked{% endif %} >Doku</input>
<input type="radio" name="syntax" value="bbcode" {% if request.POST['syntax'] == "bbcode" %}checked{% endif %} >BBCode</input>
<textarea name="text" id="text" rows="30" cols="80"> <textarea name="text" id="text" rows="30" cols="80">
{{- text -}} {{- text -}}
</textarea> </textarea>

View File

@ -108,7 +108,7 @@
</ul> </ul>
<h4>{% trans %}Other tools{% endtrans %}</h4> <h4>{% trans %}Other tools{% endtrans %}</h4>
<ul> <ul>
<li><a href="{{ url('core:doku_to_markdown') }}">{% trans %}Convert dokuwiki syntax to Markdown{% endtrans %}</a></li> <li><a href="{{ url('core:to_markdown') }}">{% trans %}Convert dokuwiki/BBcode syntax to Markdown{% endtrans %}</a></li>
<li><a href="{{ url('trombi:user_tools') }}">{% trans %}Trombi tools{% endtrans %}</a></li> <li><a href="{{ url('trombi:user_tools') }}">{% trans %}Trombi tools{% endtrans %}</a></li>
</ul> </ul>

View File

@ -28,7 +28,7 @@ from core.views import *
urlpatterns = [ urlpatterns = [
url(r'^$', index, name='index'), url(r'^$', index, name='index'),
url(r'^doku_to_markdown$', DokuToMarkdownView.as_view(), name='doku_to_markdown'), url(r'^to_markdown$', ToMarkdownView.as_view(), name='to_markdown'),
url(r'^notifications$', NotificationList.as_view(), name='notification_list'), url(r'^notifications$', NotificationList.as_view(), name='notification_list'),
url(r'^notification/(?P<notif_id>[0-9]+)$', notification, name='notification'), url(r'^notification/(?P<notif_id>[0-9]+)$', notification, name='notification'),

View File

@ -66,6 +66,7 @@ def exif_auto_rotate(image):
return image return image
def doku_to_markdown(text): def doku_to_markdown(text):
"""This is a quite correct doku translator"""
text = re.sub(r'([^:]|^)\/\/(.*?)\/\/', r'*\2*', text) # Italic (prevents protocol:// conflict) text = re.sub(r'([^:]|^)\/\/(.*?)\/\/', r'*\2*', text) # Italic (prevents protocol:// conflict)
text = re.sub(r'<del>(.*?)<\/del>', r'~~\1~~', text, flags=re.DOTALL) # Strike (may be multiline) text = re.sub(r'<del>(.*?)<\/del>', r'~~\1~~', text, flags=re.DOTALL) # Strike (may be multiline)
text = re.sub(r'<sup>(.*?)<\/sup>', r'^\1^', text) # Superscript (multiline not supported, because almost never used) text = re.sub(r'<sup>(.*?)<\/sup>', r'^\1^', text) # Superscript (multiline not supported, because almost never used)
@ -93,8 +94,10 @@ def doku_to_markdown(text):
text = re.sub(r'\\{2,}[\s]', r' \n', text) # Carriage return text = re.sub(r'\\{2,}[\s]', r' \n', text) # Carriage return
text = re.sub(r'\[\[(.*?)(\|(.*?))?\]\]', r'[\3](\1)', text) # Links text = re.sub(r'\[\[(.*?)\|(.*?)\]\]', r'[\2](\1)', text) # Links
text = re.sub(r'{{(.*?)(\|(.*?))?}}', r'![\3](\1 "\3")', text) # Images text = re.sub(r'\[\[(.*?)\]\]', r'[\1](\1)', text) # Links 2
text = re.sub(r'{{(.*?)\|(.*?)}}', r'![\2](\1 "\2")', text) # Images
text = re.sub(r'{{(.*?)(\|(.*?))?}}', r'![\1](\1 "\1")', text) # Images 2
text = re.sub(r'{\[(.*?)(\|(.*?))?\]}', r'[\1](\1)', text) # Video (transform to classic links, since we can't integrate them) text = re.sub(r'{\[(.*?)(\|(.*?))?\]}', r'[\1](\1)', text) # Video (transform to classic links, since we can't integrate them)
text = re.sub(r'###(\d*?)###', r'[[[\1]]]', text) # Progress bar text = re.sub(r'###(\d*?)###', r'[[[\1]]]', text) # Progress bar
@ -117,14 +120,58 @@ def doku_to_markdown(text):
quote_level += 1 quote_level += 1
try: try:
new_text.append("> " * quote_level + "##### " + quote.group(2)) new_text.append("> " * quote_level + "##### " + quote.group(2))
line = line.replace(quote.group(0), '')
except: except:
new_text.append("> " * quote_level) new_text.append("> " * quote_level)
line = line.replace(quote.group(0), '')
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time) for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), '') line = line.replace(quote.group(0), '')
quote_level -= 1 quote_level -= 1
final_newline = True
new_text.append("> " * final_quote_level + line) # Finally append the line new_text.append("> " * final_quote_level + line) # Finally append the line
if final_newline: new_text.append("\n") # Add a new line to ensure the separation between the quote and the following text
else:
new_text.append(line)
return "\n".join(new_text)
def bbcode_to_markdown(text):
"""This is a very basic BBcode translator"""
text = re.sub(r'\[b\](.*?)\[\/b\]', r'**\1**', text, flags=re.DOTALL) # Bold
text = re.sub(r'\[i\](.*?)\[\/i\]', r'*\1*', text, flags=re.DOTALL) # Italic
text = re.sub(r'\[u\](.*?)\[\/u\]', r'__\1__', text, flags=re.DOTALL) # Underline
text = re.sub(r'\[s\](.*?)\[\/s\]', r'~~\1~~', text, flags=re.DOTALL) # Strike (may be multiline)
text = re.sub(r'\[strike\](.*?)\[\/strike\]', r'~~\1~~', text, flags=re.DOTALL) # Strike 2
text = re.sub(r'article://', r'page://', text)
text = re.sub(r'dfile://', r'file://', text)
text = re.sub(r'\[url=(.*?)\](.*)\[\/url\]', r'[\2](\1)', text) # Links
text = re.sub(r'\[url\](.*)\[\/url\]', r'\1', text) # Links 2
text = re.sub(r'\[img\](.*)\[\/img\]', r'![\1](\1 "\1")', text) # Images
new_text = []
quote_level = 0
for line in text.splitlines(): # Tables and quotes
enter = re.finditer(r'\[quote(=(.+?))?\]', line)
quit = re.finditer(r'\[/quote\]', line)
if enter or quit: # Quote part
for quote in enter: # Enter quotes (support multiple at a time)
quote_level += 1
try:
new_text.append("> " * quote_level + "##### " + quote.group(2))
except:
new_text.append("> " * quote_level)
line = line.replace(quote.group(0), '')
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), '')
quote_level -= 1
final_newline = True
new_text.append("> " * final_quote_level + line) # Finally append the line
if final_newline: new_text.append("\n") # Add a new line to ensure the separation between the quote and the following text
else: else:
new_text.append(line) new_text.append(line)

View File

@ -37,7 +37,7 @@ from itertools import chain
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from core.models import User, Notification from core.models import User, Notification
from core.utils import doku_to_markdown from core.utils import doku_to_markdown, bbcode_to_markdown
from club.models import Club from club.models import Club
def index(request, context=None): def index(request, context=None):
@ -98,17 +98,20 @@ def search_json(request):
} }
return JsonResponse(result) return JsonResponse(result)
class DokuToMarkdownView(TemplateView): class ToMarkdownView(TemplateView):
template_name = "core/doku_to_markdown.jinja" template_name = "core/to_markdown.jinja"
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.text = request.POST['text'] self.text = request.POST['text']
self.text_md = doku_to_markdown(self.text) if request.POST['syntax'] == "doku":
self.text_md = doku_to_markdown(self.text)
else:
self.text_md = bbcode_to_markdown(self.text)
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return self.render_to_response(context) return self.render_to_response(context)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(DokuToMarkdownView, self).get_context_data(**kwargs) kwargs = super(ToMarkdownView, self).get_context_data(**kwargs)
try: try:
kwargs['text'] = self.text kwargs['text'] = self.text
kwargs['text_md'] = self.text_md kwargs['text_md'] = self.text_md

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('counter', '0011_auto_20161004_2039'),
]
operations = [
migrations.AlterField(
model_name='permanency',
name='end',
field=models.DateTimeField(db_index=True, verbose_name='end date', null=True),
),
]

View File

@ -431,7 +431,7 @@ class Permanency(models.Model):
user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user")) user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
counter = models.ForeignKey(Counter, related_name="permanencies", verbose_name=_("counter")) counter = models.ForeignKey(Counter, related_name="permanencies", verbose_name=_("counter"))
start = models.DateTimeField(_('start date')) start = models.DateTimeField(_('start date'))
end = models.DateTimeField(_('end date'), null=True) end = models.DateTimeField(_('end date'), null=True, db_index=True)
activity = models.DateTimeField(_('last activity date'), auto_now=True) activity = models.DateTimeField(_('last activity date'), auto_now=True)
class Meta: class Meta:

View File

@ -29,3 +29,4 @@ from forum.models import *
admin.site.register(Forum) admin.site.register(Forum)
admin.site.register(ForumTopic) admin.site.register(ForumTopic)
admin.site.register(ForumMessage) admin.site.register(ForumMessage)
admin.site.register(ForumUserInfo)

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('forum', '0003_auto_20170510_1754'),
]
operations = [
migrations.AlterModelOptions(
name='forummessage',
options={'ordering': ['-date']},
),
migrations.AlterModelOptions(
name='forumtopic',
options={'ordering': ['-_last_message__date']},
),
migrations.AddField(
model_name='forum',
name='_last_message',
field=models.ForeignKey(verbose_name='the last message', to='forum.ForumMessage', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='forums_where_its_last'),
),
migrations.AddField(
model_name='forum',
name='_topic_number',
field=models.IntegerField(default=0, verbose_name='number of topics'),
),
migrations.AddField(
model_name='forummessage',
name='_deleted',
field=models.BooleanField(default=False, verbose_name='is deleted'),
),
migrations.AddField(
model_name='forumtopic',
name='_last_message',
field=models.ForeignKey(verbose_name='the last message', to='forum.ForumMessage', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+'),
),
migrations.AddField(
model_name='forumtopic',
name='_message_number',
field=models.IntegerField(default=0, verbose_name='number of messages'),
),
migrations.AddField(
model_name='forumtopic',
name='_title',
field=models.CharField(max_length=64, blank=True, verbose_name='title'),
),
migrations.AlterField(
model_name='forum',
name='description',
field=models.CharField(max_length=512, default='', verbose_name='description'),
),
migrations.AlterField(
model_name='forum',
name='id',
field=models.AutoField(primary_key=True, serialize=False, db_index=True),
),
]

View File

@ -46,8 +46,9 @@ class Forum(models.Model):
edit_groups allows to put any group as a forum admin edit_groups allows to put any group as a forum admin
view_groups allows some groups to view a forum view_groups allows some groups to view a forum
""" """
id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_('name'), max_length=64) name = models.CharField(_('name'), max_length=64)
description = models.CharField(_('description'), max_length=256, default="") description = models.CharField(_('description'), max_length=512, default="")
is_category = models.BooleanField(_('is a category'), default=False) is_category = models.BooleanField(_('is a category'), default=False)
parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True) parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"), owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
@ -57,6 +58,9 @@ class Forum(models.Model):
view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True, view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
default=[settings.SITH_GROUP_PUBLIC_ID]) default=[settings.SITH_GROUP_PUBLIC_ID])
number = models.IntegerField(_("number to choose a specific forum ordering"), default=1) number = models.IntegerField(_("number to choose a specific forum ordering"), default=1)
_last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last",
verbose_name=_("the last message"), null=True, on_delete=models.SET_NULL)
_topic_number = models.IntegerField(_("number of topics"), default=0)
class Meta: class Meta:
ordering = ['number'] ordering = ['number']
@ -72,6 +76,28 @@ class Forum(models.Model):
if copy_rights: if copy_rights:
self.copy_rights() self.copy_rights()
def set_topic_number(self):
self._topic_number = self.get_topic_number()
self.save()
if self.parent:
self.parent.set_topic_number()
def set_last_message(self):
topic = ForumTopic.objects.filter(forum__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
forum = Forum.objects.filter(parent__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
if topic and forum:
if topic._last_message_id < forum._last_message_id:
self._last_message_id = forum._last_message_id
else:
self._last_message_id = topic._last_message_id
elif topic:
self._last_message_id = topic._last_message_id
elif forum:
self._last_message_id = forum._last_message_id
self.save()
if self.parent:
self.parent.set_last_message()
def apply_rights_recursively(self): def apply_rights_recursively(self):
children = self.children.all() children = self.children.all()
for c in children: for c in children:
@ -86,10 +112,19 @@ class Forum(models.Model):
self.view_groups = self.parent.view_groups.all() self.view_groups = self.parent.view_groups.all()
self.save() self.save()
_club_memberships = {} # This cache is particularly efficient:
# divided by 3 the number of requests on the main forum page
# after the first load
def is_owned_by(self, user): def is_owned_by(self, user):
if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID): if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
return True return True
m = self.owner_club.get_membership_for(user) try: m = Forum._club_memberships[self.id][user.id]
except:
m = self.owner_club.get_membership_for(user)
try: Forum._club_memberships[self.id][user.id] = m
except:
Forum._club_memberships[self.id] = {}
Forum._club_memberships[self.id][user.id] = m
if m: if m:
return m.role > settings.SITH_MAXIMUM_FREE_ROLE return m.role > settings.SITH_MAXIMUM_FREE_ROLE
return False return False
@ -122,9 +157,9 @@ class Forum(models.Model):
p = p.parent p = p.parent
return l return l
@cached_property @property
def topic_number(self): def topic_number(self):
return self.get_topic_number() return self._topic_number
def get_topic_number(self): def get_topic_number(self):
number = self.topics.all().count() number = self.topics.all().count()
@ -134,31 +169,31 @@ class Forum(models.Model):
@cached_property @cached_property
def last_message(self): def last_message(self):
return self.get_last_message() return self._last_message
forum_list = {} # Class variable used for cache purpose def get_children_list(self):
def get_last_message(self): l = [self.id]
last_msg = None for c in self.children.all():
for m in ForumMessage.objects.select_related('topic__forum').order_by('-id'): l.append(c.id)
if m.topic.forum.id in Forum.forum_list.keys(): # The object is already in Python's memory, l += c.get_children_list()
# so there's no need to query it again return l
forum = Forum.forum_list[m.topic.forum.id]
else: # Query the forum object and store it in the class variable for further use.
# Keeping the same object allows the @cached_property to work properly.
# This trick divided by 4 the number of DB queries in the main forum page, and about the same on many other forum pages.
# This also divided by 4 the amount of CPU usage for thoses pages, according to Django Debug Toolbar.
forum = m.topic.forum
Forum.forum_list[forum.id] = forum
if self in (forum.parent_list + [forum]) and not m.deleted:
return m
class ForumTopic(models.Model): class ForumTopic(models.Model):
forum = models.ForeignKey(Forum, related_name='topics') forum = models.ForeignKey(Forum, related_name='topics')
author = models.ForeignKey(User, related_name='forum_topics') author = models.ForeignKey(User, related_name='forum_topics')
description = models.CharField(_('description'), max_length=256, default="") description = models.CharField(_('description'), max_length=256, default="")
_last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"),
null=True, on_delete=models.SET_NULL)
_title = models.CharField(_('title'), max_length=64, blank=True)
_message_number = models.IntegerField(_("number of messages"), default=0)
class Meta: class Meta:
ordering = ['-id'] # TODO: add date message ordering ordering = ['-_last_message__date']
def save(self, *args, **kwargs):
super(ForumTopic, self).save(*args, **kwargs)
self.forum.set_topic_number() # Recompute the cached value
self.forum.set_last_message()
def is_owned_by(self, user): def is_owned_by(self, user):
return self.forum.is_owned_by(user) return self.forum.is_owned_by(user)
@ -184,13 +219,11 @@ class ForumTopic(models.Model):
@cached_property @cached_property
def last_message(self): def last_message(self):
for msg in self.messages.order_by('id').select_related('author').order_by('-id').all(): return self._last_message
if not msg.deleted:
return msg
@property @cached_property
def title(self): def title(self):
return self.messages.order_by('date').first().title return self._title
class ForumMessage(models.Model): class ForumMessage(models.Model):
""" """
@ -202,12 +235,29 @@ class ForumMessage(models.Model):
message = models.TextField(_("message"), default="") message = models.TextField(_("message"), default="")
date = models.DateTimeField(_('date'), default=timezone.now) date = models.DateTimeField(_('date'), default=timezone.now)
readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers")) readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
_deleted = models.BooleanField(_('is deleted'), default=False)
class Meta: class Meta:
ordering = ['id'] ordering = ['-date']
def __str__(self): def __str__(self):
return "%s - %s" % (self.id, self.title) return "%s (%s) - %s" % (self.id, self.author, self.title)
def save(self, *args, **kwargs):
self._deleted = self.is_deleted() # Recompute the cached value
super(ForumMessage, self).save(*args, **kwargs)
if self.is_last_in_topic():
self.topic._last_message_id = self.id
if self.is_first_in_topic() and self.title:
self.topic._title = self.title
self.topic._message_number = self.topic.messages.count()
self.topic.save()
def is_first_in_topic(self):
return bool(self.id == self.topic.messages.order_by('date').first().id)
def is_last_in_topic(self):
return bool(self.id == self.topic.messages.order_by('date').last().id)
def is_owned_by(self, user): # Anyone can create a topic: it's better to def is_owned_by(self, user): # Anyone can create a topic: it's better to
# check the rights at the forum level, since it's more controlled # check the rights at the forum level, since it's more controlled
@ -217,12 +267,15 @@ class ForumMessage(models.Model):
return user.can_edit(self.topic.forum) return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
return (not self.deleted and user.can_view(self.topic)) return not self._deleted # No need to check the real rights since it's already done by the Topic view
def can_be_moderated_by(self, user): def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id return self.topic.forum.is_owned_by(user) or user.id == self.author.id
def get_absolute_url(self): def get_absolute_url(self):
return reverse('forum:view_message', kwargs={'message_id': self.id})
def get_url(self):
return self.topic.get_absolute_url() + "?page=" + str(self.get_page()) + "#msg_" + str(self.id) return self.topic.get_absolute_url() + "?page=" + str(self.get_page()) + "#msg_" + str(self.id)
def get_page(self): def get_page(self):
@ -230,16 +283,13 @@ class ForumMessage(models.Model):
def mark_as_read(self, user): def mark_as_read(self, user):
try: # Need the try/except because of AnonymousUser try: # Need the try/except because of AnonymousUser
self.readers.add(user) if not self.is_read(user):
self.readers.add(user)
except: pass except: pass
def is_read(self, user): def is_read(self, user):
return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all()) return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all())
@cached_property
def deleted(self):
return self.is_deleted()
def is_deleted(self): def is_deleted(self):
meta = self.metas.exclude(action="EDIT").order_by('-date').first() meta = self.metas.exclude(action="EDIT").order_by('-date').first()
if meta: if meta:
@ -258,13 +308,22 @@ class ForumMessageMeta(models.Model):
date = models.DateTimeField(_('date'), default=timezone.now) date = models.DateTimeField(_('date'), default=timezone.now)
action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16) action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16)
def save(self, *args, **kwargs):
super(ForumMessageMeta, self).save(*args, **kwargs)
self.message._deleted = self.message.is_deleted()
self.message.save()
class ForumUserInfo(models.Model): class ForumUserInfo(models.Model):
""" """
This currently stores only the last date a user clicked "Mark all as read". This currently stores only the last date a user clicked "Mark all as read".
However, this can be extended with lot of user preferences dedicated to a However, this can be extended with lot of user preferences dedicated to a
user, such as the favourite topics, the signature, and so on... user, such as the favourite topics, the signature, and so on...
""" """
user = models.OneToOneField(User, related_name="_forum_infos") # TODO: see to move that to the User class in order to reduce the number of db queries user = models.OneToOneField(User, related_name="_forum_infos")
last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR, last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
month=1, day=1, tzinfo=pytz.UTC)) month=1, day=1, tzinfo=pytz.UTC))
def __str__(self):
return str(self.user)

View File

@ -6,13 +6,14 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div> <div>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> <a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %} {% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a> > <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %} {% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a> > <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
</div> </div>
<div id="forum">
<h3>{{ forum.name }}</h3> <h3>{{ forum.name }}</h3>
<p> <p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %} {% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
@ -34,7 +35,8 @@
</div> </div>
</div> </div>
</div> </div>
{% for f in forum.children.all() %} {{ display_forum(forum, user, True) }}
{% for f in forum.children.all().select_related("_last_message__author", "_last_message__topic") %}
{{ display_forum(f, user) }} {{ display_forum(f, user) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -58,7 +60,13 @@
{% for t in topics %} {% for t in topics %}
{{ display_topic(t, user) }} {{ display_topic(t, user) }}
{% endfor %} {% endfor %}
<p style="text-align: right; background: #d8e7f3;">
{% for p in topics.paginator.page_range %}
<span class="ib" style="background: {% if p == topics.number %}white{% endif %}; margin: 0;"><a href="?topic_page={{ p }}">{{ p }}</a></span>
{% endfor %}
</p>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -6,18 +6,28 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<p> <p>
<a href="{{ url('forum:main') }}">Forum</a> > <a href="{{ url('forum:main') }}">Forum</a> >
</p> </p>
<div id="forum">
<h3>{% trans %}Forum{% endtrans %}</h3> <h3>{% trans %}Forum{% endtrans %}</h3>
<h4>{% trans %}Last unread messages{% endtrans %}</h4> <h4>{% trans %}Last unread messages{% endtrans %}</h4>
<p> <p>
<a class="ib" href="{{ url('forum:mark_all_as_read') }}">{% trans %}Mark all as read{% endtrans %}</a> <a class="ib" href="{{ url('forum:mark_all_as_read') }}">{% trans %}Mark all as read{% endtrans %}</a>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}Refresh{% endtrans %}</a> <a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}Refresh{% endtrans %}</a>
</p> </p>
{% for t in forumtopic_list %} {% for t in page_obj.object_list %}
{{ display_topic(t, user, True) }} {{ display_topic(t, user, True) }}
{% endfor %} {% endfor %}
<p style="text-align: right; background: #d8e7f3;">
{% for p in paginator.page_range %}
<span class="ib" style="background: {% if p == paginator.number %}white{% endif %}; margin: 0;">
<a href="?page={{ p }}">{{ p }}</a>
</span>
{% endfor %}
</p>
</div>
{% endblock %} {% endblock %}

View File

@ -1,37 +1,43 @@
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link_short_name %}
{% macro display_forum(forum, user) %} {% macro display_forum(forum, user, is_root=False) %}
<div class="forum {% if forum.is_category %}category{% endif %}"> <div class="forum {% if is_root %}category{% endif %}">
<div class="ib w_big"> <div class="ib w_big">
{% if not forum.is_category %} {% if not is_root %}
<a class="ib w_big" href="{{ url('forum:view_forum', forum_id=forum.id) }}"> <a class="ib w_big" href="{{ url('forum:view_forum', forum_id=forum.id) }}">
{% else %} {% else %}
<div class="ib w_big"> <div class="ib w_big">
{% endif %} {% endif %}
<h5>{{ forum.name }}</h5> <div class="title">{{ forum.name }}</div>
<p>{{ forum.description }}</p> <p>{{ forum.description }}</p>
{% if not forum.is_category %} {% if not is_root %}
</a> </a>
{% else %} {% else %}
</div> </div>
{% endif %} {% endif %}
{% if user.is_owner(forum) %} {% if user.is_owner(forum) %}
<div class="tools">
<a class="ib" href="{{ url('forum:edit_forum', forum_id=forum.id) }}">{% trans %}Edit{% endtrans %}</a> <a class="ib" href="{{ url('forum:edit_forum', forum_id=forum.id) }}">{% trans %}Edit{% endtrans %}</a>
<a class="ib" href="{{ url('forum:delete_forum', forum_id=forum.id) }}">{% trans %}Delete{% endtrans %}</a> <a class="ib" href="{{ url('forum:delete_forum', forum_id=forum.id) }}">{% trans %}Delete{% endtrans %}</a>
</div>
{% endif %} {% endif %}
</div> </div>
{% if not forum.is_category %} {% if not is_root %}
<div class="ib w_small"> <div class="ib w_small">
<div class="ib w_medium"> <p class="ib w_medium">
{{ forum.topic_number }} {{ forum.topic_number }}
</div> </p>
<div class="ib w_medium" style="font-size: x-small; text-align: center"> <div class="ib w_medium last_message" style="font-size: x-small; text-align: center">
{% if forum.last_message %} {% if forum.last_message %}
{{ forum.last_message.author }} <br/> {{ user_profile_link_short_name(forum.last_message.author) }} <br/>
<a href="{{ forum.last_message.get_absolute_url() }}"> <a href="{{ forum.last_message.get_absolute_url() }}">
<date>
{{ forum.last_message.date|localtime|date(DATETIME_FORMAT) }} {{ forum.last_message.date|localtime|date(DATETIME_FORMAT) }}
{{ forum.last_message.date|localtime|time(DATETIME_FORMAT) }}<br/> {{ forum.last_message.date|localtime|time(DATETIME_FORMAT) }}
</date><br>
<span>
{{ forum.last_message.topic }} {{ forum.last_message.topic }}
</span>
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -48,33 +54,33 @@
{% else %} {% else %}
<a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}"> <a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}">
{% endif %} {% endif %}
<h5>{{ topic.title }}</h5> <div class="title">{{ topic.title or topic.messages.first().title }}</div>
<p>{{ topic.description }}</p> <p>{{ topic.description }}</p>
</a> </a>
{% if user.can_edit(topic) %} {% if user.can_edit(topic) %}
<div class="ib" style="text-align: center;"> <div class="ib tools" style="text-align: center;">
<a href="{{ url('forum:edit_topic', topic_id=topic.id) }}">{% trans %}Edit{% endtrans %}</a> <a href="{{ url('forum:edit_topic', topic_id=topic.id) }}">{% trans %}Edit{% endtrans %}</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="ib w_medium"> <div class="ib w_medium last_message">
<div class="ib w_medium"> <div class="ib w_medium">
<div class="ib w_medium" style="text-align: center;"> <p class="ib w_medium" style="text-align: center;">
{{ user_profile_link(topic.author) }} {{ user_profile_link_short_name(topic.author) }}
</div> </p>
<div class="ib w_medium" style="text-align: center;"> <p class="ib w_medium" style="text-align: center;">
{{ topic.messages.count() }} {{ topic._message_number }}
</div> </p>
</div> </div>
<div class="ib w_medium" style="text-align: center;"> <p class="ib w_medium" style="text-align: center;">
{% set last_msg = topic.last_message %} {% set last_msg = topic.last_message %}
{% if last_msg %} {% if last_msg %}
{{ user_profile_link(last_msg.author) }} <br/> {{ user_profile_link_short_name(last_msg.author) }} <br/>
<a href="{{ last_msg.get_absolute_url() }}"> <a href="{{ last_msg.get_absolute_url() }}">
{{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }} <date>{{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }}</date>
</a> </a>
{% endif %} {% endif %}
</div> </p>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@ -92,26 +98,32 @@
<strong><a href="{{ m.author.get_absolute_url() }}">{{ m.author.get_short_name() }}</a></strong> <strong><a href="{{ m.author.get_absolute_url() }}">{{ m.author.get_short_name() }}</a></strong>
</div> </div>
<div class="msg_content {% if m.deleted %}deleted{% endif %}" {% if m.id == first_unread_message_id %}id="first_unread"{% endif %}> <div class="msg_content {% if m.deleted %}deleted{% endif %}" {% if m.id == first_unread_message_id %}id="first_unread"{% endif %}>
<div style="display: inline-block; width: 74%;"> <div class="msg_header">
{% if m.title %} <div class="ib w_big title">
<h5>{{ m.title }}</h5> <a href="{{ m.get_absolute_url() }}">
{% endif %} {{ m.date|localtime|date(DATETIME_FORMAT) }}
</div> {{ m.date|localtime|time(DATETIME_FORMAT) }}
<div style="display: inline-block; width: 25%;"> {%- if m.title -%}
<span><a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}"> - {{ m.title }}
{% trans %}Reply as quote{% endtrans %}</a></span> {%- endif -%}
{% if user.can_edit(m) %} </a>
<span> <a href="{{ url('forum:edit_message', message_id=m.id) }}">{% trans %}Edit{% endtrans %}</a></span> </div>
{% endif %} <div class="ib w_small">
{% if m.can_be_moderated_by(user) %} <span><a href="{{ m.get_absolute_url() }}">#{{ m.id }}</a></span>
{% if m.deleted %} <br/>
<span> <a href="{{ url('forum:undelete_message', message_id=m.id) }}">{% trans %}Undelete{% endtrans %}</a></span> <span><a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}">
{% else %} {% trans %}Reply as quote{% endtrans %}</a></span>
<span> <a href="{{ url('forum:delete_message', message_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></span> {% if user.can_edit(m) %}
{% endif %} <span> <a href="{{ url('forum:edit_message', message_id=m.id) }}">{% trans %}Edit{% endtrans %}</a></span>
{% endif %} {% endif %}
<br/> {% if m.can_be_moderated_by(user) %}
<span>{{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }}</span> {% if m.deleted %}
<span> <a href="{{ url('forum:undelete_message', message_id=m.id) }}">{% trans %}Undelete{% endtrans %}</a></span>
{% else %}
<span> <a href="{{ url('forum:delete_message', message_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></span>
{% endif %}
{% endif %}
</div>
</div> </div>
<hr> <hr>
<div> <div>
@ -121,7 +133,7 @@
<ul class="msg_meta"> <ul class="msg_meta">
{% for meta in m.metas.select_related('user').order_by('id') %} {% for meta in m.metas.select_related('user').order_by('id') %}
<li style="background: {% if m.author == meta.user %}#bfffbf{% else %}#ffffbf{% endif %}"> <li style="background: {% if m.author == meta.user %}#bfffbf{% else %}#ffffbf{% endif %}">
{{ meta.get_action_display() }} {{ meta.user.get_display_name() }} {{ meta.get_action_display() }} {{ meta.user.get_short_name() }}
{% trans %} at {% endtrans %}{{ meta.date|localtime|time(DATETIME_FORMAT) }} {% trans %} at {% endtrans %}{{ meta.date|localtime|time(DATETIME_FORMAT) }}
{% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}</li> {% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}</li>
{% endfor %} {% endfor %}

View File

@ -7,9 +7,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<p> <p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> > <a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >
</p> </p>
<div id="forum">
<h3>{% trans %}Forum{% endtrans %}</h3> <h3>{% trans %}Forum{% endtrans %}</h3>
<p> <p>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a> <a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a>
@ -19,14 +20,28 @@
<a href="{{ url('forum:new_forum') }}">{% trans %}New forum{% endtrans %}</a> <a href="{{ url('forum:new_forum') }}">{% trans %}New forum{% endtrans %}</a>
</p> </p>
{% endif %} {% endif %}
<div>
<div class="ib w_big">
{% trans %}Title{% endtrans %}
</div>
<div class="ib w_small">
<div class="ib w_medium">
{% trans %}Topics{% endtrans %}
</div>
<div class="ib w_small">
{% trans %}Last message{% endtrans %}
</div>
</div>
</div>
{% for f in forum_list %} {% for f in forum_list %}
<div style="padding: 4px; margin: 4px"> <div>
{{ display_forum(f, user) }} {{ display_forum(f, user, True) }}
{% for c in f.children.all() %} {% for c in f.children.all() %}
{{ display_forum(c, user) }} {{ display_forum(c, user) }}
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endblock %} {% endblock %}

View File

@ -35,6 +35,7 @@
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a> > <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
</p> </p>
<h3>{{ topic.title }}</h3> <h3>{{ topic.title }}</h3>
<div id="forum">
<p>{{ topic.description }}</p> <p>{{ topic.description }}</p>
<p><a href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p> <p><a href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p>
@ -62,6 +63,7 @@
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span> <span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
{% endfor %} {% endfor %}
</p> </p>
</div>
{% endblock %} {% endblock %}

View File

@ -38,6 +38,7 @@ urlpatterns = [
url(r'^topic/(?P<topic_id>[0-9]+)$', ForumTopicDetailView.as_view(), name='view_topic'), url(r'^topic/(?P<topic_id>[0-9]+)$', ForumTopicDetailView.as_view(), name='view_topic'),
url(r'^topic/(?P<topic_id>[0-9]+)/edit$', ForumTopicEditView.as_view(), name='edit_topic'), url(r'^topic/(?P<topic_id>[0-9]+)/edit$', ForumTopicEditView.as_view(), name='edit_topic'),
url(r'^topic/(?P<topic_id>[0-9]+)/new_message$', ForumMessageCreateView.as_view(), name='new_message'), url(r'^topic/(?P<topic_id>[0-9]+)/new_message$', ForumMessageCreateView.as_view(), name='new_message'),
url(r'^message/(?P<message_id>[0-9]+)$', ForumMessageView.as_view(), name='view_message'),
url(r'^message/(?P<message_id>[0-9]+)/edit$', ForumMessageEditView.as_view(), name='edit_message'), url(r'^message/(?P<message_id>[0-9]+)/edit$', ForumMessageEditView.as_view(), name='edit_message'),
url(r'^message/(?P<message_id>[0-9]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'), url(r'^message/(?P<message_id>[0-9]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'),
url(r'^message/(?P<message_id>[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'), url(r'^message/(?P<message_id>[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'),

View File

@ -41,7 +41,7 @@ from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMi
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
class ForumMainView(ListView): class ForumMainView(ListView):
queryset = Forum.objects.filter(parent=None) queryset = Forum.objects.filter(parent=None).prefetch_related("children___last_message__author", "children___last_message__topic")
template_name = "forum/main.jinja" template_name = "forum/main.jinja"
class ForumMarkAllAsRead(RedirectView): class ForumMarkAllAsRead(RedirectView):
@ -53,17 +53,23 @@ class ForumMarkAllAsRead(RedirectView):
fi = request.user.forum_infos fi = request.user.forum_infos
fi.last_read_date = timezone.now() fi.last_read_date = timezone.now()
fi.save() fi.save()
for m in request.user.read_messages.filter(date__lt=fi.last_read_date):
m.readers.remove(request.user) # Clean up to keep table low in data
except: pass except: pass
return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs) return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs)
class ForumLastUnread(ListView): class ForumLastUnread(ListView):
model = ForumTopic model = ForumTopic
template_name = "forum/last_unread.jinja" template_name = "forum/last_unread.jinja"
paginate_by = settings.SITH_FORUM_PAGE_LENGTH / 2
def get_queryset(self): def get_queryset(self):
l = ForumMessage.objects.exclude(readers=self.request.user).filter( topic_list = self.model.objects.filter(_last_message__date__gt=self.request.user.forum_infos.last_read_date)\
date__gt=self.request.user.forum_infos.last_read_date).values_list('topic') # TODO try to do better .exclude(_last_message__readers=self.request.user)\
return self.model.objects.filter(id__in=l).annotate(models.Max('messages__date')).order_by('-messages__date__max').select_related('author') .order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\
.prefetch_related('forum__edit_groups')
return topic_list
class ForumForm(forms.ModelForm): class ForumForm(forms.ModelForm):
class Meta: class Meta:
@ -117,7 +123,18 @@ class ForumDetailView(CanViewMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ForumDetailView, self).get_context_data(**kwargs) kwargs = super(ForumDetailView, self).get_context_data(**kwargs)
kwargs['topics'] = self.object.topics.annotate(models.Max('messages__date')).order_by('-messages__date__max') qs = self.object.topics.order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\
.prefetch_related("forum__edit_groups")
paginator = Paginator(qs,
settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('topic_page')
try:
kwargs["topics"] = paginator.page(page)
except PageNotAnInteger:
kwargs["topics"] = paginator.page(1)
except EmptyPage:
kwargs["topics"] = paginator.page(paginator.num_pages)
return kwargs return kwargs
class TopicForm(forms.ModelForm): class TopicForm(forms.ModelForm):
@ -164,7 +181,8 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs['first_unread_message_id'] = msg.id kwargs['first_unread_message_id'] = msg.id
except: except:
kwargs['first_unread_message_id'] = float("inf") kwargs['first_unread_message_id'] = float("inf")
paginator = Paginator(self.object.messages.select_related('author__avatar_pict').all(), paginator = Paginator(self.object.messages.select_related('author__avatar_pict')\
.prefetch_related('topic__forum__edit_groups', 'readers').order_by('date'),
settings.SITH_FORUM_PAGE_LENGTH) settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('page') page = self.request.GET.get('page')
try: try:
@ -175,6 +193,15 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs["msgs"] = paginator.page(paginator.num_pages) kwargs["msgs"] = paginator.page(paginator.num_pages)
return kwargs return kwargs
class ForumMessageView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
return self.object.get_url()
class ForumMessageEditView(CanEditMixin, UpdateView): class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage model = ForumMessage
fields = ['title', 'message'] fields = ['title', 'message']

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,14 @@ from django.core.files import File
from core.models import User, SithFile from core.models import User, SithFile
from core.utils import doku_to_markdown, bbcode_to_markdown
from club.models import Club, Membership from club.models import Club, Membership
from counter.models import Customer, Counter, Selling, Refilling, Product, ProductType, Permanency, Eticket from counter.models import Customer, Counter, Selling, Refilling, Product, ProductType, Permanency, Eticket
from subscription.models import Subscription from subscription.models import Subscription
from eboutic.models import Invoice, InvoiceItem from eboutic.models import Invoice, InvoiceItem
from accounting.models import BankAccount, ClubAccount, GeneralJournal, Operation, AccountingType, Company, SimplifiedAccountingType, Label from accounting.models import BankAccount, ClubAccount, GeneralJournal, Operation, AccountingType, Company, SimplifiedAccountingType, Label
from sas.models import Album, Picture, PeoplePictureRelation from sas.models import Album, Picture, PeoplePictureRelation
from forum.models import Forum, ForumTopic, ForumMessage, ForumMessageMeta, ForumUserInfo
db = MySQLdb.connect(**settings.OLD_MYSQL_INFOS) db = MySQLdb.connect(**settings.OLD_MYSQL_INFOS)
start = datetime.datetime.now() start = datetime.datetime.now()
@ -1181,6 +1183,155 @@ def reset_sas_moderators():
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
def migrate_forum():
print("Migrating forum")
def migrate_forums():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up forums")
Forum.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_forum
WHERE id_forum <> 1
""")
print(" Migrating forums")
for r in cur:
try:
# parent = Forum.objects.filter(id=r['id_forum_parent']).first()
club = Club.objects.filter(id=r['id_asso']).first()
ae = Club.objects.filter(id=settings.SITH_MAIN_CLUB_ID).first()
forum = Forum(
id=r['id_forum'],
name=to_unicode(r['titre_forum']),
description=to_unicode(r['description_forum'])[:511],
is_category=bool(r['categorie_forum']),
# parent=parent,
owner_club=club or ae,
number=r['ordre_forum'],
)
forum.save()
except Exception as e:
print(" FAIL to migrate forum: %s" % (repr(e)))
cur.execute("""
SELECT *
FROM frm_forum
WHERE id_forum_parent <> 1
""")
for r in cur:
parent = Forum.objects.filter(id=r['id_forum_parent']).first()
forum = Forum.objects.filter(id=r['id_forum']).first()
forum.parent = parent
forum.save()
cur.close()
print(" Forums migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_topics():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up topics")
ForumTopic.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_sujet
""")
print(" Migrating topics")
for r in cur:
try:
parent = Forum.objects.filter(id=r['id_forum']).first()
saloon = Forum.objects.filter(id=3).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
topic = ForumTopic(
id=r['id_sujet'],
author=author or root,
forum=parent or saloon,
_title=to_unicode(r['titre_sujet'])[:64],
description=to_unicode(r['soustitre_sujet']),
)
topic.save()
except Exception as e:
print(" FAIL to migrate topic: %s" % (repr(e)))
cur.close()
print(" Topics migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_messages():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up messages")
ForumMessage.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_message
""")
print(" Migrating messages")
for r in cur:
try:
topic = ForumTopic.objects.filter(id=r['id_sujet']).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
msg = ForumMessage(
id=r['id_message'],
topic=topic,
author=author or root,
title=to_unicode(r['titre_message'])[:63],
date=r['date_message'].replace(tzinfo=timezone('Europe/Paris')),
)
try:
if r['syntaxengine_message'] == "doku":
msg.message = doku_to_markdown(to_unicode(r['contenu_message']))
else:
msg.message = bbcode_to_markdown(to_unicode(r['contenu_message']))
except:
msg.message = to_unicode(r['contenu_message'])
msg.save()
except Exception as e:
print(" FAIL to migrate message: %s" % (repr(e)))
cur.close()
print(" Messages migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_message_infos():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up message meta")
ForumMessageMeta.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_modere_info
""")
print(" Migrating message meta")
ACTIONS = {
"EDIT": "EDIT",
"AUTOEDIT": "EDIT",
"UNDELETE": "UNDELETE",
"DELETE": "DELETE",
"DELETEFIRST": "DELETE",
"AUTODELETE": "DELETE",
}
for r in cur:
try:
msg = ForumMessage.objects.filter(id=r['id_message']).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
meta = ForumMessageMeta(
message=msg,
user=author or root,
date=r['modere_date'].replace(tzinfo=timezone('Europe/Paris')),
action=ACTIONS[r['modere_action']],
)
meta.save()
except Exception as e:
print(" FAIL to migrate message meta: %s" % (repr(e)))
cur.close()
print(" Messages meta migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
migrate_forums()
migrate_topics()
migrate_messages()
migrate_message_infos()
print("Forum migrated at %s" % datetime.datetime.now())
print("Running time: %s" % (datetime.datetime.now()-start))
def main(): def main():
print("Start at %s" % start) print("Start at %s" % start)
# Core # Core
@ -1199,7 +1350,9 @@ def main():
# reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter') # reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter')
# migrate_sas() # migrate_sas()
# reset_index('core', 'sas') # reset_index('core', 'sas')
reset_sas_moderators() # reset_sas_moderators()
migrate_forum()
reset_index('forum')
end = datetime.datetime.now() end = datetime.datetime.now()
print("End at %s" % end) print("End at %s" % end)
print("Running time: %s" % (end-start)) print("Running time: %s" % (end-start))

View File

@ -52,7 +52,6 @@ class Subscription(models.Model):
subscription_end = models.DateField(_('subscription end')) subscription_end = models.DateField(_('subscription end'))
payment_method = models.CharField(_('payment method'), payment_method = models.CharField(_('payment method'),
max_length=255, max_length=255,
help_text=_('Eboutic is reserved to specific users. In doubt, don\'t use it.'),
choices=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD) choices=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)
location = models.CharField(choices=settings.SITH_SUBSCRIPTION_LOCATIONS, location = models.CharField(choices=settings.SITH_SUBSCRIPTION_LOCATIONS,
max_length=20, verbose_name=_('location')) max_length=20, verbose_name=_('location'))

View File

@ -19,7 +19,7 @@
<p>{{ form.subscription_type.errors }}<label for="{{ form.subscription_type.name }}">{{ form.subscription_type.label }}</label> {{ form.subscription_type }}</p> <p>{{ form.subscription_type.errors }}<label for="{{ form.subscription_type.name }}">{{ form.subscription_type.label }}</label> {{ form.subscription_type }}</p>
<p>{{ form.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{ <p>{{ form.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{
form.payment_method }}</p> form.payment_method }}</p>
<p>{{ form.payment_method.help_text }}</p> <p>{% trans %}Eboutic is reserved to specific users. In doubt, don't use it.{% endtrans %}</p>
<p>{{ form.location.errors }}<label for="{{ form.location.name }}">{{ form.location.label }}</label> {{ form.location }}</p> <p>{{ form.location.errors }}<label for="{{ form.location.name }}">{{ form.location.label }}</label> {{ form.location }}</p>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>