mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-26 02:54:20 +00:00
Merge branch 'wip' into 'master'
Forum improvements See merge request !75
This commit is contained in:
commit
38622c98e9
19
club/migrations/0008_auto_20170515_2214.py
Normal file
19
club/migrations/0008_auto_20170515_2214.py
Normal 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),
|
||||
),
|
||||
]
|
@ -39,6 +39,7 @@ class Club(models.Model):
|
||||
"""
|
||||
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)
|
||||
parent = models.ForeignKey('Club', related_name='children', null=True, blank=True)
|
||||
unix_name = models.CharField(_('unix name'), max_length=30, unique=True,
|
||||
@ -151,11 +152,21 @@ class Club(models.Model):
|
||||
return False
|
||||
return sub.is_subscribed
|
||||
|
||||
_memberships = {}
|
||||
def get_membership_for(self, 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):
|
||||
"""
|
||||
|
@ -22,3 +22,4 @@
|
||||
#
|
||||
#
|
||||
|
||||
default_app_config = 'core.apps.SithConfig'
|
||||
|
57
core/apps.py
Normal file
57
core/apps.py
Normal 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
|
||||
|
@ -223,14 +223,25 @@ class User(AbstractBaseUser):
|
||||
s = self.subscriptions.last()
|
||||
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):
|
||||
"""If the user is in the group passed in argument (as string or by id)"""
|
||||
group_id = 0
|
||||
g = None
|
||||
if isinstance(group_name, int): # Handle the case where group_name is an ID
|
||||
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:
|
||||
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:
|
||||
group_name = g.name
|
||||
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
|
||||
return self.is_subscribed
|
||||
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)]
|
||||
if name in User._club_memberships.keys():
|
||||
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:
|
||||
return mem.role > settings.SITH_MAXIMUM_FREE_ROLE
|
||||
return False
|
||||
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)]
|
||||
if name in User._club_memberships.keys():
|
||||
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:
|
||||
return True
|
||||
return False
|
||||
|
@ -48,8 +48,8 @@ a {
|
||||
|
||||
.ib {
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.w_big {
|
||||
@ -57,11 +57,11 @@ a {
|
||||
}
|
||||
|
||||
.w_medium {
|
||||
width: 45%;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.w_small {
|
||||
width: 20%;
|
||||
width: 23%;
|
||||
}
|
||||
|
||||
/*--------------------------------HEADER-------------------------------*/
|
||||
@ -271,11 +271,15 @@ code {
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
border: solid 1px $black-color;
|
||||
}
|
||||
|
||||
blockquote h5:first-child {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.edit-bar {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
@ -498,60 +502,99 @@ textarea {
|
||||
|
||||
/*------------------------------FORUM----------------------------------*/
|
||||
|
||||
.topic a, .forum a, .category a {
|
||||
#forum {
|
||||
a {
|
||||
color: $black-color;
|
||||
}
|
||||
}
|
||||
|
||||
.topic a:hover, .forum a:hover, .category a:hover {
|
||||
a:hover {
|
||||
color: #424242;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.topic {
|
||||
.topic {
|
||||
border: solid $primary-neutral-color 1px;
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
p {
|
||||
margin: 1px;
|
||||
font-size: smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.forum {
|
||||
.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: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
p {
|
||||
margin: 1px;
|
||||
font-size: smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.category {
|
||||
.category {
|
||||
margin-top: 5px;
|
||||
background: $secondary-color;
|
||||
}
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
.message {
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
background: $white-color;
|
||||
&:nth-child(odd) {
|
||||
background: $primary-neutral-light-color;
|
||||
}
|
||||
h5 {
|
||||
.title {
|
||||
font-size: 100%;
|
||||
}
|
||||
&.unread {
|
||||
background: #d8e7f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg_author.deleted {
|
||||
.msg_author.deleted {
|
||||
background: #ffcfcf;
|
||||
}
|
||||
}
|
||||
|
||||
.msg_content {
|
||||
.msg_content {
|
||||
&.deleted {
|
||||
background: #ffefef;
|
||||
}
|
||||
display: inline-block;
|
||||
width: 80%;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.msg_author {
|
||||
.msg_author {
|
||||
display: inline-block;
|
||||
width: 19%;
|
||||
text-align: center;
|
||||
@ -560,18 +603,24 @@ textarea {
|
||||
max-width: 70%;
|
||||
margin: 0px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.msg_meta {
|
||||
.msg_header {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.msg_meta {
|
||||
font-size: small;
|
||||
list-style-type: none;
|
||||
li {
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
padding: 1px;
|
||||
margin: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forum_signature {
|
||||
.forum_signature {
|
||||
color: #C0C0C0;
|
||||
border-top: 1px solid #C0C0C0;
|
||||
a {
|
||||
@ -580,6 +629,7 @@ textarea {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*------------------------------SAS------------------------------------*/
|
||||
|
@ -2,6 +2,10 @@
|
||||
<a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
||||
{%- 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) -%}
|
||||
<a href="{{ url("core:user_profile", user_id=user.id) }}" class="mini_profile_link" >
|
||||
{{ user.get_mini_item()|safe }}
|
||||
|
@ -1,12 +1,14 @@
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
{% trans %}Doku to Markdown{% endtrans %}
|
||||
{% trans %}To Markdown{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
{% 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">
|
||||
{{- text -}}
|
||||
</textarea>
|
@ -108,7 +108,7 @@
|
||||
</ul>
|
||||
<h4>{% trans %}Other tools{% endtrans %}</h4>
|
||||
<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>
|
||||
</ul>
|
||||
|
||||
|
@ -28,7 +28,7 @@ from core.views import *
|
||||
|
||||
urlpatterns = [
|
||||
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'^notification/(?P<notif_id>[0-9]+)$', notification, name='notification'),
|
||||
|
||||
|
@ -66,6 +66,7 @@ def exif_auto_rotate(image):
|
||||
return image
|
||||
|
||||
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'<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)
|
||||
@ -93,8 +94,10 @@ def doku_to_markdown(text):
|
||||
|
||||
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'![\3](\1 "\3")', text) # Images
|
||||
text = re.sub(r'\[\[(.*?)\|(.*?)\]\]', r'[\2](\1)', text) # Links
|
||||
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'###(\d*?)###', r'[[[\1]]]', text) # Progress bar
|
||||
@ -117,14 +120,58 @@ def doku_to_markdown(text):
|
||||
quote_level += 1
|
||||
try:
|
||||
new_text.append("> " * quote_level + "##### " + quote.group(2))
|
||||
line = line.replace(quote.group(0), '')
|
||||
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:
|
||||
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:
|
||||
new_text.append(line)
|
||||
|
||||
|
@ -37,7 +37,7 @@ from itertools import chain
|
||||
from haystack.query import SearchQuerySet
|
||||
|
||||
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
|
||||
|
||||
def index(request, context=None):
|
||||
@ -98,17 +98,20 @@ def search_json(request):
|
||||
}
|
||||
return JsonResponse(result)
|
||||
|
||||
class DokuToMarkdownView(TemplateView):
|
||||
template_name = "core/doku_to_markdown.jinja"
|
||||
class ToMarkdownView(TemplateView):
|
||||
template_name = "core/to_markdown.jinja"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.text = request.POST['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)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super(DokuToMarkdownView, self).get_context_data(**kwargs)
|
||||
kwargs = super(ToMarkdownView, self).get_context_data(**kwargs)
|
||||
try:
|
||||
kwargs['text'] = self.text
|
||||
kwargs['text_md'] = self.text_md
|
||||
|
19
counter/migrations/0012_auto_20170515_2202.py
Normal file
19
counter/migrations/0012_auto_20170515_2202.py
Normal 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),
|
||||
),
|
||||
]
|
@ -431,7 +431,7 @@ class Permanency(models.Model):
|
||||
user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
|
||||
counter = models.ForeignKey(Counter, related_name="permanencies", verbose_name=_("counter"))
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
@ -29,3 +29,4 @@ from forum.models import *
|
||||
admin.site.register(Forum)
|
||||
admin.site.register(ForumTopic)
|
||||
admin.site.register(ForumMessage)
|
||||
admin.site.register(ForumUserInfo)
|
||||
|
63
forum/migrations/0004_auto_20170531_1949.py
Normal file
63
forum/migrations/0004_auto_20170531_1949.py
Normal 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),
|
||||
),
|
||||
]
|
125
forum/models.py
125
forum/models.py
@ -46,8 +46,9 @@ class Forum(models.Model):
|
||||
edit_groups allows to put any group as a forum admin
|
||||
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)
|
||||
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)
|
||||
parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
|
||||
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,
|
||||
default=[settings.SITH_GROUP_PUBLIC_ID])
|
||||
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:
|
||||
ordering = ['number']
|
||||
@ -72,6 +76,28 @@ class Forum(models.Model):
|
||||
if 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):
|
||||
children = self.children.all()
|
||||
for c in children:
|
||||
@ -86,10 +112,19 @@ class Forum(models.Model):
|
||||
self.view_groups = self.parent.view_groups.all()
|
||||
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):
|
||||
if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
|
||||
return True
|
||||
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:
|
||||
return m.role > settings.SITH_MAXIMUM_FREE_ROLE
|
||||
return False
|
||||
@ -122,9 +157,9 @@ class Forum(models.Model):
|
||||
p = p.parent
|
||||
return l
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
def topic_number(self):
|
||||
return self.get_topic_number()
|
||||
return self._topic_number
|
||||
|
||||
def get_topic_number(self):
|
||||
number = self.topics.all().count()
|
||||
@ -134,31 +169,31 @@ class Forum(models.Model):
|
||||
|
||||
@cached_property
|
||||
def last_message(self):
|
||||
return self.get_last_message()
|
||||
return self._last_message
|
||||
|
||||
forum_list = {} # Class variable used for cache purpose
|
||||
def get_last_message(self):
|
||||
last_msg = None
|
||||
for m in ForumMessage.objects.select_related('topic__forum').order_by('-id'):
|
||||
if m.topic.forum.id in Forum.forum_list.keys(): # The object is already in Python's memory,
|
||||
# so there's no need to query it again
|
||||
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
|
||||
def get_children_list(self):
|
||||
l = [self.id]
|
||||
for c in self.children.all():
|
||||
l.append(c.id)
|
||||
l += c.get_children_list()
|
||||
return l
|
||||
|
||||
class ForumTopic(models.Model):
|
||||
forum = models.ForeignKey(Forum, related_name='topics')
|
||||
author = models.ForeignKey(User, related_name='forum_topics')
|
||||
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:
|
||||
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):
|
||||
return self.forum.is_owned_by(user)
|
||||
@ -184,13 +219,11 @@ class ForumTopic(models.Model):
|
||||
|
||||
@cached_property
|
||||
def last_message(self):
|
||||
for msg in self.messages.order_by('id').select_related('author').order_by('-id').all():
|
||||
if not msg.deleted:
|
||||
return msg
|
||||
return self._last_message
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def title(self):
|
||||
return self.messages.order_by('date').first().title
|
||||
return self._title
|
||||
|
||||
class ForumMessage(models.Model):
|
||||
"""
|
||||
@ -202,12 +235,29 @@ class ForumMessage(models.Model):
|
||||
message = models.TextField(_("message"), default="")
|
||||
date = models.DateTimeField(_('date'), default=timezone.now)
|
||||
readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
|
||||
_deleted = models.BooleanField(_('is deleted'), default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ['id']
|
||||
ordering = ['-date']
|
||||
|
||||
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
|
||||
# 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)
|
||||
|
||||
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):
|
||||
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
|
||||
|
||||
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)
|
||||
|
||||
def get_page(self):
|
||||
@ -230,16 +283,13 @@ class ForumMessage(models.Model):
|
||||
|
||||
def mark_as_read(self, user):
|
||||
try: # Need the try/except because of AnonymousUser
|
||||
if not self.is_read(user):
|
||||
self.readers.add(user)
|
||||
except: pass
|
||||
|
||||
def is_read(self, user):
|
||||
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):
|
||||
meta = self.metas.exclude(action="EDIT").order_by('-date').first()
|
||||
if meta:
|
||||
@ -258,13 +308,22 @@ class ForumMessageMeta(models.Model):
|
||||
date = models.DateTimeField(_('date'), default=timezone.now)
|
||||
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):
|
||||
"""
|
||||
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
|
||||
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,
|
||||
month=1, day=1, tzinfo=pytz.UTC))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
|
@ -6,13 +6,14 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<div>
|
||||
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
|
||||
{% for f in forum.get_parent_list()|reverse %}
|
||||
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
|
||||
{% endfor %}
|
||||
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="forum">
|
||||
<h3>{{ forum.name }}</h3>
|
||||
<p>
|
||||
{% 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>
|
||||
{% 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) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@ -58,7 +60,13 @@
|
||||
{% for t in topics %}
|
||||
{{ display_topic(t, user) }}
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -6,18 +6,28 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<p>
|
||||
<a href="{{ url('forum:main') }}">Forum</a> >
|
||||
</p>
|
||||
</p>
|
||||
<div id="forum">
|
||||
<h3>{% trans %}Forum{% endtrans %}</h3>
|
||||
<h4>{% trans %}Last unread messages{% endtrans %}</h4>
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
{% for t in forumtopic_list %}
|
||||
{% for t in page_obj.object_list %}
|
||||
{{ display_topic(t, user, True) }}
|
||||
{% 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 %}
|
||||
|
||||
|
||||
|
@ -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) %}
|
||||
<div class="forum {% if forum.is_category %}category{% endif %}">
|
||||
{% macro display_forum(forum, user, is_root=False) %}
|
||||
<div class="forum {% if is_root %}category{% endif %}">
|
||||
<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) }}">
|
||||
{% else %}
|
||||
<div class="ib w_big">
|
||||
{% endif %}
|
||||
<h5>{{ forum.name }}</h5>
|
||||
<div class="title">{{ forum.name }}</div>
|
||||
<p>{{ forum.description }}</p>
|
||||
{% if not forum.is_category %}
|
||||
{% if not is_root %}
|
||||
</a>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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:delete_forum', forum_id=forum.id) }}">{% trans %}Delete{% endtrans %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not forum.is_category %}
|
||||
{% if not is_root %}
|
||||
<div class="ib w_small">
|
||||
<div class="ib w_medium">
|
||||
<p class="ib w_medium">
|
||||
{{ forum.topic_number }}
|
||||
</div>
|
||||
<div class="ib w_medium" style="font-size: x-small; text-align: center">
|
||||
</p>
|
||||
<div class="ib w_medium last_message" style="font-size: x-small; text-align: center">
|
||||
{% 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() }}">
|
||||
<date>
|
||||
{{ 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 }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -48,33 +54,33 @@
|
||||
{% else %}
|
||||
<a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}">
|
||||
{% endif %}
|
||||
<h5>{{ topic.title }}</h5>
|
||||
<div class="title">{{ topic.title or topic.messages.first().title }}</div>
|
||||
<p>{{ topic.description }}</p>
|
||||
</a>
|
||||
{% 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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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;">
|
||||
{{ user_profile_link(topic.author) }}
|
||||
<p class="ib w_medium" style="text-align: center;">
|
||||
{{ user_profile_link_short_name(topic.author) }}
|
||||
</p>
|
||||
<p class="ib w_medium" style="text-align: center;">
|
||||
{{ topic._message_number }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ib w_medium" style="text-align: center;">
|
||||
{{ topic.messages.count() }}
|
||||
</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 %}
|
||||
{% 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() }}">
|
||||
{{ 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@ -92,12 +98,19 @@
|
||||
<strong><a href="{{ m.author.get_absolute_url() }}">{{ m.author.get_short_name() }}</a></strong>
|
||||
</div>
|
||||
<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%;">
|
||||
{% if m.title %}
|
||||
<h5>{{ m.title }}</h5>
|
||||
{% endif %}
|
||||
<div class="msg_header">
|
||||
<div class="ib w_big title">
|
||||
<a href="{{ m.get_absolute_url() }}">
|
||||
{{ m.date|localtime|date(DATETIME_FORMAT) }}
|
||||
{{ m.date|localtime|time(DATETIME_FORMAT) }}
|
||||
{%- if m.title -%}
|
||||
- {{ m.title }}
|
||||
{%- endif -%}
|
||||
</a>
|
||||
</div>
|
||||
<div style="display: inline-block; width: 25%;">
|
||||
<div class="ib w_small">
|
||||
<span><a href="{{ m.get_absolute_url() }}">#{{ m.id }}</a></span>
|
||||
<br/>
|
||||
<span><a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}">
|
||||
{% trans %}Reply as quote{% endtrans %}</a></span>
|
||||
{% if user.can_edit(m) %}
|
||||
@ -110,8 +123,7 @@
|
||||
<span> <a href="{{ url('forum:delete_message', message_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br/>
|
||||
<span>{{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
@ -121,7 +133,7 @@
|
||||
<ul class="msg_meta">
|
||||
{% for meta in m.metas.select_related('user').order_by('id') %}
|
||||
<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 %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}</li>
|
||||
{% endfor %}
|
||||
|
@ -7,9 +7,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >
|
||||
</p>
|
||||
<div id="forum">
|
||||
<h3>{% trans %}Forum{% endtrans %}</h3>
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
{% 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 %}
|
||||
<div style="padding: 4px; margin: 4px">
|
||||
{{ display_forum(f, user) }}
|
||||
<div>
|
||||
{{ display_forum(f, user, True) }}
|
||||
{% for c in f.children.all() %}
|
||||
{{ display_forum(c, user) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -35,6 +35,7 @@
|
||||
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
|
||||
</p>
|
||||
<h3>{{ topic.title }}</h3>
|
||||
<div id="forum">
|
||||
<p>{{ topic.description }}</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>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -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]+)/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'^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]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'),
|
||||
url(r'^message/(?P<message_id>[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'),
|
||||
|
@ -41,7 +41,7 @@ from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMi
|
||||
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
|
||||
|
||||
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"
|
||||
|
||||
class ForumMarkAllAsRead(RedirectView):
|
||||
@ -53,17 +53,23 @@ class ForumMarkAllAsRead(RedirectView):
|
||||
fi = request.user.forum_infos
|
||||
fi.last_read_date = timezone.now()
|
||||
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
|
||||
return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs)
|
||||
|
||||
class ForumLastUnread(ListView):
|
||||
model = ForumTopic
|
||||
template_name = "forum/last_unread.jinja"
|
||||
paginate_by = settings.SITH_FORUM_PAGE_LENGTH / 2
|
||||
|
||||
def get_queryset(self):
|
||||
l = ForumMessage.objects.exclude(readers=self.request.user).filter(
|
||||
date__gt=self.request.user.forum_infos.last_read_date).values_list('topic') # TODO try to do better
|
||||
return self.model.objects.filter(id__in=l).annotate(models.Max('messages__date')).order_by('-messages__date__max').select_related('author')
|
||||
topic_list = self.model.objects.filter(_last_message__date__gt=self.request.user.forum_infos.last_read_date)\
|
||||
.exclude(_last_message__readers=self.request.user)\
|
||||
.order_by('-_last_message__date')\
|
||||
.select_related('_last_message__author', 'author')\
|
||||
.prefetch_related('forum__edit_groups')
|
||||
return topic_list
|
||||
|
||||
class ForumForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@ -117,7 +123,18 @@ class ForumDetailView(CanViewMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **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
|
||||
|
||||
class TopicForm(forms.ModelForm):
|
||||
@ -164,7 +181,8 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
|
||||
kwargs['first_unread_message_id'] = msg.id
|
||||
except:
|
||||
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)
|
||||
page = self.request.GET.get('page')
|
||||
try:
|
||||
@ -175,6 +193,15 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
|
||||
kwargs["msgs"] = paginator.page(paginator.num_pages)
|
||||
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):
|
||||
model = ForumMessage
|
||||
fields = ['title', 'message']
|
||||
|
File diff suppressed because it is too large
Load Diff
155
migrate.py
155
migrate.py
@ -45,12 +45,14 @@ from django.core.files import File
|
||||
|
||||
|
||||
from core.models import User, SithFile
|
||||
from core.utils import doku_to_markdown, bbcode_to_markdown
|
||||
from club.models import Club, Membership
|
||||
from counter.models import Customer, Counter, Selling, Refilling, Product, ProductType, Permanency, Eticket
|
||||
from subscription.models import Subscription
|
||||
from eboutic.models import Invoice, InvoiceItem
|
||||
from accounting.models import BankAccount, ClubAccount, GeneralJournal, Operation, AccountingType, Company, SimplifiedAccountingType, Label
|
||||
from sas.models import Album, Picture, PeoplePictureRelation
|
||||
from forum.models import Forum, ForumTopic, ForumMessage, ForumMessageMeta, ForumUserInfo
|
||||
|
||||
db = MySQLdb.connect(**settings.OLD_MYSQL_INFOS)
|
||||
start = datetime.datetime.now()
|
||||
@ -1181,6 +1183,155 @@ def reset_sas_moderators():
|
||||
except Exception as 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():
|
||||
print("Start at %s" % start)
|
||||
# Core
|
||||
@ -1199,7 +1350,9 @@ def main():
|
||||
# reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter')
|
||||
# migrate_sas()
|
||||
# reset_index('core', 'sas')
|
||||
reset_sas_moderators()
|
||||
# reset_sas_moderators()
|
||||
migrate_forum()
|
||||
reset_index('forum')
|
||||
end = datetime.datetime.now()
|
||||
print("End at %s" % end)
|
||||
print("Running time: %s" % (end-start))
|
||||
|
@ -52,7 +52,6 @@ class Subscription(models.Model):
|
||||
subscription_end = models.DateField(_('subscription end'))
|
||||
payment_method = models.CharField(_('payment method'),
|
||||
max_length=255,
|
||||
help_text=_('Eboutic is reserved to specific users. In doubt, don\'t use it.'),
|
||||
choices=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)
|
||||
location = models.CharField(choices=settings.SITH_SUBSCRIPTION_LOCATIONS,
|
||||
max_length=20, verbose_name=_('location'))
|
||||
|
@ -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.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{
|
||||
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><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user