Merge branch 'master' into gender_options

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

View File

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

18
.mailmap Normal file
View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User
from core.views.forms import SelectDate, SelectDateTime
from counter.models import Counter
from core.views.forms import TzAwareDateTimeField
class ClubEditForm(forms.ModelForm):
@ -158,18 +159,9 @@ class MailingForm(forms.Form):
class SellingsForm(forms.Form):
begin_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
counters = forms.ModelMultipleChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
)

View File

@ -6,65 +6,13 @@
{% endblock %}
{% block content %}
<div id="news">
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
{% if user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) %}
<div id="news_admin">
<a href="{{ url('com:news_admin_list') }}">{% trans %}Administrate news{% endtrans %}</a>
</div>
{% endif %}
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 birthdays %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
<div id="news">
<div id="left_column" class="news_column">
{% for news in object_list.filter(type="NOTICE") %}
@ -74,8 +22,7 @@
</section>
{% endfor %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(),
dates__end_date__gte=timezone.now(), type="CALL") %}
{% for news in object_list.filter(dates__start_date__lte=timezone.now(), dates__end_date__gte=timezone.now(), type="CALL") %}
<section class="news_call">
<h4> <a href="{{ url('com:news_detail', news_id=news.id) }}">{{ news.title }}</a></h4>
<div class="news_date">
@ -88,8 +35,7 @@
</section>
{% endfor %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5),
news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
{% set events_dates = NewsDate.objects.filter(end_date__gte=timezone.now(), start_date__lte=timezone.now()+timedelta(days=5), news__type="EVENT", news__is_moderated=True).datetimes('start_date', 'day') %}
<h3>{% trans %}Events today and the next few days{% endtrans %}</h3>
{% if events_dates %}
{% for d in events_dates %}
@ -152,6 +98,58 @@
{% endfor %}
{% endif %}
</div>
<div id="right_column" class="news_column">
<div id="agenda">
<div id="agenda_title">{% trans %}Agenda{% endtrans %}</div>
<div id="agenda_content">
{% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
news__is_moderated=True, news__type__in=["WEEKLY",
"EVENT"]).order_by('start_date', 'end_date') %}
<div class="agenda_item">
<div class="agenda_date">
<strong>{{ d.start_date|localtime|date('D d M Y') }}</strong>
</div>
<div class="agenda_time">
<span>{{ d.start_date|localtime|time(DATETIME_FORMAT) }}</span> -
<span>{{ d.end_date|localtime|time(DATETIME_FORMAT) }}</span>
</div>
<div>
<strong><a href="{{ url('com:news_detail', news_id=d.news.id) }}">{{ d.news.title }}</a></strong>
<a href="{{ d.news.club.get_absolute_url() }}">{{ d.news.club }}</a>
</div>
<div class="agenda_item_content">{{ d.news.summary|markdown }}</div>
</div>
{% endfor %}
</div>
</div>
<div id="birthdays">
<div id="birthdays_title">{% trans %}Birthdays{% endtrans %}</div>
<div id="birthdays_content">
{% if user.is_subscribed %}
{# Cache request for 1 hour #}
{% cache 3600 "birthdays" %}
<ul class="birthdays_year">
{% for d in birthdays.dates('date_of_birth', 'year', 'DESC') %}
<li>
{% trans age=timezone.now().year - d.year %}{{ age }} year old{% endtrans %}
<ul>
{% for u in birthdays.filter(date_of_birth__year=d.year) %}
<li><a href="{{ u.get_absolute_url() }}">{{ u.get_short_name() }}</a></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endcache %}
{% else %}
<p>{% trans %}You need an up to date subscription to access this content{% endtrans %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
from django.db.models.query import QuerySet
from core.views.forms import SelectDateTime, MarkdownInput
from election.models import Election, Role, Candidature, ElectionList, Vote
from core.views.forms import TzAwareDateTimeField
from ajax_select.fields import AutoCompleteSelectField
from ajax_select import make_ajax_field
@ -49,7 +50,7 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField):
class CandidateForm(forms.ModelForm):
""" Form to candidate """
"""Form to candidate"""
class Meta:
model = Candidature
@ -95,7 +96,7 @@ class VoteForm(forms.Form):
class RoleForm(forms.ModelForm):
""" Form for creating a role """
"""Form for creating a role"""
class Meta:
model = Role
@ -167,30 +168,12 @@ class ElectionForm(forms.ModelForm):
label=_("candidature groups"),
)
start_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start date"),
widget=SelectDateTime,
required=True,
)
end_date = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
widget=SelectDateTime,
required=True,
)
start_candidature = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("Start candidature"),
widget=SelectDateTime,
required=True,
)
end_candidature = forms.DateTimeField(
input_formats=["%Y-%m-%d %H:%M:%S"],
label=_("End candidature"),
widget=SelectDateTime,
required=True,
start_date = TzAwareDateTimeField(label=_("Start date"), required=True)
end_date = TzAwareDateTimeField(label=_("End date"), required=True)
start_candidature = TzAwareDateTimeField(
label=_("Start candidature"), required=True
)
end_candidature = TzAwareDateTimeField(label=_("End candidature"), required=True)
# Display elections
@ -261,7 +244,7 @@ class ElectionDetailView(CanViewMixin, DetailView):
return r
def get_context_data(self, **kwargs):
""" Add additionnal data to the template """
"""Add additionnal data to the template"""
kwargs = super(ElectionDetailView, self).get_context_data(**kwargs)
kwargs["election_form"] = VoteForm(self.object, self.request.user)
kwargs["election_results"] = self.object.results
@ -322,7 +305,7 @@ class VoteFormView(CanCreateMixin, FormView):
return reverse_lazy("election:detail", kwargs={"election_id": self.election.id})
def get_context_data(self, **kwargs):
""" Add additionnal data to the template """
"""Add additionnal data to the template"""
kwargs = super(VoteFormView, self).get_context_data(**kwargs)
kwargs["object"] = self.election
kwargs["election"] = self.election

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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