Merge branch 'master' into gender_options

This commit is contained in:
Celeste 2021-10-11 17:13:06 +02:00
commit 677a9da469
64 changed files with 1740 additions and 1161 deletions

View File

@ -4,19 +4,27 @@ stages:
test: test:
stage: test stage: test
script: script:
- env
- apt-get update - apt-get update
- apt-get install -y gettext python3-xapian libgraphviz-dev - apt-get install -y gettext python3-xapian libgraphviz-dev
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd - pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH" - export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
- python -c 'import xapian' # Fail immediately if there is a problem with xapian - python -c 'import xapian' # Fail immediately if there is a problem with xapian
- pip install -r requirements.txt - pip install -U -r requirements.txt
- pip install coverage - pip install -U coverage
- mkdir -p /dev/shm/search_indexes
- ln -s /dev/shm/search_indexes sith/search_indexes
- ./manage.py compilemessages - ./manage.py compilemessages
- coverage run ./manage.py test - coverage run ./manage.py test
- coverage html - coverage html
- coverage report - coverage report
- cd doc - cd doc
- make html # Make documentation - make html # Make documentation
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_tests"
cache:
paths:
- .cache/pip_tests
artifacts: artifacts:
paths: paths:
- coverage_report/ - coverage_report/
@ -24,5 +32,10 @@ test:
black: black:
stage: test stage: test
script: script:
- pip install black - pip install -U black
- black --check . - black --check .
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_black"
cache:
paths:
- .cache/pip_black

18
.mailmap Normal file
View File

@ -0,0 +1,18 @@
Code <gregoire.duvauchelle@utbm.fr>
Cyl <labetowiez@aol.fr>
Juste <maaxleblanc@gmail.com>
Krophil <pierre.brunet@krophil.fr>
Lo-J <renaudg779@gmail.com>
Nabos <gnikwo@hotmail.com>
Och <francescowitz68@gmail.com>
Partoo <joqaste@gmail.com>
Skia <skia@hya.sk> <lordbanana25@mailoo.org>
Skia <skia@hya.sk> <skia@libskia.so>
Sli <klmp200@klmp200.net>
Soldat <ryan-68@live.fr>
Terre <jbaptiste.lenglet+git@gmail.com>
Vial <robin.trioux@utbm.fr>
Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
root <root@localhost.localdomain>
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>

View File

