Merge branch 'forum' into 'master'

Forum

See merge request !49
This commit is contained in:
Skia 2017-02-24 03:08:07 +01:00
commit 59dfcbd567
38 changed files with 1171 additions and 59 deletions

View File

@ -28,7 +28,7 @@ already.
You can check all of them with: You can check all of them with:
``` ```
sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev sudo apt install libmysqlclient-dev libssl-dev libjpeg-dev zlib1g-dev python3-dev libffi-dev
``` ```
The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example) The development is done with sqlite, but it is advised to set a more robust DBMS for production (Postgresql for example)

View File

@ -122,7 +122,7 @@ class Club(models.Model):
sub = User.objects.filter(pk=user.pk).first() sub = User.objects.filter(pk=user.pk).first()
if sub is None: if sub is None:
return False return False
return sub.is_subscribed() return sub.is_subscribed
def get_membership_for(self, user): def get_membership_for(self, user):
""" """
@ -151,7 +151,7 @@ class Membership(models.Model):
def clean(self): def clean(self):
sub = User.objects.filter(pk=self.user.pk).first() sub = User.objects.filter(pk=self.user.pk).first()
if sub is None or not sub.is_subscribed(): if sub is None or not sub.is_subscribed:
raise ValidationError(_('User must be subscriber to take part to a club')) raise ValidationError(_('User must be subscriber to take part to a club'))
if Membership.objects.filter(user=self.user).filter(club=self.club).filter(end_date=None).exists(): if Membership.objects.filter(user=self.user).filter(club=self.club).filter(end_date=None).exists():
raise ValidationError(_('User is already member of that club')) raise ValidationError(_('User is already member of that club'))

View File

