mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 14:13:21 +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
|
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):
|
||||||
"""
|
"""
|
||||||
|
@ -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()
|
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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }}
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
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"))
|
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:
|
||||||
|
@ -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)
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
129
forum/models.py
129
forum/models.py
@ -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)
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
@ -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
155
migrate.py
155
migrate.py
@ -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))
|
||||||
|
@ -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'))
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user