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:
stage: test
script:
- env
- apt-get update
- apt-get install -y gettext python3-xapian libgraphviz-dev
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
- pip install -r requirements.txt
- pip install coverage
- pip install -U -r requirements.txt
- pip install -U coverage
- mkdir -p /dev/shm/search_indexes
- ln -s /dev/shm/search_indexes sith/search_indexes
- ./manage.py compilemessages
- coverage run ./manage.py test
- coverage html
- coverage report
- cd doc
- make html # Make documentation
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_tests"
cache:
paths:
- .cache/pip_tests
artifacts:
paths:
- coverage_report/
@ -24,5 +32,10 @@ test:
black:
stage: test
script:
- pip install black
- pip install -U black
- 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.urls import reverse
from django.core.management import call_command
from datetime import date
from datetime import date, timedelta
from core.models import User
from accounting.models import (
@ -110,6 +110,9 @@ class JournalTest(TestCase):
class OperationTest(TestCase):
def setUp(self):
call_command("populate")
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y"
)
self.journal = GeneralJournal.objects.filter(id=1).first()
self.skia = User.objects.filter(username="skia").first()
at = AccountingType(
@ -158,7 +161,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -191,7 +194,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -218,7 +221,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome du jour",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
@ -245,7 +248,7 @@ class OperationTest(TestCase):
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de l'aurore",
"date": "04/12/2020",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",

View File

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

View File

@ -33,8 +33,8 @@ from core.views import can_view, can_edit
def check_if(obj, user, test):
"""
Detect if it's a single object or a queryset
aply a given test on individual object and return global permission
Detect if it's a single object or a queryset
aply a given test on individual object and return global permission
"""
if isinstance(obj, QuerySet):
for o in obj:
@ -49,7 +49,7 @@ class ManageModelMixin:
@action(detail=True)
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))
serializer = self.get_serializer(self.queryset)

View File

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

View File

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

View File

@ -45,7 +45,7 @@ class CounterSerializer(serializers.ModelSerializer):
class CounterViewSet(RightModelViewSet):
"""
Manage Counters (api/v1/counter/)
Manage Counters (api/v1/counter/)
"""
serializer_class = CounterSerializer
@ -54,7 +54,7 @@ class CounterViewSet(RightModelViewSet):
@action(detail=False)
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")
serializer = self.get_serializer(self.queryset, many=True)

View File

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

View File