@ -18,6 +18,7 @@ from subscription.models import Subscription
from counter.models import Customer, ProductType, Product, Counter from counter.models import Customer, ProductType, Product, Counter
from com.models import Sith, Weekmail from com.models import Sith, Weekmail
from election.models import Election, Role, Candidature, ElectionList from election.models import Election, Role, Candidature, ElectionList
from forum.models import Forum, ForumMessage, ForumTopic
class Command(BaseCommand): class Command(BaseCommand):
@ -37,7 +38,9 @@ class Command(BaseCommand):
Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save() Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save()
root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
Group(name="Root").save() Group(name="Root").save()
Group(name="Not registered users").save() Group(name="Public").save()
Group(name="Subscribers").save()
Group(name="Old subscribers").save()
Group(name="Accounting admin").save() Group(name="Accounting admin").save()
Group(name="Communication admin").save() Group(name="Communication admin").save()
Group(name="Counter admin").save() Group(name="Counter admin").save()
@ -45,6 +48,7 @@ class Command(BaseCommand):
Group(name="Banned from counters").save() Group(name="Banned from counters").save()
Group(name="Banned to subscribe").save() Group(name="Banned to subscribe").save()
Group(name="SAS admin").save() Group(name="SAS admin").save()
Group(name="Forum admin").save()
self.reset_index("core", "auth") self.reset_index("core", "auth")
root = User(id=0, username='root', last_name="", first_name="Bibou", root = User(id=0, username='root', last_name="", first_name="Bibou",
email="ae.info@utbm.fr", email="ae.info@utbm.fr",
@ -429,3 +433,15 @@ Welcome to the wiki page!
cand = Candidature(role=pres, user=sli, election_list=listeT, program="En fait j'aime pas l'info, je voulais faire GMC") cand = Candidature(role=pres, user=sli, election_list=listeT, program="En fait j'aime pas l'info, je voulais faire GMC")
cand.save() cand.save()
# Forum
room = Forum(name="Salon de discussions", description="Pour causer de tout", is_category=True)
room.save()
Forum(name="AE", description="Réservé au bureau AE", parent=room).save()
Forum(name="BdF", description="Réservé au bureau BdF", parent=room).save()
hall = Forum(name="Hall de discussions", description="Pour toutes les discussions", parent=room)
hall.save()
various = Forum(name="Divers", description="Pour causer de rien", is_category=True)
various.save()
Forum(name="Promos", description="Réservé aux Promos", parent=various).save()
ForumTopic(forum=hall)

View File

@ -7,9 +7,102 @@ class SithRenderer(Renderer):
def file_link(self, id, suffix): def file_link(self, id, suffix):
return reverse('core:file_detail', kwargs={'file_id': id}) + suffix return reverse('core:file_detail', kwargs={'file_id': id}) + suffix
def exposant(self, text):
return """<sup>%s</sup>""" % text
def indice(self, text):
return """<sub>%s</sub>""" % text
def underline(self, text):
return """<span class="underline">%s</span>""" % text
class SithInlineGrammar(InlineGrammar):
double_emphasis = re.compile(
r'^\*{2}([\s\S]+?)\*{2}(?!\*)' # **word**
)
emphasis = re.compile(
r'^\*((?:\*\*|[^\*])+?)\*(?!\*)' # *word*
)
underline = re.compile(
r'^_{2}([\s\S]+?)_{2}(?!_)' # __word__
)
exposant = re.compile( # FIXME Does not work for now
r'^\^([\s\S]+?)\^' # ^text^
# r'|' # FIXME doesn't properly works like this
# r'^\^(\S+)' # ^word
)
indice = re.compile(
r'^_([\s\S]+?)_' # _text_ (^` hack, because no other solution were found :/ this sadly prevent code in indices)
# r'|' # FIXME doesn't properly works like this
# r'^_(\S+)' # _word
)
class SithInlineLexer(InlineLexer): class SithInlineLexer(InlineLexer):
grammar_class = SithInlineGrammar
default_rules = [
'escape',
'inline_html',
'autolink',
'url',
'footnote',
'link',
'reflink',
'nolink',
'exposant',
'double_emphasis',
'emphasis',
'underline',
'indice',
'code',
'linebreak',
'strikethrough',
'text',
]
inline_html_rules = [
'escape',
'autolink',
'url',
'link',
'reflink',
'nolink',
'exposant',
'double_emphasis',
'emphasis',
'underline',
'indice',
'code',
'linebreak',
'strikethrough',
'text',
]
def output_underline(self, m):
text = m.group(1)
return self.renderer.underline(text)
def output_exposant(self, m):
text = m.group(1)
return self.renderer.exposant(text)
def output_indice(self, m):
text = m.group(1)
return self.renderer.indice(text)
# Double emphasis rule changed
def output_double_emphasis(self, m):
text = m.group(1)
text = self.output(text)
return self.renderer.double_emphasis(text)
# Emphasis rule changed
def output_emphasis(self, m):
text = m.group(1)
text = self.output(text)
return self.renderer.emphasis(text)
def _process_link(self, m, link, title=None): def _process_link(self, m, link, title=None):
try: try: # Add page:// support for links
page = re.compile( page = re.compile(
r'^page://(\S*)' # page://nom_de_ma_page r'^page://(\S*)' # page://nom_de_ma_page
) )
@ -17,7 +110,7 @@ class SithInlineLexer(InlineLexer):
page = match.group(1) or "" page = match.group(1) or ""
link = reverse('core:page', kwargs={'page_name': page}) link = reverse('core:page', kwargs={'page_name': page})
except: pass except: pass
try: try: # Add file:// support for links
file_link = re.compile( file_link = re.compile(
r'^file://(\d*)/?(\S*)?' # file://4000/download r'^file://(\d*)/?(\S*)?' # file://4000/download
) )
@ -28,30 +121,48 @@ class SithInlineLexer(InlineLexer):
except: pass except: pass
return super(SithInlineLexer, self)._process_link(m, link, title) return super(SithInlineLexer, self)._process_link(m, link, title)
# def enable_file_link(self): renderer = SithRenderer(escape=True)
# # add file_link rules
# self.rules.file_link = re.compile(
# r'dfile://(\d*)/?(\S*)?' # dfile://4000/download
# )
# # Add file_link parser to default rules
# # you can insert it some place you like
# # but place matters, maybe 2 is not good
# self.default_rules.insert(0, 'file_link')
# def output_file_link(self, m):
# id = m.group(1)
# suffix = m.group(2) or ""
# # you can create an custom render
# # you can also return the html if you like
# # return directly html like this:
# # return reverse('core:file_detail', kwargs={'file_id': id}) + suffix
# return self.renderer.file_link(id, suffix)
renderer = SithRenderer()
inline = SithInlineLexer(renderer) inline = SithInlineLexer(renderer)
# enable the features
# inline.enable_file_link()
markdown = Markdown(renderer, inline=inline) markdown = Markdown(renderer, inline=inline)
if __name__ == "__main__":
print(markdown.inline.default_rules)
print(markdown.inline.inline_html_rules)
text = """
## Basique
* Mettre le texte en **gras** : `**texte**`
* Mettre le texte en *italique* : `*texte*`
* __Souligner__ le texte : `__texte__`
* ~~Barrer du texte~~ : `~~texte~~`
* Mettre ^du texte^ en ^exposant^ : `^mot` ou `^texte^`
* _Mettre du texte_ en _indice_ : `_mot` ou `_texte_`
* Pied de page [^en pied de page]
## Blocs de citations
Un bloc de citation se crée ainsi :
```
> Ceci est
> un bloc de
> citation
```
> Ceci est
> un bloc de
> citation
Il est possible d'intégrer de la syntaxe Markdown-AE dans un tel bloc.
Petit *test* _sur_ ^une^ **seule** ^ligne pour voir^
"""
print(markdown(text))

View File

@ -10,6 +10,8 @@ from django.conf import settings
from django.db import transaction from django.db import transaction
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.utils.html import escape from django.utils.html import escape
from django.utils.functional import cached_property
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
@ -182,9 +184,11 @@ class User(AbstractBaseUser):
def to_dict(self): def to_dict(self):
return self.__dict__ return self.__dict__
@cached_property
def was_subscribed(self): def was_subscribed(self):
return self.subscriptions.exists() return self.subscriptions.exists()
@cached_property
def is_subscribed(self): def is_subscribed(self):
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
@ -204,8 +208,12 @@ class User(AbstractBaseUser):
return False return False
if group_id == settings.SITH_GROUP_PUBLIC_ID: if group_id == settings.SITH_GROUP_PUBLIC_ID:
return True return True
if group_id == settings.SITH_GROUP_SUBSCRIBERS_ID:
return self.is_subscribed
if group_id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:
return self.was_subscribed
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 from club.models import Club
name = group_name[:-len(settings.SITH_BOARD_SUFFIX)] name = group_name[:-len(settings.SITH_BOARD_SUFFIX)]
@ -226,25 +234,25 @@ class User(AbstractBaseUser):
return True return True
return self.groups.filter(name=group_name).exists() return self.groups.filter(name=group_name).exists()
@property @cached_property
def is_root(self): def is_root(self):
return self.is_superuser or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists() return self.is_superuser or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists()
@property @cached_property
def is_board_member(self): def is_board_member(self):
from club.models import Club from club.models import Club
return Club.objects.filter(unix_name=settings.SITH_MAIN_CLUB['unix_name']).first().get_membership_for(self) return Club.objects.filter(unix_name=settings.SITH_MAIN_CLUB['unix_name']).first().get_membership_for(self)
@property @cached_property
def is_launderette_manager(self): def is_launderette_manager(self):
from club.models import Club from club.models import Club
return Club.objects.filter(unix_name=settings.SITH_LAUNDERETTE_MANAGER['unix_name']).first().get_membership_for(self) return Club.objects.filter(unix_name=settings.SITH_LAUNDERETTE_MANAGER['unix_name']).first().get_membership_for(self)
@property @cached_property
def is_banned_alcohol(self): def is_banned_alcohol(self):
return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID) return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID)
@property @cached_property
def is_banned_counter(self): def is_banned_counter(self):
return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID) return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID)
@ -418,10 +426,20 @@ class User(AbstractBaseUser):
escape(self.get_display_name()), escape(self.get_display_name()),
) )
@property @cached_property
def subscribed(self): def subscribed(self):
return self.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP) return self.is_in_group(settings.SITH_MAIN_MEMBERS_GROUP)
@cached_property
def forum_infos(self):
try:
return self._forum_infos
except:
from forum.models import ForumUserInfo
infos = ForumUserInfo(user=self)
infos.save()
return infos
class AnonymousUser(AuthAnonymousUser): class AnonymousUser(AuthAnonymousUser):
def __init__(self, request): def __init__(self, request):
super(AnonymousUser, self).__init__() super(AnonymousUser, self).__init__()
@ -652,12 +670,12 @@ class SithFile(models.Model):
else: else:
return super(SithFile, self).__getattribute__(attr) return super(SithFile, self).__getattribute__(attr)
@property @cached_property
def as_picture(self): def as_picture(self):
from sas.models import Picture from sas.models import Picture
return Picture.objects.filter(id=self.id).first() return Picture.objects.filter(id=self.id).first()
@property @cached_property
def as_album(self): def as_album(self):
from sas.models import Album from sas.models import Album
return Album.objects.filter(id=self.id).first() return Album.objects.filter(id=self.id).first()

