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