@ -72,7 +72,7 @@ class LaunderetteTokenSerializer(serializers.ModelSerializer):
class LaunderettePlaceViewSet(RightModelViewSet):
"""
Manage Launderette (api/v1/launderette/place/)
Manage Launderette (api/v1/launderette/place/)
"""
serializer_class = LaunderettePlaceSerializer
@ -81,7 +81,7 @@ class LaunderettePlaceViewSet(RightModelViewSet):
class LaunderetteMachineViewSet(RightModelViewSet):
"""
Manage Washing Machines (api/v1/launderette/machine/)
Manage Washing Machines (api/v1/launderette/machine/)
"""
serializer_class = LaunderetteMachineSerializer
@ -90,7 +90,7 @@ class LaunderetteMachineViewSet(RightModelViewSet):
class LaunderetteTokenViewSet(RightModelViewSet):
"""
Manage Launderette's tokens (api/v1/launderette/token/)
Manage Launderette's tokens (api/v1/launderette/token/)
"""
serializer_class = LaunderetteTokenSerializer
@ -99,7 +99,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
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")
serializer = self.get_serializer(self.queryset, many=True)
@ -108,7 +108,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
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")
serializer = self.get_serializer(self.queryset, many=True)
@ -117,7 +117,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
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(
borrow_date__isnull=True, user__isnull=True
@ -128,7 +128,7 @@ class LaunderetteTokenViewSet(RightModelViewSet):
@action(detail=False)
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(
borrow_date__isnull=False, user__isnull=False

View File

@ -50,8 +50,8 @@ class UserSerializer(serializers.ModelSerializer):
class UserViewSet(RightModelViewSet):
"""
Manage Users (api/v1/user/)
Only show active users
Manage Users (api/v1/user/)
Only show active users
"""
serializer_class = UserSerializer
@ -60,7 +60,7 @@ class UserViewSet(RightModelViewSet):
@action(detail=False)
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()
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):
"""
Uses the UTBM API to find an UV.
short_uv is the UV entry in the UV list. It is returned as it contains
information which are not in full_uv.
full_uv is the detailed representation of 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
information which are not in full_uv.
full_uv is the detailed representation of an UV.
"""
# query the UV list
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):
"""
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 = {}

View File

@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User
from core.views.forms import SelectDate, SelectDateTime
from counter.models import Counter
from core.views.forms import TzAwareDateTimeField
class ClubEditForm(forms.ModelForm):
@ -158,18 +159,9 @@ class MailingForm(forms.Form):
class SellingsForm(forms.Form):
begin_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
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,
)
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
counters = forms.ModelMultipleChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
)
@ -252,8 +244,8 @@ class ClubMemberForm(forms.Form):
def clean_users(self):
"""
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
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
"""
cleaned_data = super(ClubMemberForm, self).clean()
users = []
@ -276,7 +268,7 @@ class ClubMemberForm(forms.Form):
def clean(self):
"""
Check user rights for adding an user
Check user rights for adding an user
"""
cleaned_data = super(ClubMemberForm, self).clean()

View File

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

View File

@ -6,152 +6,150 @@
{% endblock %}
{% 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">
{% 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">
{% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(type="NOTICE") %}
<section class="news_notice">
<h4><a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
<span>{{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}</div>
</section>
{% endfor %}
{% 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') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if events_dates %}
{% for d in events_dates %}
<div class="news_events_group">
<div class="news_events_group_date">
<div>
<div>{{ d|localtime|date('D') }}</div>
<div class="day">{{ d|localtime|date('d') }}</div>
<div>{{ d|localtime|date('b') }}</div>
</div>
</div>
<div class="news_events_group_items">
{% for news in object_list.filter(dates__start_date__gte=d,
dates__start_date__lte=d+timedelta(days=1),
type="EVENT").exclude(dates__end_date__lt=timezone.now())
.order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
<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 class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
{% endif %}
{% 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') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if events_dates %}
{% for d in events_dates %}
<div class="news_events_group">
<div class="news_events_group_date">
<div>
<div>{{ d|localtime|date('D') }}</div>
<div class="day">{{ d|localtime|date('d') }}</div>
<div>{{ d|localtime|date('b') }}</div>
</div>
</div>
<div class="news_events_group_items">
{% for news in object_list.filter(dates__start_date__gte=d,
dates__start_date__lte=d+timedelta(days=1),
type="EVENT").exclude(dates__end_date__lt=timezone.now())
.order_by('dates__start_date') %}
<section class="news_event">
<div class="club_logo">
{% if news.club.logo %}
<img src="{{ news.club.logo.url }}" alt="{{ news.club }}" />
{% else %}
<img src="{{ static("com/img/news.png") }}" alt="{{ news.club }}" />
{% endif %}
</div>
<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 class="news_date">
<span>{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div class="news_content">{{ news.summary|markdown }}
<div class="button_bar">
{{ fb_quick(news) }}
{{ tweet_quick(news) }}
</div>
</div>
</section>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<div class="news_empty">
<em>{% trans %}Nothing to come...{% endtrans %}</em>
</div>
{% endif %}
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
type="EVENT").order_by('dates__start_date') %}
{% if coming_soon %}
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
{% for news in coming_soon %}
<section class="news_coming_soon">
<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) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</section>
{% endfor %}
{% endif %}
{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5),
type="EVENT").order_by('dates__start_date') %}
{% if coming_soon %}
<h3>{% trans %}Coming soon... don't miss!{% endtrans %}</h3>
{% for news in coming_soon %}
<section class="news_coming_soon">
<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) }}
{{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} -
{{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }}
{{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }}</span>
</section>
{% endfor %}
{% endif %}
</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>
{% endblock %}

View File

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

View File

@ -7,15 +7,33 @@
{% block content %}
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
{% 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>
{% if bad_recipients %}
<p>
<span class="important">
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
</span>
<ul>
{% for r in bad_recipients.keys() %}
<li>{{ r }}</li>
{% 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 %}
<hr>
{{ weekmail_rendered|safe }}

View File

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

View File

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

View File

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

View File

@ -35,9 +35,9 @@ from core.scss.storage import ScssFileStorage, find_file
class ScssProcessor(object):
"""
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
If DEBUG mode enabled : compile the scss file
Else : give the path of the corresponding css supposed to already be compiled
Don't forget to use compilestatics to compile scss for production
"""
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):
text = indexes.CharField(document=True, use_template=True)
auto = indexes.EdgeNgramField(use_template=True)
last_update = indexes.DateTimeField(model_attr="last_update")
def get_model(self):
return User
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
def get_updated_field(self):
return "last_update"
def prepare_auto(self, obj):
return self.prepared_data["auto"].strip()[:245]
class IndexSignalProcessor(signals.BaseSignalProcessor):
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);
$background-bouton-color: hsl(0, 0%, 90%);
$background-button-color: hsl(0, 0%, 95%);
/*--------------------------MEDIA QUERY HELPERS------------------------*/
$small-devices: 576px;
@ -47,10 +47,11 @@ body {
input[type=button], input[type=submit], input[type=reset],input[type=file] {
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-weight: bold;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
cursor: pointer;
box-shadow: $shadow-color 0px 0px 1px;
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
button{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 10px;
font-size: 14px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.18em;
border-radius: 5px;
box-shadow: $shadow-color 0px 0px 1px;
cursor: pointer;
@ -75,24 +77,26 @@ button{
input,textarea[type=text],[type=number]{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
padding: 7px;
font-size: 16px;
background-color: $background-button-color;
padding: 0.4em;
margin: 0.1em;
font-size: 1.2em;
border-radius: 5px;
max-width: 95%;
}
textarea{
border: none;
text-decoration: none;
background-color: $background-bouton-color;
background-color: $background-button-color;
padding: 7px;
font-size: 16px;
font-size: 1.2em;
border-radius: 5px;
}
select{
border: none;
text-decoration: none;
font-size: 15px;
background-color: $background-bouton-color;
font-size: 1.2em;
background-color: $background-button-color;
padding: 10px;
border-radius: 5px;
cursor: pointer;
@ -130,9 +134,10 @@ a {
#header_language_chooser {
position: absolute;
top: 0.2em;
right: 0.5em;
top: 2em;
left: 0.5em;
width: 3%;
min-width: 2.2em;
text-align: center;
input {
display: block;
@ -157,9 +162,6 @@ header {
border-radius: 0px 0px 10px 10px;
#header_logo {
display: inline-block;
flex: none;
background-size: 100% 100%;
background-color: $white-color;
padding: 0.2em;
border-radius: 0px 0px 0px 9px;
@ -169,11 +171,19 @@ header {
margin: 0px;
width: 100%;
height: 100%;
img {
max-width: 70%;
max-height: 100%;
margin: auto;
display: block;
}
}
}
#header_connect_links {
margin: 0.6em 0.6em 0em auto;
padding: 0.2em;
color: $white-color;
form {
display: inline;
@ -190,8 +200,9 @@ header {
#header_bar {
display: flex;
flex: auto;
flex-wrap: wrap;
width: 80%;
a {
text-decoration: none;
margin: 0em 1em;
@ -203,7 +214,6 @@ header {
}
#header_bars_infos {
width: 35ch;
flex: initial;
list-style-type: none;
margin: 0.2em 0.2em;
@ -213,12 +223,15 @@ header {
display: inline-block;
flex: auto;
margin: 0.8em 0em;
input {
width: 14ch;
}
}
#header_user_links {
display: flex;
width: 120ch;
flex: initial;
flex-wrap: wrap;
text-align: right;
margin: 0em;
div {
@ -287,42 +300,34 @@ header {
#info_boxes {
display: flex;
flex-wrap: wrap;
width: 90%;
margin: 1em auto;
p {
margin: 0px;
padding: 7px;
}
#alert_box, #info_box {
font-size: 14px;
display: inline-block;
flex: auto;
padding: 2px;
margin: 0.2em 1.5%;
min-width: 10%;
max-width: 46%;
min-height: 20px;
flex: 49%;
font-size: 0.9em;
margin: 0.2em;
border-radius: 0.6em;
.markdown {
margin: 0.5em;
}
&:before {
float: left;
font-family: FontAwesome;
font-size: 4em;
float: right;
margin: 0.2em;
}
}
#info_box {
border-radius: 10px;
background: $primary-neutral-light-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f05a";
color: hsl(210, 100%, 56%);
}
}
#alert_box {
border-radius: 10px;
background: $second-color;
&:before {
font-family: FontAwesome;
font-size: 4em;
content: "\f06a";
color: $white-color;
}
@ -345,12 +350,12 @@ header {
a {
flex: auto;
text-align: center;
padding: 20px;
padding: 1.5em;
color: $white-color;
font-style: normal;
font-weight: bolder;
text-decoration: none;
&:hover {
background: $secondary-neutral-color;
color: $white-color;
@ -458,6 +463,8 @@ header {
/*---------------------------------NEWS--------------------------------*/
#news {
display: flex;
flex-wrap: wrap;
.news_column {
display: inline-block;
margin: 0px;
@ -467,11 +474,13 @@ header {
margin-bottom: 1em;
}
#right_column {
width: 20%;
flex: 20%;
float: right;
margin: 0.2em;
}
#left_column {
width: 79%;
flex: 79%;
margin: 0.2em;
h3 {
background: $second-color;
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 {
@ -691,6 +705,12 @@ header {
}
}
@media screen and (max-width: $small-devices){
#page {
width: 98%;
}
}
#news_details {
display: inline-block;
margin-top: 20px;
@ -723,7 +743,7 @@ header {
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
font-size: 1.2em;
border-radius: 2px;
float: right;
display: block;
@ -1111,33 +1131,36 @@ u, .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 {
float: left;
min-width: 57%;
}
padding: 0.4em;
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
#user_info_container {}
#products {
flex-basis: 100%;
margin: 0.2em;
overflow: auto;
}
#user_info {
float: right;
padding: 5px;
width: 40%;
margin: 0px auto;
background: $secondary-neutral-light-color;
#click_form {
flex: auto;
margin: 0.2em;
}
#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----------------------------*/
@ -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 {
margin: 10px 0px;
display: flex;
flex-wrap: wrap;
height: 20p;
align-items: center;
}
@ -1551,6 +1580,7 @@ footer {
color: $white-color;
border-radius: 5px;
display: flex;
flex-wrap: wrap;
background-color: $primary-neutral-dark-color;
box-shadow: $shadow-color 0px 0px 15px;
a {
@ -2181,4 +2211,4 @@ $pedagogy-white-text: #f0f0f0;
}
}
}
}
}

View File

@ -3,6 +3,7 @@
<head>
{% block head %}
<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="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
@ -27,6 +28,7 @@
<!-- BEGIN HEADER -->
{% block header %}
{% if not popup %}
<header>
<div id="header_language_chooser">
{% for language in LANGUAGES %}
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
@ -37,10 +39,11 @@
{% endfor %}
</div>
<header>
{% if not user.is_authenticated %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_connect_links">
<form method="post" action="{{ url('core:login') }}">
@ -54,12 +57,14 @@
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
</div>
{% else %}
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;">
<a href="{{ url('core:index') }}"></a>
<div id="header_logo">
<a href="{{ url('core:index') }}">
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
</a>
</div>
<div id="header_bar">
<ul id="header_bars_infos">
{% cache 100 counters_activity %}
{% cache 100 "counters_activity" %}
{% for bar in Counter.objects.filter(type="BAR").all() %}
<li>
<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>
</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">
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
<li>
@ -126,17 +131,19 @@
</header>
<div id="info_boxes">
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% block info_boxes %}
{% set sith = get_sith() %}
{% if sith.alert_msg %}
<div id="alert_box">
{{ sith.alert_msg|markdown }}
</div>
{% endif %}
{% if sith.info_msg %}
<div id="info_box">
{{ sith.info_msg|markdown }}
</div>
{% endif %}
{% endblock %}
</div>
{% else %}{# if not popup #}

View File

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

View File

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

View File

@ -12,7 +12,7 @@
{% for picture in pictures[a.id] %}
<div class="picture">
<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>
</div>
{% endfor %}

View File

@ -1,3 +1,13 @@
{{ object.first_name }}
{{ object.last_name }}
{{ object.nick_name }}
{% load search_helpers %}
{% 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()
def scss(path):
"""
Return path of the corresponding css file after compilation
Return path of the corresponding css file after compilation
"""
processor = ScssProcessor(path)
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 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):
"""
Get current group from id in url
Get current group from id in url
"""
return self.cached_object
@cached_property
def cached_object(self):
"""
Optimisation on group retrieval
Optimisation on group retrieval
"""
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 ajax_select.fields import AutoCompleteSelectField
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
@ -114,14 +118,11 @@ class SelectFile(TextInput):
attrs["class"] = "select_file"
else:
attrs = {"class": "select_file"}
output = (
'%(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"),
"name": name,
}
)
output = '%(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"),
"name": name,
}
output += (
'<span name="'
+ name
@ -138,14 +139,11 @@ class SelectUser(TextInput):
attrs["class"] = "select_user"
else:
attrs = {"class": "select_user"}
output = (
'%(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"),
"name": name,
}
)
output = '%(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"),
"name": name,
}
output += (
'<span name="'
+ name
@ -399,3 +397,26 @@ class GiftForm(forms.ModelForm):
id=user_id
)
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):
"""
Add and remove members from a Group
Add and remove members from a Group
"""
def __init__(self, *args, **kwargs):
@ -66,7 +66,7 @@ class EditMembersForm(forms.Form):
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()
users_added = cleaned_data.get("users_added", None)
@ -100,7 +100,7 @@ class GroupListView(CanEditMixin, ListView):
class GroupEditView(CanEditMixin, UpdateView):
"""
Edit infos of a Group
Edit infos of a Group
"""
model = RealGroup
@ -111,7 +111,7 @@ class GroupEditView(CanEditMixin, UpdateView):
class GroupCreateView(CanEditMixin, CreateView):
"""
Add a new Group
Add a new Group
"""
model = RealGroup
@ -121,8 +121,8 @@ class GroupCreateView(CanEditMixin, CreateView):
class GroupTemplateView(CanEditMixin, DetailFormView):
"""
Display all users in a given Group
Allow adding and removing users from it
Display all users in a given Group
Allow adding and removing users from it
"""
model = RealGroup
@ -156,7 +156,7 @@ class GroupTemplateView(CanEditMixin, DetailFormView):
class GroupDeleteView(CanEditMixin, DeleteView):
"""
Delete a Group
Delete a Group
"""
model = RealGroup

View File

@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
from django.utils import html
from django.views.generic import ListView, TemplateView
from django.conf import settings
from django.utils.text import slugify
import json
@ -73,7 +74,18 @@ def notification(request, notif_id):
def search_user(query, as_json=False):
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]
except TypeError:
return []

View File

@ -89,9 +89,9 @@ class Customer(models.Model):
def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
"""
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
Those two parameters avoid blocking the save method of a customer if his account is negative
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
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):
raise ValidationError(_("Not enough money"))
@ -527,7 +527,7 @@ class Selling(models.Model):
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:
self.date = timezone.now()