@ -25,7 +25,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.core.management import call_command from django.core.management import call_command
from datetime import date from datetime import date, timedelta
from core.models import User from core.models import User
from accounting.models import ( from accounting.models import (
@ -110,6 +110,9 @@ class JournalTest(TestCase):
class OperationTest(TestCase): class OperationTest(TestCase):
def setUp(self): def setUp(self):
call_command("populate") call_command("populate")
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y"
)
self.journal = GeneralJournal.objects.filter(id=1).first() self.journal = GeneralJournal.objects.filter(id=1).first()
self.skia = User.objects.filter(username="skia").first() self.skia = User.objects.filter(username="skia").first()
at = AccountingType( at = AccountingType(
@ -158,7 +161,7 @@ class OperationTest(TestCase):
"target_type": "OTHER", "target_type": "OTHER",
"target_id": "", "target_id": "",
"target_label": "Le fantome de la nuit", "target_label": "Le fantome de la nuit",
"date": "04/12/2020", "date": self.tomorrow_formatted,
"mode": "CASH", "mode": "CASH",
"cheque_number": "", "cheque_number": "",
"invoice": "", "invoice": "",
@ -191,7 +194,7 @@ class OperationTest(TestCase):
"target_type": "OTHER", "target_type": "OTHER",
"target_id": "", "target_id": "",
"target_label": "Le fantome de la nuit", "target_label": "Le fantome de la nuit",
"date": "04/12/2020", "date": self.tomorrow_formatted,
"mode": "CASH", "mode": "CASH",
"cheque_number": "", "cheque_number": "",
"invoice": "", "invoice": "",
@ -218,7 +221,7 @@ class OperationTest(TestCase):
"target_type": "OTHER", "target_type": "OTHER",
"target_id": "", "target_id": "",
"target_label": "Le fantome du jour", "target_label": "Le fantome du jour",
"date": "04/12/2020", "date": self.tomorrow_formatted,
"mode": "CASH", "mode": "CASH",
"cheque_number": "", "cheque_number": "",
"invoice": "", "invoice": "",
@ -245,7 +248,7 @@ class OperationTest(TestCase):
"target_type": "OTHER", "target_type": "OTHER",
"target_id": "", "target_id": "",
"target_label": "Le fantome de l'aurore", "target_label": "Le fantome de l'aurore",
"date": "04/12/2020", "date": self.tomorrow_formatted,
"mode": "CASH", "mode": "CASH",
"cheque_number": "", "cheque_number": "",
"invoice": "", "invoice": "",

View File

@ -496,7 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
return ret return ret
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add journal to the context """ """Add journal to the context"""
kwargs = super(OperationCreateView, self).get_context_data(**kwargs) kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
if self.journal: if self.journal:
kwargs["object"] = self.journal kwargs["object"] = self.journal
@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
template_name = "accounting/operation_edit.jinja" template_name = "accounting/operation_edit.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add journal to the context """ """Add journal to the context"""
kwargs = super(OperationEditView, self).get_context_data(**kwargs) kwargs = super(OperationEditView, self).get_context_data(**kwargs)
kwargs["object"] = self.object.journal kwargs["object"] = self.object.journal
return kwargs return kwargs
@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add infos to the context """ """Add infos to the context"""
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs) kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
kwargs["statement"] = self.big_statement() kwargs["statement"] = self.big_statement()
return kwargs return kwargs
@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
return sum(self.statement(movement_type).values()) return sum(self.statement(movement_type).values())
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add journal to the context """ """Add journal to the context"""
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs) kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
kwargs["credit_statement"] = self.statement("CREDIT") kwargs["credit_statement"] = self.statement("CREDIT")
kwargs["debit_statement"] = self.statement("DEBIT") kwargs["debit_statement"] = self.statement("DEBIT")
@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
return statement return statement
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add journal to the context """ """Add journal to the context"""
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs) kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
kwargs["statement"] = self.statement() kwargs["statement"] = self.statement()
return kwargs return kwargs

View File

@ -33,8 +33,8 @@ from core.views import can_view, can_edit
def check_if(obj, user, test): def check_if(obj, user, test):
""" """
Detect if it's a single object or a queryset Detect if it's a single object or a queryset
aply a given test on individual object and return global permission aply a given test on individual object and return global permission
""" """
if isinstance(obj, QuerySet): if isinstance(obj, QuerySet):
for o in obj: for o in obj:
@ -49,7 +49,7 @@ class ManageModelMixin:
@action(detail=True) @action(detail=True)
def id(self, request, pk=None): def id(self, request, pk=None):
""" """
Get by id (api/v1/router/{pk}/id/) Get by id (api/v1/router/{pk}/id/)
""" """
self.queryset = get_object_or_404(self.queryset.filter(id=pk)) self.queryset = get_object_or_404(self.queryset.filter(id=pk))
serializer = self.get_serializer(self.queryset) serializer = self.get_serializer(self.queryset)

View File

@ -33,7 +33,7 @@ from core.templatetags.renderer import markdown
@renderer_classes((StaticHTMLRenderer,)) @renderer_classes((StaticHTMLRenderer,))
def RenderMarkdown(request): def RenderMarkdown(request):
""" """
Render Markdown Render Markdown
""" """
try: try:
data = markdown(request.POST["text"]) data = markdown(request.POST["text"])

View File

@ -43,7 +43,7 @@ class ClubSerializer(serializers.ModelSerializer):
class ClubViewSet(RightModelViewSet): class ClubViewSet(RightModelViewSet):
""" """
Manage Clubs (api/v1/club/) Manage Clubs (api/v1/club/)
""" """
serializer_class = ClubSerializer serializer_class = ClubSerializer

View File

@ -45,7 +45,7 @@ class CounterSerializer(serializers.ModelSerializer):
class CounterViewSet(RightModelViewSet): class CounterViewSet(RightModelViewSet):
""" """
Manage Counters (api/v1/counter/) Manage Counters (api/v1/counter/)
""" """
serializer_class = CounterSerializer serializer_class = CounterSerializer
@ -54,7 +54,7 @@ class CounterViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def bar(self, request): def bar(self, request):
""" """
Return all bars (api/v1/counter/bar/) Return all bars (api/v1/counter/bar/)
""" """
self.queryset = self.queryset.filter(type="BAR") self.queryset = self.queryset.filter(type="BAR")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)

View File

@ -36,7 +36,7 @@ class GroupSerializer(serializers.ModelSerializer):
class GroupViewSet(RightModelViewSet): class GroupViewSet(RightModelViewSet):
""" """
Manage Groups (api/v1/group/) Manage Groups (api/v1/group/)
""" """
serializer_class = GroupSerializer serializer_class = GroupSerializer

View File

@ -72,7 +72,7 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
class LaunderettePlaceViewSet(RightModelViewSet): class LaunderettePlaceViewSet(RightModelViewSet):
""" """
Manage Launderette (api/v1/launderette/place/) Manage Launderette (api/v1/launderette/place/)
""" """
serializer_class = LaunderettePlaceSerializer serializer_class = LaunderettePlaceSerializer
@ -81,7 +81,7 @@ class LaunderettePlaceViewSet(RightModelViewSet):
class LaunderetteMachineViewSet(RightModelViewSet): class LaunderetteMachineViewSet(RightModelViewSet):
""" """
Manage Washing Machines (api/v1/launderette/machine/) Manage Washing Machines (api/v1/launderette/machine/)
""" """
serializer_class = LaunderetteMachineSerializer serializer_class = LaunderetteMachineSerializer
@ -90,7 +90,7 @@ class LaunderetteMachineViewSet(RightModelViewSet):
class LaunderetteTokenViewSet(RightModelViewSet): class LaunderetteTokenViewSet(RightModelViewSet):
""" """
Manage Launderette's tokens (api/v1/launderette/token/) Manage Launderette's tokens (api/v1/launderette/token/)
""" """
serializer_class = LaunderetteTokenSerializer serializer_class = LaunderetteTokenSerializer
@ -99,7 +99,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def washing(self, request): def washing(self, request):
""" """
Return all washing tokens (api/v1/launderette/token/washing) Return all washing tokens (api/v1/launderette/token/washing)
""" """
self.queryset = self.queryset.filter(type="WASHING") self.queryset = self.queryset.filter(type="WASHING")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)
@ -108,7 +108,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def drying(self, request): def drying(self, request):
""" """
Return all drying tokens (api/v1/launderette/token/drying) Return all drying tokens (api/v1/launderette/token/drying)
""" """
self.queryset = self.queryset.filter(type="DRYING") self.queryset = self.queryset.filter(type="DRYING")
serializer = self.get_serializer(self.queryset, many=True) serializer = self.get_serializer(self.queryset, many=True)
@ -117,7 +117,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def avaliable(self, request): def avaliable(self, request):
""" """
Return all avaliable tokens (api/v1/launderette/token/avaliable) Return all avaliable tokens (api/v1/launderette/token/avaliable)
""" """
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
borrow_date__isnull=True, user__isnull=True borrow_date__isnull=True, user__isnull=True
@ -128,7 +128,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def unavaliable(self, request): def unavaliable(self, request):
""" """
Return all unavaliable tokens (api/v1/launderette/token/unavaliable) Return all unavaliable tokens (api/v1/launderette/token/unavaliable)
""" """
self.queryset = self.queryset.filter( self.queryset = self.queryset.filter(
borrow_date__isnull=False, user__isnull=False borrow_date__isnull=False, user__isnull=False

View File

@ -50,8 +50,8 @@ class UserSerializer(serializers.ModelSerializer):
class UserViewSet(RightModelViewSet): class UserViewSet(RightModelViewSet):
""" """
Manage Users (api/v1/user/) Manage Users (api/v1/user/)
Only show active users Only show active users
""" """
serializer_class = UserSerializer serializer_class = UserSerializer
@ -60,7 +60,7 @@ class UserViewSet(RightModelViewSet):
@action(detail=False) @action(detail=False)
def birthday(self, request): def birthday(self, request):
""" """
Return all users born today (api/v1/user/birstdays) Return all users born today (api/v1/user/birstdays)
""" """
date = datetime.datetime.today() date = datetime.datetime.today()
self.queryset = self.queryset.filter(date_of_birth=date) self.queryset = self.queryset.filter(date_of_birth=date)

View File

@ -29,10 +29,10 @@ def uv_endpoint(request):
def find_uv(lang, year, code): def find_uv(lang, year, code):
""" """
Uses the UTBM API to find an UV. Uses the UTBM API to find an UV.
short_uv is the UV entry in the UV list. It is returned as it contains short_uv is the UV entry in the UV list. It is returned as it contains
information which are not in full_uv. information which are not in full_uv.
full_uv is the detailed representation of an UV. full_uv is the detailed representation of an UV.
""" """
# query the UV list # query the UV list
uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year) uvs_url = settings.SITH_PEDAGOGY_UTBM_API + "/uvs/{}/{}".format(lang, year)
@ -57,7 +57,7 @@ def find_uv(lang, year, code):
def make_clean_uv(short_uv, full_uv): def make_clean_uv(short_uv, full_uv):
""" """
Cleans the data up so that it corresponds to our data representation. Cleans the data up so that it corresponds to our data representation.
""" """
res = {} res = {}

View File

@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User from core.models import User
from core.views.forms import SelectDate, SelectDateTime from core.views.forms import SelectDate, SelectDateTime
from counter.models import Counter from counter.models import Counter
from core.views.forms import TzAwareDateTimeField
class ClubEditForm(forms.ModelForm): class ClubEditForm(forms.ModelForm):
@ -158,18 +159,9 @@ class MailingForm(forms.Form):
class SellingsForm(forms.Form): class SellingsForm(forms.Form):
begin_date = forms.DateTimeField( begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
input_formats=["%Y-%m-%d %H:%M:%S"], end_date = TzAwareDateTimeField(label=_("End date"), required=False)
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
counters = forms.ModelMultipleChoiceField( counters = forms.ModelMultipleChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False Counter.objects.order_by("name").all(), label=_("Counter"), required=False
) )
@ -252,8 +244,8 @@ class ClubMemberForm(forms.Form):
def clean_users(self): def clean_users(self):
""" """
Check that the user is not trying to add an user already in the club Check that the user is not trying to add an user already in the club
Also check that the user is valid and has a valid subscription Also check that the user is valid and has a valid subscription
""" """
cleaned_data = super(ClubMemberForm, self).clean() cleaned_data = super(ClubMemberForm, self).clean()
users = [] users = []
@ -276,7 +268,7 @@ class ClubMemberForm(forms.Form):
def clean(self): def clean(self):
""" """
Check user rights for adding an user Check user rights for adding an user
""" """
cleaned_data = super(ClubMemberForm, self).clean() cleaned_data = super(ClubMemberForm, self).clean()

View File

@ -290,7 +290,7 @@ class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
def form_valid(self, form): def form_valid(self, form):
""" """
Check user rights Check user rights
""" """
resp = super(ClubMembersView, self).form_valid(form) resp = super(ClubMembersView, self).form_valid(form)

View File

@ -6,152 +6,150 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
{% endif %}
<div id="news"> <div id="news">
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
{% endif %}
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 birthdays %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
<div id="left_column" class="news_column"> <div id="left_column" class="news_column">
{% for news in object_list.filter(type="NOTICE") %} {% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice"> <section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4> <h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div> <div class="news_content">{{ news.summary|markdown }}</div>
</section> </section>
{% endfor %} {% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), {% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
dates__end_date__gte=timezone.now(), type="CALL") %} <section class="news_call">
<section class="news_call"> <h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4> <div class="news_date">
<div class="news_date"> <span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> - <span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span> </div>
</div> <div class="news_content">{{ news.summary|markdown }}</div>
<div class="news_content">{{ news.summary|markdown }}</div> </section>
</section> {% endfor %}
{% endfor %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), {% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %} <h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3> {% if events_dates %}
{% if events_dates %} {% for d in events_dates %}
{% for d in events_dates %} <div class="news_events_group">
<div class="news_events_group"> <div class="news_events_group_date">
<div class="news_events_group_date"> <div>
<div> <div>{{ d|localtime|date('D') }}</div>
<div>{{ d|localtime|date('D') }}</div> <div class="day">{{ d|localtime|date('d') }}</div>
<div class="day">{{ d|localtime|date('d') }}</div> <div>{{ d|localtime|date('b') }}</div>
<div>{{ d|localtime|date('b') }}</div> </div>
</div> </div>
</div> <div class="news_events_group_items">
<div class="news_events_group_items"> {% for news in object_list.filter(dates__start_date__gte=d,
{% for news in object_list.filter(dates__start_date__gte=d, dates__start_date__lte=d+timedelta(days=1),
dates__start_date__lte=d+timedelta(days=1), type="EVENT").exclude(dates__end_date__lt=timezone.now())
type="EVENT").exclude(dates__end_date__lt=timezone.now()) .order_by('dates__start_date') %}
.order_by('dates__start_date') %} <section class="news_event">
<section class="news_event"> <div class="club_logo">
<div class="club_logo"> {% if news.club.logo %}
{% if news.club.logo %} <img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" /> {% else %}
{% else %} <img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" /> {% endif %}
{% endif %} </div>
</div> <h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4> <div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div>
<div><a href="{{ news.club.get_absolute_url() }}">{{ news.club }}</a></div> <div class="news_date">
<div class="news_date"> <span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> - <span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span> </div>
</div> <div class="news_content">{{ news.summary|markdown }}
<div class="news_content">{{ news.summary|markdown }} <div class="button_bar">
<div class="button_bar"> {{ fb_quick(news) }}
{{ fb_quick(news) }} {{ tweet_quick(news) }}
{{ tweet_quick(news) }} </div>
</div> </div>
</div> </section>
</section> {% endfor %}
{% endfor %} </div>
</div> </div>
</div> {% endfor %}
{% endfor %} {% else %}
{% else %} <div class="news_empty">
<div class="news_empty"> <em>{% trans %}Nothing to come...{% endtrans %}</em>
<em>{% trans %}Nothing to come...{% endtrans %}</em> </div>
</div> {% endif %}
{% endif %}
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5), {% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
type="EVENT").order_by('dates__start_date') %} type="EVENT").order_by('dates__start_date') %}
{% if coming_soon %} {% if coming_soon %}
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3> <h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
{% for news in coming_soon %} {% for news in coming_soon %}
<section class="news_coming_soon"> <section class="news_coming_soon">
<a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a>
<span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} <span class="news_date">{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} {{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span> {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</section> </section>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 "birthdays" %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -36,8 +36,8 @@
<div class="name">{{ poster.name }}</div> <div class="name">{{ poster.name }}</div>
<div class="image"><img src="{{ poster.file.url }}"></img></div> <div class="image"><img src="{{ poster.file.url }}"></img></div>
<div class="dates"> <div class="dates">
<div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div> <div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
<div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div> <div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
</div> </div>
{% if app == "com" %} {% if app == "com" %}
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a> <a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>

View File

@ -7,15 +7,33 @@
{% block content %} {% block content %}
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a> <a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
{% if request.GET['send'] %} {% if bad_recipients %}
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p> <p>
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %} <span class="important">
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p> {% trans %}The following recipients were refused by the SMTP:{% endtrans %}
{% endif %} </span>
<form method="post" action=""> <ul>
{% csrf_token %} {% for r in bad_recipients.keys() %}
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button> <li>{{ r }}</li>
</form> {% endfor %}
</ul>
</p>
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="clean">{% trans %}Clean subscribers{% endtrans %}</button>
</form>
{% else %}
{% if request.GET['send'] %}
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
{% endif %}
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
</form>
{% endif %}
{% endif %} {% endif %}
<hr> <hr>
{{ weekmail_rendered|safe }} {{ weekmail_rendered|safe }}

View File

@ -79,9 +79,11 @@ class ComTest(TestCase):
) )
r = self.client.get(reverse("core:index")) r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200) self.assertTrue(r.status_code == 200)
self.assertTrue( self.assertContains(
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""" r,
in str(r.content) """<div id="alert_box">
<div class="markdown"><h3>ALERTE!</h3>
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
) )
def test_info_msg(self): def test_info_msg(self):
@ -95,9 +97,10 @@ class ComTest(TestCase):
) )
r = self.client.get(reverse("core:index")) r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200) self.assertTrue(r.status_code == 200)
self.assertTrue( self.assertContains(
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""" r,
in str(r.content) """<div id="info_box">
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
) )
def test_birthday_non_subscribed_user(self): def test_birthday_non_subscribed_user(self):

View File

@ -39,6 +39,7 @@ from django.core.exceptions import PermissionDenied
from django import forms from django import forms
from datetime import timedelta from datetime import timedelta
from smtplib import SMTPRecipientsRefused
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
from core.views import ( from core.views import (
@ -52,6 +53,7 @@ from core.views import (
from core.views.forms import SelectDateTime, MarkdownInput from core.views.forms import SelectDateTime, MarkdownInput
from core.models import Notification, RealGroup, User from core.models import Notification, RealGroup, User
from club.models import Club, Mailing from club.models import Club, Mailing
from core.views.forms import TzAwareDateTimeField
# Sith object # Sith object
@ -72,20 +74,14 @@ class PosterForm(forms.ModelForm):
"display_time", "display_time",
] ]
widgets = {"screens": forms.CheckboxSelectMultiple} widgets = {"screens": forms.CheckboxSelectMultiple}
help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
date_begin = forms.DateTimeField( date_begin = TzAwareDateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"), label=_("Start date"),
widget=SelectDateTime,
required=True, required=True,
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"), initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
) )
date_end = forms.DateTimeField( date_end = TzAwareDateTimeField(label=_("End date"), required=False)
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
@ -199,24 +195,10 @@ class NewsForm(forms.ModelForm):
"content": MarkdownInput, "content": MarkdownInput,
} }
start_date = forms.DateTimeField( start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
input_formats=["%Y-%m-%d %H:%M:%S"], end_date = TzAwareDateTimeField(label=_("End date"), required=False)
label=_("Start date"), until = TzAwareDateTimeField(label=_("Until"), required=False)
widget=SelectDateTime,
required=False,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=False,
)
until = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Until"),
widget=SelectDateTime,
required=False,
)
automoderation = forms.BooleanField(label=_("Automoderation"), required=False) automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
def clean(self): def clean(self):
@ -433,22 +415,35 @@ class NewsDetailView(CanViewMixin, DetailView):
# Weekmail # Weekmail
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView): class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
model = Weekmail model = Weekmail
template_name = "com/weekmail_preview.jinja" template_name = "com/weekmail_preview.jinja"
success_url = reverse_lazy("com:weekmail") success_url = reverse_lazy("com:weekmail")
current_tab = "weekmail" current_tab = "weekmail"
def dispatch(self, request, *args, **kwargs):
self.bad_recipients = []
return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
try: if request.POST["send"] == "validate":
if request.POST["send"] == "validate": try:
self.object.send() self.object.send()
return HttpResponseRedirect( return HttpResponseRedirect(
reverse("com:weekmail") + "?qn_weekmail_send_success" reverse("com:weekmail") + "?qn_weekmail_send_success"
) )
except: except SMTPRecipientsRefused as e:
pass self.bad_recipients = e.recipients
elif request.POST["send"] == "clean":
try:
self.object.send() # This should fail
except SMTPRecipientsRefused as e:
users = User.objects.filter(email__in=e.recipients.keys())
for u in users:
u.preferences.receive_weekmail = False
u.preferences.save()
self.quick_notif_list += ["qn_success"]
return super(WeekmailPreviewView, self).get(request, *args, **kwargs) return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
def get_object(self, queryset=None): def get_object(self, queryset=None):
@ -458,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
"""Add rendered weekmail""" """Add rendered weekmail"""
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs) kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
kwargs["weekmail_rendered"] = self.object.render_html() kwargs["weekmail_rendered"] = self.object.render_html()
kwargs["bad_recipients"] = self.bad_recipients
return kwargs return kwargs
@ -534,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
return super(WeekmailEditView, self).get(request, *args, **kwargs) return super(WeekmailEditView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add orphan articles """ """Add orphan articles"""
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs) kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None) kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
return kwargs return kwargs