View File

@ -11,6 +11,20 @@ a {
} }
a:hover { color: #7FDBFF; } a:hover { color: #7FDBFF; }
a:active { color: #007BE6; } a:active { color: #007BE6; }
.ib {
display: inline-block;
padding: 2px;
margin: 2px;
}
.w_big {
width: 75%;
}
.w_medium {
width: 45%;
}
.w_small {
width: 20%;
}
/*--------------------------------HEADER-------------------------------*/ /*--------------------------------HEADER-------------------------------*/
#logo { #logo {
margin-left: 5%; margin-left: 5%;
@ -189,7 +203,11 @@ ul, ol {
code { code {
font-family: monospace; font-family: monospace;
} }
blockquote {
margin: 10px;
padding: 5px;
border: solid 1px black;
}
.edit-bar { .edit-bar {
display: block; display: block;
margin: 4px; margin: 4px;
@ -372,6 +390,47 @@ textarea {
display: inline; display: inline;
} }
/*------------------------------FORUM----------------------------------*/
.topic a, .forum a, .category a {
color: black;
}
.topic a:hover, .forum a:hover, .category a:hover {
color: #424242;
text-decoration: underline;
}
.topic {
border: solid skyblue 1px;
padding: 2px;
margin: 2px;
}
.forum {
background: lightblue;
padding: 2px;
margin: 2px;
}
.category {
background: skyblue;
}
.message {
padding: 2px;
margin: 2px;
background: skyblue;
}
.unread {
background: #e6ddad;
}
.message h5 {
font-size: 100%;
}
.msg_author {
display: inline-block;
width: 19%;
text-align: center;
}
.msg_author img {
max-width: 80%;
margin: 0px auto;
}
/*------------------------------SAS------------------------------------*/ /*------------------------------SAS------------------------------------*/
.album { .album {
display: inline-block; display: inline-block;

View File

@ -91,7 +91,7 @@
<a href="https://ae.utbm.fr/matmatronch/">{% trans %}Matmatronch{% endtrans %}</a> <a href="https://ae.utbm.fr/matmatronch/">{% trans %}Matmatronch{% endtrans %}</a>
<a href="{{ url('core:page', page_name="Index") }}">{% trans %}Wiki{% endtrans %}</a> <a href="{{ url('core:page', page_name="Index") }}">{% trans %}Wiki{% endtrans %}</a>
<a href="{{ url('sas:main') }}">{% trans %}SAS{% endtrans %}</a> <a href="{{ url('sas:main') }}">{% trans %}SAS{% endtrans %}</a>
<a href="https://ae.utbm.fr/forum2/">{% trans %}Forum{% endtrans %}</a> <a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
<a href="{{ url('core:page', "services") }}">{% trans %}Services{% endtrans %}</a> <a href="{{ url('core:page', "services") }}">{% trans %}Services{% endtrans %}</a>
<a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a> <a href="{{ url('core:file_list') }}">{% trans %}Files{% endtrans %}</a>
<a href="https://ae.utbm.fr/article.php?name=liens">{% trans %}Sponsors{% endtrans %}</a> <a href="https://ae.utbm.fr/article.php?name=liens">{% trans %}Sponsors{% endtrans %}</a>

View File

@ -54,7 +54,7 @@
{% if user.memberships.filter(end_date=None).exists() or user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user == profile %} {% if user.memberships.filter(end_date=None).exists() or user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user == profile %}
{# if the user is member of a club, he can view the subscription state #} {# if the user is member of a club, he can view the subscription state #}
<p> <p>
{% if profile.is_subscribed() %} {% if profile.is_subscribed %}
{% if user == profile or user.is_root or user.is_board_member %} {% if user == profile or user.is_root or user.is_board_member %}
{{ user_subscription(profile) }} {{ user_subscription(profile) }}
{% endif %} {% endif %}

View File

@ -91,7 +91,7 @@
<h4>{% trans %}Elections{% endtrans %}</h4> <h4>{% trans %}Elections{% endtrans %}</h4>
<ul> <ul>
<li><a href="{{ url('election:list') }}">{% trans %}See available elections{% endtrans %}</a></li> <li><a href="{{ url('election:list') }}">{% trans %}See available elections{% endtrans %}</a></li>
{%- if user.is_subscribed() -%} {%- if user.is_subscribed -%}
<li><a href="{{ url('election:create') }}">{% trans %}Create a new election{% endtrans %}</a></li> <li><a href="{{ url('election:create') }}">{% trans %}Create a new election{% endtrans %}</a></li>
{%- endif -%} {%- endif -%}
</ul> </ul>

View File

@ -10,7 +10,7 @@ register = template.Library()
@register.filter(is_safe=False) @register.filter(is_safe=False)
@stringfilter @stringfilter
def markdown(text): def markdown(text):
return mark_safe(md(escape(text))) return mark_safe(md(text))
@register.filter() @register.filter()
@stringfilter @stringfilter

View File

@ -253,8 +253,8 @@ http://git.an
response = self.client.get(reverse('core:page', kwargs={'page_name': 'guy'})) response = self.client.get(reverse('core:page', kwargs={'page_name': 'guy'}))
self.assertTrue(response.status_code == 200) self.assertTrue(response.status_code == 200)
self.assertTrue('<p>Guy <em>bibou</em></p>\\n<p><a href="http://git.an">http://git.an</a></p>\\n' + self.assertTrue('<p>Guy <em>bibou</em></p>\\n<p><a href="http://git.an">http://git.an</a></p>\\n' +
'<h1>Swag</h1>\\n<p>&lt;guy&gt;Bibou&lt;/guy&gt;</p>\\n' + '<h1>Swag</h1>\\n&lt;guy&gt;Bibou&lt;/guy&gt;' +
'<p>&lt;script&gt;alert(&#39;Guy&#39;);&lt;/script&gt;</p>' in str(response.content)) "&lt;script&gt;alert(\\'Guy\\');&lt;/script&gt;" in str(response.content))
#TODO: many tests on the pages: #TODO: many tests on the pages:
# - renaming a page # - renaming a page

View File

@ -347,7 +347,7 @@ class Selling(models.Model):
self.customer.save() self.customer.save()
self.is_validated = True self.is_validated = True
u = User.objects.filter(id=self.customer.user.id).first() u = User.objects.filter(id=self.customer.user.id).first()
if u.was_subscribed(): if u.was_subscribed:
if self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER: if self.product and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER:
sub = Subscription( sub = Subscription(
member=u, member=u,

View File

@ -80,7 +80,7 @@ class EbouticMain(TemplateView):
kwargs['basket'] = self.basket kwargs['basket'] = self.basket
kwargs['eboutic'] = Counter.objects.filter(type="EBOUTIC").first() kwargs['eboutic'] = Counter.objects.filter(type="EBOUTIC").first()
kwargs['categories'] = ProductType.objects.all() kwargs['categories'] = ProductType.objects.all()
if not self.request.user.was_subscribed(): if not self.request.user.was_subscribed:
kwargs['categories'] = kwargs['categories'].exclude(id=settings.SITH_PRODUCTTYPE_SUBSCRIPTION) kwargs['categories'] = kwargs['categories'].exclude(id=settings.SITH_PRODUCTTYPE_SUBSCRIPTION)
return kwargs return kwargs

View File

@ -271,7 +271,7 @@ class ElectionCreateView(CanCreateMixin, CreateView):
template_name = 'core/create.jinja' template_name = 'core/create.jinja'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_subscribed(): if not request.user.is_subscribed:
raise PermissionDenied raise PermissionDenied
return super(ElectionCreateView, self).dispatch(request, *args, **kwargs) return super(ElectionCreateView, self).dispatch(request, *args, **kwargs)

0
forum/__init__.py Normal file
View File

7
forum/admin.py Normal file
View File

@ -0,0 +1,7 @@
from django.contrib import admin
from forum.models import *
admin.site.register(Forum)
admin.site.register(ForumTopic)
admin.site.register(ForumMessage)

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0019_preferences_receive_weekmail'),
]
operations = [
migrations.CreateModel(
name='Forum',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
('name', models.CharField(verbose_name='name', max_length=64)),
('description', models.CharField(default='', verbose_name='description', max_length=256)),
('is_category', models.BooleanField(default=False, verbose_name='is a category')),
('edit_groups', models.ManyToManyField(default=[4], related_name='editable_forums', blank=True, to='core.Group')),
('owner_group', models.ForeignKey(default=12, related_name='owned_forums', to='core.Group')),
('parent', models.ForeignKey(null=True, related_name='children', blank=True, to='forum.Forum')),
('view_groups', models.ManyToManyField(default=[2], related_name='viewable_forums', blank=True, to='core.Group')),
],
),
migrations.CreateModel(
name='ForumMessage',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
('title', models.CharField(default='', verbose_name='title', blank=True, max_length=64)),
('message', models.TextField(default='', verbose_name='message')),
('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date')),
('author', models.ForeignKey(related_name='forum_messages', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='ForumTopic',
fields=[
('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)),
('title', models.CharField(default='', verbose_name='title', max_length=64)),
('description', models.CharField(default='', verbose_name='description', max_length=256)),
('author', models.ForeignKey(related_name='forum_topics', to=settings.AUTH_USER_MODEL)),
('forum', models.ForeignKey(related_name='topics', to='forum.Forum')),
],
options={
'ordering': ['-id'],
},
),
migrations.AddField(
model_name='forummessage',
name='topic',
field=models.ForeignKey(related_name='messages', to='forum.ForumTopic'),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('forum', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='forum',
name='owner_group',
),
migrations.RemoveField(
model_name='forumtopic',
name='title',
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('club', '0006_auto_20161229_0040'),
('forum', '0002_auto_20170128_1958'),
]
operations = [
migrations.AddField(
model_name='forum',
name='owner_club',
field=models.ForeignKey(related_name='owned_forums', verbose_name='owner club', to='club.Club', default=1),
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
from django.utils.timezone import utc
import datetime
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('forum', '0003_forum_owner_club'),
]
operations = [
migrations.CreateModel(
name='ForumUserInfo',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('last_read_date', models.DateTimeField(verbose_name='last read date', default=datetime.datetime(1999, 1, 1, 0, 0, tzinfo=utc))),
('user', models.OneToOneField(related_name='_forum_infos', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('forum', '0004_forumuserinfo'),
]
operations = [
migrations.AddField(
model_name='forumuserinfo',
name='read_messages',
field=models.ManyToManyField(to='forum.ForumMessage', related_name='readers', verbose_name='read messages'),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('forum', '0005_forumuserinfo_read_messages'),
]
operations = [
migrations.RemoveField(
model_name='forumuserinfo',
name='read_messages',
),
migrations.AddField(
model_name='forummessage',
name='readers',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='read_messages', verbose_name='readers'),
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('forum', '0006_auto_20170128_2243'),
]
operations = [
migrations.CreateModel(
name='ForumMessageMeta',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)),
('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date')),
('action', models.CharField(max_length=16, choices=[('EDIT', 'Edit'), ('DELETE', 'Delete'), ('UNDELETE', 'Undelete')], verbose_name='action')),
('message', models.ForeignKey(related_name='metas', to='forum.ForumMessage')),
('user', models.ForeignKey(related_name='forum_message_metas', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

217
forum/models.py Normal file
View File

@ -0,0 +1,217 @@
from django.db import models
from django.core import validators
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from datetime import datetime
import pytz
from core.models import User, MetaGroup, Group, SithFile
from club.models import Club
class Forum(models.Model):
"""
The Forum class, made as a tree to allow nice tidy organization
owner_club allows club members to moderate there own topics
edit_groups allows to put any group as a forum admin
view_groups allows some groups to view a forum
"""
name = models.CharField(_('name'), max_length=64)
description = models.CharField(_('description'), max_length=256, default="")
is_category = models.BooleanField(_('is a category'), default=False)
parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
default=settings.SITH_MAIN_CLUB_ID)
edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True,
default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID])
view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
default=[settings.SITH_GROUP_PUBLIC_ID])
def clean(self):
self.check_loop()
def save(self, *args, **kwargs):
copy_rights = False
if self.id is None:
copy_rights = True
super(Forum, self).save(*args, **kwargs)
if copy_rights:
self.copy_rights()
def apply_rights_recursively(self):
children = self.children.all()
for c in children:
c.copy_rights()
c.apply_rights_recursively()
def copy_rights(self):
"""Copy, if possible, the rights of the parent folder"""
if self.parent is not None:
self.owner_club = self.parent.owner_club
self.edit_groups = self.parent.edit_groups.all()
self.view_groups = self.parent.view_groups.all()
self.save()
def is_owned_by(self, user):
if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
return True
m = self.owner_club.get_membership_for(user)
if m:
return m.role > settings.SITH_MAXIMUM_FREE_ROLE
return False
def check_loop(self):
"""Raise a validation error when a loop is found within the parent list"""
objs = []
cur = self
while cur.parent is not None:
if cur in objs:
raise ValidationError(_('You can not make loops in forums'))
objs.append(cur)
cur = cur.parent
def __str__(self):
return "%s" % (self.name)
def get_absolute_url(self):
return reverse('forum:view_forum', kwargs={'forum_id': self.id})
@cached_property
def parent_list(self):
return self.get_parent_list()
def get_parent_list(self):
l = []
p = self.parent
while p is not None:
l.append(p)
p = p.parent
return l
@cached_property
def topic_number(self):
return self.get_topic_number()
def get_topic_number(self):
number = self.topics.all().count()
for c in self.children.all():
number += c.topic_number
return number
@cached_property
def last_message(self):
return self.get_last_message()
def get_last_message(self):
last_msg = None
for m in ForumMessage.objects.select_related('topic__forum', 'author').order_by('-id'):
forum = m.topic.forum
if self in (forum.parent_list + [forum]):
return m
last_msg = m
return last_msg
class ForumTopic(models.Model):
forum = models.ForeignKey(Forum, related_name='topics')
author = models.ForeignKey(User, related_name='forum_topics')
description = models.CharField(_('description'), max_length=256, default="")
class Meta:
ordering = ['-id'] # TODO: add date message ordering
def is_owned_by(self, user):
return self.forum.is_owned_by(user) or user.id == self.author.id
def can_be_edited_by(self, user):
return user.can_edit(self.forum)
def can_be_viewed_by(self, user):
return user.can_view(self.forum)
def __str__(self):
return "%s" % (self.title)
def get_absolute_url(self):
return reverse('forum:view_topic', kwargs={'topic_id': self.id})
@property
def title(self):
return self.messages.order_by('date').first().title
class ForumMessage(models.Model):
"""
"A ForumMessage object represents a message in the forum" -- Cpt. Obvious
"""
topic = models.ForeignKey(ForumTopic, related_name='messages')
author = models.ForeignKey(User, related_name='forum_messages')
title = models.CharField(_("title"), default="", max_length=64, blank=True)
message = models.TextField(_("message"), default="")
date = models.DateTimeField(_('date'), default=timezone.now)
readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
class Meta:
ordering = ['id']
def __str__(self):
return "%s - %s" % (self.id, self.title)
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
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
def can_be_edited_by(self, user):
return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user):
return user.can_view(self.topic)
def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user)
def get_absolute_url(self):
return self.topic.get_absolute_url() + "#msg_" + str(self.id)
def mark_as_read(self, user):
self.readers.add(user)
def is_read(self, user):
return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all())
@cached_property
def deleted(self):
return self.is_deleted()
def is_deleted(self):
meta = self.metas.exclude(action="EDIT").order_by('-date').first()
if meta:
return meta.action == "DELETE"
return False
MESSAGE_META_ACTIONS = [
('EDIT', _("Message edited by")),
('DELETE', _("Message deleted by")),
('UNDELETE', _("Message undeleted by")),
]
class ForumMessageMeta(models.Model):
user = models.ForeignKey(User, related_name="forum_message_metas")
message = models.ForeignKey(ForumMessage, related_name="metas")
date = models.DateTimeField(_('date'), default=timezone.now)
action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16)
class ForumUserInfo(models.Model):
"""
This currently stores only the last date a user clicked "Mark all as read".
However, this can be extended with lot of user preferences dedicated to a
user, such as the favourite topics, the signature, and so on...
"""
user = models.OneToOneField(User, related_name="_forum_infos") # TODO: see to move that to the User class in order to reduce the number of db queries
last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
month=1, day=1, tzinfo=pytz.UTC))

View File

@ -0,0 +1,32 @@
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_forum, display_topic %}
{% block title %}
{{ forum }}
{% endblock %}
{% block content %}
<div>
<a href="{{ url('forum:main') }}">Forum</a>
{% for f in forum.get_parent_list() %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
</div>
<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) %}
<a href="{{ url('forum:new_forum') }}?parent={{ forum.id }}">New forum</a> <br/>
{% endif %}
<a href="{{ url('forum:new_topic', forum_id=forum.id) }}">New topic</a>
</p>
{% for f in forum.children.all() %}
{{ display_forum(f, user) }}
{% endfor %}
{% for t in topics %}
{{ display_topic(t, user) }}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "core/base.jinja" %}
{% from 'forum/macros.jinja' import display_topic %}
{% block title %}
{% trans %}Last unread messages{% endtrans %}
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">Forum</a> >
</p>
<h3>{% trans %}Forum{% endtrans %}</h3>
<h4>{% trans %}Last unread messages{% endtrans %}</h4>
<p>
<a class="ib" href="{{ url('forum:mark_all_as_read') }}">{% trans %}Mark all as read{% endtrans %}</a>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}Refresh{% endtrans %}</a>
</p>
{% for t in forumtopic_list %}
{{ display_topic(t, user, True) }}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,73 @@
{% from 'core/macros.jinja' import user_profile_link %}
{% macro display_forum(forum, user) %}
<div class="forum {% if forum.is_category %}category{% endif %}">
<div class="ib w_big">
{% if not forum.is_category %}
<a class="ib w_big" href="{{ url('forum:view_forum', forum_id=forum.id) }}">
{% else %}
<div class="ib w_big">
{% endif %}
<h5>{{ forum.name }}</h5>
<p>{{ forum.description }}</p>
{% if not forum.is_category %}
</a>
{% else %}
</div>
{% endif %}
{% if user.is_owner(forum) %}
<a class="ib" href="{{ url('forum:edit_forum', forum_id=forum.id) }}">Edit</a>
<a class="ib" href="{{ url('forum:delete_forum', forum_id=forum.id) }}">Delete</a>
{% endif %}
</div>
{% if not forum.is_category %}
<div class="ib w_small">
<div class="ib w_medium">
{{ forum.topic_number }}
</div>
<div class="ib w_medium">
{% if forum.last_message %}
{{ forum.last_message.author }} <br/>
{{ forum.last_message.date|date(DATETIME_FORMAT) }} {{ forum.last_message.date|time(DATETIME_FORMAT) }}
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endmacro %}
{% macro display_topic(topic, user, first_unread=False) %}
<div class="topic">
<div class="ib w_medium">
{% if first_unread %}
<a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}#first_unread">
{% else %}
<a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}">
{% endif %}
<h5>{{ topic.title }}</h5>
<p>{{ topic.description }}</p>
</a>
{% if user.is_owner(topic) %}
<div class="ib" style="text-align: center;">
<a href="{{ url('forum:edit_topic', topic_id=topic.id) }}">{% trans %}Edit{% endtrans %}</a>
</div>
{% endif %}
</div>
<div class="ib w_medium">
<div class="ib w_medium">
<div class="ib w_medium" style="text-align: center;">
{{ user_profile_link(topic.author) }}
</div>
<div class="ib w_medium" style="text-align: center;">
{{ topic.messages.count() }}
</div>
</div>
<div class="ib w_medium" style="text-align: center;">
{% set last_msg = topic.messages.order_by('id').select_related('author').last() %}
{{ user_profile_link(last_msg.author) }} <br/>
{{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }}
</div>
</div>
</div>
{% endmacro %}