View File

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

View File

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

View File

@ -1,163 +1,222 @@
{% extends "core/base.jinja" %}
{% 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 %}
{{ counter }}
{% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<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">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
<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>
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<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="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<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>
<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>
{% for id,infos in request.session['basket']|dictsort %}
{% set product = counter.products.filter(id=id).first() %}
{% set s = infos['qty'] * infos['price'] / 100 %}
<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 %}
{% for category in categories.keys() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</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() -%}
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
{%- endfor %}
</ul>
{% for category in categories.keys() -%}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% else %}
{% set file = static('core/img/na.gif') %}
{% endif %}
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %}
{{ add_product(p.id, prod, "form_button") }}
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% else %}
{% set file = static('core/img/na.gif') %}
{% endif %}
<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">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
</form>
{%- endfor %}
</div>
{%- endfor %}
</div>
{%- endfor %}
</div>
{% endblock %}
{% block script %}
<script>
document.getElementById("click_interface").scrollIntoView();
</script>
{{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script>
$( 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 -%}
{
value: "{{ p.code }}",
@ -166,6 +225,7 @@ $( function() {
},
{%- endfor %}
];
var quantity = "";
var search = "";
var pattern = /^(\d+x)?(.*)/i;
@ -183,21 +243,22 @@ $( function() {
quantity = res[1] || "";
search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products, function( value ) {
response($.grep( products_autocomplete, function( value ) {
value = value.tags;
return matcher.test( value );
}));
},
});
});
$( function() {
$("#bar_ui").accordion({
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: function(event, ui){
$(".focus").focus();
}
});
$("#products").tabs();
$("#code_field").focus();
});
</script>

View File

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

View File

@ -68,18 +68,29 @@ class CounterTest(TestCase):
location,
{
"action": "refill",
"amount": "10",
"amount": "5",
"payment_method": "CASH",
"bank": "OTHER",
},
)
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_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(
"<p>Client : Richard Batsbak - Nouveau montant : 8.30"
in str(response_get.content)
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
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 import CheckboxSelectMultiple
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 import forms
from django.utils.translation import ugettext_lazy as _
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
import re
import pytz
from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field
@ -69,6 +70,7 @@ from counter.models import (
Permanency,
)
from accounting.models import CurrencyField
from core.views.forms import TzAwareDateTimeField
class CounterAdminMixin(View):
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
pk_url_kwarg = "counter_id"
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):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
obj = self.get_object()
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
or len(obj.get_barmen_list()) < 1
):
raise PermissionDenied
return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
else:
if not request.user.is_authenticated:
raise PermissionDenied
@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return ret
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.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return True
def del_product(self, request):
""" Delete a product from the basket """
"""Delete a product from the basket"""
pid = str(request.POST["product_id"])
product = self.get_product(pid)
if pid in request.session["basket"]:
@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
return self.render_to_response(context)
def finish(self, request):
""" Finish the click session, and validate the basket """
"""Finish the click session, and validate the basket"""
with transaction.atomic():
request.session["last_basket"] = []
if self.sum_basket(request) > self.customer.amount:
@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
def cancel(self, request):
""" Cancel the click session """
"""Cancel the click session"""
kwargs = {"counter_id": self.object.id}
request.session.pop("basket", None)
return HttpResponseRedirect(
@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
raise PermissionDenied
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["products"] = self.object.products.select_related("product_type")
kwargs["categories"] = {}
@ -1360,7 +1392,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
)
def get_context_data(self, **kwargs):
"""Add form to the context """
"""Add form to the context"""
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
threshold = timezone.now() - timedelta(
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})
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["form"] = self.form
return kwargs
@ -1448,7 +1480,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
template_name = "counter/stats.jinja"
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
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
@ -1553,18 +1585,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
class CashSummaryFormBase(forms.Form):
begin_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
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,
)
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
@ -1578,7 +1600,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
def get_context_data(self, **kwargs):
""" Add sums to the context """
"""Add sums to the context"""
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
form = CashSummaryFormBase(self.request.GET)
kwargs["form"] = form
@ -1629,7 +1651,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
current_tab = "invoices_call"
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["months"] = Selling.objects.datetimes("date", "month", order="DESC")
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
./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))
def add_product(self, request):
""" Add a product to the basket """
"""Add a product to the basket"""
try:
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
if not p.buying_groups.exists():
@ -95,7 +95,7 @@ class EbouticMain(TemplateView):
pass
def del_product(self, request):
""" Delete a product from the basket """
"""Delete a product from the basket"""
try:
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
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 core.views.forms import SelectDateTime, MarkdownInput
from election.models import Election, Role, Candidature, ElectionList, Vote
from core.views.forms import TzAwareDateTimeField
from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field
@ -24,8 +25,8 @@ from ajax_select import make_ajax_field
class LimitedCheckboxField(forms.ModelMultipleChoiceField):
"""
Used to replace ModelMultipleChoiceField but with
automatic backend verification
Used to replace ModelMultipleChoiceField but with
automatic backend verification
"""
def __init__(self, queryset, max_choice, **kwargs):
@ -49,7 +50,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
class CandidateForm(forms.ModelForm):
""" Form to candidate """
"""Form to candidate"""
class Meta:
model = Candidature
@ -95,7 +96,7 @@ class VoteForm(forms.Form):
class RoleForm(forms.ModelForm):
""" Form for creating a role """
"""Form for creating a role"""
class Meta:
model = Role
@ -167,30 +168,12 @@ class ElectionForm(forms.ModelForm):
label=_("candidature groups"),
)
start_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
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,
start_date = TzAwareDateTimeField(label=_("Start date"), required=True)
end_date = TzAwareDateTimeField(label=_("End date"), required=True)
start_candidature = TzAwareDateTimeField(
label=_("Start candidature"), required=True
)
end_candidature = TzAwareDateTimeField(label=_("End candidature"), required=True)
# Display elections
@ -261,7 +244,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
return r
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["election_form"] = VoteForm(self.object, self.request.user)
kwargs["election_results"] = self.object.results
@ -308,7 +291,7 @@ class VoteFormView(CanCreateMixin, FormView):
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()
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})
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["object"] = self.election
kwargs["election"] = self.election
@ -360,7 +343,7 @@ class CandidatureCreateView(CanCreateMixin, CreateView):
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.election = Election.objects.get(id=self.election.id)
@ -391,8 +374,8 @@ class ElectionCreateView(CanCreateMixin, CreateView):
def form_valid(self, form):
"""
Allow every users that had passed the dispatch
to create an election
Allow every users that had passed the dispatch
to create an election
"""
return super(CreateView, self).form_valid(form)
@ -418,7 +401,7 @@ class RoleCreateView(CanCreateMixin, CreateView):
def form_valid(self, form):
"""
Verify that the user can edit proprely
Verify that the user can edit proprely
"""
obj = form.instance
if obj.election:
@ -461,7 +444,7 @@ class ElectionListCreateView(CanCreateMixin, CreateView):
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
if obj.election:

