mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-25 17:21:15 +00:00
Merge branch 'master' into gender_options
This commit is contained in:
commit
677a9da469
@ -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
18
.mailmap
Normal 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>
|
@ -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": "",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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"])
|
||||
|
@ -43,7 +43,7 @@ class ClubSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ClubViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Clubs (api/v1/club/)
|
||||
Manage Clubs (api/v1/club/)
|
||||
"""
|
||||
|
||||
serializer_class = ClubSerializer
|
||||
|
@ -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)
|
||||
|
@ -36,7 +36,7 @@ class GroupSerializer(serializers.ModelSerializer):
|
||||
|
||||
class GroupViewSet(RightModelViewSet):
|
||||
"""
|
||||
Manage Groups (api/v1/group/)
|
||||
Manage Groups (api/v1/group/)
|
||||
"""
|
||||
|
||||
serializer_class = GroupSerializer
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 = {}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
15
com/tests.py
15
com/tests.py
@ -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):
|
||||
|
62
com/views.py
62
com/views.py
@ -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
|
||||
|
107
core/management/commands/check_front.py
Normal file
107
core/management/commands/check_front.py
Normal 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])
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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/"))
|
||||
|
@ -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):
|
||||
|
1
core/static/core/js/vue.global.prod.js
Normal file
1
core/static/core/js/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 #}
|
||||
|
@ -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 %}
|
||||
|
@ -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,
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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()
|
||||
|
27
core/templatetags/search_helpers.py
Normal file
27
core/templatetags/search_helpers.py
Normal 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)
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 []
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -120,8 +120,7 @@ class ShoppingList(models.Model):
|
||||
|
||||
|
||||
class ShoppingListItem(models.Model):
|
||||
"""
|
||||
"""
|
||||
""""""
|
||||
|
||||
shopping_lists = models.ManyToManyField(
|
||||
ShoppingList,
|
||||
|
45
subscription/migrations/0012_auto_20200615_1438.py
Normal file
45
subscription/migrations/0012_auto_20200615_1438.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
46
subscription/migrations/0013_auto_20200828_2117.py
Normal file
46
subscription/migrations/0013_auto_20200828_2117.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
47
subscription/migrations/0014_auto_20201207_2323.py
Normal file
47
subscription/migrations/0014_auto_20201207_2323.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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(),
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user