mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 11:03:04 +00:00 
			
		
		
		
	Merge branch 'master' into gender_options
This commit is contained in:
		@@ -4,19 +4,27 @@ stages:
 | 
			
		||||
test:
 | 
			
		||||
  stage: test
 | 
			
		||||
  script:
 | 
			
		||||
  - env
 | 
			
		||||
  - apt-get update
 | 
			
		||||
  - apt-get install -y gettext python3-xapian libgraphviz-dev
 | 
			
		||||
  - pushd /usr/lib/python3/dist-packages/xapian && ln -s _xapian* _xapian.so && popd
 | 
			
		||||
  - export PYTHONPATH="/usr/lib/python3/dist-packages:$PYTHONPATH"
 | 
			
		||||
  - python -c 'import xapian' # Fail immediately if there is a problem with xapian
 | 
			
		||||
  - pip install -r requirements.txt
 | 
			
		||||
  - pip install coverage
 | 
			
		||||
  - pip install -U -r requirements.txt
 | 
			
		||||
  - pip install -U coverage
 | 
			
		||||
  - mkdir -p /dev/shm/search_indexes
 | 
			
		||||
  - ln -s /dev/shm/search_indexes sith/search_indexes
 | 
			
		||||
  - ./manage.py compilemessages
 | 
			
		||||
  - coverage run ./manage.py test
 | 
			
		||||
  - coverage html
 | 
			
		||||
  - coverage report
 | 
			
		||||
  - cd doc
 | 
			
		||||
  - make html # Make documentation
 | 
			
		||||
  variables:
 | 
			
		||||
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_tests"
 | 
			
		||||
  cache:
 | 
			
		||||
    paths:
 | 
			
		||||
      - .cache/pip_tests
 | 
			
		||||
  artifacts:
 | 
			
		||||
    paths:
 | 
			
		||||
      - coverage_report/
 | 
			
		||||
@@ -24,5 +32,10 @@ test:
 | 
			
		||||
black:
 | 
			
		||||
  stage: test
 | 
			
		||||
  script:
 | 
			
		||||
    - pip install black
 | 
			
		||||
    - pip install -U black
 | 
			
		||||
    - black --check .
 | 
			
		||||
  variables:
 | 
			
		||||
    PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip_black"
 | 
			
		||||
  cache:
 | 
			
		||||
    paths:
 | 
			
		||||
      - .cache/pip_black
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.mailmap
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.mailmap
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
Code <gregoire.duvauchelle@utbm.fr>
 | 
			
		||||
Cyl <labetowiez@aol.fr>
 | 
			
		||||
Juste <maaxleblanc@gmail.com>
 | 
			
		||||
Krophil <pierre.brunet@krophil.fr>
 | 
			
		||||
Lo-J <renaudg779@gmail.com>
 | 
			
		||||
Nabos <gnikwo@hotmail.com>
 | 
			
		||||
Och <francescowitz68@gmail.com>
 | 
			
		||||
Partoo <joqaste@gmail.com>
 | 
			
		||||
Skia <skia@hya.sk> <lordbanana25@mailoo.org>
 | 
			
		||||
Skia <skia@hya.sk> <skia@libskia.so>
 | 
			
		||||
Sli <klmp200@klmp200.net>
 | 
			
		||||
Soldat <ryan-68@live.fr>
 | 
			
		||||
Terre <jbaptiste.lenglet+git@gmail.com>
 | 
			
		||||
Vial <robin.trioux@utbm.fr>
 | 
			
		||||
Zar <antoine.charmeau@utbm.fr> <antoine.charmeau@laposte.net>
 | 
			
		||||
root <root@localhost.localdomain>
 | 
			
		||||
tleb <tleb@openmailbox.org> <theo.lebrun@live.fr>
 | 
			
		||||
tleb <tleb@openmailbox.org> <theo.lebrun@utbm.fr>
 | 
			
		||||
@@ -25,7 +25,7 @@
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from datetime import date
 | 
			
		||||
from datetime import date, timedelta
 | 
			
		||||
 | 
			
		||||
from core.models import User
 | 
			
		||||