View File

@ -52,7 +52,7 @@ class LaunderetteMainView(TemplateView):
template_name = "launderette/launderette_main.jinja"
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["page"] = Page.objects.filter(name="launderette").first()
return kwargs
@ -142,7 +142,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
currentDate += delta
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["planning"] = OrderedDict()
kwargs["slot_type"] = self.slot_type
@ -481,7 +481,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
return super(LaunderetteClickView, self).get(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.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
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):
"""
Delete all forum messages from a user
Delete all forum messages from a user
"""
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):
"""
Create a ForumMessageMeta that says a forum
message is deleted on every forum message of an user
user: the user to delete messages from
moderator: the one marked as the moderator
Create a ForumMessageMeta that says a forum
message is deleted on every forum message of an user
user: the user to delete messages from
moderator: the one marked as the moderator
"""
for message in user.forum_messages.all():
if message.is_deleted():
@ -145,9 +145,9 @@ class MergeUsersView(FormView):
class DeleteAllForumUserMessagesView(FormView):
"""
Delete all forum messages from an user
Messages are soft deleted and are still visible from admins
GUI frontend to the dedicated command
Delete all forum messages from an user
Messages are soft deleted and are still visible from admins
GUI frontend to the dedicated command
"""
template_name = "rootplace/delete_user_messages.jinja"