View File

@ -0,0 +1,107 @@
import re
from subprocess import PIPE, Popen, TimeoutExpired
from django.conf import settings
from django.core.management.base import BaseCommand
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
# added "v?"
semver_regex = re.compile(
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
)
class Command(BaseCommand):
help = "Checks the front dependencies are up to date."
def handle(self, *args, **options):
deps = settings.SITH_FRONT_DEP_VERSIONS
processes = dict(
(url, create_process(url))
for url in deps.keys()
if parse_semver(deps[url]) is not None
)
for url, process in processes.items():
try:
stdout, stderr = process.communicate(timeout=15)
except TimeoutExpired:
process.kill()
self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
continue
# error, notice, warning
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if stderr != "":
self.stderr.write(self.style.WARNING(stderr.strip()))
continue
# get all tags, parse them as semvers and find the biggest
tags = list_tags(stdout)
tags = map(parse_semver, tags)
tags = filter(lambda tag: tag is not None, tags)
latest_version = max(tags)
# cannot fail as those which fail are filtered in the processes dict creation
current_version = parse_semver(deps[url])
assert current_version is not None
if latest_version == current_version:
msg = "{}: {}".format(url, semver_to_s(current_version))
self.stdout.write(self.style.SUCCESS(msg))
else:
msg = "{}: {} < {}".format(
url, semver_to_s(current_version), semver_to_s(latest_version)
)
self.stdout.write(self.style.ERROR(msg))
def create_process(url):
"""Spawn a "git ls-remote --tags" child process."""
return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
def list_tags(s):
"""Parses "git ls-remote --tags" output. Takes a string."""
tag_prefix = "refs/tags/"
for line in s.strip().split("\n"):
# an example line could be:
# "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
parts = line.split("\t")
# check we have a commit ID (SHA-1 hash) and a tag name
assert len(parts) == 2
assert len(parts[0]) == 40
assert parts[1].startswith(tag_prefix)
# avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
if not parts[1].endswith("^{}"):
yield parts[1][len(tag_prefix) :]
def parse_semver(s):
"""
Turns a semver string into a 3-tuple or None if the parsing failed, it is a
prerelease or it has build metadata.
See https://semver.org
"""
m = semver_regex.match(s)
if (
m is None
or m.group("prerelease") is not None
or m.group("buildmetadata") is not None
):
return None
return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
def semver_to_s(t):
"""Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
return "{}.{}.{}".format(t[0], t[1], t[2])

View File

@ -31,7 +31,7 @@ from django.conf import settings
class Command(BaseCommand): class Command(BaseCommand):
""" """
Compiles scss in static folder for production Compiles scss in static folder for production
""" """
help = "Compile scss files from static folder" help = "Compile scss files from static folder"

View File

@ -1492,7 +1492,9 @@ class OperationLog(models.Model):
User, related_name="logs", on_delete=models.SET_NULL, null=True User, related_name="logs", on_delete=models.SET_NULL, null=True
) )
operation_type = models.CharField( operation_type = models.CharField(
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE, _("operation type"),
max_length=40,
choices=settings.SITH_LOG_OPERATION_TYPE,
) )
def is_owned_by(self, user): def is_owned_by(self, user):

View File

@ -33,16 +33,16 @@ from django.db import connection, migrations
class PsqlRunOnly(migrations.RunSQL): class PsqlRunOnly(migrations.RunSQL):
""" """
This is an SQL runner that will launch the given command only if This is an SQL runner that will launch the given command only if
the used DBMS is PostgreSQL. the used DBMS is PostgreSQL.
It may be useful to run Postgres' specific SQL, or to take actions It may be useful to run Postgres' specific SQL, or to take actions
that would be non-senses with backends other than Postgre, such that would be non-senses with backends other than Postgre, such
as disabling particular constraints that would prevent the migration as disabling particular constraints that would prevent the migration
to run successfully. to run successfully.
See `club/migrations/0010_auto_20170912_2028.py` as an example. See `club/migrations/0010_auto_20170912_2028.py` as an example.
Some explanations can be found here too: Some explanations can be found here too:
https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes https://stackoverflow.com/questions/28429933/django-migrations-using-runpython-to-commit-changes
""" """
def _run_sql(self, schema_editor, sqls): def _run_sql(self, schema_editor, sqls):

View File

@ -35,9 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
class ScssProcessor(object): class ScssProcessor(object):
""" """
If DEBUG mode enabled : compile the scss file If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production Don't forget to use compilestatics to compile scss for production
""" """
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/")) prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))

View File

@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
class UserIndex(indexes.SearchIndex, indexes.Indexable): class UserIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True) text = indexes.CharField(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True) auto = indexes.EdgeNgramField(use_template=True)
last_update = indexes.DateTimeField(model_attr="last_update")
def get_model(self): def get_model(self):
return User return User
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
def get_updated_field(self): def get_updated_field(self):
return "last_update" return "last_update"
def prepare_auto(self, obj):
return self.prepared_data["auto"].strip()[:245]
class IndexSignalProcessor(signals.BaseSignalProcessor): class IndexSignalProcessor(signals.BaseSignalProcessor):
def setup(self): def setup(self):

File diff suppressed because one or more lines are too long

View File

@ -28,7 +28,7 @@ $twitblue: hsl(206, 82%, 63%);
$shadow-color: rgb(223, 223, 223); $shadow-color: rgb(223, 223, 223);
$background-bouton-color: hsl(0, 0%, 90%); $background-button-color: hsl(0, 0%, 95%);
/*--------------------------MEDIA QUERY HELPERS------------------------*/ /*--------------------------MEDIA QUERY HELPERS------------------------*/
$small-devices: 576px; $small-devices: 576px;
@ -47,10 +47,11 @@ body {
input[type=button], input[type=submit], input[type=reset],input[type=file] { input[type=button], input[type=submit], input[type=reset],input[type=file] {
border: none; border: none;
text-decoration: none; text-decoration: none;
background-color: $background-bouton-color; background-color: $background-button-color;
padding: 10px; padding: 0.4em;
margin: 0.1em;
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 1.2em;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
box-shadow: $shadow-color 0px 0px 1px; box-shadow: $shadow-color 0px 0px 1px;
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
button{ button{
border: none; border: none;
text-decoration: none; text-decoration: none;
background-color: $background-bouton-color; background-color: $background-button-color;
padding: 10px; padding: 0.4em;
font-size: 14px; margin: 0.1em;
font-size: 1.18em;
border-radius: 5px; border-radius: 5px;
box-shadow: $shadow-color 0px 0px 1px; box-shadow: $shadow-color 0px 0px 1px;
cursor: pointer; cursor: pointer;
@ -75,24 +77,26 @@ button{
input,textarea[type=text],[type=number]{ input,textarea[type=text],[type=number]{
border: none; border: none;
text-decoration: none; text-decoration: none;
background-color: $background-bouton-color; background-color: $background-button-color;
padding: 7px; padding: 0.4em;
font-size: 16px; margin: 0.1em;
font-size: 1.2em;
border-radius: 5px; border-radius: 5px;
max-width: 95%;
} }
textarea{ textarea{
border: none; border: none;
text-decoration: none; text-decoration: none;
background-color: $background-bouton-color; background-color: $background-button-color;
padding: 7px; padding: 7px;
font-size: 16px; font-size: 1.2em;
border-radius: 5px; border-radius: 5px;
} }
select{ select{
border: none; border: none;
text-decoration: none; text-decoration: none;
font-size: 15px; font-size: 1.2em;
background-color: $background-bouton-color; background-color: $background-button-color;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
@ -130,9 +134,10 @@ a {
#header_language_chooser { #header_language_chooser {
position: absolute; position: absolute;
top: 0.2em; top: 2em;
right: 0.5em; left: 0.5em;
width: 3%; width: 3%;
min-width: 2.2em;
text-align: center; text-align: center;
input { input {
display: block; display: block;
@ -157,9 +162,6 @@ header {
border-radius: 0px 0px 10px 10px; border-radius: 0px 0px 10px 10px;
#header_logo { #header_logo {
display: inline-block;
flex: none;
background-size: 100% 100%;
background-color: $white-color; background-color: $white-color;
padding: 0.2em; padding: 0.2em;
border-radius: 0px 0px 0px 9px; border-radius: 0px 0px 0px 9px;
@ -169,11 +171,19 @@ header {
margin: 0px; margin: 0px;
width: 100%; width: 100%;
height: 100%; height: 100%;
img {
max-width: 70%;
max-height: 100%;
margin: auto;
display: block;
}
} }
} }
#header_connect_links { #header_connect_links {
margin: 0.6em 0.6em 0em auto; margin: 0.6em 0.6em 0em auto;
padding: 0.2em;
color: $white-color; color: $white-color;
form { form {
display: inline; display: inline;
@ -190,6 +200,7 @@ header {
#header_bar { #header_bar {
display: flex; display: flex;
flex: auto; flex: auto;
flex-wrap: wrap;
width: 80%; width: 80%;
a { a {
@ -203,7 +214,6 @@ header {
} }
#header_bars_infos { #header_bars_infos {
width: 35ch;
flex: initial; flex: initial;
list-style-type: none; list-style-type: none;
margin: 0.2em 0.2em; margin: 0.2em 0.2em;
@ -213,12 +223,15 @@ header {
display: inline-block; display: inline-block;
flex: auto; flex: auto;
margin: 0.8em 0em; margin: 0.8em 0em;
input {
width: 14ch;
}
} }
#header_user_links { #header_user_links {
display: flex; display: flex;
width: 120ch;
flex: initial; flex: initial;
flex-wrap: wrap;
text-align: right; text-align: right;
margin: 0em; margin: 0em;
div { div {
@ -287,42 +300,34 @@ header {
#info_boxes { #info_boxes {
display: flex; display: flex;
flex-wrap: wrap;
width: 90%; width: 90%;
margin: 1em auto; margin: 1em auto;
p {
margin: 0px;
padding: 7px;
}
#alert_box, #info_box { #alert_box, #info_box {
font-size: 14px; flex: 49%;
display: inline-block; font-size: 0.9em;
flex: auto; margin: 0.2em;
padding: 2px; border-radius: 0.6em;
margin: 0.2em 1.5%; .markdown {
min-width: 10%; margin: 0.5em;
max-width: 46%; }
min-height: 20px;
&:before { &:before {
float: left; font-family: FontAwesome;
font-size: 4em;
float: right;
margin: 0.2em; margin: 0.2em;
} }
} }
#info_box { #info_box {
border-radius: 10px;
background: $primary-neutral-light-color; background: $primary-neutral-light-color;
&:before { &:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f05a"; content: "\f05a";
color: hsl(210, 100%, 56%); color: hsl(210, 100%, 56%);
} }
} }
#alert_box { #alert_box {
border-radius: 10px;
background: $second-color; background: $second-color;
&:before { &:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f06a"; content: "\f06a";
color: $white-color; color: $white-color;
} }
@ -345,7 +350,7 @@ header {
a { a {
flex: auto; flex: auto;
text-align: center; text-align: center;
padding: 20px; padding: 1.5em;
color: $white-color; color: $white-color;
font-style: normal; font-style: normal;
font-weight: bolder; font-weight: bolder;
@ -458,6 +463,8 @@ header {
/*---------------------------------NEWS--------------------------------*/ /*---------------------------------NEWS--------------------------------*/
#news { #news {
display: flex;
flex-wrap: wrap;
.news_column { .news_column {
display: inline-block; display: inline-block;
margin: 0px; margin: 0px;
@ -467,11 +474,13 @@ header {
margin-bottom: 1em; margin-bottom: 1em;
} }
#right_column { #right_column {
width: 20%; flex: 20%;
float: right; float: right;
margin: 0.2em;
} }
#left_column { #left_column {
width: 79%; flex: 79%;
margin: 0.2em;
h3 { h3 {
background: $second-color; background: $second-color;
box-shadow: $shadow-color 1px 1px 1px; box-shadow: $shadow-color 1px 1px 1px;
@ -484,6 +493,11 @@ header {
} }
} }
} }
@media screen and (max-width: $small-devices){
#left_column, #right_column {
flex: 100%;
}
}
/* AGENDA/BIRTHDAYS */ /* AGENDA/BIRTHDAYS */
#agenda,#birthdays { #agenda,#birthdays {
@ -691,6 +705,12 @@ header {
} }
} }
@media screen and (max-width: $small-devices){
#page {
width: 98%;
}
}
#news_details { #news_details {
display: inline-block; display: inline-block;
margin-top: 20px; margin-top: 20px;
@ -723,7 +743,7 @@ header {
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
display: inline-block; display: inline-block;
font-size: 16px; font-size: 1.2em;
border-radius: 2px; border-radius: 2px;
float: right; float: right;
display: block; display: block;
@ -1111,33 +1131,36 @@ u, .underline {
text-decoration: underline; text-decoration: underline;
} }
#basket {
width: 40%;
background: $primary-neutral-light-color;
float: right;
padding: 10px;
border-radius: 10px;
}
#products {
width: 90%;
margin: 0px auto;
overflow: auto;
}
#bar_ui { #bar_ui {
float: left; padding: 0.4em;
min-width: 57%; display: flex;
} flex-wrap: wrap;
flex-direction: row-reverse;
#user_info_container {} #products {
flex-basis: 100%;
margin: 0.2em;
overflow: auto;
}
#user_info { #click_form {
float: right; flex: auto;
padding: 5px; margin: 0.2em;
width: 40%; }
margin: 0px auto;
background: $secondary-neutral-light-color; #user_info {
flex: auto;
padding: 0.5em;
margin: 0.2em;
height: 100%;
background: $secondary-neutral-light-color;
img {
max-width: 70%;
}
input {
background: white;
}
}
} }
/*-----------------------------USER PROFILE----------------------------*/ /*-----------------------------USER PROFILE----------------------------*/
@ -1212,6 +1235,11 @@ u, .underline {
} }
} }
} }
@media screen and (max-width: $small-devices){
#user_profile_infos, #user_profile_pictures {
flex-basis: 50%;
}
}
} }
} }
@ -1412,6 +1440,7 @@ textarea {
.search_bar { .search_bar {
margin: 10px 0px; margin: 10px 0px;
display: flex; display: flex;
flex-wrap: wrap;
height: 20p; height: 20p;
align-items: center; align-items: center;
} }
@ -1551,6 +1580,7 @@ footer {
color: $white-color; color: $white-color;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
flex-wrap: wrap;
background-color: $primary-neutral-dark-color; background-color: $primary-neutral-dark-color;
box-shadow: $shadow-color 0px 0px 15px; box-shadow: $shadow-color 0px 0px 15px;
a { a {

View File

@ -3,6 +3,7 @@
<head> <head>
{% block head %} {% block head %}
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title> <title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}"> <link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}"> <link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
@ -27,6 +28,7 @@
<!-- BEGIN HEADER --> <!-- BEGIN HEADER -->
{% block header %} {% block header %}
{% if not popup %} {% if not popup %}
<header>
<div id="header_language_chooser"> <div id="header_language_chooser">
{% for language in LANGUAGES %} {% for language in LANGUAGES %}
<form action="{{ url('set_language') }}" method="post">{% csrf_token %} <form action="{{ url('set_language') }}" method="post">{% csrf_token %}
@ -37,10 +39,11 @@
{% endfor %} {% endfor %}
</div> </div>
<header>
{% if not user.is_authenticated %} {% if not user.is_authenticated %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;"> <div id="header_logo">
<a href="{{ url('core:index') }}"></a> <a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div> </div>
<div id="header_connect_links"> <div id="header_connect_links">
<form method="post" action="{{ url('core:login') }}"> <form method="post" action="{{ url('core:login') }}">
@ -54,12 +57,14 @@
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a> <a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
</div> </div>
{% else %} {% else %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;"> <div id="header_logo">
<a href="{{ url('core:index') }}"></a> <a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div> </div>
<div id="header_bar"> <div id="header_bar">
<ul id="header_bars_infos"> <ul id="header_bars_infos">
{% cache 100 counters_activity %} {% cache 100 "counters_activity" %}
{% for bar in Counter.objects.filter(type="BAR").all() %} {% for bar in Counter.objects.filter(type="BAR").all() %}
<li> <li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px"> <a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
@ -85,7 +90,7 @@
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a> <a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
</div> </div>
<div> <div>
<a href="#" onclick="display_notif()"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a> <a href="#" onclick="display_notif()" style="white-space: nowrap;"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
<ul id="header_notif"> <ul id="header_notif">
{% for n in user.notifications.filter(viewed=False).order_by('-date') %} {% for n in user.notifications.filter(viewed=False).order_by('-date') %}
<li> <li>
@ -126,17 +131,19 @@
</header> </header>
<div id="info_boxes"> <div id="info_boxes">
{% set sith = get_sith() %} {% block info_boxes %}
{% if sith.alert_msg %} {% set sith = get_sith() %}
<div id="alert_box"> {% if sith.alert_msg %}
{{ sith.alert_msg|markdown }} <div id="alert_box">
</div> {{ sith.alert_msg|markdown }}
{% endif %} </div>
{% if sith.info_msg %} {% endif %}
<div id="info_box"> {% if sith.info_msg %}
{{ sith.info_msg|markdown }} <div id="info_box">
</div> {{ sith.info_msg|markdown }}
{% endif %} </div>
{% endif %}
{% endblock %}
</div> </div>
{% else %}{# if not popup #} {% else %}{# if not popup #}

View File

@ -4,6 +4,12 @@
{% trans %}Delete confirmation{% endtrans %} {% trans %}Delete confirmation{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2> <h2>{% trans %}Delete confirmation{% endtrans %}</h2>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}

View File

@ -52,6 +52,7 @@
{% if not form.instance.profile_pict %} {% if not form.instance.profile_pict %}
<script src="{{ static('core/js/webcam.js') }}"></script> <script src="{{ static('core/js/webcam.js') }}"></script>
<script language="JavaScript"> <script language="JavaScript">
Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
Webcam.set({ Webcam.set({
width: 320, width: 320,
height: 240, height: 240,

View File

@ -12,7 +12,7 @@
{% for picture in pictures[a.id] %} {% for picture in pictures[a.id] %}
<div class="picture"> <div class="picture">
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict"> <a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/> <img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -1,3 +1,13 @@
{{ object.first_name }} {% load search_helpers %}
{{ object.last_name }}
{{ object.nick_name }} {% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
{{ first|replace:"|-| " }}
{{ last|replace:"|-| " }}
{{ nick|replace:"|-| " }}
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
{{ first|cut:"-" }}{{ last|cut:"-" }}
{% endwith %}

View File

@ -94,7 +94,7 @@ def datetime_format_python_to_PHP(python_format_string):
@register.simple_tag() @register.simple_tag()
def scss(path): def scss(path):
""" """
Return path of the corresponding css file after compilation Return path of the corresponding css file after compilation
""" """
processor = ScssProcessor(path) processor = ScssProcessor(path)
return processor.get_converted_scss() return processor.get_converted_scss()

View File

@ -0,0 +1,27 @@
from django.template.exceptions import TemplateSyntaxError
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
# arg should be of the form "|foo|bar" where the first character is the
# separator between old and new in value.replace(old, new)
@register.filter
@stringfilter
def replace(value, arg):
# s.replace('', '') == s so len(arg) == 2 is fine
if len(arg) < 2:
raise TemplateSyntaxError("badly formatted argument")
arg = arg.split(arg[0])
if len(arg) != 3:
raise TemplateSyntaxError("badly formatted argument")
return value.replace(arg[1], arg[2])
@register.filter
def count(value, arg):
return value.count(arg)

View File

@ -344,19 +344,19 @@ class QuickNotifMixin:
class DetailFormView(SingleObjectMixin, FormView): class DetailFormView(SingleObjectMixin, FormView):
""" """
Class that allow both a detail view and a form view Class that allow both a detail view and a form view
""" """
def get_object(self): def get_object(self):
""" """
Get current group from id in url Get current group from id in url
""" """
return self.cached_object return self.cached_object
@cached_property @cached_property
def cached_object(self): def cached_object(self):
""" """
Optimisation on group retrieval Optimisation on group retrieval
""" """
return super(DetailFormView, self).get_object() return super(DetailFormView, self).get_object()

View File

@ -42,6 +42,10 @@ from django.utils.translation import ugettext
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
from ajax_select.fields import AutoCompleteSelectField from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
from django.utils.dateparse import parse_datetime
from django.utils import timezone
import datetime
from django.forms.utils import to_current_timezone
import re import re
@ -114,14 +118,11 @@ class SelectFile(TextInput):
attrs["class"] = "select_file" attrs["class"] = "select_file"
else: else:
attrs = {"class": "select_file"} attrs = {"class": "select_file"}
output = ( output = '%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>' % {
'%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>' "content": super(SelectFile, self).render(name, value, attrs, renderer),
% { "title": _("Choose file"),
"content": super(SelectFile, self).render(name, value, attrs, renderer), "name": name,
"title": _("Choose file"), }
"name": name,
}
)
output += ( output += (
'<span name="' '<span name="'
+ name + name
@ -138,14 +139,11 @@ class SelectUser(TextInput):
attrs["class"] = "select_user" attrs["class"] = "select_user"
else: else:
attrs = {"class": "select_user"} attrs = {"class": "select_user"}
output = ( output = '%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>' % {
'%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>' "content": super(SelectUser, self).render(name, value, attrs, renderer),
% { "title": _("Choose user"),
"content": super(SelectUser, self).render(name, value, attrs, renderer), "name": name,
"title": _("Choose user"), }
"name": name,
}
)
output += ( output += (
'<span name="' '<span name="'
+ name + name
@ -399,3 +397,26 @@ class GiftForm(forms.ModelForm):
id=user_id id=user_id
) )
self.fields["user"].widget = forms.HiddenInput() self.fields["user"].widget = forms.HiddenInput()
class TzAwareDateTimeField(forms.DateTimeField):
def __init__(
self, input_formats=["%Y-%m-%d %H:%M:%S"], widget=SelectDateTime, **kwargs
):
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
def prepare_value(self, value):
# the db value is a datetime as a string in UTC
if isinstance(value, str):
# convert it into a naive datetime (no timezone attached)
value = parse_datetime(value)
# attach it to the UTC timezone (so that to_current_timezone()
# converts it to the local timezone)
value = timezone.make_aware(value, timezone.utc)
if isinstance(value, datetime.datetime):
value = to_current_timezone(value)
# otherwise it is formatted according to locale (in french)
value = str(value)
return value

View File

@ -44,7 +44,7 @@ from core.views import CanEditMixin, DetailFormView
class EditMembersForm(forms.Form): class EditMembersForm(forms.Form):
""" """
Add and remove members from a Group Add and remove members from a Group
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -66,7 +66,7 @@ class EditMembersForm(forms.Form):
def clean_users_added(self): def clean_users_added(self):
""" """
Check that the user is not trying to add an user already in the group Check that the user is not trying to add an user already in the group
""" """
cleaned_data = super(EditMembersForm, self).clean() cleaned_data = super(EditMembersForm, self).clean()
users_added = cleaned_data.get("users_added", None) users_added = cleaned_data.get("users_added", None)
@ -100,7 +100,7 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView): class GroupEditView(CanEditMixin, UpdateView):
""" """
Edit infos of a Group Edit infos of a Group
""" """
model = RealGroup model = RealGroup
@ -111,7 +111,7 @@ class GroupEditView(CanEditMixin, UpdateView):
class GroupCreateView(CanEditMixin, CreateView): class GroupCreateView(CanEditMixin, CreateView):
""" """
Add a new Group Add a new Group
""" """
model = RealGroup model = RealGroup
@ -121,8 +121,8 @@ class GroupCreateView(CanEditMixin, CreateView):
class GroupTemplateView(CanEditMixin, DetailFormView): class GroupTemplateView(CanEditMixin, DetailFormView):
""" """
Display all users in a given Group Display all users in a given Group
Allow adding and removing users from it Allow adding and removing users from it
""" """
model = RealGroup model = RealGroup
@ -156,7 +156,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView): class GroupDeleteView(CanEditMixin, DeleteView):
""" """
Delete a Group Delete a Group
""" """
model = RealGroup model = RealGroup

View File

@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
from django.utils import html from django.utils import html
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from django.conf import settings from django.conf import settings
from django.utils.text import slugify
import json import json
@ -73,7 +74,18 @@ def notification(request, notif_id):
def search_user(query, as_json=False): def search_user(query, as_json=False):
try: try:
res = SearchQuerySet().models(User).autocomplete(auto=html.escape(query))[:20] # slugify turns everything into ascii and every whitespace into -
# it ends by removing duplicate - (so ' - ' will turn into '-')
# replace('-', ' ') because search is whitespace based
query = slugify(query).replace("-", " ")
# TODO: is this necessary?
query = html.escape(query)
res = (
SearchQuerySet()
.models(User)
.autocomplete(auto=query)
.order_by("-last_update")[:20]
)
return [r.object for r in res] return [r.object for r in res]
except TypeError: except TypeError:
return [] return []

View File

@ -89,9 +89,9 @@ class Customer(models.Model):
def save(self, allow_negative=False, is_selling=False, *args, **kwargs): def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
""" """
is_selling : tell if the current action is a selling is_selling : tell if the current action is a selling
allow_negative : ignored if not a selling. Allow a selling to put the account in negative allow_negative : ignored if not a selling. Allow a selling to put the account in negative
Those two parameters avoid blocking the save method of a customer if his account is negative Those two parameters avoid blocking the save method of a customer if his account is negative
""" """
if self.amount < 0 and (is_selling and not allow_negative): if self.amount < 0 and (is_selling and not allow_negative):
raise ValidationError(_("Not enough money")) raise ValidationError(_("Not enough money"))
@ -527,7 +527,7 @@ class Selling(models.Model):
def save(self, allow_negative=False, *args, **kwargs): def save(self, allow_negative=False, *args, **kwargs):
""" """
allow_negative : Allow this selling to use more money than available for this user allow_negative : Allow this selling to use more money than available for this user
""" """
if not self.date: if not self.date:
self.date = timezone.now() self.date = timezone.now()

View File

@ -55,7 +55,9 @@ def write_log(instance, operation_type):
return None return None
log = OperationLog( log = OperationLog(
label=str(instance), operator=get_user(), operation_type=operation_type, label=str(instance),
operator=get_user(),
operation_type=operation_type,
).save() ).save()

View File

@ -4,6 +4,12 @@
{% trans obj=object %}Edit {{ obj }}{% endtrans %} {% trans obj=object %}Edit {{ obj }}{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2> <h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
<form action="" method="post" id="cash_summary_form"> <form action="" method="post" id="cash_summary_form">

View File

@ -1,163 +1,222 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_mini_profile, user_subscription %} {% from "core/macros.jinja" import user_mini_profile, user_subscription %}
{% macro add_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
</form>
{% endmacro %}
{% macro del_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
</form>
{% endmacro %}
{% block title %} {% block title %}
{{ counter }} {{ counter }}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h4 id="click_interface">{{ counter }}</h4> <h4 id="click_interface">{{ counter }}</h4>
<div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" />
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="bar_ui"> <div id="bar_ui">
<h5>{% trans %}Selling{% endtrans %}</h5> <noscript>
<div> <p class="important">Javascript is required for the counter UI.</p>
<div class="important"> </noscript>
{% if request.session['too_young'] %}
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p> <div id="user_info">
{% endif %} <h5>{% trans %}Customer{% endtrans %}</h5>
{% if request.session['not_allowed'] %} {{ user_mini_profile(customer.user) }}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p> {{ user_subscription(customer.user) }}
{% endif %} <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="code"> <input type="hidden" name="action" value="add_student_card">
<input type="input" name="code" value="" class="focus" id="code_field"/> {% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" />
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" /> <input type="submit" value="{% trans %}Go{% endtrans %}" />
</form> </form>
<p>{% trans %}Basket: {% endtrans %}</p> <h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %}
<ul>
{% for card in customer.student_cards.all() %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
{% else %}
{% trans %}No card registered{% endtrans %}
{% endif %}
</div>
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
<p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul>
<li v-for="p_info,p_id in basket">
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> - </button>
</form>
{{ p_info["qty"] + p_info["bonus_qty"] }}
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> + </button>
</form>
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
</li>
</ul>
<p>
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
</p>
{% endraw %}
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
</form>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
</form>
</div>
{% if counter.type == 'BAR' %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul> <ul>
{% for id,infos in request.session['basket']|dictsort %} {% for category in categories.keys() -%}
{% set product = counter.products.filter(id=id).first() %} <li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{% set s = infos['qty'] * infos['price'] / 100 %} {%- endfor %}
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }}
{{ product.name }}: {{ "%0.2f"|format(s) }}
{% if infos['bonus_qty'] %}
P
{% endif %}
</li>
{% endfor %}
</ul> </ul>
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p>
<div class="important">
{% if request.session['too_young'] %}
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_allowed'] %}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}" />
</form>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" />
</form>
</div>
{% if counter.type == 'BAR' %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" />
</form>
</div>
{% endif %}
</div>
<div id="products">
<ul>
{% for category in categories.keys() -%} {% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li> <div id="cat_{{ category|slugify }}">
{%- endfor %} <h5>{{ category }}</h5>
</ul> {% for p in categories[category] -%}
{% for category in categories.keys() -%} {% set file = None %}
<div id="cat_{{ category|slugify }}"> {% if p.icon %}
<h5>{{ category }}</h5> {% set file = p.icon.url %}
{% for p in categories[category] -%} {% else %}
{% set file = None %} {% set file = static('core/img/na.gif') %}
{% if p.icon %} {% endif %}
{% set file = p.icon.url %} <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
{% else %} {% csrf_token %}
{% set file = static('core/img/na.gif') %} <input type="hidden" name="action" value="add_product">
{% endif %} <input type="hidden" name="product_id" value="{{ p.id }}">
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %} <button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
{{ add_product(p.id, prod, "form_button") }} </form>
{%- endfor %}
</div>
{%- endfor %} {%- endfor %}
</div> </div>
{%- endfor %}
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script>
document.getElementById("click_interface").scrollIntoView();
</script>
{{ super() }} {{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script> <script>
$( function() { $( function() {
var products = [ /* Vue.JS dynamic form */
const click_form_vue = Vue.createApp({
data() {
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}",
special_selling_price: "{{ p.special_selling_price }}",
},
{%- endfor %}
},
basket: {{ request.session["basket"]|tojson }},
errors: [],
}
},
methods: {
sum_basket() {
var vm = this;
var total = 0;
for(idx in vm.basket) {
var item = vm.basket[idx];
console.log(item);
total += item["qty"] * item["price"];
}
return total / 100;
},
handle_code(event) {
var vm = this;
var code = $(event.target).find("#code_field").val().toUpperCase();
console.log("Code:");
console.log(code);
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
$(event.target).submit();
} else {
vm.handle_action(event);
}
},
handle_action(event) {
var vm = this;
var payload = $(event.target).serialize();
$.ajax({
type: 'post',
dataType: 'json',
data: payload,
success: function(response) {
vm.basket = response.basket;
vm.errors = [];
},
error: function(error) {
vm.basket = error.responseJSON.basket;
vm.errors = error.responseJSON.errors;
}
});
$('form.code_form #code_field').val("").focus();
}
}
}).mount('#bar_ui');
/* Autocompletion in the code field */
var products_autocomplete = [
{% for p in products -%} {% for p in products -%}
{ {
value: "{{ p.code }}", value: "{{ p.code }}",
@ -166,6 +225,7 @@ $( function() {
}, },
{%- endfor %} {%- endfor %}
]; ];
var quantity = ""; var quantity = "";
var search = ""; var search = "";
var pattern = /^(\d+x)?(.*)/i; var pattern = /^(\d+x)?(.*)/i;
@ -183,21 +243,22 @@ $( function() {
quantity = res[1] || ""; quantity = res[1] || "";
search = res[2]; search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" ); var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products, function( value ) { response($.grep( products_autocomplete, function( value ) {
value = value.tags; value = value.tags;
return matcher.test( value ); return matcher.test( value );
})); }));
}, },
}); });
});
$( function() { /* Accordion UI between basket and refills */
$("#bar_ui").accordion({ $("#click_form").accordion({
heightStyle: "content", heightStyle: "content",
activate: function(event, ui){ activate: function(event, ui){
$(".focus").focus(); $(".focus").focus();
} }
}); });
$("#products").tabs(); $("#products").tabs();
$("#code_field").focus(); $("#code_field").focus();
}); });
</script> </script>

View File

@ -12,6 +12,12 @@
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %} {% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>

View File

@ -5,6 +5,12 @@
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %} {% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
<h4>{% trans %}Refillings{% endtrans %}</h4> <h4>{% trans %}Refillings{% endtrans %}</h4>

View File

@ -68,18 +68,29 @@ class CounterTest(TestCase):
location, location,
{ {
"action": "refill", "action": "refill",
"amount": "10", "amount": "5",
"payment_method": "CASH", "payment_method": "CASH",
"bank": "OTHER", "bank": "OTHER",
}, },
) )
response = self.client.post(location, {"action": "code", "code": "BARB"}) response = self.client.post(location, {"action": "code", "code": "BARB"})
response = self.client.post(
location, {"action": "add_product", "product_id": "4"}
)
response = self.client.post(
location, {"action": "del_product", "product_id": "4"}
)
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
response = self.client.post(location, {"action": "code", "code": "fin"}) response = self.client.post(location, {"action": "code", "code": "fin"})
response_get = self.client.get(response.get("location")) response_get = self.client.get(response.get("location"))
response_content = response_get.content.decode("utf-8")
self.assertTrue("<li>2 x Barbar" in str(response_content))
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue( self.assertTrue(
"<p>Client : Richard Batsbak - Nouveau montant : 8.30" "<p>Client : Richard Batsbak - Nouveau montant : 3.60"
in str(response_get.content) in str(response_content)
) )

View File

@ -38,7 +38,7 @@ from django.views.generic.edit import (
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
from django.utils import timezone from django.utils import timezone
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
import re import re
import pytz import pytz
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
@ -69,6 +70,7 @@ from counter.models import (
Permanency, Permanency,
) )
from accounting.models import CurrencyField from accounting.models import CurrencyField
from core.views.forms import TzAwareDateTimeField
class CounterAdminMixin(View): class CounterAdminMixin(View):
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
current_tab = "counter" current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests
response = {"errors": []}
status = HTTPStatus.OK
if self.request.session["too_young"]:
response["errors"].append(_("Too young for that product"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_allowed"]:
response["errors"].append(_("Not allowed for that product"))
status = HTTPStatus.FORBIDDEN
if self.request.session["no_age"]:
response["errors"].append(_("No date of birth provided"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_enough"]:
response["errors"].append(_("Not enough money"))
status = HTTPStatus.PAYMENT_REQUIRED
if len(response["errors"]) > 1:
status = HTTPStatus.BAD_REQUEST
response["basket"] = self.request.session["basket"]
return JsonResponse(response, status=status)
else: # Standard HTML page
return super().render_to_response(*args, **kwargs)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
obj = self.get_object() obj = self.get_object()
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
or len(obj.get_barmen_list()) < 1 or len(obj.get_barmen_list()) < 1
): ):
raise PermissionDenied return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
else: else:
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise PermissionDenied raise PermissionDenied
@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return ret return ret
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle the many possibilities of the post request """ """Handle the many possibilities of the post request"""
self.object = self.get_object() self.object = self.get_object()
self.refill_form = None self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or ( if (self.object.type != "BAR" and not request.user.is_authenticated) or (
@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return True return True
def del_product(self, request): def del_product(self, request):
""" Delete a product from the basket """ """Delete a product from the basket"""
pid = str(request.POST["product_id"]) pid = str(request.POST["product_id"])
product = self.get_product(pid) product = self.get_product(pid)
if pid in request.session["basket"]: if pid in request.session["basket"]:
@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def finish(self, request): def finish(self, request):
""" Finish the click session, and validate the basket """ """Finish the click session, and validate the basket"""
with transaction.atomic(): with transaction.atomic():
request.session["last_basket"] = [] request.session["last_basket"] = []
if self.sum_basket(request) > self.customer.amount: if self.sum_basket(request) > self.customer.amount:
@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
def cancel(self, request): def cancel(self, request):
""" Cancel the click session """ """Cancel the click session"""
kwargs = {"counter_id": self.object.id} kwargs = {"counter_id": self.object.id}
request.session.pop("basket", None) request.session.pop("basket", None)
return HttpResponseRedirect( return HttpResponseRedirect(
@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
raise PermissionDenied raise PermissionDenied
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add customer to the context """ """Add customer to the context"""
kwargs = super(CounterClick, self).get_context_data(**kwargs) kwargs = super(CounterClick, self).get_context_data(**kwargs)
kwargs["products"] = self.object.products.select_related("product_type") kwargs["products"] = self.object.products.select_related("product_type")
kwargs["categories"] = {} kwargs["categories"] = {}
@ -1360,7 +1392,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add form to the context """ """Add form to the context"""
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs) kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
threshold = timezone.now() - timedelta( threshold = timezone.now() - timedelta(
minutes=settings.SITH_LAST_OPERATIONS_LIMIT minutes=settings.SITH_LAST_OPERATIONS_LIMIT
@ -1422,7 +1454,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id}) return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add form to the context """ """Add form to the context"""
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs) kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
kwargs["form"] = self.form kwargs["form"] = self.form
return kwargs return kwargs
@ -1448,7 +1480,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
template_name = "counter/stats.jinja" template_name = "counter/stats.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add stats to the context """ """Add stats to the context"""
from django.db.models import Sum, Case, When, F, DecimalField from django.db.models import Sum, Case, When, F, DecimalField
kwargs = super(CounterStatView, self).get_context_data(**kwargs) kwargs = super(CounterStatView, self).get_context_data(**kwargs)
@ -1553,18 +1585,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CashSummaryFormBase(forms.Form): class CashSummaryFormBase(forms.Form):
begin_date = forms.DateTimeField( begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
input_formats=["%Y-%m-%d %H:%M:%S"], end_date = TzAwareDateTimeField(label=_("End date"), required=False)
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
@ -1578,7 +1600,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add sums to the context """ """Add sums to the context"""
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs) kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
form = CashSummaryFormBase(self.request.GET) form = CashSummaryFormBase(self.request.GET)
kwargs["form"] = form kwargs["form"] = form
@ -1629,7 +1651,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
current_tab = "invoices_call" current_tab = "invoices_call"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add sums to the context """ """Add sums to the context"""
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs) kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC") kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
start_date = None start_date = None

View File

@ -133,3 +133,14 @@ Pour lancer les tests il suffit d'utiliser la commande intégrée à django.
# Lancer une méthode en particulier de cette même classe # Lancer une méthode en particulier de cette même classe
./manage.py test core.tests.UserRegistrationTest.test_register_user_form_ok ./manage.py test core.tests.UserRegistrationTest.test_register_user_form_ok
Vérifier les dépendances Javascript
-----------------------------------
Une commande a été écrite pour vérifier les éventuelles mises à jour à faire sur les librairies Javascript utilisées.
N'oubliez pas de mettre à jour à la fois le fichier de la librairie, mais également sa version dans `sith/settings.py`.
.. code-block:: bash
# Vérifier les mises à jour
./manage.py check_front

View File

@ -82,7 +82,7 @@ class EbouticMain(TemplateView):
return self.render_to_response(self.get_context_data(**kwargs)) return self.render_to_response(self.get_context_data(**kwargs))
def add_product(self, request): def add_product(self, request):
""" Add a product to the basket """ """Add a product to the basket"""
try: try:
p = self.object.products.filter(id=int(request.POST["product_id"])).first() p = self.object.products.filter(id=int(request.POST["product_id"])).first()
if not p.buying_groups.exists(): if not p.buying_groups.exists():
@ -95,7 +95,7 @@ class EbouticMain(TemplateView):
pass pass
def del_product(self, request): def del_product(self, request):
""" Delete a product from the basket """ """Delete a product from the basket"""
try: try:
p = self.object.products.filter(id=int(request.POST["product_id"])).first() p = self.object.products.filter(id=int(request.POST["product_id"])).first()
self.basket.del_product(p) self.basket.del_product(p)

View File

@ -14,6 +14,7 @@ from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from core.views.forms import SelectDateTime, MarkdownInput from core.views.forms import SelectDateTime, MarkdownInput
from election.models import Election, Role, Candidature, ElectionList, Vote from election.models import Election, Role, Candidature, ElectionList, Vote
from core.views.forms import TzAwareDateTimeField
from ajax_select.fields import AutoCompleteSelectField from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
@ -24,8 +25,8 @@ from ajax_select import make_ajax_field
class LimitedCheckboxField(forms.ModelMultipleChoiceField): class LimitedCheckboxField(forms.ModelMultipleChoiceField):
""" """
Used to replace ModelMultipleChoiceField but with Used to replace ModelMultipleChoiceField but with
automatic backend verification automatic backend verification
""" """
def __init__(self, queryset, max_choice, **kwargs): def __init__(self, queryset, max_choice, **kwargs):
@ -49,7 +50,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
class CandidateForm(forms.ModelForm): class CandidateForm(forms.ModelForm):
""" Form to candidate """ """Form to candidate"""
class Meta: class Meta:
model = Candidature model = Candidature
@ -95,7 +96,7 @@ class VoteForm(forms.Form):
class RoleForm(forms.ModelForm): class RoleForm(forms.ModelForm):
""" Form for creating a role """ """Form for creating a role"""
class Meta: class Meta:
model = Role model = Role
@ -167,30 +168,12 @@ class ElectionForm(forms.ModelForm):
label=_("candidature groups"), label=_("candidature groups"),
) )
start_date = forms.DateTimeField( start_date = TzAwareDateTimeField(label=_("Start date"), required=True)
input_formats=["%Y-%m-%d %H:%M:%S"], end_date = TzAwareDateTimeField(label=_("End date"), required=True)
label=_("Start date"), start_candidature = TzAwareDateTimeField(
widget=SelectDateTime, label=_("Start candidature"), required=True
required=True,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=True,
)
start_candidature = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start candidature"),
widget=SelectDateTime,
required=True,
)
end_candidature = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End candidature"),
widget=SelectDateTime,
required=True,
) )
end_candidature = TzAwareDateTimeField(label=_("End candidature"), required=True)
# Display elections # Display elections
@ -261,7 +244,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
return r return r
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add additionnal data to the template """ """Add additionnal data to the template"""
kwargs = super(ElectionDetailView, self).get_context_data(**kwargs) kwargs = super(ElectionDetailView, self).get_context_data(**kwargs)
kwargs["election_form"] = VoteForm(self.object, self.request.user) kwargs["election_form"] = VoteForm(self.object, self.request.user)
kwargs["election_results"] = self.object.results kwargs["election_results"] = self.object.results
@ -308,7 +291,7 @@ class VoteFormView(CanCreateMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
""" """
Verify that the user is part in a vote group Verify that the user is part in a vote group
""" """
data = form.clean() data = form.clean()
res = super(FormView, self).form_valid(form) res = super(FormView, self).form_valid(form)
@ -322,7 +305,7 @@ class VoteFormView(CanCreateMixin, FormView):
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id}) return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add additionnal data to the template """ """Add additionnal data to the template"""
kwargs = super(VoteFormView, self).get_context_data(**kwargs) kwargs = super(VoteFormView, self).get_context_data(**kwargs)
kwargs["object"] = self.election kwargs["object"] = self.election
kwargs["election"] = self.election kwargs["election"] = self.election
@ -360,7 +343,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
""" """
Verify that the selected user is in candidate group Verify that the selected user is in candidate group
""" """
obj = form.instance obj = form.instance
obj.election = Election.objects.get(id=self.election.id) obj.election = Election.objects.get(id=self.election.id)
@ -391,8 +374,8 @@ class ElectionCreateView(CanCreateMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
""" """
Allow every users that had passed the dispatch Allow every users that had passed the dispatch
to create an election to create an election
""" """
return super(CreateView, self).form_valid(form) return super(CreateView, self).form_valid(form)
@ -418,7 +401,7 @@ class RoleCreateView(CanCreateMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
""" """
Verify that the user can edit proprely Verify that the user can edit proprely
""" """
obj = form.instance obj = form.instance
if obj.election: if obj.election:
@ -461,7 +444,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
""" """
Verify that the user can vote on this election Verify that the user can vote on this election
""" """
obj = form.instance obj = form.instance
if obj.election: if obj.election:

View File

@ -52,7 +52,7 @@ class LaunderetteMainView(TemplateView):
template_name = "launderette/launderette_main.jinja" template_name = "launderette/launderette_main.jinja"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add page to the context """ """Add page to the context"""
kwargs = super(LaunderetteMainView, self).get_context_data(**kwargs) kwargs = super(LaunderetteMainView, self).get_context_data(**kwargs)
kwargs["page"] = Page.objects.filter(name="launderette").first() kwargs["page"] = Page.objects.filter(name="launderette").first()
return kwargs return kwargs
@ -142,7 +142,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
currentDate += delta currentDate += delta
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add page to the context """ """Add page to the context"""
kwargs = super(LaunderetteBookView, self).get_context_data(**kwargs) kwargs = super(LaunderetteBookView, self).get_context_data(**kwargs)
kwargs["planning"] = OrderedDict() kwargs["planning"] = OrderedDict()
kwargs["slot_type"] = self.slot_type kwargs["slot_type"] = self.slot_type
@ -481,7 +481,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
return super(LaunderetteClickView, self).get(request, *args, **kwargs) return super(LaunderetteClickView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle the many possibilities of the post request """ """Handle the many possibilities of the post request"""
self.object = self.get_object() self.object = self.get_object()
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first() self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
self.subscriber = self.customer.user self.subscriber = self.customer.user

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ from rootplace.views import delete_all_forum_user_messages
class Command(BaseCommand): class Command(BaseCommand):
""" """
Delete all forum messages from a user Delete all forum messages from a user
""" """
help = "Delete all user's forum message" help = "Delete all user's forum message"

View File

@ -94,10 +94,10 @@ def merge_users(u1, u2):
def delete_all_forum_user_messages(user, moderator, verbose=False): def delete_all_forum_user_messages(user, moderator, verbose=False):
""" """
Create a ForumMessageMeta that says a forum Create a ForumMessageMeta that says a forum
message is deleted on every forum message of an user message is deleted on every forum message of an user
user: the user to delete messages from user: the user to delete messages from
moderator: the one marked as the moderator moderator: the one marked as the moderator
""" """
for message in user.forum_messages.all(): for message in user.forum_messages.all():
if message.is_deleted(): if message.is_deleted():
@ -145,9 +145,9 @@ class MergeUsersView(FormView):
class DeleteAllForumUserMessagesView(FormView): class DeleteAllForumUserMessagesView(FormView):
""" """
Delete all forum messages from an user Delete all forum messages from an user
Messages are soft deleted and are still visible from admins Messages are soft deleted and are still visible from admins
GUI frontend to the dedicated command GUI frontend to the dedicated command
""" """
template_name = "rootplace/delete_user_messages.jinja" template_name = "rootplace/delete_user_messages.jinja"

View File

@ -280,7 +280,8 @@ SITH_NAME = "Sith website"
SITH_TWITTER = "@ae_utbm" SITH_TWITTER = "@ae_utbm"
# AE configuration # AE configuration
SITH_MAIN_CLUB_ID = 1 # TODO: keep only that first setting, with the ID, and do the same for the other clubs # TODO: keep only that first setting, with the ID, and do the same for the other clubs
SITH_MAIN_CLUB_ID = 1
SITH_MAIN_CLUB = { SITH_MAIN_CLUB = {
"name": "AE", "name": "AE",
"unix_name": "ae", "unix_name": "ae",
@ -477,14 +478,14 @@ SITH_SUBSCRIPTION_END = 10
# Subscription durations are in semestres # Subscription durations are in semestres
# Be careful, modifying this parameter will need a migration to be applied # Be careful, modifying this parameter will need a migration to be applied
SITH_SUBSCRIPTIONS = { SITH_SUBSCRIPTIONS = {
"un-semestre": {"name": _("One semester"), "price": 15, "duration": 1}, "un-semestre": {"name": _("One semester"), "price": 20, "duration": 1},
"deux-semestres": {"name": _("Two semesters"), "price": 28, "duration": 2}, "deux-semestres": {"name": _("Two semesters"), "price": 35, "duration": 2},
"cursus-tronc-commun": { "cursus-tronc-commun": {
"name": _("Common core cursus"), "name": _("Common core cursus"),
"price": 45, "price": 60,
"duration": 4, "duration": 4,
}, },
"cursus-branche": {"name": _("Branch cursus"), "price": 45, "duration": 6}, "cursus-branche": {"name": _("Branch cursus"), "price": 60, "duration": 6},
"cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6}, "cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666}, "membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
"assidu": {"name": _("Assidu member"), "price": 0, "duration": 2}, "assidu": {"name": _("Assidu member"), "price": 0, "duration": 2},
@ -497,6 +498,7 @@ SITH_SUBSCRIPTIONS = {
"price": 0, "price": 0,
"duration": 1, "duration": 1,
}, },
"un-mois-essai": {"name": _("One month for free"), "price": 0, "duration": 0.166},
"deux-mois-essai": {"name": _("Two months for free"), "price": 0, "duration": 0.33}, "deux-mois-essai": {"name": _("Two months for free"), "price": 0, "duration": 0.33},
"benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1}, "benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1},
"six-semaines-essai": { "six-semaines-essai": {
@ -505,6 +507,7 @@ SITH_SUBSCRIPTIONS = {
"duration": 0.23, "duration": 0.23,
}, },
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333}, "un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
"membre-staff-ga": {"name": _("GA staff member"), "price": 1, "duration": 0.076},
# Discount subscriptions # Discount subscriptions
"un-semestre-reduction": { "un-semestre-reduction": {
"name": _("One semester (-20%)"), "name": _("One semester (-20%)"),
@ -530,6 +533,12 @@ SITH_SUBSCRIPTIONS = {
"name": _("Alternating cursus (-20%)"), "name": _("Alternating cursus (-20%)"),
"price": 24, "price": 24,
"duration": 6, "duration": 6,
},
# CA special offer
"un-an-offert-CA": {
"name": _("One year for free(CA offer)"),
"price": 0,
"duration": 2,
} }
# To be completed.... # To be completed....
} }
@ -665,3 +674,17 @@ if "test" in sys.argv:
if SENTRY_DSN: if SENTRY_DSN:
# Connection to sentry # Connection to sentry
sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()]) sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()])
SITH_FRONT_DEP_VERSIONS = {
"https://github.com/chartjs/Chart.js/": "2.6.0",
"https://github.com/xdan/datetimepicker/": "2.5.21",
"https://github.com/Ionaru/easy-markdown-editor/": "2.7.0",
"https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
"https://github.com/jquery/jquery/": "3.1.0",
"https://github.com/sethmcl/jquery-ui/": "1.11.1",
"https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
"https://github.com/vuejs/vue-next": "3.2.18",
}

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -* # -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017,2021
# - Sli <antoine@bartuccio.fr> # - Sli <antoine@bartuccio.fr>
# - Skia <skia@hya.sk>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -27,7 +28,10 @@ from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel
class TemplatesPanel(BaseTemplatesPanel): class TemplatesPanel(BaseTemplatesPanel):
def generate_stats(self, *args): def generate_stats(self, *args):
template = self.templates[0]["template"] try:
if not hasattr(template, "engine") and hasattr(template, "backend"): template = self.templates[0]["template"]
template.engine = template.backend if not hasattr(template, "engine") and hasattr(template, "backend"):
template.engine = template.backend
except IndexError: # No template
pass
return super().generate_stats(*args) return super().generate_stats(*args)

View File

@ -120,8 +120,7 @@ class ShoppingList(models.Model):
class ShoppingListItem(models.Model): class ShoppingListItem(models.Model):
""" """"""
"""
shopping_lists = models.ManyToManyField( shopping_lists = models.ManyToManyField(
ShoppingList, ShoppingList,

View File

@ -0,0 +1,45 @@
# Generated by Django 2.2.13 on 2020-06-15 12:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("subscription", "0011_auto_20190825_2215"),
]
operations = [
migrations.AlterField(
model_name="subscription",
name="subscription_type",
field=models.CharField(
choices=[
("amicale/doceo", "Amicale/DOCEO member"),
("assidu", "Assidu member"),
("benevoles-euroks", "Eurok's volunteer"),
("crous", "CROUS member"),
("cursus-alternant", "Alternating cursus"),
("cursus-alternant-reduction", "Alternating cursus (-20%)"),
("cursus-branche", "Branch cursus"),
("cursus-branche-reduction", "Branch cursus (-20%)"),
("cursus-tronc-commun", "Common core cursus"),
("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
("deux-mois-essai", "Two months for free"),
("deux-semestres", "Two semesters"),
("deux-semestres-reduction", "Two semesters (-20%)"),
("membre-honoraire", "Honorary member"),
("membre-staff-ga", "GA staff member"),
("reseau-ut", "UT network member"),
("sbarro/esta", "Sbarro/ESTA member"),
("six-semaines-essai", "Six weeks for free"),
("un-jour", "One day"),
("un-semestre", "One semester"),
("un-semestre-reduction", "One semester (-20%)"),
("un-semestre-welcome", "One semester Welcome Week"),
],
max_length=255,
verbose_name="subscription type",
),
),
]

View File

@ -0,0 +1,46 @@
# Generated by Django 2.2.13 on 2020-08-28 19:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("subscription", "0012_auto_20200615_1438"),
]
operations = [
migrations.AlterField(
model_name="subscription",
name="subscription_type",
field=models.CharField(
choices=[
("amicale/doceo", "Amicale/DOCEO member"),
("assidu", "Assidu member"),
("benevoles-euroks", "Eurok's volunteer"),
("crous", "CROUS member"),
("cursus-alternant", "Alternating cursus"),
("cursus-alternant-reduction", "Alternating cursus (-20%)"),
("cursus-branche", "Branch cursus"),
("cursus-branche-reduction", "Branch cursus (-20%)"),
("cursus-tronc-commun", "Common core cursus"),
("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
("deux-mois-essai", "Two months for free"),
("deux-semestres", "Two semesters"),
("deux-semestres-reduction", "Two semesters (-20%)"),
("membre-honoraire", "Honorary member"),
("membre-staff-ga", "GA staff member"),
("reseau-ut", "UT network member"),
("sbarro/esta", "Sbarro/ESTA member"),
("six-semaines-essai", "Six weeks for free"),
("un-jour", "One day"),
("un-mois-essai", "One month for free"),
("un-semestre", "One semester"),
("un-semestre-reduction", "One semester (-20%)"),
("un-semestre-welcome", "One semester Welcome Week"),
],
max_length=255,
verbose_name="subscription type",
),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 2.2.17 on 2020-12-07 22:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("subscription", "0013_auto_20200828_2117"),
]
operations = [
migrations.AlterField(
model_name="subscription",
name="subscription_type",
field=models.CharField(
choices=[
("amicale/doceo", "Amicale/DOCEO member"),
("assidu", "Assidu member"),
("benevoles-euroks", "Eurok's volunteer"),
("crous", "CROUS member"),
("cursus-alternant", "Alternating cursus"),
("cursus-alternant-reduction", "Alternating cursus (-20%)"),
("cursus-branche", "Branch cursus"),
("cursus-branche-reduction", "Branch cursus (-20%)"),
("cursus-tronc-commun", "Common core cursus"),
("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
("deux-mois-essai", "Two months for free"),
("deux-semestres", "Two semesters"),
("deux-semestres-reduction", "Two semesters (-20%)"),
("membre-honoraire", "Honorary member"),
("membre-staff-ga", "GA staff member"),
("reseau-ut", "UT network member"),
("sbarro/esta", "Sbarro/ESTA member"),
("six-semaines-essai", "Six weeks for free"),
("un-an-offert-CA", "One year for free(CA offer)"),
("un-jour", "One day"),
("un-mois-essai", "One month for free"),
("un-semestre", "One semester"),
("un-semestre-reduction", "One semester (-20%)"),
("un-semestre-welcome", "One semester Welcome Week"),
],
max_length=255,
verbose_name="subscription type",
),
),
]

View File

@ -114,6 +114,18 @@ class SubscriptionIntegrationTest(TestCase):
call_command("populate") call_command("populate")
self.user = User.objects.filter(username="public").first() self.user = User.objects.filter(username="public").first()
def test_duration_one_month(self):
s = Subscription(
member=User.objects.filter(pk=self.user.pk).first(),
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
)
s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.166, start=s.subscription_start)
s.save()
self.assertTrue(s.subscription_end == date(2017, 9, 29))
def test_duration_two_months(self): def test_duration_two_months(self):
s = Subscription( s = Subscription(
@ -122,11 +134,11 @@ class SubscriptionIntegrationTest(TestCase):
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0], payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
) )
s.subscription_start = date(2017, 8, 29) s.subscription_start = date(2017, 8, 29)
s.subscription_end = s.compute_end(duration=0.33, start=s.subscription_start) s.subscription_end = s.compute_end(duration=0.333, start=s.subscription_start)
s.save() s.save()
self.assertTrue(s.subscription_end == date(2017, 10, 29)) self.assertTrue(s.subscription_end == date(2017, 10, 29))
def test_duration_two_months(self): def test_duration_one_day(self):
s = Subscription( s = Subscription(
member=User.objects.filter(pk=self.user.pk).first(), member=User.objects.filter(pk=self.user.pk).first(),

View File

@ -36,22 +36,17 @@ from subscription.models import Subscription
from core.views.forms import SelectDateTime from core.views.forms import SelectDateTime
from core.models import User from core.models import User
from core.views.forms import SelectDate from core.views.forms import SelectDate
from core.views.forms import TzAwareDateTimeField
class SelectionDateForm(forms.Form): class SelectionDateForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SelectionDateForm, self).__init__(*args, **kwargs) super(SelectionDateForm, self).__init__(*args, **kwargs)
self.fields["start_date"] = forms.DateTimeField( self.fields["start_date"] = TzAwareDateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"], label=_("Start date"), required=True
label=_("Start date"),
widget=SelectDateTime,
required=True,
) )
self.fields["end_date"] = forms.DateTimeField( self.fields["end_date"] = TzAwareDateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"], label=_("End date"), required=True
label=_("End date"),
widget=SelectDateTime,
required=True,
) )

View File

@ -33,6 +33,7 @@
</div> </div>
<div>{{ u.user.get_display_name() }}</div> <div>{{ u.user.get_display_name() }}</div>
<div><a href="{{ url('trombi:delete_user', user_id=u.id) }}">{% trans %}Delete{% endtrans %}</a></div> <div><a href="{{ url('trombi:delete_user', user_id=u.id) }}">{% trans %}Delete{% endtrans %}</a></div>
<div><a href="{{ url('trombi:create_membership', user_id=u.id) }}">{% trans %}Add club membership{% endtrans %}</a></div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -* # -*- coding:utf-8 -*
# #
# Copyright 2017 # Copyright 2017,2020
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -81,4 +82,9 @@ urlpatterns = [
UserTrombiDeleteMembershipView.as_view(), UserTrombiDeleteMembershipView.as_view(),
name="delete_membership", name="delete_membership",
), ),
re_path(
r"^membership/(?P<user_id>[0-9]+)/create$",
UserTrombiAddMembershipView.as_view(),
name="create_membership",
),
] ]

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -* # -*- coding:utf-8 -*
# #
# Copyright 2017 # Copyright 2017,2020
# - Skia <skia@libskia.so> # - Skia <skia@libskia.so>
# - Sli <antoine.bartuccio@gmail.com>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -31,6 +32,7 @@ from django.utils.translation import ugettext_lazy as _
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied
from ajax_select.fields import AutoCompleteSelectField from ajax_select.fields import AutoCompleteSelectField
@ -410,6 +412,35 @@ class UserTrombiDeleteMembershipView(TrombiTabsMixin, CanEditMixin, DeleteView):
) )
# Used by admins when someone does not have every club in his list
class UserTrombiAddMembershipView(TrombiTabsMixin, CreateView):
model = TrombiClubMembership
template_name = "core/edit.jinja"
fields = ["club", "role", "start", "end"]
pk_url_kwarg = "user_id"
current_tab = "profile"
def dispatch(self, request, *arg, **kwargs):
self.trombi_user = get_object_or_404(TrombiUser, pk=kwargs["user_id"])
if not self.trombi_user.trombi.is_owned_by(request.user):
raise PermissionDenied()
return super(UserTrombiAddMembershipView, self).dispatch(
request, *arg, **kwargs
)
def form_valid(self, form):
membership = form.save(commit=False)
membership.user = self.trombi_user
membership.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse(
"trombi:detail", kwargs={"trombi_id": self.trombi_user.trombi.id}
)
class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView): class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView):
model = TrombiClubMembership model = TrombiClubMembership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"