mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
commit
59dfcbd567
@ -28,7 +28,7 @@ already.
|
||||
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)
|
||||
|
@ -122,7 +122,7 @@ class Club(models.Model):
|
||||
sub = User.objects.filter(pk=user.pk).first()
|
||||
if sub is None:
|
||||
return False
|
||||
return sub.is_subscribed()
|
||||
return sub.is_subscribed
|
||||
|
||||
def get_membership_for(self, user):
|
||||
"""
|
||||
@ -151,7 +151,7 @@ class Membership(models.Model):
|
||||
|
||||
def clean(self):
|
||||
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'))
|
||||
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'))
|
||||
|
@ -18,6 +18,7 @@ from subscription.models import Subscription
|
||||
from counter.models import Customer, ProductType, Product, Counter
|
||||
from com.models import Sith, Weekmail
|
||||
from election.models import Election, Role, Candidature, ElectionList
|
||||
from forum.models import Forum, ForumMessage, ForumTopic
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -37,7 +38,9 @@ class Command(BaseCommand):
|
||||
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__))))
|
||||
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="Communication admin").save()
|
||||
Group(name="Counter admin").save()
|
||||
@ -45,6 +48,7 @@ class Command(BaseCommand):
|
||||
Group(name="Banned from counters").save()
|
||||
Group(name="Banned to subscribe").save()
|
||||
Group(name="SAS admin").save()
|
||||
Group(name="Forum admin").save()
|
||||
self.reset_index("core", "auth")
|
||||
root = User(id=0, username='root', last_name="", first_name="Bibou",
|
||||
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.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)
|
||||
|
||||
|
159
core/markdown.py
159
core/markdown.py
@ -7,9 +7,102 @@ class SithRenderer(Renderer):
|
||||
def file_link(self, 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):
|
||||
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):
|
||||
try:
|
||||
try: # Add page:// support for links
|
||||
page = re.compile(
|
||||
r'^page://(\S*)' # page://nom_de_ma_page
|
||||
)
|
||||
@ -17,7 +110,7 @@ class SithInlineLexer(InlineLexer):
|
||||
page = match.group(1) or ""
|
||||
link = reverse('core:page', kwargs={'page_name': page})
|
||||
except: pass
|
||||
try:
|
||||
try: # Add file:// support for links
|
||||
file_link = re.compile(
|
||||
r'^file://(\d*)/?(\S*)?' # file://4000/download
|
||||
)
|
||||
@ -28,30 +121,48 @@ class SithInlineLexer(InlineLexer):
|
||||
except: pass
|
||||
return super(SithInlineLexer, self)._process_link(m, link, title)
|
||||
|
||||
# def enable_file_link(self):
|
||||
# # 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()
|
||||
renderer = SithRenderer(escape=True)
|
||||
inline = SithInlineLexer(renderer)
|
||||
|
||||
# enable the features
|
||||
# inline.enable_file_link()
|
||||
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))
|
||||
|
||||
|
@ -10,6 +10,8 @@ from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.utils.html import escape
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from datetime import datetime, timedelta, date
|
||||
@ -182,9 +184,11 @@ class User(AbstractBaseUser):
|
||||
def to_dict(self):
|
||||
return self.__dict__
|
||||
|
||||
@cached_property
|
||||
def was_subscribed(self):
|
||||
return self.subscriptions.exists()
|
||||
|
||||
@cached_property
|
||||
def is_subscribed(self):
|
||||
s = self.subscriptions.last()
|
||||
return s.is_valid_now() if s is not None else False
|
||||
@ -204,8 +208,12 @@ class User(AbstractBaseUser):
|
||||
return False
|
||||
if group_id == settings.SITH_GROUP_PUBLIC_ID:
|
||||
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
|
||||
return self.is_subscribed()
|
||||
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)]
|
||||
@ -226,25 +234,25 @@ class User(AbstractBaseUser):
|
||||
return True
|
||||
return self.groups.filter(name=group_name).exists()
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def is_root(self):
|
||||
return self.is_superuser or self.groups.filter(id=settings.SITH_GROUP_ROOT_ID).exists()
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def is_board_member(self):
|
||||
from club.models import Club
|
||||
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):
|
||||
from club.models import Club
|
||||
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):
|
||||
return self.is_in_group(settings.SITH_GROUP_BANNED_ALCOHOL_ID)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def is_banned_counter(self):
|
||||
return self.is_in_group(settings.SITH_GROUP_BANNED_COUNTER_ID)
|
||||
|
||||
@ -418,10 +426,20 @@ class User(AbstractBaseUser):
|
||||
escape(self.get_display_name()),
|
||||
)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def subscribed(self):
|
||||
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):
|
||||
def __init__(self, request):
|
||||
super(AnonymousUser, self).__init__()
|
||||
@ -652,12 +670,12 @@ class SithFile(models.Model):
|
||||
else:
|
||||
return super(SithFile, self).__getattribute__(attr)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def as_picture(self):
|
||||
from sas.models import Picture
|
||||
return Picture.objects.filter(id=self.id).first()
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def as_album(self):
|
||||
from sas.models import Album
|
||||
return Album.objects.filter(id=self.id).first()
|
||||
|
@ -11,6 +11,20 @@ a {
|
||||
}
|
||||
a:hover { color: #7FDBFF; }
|
||||
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-------------------------------*/
|
||||
#logo {
|
||||
margin-left: 5%;
|
||||
@ -189,7 +203,11 @@ ul, ol {
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
border: solid 1px black;
|
||||
}
|
||||
.edit-bar {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
@ -372,6 +390,47 @@ textarea {
|
||||
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------------------------------------*/
|
||||
.album {
|
||||
display: inline-block;
|
||||
|
@ -91,7 +91,7 @@
|
||||
<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('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:file_list') }}">{% trans %}Files{% endtrans %}</a>
|
||||
<a href="https://ae.utbm.fr/article.php?name=liens">{% trans %}Sponsors{% endtrans %}</a>
|
||||
|
@ -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 the user is member of a club, he can view the subscription state #}
|
||||
<p>
|
||||
{% if profile.is_subscribed() %}
|
||||
{% if profile.is_subscribed %}
|
||||
{% if user == profile or user.is_root or user.is_board_member %}
|
||||
{{ user_subscription(profile) }}
|
||||
{% endif %}
|
||||
|
@ -91,7 +91,7 @@
|
||||
<h4>{% trans %}Elections{% endtrans %}</h4>
|
||||
<ul>
|
||||
<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>
|
||||
{%- endif -%}
|
||||
</ul>
|
||||
|
@ -10,7 +10,7 @@ register = template.Library()
|
||||
@register.filter(is_safe=False)
|
||||
@stringfilter
|
||||
def markdown(text):
|
||||
return mark_safe(md(escape(text)))
|
||||
return mark_safe(md(text))
|
||||
|
||||
@register.filter()
|
||||
@stringfilter
|
||||
|
@ -253,8 +253,8 @@ http://git.an
|
||||
response = self.client.get(reverse('core:page', kwargs={'page_name': 'guy'}))
|
||||
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' +
|
||||
'<h1>Swag</h1>\\n<p><guy>Bibou</guy></p>\\n' +
|
||||
'<p><script>alert('Guy');</script></p>' in str(response.content))
|
||||
'<h1>Swag</h1>\\n<guy>Bibou</guy>' +
|
||||
"<script>alert(\\'Guy\\');</script>" in str(response.content))
|
||||
|
||||
#TODO: many tests on the pages:
|
||||
# - renaming a page
|
||||
|
@ -347,7 +347,7 @@ class Selling(models.Model):
|
||||
self.customer.save()
|
||||
self.is_validated = True
|
||||
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:
|
||||
sub = Subscription(
|
||||
member=u,
|
||||
|
@ -80,7 +80,7 @@ class EbouticMain(TemplateView):
|
||||
kwargs['basket'] = self.basket
|
||||
kwargs['eboutic'] = Counter.objects.filter(type="EBOUTIC").first()
|
||||
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)
|
||||
return kwargs
|
||||
|
||||
|
@ -271,7 +271,7 @@ class ElectionCreateView(CanCreateMixin, CreateView):
|
||||
template_name = 'core/create.jinja'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_subscribed():
|
||||
if not request.user.is_subscribed:
|
||||
raise PermissionDenied
|
||||
return super(ElectionCreateView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
0
forum/__init__.py
Normal file
0
forum/__init__.py
Normal file
7
forum/admin.py
Normal file
7
forum/admin.py
Normal 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)
|
61
forum/migrations/0001_initial.py
Normal file
61
forum/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
22
forum/migrations/0002_auto_20170128_1958.py
Normal file
22
forum/migrations/0002_auto_20170128_1958.py
Normal 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',
|
||||
),
|
||||
]
|
20
forum/migrations/0003_forum_owner_club.py
Normal file
20
forum/migrations/0003_forum_owner_club.py
Normal 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),
|
||||
),
|
||||
]
|
26
forum/migrations/0004_forumuserinfo.py
Normal file
26
forum/migrations/0004_forumuserinfo.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
19
forum/migrations/0005_forumuserinfo_read_messages.py
Normal file
19
forum/migrations/0005_forumuserinfo_read_messages.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 = [
|
||||
('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'),
|
||||
),
|
||||
]
|
25
forum/migrations/0006_auto_20170128_2243.py
Normal file
25
forum/migrations/0006_auto_20170128_2243.py
Normal 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'),
|
||||
),
|
||||
]
|
27
forum/migrations/0007_forummessagemeta.py
Normal file
27
forum/migrations/0007_forummessagemeta.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
0
forum/migrations/__init__.py
Normal file
0
forum/migrations/__init__.py
Normal file
217
forum/models.py
Normal file
217
forum/models.py
Normal 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))
|
||||
|
32
forum/templates/forum/forum.jinja
Normal file
32
forum/templates/forum/forum.jinja
Normal 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 %}
|
||||
|
||||
|
||||
|
24
forum/templates/forum/last_unread.jinja
Normal file
24
forum/templates/forum/last_unread.jinja
Normal 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 %}
|
||||
|
||||
|
||||
|
73
forum/templates/forum/macros.jinja
Normal file
73
forum/templates/forum/macros.jinja
Normal 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 %}
|
||||
|
33
forum/templates/forum/main.jinja
Normal file
33
forum/templates/forum/main.jinja
Normal 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 %}
|
||||
|
||||
|
||||
|
98
forum/templates/forum/topic.jinja
Normal file
98
forum/templates/forum/topic.jinja
Normal 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
3
forum/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
21
forum/urls.py
Normal file
21
forum/urls.py
Normal 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
193
forum/views.py
Normal 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)
|
||||
|
||||
|
@ -60,7 +60,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
||||
self.slot_type = request.POST['slot_type']
|
||||
if 'slot' in request.POST.keys() and request.user.is_authenticated():
|
||||
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)
|
||||
if self.slot_type == "WASHING":
|
||||
if self.check_slot(self.slot_type):
|
||||
|
@ -30,7 +30,7 @@ class Picture(SithFile):
|
||||
def can_be_viewed_by(self, user):
|
||||
# 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
|
||||
user.was_subscribed())# or user.can_view(file)
|
||||
user.was_subscribed)# or user.can_view(file)
|
||||
|
||||
def get_download_url(self):
|
||||
return reverse('sas:download', kwargs={'picture_id': self.id})
|
||||
@ -107,7 +107,7 @@ class Album(SithFile):
|
||||
def can_be_viewed_by(self, user):
|
||||
# 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
|
||||
user.was_subscribed())# or user.can_view(file)
|
||||
user.was_subscribed)# or user.can_view(file)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('sas:album', kwargs={'album_id': self.id})
|
||||
|
@ -162,7 +162,7 @@ class AlbumUploadView(CanViewMixin, DetailView, FormMixin):
|
||||
self.form = self.get_form()
|
||||
parent = SithFile.objects.filter(id=self.object.id).first()
|
||||
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():
|
||||
self.form.process(parent=parent, owner=request.user, files=files,
|
||||
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)
|
||||
parent = SithFile.objects.filter(id=self.object.id).first()
|
||||
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():
|
||||
self.form.process(parent=parent, owner=request.user, files=files,
|
||||
automodere=request.user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID))
|
||||
|
@ -60,6 +60,7 @@ INSTALLED_APPS = (
|
||||
'sas',
|
||||
'com',
|
||||
'election',
|
||||
'forum',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
@ -234,6 +235,7 @@ SITH_URL = "my.url.git.an"
|
||||
SITH_NAME = "Sith website"
|
||||
|
||||
# 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 = {
|
||||
'name': "AE",
|
||||
'unix_name': "ae",
|
||||
@ -263,13 +265,17 @@ SITH_SCHOOL_START_YEAR = 1999
|
||||
|
||||
SITH_GROUP_ROOT_ID = 1
|
||||
SITH_GROUP_PUBLIC_ID = 2
|
||||
SITH_GROUP_ACCOUNTING_ADMIN_ID = 3
|
||||
SITH_GROUP_COM_ADMIN_ID = 4
|
||||
SITH_GROUP_COUNTER_ADMIN_ID = 5
|
||||
SITH_GROUP_BANNED_ALCOHOL_ID = 6
|
||||
SITH_GROUP_BANNED_COUNTER_ID = 7
|
||||
SITH_GROUP_BANNED_SUBSCRIPTION_ID = 8
|
||||
SITH_GROUP_SAS_ADMIN_ID = 9
|
||||
SITH_GROUP_SUBSCRIBERS_ID = 3
|
||||
SITH_GROUP_OLD_SUBSCRIBERS_ID = 4
|
||||
SITH_GROUP_ACCOUNTING_ADMIN_ID = 5
|
||||
SITH_GROUP_COM_ADMIN_ID = 6
|
||||
SITH_GROUP_COUNTER_ADMIN_ID = 7
|
||||
SITH_GROUP_BANNED_ALCOHOL_ID = 8
|
||||
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_COUNTER_REFOUND_ID = 38
|
||||
|
@ -40,6 +40,7 @@ urlpatterns = [
|
||||
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'^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'^ajax_select/', include(ajax_select_urls)),
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
|
Loading…
Reference in New Issue
Block a user