mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-25 10:34:21 +00:00
Merge branch 'master' into gender_options
This commit is contained in:
commit
677a9da469
@ -4,19 +4,27 @@ stages:
|
|||||||
test:
|
test:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
|
- env
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y gettext python3-xapian libgraphviz-dev
|
- apt-get install -y gettext python3-xapian libgraphviz-dev
|
||||||
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
|
- pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
|
||||||
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
|
- export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
|
||||||
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
|
- python -c 'import xapian' # Fail immediately if there is a problem with xapian
|
||||||
- pip install -r requirements.txt
|
- pip install -U -r requirements.txt
|
||||||
- pip install coverage
|
- pip install -U coverage
|
||||||
|
- mkdir -p /dev/shm/search_indexes
|
||||||
|
- ln -s /dev/shm/search_indexes sith/search_indexes
|
||||||
- ./manage.py compilemessages
|
- ./manage.py compilemessages
|
||||||
- coverage run ./manage.py test
|
- coverage run ./manage.py test
|
||||||
- coverage html
|
- coverage html
|
||||||
- coverage report
|
- coverage report
|
||||||
- cd doc
|
- cd doc
|
||||||
- make html # Make documentation
|
- make html # Make documentation
|
||||||
|
variables:
|
||||||
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_tests"
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache/pip_tests
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- coverage_report/
|
- coverage_report/
|
||||||
@ -24,5 +32,10 @@ test:
|
|||||||
black:
|
black:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip install black
|
- pip install -U black
|
||||||
- black --check .
|
- black --check .
|
||||||
|
variables:
|
||||||
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_black"
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache/pip_black
|
||||||
|
18
.mailmap
Normal file
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.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from datetime import date
|
from datetime import date, timedelta
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from accounting.models import (
|
from accounting.models import (
|
||||||
@ -110,6 +110,9 @@ class JournalTest(TestCase):
|
|||||||
class OperationTest(TestCase):
|
class OperationTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
call_command("populate")
|
call_command("populate")
|
||||||
|
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
|
||||||
|
"%d/%m/%Y"
|
||||||
|
)
|
||||||
self.journal = GeneralJournal.objects.filter(id=1).first()
|
self.journal = GeneralJournal.objects.filter(id=1).first()
|
||||||
self.skia = User.objects.filter(username="skia").first()
|
self.skia = User.objects.filter(username="skia").first()
|
||||||
at = AccountingType(
|
at = AccountingType(
|
||||||
@ -158,7 +161,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -191,7 +194,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de la nuit",
|
"target_label": "Le fantome de la nuit",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -218,7 +221,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome du jour",
|
"target_label": "Le fantome du jour",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
@ -245,7 +248,7 @@ class OperationTest(TestCase):
|
|||||||
"target_type": "OTHER",
|
"target_type": "OTHER",
|
||||||
"target_id": "",
|
"target_id": "",
|
||||||
"target_label": "Le fantome de l'aurore",
|
"target_label": "Le fantome de l'aurore",
|
||||||
"date": "04/12/2020",
|
"date": self.tomorrow_formatted,
|
||||||
"mode": "CASH",
|
"mode": "CASH",
|
||||||
"cheque_number": "",
|
"cheque_number": "",
|
||||||
"invoice": "",
|
"invoice": "",
|
||||||
|
@ -496,7 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
|
||||||
if self.journal:
|
if self.journal:
|
||||||
kwargs["object"] = self.journal
|
kwargs["object"] = self.journal
|
||||||
@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
|
|||||||
template_name = "accounting/operation_edit.jinja"
|
template_name = "accounting/operation_edit.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
kwargs = super(OperationEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.object.journal
|
kwargs["object"] = self.object.journal
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add infos to the context """
|
"""Add infos to the context"""
|
||||||
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.big_statement()
|
kwargs["statement"] = self.big_statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
|
|||||||
return sum(self.statement(movement_type).values())
|
return sum(self.statement(movement_type).values())
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["credit_statement"] = self.statement("CREDIT")
|
kwargs["credit_statement"] = self.statement("CREDIT")
|
||||||
kwargs["debit_statement"] = self.statement("DEBIT")
|
kwargs["debit_statement"] = self.statement("DEBIT")
|
||||||
@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
|
|||||||
return statement
|
return statement
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add journal to the context """
|
"""Add journal to the context"""
|
||||||
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
|
||||||
kwargs["statement"] = self.statement()
|
kwargs["statement"] = self.statement()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
|
|||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDate, SelectDateTime
|
from core.views.forms import SelectDate, SelectDateTime
|
||||||
from counter.models import Counter
|
from counter.models import Counter
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
class ClubEditForm(forms.ModelForm):
|
class ClubEditForm(forms.ModelForm):
|
||||||
@ -158,18 +159,9 @@ class MailingForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class SellingsForm(forms.Form):
|
class SellingsForm(forms.Form):
|
||||||
begin_date = forms.DateTimeField(
|
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Begin date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
counters = forms.ModelMultipleChoiceField(
|
counters = forms.ModelMultipleChoiceField(
|
||||||
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
||||||
)
|
)
|
||||||
|
@ -6,65 +6,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="news">
|
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
||||||
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
|
<div id="news_admin">
|
||||||
<div id="news_admin">
|
|
||||||
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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="news">
|
||||||
<div id="left_column" class="news_column">
|
<div id="left_column" class="news_column">
|
||||||
|
|
||||||
{% for news in object_list.filter(type="NOTICE") %}
|
{% for news in object_list.filter(type="NOTICE") %}
|
||||||
@ -74,8 +22,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
|
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
|
||||||
dates__end_date__gte=timezone.now(), type="CALL") %}
|
|
||||||
<section class="news_call">
|
<section class="news_call">
|
||||||
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
|
||||||
<div class="news_date">
|
<div class="news_date">
|
||||||
@ -88,8 +35,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5),
|
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
||||||
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
|
|
||||||
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
|
||||||
{% if events_dates %}
|
{% if events_dates %}
|
||||||
{% for d in events_dates %}
|
{% for d in events_dates %}
|
||||||
@ -152,6 +98,58 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="right_column" class="news_column">
|
||||||
|
<div id="agenda">
|
||||||
|
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
|
||||||
|
<div id="agenda_content">
|
||||||
|
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
|
||||||
|
news__is_moderated=True, news__type__in=["WEEKLY",
|
||||||
|
"EVENT"]).order_by('start_date', 'end_date') %}
|
||||||
|
<div class="agenda_item">
|
||||||
|
<div class="agenda_date">
|
||||||
|
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_time">
|
||||||
|
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
|
||||||
|
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
|
||||||
|
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="birthdays">
|
||||||
|
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
|
||||||
|
<div id="birthdays_content">
|
||||||
|
{% if user.is_subscribed %}
|
||||||
|
{# Cache request for 1 hour #}
|
||||||
|
{% cache 3600 "birthdays" %}
|
||||||
|
<ul class="birthdays_year">
|
||||||
|
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
|
||||||
|
<li>
|
||||||
|
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
|
||||||
|
<ul>
|
||||||
|
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
|
||||||
|
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endcache %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<div class="name">{{ poster.name }}</div>
|
<div class="name">{{ poster.name }}</div>
|
||||||
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
<div class="image"><img src="{{ poster.file.url }}"></img></div>
|
||||||
<div class="dates">
|
<div class="dates">
|
||||||
<div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div>
|
<div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
|
||||||
<div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div>
|
<div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% if app == "com" %}
|
{% if app == "com" %}
|
||||||
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
<a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
|
||||||
|
@ -7,15 +7,33 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
|
||||||
{% if request.GET['send'] %}
|
{% if bad_recipients %}
|
||||||
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
|
<p>
|
||||||
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
|
<span class="important">
|
||||||
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
|
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
|
||||||
{% endif %}
|
</span>
|
||||||
<form method="post" action="">
|
<ul>
|
||||||
|
{% 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 %}
|
{% csrf_token %}
|
||||||
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
<button type="submit" name="send" value="validate">{% trans %}Send{% endtrans %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
{{ weekmail_rendered|safe }}
|
{{ 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"))
|
r = self.client.get(reverse("core:index"))
|
||||||
self.assertTrue(r.status_code == 200)
|
self.assertTrue(r.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>"""
|
r,
|
||||||
in str(r.content)
|
"""<div id="alert_box">
|
||||||
|
<div class="markdown"><h3>ALERTE!</h3>
|
||||||
|
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_info_msg(self):
|
def test_info_msg(self):
|
||||||
@ -95,9 +97,10 @@ class ComTest(TestCase):
|
|||||||
)
|
)
|
||||||
r = self.client.get(reverse("core:index"))
|
r = self.client.get(reverse("core:index"))
|
||||||
self.assertTrue(r.status_code == 200)
|
self.assertTrue(r.status_code == 200)
|
||||||
self.assertTrue(
|
self.assertContains(
|
||||||
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>"""
|
r,
|
||||||
in str(r.content)
|
"""<div id="info_box">
|
||||||
|
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_birthday_non_subscribed_user(self):
|
def test_birthday_non_subscribed_user(self):
|
||||||
|
60
com/views.py
60
com/views.py
@ -39,6 +39,7 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from smtplib import SMTPRecipientsRefused
|
||||||
|
|
||||||
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
|
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
|
||||||
from core.views import (
|
from core.views import (
|
||||||
@ -52,6 +53,7 @@ from core.views import (
|
|||||||
from core.views.forms import SelectDateTime, MarkdownInput
|
from core.views.forms import SelectDateTime, MarkdownInput
|
||||||
from core.models import Notification, RealGroup, User
|
from core.models import Notification, RealGroup, User
|
||||||
from club.models import Club, Mailing
|
from club.models import Club, Mailing
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
# Sith object
|
# Sith object
|
||||||
@ -72,20 +74,14 @@ class PosterForm(forms.ModelForm):
|
|||||||
"display_time",
|
"display_time",
|
||||||
]
|
]
|
||||||
widgets = {"screens": forms.CheckboxSelectMultiple}
|
widgets = {"screens": forms.CheckboxSelectMultiple}
|
||||||
|
help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
|
||||||
|
|
||||||
date_begin = forms.DateTimeField(
|
date_begin = TzAwareDateTimeField(
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("Start date"),
|
label=_("Start date"),
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
required=True,
|
||||||
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
)
|
)
|
||||||
date_end = forms.DateTimeField(
|
date_end = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop("user", None)
|
self.user = kwargs.pop("user", None)
|
||||||
@ -199,24 +195,10 @@ class NewsForm(forms.ModelForm):
|
|||||||
"content": MarkdownInput,
|
"content": MarkdownInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
start_date = forms.DateTimeField(
|
start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Start date"),
|
until = TzAwareDateTimeField(label=_("Until"), required=False)
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
until = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("Until"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
|
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -433,22 +415,35 @@ class NewsDetailView(CanViewMixin, DetailView):
|
|||||||
# Weekmail
|
# Weekmail
|
||||||
|
|
||||||
|
|
||||||
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
|
||||||
model = Weekmail
|
model = Weekmail
|
||||||
template_name = "com/weekmail_preview.jinja"
|
template_name = "com/weekmail_preview.jinja"
|
||||||
success_url = reverse_lazy("com:weekmail")
|
success_url = reverse_lazy("com:weekmail")
|
||||||
current_tab = "weekmail"
|
current_tab = "weekmail"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.bad_recipients = []
|
||||||
|
return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
try:
|
|
||||||
if request.POST["send"] == "validate":
|
if request.POST["send"] == "validate":
|
||||||
|
try:
|
||||||
self.object.send()
|
self.object.send()
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
reverse("com:weekmail") + "?qn_weekmail_send_success"
|
||||||
)
|
)
|
||||||
except:
|
except SMTPRecipientsRefused as e:
|
||||||
pass
|
self.bad_recipients = e.recipients
|
||||||
|
elif request.POST["send"] == "clean":
|
||||||
|
try:
|
||||||
|
self.object.send() # This should fail
|
||||||
|
except SMTPRecipientsRefused as e:
|
||||||
|
users = User.objects.filter(email__in=e.recipients.keys())
|
||||||
|
for u in users:
|
||||||
|
u.preferences.receive_weekmail = False
|
||||||
|
u.preferences.save()
|
||||||
|
self.quick_notif_list += ["qn_success"]
|
||||||
return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
|
return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
@ -458,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
|
|||||||
"""Add rendered weekmail"""
|
"""Add rendered weekmail"""
|
||||||
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
|
||||||
kwargs["weekmail_rendered"] = self.object.render_html()
|
kwargs["weekmail_rendered"] = self.object.render_html()
|
||||||
|
kwargs["bad_recipients"] = self.bad_recipients
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@ -534,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
|
|||||||
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
return super(WeekmailEditView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add orphan articles """
|
"""Add orphan articles"""
|
||||||
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
|
||||||
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
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])
|
@ -1492,7 +1492,9 @@ class OperationLog(models.Model):
|
|||||||
User, related_name="logs", on_delete=models.SET_NULL, null=True
|
User, related_name="logs", on_delete=models.SET_NULL, null=True
|
||||||
)
|
)
|
||||||
operation_type = models.CharField(
|
operation_type = models.CharField(
|
||||||
_("operation type"), max_length=40, choices=settings.SITH_LOG_OPERATION_TYPE,
|
_("operation type"),
|
||||||
|
max_length=40,
|
||||||
|
choices=settings.SITH_LOG_OPERATION_TYPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_owned_by(self, user):
|
def is_owned_by(self, user):
|
||||||
|
@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
|
|||||||
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
text = indexes.CharField(document=True, use_template=True)
|
text = indexes.CharField(document=True, use_template=True)
|
||||||
auto = indexes.EdgeNgramField(use_template=True)
|
auto = indexes.EdgeNgramField(use_template=True)
|
||||||
|
last_update = indexes.DateTimeField(model_attr="last_update")
|
||||||
|
|
||||||
def get_model(self):
|
def get_model(self):
|
||||||
return User
|
return User
|
||||||
@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
|
|||||||
def get_updated_field(self):
|
def get_updated_field(self):
|
||||||
return "last_update"
|
return "last_update"
|
||||||
|
|
||||||
|
def prepare_auto(self, obj):
|
||||||
|
return self.prepared_data["auto"].strip()[:245]
|
||||||
|
|
||||||
|
|
||||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
class IndexSignalProcessor(signals.BaseSignalProcessor):
|
||||||
def setup(self):
|
def setup(self):
|
||||||
|
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);
|
$shadow-color: rgb(223, 223, 223);
|
||||||
|
|
||||||
$background-bouton-color: hsl(0, 0%, 90%);
|
$background-button-color: hsl(0, 0%, 95%);
|
||||||
|
|
||||||
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
||||||
$small-devices: 576px;
|
$small-devices: 576px;
|
||||||
@ -47,10 +47,11 @@ body {
|
|||||||
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
|
margin: 0.1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
|||||||
button{
|
button{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 0.4em;
|
||||||
font-size: 14px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.18em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: $shadow-color 0px 0px 1px;
|
box-shadow: $shadow-color 0px 0px 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -75,24 +77,26 @@ button{
|
|||||||
input,textarea[type=text],[type=number]{
|
input,textarea[type=text],[type=number]{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 0.4em;
|
||||||
font-size: 16px;
|
margin: 0.1em;
|
||||||
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
max-width: 95%;
|
||||||
}
|
}
|
||||||
textarea{
|
textarea{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
select{
|
select{
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 15px;
|
font-size: 1.2em;
|
||||||
background-color: $background-bouton-color;
|
background-color: $background-button-color;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -130,9 +134,10 @@ a {
|
|||||||
|
|
||||||
#header_language_chooser {
|
#header_language_chooser {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.2em;
|
top: 2em;
|
||||||
right: 0.5em;
|
left: 0.5em;
|
||||||
width: 3%;
|
width: 3%;
|
||||||
|
min-width: 2.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
input {
|
input {
|
||||||
display: block;
|
display: block;
|
||||||
@ -157,9 +162,6 @@ header {
|
|||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
|
||||||
#header_logo {
|
#header_logo {
|
||||||
display: inline-block;
|
|
||||||
flex: none;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
background-color: $white-color;
|
background-color: $white-color;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
border-radius: 0px 0px 0px 9px;
|
border-radius: 0px 0px 0px 9px;
|
||||||
@ -169,11 +171,19 @@ header {
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_connect_links {
|
#header_connect_links {
|
||||||
margin: 0.6em 0.6em 0em auto;
|
margin: 0.6em 0.6em 0em auto;
|
||||||
|
padding: 0.2em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
form {
|
form {
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -190,6 +200,7 @@ header {
|
|||||||
#header_bar {
|
#header_bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -203,7 +214,6 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#header_bars_infos {
|
#header_bars_infos {
|
||||||
width: 35ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0.2em 0.2em;
|
margin: 0.2em 0.2em;
|
||||||
@ -213,12 +223,15 @@ header {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
margin: 0.8em 0em;
|
margin: 0.8em 0em;
|
||||||
|
input {
|
||||||
|
width: 14ch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#header_user_links {
|
#header_user_links {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 120ch;
|
|
||||||
flex: initial;
|
flex: initial;
|
||||||
|
flex-wrap: wrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
div {
|
div {
|
||||||
@ -287,42 +300,34 @@ header {
|
|||||||
|
|
||||||
#info_boxes {
|
#info_boxes {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
p {
|
|
||||||
margin: 0px;
|
|
||||||
padding: 7px;
|
|
||||||
}
|
|
||||||
#alert_box, #info_box {
|
#alert_box, #info_box {
|
||||||
font-size: 14px;
|
flex: 49%;
|
||||||
display: inline-block;
|
font-size: 0.9em;
|
||||||
flex: auto;
|
margin: 0.2em;
|
||||||
padding: 2px;
|
border-radius: 0.6em;
|
||||||
margin: 0.2em 1.5%;
|
.markdown {
|
||||||
min-width: 10%;
|
margin: 0.5em;
|
||||||
max-width: 46%;
|
}
|
||||||
min-height: 20px;
|
|
||||||
&:before {
|
&:before {
|
||||||
float: left;
|
font-family: FontAwesome;
|
||||||
|
font-size: 4em;
|
||||||
|
float: right;
|
||||||
margin: 0.2em;
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#info_box {
|
#info_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $primary-neutral-light-color;
|
background: $primary-neutral-light-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f05a";
|
content: "\f05a";
|
||||||
color: hsl(210, 100%, 56%);
|
color: hsl(210, 100%, 56%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#alert_box {
|
#alert_box {
|
||||||
border-radius: 10px;
|
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
&:before {
|
&:before {
|
||||||
font-family: FontAwesome;
|
|
||||||
font-size: 4em;
|
|
||||||
content: "\f06a";
|
content: "\f06a";
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
}
|
}
|
||||||
@ -345,7 +350,7 @@ header {
|
|||||||
a {
|
a {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 1.5em;
|
||||||
color: $white-color;
|
color: $white-color;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
@ -458,6 +463,8 @@ header {
|
|||||||
|
|
||||||
/*---------------------------------NEWS--------------------------------*/
|
/*---------------------------------NEWS--------------------------------*/
|
||||||
#news {
|
#news {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
.news_column {
|
.news_column {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
@ -467,11 +474,13 @@ header {
|
|||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
#right_column {
|
#right_column {
|
||||||
width: 20%;
|
flex: 20%;
|
||||||
float: right;
|
float: right;
|
||||||
|
margin: 0.2em;
|
||||||
}
|
}
|
||||||
#left_column {
|
#left_column {
|
||||||
width: 79%;
|
flex: 79%;
|
||||||
|
margin: 0.2em;
|
||||||
h3 {
|
h3 {
|
||||||
background: $second-color;
|
background: $second-color;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
@ -484,6 +493,11 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#left_column, #right_column {
|
||||||
|
flex: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* AGENDA/BIRTHDAYS */
|
/* AGENDA/BIRTHDAYS */
|
||||||
#agenda,#birthdays {
|
#agenda,#birthdays {
|
||||||
@ -691,6 +705,12 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#page {
|
||||||
|
width: 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#news_details {
|
#news_details {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
@ -723,7 +743,7 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 16px;
|
font-size: 1.2em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
float: right;
|
float: right;
|
||||||
display: block;
|
display: block;
|
||||||
@ -1111,33 +1131,36 @@ u, .underline {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#basket {
|
|
||||||
width: 40%;
|
|
||||||
background: $primary-neutral-light-color;
|
|
||||||
float: right;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#products {
|
|
||||||
width: 90%;
|
|
||||||
margin: 0px auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bar_ui {
|
#bar_ui {
|
||||||
float: left;
|
padding: 0.4em;
|
||||||
min-width: 57%;
|
display: flex;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
#user_info_container {}
|
#products {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin: 0.2em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#user_info {
|
#click_form {
|
||||||
float: right;
|
flex: auto;
|
||||||
padding: 5px;
|
margin: 0.2em;
|
||||||
width: 40%;
|
}
|
||||||
margin: 0px auto;
|
|
||||||
|
#user_info {
|
||||||
|
flex: auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.2em;
|
||||||
|
height: 100%;
|
||||||
background: $secondary-neutral-light-color;
|
background: $secondary-neutral-light-color;
|
||||||
|
img {
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*-----------------------------USER PROFILE----------------------------*/
|
/*-----------------------------USER PROFILE----------------------------*/
|
||||||
@ -1212,6 +1235,11 @@ u, .underline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: $small-devices){
|
||||||
|
#user_profile_infos, #user_profile_pictures {
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1412,6 +1440,7 @@ textarea {
|
|||||||
.search_bar {
|
.search_bar {
|
||||||
margin: 10px 0px;
|
margin: 10px 0px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
height: 20p;
|
height: 20p;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -1551,6 +1580,7 @@ footer {
|
|||||||
color: $white-color;
|
color: $white-color;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
background-color: $primary-neutral-dark-color;
|
background-color: $primary-neutral-dark-color;
|
||||||
box-shadow: $shadow-color 0px 0px 15px;
|
box-shadow: $shadow-color 0px 0px 15px;
|
||||||
a {
|
a {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
<title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||||
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
||||||
@ -27,6 +28,7 @@
|
|||||||
<!-- BEGIN HEADER -->
|
<!-- BEGIN HEADER -->
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if not popup %}
|
{% if not popup %}
|
||||||
|
<header>
|
||||||
<div id="header_language_chooser">
|
<div id="header_language_chooser">
|
||||||
{% for language in LANGUAGES %}
|
{% for language in LANGUAGES %}
|
||||||
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
|
<form action="{{ url('set_language') }}" method="post">{% csrf_token %}
|
||||||
@ -37,10 +39,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header>
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 185px; height: 100px;">
|
<div id="header_logo">
|
||||||
<a href="{{ url('core:index') }}"></a>
|
<a href="{{ url('core:index') }}">
|
||||||
|
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="header_connect_links">
|
<div id="header_connect_links">
|
||||||
<form method="post" action="{{ url('core:login') }}">
|
<form method="post" action="{{ url('core:login') }}">
|
||||||
@ -54,12 +57,14 @@
|
|||||||
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
|
<a href="{{ url('core:register') }}"><button type="button">{% trans %}Register{% endtrans %}</button></a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id="header_logo" style="background-image: url('{{ static('core/img/logo.png') }}'); width: 92px; height: 52px;">
|
<div id="header_logo">
|
||||||
<a href="{{ url('core:index') }}"></a>
|
<a href="{{ url('core:index') }}">
|
||||||
|
<img src="{{ static('core/img/logo.png') }}" alt="AE logo">
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="header_bar">
|
<div id="header_bar">
|
||||||
<ul id="header_bars_infos">
|
<ul id="header_bars_infos">
|
||||||
{% cache 100 counters_activity %}
|
{% cache 100 "counters_activity" %}
|
||||||
{% for bar in Counter.objects.filter(type="BAR").all() %}
|
{% for bar in Counter.objects.filter(type="BAR").all() %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
|
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px">
|
||||||
@ -85,7 +90,7 @@
|
|||||||
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
<a href="{{ url('core:user_profile', user_id=user.id) }}">{{ user.get_display_name() }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="#" onclick="display_notif()"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
|
<a href="#" onclick="display_notif()" style="white-space: nowrap;"><i class="fa fa-bell-o"></i> ({{ user.notifications.filter(viewed=False).count() }})</a>
|
||||||
<ul id="header_notif">
|
<ul id="header_notif">
|
||||||
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
{% for n in user.notifications.filter(viewed=False).order_by('-date') %}
|
||||||
<li>
|
<li>
|
||||||
@ -126,6 +131,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="info_boxes">
|
<div id="info_boxes">
|
||||||
|
{% block info_boxes %}
|
||||||
{% set sith = get_sith() %}
|
{% set sith = get_sith() %}
|
||||||
{% if sith.alert_msg %}
|
{% if sith.alert_msg %}
|
||||||
<div id="alert_box">
|
<div id="alert_box">
|
||||||
@ -137,6 +143,7 @@
|
|||||||
{{ sith.info_msg|markdown }}
|
{{ sith.info_msg|markdown }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}{# if not popup #}
|
{% else %}{# if not popup #}
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
{% trans %}Delete confirmation{% endtrans %}
|
{% trans %}Delete confirmation{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
{% if not form.instance.profile_pict %}
|
{% if not form.instance.profile_pict %}
|
||||||
<script src="{{ static('core/js/webcam.js') }}"></script>
|
<script src="{{ static('core/js/webcam.js') }}"></script>
|
||||||
<script language="JavaScript">
|
<script language="JavaScript">
|
||||||
|
Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
|
||||||
Webcam.set({
|
Webcam.set({
|
||||||
width: 320,
|
width: 320,
|
||||||
height: 240,
|
height: 240,
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
{% for picture in pictures[a.id] %}
|
{% for picture in pictures[a.id] %}
|
||||||
<div class="picture">
|
<div class="picture">
|
||||||
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
|
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
|
||||||
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/>
|
<img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
{{ object.first_name }}
|
{% load search_helpers %}
|
||||||
{{ object.last_name }}
|
|
||||||
{{ object.nick_name }}
|
{% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
|
||||||
|
|
||||||
|
{{ first|replace:"|-| " }}
|
||||||
|
{{ last|replace:"|-| " }}
|
||||||
|
{{ nick|replace:"|-| " }}
|
||||||
|
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
|
||||||
|
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
|
||||||
|
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
|
||||||
|
{{ first|cut:"-" }}{{ last|cut:"-" }}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
|
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)
|
@ -42,6 +42,10 @@ from django.utils.translation import ugettext
|
|||||||
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
||||||
from ajax_select.fields import AutoCompleteSelectField
|
from ajax_select.fields import AutoCompleteSelectField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
|
from django.utils.dateparse import parse_datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
from django.forms.utils import to_current_timezone
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -114,14 +118,11 @@ class SelectFile(TextInput):
|
|||||||
attrs["class"] = "select_file"
|
attrs["class"] = "select_file"
|
||||||
else:
|
else:
|
||||||
attrs = {"class": "select_file"}
|
attrs = {"class": "select_file"}
|
||||||
output = (
|
output = '%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>' % {
|
||||||
'%(content)s<div name="%(name)s" class="choose_file_widget" title="%(title)s"></div>'
|
|
||||||
% {
|
|
||||||
"content": super(SelectFile, self).render(name, value, attrs, renderer),
|
"content": super(SelectFile, self).render(name, value, attrs, renderer),
|
||||||
"title": _("Choose file"),
|
"title": _("Choose file"),
|
||||||
"name": name,
|
"name": name,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
output += (
|
output += (
|
||||||
'<span name="'
|
'<span name="'
|
||||||
+ name
|
+ name
|
||||||
@ -138,14 +139,11 @@ class SelectUser(TextInput):
|
|||||||
attrs["class"] = "select_user"
|
attrs["class"] = "select_user"
|
||||||
else:
|
else:
|
||||||
attrs = {"class": "select_user"}
|
attrs = {"class": "select_user"}
|
||||||
output = (
|
output = '%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>' % {
|
||||||
'%(content)s<div name="%(name)s" class="choose_user_widget" title="%(title)s"></div>'
|
|
||||||
% {
|
|
||||||
"content": super(SelectUser, self).render(name, value, attrs, renderer),
|
"content": super(SelectUser, self).render(name, value, attrs, renderer),
|
||||||
"title": _("Choose user"),
|
"title": _("Choose user"),
|
||||||
"name": name,
|
"name": name,
|
||||||
}
|
}
|
||||||
)
|
|
||||||
output += (
|
output += (
|
||||||
'<span name="'
|
'<span name="'
|
||||||
+ name
|
+ name
|
||||||
@ -399,3 +397,26 @@ class GiftForm(forms.ModelForm):
|
|||||||
id=user_id
|
id=user_id
|
||||||
)
|
)
|
||||||
self.fields["user"].widget = forms.HiddenInput()
|
self.fields["user"].widget = forms.HiddenInput()
|
||||||
|
|
||||||
|
|
||||||
|
class TzAwareDateTimeField(forms.DateTimeField):
|
||||||
|
def __init__(
|
||||||
|
self, input_formats=["%Y-%m-%d %H:%M:%S"], widget=SelectDateTime, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
|
||||||
|
|
||||||
|
def prepare_value(self, value):
|
||||||
|
# the db value is a datetime as a string in UTC
|
||||||
|
if isinstance(value, str):
|
||||||
|
# convert it into a naive datetime (no timezone attached)
|
||||||
|
value = parse_datetime(value)
|
||||||
|
# attach it to the UTC timezone (so that to_current_timezone()
|
||||||
|
# converts it to the local timezone)
|
||||||
|
value = timezone.make_aware(value, timezone.utc)
|
||||||
|
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
value = to_current_timezone(value)
|
||||||
|
# otherwise it is formatted according to locale (in french)
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
@ -30,6 +30,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.utils import html
|
from django.utils import html
|
||||||
from django.views.generic import ListView, TemplateView
|
from django.views.generic import ListView, TemplateView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -73,7 +74,18 @@ def notification(request, notif_id):
|
|||||||
|
|
||||||
def search_user(query, as_json=False):
|
def search_user(query, as_json=False):
|
||||||
try:
|
try:
|
||||||
res = SearchQuerySet().models(User).autocomplete(auto=html.escape(query))[:20]
|
# slugify turns everything into ascii and every whitespace into -
|
||||||
|
# it ends by removing duplicate - (so ' - ' will turn into '-')
|
||||||
|
# replace('-', ' ') because search is whitespace based
|
||||||
|
query = slugify(query).replace("-", " ")
|
||||||
|
# TODO: is this necessary?
|
||||||
|
query = html.escape(query)
|
||||||
|
res = (
|
||||||
|
SearchQuerySet()
|
||||||
|
.models(User)
|
||||||
|
.autocomplete(auto=query)
|
||||||
|
.order_by("-last_update")[:20]
|
||||||
|
)
|
||||||
return [r.object for r in res]
|
return [r.object for r in res]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return []
|
return []
|
||||||
|
@ -55,7 +55,9 @@ def write_log(instance, operation_type):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
log = OperationLog(
|
log = OperationLog(
|
||||||
label=str(instance), operator=get_user(), operation_type=operation_type,
|
label=str(instance),
|
||||||
|
operator=get_user(),
|
||||||
|
operation_type=operation_type,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
|
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
|
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
|
||||||
<form action="" method="post" id="cash_summary_form">
|
<form action="" method="post" id="cash_summary_form">
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
||||||
|
|
||||||
|
|
||||||
{% macro add_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="add_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro del_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="del_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ counter }}
|
{{ counter }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h4 id="click_interface">{{ counter }}</h4>
|
<h4 id="click_interface">{{ counter }}</h4>
|
||||||
|
|
||||||
<div id="user_info">
|
<div id="bar_ui">
|
||||||
|
<noscript>
|
||||||
|
<p class="important">Javascript is required for the counter UI.</p>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div id="user_info">
|
||||||
<h5>{% trans %}Customer{% endtrans %}</h5>
|
<h5>{% trans %}Customer{% endtrans %}</h5>
|
||||||
{{ user_mini_profile(customer.user) }}
|
{{ user_mini_profile(customer.user) }}
|
||||||
{{ user_subscription(customer.user) }}
|
{{ user_subscription(customer.user) }}
|
||||||
@ -50,58 +44,56 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% trans %}No card registered{% endtrans %}
|
{% trans %}No card registered{% endtrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="bar_ui">
|
|
||||||
|
<div id="click_form">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<h5>{% trans %}Selling{% endtrans %}</h5>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="important">
|
<div class="important">
|
||||||
{% if request.session['too_young'] %}
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
<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>
|
</div>
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
|
||||||
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="code">
|
<input type="hidden" name="action" value="code">
|
||||||
<input type="input" name="code" value="" class="focus" id="code_field"/>
|
<input type="input" name="code" value="" class="focus" id="code_field"/>
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
</form>
|
</form>
|
||||||
<p>{% trans %}Basket: {% endtrans %}</p>
|
<p>{% trans %}Basket: {% endtrans %}</p>
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for id,infos in request.session['basket']|dictsort %}
|
<li v-for="p_info,p_id in basket">
|
||||||
{% set product = counter.products.filter(id=id).first() %}
|
|
||||||
{% set s = infos['qty'] * infos['price'] / 100 %}
|
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
|
||||||
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }}
|
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
||||||
{{ product.name }}: {{ "%0.2f"|format(s) }} €
|
<input type="hidden" name="action" value="del_product">
|
||||||
{% if infos['bonus_qty'] %}
|
<input type="hidden" name="product_id" v-bind:value="p_id">
|
||||||
P
|
<button type="submit"> - </button>
|
||||||
{% endif %}
|
</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>
|
</li>
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p>
|
<p>
|
||||||
|
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
|
||||||
|
</p>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
<div class="important">
|
<div class="important">
|
||||||
{% if request.session['too_young'] %}
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="finish">
|
<input type="hidden" name="action" value="finish">
|
||||||
@ -124,8 +116,9 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="products">
|
|
||||||
|
<div id="products">
|
||||||
<ul>
|
<ul>
|
||||||
{% for category in categories.keys() -%}
|
{% for category in categories.keys() -%}
|
||||||
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
||||||
@ -141,23 +134,89 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set file = static('core/img/na.gif') %}
|
{% set file = static('core/img/na.gif') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %}
|
<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">
|
||||||
{{ add_product(p.id, prod, "form_button") }}
|
{% 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 %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
|
||||||
document.getElementById("click_interface").scrollIntoView();
|
|
||||||
</script>
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
$( function() {
|
$( function() {
|
||||||
var products = [
|
/* Vue.JS dynamic form */
|
||||||
|
const click_form_vue = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
js_csrf_token: "{{ csrf_token }}",
|
||||||
|
products: {
|
||||||
|
{% for p in products -%}
|
||||||
|
{{ p.id }}: {
|
||||||
|
code: "{{ p.code }}",
|
||||||
|
name: "{{ p.name }}",
|
||||||
|
selling_price: "{{ p.selling_price }}",
|
||||||
|
special_selling_price: "{{ p.special_selling_price }}",
|
||||||
|
},
|
||||||
|
{%- endfor %}
|
||||||
|
},
|
||||||
|
basket: {{ request.session["basket"]|tojson }},
|
||||||
|
errors: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sum_basket() {
|
||||||
|
var vm = this;
|
||||||
|
var total = 0;
|
||||||
|
for(idx in vm.basket) {
|
||||||
|
var item = vm.basket[idx];
|
||||||
|
console.log(item);
|
||||||
|
total += item["qty"] * item["price"];
|
||||||
|
}
|
||||||
|
return total / 100;
|
||||||
|
},
|
||||||
|
handle_code(event) {
|
||||||
|
var vm = this;
|
||||||
|
var code = $(event.target).find("#code_field").val().toUpperCase();
|
||||||
|
console.log("Code:");
|
||||||
|
console.log(code);
|
||||||
|
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
|
||||||
|
$(event.target).submit();
|
||||||
|
} else {
|
||||||
|
vm.handle_action(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handle_action(event) {
|
||||||
|
var vm = this;
|
||||||
|
var payload = $(event.target).serialize();
|
||||||
|
$.ajax({
|
||||||
|
type: 'post',
|
||||||
|
dataType: 'json',
|
||||||
|
data: payload,
|
||||||
|
success: function(response) {
|
||||||
|
vm.basket = response.basket;
|
||||||
|
vm.errors = [];
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
vm.basket = error.responseJSON.basket;
|
||||||
|
vm.errors = error.responseJSON.errors;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('form.code_form #code_field').val("").focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).mount('#bar_ui');
|
||||||
|
|
||||||
|
/* Autocompletion in the code field */
|
||||||
|
var products_autocomplete = [
|
||||||
{% for p in products -%}
|
{% for p in products -%}
|
||||||
{
|
{
|
||||||
value: "{{ p.code }}",
|
value: "{{ p.code }}",
|
||||||
@ -166,6 +225,7 @@ $( function() {
|
|||||||
},
|
},
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
];
|
];
|
||||||
|
|
||||||
var quantity = "";
|
var quantity = "";
|
||||||
var search = "";
|
var search = "";
|
||||||
var pattern = /^(\d+x)?(.*)/i;
|
var pattern = /^(\d+x)?(.*)/i;
|
||||||
@ -183,21 +243,22 @@ $( function() {
|
|||||||
quantity = res[1] || "";
|
quantity = res[1] || "";
|
||||||
search = res[2];
|
search = res[2];
|
||||||
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
|
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
|
||||||
response($.grep( products, function( value ) {
|
response($.grep( products_autocomplete, function( value ) {
|
||||||
value = value.tags;
|
value = value.tags;
|
||||||
return matcher.test( value );
|
return matcher.test( value );
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
$( function() {
|
/* Accordion UI between basket and refills */
|
||||||
$("#bar_ui").accordion({
|
$("#click_form").accordion({
|
||||||
heightStyle: "content",
|
heightStyle: "content",
|
||||||
activate: function(event, ui){
|
activate: function(event, ui){
|
||||||
$(".focus").focus();
|
$(".focus").focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#products").tabs();
|
$("#products").tabs();
|
||||||
|
|
||||||
$("#code_field").focus();
|
$("#code_field").focus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -12,6 +12,12 @@
|
|||||||
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>
|
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>
|
||||||
|
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
|
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block info_boxes %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block nav %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
|
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
|
||||||
<h4>{% trans %}Refillings{% endtrans %}</h4>
|
<h4>{% trans %}Refillings{% endtrans %}</h4>
|
||||||
|
@ -68,18 +68,29 @@ class CounterTest(TestCase):
|
|||||||
location,
|
location,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
"action": "refill",
|
||||||
"amount": "10",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
||||||
|
response = self.client.post(
|
||||||
|
location, {"action": "add_product", "product_id": "4"}
|
||||||
|
)
|
||||||
|
response = self.client.post(
|
||||||
|
location, {"action": "del_product", "product_id": "4"}
|
||||||
|
)
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
|
||||||
response = self.client.post(location, {"action": "code", "code": "fin"})
|
response = self.client.post(location, {"action": "code", "code": "fin"})
|
||||||
|
|
||||||
response_get = self.client.get(response.get("location"))
|
response_get = self.client.get(response.get("location"))
|
||||||
|
response_content = response_get.content.decode("utf-8")
|
||||||
|
self.assertTrue("<li>2 x Barbar" in str(response_content))
|
||||||
|
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
"<p>Client : Richard Batsbak - Nouveau montant : 8.30"
|
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
|
||||||
in str(response_get.content)
|
in str(response_content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ from django.views.generic.edit import (
|
|||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
|
|||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
from datetime import date, timedelta, datetime
|
from datetime import date, timedelta, datetime
|
||||||
|
from http import HTTPStatus
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ from counter.models import (
|
|||||||
Permanency,
|
Permanency,
|
||||||
)
|
)
|
||||||
from accounting.models import CurrencyField
|
from accounting.models import CurrencyField
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
class CounterAdminMixin(View):
|
class CounterAdminMixin(View):
|
||||||
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
current_tab = "counter"
|
current_tab = "counter"
|
||||||
|
|
||||||
|
def render_to_response(self, *args, **kwargs):
|
||||||
|
if self.request.is_ajax(): # JSON response for AJAX requests
|
||||||
|
response = {"errors": []}
|
||||||
|
status = HTTPStatus.OK
|
||||||
|
|
||||||
|
if self.request.session["too_young"]:
|
||||||
|
response["errors"].append(_("Too young for that product"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_allowed"]:
|
||||||
|
response["errors"].append(_("Not allowed for that product"))
|
||||||
|
status = HTTPStatus.FORBIDDEN
|
||||||
|
if self.request.session["no_age"]:
|
||||||
|
response["errors"].append(_("No date of birth provided"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_enough"]:
|
||||||
|
response["errors"].append(_("Not enough money"))
|
||||||
|
status = HTTPStatus.PAYMENT_REQUIRED
|
||||||
|
|
||||||
|
if len(response["errors"]) > 1:
|
||||||
|
status = HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
response["basket"] = self.request.session["basket"]
|
||||||
|
|
||||||
|
return JsonResponse(response, status=status)
|
||||||
|
|
||||||
|
else: # Standard HTML page
|
||||||
|
return super().render_to_response(*args, **kwargs)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
or len(obj.get_barmen_list()) < 1
|
or len(obj.get_barmen_list()) < 1
|
||||||
):
|
):
|
||||||
raise PermissionDenied
|
return HttpResponseRedirect(
|
||||||
|
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
""" Handle the many possibilities of the post request """
|
"""Handle the many possibilities of the post request"""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.refill_form = None
|
self.refill_form = None
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def del_product(self, request):
|
def del_product(self, request):
|
||||||
""" Delete a product from the basket """
|
"""Delete a product from the basket"""
|
||||||
pid = str(request.POST["product_id"])
|
pid = str(request.POST["product_id"])
|
||||||
product = self.get_product(pid)
|
product = self.get_product(pid)
|
||||||
if pid in request.session["basket"]:
|
if pid in request.session["basket"]:
|
||||||
@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def finish(self, request):
|
def finish(self, request):
|
||||||
""" Finish the click session, and validate the basket """
|
"""Finish the click session, and validate the basket"""
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
request.session["last_basket"] = []
|
request.session["last_basket"] = []
|
||||||
if self.sum_basket(request) > self.customer.amount:
|
if self.sum_basket(request) > self.customer.amount:
|
||||||
@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def cancel(self, request):
|
def cancel(self, request):
|
||||||
""" Cancel the click session """
|
"""Cancel the click session"""
|
||||||
kwargs = {"counter_id": self.object.id}
|
kwargs = {"counter_id": self.object.id}
|
||||||
request.session.pop("basket", None)
|
request.session.pop("basket", None)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add customer to the context """
|
"""Add customer to the context"""
|
||||||
kwargs = super(CounterClick, self).get_context_data(**kwargs)
|
kwargs = super(CounterClick, self).get_context_data(**kwargs)
|
||||||
kwargs["products"] = self.object.products.select_related("product_type")
|
kwargs["products"] = self.object.products.select_related("product_type")
|
||||||
kwargs["categories"] = {}
|
kwargs["categories"] = {}
|
||||||
@ -1360,7 +1392,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add form to the context """
|
"""Add form to the context"""
|
||||||
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
|
kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
|
||||||
threshold = timezone.now() - timedelta(
|
threshold = timezone.now() - timedelta(
|
||||||
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
|
||||||
@ -1422,7 +1454,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add form to the context """
|
"""Add form to the context"""
|
||||||
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
|
kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
|
||||||
kwargs["form"] = self.form
|
kwargs["form"] = self.form
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -1448,7 +1480,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
|
|||||||
template_name = "counter/stats.jinja"
|
template_name = "counter/stats.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add stats to the context """
|
"""Add stats to the context"""
|
||||||
from django.db.models import Sum, Case, When, F, DecimalField
|
from django.db.models import Sum, Case, When, F, DecimalField
|
||||||
|
|
||||||
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
|
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
|
||||||
@ -1553,18 +1585,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
|
|||||||
|
|
||||||
|
|
||||||
class CashSummaryFormBase(forms.Form):
|
class CashSummaryFormBase(forms.Form):
|
||||||
begin_date = forms.DateTimeField(
|
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||||
label=_("Begin date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
required=False,
|
|
||||||
widget=SelectDateTime,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
||||||
@ -1578,7 +1600,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
|
|||||||
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add sums to the context """
|
"""Add sums to the context"""
|
||||||
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
|
kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
|
||||||
form = CashSummaryFormBase(self.request.GET)
|
form = CashSummaryFormBase(self.request.GET)
|
||||||
kwargs["form"] = form
|
kwargs["form"] = form
|
||||||
@ -1629,7 +1651,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
|
|||||||
current_tab = "invoices_call"
|
current_tab = "invoices_call"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add sums to the context """
|
"""Add sums to the context"""
|
||||||
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
|
kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
|
||||||
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
|
||||||
start_date = None
|
start_date = None
|
||||||
|
@ -133,3 +133,14 @@ Pour lancer les tests il suffit d'utiliser la commande intégrée à django.
|
|||||||
|
|
||||||
# Lancer une méthode en particulier de cette même classe
|
# Lancer une méthode en particulier de cette même classe
|
||||||
./manage.py test core.tests.UserRegistrationTest.test_register_user_form_ok
|
./manage.py test core.tests.UserRegistrationTest.test_register_user_form_ok
|
||||||
|
|
||||||
|
Vérifier les dépendances Javascript
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
Une commande a été écrite pour vérifier les éventuelles mises à jour à faire sur les librairies Javascript utilisées.
|
||||||
|
N'oubliez pas de mettre à jour à la fois le fichier de la librairie, mais également sa version dans `sith/settings.py`.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# Vérifier les mises à jour
|
||||||
|
./manage.py check_front
|
||||||
|
@ -82,7 +82,7 @@ class EbouticMain(TemplateView):
|
|||||||
return self.render_to_response(self.get_context_data(**kwargs))
|
return self.render_to_response(self.get_context_data(**kwargs))
|
||||||
|
|
||||||
def add_product(self, request):
|
def add_product(self, request):
|
||||||
""" Add a product to the basket """
|
"""Add a product to the basket"""
|
||||||
try:
|
try:
|
||||||
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
||||||
if not p.buying_groups.exists():
|
if not p.buying_groups.exists():
|
||||||
@ -95,7 +95,7 @@ class EbouticMain(TemplateView):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def del_product(self, request):
|
def del_product(self, request):
|
||||||
""" Delete a product from the basket """
|
"""Delete a product from the basket"""
|
||||||
try:
|
try:
|
||||||
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
p = self.object.products.filter(id=int(request.POST["product_id"])).first()
|
||||||
self.basket.del_product(p)
|
self.basket.del_product(p)
|
||||||
|
@ -14,6 +14,7 @@ from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
|
|||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from core.views.forms import SelectDateTime, MarkdownInput
|
from core.views.forms import SelectDateTime, MarkdownInput
|
||||||
from election.models import Election, Role, Candidature, ElectionList, Vote
|
from election.models import Election, Role, Candidature, ElectionList, Vote
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField
|
from ajax_select.fields import AutoCompleteSelectField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
@ -49,7 +50,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
|
|||||||
|
|
||||||
|
|
||||||
class CandidateForm(forms.ModelForm):
|
class CandidateForm(forms.ModelForm):
|
||||||
""" Form to candidate """
|
"""Form to candidate"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Candidature
|
model = Candidature
|
||||||
@ -95,7 +96,7 @@ class VoteForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class RoleForm(forms.ModelForm):
|
class RoleForm(forms.ModelForm):
|
||||||
""" Form for creating a role """
|
"""Form for creating a role"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
@ -167,30 +168,12 @@ class ElectionForm(forms.ModelForm):
|
|||||||
label=_("candidature groups"),
|
label=_("candidature groups"),
|
||||||
)
|
)
|
||||||
|
|
||||||
start_date = forms.DateTimeField(
|
start_date = TzAwareDateTimeField(label=_("Start date"), required=True)
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
end_date = TzAwareDateTimeField(label=_("End date"), required=True)
|
||||||
label=_("Start date"),
|
start_candidature = TzAwareDateTimeField(
|
||||||
widget=SelectDateTime,
|
label=_("Start candidature"), required=True
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
end_date = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
start_candidature = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("Start candidature"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
end_candidature = forms.DateTimeField(
|
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
|
||||||
label=_("End candidature"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
|
end_candidature = TzAwareDateTimeField(label=_("End candidature"), required=True)
|
||||||
|
|
||||||
|
|
||||||
# Display elections
|
# Display elections
|
||||||
@ -261,7 +244,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add additionnal data to the template """
|
"""Add additionnal data to the template"""
|
||||||
kwargs = super(ElectionDetailView, self).get_context_data(**kwargs)
|
kwargs = super(ElectionDetailView, self).get_context_data(**kwargs)
|
||||||
kwargs["election_form"] = VoteForm(self.object, self.request.user)
|
kwargs["election_form"] = VoteForm(self.object, self.request.user)
|
||||||
kwargs["election_results"] = self.object.results
|
kwargs["election_results"] = self.object.results
|
||||||
@ -322,7 +305,7 @@ class VoteFormView(CanCreateMixin, FormView):
|
|||||||
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
|
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add additionnal data to the template """
|
"""Add additionnal data to the template"""
|
||||||
kwargs = super(VoteFormView, self).get_context_data(**kwargs)
|
kwargs = super(VoteFormView, self).get_context_data(**kwargs)
|
||||||
kwargs["object"] = self.election
|
kwargs["object"] = self.election
|
||||||
kwargs["election"] = self.election
|
kwargs["election"] = self.election
|
||||||
|
@ -52,7 +52,7 @@ class LaunderetteMainView(TemplateView):
|
|||||||
template_name = "launderette/launderette_main.jinja"
|
template_name = "launderette/launderette_main.jinja"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add page to the context """
|
"""Add page to the context"""
|
||||||
kwargs = super(LaunderetteMainView, self).get_context_data(**kwargs)
|
kwargs = super(LaunderetteMainView, self).get_context_data(**kwargs)
|
||||||
kwargs["page"] = Page.objects.filter(name="launderette").first()
|
kwargs["page"] = Page.objects.filter(name="launderette").first()
|
||||||
return kwargs
|
return kwargs
|
||||||
@ -142,7 +142,7 @@ class LaunderetteBookView(CanViewMixin, DetailView):
|
|||||||
currentDate += delta
|
currentDate += delta
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" Add page to the context """
|
"""Add page to the context"""
|
||||||
kwargs = super(LaunderetteBookView, self).get_context_data(**kwargs)
|
kwargs = super(LaunderetteBookView, self).get_context_data(**kwargs)
|
||||||
kwargs["planning"] = OrderedDict()
|
kwargs["planning"] = OrderedDict()
|
||||||
kwargs["slot_type"] = self.slot_type
|
kwargs["slot_type"] = self.slot_type
|
||||||
@ -481,7 +481,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView):
|
|||||||
return super(LaunderetteClickView, self).get(request, *args, **kwargs)
|
return super(LaunderetteClickView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
""" Handle the many possibilities of the post request """
|
"""Handle the many possibilities of the post request"""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
|
self.customer = Customer.objects.filter(user__id=self.kwargs["user_id"]).first()
|
||||||
self.subscriber = self.customer.user
|
self.subscriber = self.customer.user
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -280,7 +280,8 @@ SITH_NAME = "Sith website"
|
|||||||
SITH_TWITTER = "@ae_utbm"
|
SITH_TWITTER = "@ae_utbm"
|
||||||
|
|
||||||
# AE configuration
|
# AE configuration
|
||||||
SITH_MAIN_CLUB_ID = 1 # TODO: keep only that first setting, with the ID, and do the same for the other clubs
|
# TODO: keep only that first setting, with the ID, and do the same for the other clubs
|
||||||
|
SITH_MAIN_CLUB_ID = 1
|
||||||
SITH_MAIN_CLUB = {
|
SITH_MAIN_CLUB = {
|
||||||
"name": "AE",
|
"name": "AE",
|
||||||
"unix_name": "ae",
|
"unix_name": "ae",
|
||||||
@ -477,14 +478,14 @@ SITH_SUBSCRIPTION_END = 10
|
|||||||
# Subscription durations are in semestres
|
# Subscription durations are in semestres
|
||||||
# Be careful, modifying this parameter will need a migration to be applied
|
# Be careful, modifying this parameter will need a migration to be applied
|
||||||
SITH_SUBSCRIPTIONS = {
|
SITH_SUBSCRIPTIONS = {
|
||||||
"un-semestre": {"name": _("One semester"), "price": 15, "duration": 1},
|
"un-semestre": {"name": _("One semester"), "price": 20, "duration": 1},
|
||||||
"deux-semestres": {"name": _("Two semesters"), "price": 28, "duration": 2},
|
"deux-semestres": {"name": _("Two semesters"), "price": 35, "duration": 2},
|
||||||
"cursus-tronc-commun": {
|
"cursus-tronc-commun": {
|
||||||
"name": _("Common core cursus"),
|
"name": _("Common core cursus"),
|
||||||
"price": 45,
|
"price": 60,
|
||||||
"duration": 4,
|
"duration": 4,
|
||||||
},
|
},
|
||||||
"cursus-branche": {"name": _("Branch cursus"), "price": 45, "duration": 6},
|
"cursus-branche": {"name": _("Branch cursus"), "price": 60, "duration": 6},
|
||||||
"cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
|
"cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
|
||||||
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
|
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
|
||||||
"assidu": {"name": _("Assidu member"), "price": 0, "duration": 2},
|
"assidu": {"name": _("Assidu member"), "price": 0, "duration": 2},
|
||||||
@ -497,6 +498,7 @@ SITH_SUBSCRIPTIONS = {
|
|||||||
"price": 0,
|
"price": 0,
|
||||||
"duration": 1,
|
"duration": 1,
|
||||||
},
|
},
|
||||||
|
"un-mois-essai": {"name": _("One month for free"), "price": 0, "duration": 0.166},
|
||||||
"deux-mois-essai": {"name": _("Two months for free"), "price": 0, "duration": 0.33},
|
"deux-mois-essai": {"name": _("Two months for free"), "price": 0, "duration": 0.33},
|
||||||
"benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1},
|
"benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1},
|
||||||
"six-semaines-essai": {
|
"six-semaines-essai": {
|
||||||
@ -505,6 +507,7 @@ SITH_SUBSCRIPTIONS = {
|
|||||||
"duration": 0.23,
|
"duration": 0.23,
|
||||||
},
|
},
|
||||||
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
|
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
|
||||||
|
"membre-staff-ga": {"name": _("GA staff member"), "price": 1, "duration": 0.076},
|
||||||
# Discount subscriptions
|
# Discount subscriptions
|
||||||
"un-semestre-reduction": {
|
"un-semestre-reduction": {
|
||||||
"name": _("One semester (-20%)"),
|
"name": _("One semester (-20%)"),
|
||||||
@ -530,6 +533,12 @@ SITH_SUBSCRIPTIONS = {
|
|||||||
"name": _("Alternating cursus (-20%)"),
|
"name": _("Alternating cursus (-20%)"),
|
||||||
"price": 24,
|
"price": 24,
|
||||||
"duration": 6,
|
"duration": 6,
|
||||||
|
},
|
||||||
|
# CA special offer
|
||||||
|
"un-an-offert-CA": {
|
||||||
|
"name": _("One year for free(CA offer)"),
|
||||||
|
"price": 0,
|
||||||
|
"duration": 2,
|
||||||
}
|
}
|
||||||
# To be completed....
|
# To be completed....
|
||||||
}
|
}
|
||||||
@ -665,3 +674,17 @@ if "test" in sys.argv:
|
|||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
# Connection to sentry
|
# Connection to sentry
|
||||||
sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()])
|
sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()])
|
||||||
|
|
||||||
|
|
||||||
|
SITH_FRONT_DEP_VERSIONS = {
|
||||||
|
"https://github.com/chartjs/Chart.js/": "2.6.0",
|
||||||
|
"https://github.com/xdan/datetimepicker/": "2.5.21",
|
||||||
|
"https://github.com/Ionaru/easy-markdown-editor/": "2.7.0",
|
||||||
|
"https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
|
||||||
|
"https://github.com/jquery/jquery/": "3.1.0",
|
||||||
|
"https://github.com/sethmcl/jquery-ui/": "1.11.1",
|
||||||
|
"https://github.com/viralpatel/jquery.shorten/": "",
|
||||||
|
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
||||||
|
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
||||||
|
"https://github.com/vuejs/vue-next": "3.2.18",
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017,2021
|
||||||
# - Sli <antoine@bartuccio.fr>
|
# - Sli <antoine@bartuccio.fr>
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -27,7 +28,10 @@ from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel
|
|||||||
|
|
||||||
class TemplatesPanel(BaseTemplatesPanel):
|
class TemplatesPanel(BaseTemplatesPanel):
|
||||||
def generate_stats(self, *args):
|
def generate_stats(self, *args):
|
||||||
|
try:
|
||||||
template = self.templates[0]["template"]
|
template = self.templates[0]["template"]
|
||||||
if not hasattr(template, "engine") and hasattr(template, "backend"):
|
if not hasattr(template, "engine") and hasattr(template, "backend"):
|
||||||
template.engine = template.backend
|
template.engine = template.backend
|
||||||
|
except IndexError: # No template
|
||||||
|
pass
|
||||||
return super().generate_stats(*args)
|
return super().generate_stats(*args)
|
||||||
|
@ -120,8 +120,7 @@ class ShoppingList(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ShoppingListItem(models.Model):
|
class ShoppingListItem(models.Model):
|
||||||
"""
|
""""""
|
||||||
"""
|
|
||||||
|
|
||||||
shopping_lists = models.ManyToManyField(
|
shopping_lists = models.ManyToManyField(
|
||||||
ShoppingList,
|
ShoppingList,
|
||||||
|
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")
|
call_command("populate")
|
||||||
self.user = User.objects.filter(username="public").first()
|
self.user = User.objects.filter(username="public").first()
|
||||||
|
|
||||||
|
def test_duration_one_month(self):
|
||||||
|
|
||||||
|
s = Subscription(
|
||||||
|
member=User.objects.filter(pk=self.user.pk).first(),
|
||||||
|
subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
|
||||||
|
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
|
||||||
|
)
|
||||||
|
s.subscription_start = date(2017, 8, 29)
|
||||||
|
s.subscription_end = s.compute_end(duration=0.166, start=s.subscription_start)
|
||||||
|
s.save()
|
||||||
|
self.assertTrue(s.subscription_end == date(2017, 9, 29))
|
||||||
|
|
||||||
def test_duration_two_months(self):
|
def test_duration_two_months(self):
|
||||||
|
|
||||||
s = Subscription(
|
s = Subscription(
|
||||||
@ -122,11 +134,11 @@ class SubscriptionIntegrationTest(TestCase):
|
|||||||
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
|
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
|
||||||
)
|
)
|
||||||
s.subscription_start = date(2017, 8, 29)
|
s.subscription_start = date(2017, 8, 29)
|
||||||
s.subscription_end = s.compute_end(duration=0.33, start=s.subscription_start)
|
s.subscription_end = s.compute_end(duration=0.333, start=s.subscription_start)
|
||||||
s.save()
|
s.save()
|
||||||
self.assertTrue(s.subscription_end == date(2017, 10, 29))
|
self.assertTrue(s.subscription_end == date(2017, 10, 29))
|
||||||
|
|
||||||
def test_duration_two_months(self):
|
def test_duration_one_day(self):
|
||||||
|
|
||||||
s = Subscription(
|
s = Subscription(
|
||||||
member=User.objects.filter(pk=self.user.pk).first(),
|
member=User.objects.filter(pk=self.user.pk).first(),
|
||||||
|
@ -36,22 +36,17 @@ from subscription.models import Subscription
|
|||||||
from core.views.forms import SelectDateTime
|
from core.views.forms import SelectDateTime
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.forms import SelectDate
|
from core.views.forms import SelectDate
|
||||||
|
from core.views.forms import TzAwareDateTimeField
|
||||||
|
|
||||||
|
|
||||||
class SelectionDateForm(forms.Form):
|
class SelectionDateForm(forms.Form):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SelectionDateForm, self).__init__(*args, **kwargs)
|
super(SelectionDateForm, self).__init__(*args, **kwargs)
|
||||||
self.fields["start_date"] = forms.DateTimeField(
|
self.fields["start_date"] = TzAwareDateTimeField(
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
label=_("Start date"), required=True
|
||||||
label=_("Start date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
self.fields["end_date"] = forms.DateTimeField(
|
self.fields["end_date"] = TzAwareDateTimeField(
|
||||||
input_formats=["%Y-%m-%d %H:%M:%S"],
|
label=_("End date"), required=True
|
||||||
label=_("End date"),
|
|
||||||
widget=SelectDateTime,
|
|
||||||
required=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>{{ u.user.get_display_name() }}</div>
|
<div>{{ u.user.get_display_name() }}</div>
|
||||||
<div><a href="{{ url('trombi:delete_user', user_id=u.id) }}">{% trans %}Delete{% endtrans %}</a></div>
|
<div><a href="{{ url('trombi:delete_user', user_id=u.id) }}">{% trans %}Delete{% endtrans %}</a></div>
|
||||||
|
<div><a href="{{ url('trombi:create_membership', user_id=u.id) }}">{% trans %}Add club membership{% endtrans %}</a></div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2017
|
# Copyright 2017,2020
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
|
# - Sli <antoine@bartuccio.fr>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -81,4 +82,9 @@ urlpatterns = [
|
|||||||
UserTrombiDeleteMembershipView.as_view(),
|
UserTrombiDeleteMembershipView.as_view(),
|
||||||
name="delete_membership",
|
name="delete_membership",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^membership/(?P<user_id>[0-9]+)/create$",
|
||||||
|
UserTrombiAddMembershipView.as_view(),
|
||||||
|
name="create_membership",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2017
|
# Copyright 2017,2020
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@libskia.so>
|
||||||
|
# - Sli <antoine.bartuccio@gmail.com>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -31,6 +32,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
||||||
from ajax_select.fields import AutoCompleteSelectField
|
from ajax_select.fields import AutoCompleteSelectField
|
||||||
|
|
||||||
@ -410,6 +412,35 @@ class UserTrombiDeleteMembershipView(TrombiTabsMixin, CanEditMixin, DeleteView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Used by admins when someone does not have every club in his list
|
||||||
|
class UserTrombiAddMembershipView(TrombiTabsMixin, CreateView):
|
||||||
|
model = TrombiClubMembership
|
||||||
|
template_name = "core/edit.jinja"
|
||||||
|
fields = ["club", "role", "start", "end"]
|
||||||
|
pk_url_kwarg = "user_id"
|
||||||
|
current_tab = "profile"
|
||||||
|
|
||||||
|
def dispatch(self, request, *arg, **kwargs):
|
||||||
|
self.trombi_user = get_object_or_404(TrombiUser, pk=kwargs["user_id"])
|
||||||
|
if not self.trombi_user.trombi.is_owned_by(request.user):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
return super(UserTrombiAddMembershipView, self).dispatch(
|
||||||
|
request, *arg, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
membership = form.save(commit=False)
|
||||||
|
membership.user = self.trombi_user
|
||||||
|
membership.save()
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse(
|
||||||
|
"trombi:detail", kwargs={"trombi_id": self.trombi_user.trombi.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView):
|
class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView):
|
||||||
model = TrombiClubMembership
|
model = TrombiClubMembership
|
||||||
pk_url_kwarg = "membership_id"
|
pk_url_kwarg = "membership_id"
|
||||||
|
Loading…
Reference in New Issue
Block a user