View File

@ -0,0 +1,33 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_forum %}
{% block title %}
{% trans %}Forum{% endtrans %}
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">Forum</a> >
</p>
<h3>{% trans %}Forum{% endtrans %}</h3>
<p>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a>
</p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<p>
<a href="{{ url('forum:new_forum') }}">{% trans %}New forum{% endtrans %}</a>
</p>
{% endif %}
{% for f in forum_list %}
<div style="padding: 4px; margin: 4px">
{{ display_forum(f, user) }}
{% for c in f.children.all() %}
{{ display_forum(c, user) }}
{% endfor %}
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,98 @@
{% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %}
{% block title %}
{{ topic }}
{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css" media="all">
.topic {
border: solid skyblue 1px;
padding: 2px;
margin: 2px;
}
.forum {
background: lightblue;
padding: 2px;
margin: 2px;
}
.category {
background: skyblue;
}
</style>
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">Forum</a>
{% for f in topic.forum.get_parent_list() %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ topic.forum.get_absolute_url() }}">{{ topic.forum }}</a>
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
</p>
<h3>{{ topic.title }}</h3>
<p>{{ topic.description }}</p>
<p><a href="{{ url('forum:new_message', topic_id=topic.id) }}">Reply</a></p>
{% for m in topic.messages.select_related('author__profile_pict').all() %}
{% if m.id >= first_unread_message_id %}
<div id="msg_{{ m.id }}" class="message unread">
{% else %}
<div id="msg_{{ m.id }}" class="message">
{% endif %}
<div class="msg_author">
{% if m.author.profile_pict %}
<img src="{{ m.author.profile_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% else %}
<img src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% endif %}
<br/>
<strong>{{ user_profile_link(m.author) }}</strong>
</div>
<div {% if m.id == first_unread_message_id %}id="first_unread"{% endif %} style="display: inline-block; width: 80%; vertical-align: top;">
<div style="display: inline-block; width: 74%;">
{% if m.title %}
<h5>{{ m.title }}</h5>
{% endif %}
</div>
<div style="display: inline-block; width: 25%;">
<span><a href="{{ url('forum:new_message', topic_id=topic.id) }}?quote_id={{ m.id }}">
{% trans %}Reply as quote{% endtrans %}</a></span>
{% if user.can_edit(m) %}
<span> <a href="{{ url('forum:edit_message', message_id=m.id) }}">{% trans %}Edit{% endtrans %}</a></span>
{% endif %}
{% if m.can_be_moderated_by(user) %}
{% 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 %}
<br/>
<span>{{ m.date|date(DATETIME_FORMAT) }} {{ m.date|time(DATETIME_FORMAT) }}</span>
</div>
<hr>
<div>
{{ m.message|markdown }}
</div>
{% if m.can_be_moderated_by(user) %}
<ul>
{% for meta in m.metas.select_related('user').all() %}
<li>{{ meta.get_action_display() }} {{ meta.user.get_display_name() }}
{% trans %} at {% endtrans %}{{ meta.date|time(DATETIME_FORMAT) }}
{% trans %} the {% endtrans %}{{ meta.date|date(DATETIME_FORMAT)}}</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{{ m.mark_as_read(user) or "" }}
{% endfor %}
<p><a href="{{ url('forum:new_message', topic_id=topic.id) }}">Reply</a></p>
{% endblock %}

