Merge branch 'master' into gender_options

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

View File

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

18
.mailmap Normal file
View File

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

View File

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

View File

@ -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
) )

View File

@ -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="news">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 birthdays %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
<div id="left_column" class="news_column"> <div id="left_column" class="news_column">
{% for news in object_list.filter(type="NOTICE") %} {% for news in object_list.filter(type="NOTICE") %}
@ -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 %}

View File

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

View File

@ -7,6 +7,23 @@
{% block content %} {% block content %}
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a> <a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
{% if bad_recipients %}
<p>
<span class="important">
{% trans %}The following recipients were refused by the SMTP:{% endtrans %}
</span>
<ul>
{% for r in bad_recipients.keys() %}
<li>{{ r }}</li>
{% endfor %}
</ul>
</p>
<form method="post" action="">
{% csrf_token %}
<button type="submit" name="send" value="clean">{% trans %}Clean subscribers{% endtrans %}</button>
</form>
{% else %}
{% if request.GET['send'] %} {% if request.GET['send'] %}
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p> <p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %} {% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
@ -17,6 +34,7 @@
<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 }}
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,24 @@
{% 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="bar_ui">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="user_info"> <div id="user_info">
<h5>{% trans %}Customer{% endtrans %}</h5> <h5>{% trans %}Customer{% endtrans %}</h5>
{{ user_mini_profile(customer.user) }} {{ user_mini_profile(customer.user) }}
@ -51,57 +45,55 @@
{% 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">
@ -125,6 +117,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div id="products"> <div id="products">
<ul> <ul>
{% for category in categories.keys() -%} {% for category in categories.keys() -%}
@ -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>

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ from django.views.generic.edit import (
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
from django.utils import timezone from django.utils import timezone
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
import re import re
import pytz import pytz
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
@ -69,6 +70,7 @@ from counter.models import (
Permanency, Permanency,
) )
from accounting.models import CurrencyField from accounting.models import CurrencyField
from core.views.forms import TzAwareDateTimeField
class CounterAdminMixin(View): class CounterAdminMixin(View):
@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
current_tab = "counter" current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests
response = {"errors": []}
status = HTTPStatus.OK
if self.request.session["too_young"]:
response["errors"].append(_("Too young for that product"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_allowed"]:
response["errors"].append(_("Not allowed for that product"))
status = HTTPStatus.FORBIDDEN
if self.request.session["no_age"]:
response["errors"].append(_("No date of birth provided"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_enough"]:
response["errors"].append(_("Not enough money"))
status = HTTPStatus.PAYMENT_REQUIRED
if len(response["errors"]) > 1:
status = HTTPStatus.BAD_REQUEST
response["basket"] = self.request.session["basket"]
return JsonResponse(response, status=status)
else: # Standard HTML page
return super().render_to_response(*args, **kwargs)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
obj = self.get_object() obj = self.get_object()
@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
or len(obj.get_barmen_list()) < 1 or len(obj.get_barmen_list()) < 1
): ):
raise PermissionDenied return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
else: else:
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise PermissionDenied raise PermissionDenied
@ -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):

View File

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

View File

@ -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
@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,7 +1,8 @@
# -*- coding:utf-8 -* # -*- coding:utf-8 -*
# #
# Copyright 2016,2017 # Copyright 2016,2017,2021
# - Sli <antoine@bartuccio.fr> # - Sli <antoine@bartuccio.fr>
# - Skia <skia@hya.sk>
# #
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr. # http://ae.utbm.fr.
@ -27,7 +28,10 @@ from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel
class TemplatesPanel(BaseTemplatesPanel): class TemplatesPanel(BaseTemplatesPanel):
def generate_stats(self, *args): def generate_stats(self, *args):
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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