Commit 38622c98 authored by Skia's avatar Skia

Merge branch 'wip' into 'master'

Forum improvements

See merge request !75
parents 2f5bd7d2 f3c1ab4a
Pipeline #1009 passed with stage
in 4 minutes and 45 seconds
# -*- 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'
# -*- 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
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:
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:
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)]
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
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)]
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
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,86 +502,132 @@ textarea {
/*------------------------------FORUM----------------------------------*/
.topic a, .forum a, .category a {
color: $black-color;
}
#forum {
a {
color: $black-color;
}
.topic a:hover, .forum a:hover, .category a:hover {
color: #424242;
text-decoration: underline;
}
a:hover {
color: #424242;
text-decoration: underline;
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 2px;
margin: 2px;
}
.topic {
border: solid $primary-neutral-color 1px;
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
}
.forum {
background: $primary-neutral-light-color;
padding: 2px;
margin: 2px;
}
.tools {
font-size: x-small;
border: none;
a {
padding: 1px;
}
}
.category {
background: $secondary-color;
}
.title {
font-size: small;
font-weight: bold;
padding: 2px;
}
.message {
padding: 2px;
margin: 2px;
background: $white-color;
&:nth-child(odd) {
.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;
}
}
h5 {
font-size: 100%;
.category {
margin-top: 5px;
background: $secondary-color;
.title {
text-transform: uppercase;
}
}
&.unread {
background: #d8e7f3;
.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_author.deleted {
background: #ffcfcf;
}
.msg_content {
&.deleted {
background: #ffefef;
.msg_content {
&.deleted {
background: #ffefef;
}
display: inline-block;
width: 80%;
vertical-align: top;
}
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_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;
.msg_header {
display: inline-block;
width: 100%;
font-size: small;
}
}
.forum_signature {
color: #C0C0C0;
border-top: 1px solid #C0C0C0;
a {
.msg_meta {
font-size: small;
list-style-type: none;
li {
padding: 1px;
margin: 1px;
}
}
.forum_signature {
color: #C0C0C0;
&:hover {
text-decoration: underline;
border-top: 1px solid #C0C0C0;
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>
{%- 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 }}
......
{% 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']
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)
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
......
# -*- 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)
# -*- 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),
),
]
This diff is collapsed.
......@@ -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 %}