from accounting.models import (
 | 
			
		||||
@@ -110,6 +110,9 @@ class JournalTest(TestCase):
 | 
			
		||||
class OperationTest(TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        call_command("populate")
 | 
			
		||||
        self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
 | 
			
		||||
            "%d/%m/%Y"
 | 
			
		||||
        )
 | 
			
		||||
        self.journal = GeneralJournal.objects.filter(id=1).first()
 | 
			
		||||
        self.skia = User.objects.filter(username="skia").first()
 | 
			
		||||
        at = AccountingType(
 | 
			
		||||
@@ -158,7 +161,7 @@ class OperationTest(TestCase):
 | 
			
		||||
                "target_type": "OTHER",
 | 
			
		||||
                "target_id": "",
 | 
			
		||||
                "target_label": "Le fantome de la nuit",
 | 
			
		||||
                "date": "04/12/2020",
 | 
			
		||||
                "date": self.tomorrow_formatted,
 | 
			
		||||
                "mode": "CASH",
 | 
			
		||||
                "cheque_number": "",
 | 
			
		||||
                "invoice": "",
 | 
			
		||||
@@ -191,7 +194,7 @@ class OperationTest(TestCase):
 | 
			
		||||
                "target_type": "OTHER",
 | 
			
		||||
                "target_id": "",
 | 
			
		||||
                "target_label": "Le fantome de la nuit",
 | 
			
		||||
                "date": "04/12/2020",
 | 
			
		||||
                "date": self.tomorrow_formatted,
 | 
			
		||||
                "mode": "CASH",
 | 
			
		||||
                "cheque_number": "",
 | 
			
		||||
                "invoice": "",
 | 
			
		||||
@@ -218,7 +221,7 @@ class OperationTest(TestCase):
 | 
			
		||||
                "target_type": "OTHER",
 | 
			
		||||
                "target_id": "",
 | 
			
		||||
                "target_label": "Le fantome du jour",
 | 
			
		||||
                "date": "04/12/2020",
 | 
			
		||||
                "date": self.tomorrow_formatted,
 | 
			
		||||
                "mode": "CASH",
 | 
			
		||||
                "cheque_number": "",
 | 
			
		||||
                "invoice": "",
 | 
			
		||||
@@ -245,7 +248,7 @@ class OperationTest(TestCase):
 | 
			
		||||
                "target_type": "OTHER",
 | 
			
		||||
                "target_id": "",
 | 
			
		||||
                "target_label": "Le fantome de l'aurore",
 | 
			
		||||
                "date": "04/12/2020",
 | 
			
		||||
                "date": self.tomorrow_formatted,
 | 
			
		||||
                "mode": "CASH",
 | 
			
		||||
                "cheque_number": "",
 | 
			
		||||
                "invoice": "",
 | 
			
		||||
 
 | 
			
		||||
@@ -496,7 +496,7 @@ class OperationCreateView(CanCreateMixin, CreateView):
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add journal to the context """
 | 
			
		||||
        """Add journal to the context"""
 | 
			
		||||
        kwargs = super(OperationCreateView, self).get_context_data(**kwargs)
 | 
			
		||||
        if self.journal:
 | 
			
		||||
            kwargs["object"] = self.journal
 | 
			
		||||
@@ -514,7 +514,7 @@ class OperationEditView(CanEditMixin, UpdateView):
 | 
			
		||||
    template_name = "accounting/operation_edit.jinja"
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add journal to the context """
 | 
			
		||||
        """Add journal to the context"""
 | 
			
		||||
        kwargs = super(OperationEditView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["object"] = self.object.journal
 | 
			
		||||
        return kwargs
 | 
			
		||||
@@ -735,7 +735,7 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return statement
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add infos to the context """
 | 
			
		||||
        """Add infos to the context"""
 | 
			
		||||
        kwargs = super(JournalNatureStatementView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["statement"] = self.big_statement()
 | 
			
		||||
        return kwargs
 | 
			
		||||
@@ -774,7 +774,7 @@ class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return sum(self.statement(movement_type).values())
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add journal to the context """
 | 
			
		||||
        """Add journal to the context"""
 | 
			
		||||
        kwargs = super(JournalPersonStatementView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["credit_statement"] = self.statement("CREDIT")
 | 
			
		||||
        kwargs["debit_statement"] = self.statement("DEBIT")
 | 
			
		||||
@@ -804,7 +804,7 @@ class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView)
 | 
			
		||||
        return statement
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add journal to the context """
 | 
			
		||||
        """Add journal to the context"""
 | 
			
		||||
        kwargs = super(JournalAccountingStatementView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["statement"] = self.statement()
 | 
			
		||||
        return kwargs
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,8 +36,8 @@
 | 
			
		||||
                    <div class="name">{{ poster.name }}</div>
 | 
			
		||||
                    <div class="image"><img src="{{ poster.file.url }}"></img></div>
 | 
			
		||||
                    <div class="dates">
 | 
			
		||||
                        <div class="begin">{{ poster.date_begin | date("d/M/Y H:m") }}</div>
 | 
			
		||||
                        <div class="end">{{ poster.date_end | date("d/M/Y H:m") }}</div>
 | 
			
		||||
                        <div class="begin">{{ poster.date_begin | localtime | date("d/M/Y H:m") }}</div>
 | 
			
		||||
                        <div class="end">{{ poster.date_end | localtime | date("d/M/Y H:m") }}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% if app == "com" %}
 | 
			
		||||
                        <a class="edit" href="{{ url(app + ":poster_edit", poster.id) }}">{% trans %}Edit{% endtrans %}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,15 +7,33 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<a href="{{ url('com:weekmail') }}">{% trans %}Back{% endtrans %}</a>
 | 
			
		||||
{% if request.GET['send'] %}
 | 
			
		||||
<p>{% trans %}Are you sure you want to send this weekmail?{% endtrans %}</p>
 | 
			
		||||
{% if request.LANGUAGE_CODE != settings.LANGUAGE_CODE[:2] %}
 | 
			
		||||
<p><strong>{% trans %}Warning: you are sending the weekmail in another language than the default one!{% endtrans %}</strong></p>
 | 
			
		||||
{% endif %}
 | 
			
		||||
<form method="post" action="">
 | 
			
		||||
{% 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 }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								com/tests.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								com/tests.py
									
									
									
									
									
								
							@@ -79,9 +79,11 @@ class ComTest(TestCase):
 | 
			
		||||
        )
 | 
			
		||||
        r = self.client.get(reverse("core:index"))
 | 
			
		||||
        self.assertTrue(r.status_code == 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            """<div id="alert_box">\\n                <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>"""
 | 
			
		||||
            in str(r.content)
 | 
			
		||||
        self.assertContains(
 | 
			
		||||
            r,
 | 
			
		||||
            """<div id="alert_box">
 | 
			
		||||
                    <div class="markdown"><h3>ALERTE!</h3>
 | 
			
		||||
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_info_msg(self):
 | 
			
		||||
@@ -95,9 +97,10 @@ class ComTest(TestCase):
 | 
			
		||||
        )
 | 
			
		||||
        r = self.client.get(reverse("core:index"))
 | 
			
		||||
        self.assertTrue(r.status_code == 200)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            """<div id="info_box">\\n                <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>"""
 | 
			
		||||
            in str(r.content)
 | 
			
		||||
        self.assertContains(
 | 
			
		||||
            r,
 | 
			
		||||
            """<div id="info_box">
 | 
			
		||||
                    <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_birthday_non_subscribed_user(self):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								com/views.py
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								com/views.py
									
									
									
									
									
								
							@@ -39,6 +39,7 @@ from django.core.exceptions import PermissionDenied
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from smtplib import SMTPRecipientsRefused
 | 
			
		||||
 | 
			
		||||
from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle, Screen, Poster
 | 
			
		||||
from core.views import (
 | 
			
		||||
@@ -52,6 +53,7 @@ from core.views import (
 | 
			
		||||
from core.views.forms import SelectDateTime, MarkdownInput
 | 
			
		||||
from core.models import Notification, RealGroup, User
 | 
			
		||||
from club.models import Club, Mailing
 | 
			
		||||
from core.views.forms import TzAwareDateTimeField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Sith object
 | 
			
		||||
@@ -72,20 +74,14 @@ class PosterForm(forms.ModelForm):
 | 
			
		||||
            "display_time",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {"screens": forms.CheckboxSelectMultiple}
 | 
			
		||||
        help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
 | 
			
		||||
 | 
			
		||||
    date_begin = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
    date_begin = TzAwareDateTimeField(
 | 
			
		||||
        label=_("Start date"),
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
        required=True,
 | 
			
		||||
        initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
 | 
			
		||||
    )
 | 
			
		||||
    date_end = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("End date"),
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
    date_end = TzAwareDateTimeField(label=_("End date"), required=False)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        self.user = kwargs.pop("user", None)
 | 
			
		||||
@@ -199,24 +195,10 @@ class NewsForm(forms.ModelForm):
 | 
			
		||||
            "content": MarkdownInput,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    start_date = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("Start date"),
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
    end_date = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("End date"),
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
    until = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("Until"),
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
    start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
 | 
			
		||||
    end_date = TzAwareDateTimeField(label=_("End date"), required=False)
 | 
			
		||||
    until = TzAwareDateTimeField(label=_("Until"), required=False)
 | 
			
		||||
 | 
			
		||||
    automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
@@ -433,22 +415,35 @@ class NewsDetailView(CanViewMixin, DetailView):
 | 
			
		||||
# Weekmail
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
 | 
			
		||||
class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView):
 | 
			
		||||
    model = Weekmail
 | 
			
		||||
    template_name = "com/weekmail_preview.jinja"
 | 
			
		||||
    success_url = reverse_lazy("com:weekmail")
 | 
			
		||||
    current_tab = "weekmail"
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        self.bad_recipients = []
 | 
			
		||||
        return super(WeekmailPreviewView, self).dispatch(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def post(self, request, *args, **kwargs):
 | 
			
		||||
        self.object = self.get_object()
 | 
			
		||||
        try:
 | 
			
		||||
        if request.POST["send"] == "validate":
 | 
			
		||||
            try:
 | 
			
		||||
                self.object.send()
 | 
			
		||||
                return HttpResponseRedirect(
 | 
			
		||||
                    reverse("com:weekmail") + "?qn_weekmail_send_success"
 | 
			
		||||
                )
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
            except SMTPRecipientsRefused as e:
 | 
			
		||||
                self.bad_recipients = e.recipients
 | 
			
		||||
        elif request.POST["send"] == "clean":
 | 
			
		||||
            try:
 | 
			
		||||
                self.object.send()  # This should fail
 | 
			
		||||
            except SMTPRecipientsRefused as e:
 | 
			
		||||
                users = User.objects.filter(email__in=e.recipients.keys())
 | 
			
		||||
                for u in users:
 | 
			
		||||
                    u.preferences.receive_weekmail = False
 | 
			
		||||
                    u.preferences.save()
 | 
			
		||||
                self.quick_notif_list += ["qn_success"]
 | 
			
		||||
        return super(WeekmailPreviewView, self).get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_object(self, queryset=None):
 | 
			
		||||
@@ -458,6 +453,7 @@ class WeekmailPreviewView(ComTabsMixin, CanEditPropMixin, DetailView):
 | 
			
		||||
        """Add rendered weekmail"""
 | 
			
		||||
        kwargs = super(WeekmailPreviewView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["weekmail_rendered"] = self.object.render_html()
 | 
			
		||||
        kwargs["bad_recipients"] = self.bad_recipients
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -534,7 +530,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
 | 
			
		||||
        return super(WeekmailEditView, self).get(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """Add orphan articles """
 | 
			
		||||
        """Add orphan articles"""
 | 
			
		||||
        kwargs = super(WeekmailEditView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None)
 | 
			
		||||
        return kwargs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										107
									
								
								core/management/commands/check_front.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								core/management/commands/check_front.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import re
 | 
			
		||||
from subprocess import PIPE, Popen, TimeoutExpired
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
 | 
			
		||||
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
 | 
			
		||||
# added "v?"
 | 
			
		||||
semver_regex = re.compile(
 | 
			
		||||
    """^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = "Checks the front dependencies are up to date."
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        deps = settings.SITH_FRONT_DEP_VERSIONS
 | 
			
		||||
 | 
			
		||||
        processes = dict(
 | 
			
		||||
            (url, create_process(url))
 | 
			
		||||
            for url in deps.keys()
 | 
			
		||||
            if parse_semver(deps[url]) is not None
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for url, process in processes.items():
 | 
			
		||||
            try:
 | 
			
		||||
                stdout, stderr = process.communicate(timeout=15)
 | 
			
		||||
            except TimeoutExpired:
 | 
			
		||||
                process.kill()
 | 
			
		||||
                self.stderr.write(self.style.WARNING("{}: timeout".format(url)))
 | 
			
		||||
                continue
 | 
			
		||||
                # error, notice, warning
 | 
			
		||||
 | 
			
		||||
            stdout = stdout.decode("utf-8")
 | 
			
		||||
            stderr = stderr.decode("utf-8")
 | 
			
		||||
 | 
			
		||||
            if stderr != "":
 | 
			
		||||
                self.stderr.write(self.style.WARNING(stderr.strip()))
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # get all tags, parse them as semvers and find the biggest
 | 
			
		||||
            tags = list_tags(stdout)
 | 
			
		||||
            tags = map(parse_semver, tags)
 | 
			
		||||
            tags = filter(lambda tag: tag is not None, tags)
 | 
			
		||||
            latest_version = max(tags)
 | 
			
		||||
 | 
			
		||||
            # cannot fail as those which fail are filtered in the processes dict creation
 | 
			
		||||
            current_version = parse_semver(deps[url])
 | 
			
		||||
            assert current_version is not None
 | 
			
		||||
 | 
			
		||||
            if latest_version == current_version:
 | 
			
		||||
                msg = "{}: {}".format(url, semver_to_s(current_version))
 | 
			
		||||
                self.stdout.write(self.style.SUCCESS(msg))
 | 
			
		||||
            else:
 | 
			
		||||
                msg = "{}: {} < {}".format(
 | 
			
		||||
                    url, semver_to_s(current_version), semver_to_s(latest_version)
 | 
			
		||||
                )
 | 
			
		||||
                self.stdout.write(self.style.ERROR(msg))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_process(url):
 | 
			
		||||
    """Spawn a "git ls-remote --tags" child process."""
 | 
			
		||||
    return Popen(["git", "ls-remote", "--tags", url], stdout=PIPE, stderr=PIPE)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def list_tags(s):
 | 
			
		||||
    """Parses "git ls-remote --tags" output. Takes a string."""
 | 
			
		||||
    tag_prefix = "refs/tags/"
 | 
			
		||||
 | 
			
		||||
    for line in s.strip().split("\n"):
 | 
			
		||||
        # an example line could be:
 | 
			
		||||
        # "1f41e2293f9c3c1962d2d97afa666207b98a222a\trefs/tags/foo"
 | 
			
		||||
        parts = line.split("\t")
 | 
			
		||||
 | 
			
		||||
        # check we have a commit ID (SHA-1 hash) and a tag name
 | 
			
		||||
        assert len(parts) == 2
 | 
			
		||||
        assert len(parts[0]) == 40
 | 
			
		||||
        assert parts[1].startswith(tag_prefix)
 | 
			
		||||
 | 
			
		||||
        # avoid duplicates (a peeled tag will appear twice: as "name" and as "name^{}")
 | 
			
		||||
        if not parts[1].endswith("^{}"):
 | 
			
		||||
            yield parts[1][len(tag_prefix) :]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_semver(s):
 | 
			
		||||
    """
 | 
			
		||||
    Turns a semver string into a 3-tuple or None if the parsing failed, it is a
 | 
			
		||||
    prerelease or it has build metadata.
 | 
			
		||||
 | 
			
		||||
    See https://semver.org
 | 
			
		||||
    """
 | 
			
		||||
    m = semver_regex.match(s)
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        m is None
 | 
			
		||||
        or m.group("prerelease") is not None
 | 
			
		||||
        or m.group("buildmetadata") is not None
 | 
			
		||||
    ):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    return (int(m.group("major")), int(m.group("minor")), int(m.group("patch")))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def semver_to_s(t):
 | 
			
		||||
    """Expects a 3-tuple with ints and turns it into a string of type "1.2.3"."""
 | 
			
		||||
    return "{}.{}.{}".format(t[0], t[1], t[2])
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ from forum.models import ForumMessage, ForumMessageMeta
 | 
			
		||||
class UserIndex(indexes.SearchIndex, indexes.Indexable):
 | 
			
		||||
    text = indexes.CharField(document=True, use_template=True)
 | 
			
		||||
    auto = indexes.EdgeNgramField(use_template=True)
 | 
			
		||||
    last_update = indexes.DateTimeField(model_attr="last_update")
 | 
			
		||||
 | 
			
		||||
    def get_model(self):
 | 
			
		||||
        return User
 | 
			
		||||
@@ -45,6 +46,9 @@ class UserIndex(indexes.SearchIndex, indexes.Indexable):
 | 
			
		||||
    def get_updated_field(self):
 | 
			
		||||
        return "last_update"
 | 
			
		||||
 | 
			
		||||
    def prepare_auto(self, obj):
 | 
			
		||||
        return self.prepared_data["auto"].strip()[:245]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IndexSignalProcessor(signals.BaseSignalProcessor):
 | 
			
		||||
    def setup(self):
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								core/static/core/js/vue.global.prod.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								core/static/core/js/vue.global.prod.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -28,7 +28,7 @@ $twitblue: hsl(206, 82%, 63%);
 | 
			
		||||
 | 
			
		||||
$shadow-color: rgb(223, 223, 223);
 | 
			
		||||
 | 
			
		||||
$background-bouton-color: hsl(0, 0%, 90%);
 | 
			
		||||
$background-button-color: hsl(0, 0%, 95%);
 | 
			
		||||
 | 
			
		||||
/*--------------------------MEDIA QUERY HELPERS------------------------*/
 | 
			
		||||
$small-devices: 576px;
 | 
			
		||||
@@ -47,10 +47,11 @@ body {
 | 
			
		||||
input[type=button], input[type=submit], input[type=reset],input[type=file] {
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  background-color: $background-bouton-color;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  background-color: $background-button-color;
 | 
			
		||||
  padding: 0.4em;
 | 
			
		||||
  margin: 0.1em;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  box-shadow: $shadow-color 0px 0px 1px;
 | 
			
		||||
@@ -62,9 +63,10 @@ input[type=button], input[type=submit], input[type=reset],input[type=file] {
 | 
			
		||||
button{
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  background-color: $background-bouton-color;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  background-color: $background-button-color;
 | 
			
		||||
  padding: 0.4em;
 | 
			
		||||
  margin: 0.1em;
 | 
			
		||||
  font-size: 1.18em;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  box-shadow: $shadow-color 0px 0px 1px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
@@ -75,24 +77,26 @@ button{
 | 
			
		||||
input,textarea[type=text],[type=number]{
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  background-color: $background-bouton-color;
 | 
			
		||||
  padding: 7px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  background-color: $background-button-color;
 | 
			
		||||
  padding: 0.4em;
 | 
			
		||||
  margin: 0.1em;
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  max-width: 95%;
 | 
			
		||||
}
 | 
			
		||||
textarea{
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  background-color: $background-bouton-color;
 | 
			
		||||
  background-color: $background-button-color;
 | 
			
		||||
  padding: 7px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
}
 | 
			
		||||
select{
 | 
			
		||||
  border: none;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  background-color: $background-bouton-color;
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  background-color: $background-button-color;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
@@ -130,9 +134,10 @@ a {
 | 
			
		||||
 | 
			
		||||
#header_language_chooser {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0.2em;
 | 
			
		||||
  right: 0.5em;
 | 
			
		||||
  top: 2em;
 | 
			
		||||
  left: 0.5em;
 | 
			
		||||
  width: 3%;
 | 
			
		||||
  min-width: 2.2em;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  input {
 | 
			
		||||
    display: block;
 | 
			
		||||
@@ -157,9 +162,6 @@ header {
 | 
			
		||||
  border-radius: 0px 0px 10px 10px;
 | 
			
		||||
 | 
			
		||||
  #header_logo {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    flex: none;
 | 
			
		||||
    background-size: 100% 100%;
 | 
			
		||||
    background-color: $white-color;
 | 
			
		||||
    padding: 0.2em;
 | 
			
		||||
    border-radius: 0px 0px 0px 9px;
 | 
			
		||||
@@ -169,11 +171,19 @@ header {
 | 
			
		||||
      margin: 0px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
 | 
			
		||||
      img {
 | 
			
		||||
          max-width: 70%;
 | 
			
		||||
          max-height: 100%;
 | 
			
		||||
          margin: auto;
 | 
			
		||||
          display: block;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #header_connect_links {
 | 
			
		||||
    margin: 0.6em 0.6em 0em auto;
 | 
			
		||||
    padding: 0.2em;
 | 
			
		||||
    color: $white-color;
 | 
			
		||||
    form {
 | 
			
		||||
      display: inline;
 | 
			
		||||
@@ -190,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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 #}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,12 @@
 | 
			
		||||
{% trans %}Delete confirmation{% endtrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block info_boxes %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block nav %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
 | 
			
		||||
<form action="" method="post">{% csrf_token %}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@
 | 
			
		||||
    {% if not form.instance.profile_pict %}
 | 
			
		||||
    <script src="{{ static('core/js/webcam.js') }}"></script>
 | 
			
		||||
    <script language="JavaScript">
 | 
			
		||||
        Webcam.on('error', function(msg) { console.log('Webcam.js error: ' + msg) })
 | 
			
		||||
        Webcam.set({
 | 
			
		||||
                width: 320,
 | 
			
		||||
                height: 240,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
    {% for picture in pictures[a.id] %}
 | 
			
		||||
        <div class="picture">
 | 
			
		||||
            <a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
 | 
			
		||||
                <img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%"/>
 | 
			
		||||
                <img src="{{ picture.get_download_thumb_url() }}" alt="{{ picture.get_display_name() }}" style="max-width: 100%" loading="lazy"/>
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,13 @@
 | 
			
		||||
{{ object.first_name }}
 | 
			
		||||
{{ object.last_name }}
 | 
			
		||||
{{ object.nick_name }}
 | 
			
		||||
{% load search_helpers %}
 | 
			
		||||
 | 
			
		||||
{% with first=object.first_name|safe|slugify last=object.last_name|safe|slugify nick=object.nick_name|default_if_none:""|safe|slugify %}
 | 
			
		||||
 | 
			
		||||
{{ first|replace:"|-| " }}
 | 
			
		||||
{{ last|replace:"|-| " }}
 | 
			
		||||
{{ nick|replace:"|-| " }}
 | 
			
		||||
{% if first|count:"-" != 0 %}{{ first|cut:"-" }}{% endif %}
 | 
			
		||||
{% if last|count:"-" != 0 %}{{ last|cut:"-" }}{% endif %}
 | 
			
		||||
{% if nick|count:"-" != 0 %}{{ nick|cut:"-" }}{% endif %}
 | 
			
		||||
{{ first|cut:"-" }}{{ last|cut:"-" }}
 | 
			
		||||
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								core/templatetags/search_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								core/templatetags/search_helpers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
from django.template.exceptions import TemplateSyntaxError
 | 
			
		||||
from django import template
 | 
			
		||||
from django.template.defaultfilters import stringfilter
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# arg should be of the form "|foo|bar" where the first character is the
 | 
			
		||||
# separator between old and new in value.replace(old, new)
 | 
			
		||||
@register.filter
 | 
			
		||||
@stringfilter
 | 
			
		||||
def replace(value, arg):
 | 
			
		||||
    # s.replace('', '') == s so len(arg) == 2 is fine
 | 
			
		||||
    if len(arg) < 2:
 | 
			
		||||
        raise TemplateSyntaxError("badly formatted argument")
 | 
			
		||||
 | 
			
		||||
    arg = arg.split(arg[0])
 | 
			
		||||
 | 
			
		||||
    if len(arg) != 3:
 | 
			
		||||
        raise TemplateSyntaxError("badly formatted argument")
 | 
			
		||||
 | 
			
		||||
    return value.replace(arg[1], arg[2])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
def count(value, arg):
 | 
			
		||||
    return value.count(arg)
 | 
			
		||||
@@ -42,6 +42,10 @@ from django.utils.translation import ugettext
 | 
			
		||||
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
 | 
			
		||||
from 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 []
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,9 @@ def write_log(instance, operation_type):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    log = OperationLog(
 | 
			
		||||
        label=str(instance), operator=get_user(), operation_type=operation_type,
 | 
			
		||||
        label=str(instance),
 | 
			
		||||
        operator=get_user(),
 | 
			
		||||
        operation_type=operation_type,
 | 
			
		||||
    ).save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,12 @@
 | 
			
		||||
{% trans obj=object %}Edit {{ obj }}{% endtrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block info_boxes %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block nav %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
 | 
			
		||||
<form action="" method="post" id="cash_summary_form">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,12 @@
 | 
			
		||||
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block info_boxes %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block nav %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,12 @@
 | 
			
		||||
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block info_boxes %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block nav %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
 | 
			
		||||
<h4>{% trans %}Refillings{% endtrans %}</h4>
 | 
			
		||||
 
 | 
			
		||||
@@ -68,18 +68,29 @@ class CounterTest(TestCase):
 | 
			
		||||
            location,
 | 
			
		||||
            {
 | 
			
		||||
                "action": "refill",
 | 
			
		||||
                "amount": "10",
 | 
			
		||||
                "amount": "5",
 | 
			
		||||
                "payment_method": "CASH",
 | 
			
		||||
                "bank": "OTHER",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        response = self.client.post(location, {"action": "code", "code": "BARB"})
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            location, {"action": "add_product", "product_id": "4"}
 | 
			
		||||
        )
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            location, {"action": "del_product", "product_id": "4"}
 | 
			
		||||
        )
 | 
			
		||||
        response = self.client.post(location, {"action": "code", "code": "2xdeco"})
 | 
			
		||||
        response = self.client.post(location, {"action": "code", "code": "1xbarb"})
 | 
			
		||||
        response = self.client.post(location, {"action": "code", "code": "fin"})
 | 
			
		||||
 | 
			
		||||
        response_get = self.client.get(response.get("location"))
 | 
			
		||||
        response_content = response_get.content.decode("utf-8")
 | 
			
		||||
        self.assertTrue("<li>2 x Barbar" in str(response_content))
 | 
			
		||||
        self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            "<p>Client : Richard Batsbak - Nouveau montant : 8.30"
 | 
			
		||||
            in str(response_get.content)
 | 
			
		||||
            "<p>Client : Richard Batsbak - Nouveau montant : 3.60"
 | 
			
		||||
            in str(response_content)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ from django.views.generic.edit import (
 | 
			
		||||
from django.forms.models import modelform_factory
 | 
			
		||||
from django.forms import CheckboxSelectMultiple
 | 
			
		||||
from django.urls import reverse_lazy, reverse
 | 
			
		||||
from django.http import HttpResponseRedirect, HttpResponse
 | 
			
		||||
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
@@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
 | 
			
		||||
import re
 | 
			
		||||
import pytz
 | 
			
		||||
from datetime import date, timedelta, datetime
 | 
			
		||||
from http import HTTPStatus
 | 
			
		||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
 | 
			
		||||
from ajax_select import make_ajax_field
 | 
			
		||||
 | 
			
		||||
@@ -69,6 +70,7 @@ from counter.models import (
 | 
			
		||||
    Permanency,
 | 
			
		||||
)
 | 
			
		||||
from accounting.models import CurrencyField
 | 
			
		||||
from core.views.forms import TzAwareDateTimeField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CounterAdminMixin(View):
 | 
			
		||||
@@ -357,6 +359,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
    pk_url_kwarg = "counter_id"
 | 
			
		||||
    current_tab = "counter"
 | 
			
		||||
 | 
			
		||||
    def render_to_response(self, *args, **kwargs):
 | 
			
		||||
        if self.request.is_ajax():  # JSON response for AJAX requests
 | 
			
		||||
            response = {"errors": []}
 | 
			
		||||
            status = HTTPStatus.OK
 | 
			
		||||
 | 
			
		||||
            if self.request.session["too_young"]:
 | 
			
		||||
                response["errors"].append(_("Too young for that product"))
 | 
			
		||||
                status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
 | 
			
		||||
            if self.request.session["not_allowed"]:
 | 
			
		||||
                response["errors"].append(_("Not allowed for that product"))
 | 
			
		||||
                status = HTTPStatus.FORBIDDEN
 | 
			
		||||
            if self.request.session["no_age"]:
 | 
			
		||||
                response["errors"].append(_("No date of birth provided"))
 | 
			
		||||
                status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
 | 
			
		||||
            if self.request.session["not_enough"]:
 | 
			
		||||
                response["errors"].append(_("Not enough money"))
 | 
			
		||||
                status = HTTPStatus.PAYMENT_REQUIRED
 | 
			
		||||
 | 
			
		||||
            if len(response["errors"]) > 1:
 | 
			
		||||
                status = HTTPStatus.BAD_REQUEST
 | 
			
		||||
 | 
			
		||||
            response["basket"] = self.request.session["basket"]
 | 
			
		||||
 | 
			
		||||
            return JsonResponse(response, status=status)
 | 
			
		||||
 | 
			
		||||
        else:  # Standard HTML page
 | 
			
		||||
            return super().render_to_response(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
 | 
			
		||||
        obj = self.get_object()
 | 
			
		||||
@@ -370,7 +400,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
                )
 | 
			
		||||
                or len(obj.get_barmen_list()) < 1
 | 
			
		||||
            ):
 | 
			
		||||
                raise PermissionDenied
 | 
			
		||||
                return HttpResponseRedirect(
 | 
			
		||||
                    reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            if not request.user.is_authenticated:
 | 
			
		||||
                raise PermissionDenied
 | 
			
		||||
@@ -394,7 +426,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def post(self, request, *args, **kwargs):
 | 
			
		||||
        """ Handle the many possibilities of the post request """
 | 
			
		||||
        """Handle the many possibilities of the post request"""
 | 
			
		||||
        self.object = self.get_object()
 | 
			
		||||
        self.refill_form = None
 | 
			
		||||
        if (self.object.type != "BAR" and not request.user.is_authenticated) or (
 | 
			
		||||
@@ -590,7 +622,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def del_product(self, request):
 | 
			
		||||
        """ Delete a product from the basket """
 | 
			
		||||
        """Delete a product from the basket"""
 | 
			
		||||
        pid = str(request.POST["product_id"])
 | 
			
		||||
        product = self.get_product(pid)
 | 
			
		||||
        if pid in request.session["basket"]:
 | 
			
		||||
@@ -632,7 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return self.render_to_response(context)
 | 
			
		||||
 | 
			
		||||
    def finish(self, request):
 | 
			
		||||
        """ Finish the click session, and validate the basket """
 | 
			
		||||
        """Finish the click session, and validate the basket"""
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            request.session["last_basket"] = []
 | 
			
		||||
            if self.sum_basket(request) > self.customer.amount:
 | 
			
		||||
@@ -684,7 +716,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def cancel(self, request):
 | 
			
		||||
        """ Cancel the click session """
 | 
			
		||||
        """Cancel the click session"""
 | 
			
		||||
        kwargs = {"counter_id": self.object.id}
 | 
			
		||||
        request.session.pop("basket", None)
 | 
			
		||||
        return HttpResponseRedirect(
 | 
			
		||||
@@ -706,7 +738,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
            raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add customer to the context """
 | 
			
		||||
        """Add customer to the context"""
 | 
			
		||||
        kwargs = super(CounterClick, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["products"] = self.object.products.select_related("product_type")
 | 
			
		||||
        kwargs["categories"] = {}
 | 
			
		||||
@@ -1360,7 +1392,7 @@ class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """Add form to the context """
 | 
			
		||||
        """Add form to the context"""
 | 
			
		||||
        kwargs = super(CounterLastOperationsView, self).get_context_data(**kwargs)
 | 
			
		||||
        threshold = timezone.now() - timedelta(
 | 
			
		||||
            minutes=settings.SITH_LAST_OPERATIONS_LIMIT
 | 
			
		||||
@@ -1422,7 +1454,7 @@ class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
 | 
			
		||||
        return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add form to the context """
 | 
			
		||||
        """Add form to the context"""
 | 
			
		||||
        kwargs = super(CounterCashSummaryView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["form"] = self.form
 | 
			
		||||
        return kwargs
 | 
			
		||||
@@ -1448,7 +1480,7 @@ class CounterStatView(DetailView, CounterAdminMixin):
 | 
			
		||||
    template_name = "counter/stats.jinja"
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add stats to the context """
 | 
			
		||||
        """Add stats to the context"""
 | 
			
		||||
        from django.db.models import Sum, Case, When, F, DecimalField
 | 
			
		||||
 | 
			
		||||
        kwargs = super(CounterStatView, self).get_context_data(**kwargs)
 | 
			
		||||
@@ -1553,18 +1585,8 @@ class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CashSummaryFormBase(forms.Form):
 | 
			
		||||
    begin_date = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("Begin date"),
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
    )
 | 
			
		||||
    end_date = forms.DateTimeField(
 | 
			
		||||
        input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
        label=_("End date"),
 | 
			
		||||
        required=False,
 | 
			
		||||
        widget=SelectDateTime,
 | 
			
		||||
    )
 | 
			
		||||
    begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
 | 
			
		||||
    end_date = TzAwareDateTimeField(label=_("End date"), required=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
			
		||||
@@ -1578,7 +1600,7 @@ class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
 | 
			
		||||
    paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add sums to the context """
 | 
			
		||||
        """Add sums to the context"""
 | 
			
		||||
        kwargs = super(CashSummaryListView, self).get_context_data(**kwargs)
 | 
			
		||||
        form = CashSummaryFormBase(self.request.GET)
 | 
			
		||||
        kwargs["form"] = form
 | 
			
		||||
@@ -1629,7 +1651,7 @@ class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
 | 
			
		||||
    current_tab = "invoices_call"
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        """ Add sums to the context """
 | 
			
		||||
        """Add sums to the context"""
 | 
			
		||||
        kwargs = super(InvoiceCallView, self).get_context_data(**kwargs)
 | 
			
		||||
        kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
 | 
			
		||||
        start_date = None
 | 
			
		||||
 
 | 
			
		||||
@@ -133,3 +133,14 @@ Pour lancer les tests il suffit d'utiliser la commande intégrée à django.
 | 
			
		||||
 | 
			
		||||
    # Lancer une méthode en particulier de cette même classe
 | 
			
		||||
    ./manage.py test core.tests.UserRegistrationTest.test_register_user_form_ok
 | 
			
		||||
 | 
			
		||||
Vérifier les dépendances Javascript
 | 
			
		||||
-----------------------------------
 | 
			
		||||
 | 
			
		||||
Une commande a été écrite pour vérifier les éventuelles mises à jour à faire sur les librairies Javascript utilisées.
 | 
			
		||||
N'oubliez pas de mettre à jour à la fois le fichier de la librairie, mais également sa version dans `sith/settings.py`.
 | 
			
		||||
 | 
			
		||||
.. code-block:: bash
 | 
			
		||||
 | 
			
		||||
    # Vérifier les mises à jour
 | 
			
		||||
    ./manage.py check_front
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ class EbouticMain(TemplateView):
 | 
			
		||||
        return self.render_to_response(self.get_context_data(**kwargs))
 | 
			
		||||
 | 
			
		||||
    def add_product(self, request):
 | 
			
		||||
        """ Add a product to the basket """
 | 
			
		||||
        """Add a product to the basket"""
 | 
			
		||||
        try:
 | 
			
		||||
            p = self.object.products.filter(id=int(request.POST["product_id"])).first()
 | 
			
		||||
            if not p.buying_groups.exists():
 | 
			
		||||
@@ -95,7 +95,7 @@ class EbouticMain(TemplateView):
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    def del_product(self, request):
 | 
			
		||||
        """ Delete a product from the basket """
 | 
			
		||||
        """Delete a product from the basket"""
 | 
			
		||||
        try:
 | 
			
		||||
            p = self.object.products.filter(id=int(request.POST["product_id"])).first()
 | 
			
		||||
            self.basket.del_product(p)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
 | 
			
		||||
from django.db.models.query import QuerySet
 | 
			
		||||
from core.views.forms import SelectDateTime, MarkdownInput
 | 
			
		||||
from election.models import Election, Role, Candidature, ElectionList, Vote
 | 
			
		||||
from core.views.forms import TzAwareDateTimeField
 | 
			
		||||
 | 
			
		||||
from ajax_select.fields import AutoCompleteSelectField
 | 
			
		||||
from ajax_select import make_ajax_field
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
											
										
									
								
							@@ -280,7 +280,8 @@ SITH_NAME = "Sith website"
 | 
			
		||||
SITH_TWITTER = "@ae_utbm"
 | 
			
		||||
 | 
			
		||||
# AE configuration
 | 
			
		||||
SITH_MAIN_CLUB_ID = 1  # TODO: keep only that first setting, with the ID, and do the same for the other clubs
 | 
			
		||||
# TODO: keep only that first setting, with the ID, and do the same for the other clubs
 | 
			
		||||
SITH_MAIN_CLUB_ID = 1
 | 
			
		||||
SITH_MAIN_CLUB = {
 | 
			
		||||
    "name": "AE",
 | 
			
		||||
    "unix_name": "ae",
 | 
			
		||||
@@ -477,14 +478,14 @@ SITH_SUBSCRIPTION_END = 10
 | 
			
		||||
# Subscription durations are in semestres
 | 
			
		||||
# Be careful, modifying this parameter will need a migration to be applied
 | 
			
		||||
SITH_SUBSCRIPTIONS = {
 | 
			
		||||
    "un-semestre": {"name": _("One semester"), "price": 15, "duration": 1},
 | 
			
		||||
    "deux-semestres": {"name": _("Two semesters"), "price": 28, "duration": 2},
 | 
			
		||||
    "un-semestre": {"name": _("One semester"), "price": 20, "duration": 1},
 | 
			
		||||
    "deux-semestres": {"name": _("Two semesters"), "price": 35, "duration": 2},
 | 
			
		||||
    "cursus-tronc-commun": {
 | 
			
		||||
        "name": _("Common core cursus"),
 | 
			
		||||
        "price": 45,
 | 
			
		||||
        "price": 60,
 | 
			
		||||
        "duration": 4,
 | 
			
		||||
    },
 | 
			
		||||
    "cursus-branche": {"name": _("Branch cursus"), "price": 45, "duration": 6},
 | 
			
		||||
    "cursus-branche": {"name": _("Branch cursus"), "price": 60, "duration": 6},
 | 
			
		||||
    "cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
 | 
			
		||||
    "membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
 | 
			
		||||
    "assidu": {"name": _("Assidu member"), "price": 0, "duration": 2},
 | 
			
		||||
@@ -497,6 +498,7 @@ SITH_SUBSCRIPTIONS = {
 | 
			
		||||
        "price": 0,
 | 
			
		||||
        "duration": 1,
 | 
			
		||||
    },
 | 
			
		||||
    "un-mois-essai": {"name": _("One month for free"), "price": 0, "duration": 0.166},
 | 
			
		||||
    "deux-mois-essai": {"name": _("Two months for free"), "price": 0, "duration": 0.33},
 | 
			
		||||
    "benevoles-euroks": {"name": _("Eurok's volunteer"), "price": 5, "duration": 0.1},
 | 
			
		||||
    "six-semaines-essai": {
 | 
			
		||||
@@ -505,6 +507,7 @@ SITH_SUBSCRIPTIONS = {
 | 
			
		||||
        "duration": 0.23,
 | 
			
		||||
    },
 | 
			
		||||
    "un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
 | 
			
		||||
    "membre-staff-ga": {"name": _("GA staff member"), "price": 1, "duration": 0.076},
 | 
			
		||||
    # Discount subscriptions
 | 
			
		||||
    "un-semestre-reduction": {
 | 
			
		||||
        "name": _("One semester (-20%)"),
 | 
			
		||||
@@ -530,6 +533,12 @@ SITH_SUBSCRIPTIONS = {
 | 
			
		||||
        "name": _("Alternating cursus (-20%)"),
 | 
			
		||||
        "price": 24,
 | 
			
		||||
        "duration": 6,
 | 
			
		||||
    },
 | 
			
		||||
    # CA special offer
 | 
			
		||||
    "un-an-offert-CA": {
 | 
			
		||||
        "name": _("One year for free(CA offer)"),
 | 
			
		||||
        "price": 0,
 | 
			
		||||
        "duration": 2,
 | 
			
		||||
    }
 | 
			
		||||
    # To be completed....
 | 
			
		||||
}
 | 
			
		||||
@@ -665,3 +674,17 @@ if "test" in sys.argv:
 | 
			
		||||
if SENTRY_DSN:
 | 
			
		||||
    # Connection to sentry
 | 
			
		||||
    sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SITH_FRONT_DEP_VERSIONS = {
 | 
			
		||||
    "https://github.com/chartjs/Chart.js/": "2.6.0",
 | 
			
		||||
    "https://github.com/xdan/datetimepicker/": "2.5.21",
 | 
			
		||||
    "https://github.com/Ionaru/easy-markdown-editor/": "2.7.0",
 | 
			
		||||
    "https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
 | 
			
		||||
    "https://github.com/jquery/jquery/": "3.1.0",
 | 
			
		||||
    "https://github.com/sethmcl/jquery-ui/": "1.11.1",
 | 
			
		||||
    "https://github.com/viralpatel/jquery.shorten/": "",
 | 
			
		||||
    "https://github.com/getsentry/sentry-javascript/": "4.0.6",
 | 
			
		||||
    "https://github.com/jhuckaby/webcamjs/": "1.0.0",
 | 
			
		||||
    "https://github.com/vuejs/vue-next": "3.2.18",
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
# -*- coding:utf-8 -*
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2016,2017
 | 
			
		||||
# Copyright 2016,2017,2021
 | 
			
		||||
# - Sli <antoine@bartuccio.fr>
 | 
			
		||||
# - Skia <skia@hya.sk>
 | 
			
		||||
#
 | 
			
		||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
 | 
			
		||||
# http://ae.utbm.fr.
 | 
			
		||||
@@ -27,7 +28,10 @@ from debug_toolbar.panels.templates import TemplatesPanel as BaseTemplatesPanel
 | 
			
		||||
 | 
			
		||||
class TemplatesPanel(BaseTemplatesPanel):
 | 
			
		||||
    def generate_stats(self, *args):
 | 
			
		||||
        try:
 | 
			
		||||
            template = self.templates[0]["template"]
 | 
			
		||||
            if not hasattr(template, "engine") and hasattr(template, "backend"):
 | 
			
		||||
                template.engine = template.backend
 | 
			
		||||
        except IndexError:  # No template
 | 
			
		||||
            pass
 | 
			
		||||
        return super().generate_stats(*args)
 | 
			
		||||
 
 | 
			
		||||
@@ -120,8 +120,7 @@ class ShoppingList(models.Model):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShoppingListItem(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    """
 | 
			
		||||
    """"""
 | 
			
		||||
 | 
			
		||||
    shopping_lists = models.ManyToManyField(
 | 
			
		||||
        ShoppingList,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								subscription/migrations/0012_auto_20200615_1438.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								subscription/migrations/0012_auto_20200615_1438.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
# Generated by Django 2.2.13 on 2020-06-15 12:38
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("subscription", "0011_auto_20190825_2215"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="subscription",
 | 
			
		||||
            name="subscription_type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("amicale/doceo", "Amicale/DOCEO member"),
 | 
			
		||||
                    ("assidu", "Assidu member"),
 | 
			
		||||
                    ("benevoles-euroks", "Eurok's volunteer"),
 | 
			
		||||
                    ("crous", "CROUS member"),
 | 
			
		||||
                    ("cursus-alternant", "Alternating cursus"),
 | 
			
		||||
                    ("cursus-alternant-reduction", "Alternating cursus (-20%)"),
 | 
			
		||||
                    ("cursus-branche", "Branch cursus"),
 | 
			
		||||
                    ("cursus-branche-reduction", "Branch cursus (-20%)"),
 | 
			
		||||
                    ("cursus-tronc-commun", "Common core cursus"),
 | 
			
		||||
                    ("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
 | 
			
		||||
                    ("deux-mois-essai", "Two months for free"),
 | 
			
		||||
                    ("deux-semestres", "Two semesters"),
 | 
			
		||||
                    ("deux-semestres-reduction", "Two semesters (-20%)"),
 | 
			
		||||
                    ("membre-honoraire", "Honorary member"),
 | 
			
		||||
                    ("membre-staff-ga", "GA staff member"),
 | 
			
		||||
                    ("reseau-ut", "UT network member"),
 | 
			
		||||
                    ("sbarro/esta", "Sbarro/ESTA member"),
 | 
			
		||||
                    ("six-semaines-essai", "Six weeks for free"),
 | 
			
		||||
                    ("un-jour", "One day"),
 | 
			
		||||
                    ("un-semestre", "One semester"),
 | 
			
		||||
                    ("un-semestre-reduction", "One semester (-20%)"),
 | 
			
		||||
                    ("un-semestre-welcome", "One semester Welcome Week"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                verbose_name="subscription type",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										46
									
								
								subscription/migrations/0013_auto_20200828_2117.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								subscription/migrations/0013_auto_20200828_2117.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
# Generated by Django 2.2.13 on 2020-08-28 19:17
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("subscription", "0012_auto_20200615_1438"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="subscription",
 | 
			
		||||
            name="subscription_type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("amicale/doceo", "Amicale/DOCEO member"),
 | 
			
		||||
                    ("assidu", "Assidu member"),
 | 
			
		||||
                    ("benevoles-euroks", "Eurok's volunteer"),
 | 
			
		||||
                    ("crous", "CROUS member"),
 | 
			
		||||
                    ("cursus-alternant", "Alternating cursus"),
 | 
			
		||||
                    ("cursus-alternant-reduction", "Alternating cursus (-20%)"),
 | 
			
		||||
                    ("cursus-branche", "Branch cursus"),
 | 
			
		||||
                    ("cursus-branche-reduction", "Branch cursus (-20%)"),
 | 
			
		||||
                    ("cursus-tronc-commun", "Common core cursus"),
 | 
			
		||||
                    ("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
 | 
			
		||||
                    ("deux-mois-essai", "Two months for free"),
 | 
			
		||||
                    ("deux-semestres", "Two semesters"),
 | 
			
		||||
                    ("deux-semestres-reduction", "Two semesters (-20%)"),
 | 
			
		||||
                    ("membre-honoraire", "Honorary member"),
 | 
			
		||||
                    ("membre-staff-ga", "GA staff member"),
 | 
			
		||||
                    ("reseau-ut", "UT network member"),
 | 
			
		||||
                    ("sbarro/esta", "Sbarro/ESTA member"),
 | 
			
		||||
                    ("six-semaines-essai", "Six weeks for free"),
 | 
			
		||||
                    ("un-jour", "One day"),
 | 
			
		||||
                    ("un-mois-essai", "One month for free"),
 | 
			
		||||
                    ("un-semestre", "One semester"),
 | 
			
		||||
                    ("un-semestre-reduction", "One semester (-20%)"),
 | 
			
		||||
                    ("un-semestre-welcome", "One semester Welcome Week"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                verbose_name="subscription type",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										47
									
								
								subscription/migrations/0014_auto_20201207_2323.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								subscription/migrations/0014_auto_20201207_2323.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
# Generated by Django 2.2.17 on 2020-12-07 22:23
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("subscription", "0013_auto_20200828_2117"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="subscription",
 | 
			
		||||
            name="subscription_type",
 | 
			
		||||
            field=models.CharField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("amicale/doceo", "Amicale/DOCEO member"),
 | 
			
		||||
                    ("assidu", "Assidu member"),
 | 
			
		||||
                    ("benevoles-euroks", "Eurok's volunteer"),
 | 
			
		||||
                    ("crous", "CROUS member"),
 | 
			
		||||
                    ("cursus-alternant", "Alternating cursus"),
 | 
			
		||||
                    ("cursus-alternant-reduction", "Alternating cursus (-20%)"),
 | 
			
		||||
                    ("cursus-branche", "Branch cursus"),
 | 
			
		||||
                    ("cursus-branche-reduction", "Branch cursus (-20%)"),
 | 
			
		||||
                    ("cursus-tronc-commun", "Common core cursus"),
 | 
			
		||||
                    ("cursus-tronc-commun-reduction", "Common core cursus (-20%)"),
 | 
			
		||||
                    ("deux-mois-essai", "Two months for free"),
 | 
			
		||||
                    ("deux-semestres", "Two semesters"),
 | 
			
		||||
                    ("deux-semestres-reduction", "Two semesters (-20%)"),
 | 
			
		||||
                    ("membre-honoraire", "Honorary member"),
 | 
			
		||||
                    ("membre-staff-ga", "GA staff member"),
 | 
			
		||||
                    ("reseau-ut", "UT network member"),
 | 
			
		||||
                    ("sbarro/esta", "Sbarro/ESTA member"),
 | 
			
		||||
                    ("six-semaines-essai", "Six weeks for free"),
 | 
			
		||||
                    ("un-an-offert-CA", "One year for free(CA offer)"),
 | 
			
		||||
                    ("un-jour", "One day"),
 | 
			
		||||
                    ("un-mois-essai", "One month for free"),
 | 
			
		||||
                    ("un-semestre", "One semester"),
 | 
			
		||||
                    ("un-semestre-reduction", "One semester (-20%)"),
 | 
			
		||||
                    ("un-semestre-welcome", "One semester Welcome Week"),
 | 
			
		||||
                ],
 | 
			
		||||
                max_length=255,
 | 
			
		||||
                verbose_name="subscription type",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -114,6 +114,18 @@ class SubscriptionIntegrationTest(TestCase):
 | 
			
		||||
        call_command("populate")
 | 
			
		||||
        self.user = User.objects.filter(username="public").first()
 | 
			
		||||
 | 
			
		||||
    def test_duration_one_month(self):
 | 
			
		||||
 | 
			
		||||
        s = Subscription(
 | 
			
		||||
            member=User.objects.filter(pk=self.user.pk).first(),
 | 
			
		||||
            subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[3],
 | 
			
		||||
            payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
 | 
			
		||||
        )
 | 
			
		||||
        s.subscription_start = date(2017, 8, 29)
 | 
			
		||||
        s.subscription_end = s.compute_end(duration=0.166, start=s.subscription_start)
 | 
			
		||||
        s.save()
 | 
			
		||||
        self.assertTrue(s.subscription_end == date(2017, 9, 29))
 | 
			
		||||
 | 
			
		||||
    def test_duration_two_months(self):
 | 
			
		||||
 | 
			
		||||
        s = Subscription(
 | 
			
		||||
@@ -122,11 +134,11 @@ class SubscriptionIntegrationTest(TestCase):
 | 
			
		||||
            payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0],
 | 
			
		||||
        )
 | 
			
		||||
        s.subscription_start = date(2017, 8, 29)
 | 
			
		||||
        s.subscription_end = s.compute_end(duration=0.33, start=s.subscription_start)
 | 
			
		||||
        s.subscription_end = s.compute_end(duration=0.333, start=s.subscription_start)
 | 
			
		||||
        s.save()
 | 
			
		||||
        self.assertTrue(s.subscription_end == date(2017, 10, 29))
 | 
			
		||||
 | 
			
		||||
    def test_duration_two_months(self):
 | 
			
		||||
    def test_duration_one_day(self):
 | 
			
		||||
 | 
			
		||||
        s = Subscription(
 | 
			
		||||
            member=User.objects.filter(pk=self.user.pk).first(),
 | 
			
		||||
 
 | 
			
		||||
@@ -36,22 +36,17 @@ from subscription.models import Subscription
 | 
			
		||||
from core.views.forms import SelectDateTime
 | 
			
		||||
from core.models import User
 | 
			
		||||
from core.views.forms import SelectDate
 | 
			
		||||
from core.views.forms import TzAwareDateTimeField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SelectionDateForm(forms.Form):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super(SelectionDateForm, self).__init__(*args, **kwargs)
 | 
			
		||||
        self.fields["start_date"] = forms.DateTimeField(
 | 
			
		||||
            input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
            label=_("Start date"),
 | 
			
		||||
            widget=SelectDateTime,
 | 
			
		||||
            required=True,
 | 
			
		||||
        self.fields["start_date"] = TzAwareDateTimeField(
 | 
			
		||||
            label=_("Start date"), required=True
 | 
			
		||||
        )
 | 
			
		||||
        self.fields["end_date"] = forms.DateTimeField(
 | 
			
		||||
            input_formats=["%Y-%m-%d %H:%M:%S"],
 | 
			
		||||
            label=_("End date"),
 | 
			
		||||
            widget=SelectDateTime,
 | 
			
		||||
            required=True,
 | 
			
		||||
        self.fields["end_date"] = TzAwareDateTimeField(
 | 
			
		||||
            label=_("End date"), required=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>{{ u.user.get_display_name() }}</div>
 | 
			
		||||
            <div><a href="{{ url('trombi:delete_user', user_id=u.id) }}">{% trans %}Delete{% endtrans %}</a></div>
 | 
			
		||||
            <div><a href="{{ url('trombi:create_membership', user_id=u.id) }}">{% trans %}Add club membership{% endtrans %}</a></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
# -*- coding:utf-8 -*
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2017
 | 
			
		||||
# Copyright 2017,2020
 | 
			
		||||
# - Skia <skia@libskia.so>
 | 
			
		||||
# - Sli <antoine@bartuccio.fr>
 | 
			
		||||
#
 | 
			
		||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
 | 
			
		||||
# http://ae.utbm.fr.
 | 
			
		||||
@@ -81,4 +82,9 @@ urlpatterns = [
 | 
			
		||||
        UserTrombiDeleteMembershipView.as_view(),
 | 
			
		||||
        name="delete_membership",
 | 
			
		||||
    ),
 | 
			
		||||
    re_path(
 | 
			
		||||
        r"^membership/(?P<user_id>[0-9]+)/create$",
 | 
			
		||||
        UserTrombiAddMembershipView.as_view(),
 | 
			
		||||
        name="create_membership",
 | 
			
		||||
    ),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
# -*- coding:utf-8 -*
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2017
 | 
			
		||||
# Copyright 2017,2020
 | 
			
		||||
# - Skia <skia@libskia.so>
 | 
			
		||||
# - Sli <antoine.bartuccio@gmail.com>
 | 
			
		||||
#
 | 
			
		||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
 | 
			
		||||
# http://ae.utbm.fr.
 | 
			
		||||
@@ -31,6 +32,7 @@ from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.forms.models import modelform_factory
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
 | 
			
		||||
from ajax_select.fields import AutoCompleteSelectField
 | 
			
		||||
 | 
			
		||||
@@ -410,6 +412,35 @@ class UserTrombiDeleteMembershipView(TrombiTabsMixin, CanEditMixin, DeleteView):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Used by admins when someone does not have every club in his list
 | 
			
		||||
class UserTrombiAddMembershipView(TrombiTabsMixin, CreateView):
 | 
			
		||||
    model = TrombiClubMembership
 | 
			
		||||
    template_name = "core/edit.jinja"
 | 
			
		||||
    fields = ["club", "role", "start", "end"]
 | 
			
		||||
    pk_url_kwarg = "user_id"
 | 
			
		||||
    current_tab = "profile"
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *arg, **kwargs):
 | 
			
		||||
        self.trombi_user = get_object_or_404(TrombiUser, pk=kwargs["user_id"])
 | 
			
		||||
        if not self.trombi_user.trombi.is_owned_by(request.user):
 | 
			
		||||
            raise PermissionDenied()
 | 
			
		||||
 | 
			
		||||
        return super(UserTrombiAddMembershipView, self).dispatch(
 | 
			
		||||
            request, *arg, **kwargs
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        membership = form.save(commit=False)
 | 
			
		||||
        membership.user = self.trombi_user
 | 
			
		||||
        membership.save()
 | 
			
		||||
        return HttpResponseRedirect(self.get_success_url())
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse(
 | 
			
		||||
            "trombi:detail", kwargs={"trombi_id": self.trombi_user.trombi.id}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserTrombiEditMembershipView(CanEditMixin, TrombiTabsMixin, UpdateView):
 | 
			
		||||
    model = TrombiClubMembership
 | 
			
		||||
    pk_url_kwarg = "membership_id"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user