View File

@ -280,7 +280,8 @@ SITH_NAME = "Sith website"
SITH_TWITTER = "@ae_utbm"
# 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 = {
"name": "AE",
"unix_name": "ae",
@ -477,14 +478,14 @@ SITH_SUBSCRIPTION_END = 10
# Subscription durations are in semestres
# Be careful, modifying this parameter will need a migration to be applied
SITH_SUBSCRIPTIONS = {
"un-semestre": {"name": _("One semester"), "price": 15, "duration": 1},
"deux-semestres": {"name": _("Two semesters"), "price": 28, "duration": 2},
"un-semestre": {"name": _("One semester"), "price": 20, "duration": 1},
"deux-semestres": {"name": _("Two semesters"), "price": 35, "duration": 2},
"cursus-tronc-commun": {
"name": _("Common core cursus"),
"price": 45,
"price": 60,
"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},
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
"assidu": {"name": _("Assidu member"), "price": 0, "duration": 2},
@ -497,6 +498,7 @@ SITH_SUBSCRIPTIONS = {
"price": 0,
"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},
"benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1},
"six-semaines-essai": {
@ -505,6 +507,7 @@ SITH_SUBSCRIPTIONS = {
"duration": 0.23,
},
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
"membre-staff-ga": {"name": _("GA staff member"), "price": 1, "duration": 0.076},
# Discount subscriptions
"un-semestre-reduction": {
"name": _("One semester (-20%)"),
@ -530,6 +533,12 @@ SITH_SUBSCRIPTIONS = {
"name": _("Alternating cursus (-20%)"),
"price": 24,
"duration": 6,
},
# CA special offer
"un-an-offert-CA": {
"name": _("One year for free(CA offer)"),
"price": 0,
"duration": 2,
}
# To be completed....
}
@ -665,3 +674,17 @@ if "test" in sys.argv:
if SENTRY_DSN:
# Connection to sentry
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 -*
#
# Copyright 2016,2017
# Copyright 2016,2017,2021
# - Sli <antoine@bartuccio.fr>
# - Skia <skia@hya.sk>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -27,7 +28,10 @@ from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel
class TemplatesPanel(BaseTemplatesPanel):
def generate_stats(self, *args):
template = self.templates[0]["template"]
if not hasattr(template, "engine") and hasattr(template, "backend"):
template.engine = template.backend
try:
template = self.templates[0]["template"]
if not hasattr(template, "engine") and hasattr(template, "backend"):
template.engine = template.backend
except IndexError: # No template
pass
return super().generate_stats(*args)

View File

@ -120,8 +120,7 @@ class ShoppingList(models.Model):
class ShoppingListItem(models.Model):
"""
"""
""""""
shopping_lists = models.ManyToManyField(
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")
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):
s = Subscription(
@ -122,11 +134,11 @@ class SubscriptionIntegrationTest(TestCase):
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
)
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()
self.assertTrue(s.subscription_end == date(2017, 10, 29))
def test_duration_two_months(self):
def test_duration_one_day(self):
s = Subscription(
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.models import User
from core.views.forms import SelectDate
from core.views.forms import TzAwareDateTimeField
class SelectionDateForm(forms.Form):
def __init__(self, *args, **kwargs):
super(SelectionDateForm, self).__init__(*args, **kwargs)
self.fields["start_date"] = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
required=True,
self.fields["start_date"] = TzAwareDateTimeField(
label=_("Start date"), required=True
)
self.fields["end_date"] = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=True,
self.fields["end_date"] = TzAwareDateTimeField(
label=_("End date"), required=True
)

View File

@ -33,6 +33,7 @@
</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:create_membership', user_id=u.id) }}">{% trans %}Add club membership{% endtrans %}</a></div>
</div>
{% endfor %}
</div>

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -*
#
# Copyright 2017
# Copyright 2017,2020
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -81,4 +82,9 @@ urlpatterns = [
UserTrombiDeleteMembershipView.as_view(),
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 -*
#
# Copyright 2017
# Copyright 2017,2020
# - Skia <skia@libskia.so>
# - Sli <antoine.bartuccio@gmail.com>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
@ -31,6 +32,7 @@ from django.utils.translation import ugettext_lazy as _
from django import forms
from django.conf import settings
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied
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):
model = TrombiClubMembership
pk_url_kwarg = "membership_id"