3
forum/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

21
forum/urls.py Normal file
View File

@ -0,0 +1,21 @@
from django.conf.urls import url, include
from forum.views import *
urlpatterns = [
url(r'^$', ForumMainView.as_view(), name='main'),
url(r'^new_forum$', ForumCreateView.as_view(), name='new_forum'),
url(r'^mark_all_as_read$', ForumMarkAllAsRead.as_view(), name='mark_all_as_read'),
url(r'^last_unread$', ForumLastUnread.as_view(), name='last_unread'),
url(r'^(?P<forum_id>[0-9]+)$', ForumDetailView.as_view(), name='view_forum'),
url(r'^(?P<forum_id>[0-9]+)/edit$', ForumEditView.as_view(), name='edit_forum'),
url(r'^(?P<forum_id>[0-9]+)/delete$', ForumDeleteView.as_view(), name='delete_forum'),
url(r'^(?P<forum_id>[0-9]+)/new_topic$', ForumTopicCreateView.as_view(), name='new_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]+)/new_message$', ForumMessageCreateView.as_view(), name='new_message'),
url(r'^message/(?P<message_id>[0-9]+)/edit$', ForumMessageEditView.as_view(), name='edit_message'),
url(r'^message/(?P<message_id>[0-9]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'),
url(r'^message/(?P<message_id>[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'),
]

