Merge branch 'wip' into 'master'

Forum improvements

See merge request !75
This commit is contained in:
Skia 2017-06-01 13:31:35 +02:00
commit 38622c98e9
28 changed files with 1016 additions and 403 deletions

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 = [
('club', '0007_auto_20170324_0917'),
]
operations = [
migrations.AlterField(
model_name='club',
name='id',
field=models.AutoField(primary_key=True, serialize=False, db_index=True),
),
]

View File

@ -39,6 +39,7 @@ class Club(models.Model):
"""
The Club class, made as a tree to allow nice tidy organization
"""
id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_('name'), max_length=64)
parent = models.ForeignKey('Club', related_name='children', null=True, blank=True)
unix_name = models.CharField(_('unix name'), max_length=30, unique=True,
@ -151,11 +152,21 @@ class Club(models.Model):
return False
return sub.is_subscribed
_memberships = {}
def get_membership_for(self, user):
"""
Returns the current membership the given user
"""
return self.members.filter(user=user.id).filter(end_date=None).first()
try:
return Club._memberships[self.id][user.id]
except:
m = self.members.filter(user=user.id).filter(end_date=None).first()
try:
Club._memberships[self.id][user.id] = m
except:
Club._memberships[self.id] = {}
Club._memberships[self.id][user.id] = m
return m
class Membership(models.Model):
"""

View File

@ -22,3 +22,4 @@
#
#
default_app_config = 'core.apps.SithConfig'

57
core/apps.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding:utf-8 -*
#
# Copyright 2017
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.apps import AppConfig
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, m2m_changed
class SithConfig(AppConfig):
name = 'core'
verbose_name = "Core app of the Sith"
def ready(self):
from core.models import User, Group
from club.models import Club, Membership
from forum.models import Forum
def clear_cached_groups(sender, **kwargs):
if kwargs['model'] == Group:
User._group_ids = {}
User._group_name = {}
def clear_cached_memberships(sender, **kwargs):
User._club_memberships = {}
Club._memberships = {}
Forum._club_memberships = {}
print("Connecting signals!")
m2m_changed.connect(clear_cached_groups, weak=False, dispatch_uid="clear_cached_groups")
post_save.connect(clear_cached_memberships, weak=False, sender=Membership, # Membership is cached
dispatch_uid="clear_cached_memberships_membership")
post_save.connect(clear_cached_memberships, weak=False, sender=Club, # Club has a cache of Membership
dispatch_uid="clear_cached_memberships_club")
post_save.connect(clear_cached_memberships, weak=False, sender=Forum, # Forum has a cache of Membership
dispatch_uid="clear_cached_memberships_forum")
# TODO: there may be a need to add more cache clearing

View File

@ -223,14 +223,25 @@ class User(AbstractBaseUser):
s = self.subscriptions.last()
return s.is_valid_now() if s is not None else False
_club_memberships = {}
_group_names = {}
_group_ids = {}
def is_in_group(self, group_name):
"""If the user is in the group passed in argument (as string or by id)"""
group_id = 0
g = None
if isinstance(group_name, int): # Handle the case where group_name is an ID
if group_name in User._group_ids.keys():
g = User._group_ids[group_name]
else:
g = Group.objects.filter(id=group_name).first()
User._group_ids[group_name] = g
else:
if group_name in User._group_names.keys():
g = User._group_names[group_name]
else:
g = Group.objects.filter(name=group_name).first()
User._group_names[group_name] = g
if g:
group_name = g.name
group_id = g.id
@ -245,18 +256,26 @@ class User(AbstractBaseUser):
if group_name == settings.SITH_MAIN_MEMBERS_GROUP: # We check the subscription if asked
return self.is_subscribed
if group_name[-len(settings.SITH_BOARD_SUFFIX):] == settings.SITH_BOARD_SUFFIX:
from club.models import Club
name = group_name[:-len(settings.SITH_BOARD_SUFFIX)]
if name in User._club_memberships.keys():
mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem:
return mem.role > settings.SITH_MAXIMUM_FREE_ROLE
return False
if group_name[-len(settings.SITH_MEMBER_SUFFIX):] == settings.SITH_MEMBER_SUFFIX:
from club.models import Club
name = group_name[:-len(settings.SITH_MEMBER_SUFFIX)]
if name in User._club_memberships.keys():
mem = User._club_memberships[name]
else:
from club.models import Club
c = Club.objects.filter(unix_name=name).first()
mem = c.get_membership_for(self)
User._club_memberships[name] = mem
if mem:
return True
return False

View File

@ -48,8 +48,8 @@ a {
.ib {
display: inline-block;
padding: 2px;
margin: 2px;
padding: 1px;
margin: 1px;
}
.w_big {
@ -57,11 +57,11 @@ a {
}
.w_medium {
width: 45%;
width: 47%;
}
.w_small {
width: 20%;
width: 23%;
}
/*--------------------------------HEADER-------------------------------*/
@ -271,11 +271,15 @@ code {
}
blockquote {
margin: 10px;
padding: 5px;
margin: 5px;
padding: 2px;
border: solid 1px $black-color;
}
blockquote h5:first-child {
font-size: 100%;
}
.edit-bar {
display: block;
margin: 4px;
@ -498,60 +502,99 @@ textarea {
/*------------------------------FORUM----------------------------------*/
.topic a, .forum a, .category a {
#forum {
a {
color: $black-color;
}
}
.topic a:hover, .forum a:hover, .category a:hover {
a:hover {
color: #424242;
text-decoration: underline;
}
}
.topic {
.topic {
border: solid $primary-neutral-color 1px;
padding: 2px;
margin: 2px;
}
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
}
.forum {
.tools {
font-size: x-small;
border: none;
a {
padding: 1px;
}
}
.title {
font-size: small;
font-weight: bold;
padding: 2px;
}
.last_message date {
white-space: nowrap;
}
.last_message span {
white-space: nowrap;
text-overflow: ellipsis;
overflow:hidden;
width: 100%;
display: block;
}
.forum {
background: $primary-neutral-light-color;
padding: 2px;
margin: 2px;
}
padding: 1px;
margin: 1px;
p {
margin: 1px;
font-size: smaller;
}
}
.category {
.category {
margin-top: 5px;
background: $secondary-color;
}
.title {
text-transform: uppercase;
}
}
.message {
padding: 2px;
margin: 2px;
.message {
padding: 1px;
margin: 1px;
background: $white-color;
&:nth-child(odd) {
background: $primary-neutral-light-color;
}
h5 {
.title {
font-size: 100%;
}
&.unread {
background: #d8e7f3;
}
}
}
.msg_author.deleted {
.msg_author.deleted {
background: #ffcfcf;
}
}
.msg_content {
.msg_content {
&.deleted {
background: #ffefef;
}
display: inline-block;
width: 80%;
vertical-align: top;
}
}
.msg_author {
.msg_author {
display: inline-block;
width: 19%;
text-align: center;
@ -560,18 +603,24 @@ textarea {
max-width: 70%;
margin: 0px auto;
}
}
}
.msg_meta {
.msg_header {
display: inline-block;
width: 100%;
font-size: small;
}
.msg_meta {
font-size: small;
list-style-type: none;
li {
padding: 2px;
margin: 2px;
padding: 1px;
margin: 1px;
}
}
}
.forum_signature {
.forum_signature {
color: #C0C0C0;
border-top: 1px solid #C0C0C0;
a {
@ -580,6 +629,7 @@ textarea {
text-decoration: underline;
}
}
}
}
/*------------------------------SAS------------------------------------*/

View File

@ -2,6 +2,10 @@
<a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_display_name() }}</a>
{%- endmacro %}
{% macro user_profile_link_short_name(user) -%}
<a href="{{ url("core:user_profile", user_id=user.id) }}">{{ user.get_short_name() }}</a>
{%- endmacro %}
{% macro user_link_with_pict(user) -%}
<a href="{{ url("core:user_profile", user_id=user.id) }}" class="mini_profile_link" >
{{ user.get_mini_item()|safe }}

View File

@ -1,12 +1,14 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Doku to Markdown{% endtrans %}
{% trans %}To Markdown{% endtrans %}
{% endblock %}
{% block content %}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="radio" name="syntax" value="doku" {% if request.POST['syntax'] != "bbcode" %}checked{% endif %} >Doku</input>
<input type="radio" name="syntax" value="bbcode" {% if request.POST['syntax'] == "bbcode" %}checked{% endif %} >BBCode</input>
<textarea name="text" id="text" rows="30" cols="80">
{{- text -}}
</textarea>

View File

@ -108,7 +108,7 @@
</ul>
<h4>{% trans %}Other tools{% endtrans %}</h4>
<ul>
<li><a href="{{ url('core:doku_to_markdown') }}">{% trans %}Convert dokuwiki syntax to Markdown{% endtrans %}</a></li>
<li><a href="{{ url('core:to_markdown') }}">{% trans %}Convert dokuwiki/BBcode syntax to Markdown{% endtrans %}</a></li>
<li><a href="{{ url('trombi:user_tools') }}">{% trans %}Trombi tools{% endtrans %}</a></li>
</ul>

View File

@ -28,7 +28,7 @@ from core.views import *
urlpatterns = [
url(r'^$', index, name='index'),
url(r'^doku_to_markdown$', DokuToMarkdownView.as_view(), name='doku_to_markdown'),
url(r'^to_markdown$', ToMarkdownView.as_view(), name='to_markdown'),
url(r'^notifications$', NotificationList.as_view(), name='notification_list'),
url(r'^notification/(?P<notif_id>[0-9]+)$', notification, name='notification'),

View File

@ -66,6 +66,7 @@ def exif_auto_rotate(image):
return image
def doku_to_markdown(text):
"""This is a quite correct doku translator"""
text = re.sub(r'([^:]|^)\/\/(.*?)\/\/', r'*\2*', text) # Italic (prevents protocol:// conflict)
text = re.sub(r'<del>(.*?)<\/del>', r'~~\1~~', text, flags=re.DOTALL) # Strike (may be multiline)
text = re.sub(r'<sup>(.*?)<\/sup>', r'^\1^', text) # Superscript (multiline not supported, because almost never used)
@ -93,8 +94,10 @@ def doku_to_markdown(text):
text = re.sub(r'\\{2,}[\s]', r' \n', text) # Carriage return
text = re.sub(r'\[\[(.*?)(\|(.*?))?\]\]', r'[\3](\1)', text) # Links
text = re.sub(r'{{(.*?)(\|(.*?))?}}', r'![\3](\1 "\3")', text) # Images
text = re.sub(r'\[\[(.*?)\|(.*?)\]\]', r'[\2](\1)', text) # Links
text = re.sub(r'\[\[(.*?)\]\]', r'[\1](\1)', text) # Links 2
text = re.sub(r'{{(.*?)\|(.*?)}}', r'![\2](\1 "\2")', text) # Images
text = re.sub(r'{{(.*?)(\|(.*?))?}}', r'![\1](\1 "\1")', text) # Images 2
text = re.sub(r'{\[(.*?)(\|(.*?))?\]}', r'[\1](\1)', text) # Video (transform to classic links, since we can't integrate them)
text = re.sub(r'###(\d*?)###', r'[[[\1]]]', text) # Progress bar
@ -117,14 +120,58 @@ def doku_to_markdown(text):
quote_level += 1
try:
new_text.append("> " * quote_level + "##### " + quote.group(2))
line = line.replace(quote.group(0), '')
except:
new_text.append("> " * quote_level)
line = line.replace(quote.group(0), '')
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), '')
quote_level -= 1
final_newline = True
new_text.append("> " * final_quote_level + line) # Finally append the line
if final_newline: new_text.append("\n") # Add a new line to ensure the separation between the quote and the following text
else:
new_text.append(line)
return "\n".join(new_text)
def bbcode_to_markdown(text):
"""This is a very basic BBcode translator"""
text = re.sub(r'\[b\](.*?)\[\/b\]', r'**\1**', text, flags=re.DOTALL) # Bold
text = re.sub(r'\[i\](.*?)\[\/i\]', r'*\1*', text, flags=re.DOTALL) # Italic
text = re.sub(r'\[u\](.*?)\[\/u\]', r'__\1__', text, flags=re.DOTALL) # Underline
text = re.sub(r'\[s\](.*?)\[\/s\]', r'~~\1~~', text, flags=re.DOTALL) # Strike (may be multiline)
text = re.sub(r'\[strike\](.*?)\[\/strike\]', r'~~\1~~', text, flags=re.DOTALL) # Strike 2
text = re.sub(r'article://', r'page://', text)
text = re.sub(r'dfile://', r'file://', text)
text = re.sub(r'\[url=(.*?)\](.*)\[\/url\]', r'[\2](\1)', text) # Links
text = re.sub(r'\[url\](.*)\[\/url\]', r'\1', text) # Links 2
text = re.sub(r'\[img\](.*)\[\/img\]', r'![\1](\1 "\1")', text) # Images
new_text = []
quote_level = 0
for line in text.splitlines(): # Tables and quotes
enter = re.finditer(r'\[quote(=(.+?))?\]', line)
quit = re.finditer(r'\[/quote\]', line)
if enter or quit: # Quote part
for quote in enter: # Enter quotes (support multiple at a time)
quote_level += 1
try:
new_text.append("> " * quote_level + "##### " + quote.group(2))
except:
new_text.append("> " * quote_level)
line = line.replace(quote.group(0), '')
final_quote_level = quote_level # Store quote_level to use at the end, since it will be modified during quit iteration
final_newline = False
for quote in quit: # Quit quotes (support multiple at a time)
line = line.replace(quote.group(0), '')
quote_level -= 1
final_newline = True
new_text.append("> " * final_quote_level + line) # Finally append the line
if final_newline: new_text.append("\n") # Add a new line to ensure the separation between the quote and the following text
else:
new_text.append(line)

View File

@ -37,7 +37,7 @@ from itertools import chain
from haystack.query import SearchQuerySet
from core.models import User, Notification
from core.utils import doku_to_markdown
from core.utils import doku_to_markdown, bbcode_to_markdown
from club.models import Club
def index(request, context=None):
@ -98,17 +98,20 @@ def search_json(request):
}
return JsonResponse(result)
class DokuToMarkdownView(TemplateView):
template_name = "core/doku_to_markdown.jinja"
class ToMarkdownView(TemplateView):
template_name = "core/to_markdown.jinja"
def post(self, request, *args, **kwargs):
self.text = request.POST['text']
if request.POST['syntax'] == "doku":
self.text_md = doku_to_markdown(self.text)
else:
self.text_md = bbcode_to_markdown(self.text)
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
kwargs = super(DokuToMarkdownView, self).get_context_data(**kwargs)
kwargs = super(ToMarkdownView, self).get_context_data(**kwargs)
try:
kwargs['text'] = self.text
kwargs['text_md'] = self.text_md

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 = [
('counter', '0011_auto_20161004_2039'),
]
operations = [
migrations.AlterField(
model_name='permanency',
name='end',
field=models.DateTimeField(db_index=True, verbose_name='end date', null=True),
),
]

View File

@ -431,7 +431,7 @@ class Permanency(models.Model):
user = models.ForeignKey(User, related_name="permanencies", verbose_name=_("user"))
counter = models.ForeignKey(Counter, related_name="permanencies", verbose_name=_("counter"))
start = models.DateTimeField(_('start date'))
end = models.DateTimeField(_('end date'), null=True)
end = models.DateTimeField(_('end date'), null=True, db_index=True)
activity = models.DateTimeField(_('last activity date'), auto_now=True)
class Meta:

View File

@ -29,3 +29,4 @@ from forum.models import *
admin.site.register(Forum)
admin.site.register(ForumTopic)
admin.site.register(ForumMessage)
admin.site.register(ForumUserInfo)

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('forum', '0003_auto_20170510_1754'),
]
operations = [
migrations.AlterModelOptions(
name='forummessage',
options={'ordering': ['-date']},
),
migrations.AlterModelOptions(
name='forumtopic',
options={'ordering': ['-_last_message__date']},
),
migrations.AddField(
model_name='forum',
name='_last_message',
field=models.ForeignKey(verbose_name='the last message', to='forum.ForumMessage', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='forums_where_its_last'),
),
migrations.AddField(
model_name='forum',
name='_topic_number',
field=models.IntegerField(default=0, verbose_name='number of topics'),
),
migrations.AddField(
model_name='forummessage',
name='_deleted',
field=models.BooleanField(default=False, verbose_name='is deleted'),
),
migrations.AddField(
model_name='forumtopic',
name='_last_message',
field=models.ForeignKey(verbose_name='the last message', to='forum.ForumMessage', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+'),
),
migrations.AddField(
model_name='forumtopic',
name='_message_number',
field=models.IntegerField(default=0, verbose_name='number of messages'),
),
migrations.AddField(
model_name='forumtopic',
name='_title',
field=models.CharField(max_length=64, blank=True, verbose_name='title'),
),
migrations.AlterField(
model_name='forum',
name='description',
field=models.CharField(max_length=512, default='', verbose_name='description'),
),
migrations.AlterField(
model_name='forum',
name='id',
field=models.AutoField(primary_key=True, serialize=False, db_index=True),
),
]

View File

@ -46,8 +46,9 @@ class Forum(models.Model):
edit_groups allows to put any group as a forum admin
view_groups allows some groups to view a forum
"""
id = models.AutoField(primary_key=True, db_index=True)
name = models.CharField(_('name'), max_length=64)
description = models.CharField(_('description'), max_length=256, default="")
description = models.CharField(_('description'), max_length=512, default="")
is_category = models.BooleanField(_('is a category'), default=False)
parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
@ -57,6 +58,9 @@ class Forum(models.Model):
view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
default=[settings.SITH_GROUP_PUBLIC_ID])
number = models.IntegerField(_("number to choose a specific forum ordering"), default=1)
_last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last",
verbose_name=_("the last message"), null=True, on_delete=models.SET_NULL)
_topic_number = models.IntegerField(_("number of topics"), default=0)
class Meta:
ordering = ['number']
@ -72,6 +76,28 @@ class Forum(models.Model):
if copy_rights:
self.copy_rights()
def set_topic_number(self):
self._topic_number = self.get_topic_number()
self.save()
if self.parent:
self.parent.set_topic_number()
def set_last_message(self):
topic = ForumTopic.objects.filter(forum__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
forum = Forum.objects.filter(parent__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
if topic and forum:
if topic._last_message_id < forum._last_message_id:
self._last_message_id = forum._last_message_id
else:
self._last_message_id = topic._last_message_id
elif topic:
self._last_message_id = topic._last_message_id
elif forum:
self._last_message_id = forum._last_message_id
self.save()
if self.parent:
self.parent.set_last_message()
def apply_rights_recursively(self):
children = self.children.all()
for c in children:
@ -86,10 +112,19 @@ class Forum(models.Model):
self.view_groups = self.parent.view_groups.all()
self.save()
_club_memberships = {} # This cache is particularly efficient:
# divided by 3 the number of requests on the main forum page
# after the first load
def is_owned_by(self, user):
if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
return True
try: m = Forum._club_memberships[self.id][user.id]
except:
m = self.owner_club.get_membership_for(user)
try: Forum._club_memberships[self.id][user.id] = m
except:
Forum._club_memberships[self.id] = {}
Forum._club_memberships[self.id][user.id] = m
if m:
return m.role > settings.SITH_MAXIMUM_FREE_ROLE
return False
@ -122,9 +157,9 @@ class Forum(models.Model):
p = p.parent
return l
@cached_property
@property
def topic_number(self):
return self.get_topic_number()
return self._topic_number
def get_topic_number(self):
number = self.topics.all().count()
@ -134,31 +169,31 @@ class Forum(models.Model):
@cached_property
def last_message(self):
return self.get_last_message()
return self._last_message
forum_list = {} # Class variable used for cache purpose
def get_last_message(self):
last_msg = None
for m in ForumMessage.objects.select_related('topic__forum').order_by('-id'):
if m.topic.forum.id in Forum.forum_list.keys(): # The object is already in Python's memory,
# so there's no need to query it again
forum = Forum.forum_list[m.topic.forum.id]
else: # Query the forum object and store it in the class variable for further use.
# Keeping the same object allows the @cached_property to work properly.
# This trick divided by 4 the number of DB queries in the main forum page, and about the same on many other forum pages.
# This also divided by 4 the amount of CPU usage for thoses pages, according to Django Debug Toolbar.
forum = m.topic.forum
Forum.forum_list[forum.id] = forum
if self in (forum.parent_list + [forum]) and not m.deleted:
return m
def get_children_list(self):
l = [self.id]
for c in self.children.all():
l.append(c.id)
l += c.get_children_list()
return l
class ForumTopic(models.Model):
forum = models.ForeignKey(Forum, related_name='topics')
author = models.ForeignKey(User, related_name='forum_topics')
description = models.CharField(_('description'), max_length=256, default="")
_last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"),
null=True, on_delete=models.SET_NULL)
_title = models.CharField(_('title'), max_length=64, blank=True)
_message_number = models.IntegerField(_("number of messages"), default=0)
class Meta:
ordering = ['-id'] # TODO: add date message ordering
ordering = ['-_last_message__date']
def save(self, *args, **kwargs):
super(ForumTopic, self).save(*args, **kwargs)
self.forum.set_topic_number() # Recompute the cached value
self.forum.set_last_message()
def is_owned_by(self, user):
return self.forum.is_owned_by(user)
@ -184,13 +219,11 @@ class ForumTopic(models.Model):
@cached_property
def last_message(self):
for msg in self.messages.order_by('id').select_related('author').order_by('-id').all():
if not msg.deleted:
return msg
return self._last_message
@property
@cached_property
def title(self):
return self.messages.order_by('date').first().title
return self._title
class ForumMessage(models.Model):
"""
@ -202,12 +235,29 @@ class ForumMessage(models.Model):
message = models.TextField(_("message"), default="")
date = models.DateTimeField(_('date'), default=timezone.now)
readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
_deleted = models.BooleanField(_('is deleted'), default=False)
class Meta:
ordering = ['id']
ordering = ['-date']
def __str__(self):
return "%s - %s" % (self.id, self.title)
return "%s (%s) - %s" % (self.id, self.author, self.title)
def save(self, *args, **kwargs):
self._deleted = self.is_deleted() # Recompute the cached value
super(ForumMessage, self).save(*args, **kwargs)
if self.is_last_in_topic():
self.topic._last_message_id = self.id
if self.is_first_in_topic() and self.title:
self.topic._title = self.title
self.topic._message_number = self.topic.messages.count()
self.topic.save()
def is_first_in_topic(self):
return bool(self.id == self.topic.messages.order_by('date').first().id)
def is_last_in_topic(self):
return bool(self.id == self.topic.messages.order_by('date').last().id)
def is_owned_by(self, user): # Anyone can create a topic: it's better to
# check the rights at the forum level, since it's more controlled
@ -217,12 +267,15 @@ class ForumMessage(models.Model):
return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user):
return (not self.deleted and user.can_view(self.topic))
return not self._deleted # No need to check the real rights since it's already done by the Topic view
def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id
def get_absolute_url(self):
return reverse('forum:view_message', kwargs={'message_id': self.id})
def get_url(self):
return self.topic.get_absolute_url() + "?page=" + str(self.get_page()) + "#msg_" + str(self.id)
def get_page(self):
@ -230,16 +283,13 @@ class ForumMessage(models.Model):
def mark_as_read(self, user):
try: # Need the try/except because of AnonymousUser
if not self.is_read(user):
self.readers.add(user)
except: pass
def is_read(self, user):
return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all())
@cached_property
def deleted(self):
return self.is_deleted()
def is_deleted(self):
meta = self.metas.exclude(action="EDIT").order_by('-date').first()
if meta:
@ -258,13 +308,22 @@ class ForumMessageMeta(models.Model):
date = models.DateTimeField(_('date'), default=timezone.now)
action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16)
def save(self, *args, **kwargs):
super(ForumMessageMeta, self).save(*args, **kwargs)
self.message._deleted = self.message.is_deleted()
self.message.save()
class ForumUserInfo(models.Model):
"""
This currently stores only the last date a user clicked "Mark all as read".
However, this can be extended with lot of user preferences dedicated to a
user, such as the favourite topics, the signature, and so on...
"""
user = models.OneToOneField(User, related_name="_forum_infos") # TODO: see to move that to the User class in order to reduce the number of db queries
user = models.OneToOneField(User, related_name="_forum_infos")
last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
month=1, day=1, tzinfo=pytz.UTC))
def __str__(self):
return str(self.user)

View File

@ -6,13 +6,14 @@
{% endblock %}
{% block content %}
<div>
<div>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a>
{% for f in forum.get_parent_list()|reverse %}
> <a href="{{ f.get_absolute_url() }}">{{ f }}</a>
{% endfor %}
> <a href="{{ forum.get_absolute_url() }}">{{ forum }}</a>
</div>
</div>
<div id="forum">
<h3>{{ forum.name }}</h3>
<p>
{% if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID) or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
@ -34,7 +35,8 @@
</div>
</div>
</div>
{% for f in forum.children.all() %}
{{ display_forum(forum, user, True) }}
{% for f in forum.children.all().select_related("_last_message__author", "_last_message__topic") %}
{{ display_forum(f, user) }}
{% endfor %}
{% endif %}
@ -58,7 +60,13 @@
{% for t in topics %}
{{ display_topic(t, user) }}
{% endfor %}
<p style="text-align: right; background: #d8e7f3;">
{% for p in topics.paginator.page_range %}
<span class="ib" style="background: {% if p == topics.number %}white{% endif %}; margin: 0;"><a href="?topic_page={{ p }}">{{ p }}</a></span>
{% endfor %}
</p>
{% endif %}
</div>
{% endblock %}

View File

@ -6,18 +6,28 @@
{% endblock %}
{% block content %}
<p>
<p>
<a href="{{ url('forum:main') }}">Forum</a> >
</p>
</p>
<div id="forum">
<h3>{% trans %}Forum{% endtrans %}</h3>
<h4>{% trans %}Last unread messages{% endtrans %}</h4>
<p>
<a class="ib" href="{{ url('forum:mark_all_as_read') }}">{% trans %}Mark all as read{% endtrans %}</a>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}Refresh{% endtrans %}</a>
</p>
{% for t in forumtopic_list %}
{% for t in page_obj.object_list %}
{{ display_topic(t, user, True) }}
{% endfor %}
<p style="text-align: right; background: #d8e7f3;">
{% for p in paginator.page_range %}
<span class="ib" style="background: {% if p == paginator.number %}white{% endif %}; margin: 0;">
<a href="?page={{ p }}">{{ p }}</a>
</span>
{% endfor %}
</p>
</div>
{% endblock %}

View File

@ -1,37 +1,43 @@
{% from 'core/macros.jinja' import user_profile_link %}
{% from 'core/macros.jinja' import user_profile_link_short_name %}
{% macro display_forum(forum, user) %}
<div class="forum {% if forum.is_category %}category{% endif %}">
{% macro display_forum(forum, user, is_root=False) %}
<div class="forum {% if is_root %}category{% endif %}">
<div class="ib w_big">
{% if not forum.is_category %}
{% if not is_root %}
<a class="ib w_big" href="{{ url('forum:view_forum', forum_id=forum.id) }}">
{% else %}
<div class="ib w_big">
{% endif %}
<h5>{{ forum.name }}</h5>
<div class="title">{{ forum.name }}</div>
<p>{{ forum.description }}</p>
{% if not forum.is_category %}
{% if not is_root %}
</a>
{% else %}
</div>
{% endif %}
{% if user.is_owner(forum) %}
<div class="tools">
<a class="ib" href="{{ url('forum:edit_forum', forum_id=forum.id) }}">{% trans %}Edit{% endtrans %}</a>
<a class="ib" href="{{ url('forum:delete_forum', forum_id=forum.id) }}">{% trans %}Delete{% endtrans %}</a>
</div>
{% endif %}
</div>
{% if not forum.is_category %}
{% if not is_root %}
<div class="ib w_small">
<div class="ib w_medium">
<p class="ib w_medium">
{{ forum.topic_number }}
</div>
<div class="ib w_medium" style="font-size: x-small; text-align: center">
</p>
<div class="ib w_medium last_message" style="font-size: x-small; text-align: center">
{% if forum.last_message %}
{{ forum.last_message.author }} <br/>
{{ user_profile_link_short_name(forum.last_message.author) }} <br/>
<a href="{{ forum.last_message.get_absolute_url() }}">
<date>
{{ forum.last_message.date|localtime|date(DATETIME_FORMAT) }}
{{ forum.last_message.date|localtime|time(DATETIME_FORMAT) }}<br/>
{{ forum.last_message.date|localtime|time(DATETIME_FORMAT) }}
</date><br>
<span>
{{ forum.last_message.topic }}
</span>
</a>
{% endif %}
</div>
@ -48,33 +54,33 @@
{% else %}
<a class="ib w_big" href="{{ url('forum:view_topic', topic_id=topic.id) }}">
{% endif %}
<h5>{{ topic.title }}</h5>
<div class="title">{{ topic.title or topic.messages.first().title }}</div>
<p>{{ topic.description }}</p>
</a>
{% if user.can_edit(topic) %}
<div class="ib" style="text-align: center;">
<div class="ib tools" style="text-align: center;">
<a href="{{ url('forum:edit_topic', topic_id=topic.id) }}">{% trans %}Edit{% endtrans %}</a>
</div>
{% endif %}
</div>
<div class="ib w_medium last_message">
<div class="ib w_medium">
<div class="ib w_medium">
<div class="ib w_medium" style="text-align: center;">
{{ user_profile_link(topic.author) }}
<p class="ib w_medium" style="text-align: center;">
{{ user_profile_link_short_name(topic.author) }}
</p>
<p class="ib w_medium" style="text-align: center;">
{{ topic._message_number }}
</p>
</div>
<div class="ib w_medium" style="text-align: center;">
{{ topic.messages.count() }}
</div>
</div>
<div class="ib w_medium" style="text-align: center;">
<p class="ib w_medium" style="text-align: center;">
{% set last_msg = topic.last_message %}
{% if last_msg %}
{{ user_profile_link(last_msg.author) }} <br/>
{{ user_profile_link_short_name(last_msg.author) }} <br/>
<a href="{{ last_msg.get_absolute_url() }}">
{{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }}
<date>{{ last_msg.date|date(DATETIME_FORMAT) }} {{ last_msg.date|time(DATETIME_FORMAT) }}</date>
</a>
{% endif %}
</div>
</p>
</div>
</div>
{% endmacro %}
@ -92,12 +98,19 @@
<strong><a href="{{ m.author.get_absolute_url() }}">{{ m.author.get_short_name() }}</a></strong>
</div>
<div class="msg_content {% if m.deleted %}deleted{% endif %}" {% if m.id == first_unread_message_id %}id="first_unread"{% endif %}>
<div style="display: inline-block; width: 74%;">
{% if m.title %}
<h5>{{ m.title }}</h5>
{% endif %}
<div class="msg_header">
<div class="ib w_big title">
<a href="{{ m.get_absolute_url() }}">
{{ m.date|localtime|date(DATETIME_FORMAT) }}
{{ m.date|localtime|time(DATETIME_FORMAT) }}
{%- if m.title -%}
- {{ m.title }}
{%- endif -%}
</a>
</div>
<div style="display: inline-block; width: 25%;">
<div class="ib w_small">
<span><a href="{{ m.get_absolute_url() }}">#{{ m.id }}</a></span>
<br/>
<span><a href="{{ url('forum:new_message', topic_id=m.topic.id) }}?quote_id={{ m.id }}">
{% trans %}Reply as quote{% endtrans %}</a></span>
{% if user.can_edit(m) %}
@ -110,8 +123,7 @@
<span> <a href="{{ url('forum:delete_message', message_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></span>
{% endif %}
{% endif %}
<br/>
<span>{{ m.date|localtime|date(DATETIME_FORMAT) }} {{ m.date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
</div>
<hr>
<div>
@ -121,7 +133,7 @@
<ul class="msg_meta">
{% for meta in m.metas.select_related('user').order_by('id') %}
<li style="background: {% if m.author == meta.user %}#bfffbf{% else %}#ffffbf{% endif %}">
{{ meta.get_action_display() }} {{ meta.user.get_display_name() }}
{{ meta.get_action_display() }} {{ meta.user.get_short_name() }}
{% trans %} at {% endtrans %}{{ meta.date|localtime|time(DATETIME_FORMAT) }}
{% trans %} the {% endtrans %}{{ meta.date|localtime|date(DATETIME_FORMAT)}}</li>
{% endfor %}

View File

@ -7,9 +7,10 @@
{% endblock %}
{% block content %}
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >
</p>
<p>
<a href="{{ url('forum:main') }}">{% trans %}Forum{% endtrans %}</a> >
</p>
<div id="forum">
<h3>{% trans %}Forum{% endtrans %}</h3>
<p>
<a class="ib" href="{{ url('forum:last_unread') }}">{% trans %}View last unread messages{% endtrans %}</a>
@ -19,14 +20,28 @@
<a href="{{ url('forum:new_forum') }}">{% trans %}New forum{% endtrans %}</a>
</p>
{% endif %}
<div>
<div class="ib w_big">
{% trans %}Title{% endtrans %}
</div>
<div class="ib w_small">
<div class="ib w_medium">
{% trans %}Topics{% endtrans %}
</div>
<div class="ib w_small">
{% trans %}Last message{% endtrans %}
</div>
</div>
</div>
{% for f in forum_list %}
<div style="padding: 4px; margin: 4px">
{{ display_forum(f, user) }}
<div>
{{ display_forum(f, user, True) }}
{% for c in f.children.all() %}
{{ display_forum(c, user) }}
{% endfor %}
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -35,6 +35,7 @@
> <a href="{{ topic.get_absolute_url() }}">{{ topic }}</a>
</p>
<h3>{{ topic.title }}</h3>
<div id="forum">
<p>{{ topic.description }}</p>
<p><a href="{{ url('forum:new_message', topic_id=topic.id) }}">{% trans %}Reply{% endtrans %}</a></p>
@ -62,6 +63,7 @@
<span class="ib" style="background: {% if p == msgs.number %}white{% endif %}; margin: 0;"><a href="?page={{ p }}">{{ p }}</a></span>
{% endfor %}
</p>
</div>
{% endblock %}

View File

@ -38,6 +38,7 @@ urlpatterns = [
url(r'^topic/(?P<topic_id>[0-9]+)$', ForumTopicDetailView.as_view(), name='view_topic'),
url(r'^topic/(?P<topic_id>[0-9]+)/edit$', ForumTopicEditView.as_view(), name='edit_topic'),
url(r'^topic/(?P<topic_id>[0-9]+)/new_message$', ForumMessageCreateView.as_view(), name='new_message'),
url(r'^message/(?P<message_id>[0-9]+)$', ForumMessageView.as_view(), name='view_message'),
url(r'^message/(?P<message_id>[0-9]+)/edit$', ForumMessageEditView.as_view(), name='edit_message'),
url(r'^message/(?P<message_id>[0-9]+)/delete$', ForumMessageDeleteView.as_view(), name='delete_message'),
url(r'^message/(?P<message_id>[0-9]+)/undelete$', ForumMessageUndeleteView.as_view(), name='undelete_message'),

View File

@ -41,7 +41,7 @@ from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMi
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
class ForumMainView(ListView):
queryset = Forum.objects.filter(parent=None)
queryset = Forum.objects.filter(parent=None).prefetch_related("children___last_message__author", "children___last_message__topic")
template_name = "forum/main.jinja"
class ForumMarkAllAsRead(RedirectView):
@ -53,17 +53,23 @@ class ForumMarkAllAsRead(RedirectView):
fi = request.user.forum_infos
fi.last_read_date = timezone.now()
fi.save()
for m in request.user.read_messages.filter(date__lt=fi.last_read_date):
m.readers.remove(request.user) # Clean up to keep table low in data
except: pass
return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs)
class ForumLastUnread(ListView):
model = ForumTopic
template_name = "forum/last_unread.jinja"
paginate_by = settings.SITH_FORUM_PAGE_LENGTH / 2
def get_queryset(self):
l = ForumMessage.objects.exclude(readers=self.request.user).filter(
date__gt=self.request.user.forum_infos.last_read_date).values_list('topic') # TODO try to do better
return self.model.objects.filter(id__in=l).annotate(models.Max('messages__date')).order_by('-messages__date__max').select_related('author')
topic_list = self.model.objects.filter(_last_message__date__gt=self.request.user.forum_infos.last_read_date)\
.exclude(_last_message__readers=self.request.user)\
.order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\
.prefetch_related('forum__edit_groups')
return topic_list
class ForumForm(forms.ModelForm):
class Meta:
@ -117,7 +123,18 @@ class ForumDetailView(CanViewMixin, DetailView):
def get_context_data(self, **kwargs):
kwargs = super(ForumDetailView, self).get_context_data(**kwargs)
kwargs['topics'] = self.object.topics.annotate(models.Max('messages__date')).order_by('-messages__date__max')
qs = self.object.topics.order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\
.prefetch_related("forum__edit_groups")
paginator = Paginator(qs,
settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('topic_page')
try:
kwargs["topics"] = paginator.page(page)
except PageNotAnInteger:
kwargs["topics"] = paginator.page(1)
except EmptyPage:
kwargs["topics"] = paginator.page(paginator.num_pages)
return kwargs
class TopicForm(forms.ModelForm):
@ -164,7 +181,8 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs['first_unread_message_id'] = msg.id
except:
kwargs['first_unread_message_id'] = float("inf")
paginator = Paginator(self.object.messages.select_related('author__avatar_pict').all(),
paginator = Paginator(self.object.messages.select_related('author__avatar_pict')\
.prefetch_related('topic__forum__edit_groups', 'readers').order_by('date'),
settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('page')
try:
@ -175,6 +193,15 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs["msgs"] = paginator.page(paginator.num_pages)
return kwargs
class ForumMessageView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
return self.object.get_url()
class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage
fields = ['title', 'message']

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,14 @@ from django.core.files import File
from core.models import User, SithFile
from core.utils import doku_to_markdown, bbcode_to_markdown
from club.models import Club, Membership
from counter.models import Customer, Counter, Selling, Refilling, Product, ProductType, Permanency, Eticket
from subscription.models import Subscription
from eboutic.models import Invoice, InvoiceItem
from accounting.models import BankAccount, ClubAccount, GeneralJournal, Operation, AccountingType, Company, SimplifiedAccountingType, Label
from sas.models import Album, Picture, PeoplePictureRelation
from forum.models import Forum, ForumTopic, ForumMessage, ForumMessageMeta, ForumUserInfo
db = MySQLdb.connect(**settings.OLD_MYSQL_INFOS)
start = datetime.datetime.now()
@ -1181,6 +1183,155 @@ def reset_sas_moderators():
except Exception as e:
print(repr(e))
def migrate_forum():
print("Migrating forum")
def migrate_forums():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up forums")
Forum.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_forum
WHERE id_forum <> 1
""")
print(" Migrating forums")
for r in cur:
try:
# parent = Forum.objects.filter(id=r['id_forum_parent']).first()
club = Club.objects.filter(id=r['id_asso']).first()
ae = Club.objects.filter(id=settings.SITH_MAIN_CLUB_ID).first()
forum = Forum(
id=r['id_forum'],
name=to_unicode(r['titre_forum']),
description=to_unicode(r['description_forum'])[:511],
is_category=bool(r['categorie_forum']),
# parent=parent,
owner_club=club or ae,
number=r['ordre_forum'],
)
forum.save()
except Exception as e:
print(" FAIL to migrate forum: %s" % (repr(e)))
cur.execute("""
SELECT *
FROM frm_forum
WHERE id_forum_parent <> 1
""")
for r in cur:
parent = Forum.objects.filter(id=r['id_forum_parent']).first()
forum = Forum.objects.filter(id=r['id_forum']).first()
forum.parent = parent
forum.save()
cur.close()
print(" Forums migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_topics():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up topics")
ForumTopic.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_sujet
""")
print(" Migrating topics")
for r in cur:
try:
parent = Forum.objects.filter(id=r['id_forum']).first()
saloon = Forum.objects.filter(id=3).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
topic = ForumTopic(
id=r['id_sujet'],
author=author or root,
forum=parent or saloon,
_title=to_unicode(r['titre_sujet'])[:64],
description=to_unicode(r['soustitre_sujet']),
)
topic.save()
except Exception as e:
print(" FAIL to migrate topic: %s" % (repr(e)))
cur.close()
print(" Topics migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_messages():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up messages")
ForumMessage.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_message
""")
print(" Migrating messages")
for r in cur:
try:
topic = ForumTopic.objects.filter(id=r['id_sujet']).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
msg = ForumMessage(
id=r['id_message'],
topic=topic,
author=author or root,
title=to_unicode(r['titre_message'])[:63],
date=r['date_message'].replace(tzinfo=timezone('Europe/Paris')),
)
try:
if r['syntaxengine_message'] == "doku":
msg.message = doku_to_markdown(to_unicode(r['contenu_message']))
else:
msg.message = bbcode_to_markdown(to_unicode(r['contenu_message']))
except:
msg.message = to_unicode(r['contenu_message'])
msg.save()
except Exception as e:
print(" FAIL to migrate message: %s" % (repr(e)))
cur.close()
print(" Messages migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
def migrate_message_infos():
cur = db.cursor(MySQLdb.cursors.SSDictCursor)
print(" Cleaning up message meta")
ForumMessageMeta.objects.all().delete()
cur.execute("""
SELECT *
FROM frm_modere_info
""")
print(" Migrating message meta")
ACTIONS = {
"EDIT": "EDIT",
"AUTOEDIT": "EDIT",
"UNDELETE": "UNDELETE",
"DELETE": "DELETE",
"DELETEFIRST": "DELETE",
"AUTODELETE": "DELETE",
}
for r in cur:
try:
msg = ForumMessage.objects.filter(id=r['id_message']).first()
author = User.objects.filter(id=r['id_utilisateur']).first()
root = User.objects.filter(id=0).first()
meta = ForumMessageMeta(
message=msg,
user=author or root,
date=r['modere_date'].replace(tzinfo=timezone('Europe/Paris')),
action=ACTIONS[r['modere_action']],
)
meta.save()
except Exception as e:
print(" FAIL to migrate message meta: %s" % (repr(e)))
cur.close()
print(" Messages meta migrated at %s" % datetime.datetime.now())
print(" Running time: %s" % (datetime.datetime.now()-start))
migrate_forums()
migrate_topics()
migrate_messages()
migrate_message_infos()
print("Forum migrated at %s" % datetime.datetime.now())
print("Running time: %s" % (datetime.datetime.now()-start))
def main():
print("Start at %s" % start)
# Core
@ -1199,7 +1350,9 @@ def main():
# reset_index('core', 'club', 'subscription', 'accounting', 'eboutic', 'launderette', 'counter')
# migrate_sas()
# reset_index('core', 'sas')
reset_sas_moderators()
# reset_sas_moderators()
migrate_forum()
reset_index('forum')
end = datetime.datetime.now()
print("End at %s" % end)
print("Running time: %s" % (end-start))

View File

@ -52,7 +52,6 @@ class Subscription(models.Model):
subscription_end = models.DateField(_('subscription end'))
payment_method = models.CharField(_('payment method'),
max_length=255,
help_text=_('Eboutic is reserved to specific users. In doubt, don\'t use it.'),
choices=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)
location = models.CharField(choices=settings.SITH_SUBSCRIPTION_LOCATIONS,
max_length=20, verbose_name=_('location'))

View File

@ -19,7 +19,7 @@
<p>{{ form.subscription_type.errors }}<label for="{{ form.subscription_type.name }}">{{ form.subscription_type.label }}</label> {{ form.subscription_type }}</p>
<p>{{ form.payment_method.errors }}<label for="{{ form.payment_method.name }}">{{ form.payment_method.label }}</label> {{
form.payment_method }}</p>
<p>{{ form.payment_method.help_text }}</p>
<p>{% trans %}Eboutic is reserved to specific users. In doubt, don't use it.{% endtrans %}</p>
<p>{{ form.location.errors }}<label for="{{ form.location.name }}">{{ form.location.label }}</label> {{ form.location }}</p>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>