193
forum/views.py Normal file
View File

@ -0,0 +1,193 @@
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView, RedirectView
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils import timezone
from django.conf import settings
from django import forms
from django.db import models
from django.core.exceptions import PermissionDenied
from math import inf
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
class ForumMainView(ListView):
queryset = Forum.objects.filter(parent=None)
template_name = "forum/main.jinja"
class ForumMarkAllAsRead(RedirectView):
permanent = False
url = reverse_lazy('forum:last_unread')
def get(self, request, *args, **kwargs):
try:
fi = request.user.forum_infos
fi.last_read_date = timezone.now()
fi.save()
except: pass
return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs)
class ForumLastUnread(ListView):
model = ForumTopic
template_name = "forum/last_unread.jinja"
def get_queryset(self):
l = ForumMessage.objects.exclude(readers=self.request.user).filter(
date__gt=self.request.user.forum_infos.last_read_date).values_list('topic') # TODO try to do better
return self.model.objects.filter(id__in=l).annotate(models.Max('messages__date')).order_by('-messages__date__max').select_related('author')
class ForumCreateView(CanCreateMixin, CreateView):
model = Forum
fields = ['name', 'parent', 'owner_club', 'is_category', 'edit_groups', 'view_groups']
template_name = "core/create.jinja"
def get_initial(self):
init = super(ForumCreateView, self).get_initial()
try:
parent = Forum.objects.filter(id=self.request.GET['parent']).first()
init['parent'] = parent
init['owner_club'] = parent.owner_club
except: pass
return init
class ForumEditForm(forms.ModelForm):
class Meta:
model = Forum
fields = ['name', 'parent', 'owner_club', 'is_category', 'edit_groups', 'view_groups']
recursive = forms.BooleanField(label=_("Apply rights and club owner recursively"), required=False)
class ForumEditView(CanEditPropMixin, UpdateView):
model = Forum
pk_url_kwarg = "forum_id"
form_class = ForumEditForm
template_name = "core/edit.jinja"
success_url = reverse_lazy('forum:main')
def form_valid(self, form):
ret = super(ForumEditView, self).form_valid(form)
if form.cleaned_data['recursive']:
self.object.apply_rights_recursively()
return ret
class ForumDeleteView(CanEditPropMixin, DeleteView):
model = Forum
pk_url_kwarg = "forum_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy('forum:main')
class ForumDetailView(CanViewMixin, DetailView):
model = Forum
template_name = "forum/forum.jinja"
pk_url_kwarg = "forum_id"
def get_context_data(self, **kwargs):
kwargs = super(ForumDetailView, self).get_context_data(**kwargs)
kwargs['topics'] = self.object.topics.annotate(models.Max('messages__date')).order_by('-messages__date__max')
return kwargs
class ForumTopicCreateView(CanCreateMixin, CreateView):
model = ForumMessage
fields = ['title', 'message']
template_name = "core/create.jinja"
def dispatch(self, request, *args, **kwargs):
self.forum = get_object_or_404(Forum, id=self.kwargs['forum_id'], is_category=False)
if not request.user.can_view(self.forum):
raise PermissionDenied
return super(ForumTopicCreateView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
topic = ForumTopic(title=form.instance.title, author=self.request.user, forum=self.forum)
topic.save()
form.instance.topic = topic
form.instance.author = self.request.user
return super(ForumTopicCreateView, self).form_valid(form)
class ForumTopicEditView(CanEditMixin, UpdateView):
model = ForumTopic
fields = ['forum']
pk_url_kwarg = "topic_id"
template_name = "core/edit.jinja"
class ForumTopicDetailView(CanViewMixin, DetailView):
model = ForumTopic
pk_url_kwarg = "topic_id"
template_name = "forum/topic.jinja"
context_object_name = "topic"
queryset = ForumTopic.objects.select_related('forum__parent')
def get_context_data(self, **kwargs):
kwargs = super(ForumTopicDetailView, self).get_context_data(**kwargs)
msg = self.object.messages.exclude(readers=self.request.user).filter(date__gte=self.request.user.forum_infos.last_read_date).order_by('id').first()
try:
kwargs['first_unread_message_id'] = msg.id
except:
kwargs['first_unread_message_id'] = inf
return kwargs
class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage
fields = ['title', 'message']
template_name = "core/edit.jinja"
pk_url_kwarg = "message_id"
def form_valid(self, form):
ForumMessageMeta(message=self.object, user=self.request.user, action="EDIT").save()
return super(ForumMessageEditView, self).form_valid(form)
class ForumMessageDeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
if self.object.can_be_moderated_by(self.request.user):
ForumMessageMeta(message=self.object, user=self.request.user, action="DELETE").save()
return self.object.get_absolute_url()
class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
if self.object.can_be_moderated_by(self.request.user):
ForumMessageMeta(message=self.object, user=self.request.user, action="UNDELETE").save()
return self.object.get_absolute_url()
class ForumMessageCreateView(CanCreateMixin, CreateView):
model = ForumMessage
fields = ['title', 'message']
template_name = "core/create.jinja"
def dispatch(self, request, *args, **kwargs):
self.topic = get_object_or_404(ForumTopic, id=self.kwargs['topic_id'])
if not request.user.can_view(self.topic):
raise PermissionDenied
return super(ForumMessageCreateView, self).dispatch(request, *args, **kwargs)
def get_initial(self):
init = super(ForumMessageCreateView, self).get_initial()
try:
message = ForumMessage.objects.select_related('author').filter(id=self.request.GET['quote_id']).first()
init['message'] = "> ##### %s\n" % (_("%(author)s said") % {'author': message.author.get_short_name()})
init['message'] += "\n".join([
"> " + line for line in message.message.split('\n')
])
init['message'] += "\n\n"
except Exception as e:
print(repr(e))
return init
def form_valid(self, form):
form.instance.topic = self.topic
form.instance.author = self.request.user
return super(ForumMessageCreateView, self).form_valid(form)

View File

@ -60,7 +60,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
self.slot_type = request.POST['slot_type'] self.slot_type = request.POST['slot_type']
if 'slot' in request.POST.keys() and request.user.is_authenticated(): if 'slot' in request.POST.keys() and request.user.is_authenticated():
self.subscriber = request.user self.subscriber = request.user
if self.subscriber.is_subscribed(): if self.subscriber.is_subscribed:
self.date = dateparse.parse_datetime(request.POST['slot']).replace(tzinfo=pytz.UTC) self.date = dateparse.parse_datetime(request.POST['slot']).replace(tzinfo=pytz.UTC)
if self.slot_type == "WASHING": if self.slot_type == "WASHING":
if self.check_slot(self.slot_type): if self.check_slot(self.slot_type):

View File

@ -30,7 +30,7 @@ class Picture(SithFile):
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
# file = SithFile.objects.filter(id=self.id).first() # file = SithFile.objects.filter(id=self.id).first()
return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and
user.was_subscribed())# or user.can_view(file) user.was_subscribed)# or user.can_view(file)
def get_download_url(self): def get_download_url(self):
return reverse('sas:download', kwargs={'picture_id': self.id}) return reverse('sas:download', kwargs={'picture_id': self.id})
@ -107,7 +107,7 @@ class Album(SithFile):
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
# file = SithFile.objects.filter(id=self.id).first() # file = SithFile.objects.filter(id=self.id).first()
return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and return self.can_be_edited_by(user) or (self.is_in_sas and self.is_moderated and
user.was_subscribed())# or user.can_view(file) user.was_subscribed)# or user.can_view(file)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('sas:album', kwargs={'album_id': self.id}) return reverse('sas:album', kwargs={'album_id': self.id})

View File

@ -162,7 +162,7 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
self.form = self.get_form() self.form = self.get_form()
parent = SithFile.objects.filter(id=self.object.id).first() parent = SithFile.objects.filter(id=self.object.id).first()
files = request.FILES.getlist('images') files = request.FILES.getlist('images')
if request.user.is_authenticated() and request.user.is_subscribed(): if request.user.is_authenticated() and request.user.is_subscribed:
if self.form.is_valid(): if self.form.is_valid():
self.form.process(parent=parent, owner=request.user, files=files, self.form.process(parent=parent, owner=request.user, files=files,
automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID)) automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID))
@ -194,7 +194,7 @@ class AlbumView(CanViewMixin, DetailView, FormMixin):
FileView.handle_clipboard(request, self.object) FileView.handle_clipboard(request, self.object)
parent = SithFile.objects.filter(id=self.object.id).first() parent = SithFile.objects.filter(id=self.object.id).first()
files = request.FILES.getlist('images') files = request.FILES.getlist('images')
if request.user.is_authenticated() and request.user.is_subscribed(): if request.user.is_authenticated() and request.user.is_subscribed:
if self.form.is_valid(): if self.form.is_valid():
self.form.process(parent=parent, owner=request.user, files=files, self.form.process(parent=parent, owner=request.user, files=files,
automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID)) automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID))

View File

@ -60,6 +60,7 @@ INSTALLED_APPS = (
'sas', 'sas',
'com', 'com',
'election', 'election',
'forum',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
@ -234,6 +235,7 @@ SITH_URL = "my.url.git.an"
SITH_NAME = "Sith website" SITH_NAME = "Sith website"
# AE configuration # AE configuration
SITH_MAIN_CLUB_ID = 1 # TODO: keep only that first setting, with the ID, and do the same for the other clubs
SITH_MAIN_CLUB = { SITH_MAIN_CLUB = {
'name': "AE", 'name': "AE",
'unix_name': "ae", 'unix_name': "ae",
@ -263,13 +265,17 @@ SITH_SCHOOL_START_YEAR = 1999
SITH_GROUP_ROOT_ID = 1 SITH_GROUP_ROOT_ID = 1
SITH_GROUP_PUBLIC_ID = 2 SITH_GROUP_PUBLIC_ID = 2
SITH_GROUP_ACCOUNTING_ADMIN_ID = 3 SITH_GROUP_SUBSCRIBERS_ID = 3
SITH_GROUP_COM_ADMIN_ID = 4 SITH_GROUP_OLD_SUBSCRIBERS_ID = 4
SITH_GROUP_COUNTER_ADMIN_ID = 5 SITH_GROUP_ACCOUNTING_ADMIN_ID = 5
SITH_GROUP_BANNED_ALCOHOL_ID = 6 SITH_GROUP_COM_ADMIN_ID = 6
SITH_GROUP_BANNED_COUNTER_ID = 7 SITH_GROUP_COUNTER_ADMIN_ID = 7
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 8 SITH_GROUP_BANNED_ALCOHOL_ID = 8
SITH_GROUP_SAS_ADMIN_ID = 9 SITH_GROUP_BANNED_COUNTER_ID = 9
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 10
SITH_GROUP_SAS_ADMIN_ID = 11
SITH_GROUP_FORUM_ADMIN_ID = 12
SITH_CLUB_REFOUND_ID = 89 SITH_CLUB_REFOUND_ID = 89
SITH_COUNTER_REFOUND_ID = 38 SITH_COUNTER_REFOUND_ID = 38

View File

@ -40,6 +40,7 @@ urlpatterns = [
url(r'^sas/', include('sas.urls', namespace="sas", app_name="sas")), url(r'^sas/', include('sas.urls', namespace="sas", app_name="sas")),
url(r'^api/v1/', include('api.urls', namespace="api", app_name="api")), url(r'^api/v1/', include('api.urls', namespace="api", app_name="api")),
url(r'^election/', include('election.urls', namespace="election", app_name="election")), url(r'^election/', include('election.urls', namespace="election", app_name="election")),
url(r'^forum/', include('forum.urls', namespace="forum", app_name="forum")),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^ajax_select/', include(ajax_select_urls)), url(r'^ajax_select/', include(ajax_select_urls)),
url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^i18n/', include('django.conf.urls.i18n')),