Mise à jour de février (#581)

Co-authored-by: Thomas Girod <thgirod@hotmail.com>
Co-authored-by: Julien Constant <julienconstant190@gmail.com>
Co-authored-by: Skia <skia@hya.sk>
This commit is contained in:
Théo DURR 2023-03-09 13:39:33 +01:00 committed by GitHub
parent b7f20fed6c
commit dd3ad42eb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1432 additions and 1072 deletions

View File

@ -22,133 +22,127 @@
# #
# #
from django.urls import re_path from django.urls import path
from accounting.views import * from accounting.views import *
urlpatterns = [ urlpatterns = [
# Accounting types # Accounting types
re_path( path(
r"^simple_type$", "simple_type/",
SimplifiedAccountingTypeListView.as_view(), SimplifiedAccountingTypeListView.as_view(),
name="simple_type_list", name="simple_type_list",
), ),
re_path( path(
r"^simple_type/create$", "simple_type/create/",
SimplifiedAccountingTypeCreateView.as_view(), SimplifiedAccountingTypeCreateView.as_view(),
name="simple_type_new", name="simple_type_new",
), ),
re_path( path(
r"^simple_type/(?P<type_id>[0-9]+)/edit$", "simple_type/<int:type_id>/edit/",
SimplifiedAccountingTypeEditView.as_view(), SimplifiedAccountingTypeEditView.as_view(),
name="simple_type_edit", name="simple_type_edit",
), ),
# Accounting types # Accounting types
re_path(r"^type$", AccountingTypeListView.as_view(), name="type_list"), path("type/", AccountingTypeListView.as_view(), name="type_list"),
re_path(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"), path("type/create/", AccountingTypeCreateView.as_view(), name="type_new"),
re_path( path(
r"^type/(?P<type_id>[0-9]+)/edit$", "type/<int:type_id>/edit/",
AccountingTypeEditView.as_view(), AccountingTypeEditView.as_view(),
name="type_edit", name="type_edit",
), ),
# Bank accounts # Bank accounts
re_path(r"^$", BankAccountListView.as_view(), name="bank_list"), path("", BankAccountListView.as_view(), name="bank_list"),
re_path(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"), path("bank/create", BankAccountCreateView.as_view(), name="bank_new"),
re_path( path(
r"^bank/(?P<b_account_id>[0-9]+)$", "bank/<int:b_account_id>/",
BankAccountDetailView.as_view(), BankAccountDetailView.as_view(),
name="bank_details", name="bank_details",
), ),
re_path( path(
r"^bank/(?P<b_account_id>[0-9]+)/edit$", "bank/<int:b_account_id>/edit/",
BankAccountEditView.as_view(), BankAccountEditView.as_view(),
name="bank_edit", name="bank_edit",
), ),
re_path( path(
r"^bank/(?P<b_account_id>[0-9]+)/delete$", "bank/<int:b_account_id>/delete/",
BankAccountDeleteView.as_view(), BankAccountDeleteView.as_view(),
name="bank_delete", name="bank_delete",
), ),
# Club accounts # Club accounts
re_path(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"), path("club/create/", ClubAccountCreateView.as_view(), name="club_new"),
re_path( path(
r"^club/(?P<c_account_id>[0-9]+)$", "club/<int:c_account_id>/",
ClubAccountDetailView.as_view(), ClubAccountDetailView.as_view(),
name="club_details", name="club_details",
), ),
re_path( path(
r"^club/(?P<c_account_id>[0-9]+)/edit$", "club/<int:c_account_id>/edit/",
ClubAccountEditView.as_view(), ClubAccountEditView.as_view(),
name="club_edit", name="club_edit",
), ),
re_path( path(
r"^club/(?P<c_account_id>[0-9]+)/delete$", "club/<int:c_account_id>/delete/",
ClubAccountDeleteView.as_view(), ClubAccountDeleteView.as_view(),
name="club_delete", name="club_delete",
), ),
# Journals # Journals
re_path(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"), path("journal/create/", JournalCreateView.as_view(), name="journal_new"),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)$", "journal/<int:j_id>/",
JournalDetailView.as_view(), JournalDetailView.as_view(),
name="journal_details", name="journal_details",
), ),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)/edit$", "journal/<int:j_id>/edit/",
JournalEditView.as_view(), JournalEditView.as_view(),
name="journal_edit", name="journal_edit",
), ),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)/delete$", "journal/<int:j_id>/delete/",
JournalDeleteView.as_view(), JournalDeleteView.as_view(),
name="journal_delete", name="journal_delete",
), ),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)/statement/nature$", "journal/<int:j_id>/statement/nature/",
JournalNatureStatementView.as_view(), JournalNatureStatementView.as_view(),
name="journal_nature_statement", name="journal_nature_statement",
), ),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)/statement/person$", "journal/<int:j_id>/statement/person/",
JournalPersonStatementView.as_view(), JournalPersonStatementView.as_view(),
name="journal_person_statement", name="journal_person_statement",
), ),
re_path( path(
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$", "journal/<int:j_id>/statement/accounting/",
JournalAccountingStatementView.as_view(), JournalAccountingStatementView.as_view(),
name="journal_accounting_statement", name="journal_accounting_statement",
), ),
# Operations # Operations
re_path( path(
r"^operation/create/(?P<j_id>[0-9]+)$", "operation/create/<int:j_id>/",
OperationCreateView.as_view(), OperationCreateView.as_view(),
name="op_new", name="op_new",
), ),
re_path( path("operation/<int:op_id>/", OperationEditView.as_view(), name="op_edit"),
r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit" path("operation/<int:op_id>/pdf/", OperationPDFView.as_view(), name="op_pdf"),
),
re_path(
r"^operation/(?P<op_id>[0-9]+)/pdf$", OperationPDFView.as_view(), name="op_pdf"
),
# Companies # Companies
re_path(r"^company/list$", CompanyListView.as_view(), name="co_list"), path("company/list/", CompanyListView.as_view(), name="co_list"),
re_path(r"^company/create$", CompanyCreateView.as_view(), name="co_new"), path("company/create/", CompanyCreateView.as_view(), name="co_new"),
re_path(r"^company/(?P<co_id>[0-9]+)$", CompanyEditView.as_view(), name="co_edit"), path("company/<int:co_id>/", CompanyEditView.as_view(), name="co_edit"),
# Labels # Labels
re_path(r"^label/new$", LabelCreateView.as_view(), name="label_new"), path("label/new/", LabelCreateView.as_view(), name="label_new"),
re_path( path(
r"^label/(?P<clubaccount_id>[0-9]+)$", "label/<int:clubaccount_id>/",
LabelListView.as_view(), LabelListView.as_view(),
name="label_list", name="label_list",
), ),
re_path( path("label/<int:label_id>/edit/", LabelEditView.as_view(), name="label_edit"),
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit" path(
), "label/<int:label_id>/delete/",
re_path(
r"^label/(?P<label_id>[0-9]+)/delete$",
LabelDeleteView.as_view(), LabelDeleteView.as_view(),
name="label_delete", name="label_delete",
), ),
# User account # User account
re_path(r"^refound/account$", RefoundAccountView.as_view(), name="refound_account"), path("refound/account/", RefoundAccountView.as_view(), name="refound_account"),
] ]

View File

@ -23,94 +23,84 @@
# #
# #
from django.urls import re_path from django.urls import path
from club.views import * from club.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^$", ClubListView.as_view(), name="club_list"), path("", ClubListView.as_view(), name="club_list"),
re_path(r"^new$", ClubCreateView.as_view(), name="club_new"), path("new/", ClubCreateView.as_view(), name="club_new"),
re_path(r"^stats$", ClubStatView.as_view(), name="club_stats"), path("stats/", ClubStatView.as_view(), name="club_stats"),
re_path(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"), path("<int:club_id>/", ClubView.as_view(), name="club_view"),
re_path( path(
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$", "<int:club_id>/rev/<int:rev_id>/",
ClubRevView.as_view(), ClubRevView.as_view(),
name="club_view_rev", name="club_view_rev",
), ),
re_path( path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist" path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
), path(
re_path(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"), "<int:club_id>/edit/page/",
re_path(
r"^(?P<club_id>[0-9]+)/edit/page$",
ClubPageEditView.as_view(), ClubPageEditView.as_view(),
name="club_edit_page", name="club_edit_page",
), ),
re_path( path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members" path(
), "<int:club_id>/elderlies/",
re_path(
r"^(?P<club_id>[0-9]+)/elderlies$",
ClubOldMembersView.as_view(), ClubOldMembersView.as_view(),
name="club_old_members", name="club_old_members",
), ),
re_path( path(
r"^(?P<club_id>[0-9]+)/sellings$", "<int:club_id>/sellings/",
ClubSellingView.as_view(), ClubSellingView.as_view(),
name="club_sellings", name="club_sellings",
), ),
re_path( path(
r"^(?P<club_id>[0-9]+)/sellings/csv$", "<int:club_id>/sellings/csv/",
ClubSellingCSVView.as_view(), ClubSellingCSVView.as_view(),
name="sellings_csv", name="sellings_csv",
), ),
re_path( path("<int:club_id>/prop/", ClubEditPropView.as_view(), name="club_prop"),
r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop" path("<int:club_id>/tools/", ClubToolsView.as_view(), name="tools"),
), path("<int:club_id>/mailing/", ClubMailingView.as_view(), name="mailing"),
re_path(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"), path(
re_path( "<int:mailing_id>/mailing/generate/",
r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"
),
re_path(
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
MailingAutoGenerationView.as_view(), MailingAutoGenerationView.as_view(),
name="mailing_generate", name="mailing_generate",
), ),
re_path( path(
r"^(?P<mailing_id>[0-9]+)/mailing/delete$", "<int:mailing_id>/mailing/delete/",
MailingDeleteView.as_view(), MailingDeleteView.as_view(),
name="mailing_delete", name="mailing_delete",
), ),
re_path( path(
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$", "<int:mailing_subscription_id>/mailing/delete/subscription/",
MailingSubscriptionDeleteView.as_view(), MailingSubscriptionDeleteView.as_view(),
name="mailing_subscription_delete", name="mailing_subscription_delete",
), ),
re_path( path(
r"^membership/(?P<membership_id>[0-9]+)/set_old$", "membership/<int:membership_id>/set_old/",
MembershipSetOldView.as_view(), MembershipSetOldView.as_view(),
name="membership_set_old", name="membership_set_old",
), ),
re_path( path(
r"^membership/(?P<membership_id>[0-9]+)/delete$", "membership/<int:membership_id>/delete/",
MembershipDeleteView.as_view(), MembershipDeleteView.as_view(),
name="membership_delete", name="membership_delete",
), ),
re_path( path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list" path(
), "<int:club_id>/poster/create/",
re_path(
r"^(?P<club_id>[0-9]+)/poster/create$",
PosterCreateView.as_view(), PosterCreateView.as_view(),
name="poster_create", name="poster_create",
), ),
re_path( path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$", "<int:club_id>/poster/<int:poster_id>/edit/",
PosterEditView.as_view(), PosterEditView.as_view(),
name="poster_edit", name="poster_edit",
), ),
re_path( path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$", "<int:club_id>/poster/<int:poster_id>/delete/",
PosterDeleteView.as_view(), PosterDeleteView.as_view(),
name="poster_delete", name="poster_delete",
), ),

View File

@ -22,104 +22,98 @@
# #
# #
from django.urls import re_path from django.urls import path
from com.views import *
from club.views import MailingDeleteView from club.views import MailingDeleteView
from com.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"), path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"),
re_path(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"), path("sith/edit/info/", InfoMsgEditView.as_view(), name="info_edit"),
re_path( path(
r"^sith/edit/weekmail_destinations$", "sith/edit/weekmail_destinations/",
WeekmailDestinationEditView.as_view(), WeekmailDestinationEditView.as_view(),
name="weekmail_destinations", name="weekmail_destinations",
), ),
re_path(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"), path("weekmail/", WeekmailEditView.as_view(), name="weekmail"),
re_path( path("weekmail/preview/", WeekmailPreviewView.as_view(), name="weekmail_preview"),
r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview" path(
), "weekmail/new_article/",
re_path(
r"^weekmail/new_article$",
WeekmailArticleCreateView.as_view(), WeekmailArticleCreateView.as_view(),
name="weekmail_article", name="weekmail_article",
), ),
re_path( path(
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$", "weekmail/article/<int:article_id>/delete/",
WeekmailArticleDeleteView.as_view(), WeekmailArticleDeleteView.as_view(),
name="weekmail_article_delete", name="weekmail_article_delete",
), ),
re_path( path(
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$", "weekmail/article/<int:article_id>/edit/",
WeekmailArticleEditView.as_view(), WeekmailArticleEditView.as_view(),
name="weekmail_article_edit", name="weekmail_article_edit",
), ),
re_path(r"^news$", NewsListView.as_view(), name="news_list"), path("news/", NewsListView.as_view(), name="news_list"),
re_path(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"), path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
re_path(r"^news/create$", NewsCreateView.as_view(), name="news_new"), path("news/create/", NewsCreateView.as_view(), name="news_new"),
re_path( path(
r"^news/(?P<news_id>[0-9]+)/delete$", "news/<int:news_id>/delete/",
NewsDeleteView.as_view(), NewsDeleteView.as_view(),
name="news_delete", name="news_delete",
), ),
re_path( path(
r"^news/(?P<news_id>[0-9]+)/moderate$", "news/<int:news_id>/moderate/",
NewsModerateView.as_view(), NewsModerateView.as_view(),
name="news_moderate", name="news_moderate",
), ),
re_path( path("news/<int:news_id>/edit/", NewsEditView.as_view(), name="news_edit"),
r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit" path("news/<int:news_id>/", NewsDetailView.as_view(), name="news_detail"),
), path("mailings/", MailingListAdminView.as_view(), name="mailing_admin"),
re_path( path(
r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail" "mailings/<int:mailing_id>/moderate/",
),
re_path(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
re_path(
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
MailingModerateView.as_view(), MailingModerateView.as_view(),
name="mailing_moderate", name="mailing_moderate",
), ),
re_path( path(
r"^mailings/(?P<mailing_id>[0-9]+)/delete$", "mailings/<int:mailing_id>/delete/",
MailingDeleteView.as_view(redirect_page="com:mailing_admin"), MailingDeleteView.as_view(redirect_page="com:mailing_admin"),
name="mailing_delete", name="mailing_delete",
), ),
re_path(r"^poster$", PosterListView.as_view(), name="poster_list"), path("poster/", PosterListView.as_view(), name="poster_list"),
re_path(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"), path("poster/create/", PosterCreateView.as_view(), name="poster_create"),
re_path( path(
r"^poster/(?P<poster_id>[0-9]+)/edit$", "poster/<int:poster_id>/edit/",
PosterEditView.as_view(), PosterEditView.as_view(),
name="poster_edit", name="poster_edit",
), ),
re_path( path(
r"^poster/(?P<poster_id>[0-9]+)/delete$", "poster/<int:poster_id>/delete/",
PosterDeleteView.as_view(), PosterDeleteView.as_view(),
name="poster_delete", name="poster_delete",
), ),
re_path( path(
r"^poster/moderate$", "poster/moderate/",
PosterModerateListView.as_view(), PosterModerateListView.as_view(),
name="poster_moderate_list", name="poster_moderate_list",
), ),
re_path( path(
r"^poster/(?P<object_id>[0-9]+)/moderate$", "poster/<int:object_id>/moderate/",
PosterModerateView.as_view(), PosterModerateView.as_view(),
name="poster_moderate", name="poster_moderate",
), ),
re_path(r"^screen$", ScreenListView.as_view(), name="screen_list"), path("screen/", ScreenListView.as_view(), name="screen_list"),
re_path(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"), path("screen/create/", ScreenCreateView.as_view(), name="screen_create"),
re_path( path(
r"^screen/(?P<screen_id>[0-9]+)/slideshow$", "screen/<int:screen_id>/slideshow/",
ScreenSlideshowView.as_view(), ScreenSlideshowView.as_view(),
name="screen_slideshow", name="screen_slideshow",
), ),
re_path( path(
r"^screen/(?P<screen_id>[0-9]+)/edit$", "screen/<int:screen_id>/edit/",
ScreenEditView.as_view(), ScreenEditView.as_view(),
name="screen_edit", name="screen_edit",
), ),
re_path( path(
r"^screen/(?P<screen_id>[0-9]+)/delete$", "screen/<int:screen_id>/delete/",
ScreenDeleteView.as_view(), ScreenDeleteView.as_view(),
name="screen_delete", name="screen_delete",
), ),

35
core/converters.py Normal file
View File

@ -0,0 +1,35 @@
from core.models import Page
class FourDigitYearConverter:
regex = "[0-9]{4}"
def to_python(self, value):
return int(value)
def to_url(self, value):
return str(value).zfill(4)
class TwoDigitMonthConverter:
regex = "[0-9]{2}"
def to_python(self, value):
return int(value)
def to_url(self, value):
return str(value).zfill(2)
class BooleanStringConverter:
"""
Converter whose regex match either True or False
"""
regex = r"(True)|(False)"
def to_python(self, value):
return str(value) == "True"
def to_url(self, value):
return str(value)

File diff suppressed because one or more lines are too long

View File

@ -1263,7 +1263,7 @@ u,
text-decoration: underline; text-decoration: underline;
} }
#bar_ui { #bar-ui {
padding: 0.4em; padding: 0.4em;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -4,9 +4,6 @@
<div id="page"> <div id="page">
<h3>{% trans %}404, Not Found{% endtrans %}</h3> <h3>{% trans %}404, Not Found{% endtrans %}</h3>
<p class="alert alert-red">
{{ exception }}
</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -65,9 +65,15 @@
<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.annotate_has_barman(user).filter(type="BAR") %}
<li> <li>
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0px"> {# If the user is a barman, we redirect him directly to the barman page
else we redirect him to the activity page #}
{% if bar.has_annotated_barman %}
<a href="{{ url('counter:details', counter_id=bar.id) }}" style="padding: 0">
{% else %}
<a href="{{ url('counter:activity', counter_id=bar.id) }}" style="padding: 0">
{% endif %}
{% if bar.is_inactive(): %} {% if bar.is_inactive(): %}
<i class="fa fa-question" style="color: #f39c12"></i> <i class="fa fa-question" style="color: #f39c12"></i>
{% elif bar.is_open(): %} {% elif bar.is_open(): %}

View File

@ -290,8 +290,7 @@ class MarkdownTest(TestCase):
class PageHandlingTest(TestCase): class PageHandlingTest(TestCase):
def setUp(self): def setUp(self):
try: self.root_group = Group.objects.create(name="root")
Group.objects.create(name="root")
u = User( u = User(
username="root", username="root",
last_name="", last_name="",
@ -304,19 +303,27 @@ class PageHandlingTest(TestCase):
u.set_password("plop") u.set_password("plop")
u.save() u.save()
self.client.login(username="root", password="plop") self.client.login(username="root", password="plop")
except Exception as e:
print(e)
def test_create_page_ok(self): def test_create_page_ok(self):
""" """
Should create a page correctly Should create a page correctly
""" """
self.client.post(
reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": 1} response = self.client.post(
reverse("core:page_new"),
{"parent": "", "name": "guy", "owner_group": self.root_group.id},
) )
self.assertRedirects(
response, reverse("core:page", kwargs={"page_name": "guy"})
)
self.assertTrue(Page.objects.filter(name="guy").exists())
response = self.client.get(reverse("core:page", kwargs={"page_name": "guy"})) response = self.client.get(reverse("core:page", kwargs={"page_name": "guy"}))
self.assertTrue(response.status_code == 200) self.assertEqual(response.status_code, 200)
self.assertTrue('<a href="/page/guy/hist">' in str(response.content)) html = response.content.decode()
self.assertIn('<a href="/page/guy/hist/">', html)
self.assertIn('<a href="/page/guy/edit/">', html)
self.assertIn('<a href="/page/guy/prop/">', html)
def test_create_child_page_ok(self): def test_create_child_page_ok(self):
""" """
@ -339,29 +346,25 @@ class PageHandlingTest(TestCase):
""" """
Should display a page correctly Should display a page correctly
""" """
parent = Page(name="guy", owner_group=Group.objects.filter(id=1).first()) parent = Page(name="guy", owner_group=self.root_group)
parent.save(force_lock=True) parent.save(force_lock=True)
page = Page( page = Page(name="bibou", owner_group=self.root_group, parent=parent)
name="bibou", owner_group=Group.objects.filter(id=1).first(), parent=parent
)
page.save(force_lock=True) page.save(force_lock=True)
response = self.client.get( response = self.client.get(
reverse("core:page", kwargs={"page_name": "guy/bibou"}) reverse("core:page", kwargs={"page_name": "guy/bibou"})
) )
self.assertTrue(response.status_code == 200) self.assertTrue(response.status_code == 200)
self.assertTrue( html = response.content.decode()
'<a href="/page/guy/bibou/edit">\\xc3\\x89diter</a>' self.assertIn('<a href="/page/guy/bibou/edit/">', html)
in str(response.content)
)
def test_access_page_not_found(self): def test_access_page_not_found(self):
""" """
Should not display a page correctly Should not display a page correctly
""" """
response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"})) response = self.client.get(reverse("core:page", kwargs={"page_name": "swagg"}))
response = self.client.get("/page/swagg/")
self.assertTrue(response.status_code == 200) self.assertTrue(response.status_code == 200)
self.assertTrue('<a href="/page/create?page=swagg">' in str(response.content)) html = response.content.decode()
self.assertIn('<a href="/page/create/?page=swagg">', html)
def test_create_page_markdown_safe(self): def test_create_page_markdown_safe(self):
""" """

View File

@ -23,40 +23,46 @@
# #
# #
from django.urls import re_path, path from django.urls import path, re_path, register_converter
from core.views import * from core.views import *
from core.converters import (
FourDigitYearConverter,
TwoDigitMonthConverter,
BooleanStringConverter,
)
register_converter(FourDigitYearConverter, "yyyy")
register_converter(TwoDigitMonthConverter, "mm")
register_converter(BooleanStringConverter, "bool")
urlpatterns = [ urlpatterns = [
re_path(r"^$", index, name="index"), path("", index, name="index"),
re_path(r"^to_markdown$", ToMarkdownView.as_view(), name="to_markdown"), path("to_markdown/", ToMarkdownView.as_view(), name="to_markdown"),
re_path(r"^notifications$", NotificationList.as_view(), name="notification_list"), path("notifications/", NotificationList.as_view(), name="notification_list"),
re_path(r"^notification/(?P<notif_id>[0-9]+)$", notification, name="notification"), path("notification/<int:notif_id>/", notification, name="notification"),
# Search # Search
re_path(r"^search/$", search_view, name="search"), path("search/", search_view, name="search"),
re_path(r"^search_json/$", search_json, name="search_json"), path("search_json/", search_json, name="search_json"),
re_path(r"^search_user/$", search_user_json, name="search_user"), path("search_user/", search_user_json, name="search_user"),
# Login and co # Login and co
re_path(r"^login/$", SithLoginView.as_view(), name="login"), path("login/", SithLoginView.as_view(), name="login"),
re_path(r"^logout/$", logout, name="logout"), path("logout/", logout, name="logout"),
re_path( path("password_change/", SithPasswordChangeView.as_view(), name="password_change"),
r"^password_change/$", SithPasswordChangeView.as_view(), name="password_change" path(
), "password_change/<int:user_id>/",
re_path(
r"^password_change/(?P<user_id>[0-9]+)$",
password_root_change, password_root_change,
name="password_root_change", name="password_root_change",
), ),
re_path( path(
r"^password_change/done$", "password_change/done/",
SithPasswordChangeDoneView.as_view(), SithPasswordChangeDoneView.as_view(),
name="password_change_done", name="password_change_done",
), ),
re_path( path("password_reset/", SithPasswordResetView.as_view(), name="password_reset"),
r"^password_reset/$", SithPasswordResetView.as_view(), name="password_reset" path(
), "password_reset/done/",
re_path(
r"^password_reset/done$",
SithPasswordResetDoneView.as_view(), SithPasswordResetDoneView.as_view(),
name="password_reset_done", name="password_reset_done",
), ),
@ -65,110 +71,103 @@ urlpatterns = [
SithPasswordResetConfirmView.as_view(), SithPasswordResetConfirmView.as_view(),
name="password_reset_confirm", name="password_reset_confirm",
), ),
re_path( path(
r"^reset/done/$", "reset/done/",
SithPasswordResetCompleteView.as_view(), SithPasswordResetCompleteView.as_view(),
name="password_reset_complete", name="password_reset_complete",
), ),
re_path(r"^register$", register, name="register"), path("register/", register, name="register"),
# Group handling # Group handling
re_path(r"^group/$", GroupListView.as_view(), name="group_list"), path("group/", GroupListView.as_view(), name="group_list"),
re_path(r"^group/new/$", GroupCreateView.as_view(), name="group_new"), path("group/new/", GroupCreateView.as_view(), name="group_new"),
re_path( path("group/<int:group_id>/", GroupEditView.as_view(), name="group_edit"),
r"^group/(?P<group_id>[0-9]+)/$", GroupEditView.as_view(), name="group_edit" path(
), "group/<int:group_id>/delete/",
re_path(
r"^group/(?P<group_id>[0-9]+)/delete$",
GroupDeleteView.as_view(), GroupDeleteView.as_view(),
name="group_delete", name="group_delete",
), ),
re_path( path(
r"^group/(?P<group_id>[0-9]+)/detail$", "group/<int:group_id>/detail/",
GroupTemplateView.as_view(), GroupTemplateView.as_view(),
name="group_detail", name="group_detail",
), ),
# User views # User views
re_path(r"^user/$", UserListView.as_view(), name="user_list"), path("user/", UserListView.as_view(), name="user_list"),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/mini$", "user/<int:user_id>/mini/",
UserMiniView.as_view(), UserMiniView.as_view(),
name="user_profile_mini", name="user_profile_mini",
), ),
re_path(r"^user/(?P<user_id>[0-9]+)/$", UserView.as_view(), name="user_profile"), path("user/<int:user_id>/", UserView.as_view(), name="user_profile"),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/pictures$", "user/<int:user_id>/pictures/",
UserPicturesView.as_view(), UserPicturesView.as_view(),
name="user_pictures", name="user_pictures",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/godfathers$", "user/<int:user_id>/godfathers/",
UserGodfathersView.as_view(), UserGodfathersView.as_view(),
name="user_godfathers", name="user_godfathers",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/godfathers/tree$", "user/<int:user_id>/godfathers/tree/",
UserGodfathersTreeView.as_view(), UserGodfathersTreeView.as_view(),
name="user_godfathers_tree", name="user_godfathers_tree",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/godfathers/tree/pict$", "user/<int:user_id>/godfathers/tree/pict/",
UserGodfathersTreePictureView.as_view(), UserGodfathersTreePictureView.as_view(),
name="user_godfathers_tree_pict", name="user_godfathers_tree_pict",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/godfathers/(?P<godfather_id>[0-9]+)/(?P<is_father>(True)|(False))/delete$", "user/<int:user_id>/godfathers/<int:godfather_id>/<bool:is_father>/delete/",
DeleteUserGodfathers, delete_user_godfather,
name="user_godfathers_delete", name="user_godfathers_delete",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/edit$", "user/<int:user_id>/edit/",
UserUpdateProfileView.as_view(), UserUpdateProfileView.as_view(),
name="user_edit", name="user_edit",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/profile_upload$", "user/<int:user_id>/profile_upload/",
UserUploadProfilePictView.as_view(), UserUploadProfilePictView.as_view(),
name="user_profile_upload", name="user_profile_upload",
), ),
re_path( path("user/<int:user_id>/clubs/", UserClubView.as_view(), name="user_clubs"),
r"^user/(?P<user_id>[0-9]+)/clubs$", UserClubView.as_view(), name="user_clubs" path(
), "user/<int:user_id>/prefs/",
re_path(
r"^user/(?P<user_id>[0-9]+)/prefs$",
UserPreferencesView.as_view(), UserPreferencesView.as_view(),
name="user_prefs", name="user_prefs",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/groups$", "user/<int:user_id>/groups/",
UserUpdateGroupView.as_view(), UserUpdateGroupView.as_view(),
name="user_groups", name="user_groups",
), ),
re_path(r"^user/tools/$", UserToolsView.as_view(), name="user_tools"), path("user/tools/", UserToolsView.as_view(), name="user_tools"),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/account$", "user/<int:user_id>/account/",
UserAccountView.as_view(), UserAccountView.as_view(),
name="user_account", name="user_account",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/account/(?P<year>[0-9]+)/(?P<month>[0-9]+)$", "user/<int:user_id>/account/<yyyy:year>/<mm:month>/",
UserAccountDetailView.as_view(), UserAccountDetailView.as_view(),
name="user_account_detail", name="user_account_detail",
), ),
re_path( path("user/<int:user_id>/stats/", UserStatsView.as_view(), name="user_stats"),
r"^user/(?P<user_id>[0-9]+)/stats$", UserStatsView.as_view(), name="user_stats" path(
), "user/<int:user_id>/gift/create/",
re_path(
r"^user/(?P<user_id>[0-9]+)/gift/create$",
GiftCreateView.as_view(), GiftCreateView.as_view(),
name="user_gift_create", name="user_gift_create",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/gift/delete/(?P<gift_id>[0-9]+)/$", "user/<int:user_id>/gift/delete/<int:gift_id>/",
GiftDeleteView.as_view(), GiftDeleteView.as_view(),
name="user_gift_delete", name="user_gift_delete",
), ),
# File views # File views
# re_path(r'^file/add/(?P<popup>popup)?$', FileCreateView.as_view(), name='file_new'),
re_path(r"^file/(?P<popup>popup)?$", FileListView.as_view(), name="file_list"), re_path(r"^file/(?P<popup>popup)?$", FileListView.as_view(), name="file_list"),
re_path( re_path(
r"^file/(?P<file_id>[0-9]+)/(?P<popup>popup)?$", r"^file/(?P<file_id>[0-9]+)/(?P<popup>popup)?$",
@ -190,43 +189,43 @@ urlpatterns = [
FileDeleteView.as_view(), FileDeleteView.as_view(),
name="file_delete", name="file_delete",
), ),
re_path(r"^file/moderation$", FileModerationView.as_view(), name="file_moderation"), path("file/moderation/", FileModerationView.as_view(), name="file_moderation"),
re_path( path(
r"^file/(?P<file_id>[0-9]+)/moderate$", "file/<int:file_id>/moderate/",
FileModerateView.as_view(), FileModerateView.as_view(),
name="file_moderate", name="file_moderate",
), ),
re_path(r"^file/(?P<file_id>[0-9]+)/download$", send_file, name="download"), path("file/<int:file_id>/download/", send_file, name="download"),
# Page views # Page views
re_path(r"^page/$", PageListView.as_view(), name="page_list"), path("page/", PageListView.as_view(), name="page_list"),
re_path(r"^page/create$", PageCreateView.as_view(), name="page_new"), path("page/create/", PageCreateView.as_view(), name="page_new"),
re_path( path(
r"^page/(?P<page_id>[0-9]*)/delete$", "page/<int:page_id>/delete/",
PageDeleteView.as_view(), PageDeleteView.as_view(),
name="page_delete", name="page_delete",
), ),
re_path( path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/edit$", "page/<path:page_name>/edit/",
PageEditView.as_view(), PageEditView.as_view(),
name="page_edit", name="page_edit",
), ),
re_path( path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/prop$", "page/<path:page_name>/prop/",
PagePropView.as_view(), PagePropView.as_view(),
name="page_prop", name="page_prop",
), ),
re_path( path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/hist$", "page/<path:page_name>/hist/",
PageHistView.as_view(), PageHistView.as_view(),
name="page_hist", name="page_hist",
), ),
re_path( path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/rev/(?P<rev>[0-9]+)/", "page/<path:page_name>/rev/<int:rev>/",
PageRevView.as_view(), PageRevView.as_view(),
name="page_rev", name="page_rev",
), ),
re_path( path(
r"^page/(?P<page_name>([/a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])+)/$", "page/<path:page_name>/",
PageView.as_view(), PageView.as_view(),
name="page", name="page",
), ),

View File

@ -207,9 +207,7 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Pictures"), "name": _("Pictures"),
}, },
] ]
if ( if settings.SITH_ENABLE_GALAXY and self.request.user.was_subscribed:
False and self.request.user.was_subscribed
): # TODO: display galaxy once it's ready
tab_list.append( tab_list.append(
{ {
"url": reverse("galaxy:user", kwargs={"user_id": user.id}), "url": reverse("galaxy:user", kwargs={"user_id": user.id}),
@ -330,16 +328,16 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
return kwargs return kwargs
def DeleteUserGodfathers(request, user_id, godfather_id, is_father): def delete_user_godfather(request, user_id, godfather_id, is_father):
user = User.objects.get(id=user_id) user_is_admin = request.user.is_root or request.user.is_board_member
if (user == request.user) or request.user.is_root or request.user.is_board_member: if user_id != request.user.id and not user_is_admin:
ud = get_object_or_404(User, id=godfather_id) raise PermissionDenied()
if is_father == "True": user = get_object_or_404(User, id=user_id)
user.godfathers.remove(ud) to_remove = get_object_or_404(User, id=godfather_id)
if is_father:
user.godfathers.remove(to_remove)
else: else:
user.godchildren.remove(ud) user.godchildren.remove(to_remove)
else:
raise PermissionDenied
return redirect("core:user_godfathers", user_id=user_id) return redirect("core:user_godfathers", user_id=user_id)

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.16 on 2022-12-15 16:09
import accounting.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("counter", "0019_billinginfo"),
]
operations = [
migrations.AlterField(
model_name="customer",
name="amount",
field=accounting.models.CurrencyField(
decimal_places=2, default=0, max_digits=12, verbose_name="amount"
),
),
]

View File

@ -21,8 +21,13 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from __future__ import annotations
from django.db.models import Sum, F
from typing import Tuple
from django.db import models from django.db import models
from django.db.models import OuterRef, Exists
from django.db.models.functions import Length from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -57,7 +62,7 @@ class Customer(models.Model):
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE) user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
account_id = models.CharField(_("account id"), max_length=10, unique=True) account_id = models.CharField(_("account id"), max_length=10, unique=True)
amount = CurrencyField(_("amount")) amount = CurrencyField(_("amount"), default=0)
recorded_products = models.IntegerField(_("recorded product"), default=0) recorded_products = models.IntegerField(_("recorded product"), default=0)
class Meta: class Meta:
@ -86,20 +91,32 @@ class Customer(models.Model):
about the relation between a User (not a Customer, about the relation between a User (not a Customer,
don't mix them) and a Product. don't mix them) and a Product.
""" """
return self.user.subscriptions.last() and ( subscription = self.user.subscriptions.order_by("subscription_end").last()
date.today() time_diff = date.today() - subscription.subscription_end
- self.user.subscriptions.order_by("subscription_end") return subscription is not None and time_diff < timedelta(days=90)
.last()
.subscription_end
) < timedelta(days=90)
@classmethod @classmethod
def new_for_user(cls, user: User): def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
""" """
Create a new Customer instance for the user given in parameter without saving it Work in pretty much the same way as the usual get_or_create method,
The account if is automatically generated and the amount set at 0 but with the default field replaced by some under the hood.
If the user has an account, return it as is.
Else create a new account with no money on it and a new unique account id
Example : ::
user = User.objects.get(pk=1)
account, created = Customer.get_or_create(user)
if created:
print(f"created a new account with id {account.id}")
else:
print(f"user has already an account, with {account.id} € on it"
""" """
# account_id are number with a letter appended if hasattr(user, "customer"):
return user.customer, False
# account_id are always a number with a letter appended
account_id = ( account_id = (
Customer.objects.order_by(Length("account_id"), "account_id") Customer.objects.order_by(Length("account_id"), "account_id")
.values("account_id") .values("account_id")
@ -107,14 +124,19 @@ class Customer(models.Model):
) )
if account_id is None: if account_id is None:
# legacy from the old site # legacy from the old site
return cls(user=user, account_id="1504a", amount=0) account = cls.objects.create(user=user, account_id="1504a")
account_id = account_id["account_id"] return account, True
num = int(account_id[:-1])
while Customer.objects.filter(account_id=account_id).exists():
num += 1
account_id = str(num) + random.choice(string.ascii_lowercase)
return cls(user=user, account_id=account_id, amount=0) account_id = account_id["account_id"]
account_num = int(account_id[:-1])
while Customer.objects.filter(account_id=account_id).exists():
# when entering the first iteration, we are using an already existing account id
# so the loop should always execute at least one time
account_num += 1
account_id = f"{account_num}{random.choice(string.ascii_lowercase)}"
account = cls.objects.create(user=user, account_id=account_id)
return account, True
def save(self, allow_negative=False, is_selling=False, *args, **kwargs): def save(self, allow_negative=False, is_selling=False, *args, **kwargs):
""" """
@ -127,11 +149,15 @@ class Customer(models.Model):
super(Customer, self).save(*args, **kwargs) super(Customer, self).save(*args, **kwargs)
def recompute_amount(self): def recompute_amount(self):
self.amount = 0 refillings = self.refillings.aggregate(sum=Sum(F("amount")))["sum"]
for r in self.refillings.all(): self.amount = refillings if refillings is not None else 0
self.amount += r.amount purchases = (
for s in self.buyings.filter(payment_method="SITH_ACCOUNT"): self.buyings.filter(payment_method="SITH_ACCOUNT")
self.amount -= s.quantity * s.unit_price .annotate(amount=F("quantity") * F("unit_price"))
.aggregate(sum=Sum(F("amount")))
)["sum"]
if purchases is not None:
self.amount -= purchases
self.save() self.save()
def get_absolute_url(self): def get_absolute_url(self):
@ -313,6 +339,32 @@ class Product(models.Model):
return "%s (%s)" % (self.name, self.code) return "%s (%s)" % (self.name, self.code)
class CounterQuerySet(models.QuerySet):
def annotate_has_barman(self, user: User) -> CounterQuerySet:
"""
Annotate the queryset with the `user_is_barman` field.
For each counter, this field has value True if the user
is a barman of this counter, else False.
:param user: the user we want to check if he is a barman
Example::
sli = User.objects.get(username="sli")
counters = (
Counter.objects
.annotate_has_barman(sli) # add the user_has_barman boolean field
.filter(has_annotated_barman=True) # keep only counters where this user is barman
)
print("Sli est barman dans les comptoirs suivants :")
for counter in counters:
print(f"- {counter.name}")
"""
subquery = user.counters.filter(pk=OuterRef("pk"))
# noinspection PyTypeChecker
return self.annotate(has_annotated_barman=Exists(subquery))
class Counter(models.Model): class Counter(models.Model):
name = models.CharField(_("name"), max_length=30) name = models.CharField(_("name"), max_length=30)
club = models.ForeignKey( club = models.ForeignKey(
@ -337,6 +389,8 @@ class Counter(models.Model):
) )
token = models.CharField(_("token"), max_length=30, null=True, blank=True) token = models.CharField(_("token"), max_length=30, null=True, blank=True)
objects = CounterQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _("counter") verbose_name = _("counter")
@ -451,11 +505,11 @@ class Counter(models.Model):
Show if the counter authorize the refilling with physic money Show if the counter authorize the refilling with physic money
""" """
if ( if self.type != "BAR":
self.id in SITH_COUNTER_OFFICES return False
): # If the counter is the counters 'AE' or 'BdF', the refiling are authorized if self.id in SITH_COUNTER_OFFICES:
# If the counter is either 'AE' or 'BdF', refills are authorized
return True return True
is_ae_member = False is_ae_member = False
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"]) ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
for barman in self.get_barmen_list(): for barman in self.get_barmen_list():

View File

@ -0,0 +1,78 @@
document.addEventListener('alpine:init', () => {
Alpine.data('counter', () => ({
basket: basket,
errors: [],
sum_basket() {
if (!this.basket || Object.keys(this.basket).length === 0) {
return 0;
}
const total = Object.values(this.basket)
.reduce((acc, cur) => acc + cur["qty"] * cur["price"], 0);
return total / 100;
},
async handle_code(event) {
const code = $(event.target).find("#code_field").val().toUpperCase();
if(["FIN", "ANN"].includes(code)) {
$(event.target).submit();
} else {
await this.handle_action(event);
}
},
async handle_action(event) {
const payload = $(event.target).serialize();
let request = new Request(click_api_url, {
method: "POST",
body: payload,
headers: {
'Accept': 'application/json',
'X-CSRFToken': csrf_token,
}
})
const response = await fetch(request);
const json = await response.json();
this.basket = json["basket"]
this.errors = json["errors"]
$('form.code_form #code_field').val("").focus();
}
}))
})
$(function () {
/* Autocompletion in the code field */
const code_field = $("#code_field");
let quantity = "";
let search = "";
code_field.autocomplete({
select: function (event, ui) {
event.preventDefault();
code_field.val(quantity + ui.item.value);
},
focus: function (event, ui) {
event.preventDefault();
code_field.val(quantity + ui.item.value);
},
source: function (request, response) {
// by the dark magic of JS, parseInt("123abc") === 123
quantity = parseInt(request.term);
search = request.term.slice(quantity.toString().length)
let matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
response($.grep(products_autocomplete, function (value) {
value = value.tags;
return matcher.test(value);
}));
},
});
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: () => $(".focus").focus(),
});
$("#products").tabs();
code_field.focus();
});

View File

@ -2,19 +2,25 @@
{% from "core/macros.jinja" import user_mini_profile, user_subscription %} {% from "core/macros.jinja" import user_mini_profile, user_subscription %}
{% block title %} {% block title %}
{{ counter }} {{ counter }}
{% endblock %}
{% block additional_js %}
<script src="{{ static('counter/js/counter_click.js') }}" defer></script>
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
{% endblock %} {% endblock %}
{% block info_boxes %} {% block info_boxes %}
{% endblock %} {% endblock %}
{% block nav %} {% block nav %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h4 id="click_interface">{{ counter }}</h4> <h4 id="click_interface">{{ counter }}</h4>
<div id="bar_ui"> <div id="bar-ui" x-data="counter">
<noscript> <noscript>
<p class="important">Javascript is required for the counter UI.</p> <p class="important">Javascript is required for the counter UI.</p>
</noscript> </noscript>
@ -28,11 +34,11 @@
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="add_student_card"> <input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %} {% trans %}Add a student card{% endtrans %}
<input type="input" name="student_card_uid" /> <input type="text" name="student_card_uid"/>
{% if request.session['not_valid_student_card_uid'] %} {% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p> <p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %} {% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}" /> <input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form> </form>
<h6>{% trans %}Registered cards{% endtrans %}</h6> <h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if customer.student_cards.exists() %} {% if customer.student_cards.exists() %}
@ -49,72 +55,81 @@
<div id="click_form"> <div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5> <h5>{% trans %}Selling{% endtrans %}</h5>
<div> <div>
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %}
{% raw %} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
<div class="important"> <form method="post" action=""
<p v-for="error in errors"><strong>{{ error }}</strong></p> class="code_form" @submit.prevent="handle_code">
</div>
{% endraw %}
<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"/> <label for="code_field"></label>
<input type="submit" value="{% trans %}Go{% endtrans %}" /> <input type="text" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form> </form>
<template x-for="error in errors">
<div class="alert alert-red" x-text="error">
</div>
</template>
<p>{% trans %}Basket: {% endtrans %}</p> <p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul> <ul>
<li v-for="p_info,p_id in basket"> <template x-for="[id, item] in Object.entries(basket)" :key="id">
<div>
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action"> <form method="post" action="" class="inline del_product_form"
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token"> @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="del_product"> <input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" v-bind:value="p_id"> <input type="hidden" name="product_id" :value="id">
<button type="submit"> - </button> <input type="submit" value="-"/>
</form> </form>
{{ p_info["qty"] + p_info["bonus_qty"] }} <span x-text="item['qty'] + item['bonus_qty']"></span>
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action"> <form method="post" action="" class="inline add_product_form"
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token"> @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product"> <input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id"> <input type="hidden" name="product_id" :value="id">
<button type="submit"> + </button> <input type="submit" value="+">
</form> </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> <span x-text="products[id].name"></span> :
</li> <span x-text="(item['qty'] * item['price'] / 100)
.toLocaleString(undefined, { minimumFractionDigits: 2 })">
</span> €
<template x-if="item['bonus_qty'] > 0">P</template>
</div>
</template>
</ul> </ul>
<p> <p>
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong> <strong>Total: </strong>
<strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong>
<strong> €</strong>
</p> </p>
<div class="important"> <form method="post"
<p v-for="error in errors"><strong>{{ error }}</strong></p> action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
</div>
{% endraw %}
<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">
<input type="submit" value="{% trans %}Finish{% endtrans %}" /> <input type="submit" value="{% trans %}Finish{% endtrans %}"/>
</form> </form>
<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="cancel"> <input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}" /> <input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
</form> </form>
</div> </div>
{% if (counter.type == 'BAR' and barmens_can_refill) %} {% if (counter.type == 'BAR' and barmens_can_refill) %}
<h5>{% trans %}Refilling{% endtrans %}</h5> <h5>{% trans %}Refilling{% endtrans %}</h5>
<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 %}
{{ refill_form.as_p() }} {{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill"> <input type="hidden" name="action" value="refill">
<input type="submit" value="{% trans %}Go{% endtrans %}" /> <input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form> </form>
</div> </div>
{% endif %} {% endif %}
@ -130,95 +145,45 @@
<div id="cat_{{ category|slugify }}"> <div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5> <h5>{{ category }}</h5>
{% for p in categories[category] -%} {% for p in categories[category] -%}
{% set file = None %} <form method="post"
{% if p.icon %} action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
{% set file = p.icon.url %} class="form_button add_product_form" @submit.prevent="handle_action">
{% else %}
{% set file = static('core/img/na.gif') %}
{% endif %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="add_product"> <input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}"> <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> <button type="submit">
<strong>{{ p.name }}</strong>
{% if p.icon %}
<img src="{{ p.icon.url }}" alt="image de {{ p.name }}"/>
{% else %}
<img src="{{ static('core/img/na.gif') }}" alt="image de {{ p.name }}"/>
{% endif %}
<span>{{ p.price }} €<br>{{ p.code }}</span>
</button>
</form> </form>
{%- endfor %} {%- endfor %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script> <script>
<script> const csrf_token = "{{ csrf_token }}";
$( function() { const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
/* Vue.JS dynamic form */ const basket = {{ request.session["basket"]|tojson }};
const click_form_vue = Vue.createApp({ const products = {
data() { {%- for p in products -%}
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
{{ p.id }}: { {{ p.id }}: {
code: "{{ p.code }}", code: "{{ p.code }}",
name: "{{ p.name }}", name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}", price: {{ p.price }},
special_selling_price: "{{ p.special_selling_price }}",
}, },
{%- endfor %} {%- endfor -%}
}, };
basket: {{ request.session["basket"]|tojson }}, const products_autocomplete = [
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 }}",
@ -227,41 +192,5 @@ $( function() {
}, },
{%- endfor %} {%- endfor %}
]; ];
</script>
var quantity = "";
var search = "";
var pattern = /^(\d+x)?(.*)/i;
$( "#code_field" ).autocomplete({
select: function (event, ui) {
event.preventDefault();
$("#code_field").val(quantity + ui.item.value);
},
focus: function (event, ui) {
event.preventDefault();
$("#code_field").val(quantity + ui.item.value);
},
source: function( request, response ) {
var res = pattern.exec(request.term);
quantity = res[1] || "";
search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products_autocomplete, function( value ) {
value = value.tags;
return matcher.test( value );
}));
},
});
/* Accordion UI between basket and refills */
$("#click_form").accordion({
heightStyle: "content",
activate: function(event, ui){
$(".focus").focus();
}
});
$("#products").tabs();
$("#code_field").focus();
});
</script>
{% endblock %} {% endblock %}

View File

@ -23,6 +23,7 @@
# #
import json import json
import re import re
import string
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
@ -42,7 +43,7 @@ class CounterTest(TestCase):
self.foyer = Counter.objects.get(id=2) self.foyer = Counter.objects.get(id=2)
def test_full_click(self): def test_full_click(self):
response = self.client.post( self.client.post(
reverse("counter:login", kwargs={"counter_id": self.mde.id}), reverse("counter:login", kwargs={"counter_id": self.mde.id}),
{"username": self.skia.username, "password": "plop"}, {"username": self.skia.username, "password": "plop"},
) )
@ -62,13 +63,12 @@ class CounterTest(TestCase):
reverse("counter:details", kwargs={"counter_id": self.mde.id}), reverse("counter:details", kwargs={"counter_id": self.mde.id}),
{"code": "4000k", "counter_token": counter_token}, {"code": "4000k", "counter_token": counter_token},
) )
location = response.get("location") counter_url = response.get("location")
response = self.client.get(response.get("location")) response = self.client.get(response.get("location"))
self.assertTrue(">Richard Batsbak</" in str(response.content)) self.assertTrue(">Richard Batsbak</" in str(response.content))
self.client.post( self.client.post(
location, counter_url,
{ {
"action": "refill", "action": "refill",
"amount": "5", "amount": "5",
@ -76,17 +76,27 @@ class CounterTest(TestCase):
"bank": "OTHER", "bank": "OTHER",
}, },
) )
self.client.post(location, {"action": "code", "code": "BARB"}) self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
self.client.post(location, {"action": "add_product", "product_id": "4"}) self.client.post(
self.client.post(location, {"action": "del_product", "product_id": "4"}) counter_url, "action=add_product&product_id=4", content_type="text/xml"
self.client.post(location, {"action": "code", "code": "2xdeco"}) )
self.client.post(location, {"action": "code", "code": "1xbarb"}) self.client.post(
response = self.client.post(location, {"action": "code", "code": "fin"}) counter_url, "action=del_product&product_id=4", content_type="text/xml"
)
self.client.post(
counter_url, "action=code&code=2xdeco", content_type="text/xml"
)
self.client.post(
counter_url, "action=code&code=1xbarb", content_type="text/xml"
)
response = self.client.post(
counter_url, "action=code&code=fin", content_type="text/xml"
)
response_get = self.client.get(response.get("location")) response_get = self.client.get(response.get("location"))
response_content = response_get.content.decode("utf-8") response_content = response_get.content.decode("utf-8")
self.assertTrue("<li>2 x Barbar" in str(response_content)) self.assertTrue("2 x Barbar" in str(response_content))
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content)) self.assertTrue("2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue( self.assertTrue(
"<p>Client : Richard Batsbak - Nouveau montant : 3.60" "<p>Client : Richard Batsbak - Nouveau montant : 3.60"
in str(response_content) in str(response_content)
@ -98,7 +108,7 @@ class CounterTest(TestCase):
) )
response = self.client.post( response = self.client.post(
location, counter_url,
{ {
"action": "refill", "action": "refill",
"amount": "5", "amount": "5",
@ -108,7 +118,7 @@ class CounterTest(TestCase):
) )
self.assertTrue(response.status_code == 200) self.assertTrue(response.status_code == 200)
response = self.client.post( self.client.post(
reverse("counter:login", kwargs={"counter_id": self.foyer.id}), reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
{"username": self.krophil.username, "password": "plop"}, {"username": self.krophil.username, "password": "plop"},
) )
@ -125,10 +135,10 @@ class CounterTest(TestCase):
reverse("counter:details", kwargs={"counter_id": self.foyer.id}), reverse("counter:details", kwargs={"counter_id": self.foyer.id}),
{"code": "4000k", "counter_token": counter_token}, {"code": "4000k", "counter_token": counter_token},
) )
location = response.get("location") counter_url = response.get("location")
response = self.client.post( response = self.client.post(
location, counter_url,
{ {
"action": "refill", "action": "refill",
"amount": "5", "amount": "5",
@ -138,13 +148,27 @@ class CounterTest(TestCase):
) )
self.assertTrue(response.status_code == 200) self.assertTrue(response.status_code == 200)
def test_annotate_has_barman_queryset(self):
"""
Test if the custom queryset method ``annotate_has_barman``
works as intended
"""
self.sli.counters.clear()
self.sli.counters.add(self.foyer, self.mde)
counters = Counter.objects.annotate_has_barman(self.sli)
for counter in counters:
if counter.name in ("Foyer", "MDE"):
self.assertTrue(counter.has_annotated_barman)
else:
self.assertFalse(counter.has_annotated_barman)
class CounterStatsTest(TestCase): class CounterStatsTest(TestCase):
def setUp(self): def setUp(self):
call_command("populate") call_command("populate")
self.counter = Counter.objects.filter(id=2).first() self.counter = Counter.objects.filter(id=2).first()
def test_unothorized_user_fail(self): def test_unauthorised_user_fail(self):
# Test with not login user # Test with not login user
response = self.client.get(reverse("counter:stats", args=[self.counter.id])) response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
self.assertTrue(response.status_code == 403) self.assertTrue(response.status_code == 403)
@ -745,18 +769,30 @@ class StudentCardTest(TestCase):
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
class AccountIdTest(TestCase): class CustomerAccountIdTest(TestCase):
def setUp(self): def setUp(self):
user_a = User.objects.create(username="a", password="plop", email="a.a@a.fr") self.user_a = User.objects.create(
username="a", password="plop", email="a.a@a.fr"
)
user_b = User.objects.create(username="b", password="plop", email="b.b@b.fr") user_b = User.objects.create(username="b", password="plop", email="b.b@b.fr")
user_c = User.objects.create(username="c", password="plop", email="c.c@c.fr") user_c = User.objects.create(username="c", password="plop", email="c.c@c.fr")
Customer.objects.create(user=user_a, amount=0, account_id="1111a") Customer.objects.create(user=self.user_a, amount=10, account_id="1111a")
Customer.objects.create(user=user_b, amount=0, account_id="9999z") Customer.objects.create(user=user_b, amount=0, account_id="9999z")
Customer.objects.create(user=user_c, amount=0, account_id="12345f") Customer.objects.create(user=user_c, amount=0, account_id="12345f")
def test_create_customer(self): def test_create_customer(self):
user_d = User.objects.create(username="d", password="plop") user_d = User.objects.create(username="d", password="plop")
customer_d = Customer.new_for_user(user_d) customer, created = Customer.get_or_create(user_d)
customer_d.save() account_id = customer.account_id
number = customer_d.account_id[:-1] number = account_id[:-1]
self.assertTrue(created)
self.assertEqual(number, "12346") self.assertEqual(number, "12346")
self.assertEqual(6, len(account_id))
self.assertIn(account_id[-1], string.ascii_lowercase)
self.assertEqual(0, customer.amount)
def test_get_existing_account(self):
account, created = Customer.get_or_create(self.user_a)
self.assertFalse(created)
self.assertEqual(account.account_id, "1111a")
self.assertEqual(10, account.amount)

View File

@ -22,47 +22,47 @@
# #
# #
from django.urls import re_path, path from django.urls import path
from counter.views import * from counter.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^(?P<counter_id>[0-9]+)$", CounterMain.as_view(), name="details"), path("<int:counter_id>/", CounterMain.as_view(), name="details"),
re_path( path(
r"^(?P<counter_id>[0-9]+)/click/(?P<user_id>[0-9]+)$", "<int:counter_id>/click/<int:user_id>/",
CounterClick.as_view(), CounterClick.as_view(),
name="click", name="click",
), ),
re_path( path(
r"^(?P<counter_id>[0-9]+)/last_ops$", "<int:counter_id>/last_ops/",
CounterLastOperationsView.as_view(), CounterLastOperationsView.as_view(),
name="last_ops", name="last_ops",
), ),
re_path( path(
r"^(?P<counter_id>[0-9]+)/cash_summary$", "<int:counter_id>/cash_summary/",
CounterCashSummaryView.as_view(), CounterCashSummaryView.as_view(),
name="cash_summary", name="cash_summary",
), ),
re_path( path(
r"^(?P<counter_id>[0-9]+)/activity$", "<int:counter_id>/activity/",
CounterActivityView.as_view(), CounterActivityView.as_view(),
name="activity", name="activity",
), ),
re_path(r"^(?P<counter_id>[0-9]+)/stats$", CounterStatView.as_view(), name="stats"), path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
re_path(r"^(?P<counter_id>[0-9]+)/login$", CounterLogin.as_view(), name="login"), path("<int:counter_id>/login/", CounterLogin.as_view(), name="login"),
re_path(r"^(?P<counter_id>[0-9]+)/logout$", CounterLogout.as_view(), name="logout"), path("<int:counter_id>/logout/", CounterLogout.as_view(), name="logout"),
re_path( path(
r"^eticket/(?P<selling_id>[0-9]+)/pdf$", "eticket/<int:selling_id>/pdf/",
EticketPDFView.as_view(), EticketPDFView.as_view(),
name="eticket_pdf", name="eticket_pdf",
), ),
re_path( path(
r"^customer/(?P<customer_id>[0-9]+)/card/add$", "customer/<int:customer_id>/card/add/",
StudentCardFormView.as_view(), StudentCardFormView.as_view(),
name="add_student_card", name="add_student_card",
), ),
re_path( path(
r"^customer/(?P<customer_id>[0-9]+)/card/delete/(?P<card_id>[0-9]+)/$", "customer/<int:customer_id>/card/delete/<int:card_id>/",
StudentCardDeleteView.as_view(), StudentCardDeleteView.as_view(),
name="delete_student_card", name="delete_student_card",
), ),
@ -76,76 +76,76 @@ urlpatterns = [
edit_billing_info, edit_billing_info,
name="edit_billing_info", name="edit_billing_info",
), ),
re_path(r"^admin/(?P<counter_id>[0-9]+)$", CounterEditView.as_view(), name="admin"), path("admin/<int:counter_id>/", CounterEditView.as_view(), name="admin"),
re_path( path(
r"^admin/(?P<counter_id>[0-9]+)/prop$", "admin/<int:counter_id>/prop/",
CounterEditPropView.as_view(), CounterEditPropView.as_view(),
name="prop_admin", name="prop_admin",
), ),
re_path(r"^admin$", CounterListView.as_view(), name="admin_list"), path("admin/", CounterListView.as_view(), name="admin_list"),
re_path(r"^admin/new$", CounterCreateView.as_view(), name="new"), path("admin/new/", CounterCreateView.as_view(), name="new"),
re_path( path(
r"^admin/delete/(?P<counter_id>[0-9]+)$", "admin/delete/<int:counter_id>/",
CounterDeleteView.as_view(), CounterDeleteView.as_view(),
name="delete", name="delete",
), ),
re_path(r"^admin/invoices_call$", InvoiceCallView.as_view(), name="invoices_call"), path("admin/invoices_call/", InvoiceCallView.as_view(), name="invoices_call"),
re_path( path(
r"^admin/cash_summary/list$", "admin/cash_summary/list/",
CashSummaryListView.as_view(), CashSummaryListView.as_view(),
name="cash_summary_list", name="cash_summary_list",
), ),
re_path( path(
r"^admin/cash_summary/(?P<cashsummary_id>[0-9]+)$", "admin/cash_summary/<int:cashsummary_id>/",
CashSummaryEditView.as_view(), CashSummaryEditView.as_view(),
name="cash_summary_edit", name="cash_summary_edit",
), ),
re_path(r"^admin/product/list$", ProductListView.as_view(), name="product_list"), path("admin/product/list/", ProductListView.as_view(), name="product_list"),
re_path( path(
r"^admin/product/list_archived$", "admin/product/list_archived/",
ProductArchivedListView.as_view(), ProductArchivedListView.as_view(),
name="product_list_archived", name="product_list_archived",
), ),
re_path(r"^admin/product/create$", ProductCreateView.as_view(), name="new_product"), path("admin/product/create/", ProductCreateView.as_view(), name="new_product"),
re_path( path(
r"^admin/product/(?P<product_id>[0-9]+)$", "admin/product/<int:product_id>/",
ProductEditView.as_view(), ProductEditView.as_view(),
name="product_edit", name="product_edit",
), ),
re_path( path(
r"^admin/producttype/list$", "admin/producttype/list/",
ProductTypeListView.as_view(), ProductTypeListView.as_view(),
name="producttype_list", name="producttype_list",
), ),
re_path( path(
r"^admin/producttype/create$", "admin/producttype/create/",
ProductTypeCreateView.as_view(), ProductTypeCreateView.as_view(),
name="new_producttype", name="new_producttype",
), ),
re_path( path(
r"^admin/producttype/(?P<type_id>[0-9]+)$", "admin/producttype/<int:type_id>/",
ProductTypeEditView.as_view(), ProductTypeEditView.as_view(),
name="producttype_edit", name="producttype_edit",
), ),
re_path(r"^admin/eticket/list$", EticketListView.as_view(), name="eticket_list"), path("admin/eticket/list/", EticketListView.as_view(), name="eticket_list"),
re_path(r"^admin/eticket/new$", EticketCreateView.as_view(), name="new_eticket"), path("admin/eticket/new/", EticketCreateView.as_view(), name="new_eticket"),
re_path( path(
r"^admin/eticket/(?P<eticket_id>[0-9]+)$", "admin/eticket/<int:eticket_id>/",
EticketEditView.as_view(), EticketEditView.as_view(),
name="edit_eticket", name="edit_eticket",
), ),
re_path( path(
r"^admin/selling/(?P<selling_id>[0-9]+)/delete$", "admin/selling/<int:selling_id>/delete/",
SellingDeleteView.as_view(), SellingDeleteView.as_view(),
name="selling_delete", name="selling_delete",
), ),
re_path( path(
r"^admin/refilling/(?P<refilling_id>[0-9]+)/delete$", "admin/refilling/<int:refilling_id>/delete/",
RefillingDeleteView.as_view(), RefillingDeleteView.as_view(),
name="refilling_delete", name="refilling_delete",
), ),
re_path( path(
r"^admin/(?P<counter_id>[0-9]+)/refillings$", "admin/<int:counter_id>/refillings/",
CounterRefillingListView.as_view(), CounterRefillingListView.as_view(),
name="refilling_list", name="refilling_list",
), ),

View File

@ -22,8 +22,10 @@
# #
# #
import json import json
from urllib.parse import parse_qs
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import F
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.http import Http404 from django.http import Http404
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -300,7 +302,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
current_tab = "counter" current_tab = "counter"
def render_to_response(self, *args, **kwargs): def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests if self.is_ajax(self.request):
response = {"errors": []} response = {"errors": []}
status = HTTPStatus.OK status = HTTPStatus.OK
@ -395,42 +397,40 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["not_valid_student_card_uid"] = False request.session["not_valid_student_card_uid"] = False
if self.object.type != "BAR": if self.object.type != "BAR":
self.operator = request.user self.operator = request.user
elif self.is_barman_price(): elif self.customer_is_barman():
self.operator = self.customer.user self.operator = self.customer.user
else: else:
self.operator = self.object.get_random_barman() self.operator = self.object.get_random_barman()
action = self.request.POST.get("action", None)
if "add_product" in request.POST["action"]: if action is None:
action = parse_qs(request.body.decode()).get("action", [""])[0]
if action == "add_product":
self.add_product(request) self.add_product(request)
elif "add_student_card" in request.POST["action"]: elif action == "add_student_card":
self.add_student_card(request) self.add_student_card(request)
elif "del_product" in request.POST["action"]: elif action == "del_product":
self.del_product(request) self.del_product(request)
elif "refill" in request.POST["action"]: elif action == "refill":
self.refill(request) self.refill(request)
elif "code" in request.POST["action"]: elif action == "code":
return self.parse_code(request) return self.parse_code(request)
elif "cancel" in request.POST["action"]: elif action == "cancel":
return self.cancel(request) return self.cancel(request)
elif "finish" in request.POST["action"]: elif action == "finish":
return self.finish(request) return self.finish(request)
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
return self.render_to_response(context) return self.render_to_response(context)
def is_barman_price(self): def customer_is_barman(self) -> bool:
if self.object.type == "BAR" and self.customer.user.id in [ barmen = self.object.barmen_list
s.id for s in self.object.get_barmen_list() return self.object.type == "BAR" and self.customer.user in barmen
]:
return True
else:
return False
def get_product(self, pid): def get_product(self, pid):
return Product.objects.filter(pk=int(pid)).first() return Product.objects.filter(pk=int(pid)).first()
def get_price(self, pid): def get_price(self, pid):
p = self.get_product(pid) p = self.get_product(pid)
if self.is_barman_price(): if self.customer_is_barman():
price = p.special_selling_price price = p.special_selling_price
else: else:
price = p.selling_price price = p.selling_price
@ -475,13 +475,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
self.compute_record_product(request, product) self.compute_record_product(request, product)
) )
@staticmethod
def is_ajax(request):
# when using the fetch API, the django request.POST dict is empty
# this is but a wretched contrivance which strive to replace
# the deprecated django is_ajax() method
# and which must be replaced as soon as possible
# by a proper separation between the api endpoints of the counter
return len(request.POST) == 0 and len(request.body) != 0
def add_product(self, request, q=1, p=None): def add_product(self, request, q=1, p=None):
""" """
Add a product to the basket Add a product to the basket
q is the quantity passed as integer q is the quantity passed as integer
p is the product id, passed as an integer p is the product id, passed as an integer
""" """
pid = p or request.POST["product_id"] pid = p or parse_qs(request.body.decode())["product_id"][0]
pid = str(pid) pid = str(pid)
price = self.get_price(pid) price = self.get_price(pid)
total = self.sum_basket(request) total = self.sum_basket(request)
@ -563,7 +572,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
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 = parse_qs(request.body.decode())["product_id"][0]
product = self.get_product(pid) product = self.get_product(pid)
if pid in request.session["basket"]: if pid in request.session["basket"]:
if ( if (
@ -576,30 +585,29 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["basket"][pid]["qty"] -= 1 request.session["basket"][pid]["qty"] -= 1
if request.session["basket"][pid]["qty"] <= 0: if request.session["basket"][pid]["qty"] <= 0:
del request.session["basket"][pid] del request.session["basket"][pid]
else:
request.session["basket"][pid] = None
request.session.modified = True request.session.modified = True
def parse_code(self, request): def parse_code(self, request):
"""Parse the string entered by the barman""" """
string = str(request.POST["code"]).upper() Parse the string entered by the barman
if string == _("END"): This can be of two forms :
- <str>, where the string is the code of the product
- <int>X<str>, where the integer is the quantity and str the code
"""
string = parse_qs(request.body.decode())["code"][0].upper()
if string == "FIN":
return self.finish(request) return self.finish(request)
elif string == _("CAN"): elif string == "ANN":
return self.cancel(request) return self.cancel(request)
regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$") regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
m = regex.match(string) m = regex.match(string)
if m is not None: if m is not None:
nb = m.group("nb") nb = m.group("nb")
code = m.group("code") code = m.group("code")
if nb is None: nb = int(nb) if nb is not None else 1
nb = 1
else:
nb = int(nb)
p = self.object.products.filter(code=code).first() p = self.object.products.filter(code=code).first()
if p is not None: if p is not None:
while nb > 0 and not self.add_product(request, nb, p.id): self.add_product(request, nb, p.id)
nb -= 1
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
return self.render_to_response(context) return self.render_to_response(context)
@ -613,7 +621,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
for pid, infos in request.session["basket"].items(): for pid, infos in request.session["basket"].items():
# This duplicates code for DB optimization (prevent to load many times the same object) # This duplicates code for DB optimization (prevent to load many times the same object)
p = Product.objects.filter(pk=pid).first() p = Product.objects.filter(pk=pid).first()
if self.is_barman_price(): if self.customer_is_barman():
uprice = p.special_selling_price uprice = p.special_selling_price
else: else:
uprice = p.selling_price uprice = p.selling_price
@ -665,7 +673,8 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def refill(self, request): def refill(self, request):
"""Refill the customer's account""" """Refill the customer's account"""
if self.get_object().type == "BAR" and self.object.can_refill(): if not self.object.can_refill():
raise PermissionDenied
form = RefillForm(request.POST) form = RefillForm(request.POST)
if form.is_valid(): if form.is_valid():
form.instance.counter = self.object form.instance.counter = self.object
@ -674,13 +683,16 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
form.instance.save() form.instance.save()
else: else:
self.refill_form = form self.refill_form = form
else:
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") products = self.object.products.select_related("product_type")
if self.customer_is_barman():
products = products.annotate(price=F("special_selling_price"))
else:
products = products.annotate(price=F("selling_price"))
kwargs["products"] = products
kwargs["categories"] = {} kwargs["categories"] = {}
for product in kwargs["products"]: for product in kwargs["products"]:
if product.product_type: if product.product_type:
@ -1780,11 +1792,7 @@ def create_billing_info(request, user_id):
if user.id != user_id and not user.has_perm("counter:add_billinginfo"): if user.id != user_id and not user.has_perm("counter:add_billinginfo"):
raise PermissionDenied() raise PermissionDenied()
user = get_object_or_404(User, pk=user_id) user = get_object_or_404(User, pk=user_id)
if not hasattr(user, "customer"): customer, _ = Customer.get_or_create(user)
customer = Customer.new_for_user(user)
customer.save()
else:
customer = get_object_or_404(Customer, user_id=user_id)
BillingInfo.objects.create(customer=customer) BillingInfo.objects.create(customer=customer)
return __manage_billing_info_req(request, user_id, True) return __manage_billing_info_req(request, user_id, True)

View File

@ -245,12 +245,13 @@ class Invoice(models.Model):
def validate(self): def validate(self):
if self.validated: if self.validated:
raise DataError(_("Invoice already validated")) raise DataError(_("Invoice already validated"))
customer, created = Customer.get_or_create(user=self.user)
eboutic = Counter.objects.filter(type="EBOUTIC").first() eboutic = Counter.objects.filter(type="EBOUTIC").first()
for i in self.items.all(): for i in self.items.all():
if i.type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING: if i.type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
new = Refilling( new = Refilling(
counter=eboutic, counter=eboutic,
customer=self.user.customer, customer=customer,
operator=self.user, operator=self.user,
amount=i.product_unit_price * i.quantity, amount=i.product_unit_price * i.quantity,
payment_method="CARD", payment_method="CARD",
@ -266,7 +267,7 @@ class Invoice(models.Model):
club=product.club, club=product.club,
product=product, product=product,
seller=self.user, seller=self.user,
customer=self.user.customer, customer=customer,
unit_price=i.product_unit_price, unit_price=i.product_unit_price,
quantity=i.quantity, quantity=i.quantity,
payment_method="CARD", payment_method="CARD",

View File

@ -111,7 +111,7 @@
<i class="fa fa-2x fa-picture-o product-image" ></i> <i class="fa fa-2x fa-picture-o product-image" ></i>
{% endif %} {% endif %}
<div class="product-description"> <div class="product-description">
<h4>{{ p.name }}</strong></h4> <h4>{{ p.name }}</h4>
<p>{{ p.selling_price }} €</p> <p>{{ p.selling_price }} €</p>
</div> </div>
</button> </button>

View File

@ -1,57 +1,49 @@
from django.urls import re_path from django.urls import path
from election.views import * from election.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^$", ElectionsListView.as_view(), name="list"), path("", ElectionsListView.as_view(), name="list"),
re_path(r"^archived$", ElectionListArchivedView.as_view(), name="list_archived"), path("archived/", ElectionListArchivedView.as_view(), name="list_archived"),
re_path(r"^add$", ElectionCreateView.as_view(), name="create"), path("add/", ElectionCreateView.as_view(), name="create"),
re_path( path("<int:election_id>/edit/", ElectionUpdateView.as_view(), name="update"),
r"^(?P<election_id>[0-9]+)/edit$", ElectionUpdateView.as_view(), name="update" path("<int:election_id>/delete/", ElectionDeleteView.as_view(), name="delete"),
), path(
re_path( "<int:election_id>/list/add/",
r"^(?P<election_id>[0-9]+)/delete$", ElectionDeleteView.as_view(), name="delete"
),
re_path(
r"^(?P<election_id>[0-9]+)/list/add$",
ElectionListCreateView.as_view(), ElectionListCreateView.as_view(),
name="create_list", name="create_list",
), ),
re_path( path(
r"^(?P<list_id>[0-9]+)/list/delete$", "<int:list_id>/list/delete/",
ElectionListDeleteView.as_view(), ElectionListDeleteView.as_view(),
name="delete_list", name="delete_list",
), ),
re_path( path(
r"^(?P<election_id>[0-9]+)/role/create$", "<int:election_id>/role/create/",
RoleCreateView.as_view(), RoleCreateView.as_view(),
name="create_role", name="create_role",
), ),
re_path( path("<int:role_id>/role/edit/", RoleUpdateView.as_view(), name="update_role"),
r"^(?P<role_id>[0-9]+)/role/edit$", RoleUpdateView.as_view(), name="update_role" path(
), "<int:role_id>/role/delete/",
re_path(
r"^(?P<role_id>[0-9]+)/role/delete$",
RoleDeleteView.as_view(), RoleDeleteView.as_view(),
name="delete_role", name="delete_role",
), ),
re_path( path(
r"^(?P<election_id>[0-9]+)/candidate/add$", "<int:election_id>/candidate/add/",
CandidatureCreateView.as_view(), CandidatureCreateView.as_view(),
name="candidate", name="candidate",
), ),
re_path( path(
r"^(?P<candidature_id>[0-9]+)/candidate/edit$", "<int:candidature_id>/candidate/edit/",
CandidatureUpdateView.as_view(), CandidatureUpdateView.as_view(),
name="update_candidate", name="update_candidate",
), ),
re_path( path(
r"^(?P<candidature_id>[0-9]+)/candidate/delete$", "<int:candidature_id>/candidate/delete/",
CandidatureDeleteView.as_view(), CandidatureDeleteView.as_view(),
name="delete_candidate", name="delete_candidate",
), ),
re_path(r"^(?P<election_id>[0-9]+)/vote$", VoteFormView.as_view(), name="vote"), path("<int:election_id>/vote/", VoteFormView.as_view(), name="vote"),
re_path( path("<int:election_id>/detail/", ElectionDetailView.as_view(), name="detail"),
r"^(?P<election_id>[0-9]+)/detail$", ElectionDetailView.as_view(), name="detail"
),
] ]

View File

@ -22,69 +22,62 @@
# #
# #
from django.urls import re_path from django.urls import path
from forum.views import * from forum.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^$", ForumMainView.as_view(), name="main"), path("", ForumMainView.as_view(), name="main"),
re_path(r"^search/$", ForumSearchView.as_view(), name="search"), path("search/", ForumSearchView.as_view(), name="search"),
re_path(r"^new_forum$", ForumCreateView.as_view(), name="new_forum"), path("new_forum/", ForumCreateView.as_view(), name="new_forum"),
re_path( path("mark_all_as_read/", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"),
r"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read" path("last_unread/", ForumLastUnread.as_view(), name="last_unread"),
), path("favorite_topics/", ForumFavoriteTopics.as_view(), name="favorite_topics"),
re_path(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"), path("<int:forum_id>/", ForumDetailView.as_view(), name="view_forum"),
re_path( path("<int:forum_id>/edit/", ForumEditView.as_view(), name="edit_forum"),
r"^favorite_topics$", ForumFavoriteTopics.as_view(), name="favorite_topics" path("<int:forum_id>/delete/", ForumDeleteView.as_view(), name="delete_forum"),
), path(
re_path(r"^(?P<forum_id>[0-9]+)$", ForumDetailView.as_view(), name="view_forum"), "<int:forum_id>/new_topic/",
re_path(r"^(?P<forum_id>[0-9]+)/edit$", ForumEditView.as_view(), name="edit_forum"),
re_path(
r"^(?P<forum_id>[0-9]+)/delete$", ForumDeleteView.as_view(), name="delete_forum"
),
re_path(
r"^(?P<forum_id>[0-9]+)/new_topic$",
ForumTopicCreateView.as_view(), ForumTopicCreateView.as_view(),
name="new_topic", name="new_topic",
), ),
re_path( path(
r"^topic/(?P<topic_id>[0-9]+)$", "topic/<int:topic_id>/",
ForumTopicDetailView.as_view(), ForumTopicDetailView.as_view(),
name="view_topic", name="view_topic",
), ),
re_path( path(
r"^topic/(?P<topic_id>[0-9]+)/edit$", "topic/<int:topic_id>/edit/",
ForumTopicEditView.as_view(), ForumTopicEditView.as_view(),
name="edit_topic", name="edit_topic",
), ),
re_path( path(
r"^topic/(?P<topic_id>[0-9]+)/new_message$", "topic/<int:topic_id>/new_message/",
ForumMessageCreateView.as_view(), ForumMessageCreateView.as_view(),
name="new_message", name="new_message",
), ),
re_path( path(
r"^topic/(?P<topic_id>[0-9]+)/toggle_subscribe$", "topic/<int:topic_id>/toggle_subscribe/",
ForumTopicSubscribeView.as_view(), ForumTopicSubscribeView.as_view(),
name="toggle_subscribe_topic", name="toggle_subscribe_topic",
), ),
re_path( path(
r"^message/(?P<message_id>[0-9]+)$", "message/<int:message_id>/",
ForumMessageView.as_view(), ForumMessageView.as_view(),
name="view_message", name="view_message",
), ),
re_path( path(
r"^message/(?P<message_id>[0-9]+)/edit$", "message/<int:message_id>/edit/",
ForumMessageEditView.as_view(), ForumMessageEditView.as_view(),
name="edit_message", name="edit_message",
), ),
re_path( path(
r"^message/(?P<message_id>[0-9]+)/delete$", "message/<int:message_id>/delete/",
ForumMessageDeleteView.as_view(), ForumMessageDeleteView.as_view(),
name="delete_message", name="delete_message",
), ),
re_path( path(
r"^message/(?P<message_id>[0-9]+)/undelete$", "message/<int:message_id>/undelete/",
ForumMessageUndeleteView.as_view(), ForumMessageUndeleteView.as_view(),
name="undelete_message", name="undelete_message",
), ),

View File

@ -26,7 +26,6 @@ import math
import logging import logging
from typing import Tuple from typing import Tuple
from django.db import models from django.db import models
from django.db.models import Q, Case, F, Value, When, Count from django.db.models import Q, Case, F, Value, When, Count
from django.db.models.functions import Concat from django.db.models.functions import Concat

View File

@ -142,8 +142,4 @@ class GalaxyTest(TestCase):
Galaxy.rule() Galaxy.rule()
self.client.login(username="root", password="plop") self.client.login(username="root", password="plop")
response = self.client.get("/galaxy/2/") response = self.client.get("/galaxy/2/")
self.assertContains( self.assertEquals(response.status_code, 404)
response,
"Ce citoyen n&#39;a pas encore rejoint la galaxie",
status_code=404,
)

View File

@ -22,54 +22,54 @@
# #
# #
from django.urls import re_path from django.urls import path
from launderette.views import * from launderette.views import *
urlpatterns = [ urlpatterns = [
# views # views
re_path(r"^$", LaunderetteMainView.as_view(), name="launderette_main"), path("", LaunderetteMainView.as_view(), name="launderette_main"),
re_path( path(
r"^slot/(?P<slot_id>[0-9]+)/delete$", "slot/<int:slot_id>/delete/",
SlotDeleteView.as_view(), SlotDeleteView.as_view(),
name="delete_slot", name="delete_slot",
), ),
re_path(r"^book$", LaunderetteBookMainView.as_view(), name="book_main"), path("book/", LaunderetteBookMainView.as_view(), name="book_main"),
re_path( path(
r"^book/(?P<launderette_id>[0-9]+)$", "book/<int:launderette_id>/",
LaunderetteBookView.as_view(), LaunderetteBookView.as_view(),
name="book_slot", name="book_slot",
), ),
re_path( path(
r"^(?P<launderette_id>[0-9]+)/click$", "<int:launderette_id>/click/",
LaunderetteMainClickView.as_view(), LaunderetteMainClickView.as_view(),
name="main_click", name="main_click",
), ),
re_path( path(
r"^(?P<launderette_id>[0-9]+)/click/(?P<user_id>[0-9]+)$", "<int:launderette_id>/click/<int:user_id>/",
LaunderetteClickView.as_view(), LaunderetteClickView.as_view(),
name="click", name="click",
), ),
re_path(r"^admin$", LaunderetteListView.as_view(), name="launderette_list"), path("admin/", LaunderetteListView.as_view(), name="launderette_list"),
re_path( path(
r"^admin/(?P<launderette_id>[0-9]+)$", "admin/<int:launderette_id>/",
LaunderetteAdminView.as_view(), LaunderetteAdminView.as_view(),
name="launderette_admin", name="launderette_admin",
), ),
re_path( path(
r"^admin/(?P<launderette_id>[0-9]+)/edit$", "admin/<int:launderette_id>/edit/",
LaunderetteEditView.as_view(), LaunderetteEditView.as_view(),
name="launderette_edit", name="launderette_edit",
), ),
re_path(r"^admin/new$", LaunderetteCreateView.as_view(), name="launderette_new"), path("admin/new/", LaunderetteCreateView.as_view(), name="launderette_new"),
re_path(r"^admin/machine/new$", MachineCreateView.as_view(), name="machine_new"), path("admin/machine/new/", MachineCreateView.as_view(), name="machine_new"),
re_path( path(
r"^admin/machine/(?P<machine_id>[0-9]+)/edit$", "admin/machine/<int:machine_id>/edit/",
MachineEditView.as_view(), MachineEditView.as_view(),
name="machine_edit", name="machine_edit",
), ),
re_path( path(
r"^admin/machine/(?P<machine_id>[0-9]+)/delete$", "admin/machine/<int:machine_id>/delete/",
MachineDeleteView.as_view(), MachineDeleteView.as_view(),
name="machine_delete", name="machine_delete",
), ),

View File

@ -3070,7 +3070,7 @@ msgid "Eboutic invoices"
msgstr "Facture eboutic" msgstr "Facture eboutic"
#: core/templates/core/user_account.jinja:57 #: core/templates/core/user_account.jinja:57
#: core/templates/core/user_tools.jinja:37 counter/views.py:795 #: core/templates/core/user_tools.jinja:37 counter/views.py:787
msgid "Etickets" msgid "Etickets"
msgstr "Etickets" msgstr "Etickets"
@ -3381,7 +3381,7 @@ msgstr "Achats"
msgid "Product top 10" msgid "Product top 10"
msgstr "Top 10 produits" msgstr "Top 10 produits"
#: core/templates/core/user_stats.jinja:27 counter/forms.py:176 #: core/templates/core/user_stats.jinja:27 counter/forms.py:168
msgid "Product" msgid "Product"
msgstr "Produit" msgstr "Produit"
@ -3717,24 +3717,24 @@ msgstr "Galaxie"
msgid "User already has a profile picture" msgid "User already has a profile picture"
msgstr "L'utilisateur a déjà une photo de profil" msgstr "L'utilisateur a déjà une photo de profil"
#: counter/app.py:31 counter/models.py:341 counter/models.py:750 #: counter/app.py:31 counter/models.py:340 counter/models.py:749
#: counter/models.py:786 launderette/models.py:41 stock/models.py:43 #: counter/models.py:779 launderette/models.py:41 stock/models.py:43
msgid "counter" msgid "counter"
msgstr "comptoir" msgstr "comptoir"
#: counter/forms.py:38 #: counter/forms.py:30
msgid "This UID is invalid" msgid "This UID is invalid"
msgstr "Cet UID est invalide" msgstr "Cet UID est invalide"
#: counter/forms.py:77 #: counter/forms.py:69
msgid "User not found" msgid "User not found"
msgstr "Utilisateur non trouvé" msgstr "Utilisateur non trouvé"
#: counter/forms.py:125 #: counter/forms.py:117
msgid "Parent product" msgid "Parent product"
msgstr "Produit parent" msgstr "Produit parent"
#: counter/forms.py:131 #: counter/forms.py:123
msgid "Buying groups" msgid "Buying groups"
msgstr "Groupes d'achat" msgstr "Groupes d'achat"
@ -3758,7 +3758,7 @@ msgstr "client"
msgid "customers" msgid "customers"
msgstr "clients" msgstr "clients"
#: counter/models.py:126 counter/views.py:317 #: counter/models.py:126 counter/views.py:309
msgid "Not enough money" msgid "Not enough money"
msgstr "Solde insuffisant" msgstr "Solde insuffisant"
@ -3774,133 +3774,133 @@ msgstr "Nom de famille"
msgid "Address 1" msgid "Address 1"
msgstr "Adresse 1" msgstr "Adresse 1"
#: counter/models.py:160 #: counter/models.py:161
msgid "Address 2" msgid "Address 2"
msgstr "Adresse 2" msgstr "Adresse 2"
#: counter/models.py:161 #: counter/models.py:163
msgid "Zip code" msgid "Zip code"
msgstr "Code postal" msgstr "Code postal"
#: counter/models.py:162 #: counter/models.py:164
msgid "City" msgid "City"
msgstr "Ville" msgstr "Ville"
#: counter/models.py:163 #: counter/models.py:165
msgid "Country" msgid "Country"
msgstr "Pays" msgstr "Pays"
#: counter/models.py:206 counter/models.py:234 #: counter/models.py:209 counter/models.py:237
msgid "product type" msgid "product type"
msgstr "type du produit" msgstr "type du produit"
#: counter/models.py:240 #: counter/models.py:243
msgid "purchase price" msgid "purchase price"
msgstr "prix d'achat" msgstr "prix d'achat"
#: counter/models.py:241 #: counter/models.py:244
msgid "selling price" msgid "selling price"
msgstr "prix de vente" msgstr "prix de vente"
#: counter/models.py:242 #: counter/models.py:245
msgid "special selling price" msgid "special selling price"
msgstr "prix de vente spécial" msgstr "prix de vente spécial"
#: counter/models.py:244 #: counter/models.py:247
msgid "icon" msgid "icon"
msgstr "icône" msgstr "icône"
#: counter/models.py:249 #: counter/models.py:252
msgid "limit age" msgid "limit age"
msgstr "âge limite" msgstr "âge limite"
#: counter/models.py:250 #: counter/models.py:253
msgid "tray price" msgid "tray price"
msgstr "prix plateau" msgstr "prix plateau"
#: counter/models.py:254 #: counter/models.py:257
msgid "parent product" msgid "parent product"
msgstr "produit parent" msgstr "produit parent"
#: counter/models.py:260 #: counter/models.py:263
msgid "buying groups" msgid "buying groups"
msgstr "groupe d'achat" msgstr "groupe d'achat"
#: counter/models.py:262 election/models.py:52 #: counter/models.py:265 election/models.py:52
msgid "archived" msgid "archived"
msgstr "archivé" msgstr "archivé"
#: counter/models.py:265 counter/models.py:881 #: counter/models.py:268 counter/models.py:874
msgid "product" msgid "product"
msgstr "produit" msgstr "produit"
#: counter/models.py:322 #: counter/models.py:321
msgid "products" msgid "products"
msgstr "produits" msgstr "produits"
#: counter/models.py:325 #: counter/models.py:324
msgid "counter type" msgid "counter type"
msgstr "type de comptoir" msgstr "type de comptoir"
#: counter/models.py:327 #: counter/models.py:326
msgid "Bar" msgid "Bar"
msgstr "Bar" msgstr "Bar"
#: counter/models.py:327 #: counter/models.py:326
msgid "Office" msgid "Office"
msgstr "Bureau" msgstr "Bureau"
#: counter/models.py:330 #: counter/models.py:329
msgid "sellers" msgid "sellers"
msgstr "vendeurs" msgstr "vendeurs"
#: counter/models.py:338 launderette/models.py:207 #: counter/models.py:337 launderette/models.py:207
msgid "token" msgid "token"
msgstr "jeton" msgstr "jeton"
#: counter/models.py:493 #: counter/models.py:492
msgid "bank" msgid "bank"
msgstr "banque" msgstr "banque"
#: counter/models.py:495 counter/models.py:585 #: counter/models.py:494 counter/models.py:584
msgid "is validated" msgid "is validated"
msgstr "est validé" msgstr "est validé"
#: counter/models.py:498 #: counter/models.py:497
msgid "refilling" msgid "refilling"
msgstr "rechargement" msgstr "rechargement"
#: counter/models.py:562 eboutic/models.py:288 #: counter/models.py:561 eboutic/models.py:292
msgid "unit price" msgid "unit price"
msgstr "prix unitaire" msgstr "prix unitaire"
#: counter/models.py:563 counter/models.py:866 eboutic/models.py:289 #: counter/models.py:562 counter/models.py:859 eboutic/models.py:293
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: counter/models.py:582 #: counter/models.py:581
msgid "Sith account" msgid "Sith account"
msgstr "Compte utilisateur" msgstr "Compte utilisateur"
#: counter/models.py:582 sith/settings.py:383 sith/settings.py:388 #: counter/models.py:581 sith/settings.py:359 sith/settings.py:364
#: sith/settings.py:408 #: sith/settings.py:384
msgid "Credit card" msgid "Credit card"
msgstr "Carte bancaire" msgstr "Carte bancaire"
#: counter/models.py:588 #: counter/models.py:587
msgid "selling" msgid "selling"
msgstr "vente" msgstr "vente"
#: counter/models.py:615 #: counter/models.py:614
msgid "Unknown event" msgid "Unknown event"
msgstr "Événement inconnu" msgstr "Événement inconnu"
#: counter/models.py:616 #: counter/models.py:615
#, python-format #, python-format
msgid "Eticket bought for the event %(event)s" msgid "Eticket bought for the event %(event)s"
msgstr "Eticket acheté pour l'événement %(event)s" msgstr "Eticket acheté pour l'événement %(event)s"
#: counter/models.py:618 counter/models.py:641 #: counter/models.py:617 counter/models.py:640
#, python-format #, python-format
msgid "" msgid ""
"You bought an eticket for the event %(event)s.\n" "You bought an eticket for the event %(event)s.\n"
@ -3912,59 +3912,59 @@ msgstr ""
"Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte "
"%(url)s." "%(url)s."
#: counter/models.py:755 #: counter/models.py:754
msgid "last activity date" msgid "last activity date"
msgstr "dernière activité" msgstr "dernière activité"
#: counter/models.py:758 #: counter/models.py:757
msgid "permanency" msgid "permanency"
msgstr "permanence" msgstr "permanence"
#: counter/models.py:791 #: counter/models.py:784
msgid "emptied" msgid "emptied"
msgstr "coffre vidée" msgstr "coffre vidée"
#: counter/models.py:794 #: counter/models.py:787
msgid "cash register summary" msgid "cash register summary"
msgstr "relevé de caisse" msgstr "relevé de caisse"
#: counter/models.py:862 #: counter/models.py:855
msgid "cash summary" msgid "cash summary"
msgstr "relevé" msgstr "relevé"
#: counter/models.py:865 #: counter/models.py:858
msgid "value" msgid "value"
msgstr "valeur" msgstr "valeur"
#: counter/models.py:867 #: counter/models.py:860
msgid "check" msgid "check"
msgstr "chèque" msgstr "chèque"
#: counter/models.py:870 #: counter/models.py:863
msgid "cash register summary item" msgid "cash register summary item"
msgstr "élément de relevé de caisse" msgstr "élément de relevé de caisse"
#: counter/models.py:885 #: counter/models.py:878
msgid "banner" msgid "banner"
msgstr "bannière" msgstr "bannière"
#: counter/models.py:887 #: counter/models.py:880
msgid "event date" msgid "event date"
msgstr "date de l'événement" msgstr "date de l'événement"
#: counter/models.py:889 #: counter/models.py:882
msgid "event title" msgid "event title"
msgstr "titre de l'événement" msgstr "titre de l'événement"
#: counter/models.py:891 #: counter/models.py:884
msgid "secret" msgid "secret"
msgstr "secret" msgstr "secret"
#: counter/models.py:947 #: counter/models.py:940
msgid "uid" msgid "uid"
msgstr "uid" msgstr "uid"
#: counter/models.py:952 #: counter/models.py:945
msgid "student cards" msgid "student cards"
msgstr "cartes étudiante" msgstr "cartes étudiante"
@ -4016,7 +4016,7 @@ msgstr "Liste des relevés de caisse"
msgid "Theoric sums" msgid "Theoric sums"
msgstr "Sommes théoriques" msgstr "Sommes théoriques"
#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:1073 #: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:1065
msgid "Emptied" msgid "Emptied"
msgstr "Coffre vidé" msgstr "Coffre vidé"
@ -4074,11 +4074,11 @@ msgstr "Terminer"
msgid "Refilling" msgid "Refilling"
msgstr "Rechargement" msgstr "Rechargement"
#: counter/templates/counter/counter_click.jinja:193 counter/views.py:586 #: counter/templates/counter/counter_click.jinja:193 counter/views.py:578
msgid "END" msgid "END"
msgstr "FIN" msgstr "FIN"
#: counter/templates/counter/counter_click.jinja:193 counter/views.py:588 #: counter/templates/counter/counter_click.jinja:193 counter/views.py:580
msgid "CAN" msgid "CAN"
msgstr "ANN" msgstr "ANN"
@ -4244,109 +4244,109 @@ msgstr "Temps"
msgid "Top 100 barman %(counter_name)s (all semesters)" msgid "Top 100 barman %(counter_name)s (all semesters)"
msgstr "Top 100 barman %(counter_name)s (tous les semestres)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)"
#: counter/views.py:175 #: counter/views.py:167
msgid "Cash summary" msgid "Cash summary"
msgstr "Relevé de caisse" msgstr "Relevé de caisse"
#: counter/views.py:189 #: counter/views.py:181
msgid "Last operations" msgid "Last operations"
msgstr "Dernières opérations" msgstr "Dernières opérations"
#: counter/views.py:204 #: counter/views.py:196
msgid "Take items from stock" msgid "Take items from stock"
msgstr "Prendre des éléments du stock" msgstr "Prendre des éléments du stock"
#: counter/views.py:257 #: counter/views.py:249
msgid "Bad credentials" msgid "Bad credentials"
msgstr "Mauvais identifiants" msgstr "Mauvais identifiants"
#: counter/views.py:259 #: counter/views.py:251
msgid "User is not barman" msgid "User is not barman"
msgstr "L'utilisateur n'est pas barman." msgstr "L'utilisateur n'est pas barman."
#: counter/views.py:264 #: counter/views.py:256
msgid "Bad location, someone is already logged in somewhere else" msgid "Bad location, someone is already logged in somewhere else"
msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs"
#: counter/views.py:308 #: counter/views.py:300
msgid "Too young for that product" msgid "Too young for that product"
msgstr "Trop jeune pour ce produit" msgstr "Trop jeune pour ce produit"
#: counter/views.py:311 #: counter/views.py:303
msgid "Not allowed for that product" msgid "Not allowed for that product"
msgstr "Non autorisé pour ce produit" msgstr "Non autorisé pour ce produit"
#: counter/views.py:314 #: counter/views.py:306
msgid "No date of birth provided" msgid "No date of birth provided"
msgstr "Pas de date de naissance renseignée" msgstr "Pas de date de naissance renseignée"
#: counter/views.py:611 #: counter/views.py:603
msgid "You have not enough money to buy all the basket" msgid "You have not enough money to buy all the basket"
msgstr "Vous n'avez pas assez d'argent pour acheter le panier" msgstr "Vous n'avez pas assez d'argent pour acheter le panier"
#: counter/views.py:759 #: counter/views.py:751
msgid "Counter administration" msgid "Counter administration"
msgstr "Administration des comptoirs" msgstr "Administration des comptoirs"
#: counter/views.py:761 #: counter/views.py:753
msgid "Stocks" msgid "Stocks"
msgstr "Stocks" msgstr "Stocks"
#: counter/views.py:780 #: counter/views.py:772
msgid "Product types" msgid "Product types"
msgstr "Types de produit" msgstr "Types de produit"
#: counter/views.py:1030 #: counter/views.py:1022
msgid "10 cents" msgid "10 cents"
msgstr "10 centimes" msgstr "10 centimes"
#: counter/views.py:1031 #: counter/views.py:1023
msgid "20 cents" msgid "20 cents"
msgstr "20 centimes" msgstr "20 centimes"
#: counter/views.py:1032 #: counter/views.py:1024
msgid "50 cents" msgid "50 cents"
msgstr "50 centimes" msgstr "50 centimes"
#: counter/views.py:1033 #: counter/views.py:1025
msgid "1 euro" msgid "1 euro"
msgstr "1 €" msgstr "1 €"
#: counter/views.py:1034 #: counter/views.py:1026
msgid "2 euros" msgid "2 euros"
msgstr "2 €" msgstr "2 €"
#: counter/views.py:1035 #: counter/views.py:1027
msgid "5 euros" msgid "5 euros"
msgstr "5 €" msgstr "5 €"
#: counter/views.py:1036 #: counter/views.py:1028
msgid "10 euros" msgid "10 euros"
msgstr "10 €" msgstr "10 €"
#: counter/views.py:1037 #: counter/views.py:1029
msgid "20 euros" msgid "20 euros"
msgstr "20 €" msgstr "20 €"
#: counter/views.py:1038 #: counter/views.py:1030
msgid "50 euros" msgid "50 euros"
msgstr "50 €" msgstr "50 €"
#: counter/views.py:1040 #: counter/views.py:1032
msgid "100 euros" msgid "100 euros"
msgstr "100 €" msgstr "100 €"
#: counter/views.py:1043 counter/views.py:1049 counter/views.py:1055 #: counter/views.py:1035 counter/views.py:1041 counter/views.py:1047
#: counter/views.py:1061 counter/views.py:1067 #: counter/views.py:1053 counter/views.py:1059
msgid "Check amount" msgid "Check amount"
msgstr "Montant du chèque" msgstr "Montant du chèque"
#: counter/views.py:1046 counter/views.py:1052 counter/views.py:1058 #: counter/views.py:1038 counter/views.py:1044 counter/views.py:1050
#: counter/views.py:1064 counter/views.py:1070 #: counter/views.py:1056 counter/views.py:1062
msgid "Check quantity" msgid "Check quantity"
msgstr "Nombre de chèque" msgstr "Nombre de chèque"
#: counter/views.py:1684 #: counter/views.py:1676
msgid "people(s)" msgid "people(s)"
msgstr "personne(s)" msgstr "personne(s)"
@ -4371,37 +4371,37 @@ msgstr "Votre panier est vide"
msgid "%(name)s : this product does not exist." msgid "%(name)s : this product does not exist."
msgstr "%(name)s : ce produit n'existe pas." msgstr "%(name)s : ce produit n'existe pas."
#: eboutic/forms.py:150 #: eboutic/forms.py:134
#, python-format #, python-format
msgid "%(name)s : this product does not exist or may no longer be available." msgid "%(name)s : this product does not exist or may no longer be available."
msgstr "%(name)s : ce produit n'existe pas ou n'est peut-être plus disponible." msgstr "%(name)s : ce produit n'existe pas ou n'est peut-être plus disponible."
#: eboutic/forms.py:157 #: eboutic/forms.py:141
#, python-format #, python-format
msgid "You cannot buy %(nbr)d %(name)s." msgid "You cannot buy %(nbr)d %(name)s."
msgstr "Vous ne pouvez pas acheter %(nbr)d %(name)s." msgstr "Vous ne pouvez pas acheter %(nbr)d %(name)s."
#: eboutic/models.py:237 #: eboutic/models.py:241
msgid "validated" msgid "validated"
msgstr "validé" msgstr "validé"
#: eboutic/models.py:247 #: eboutic/models.py:251
msgid "Invoice already validated" msgid "Invoice already validated"
msgstr "Facture déjà validée" msgstr "Facture déjà validée"
#: eboutic/models.py:285 #: eboutic/models.py:289
msgid "product id" msgid "product id"
msgstr "ID du produit" msgstr "ID du produit"
#: eboutic/models.py:286 #: eboutic/models.py:290
msgid "product name" msgid "product name"
msgstr "nom du produit" msgstr "nom du produit"
#: eboutic/models.py:287 #: eboutic/models.py:291
msgid "product type id" msgid "product type id"
msgstr "id du type du produit" msgstr "id du type du produit"
#: eboutic/models.py:304 #: eboutic/models.py:308
msgid "basket" msgid "basket"
msgstr "panier" msgstr "panier"
@ -4467,18 +4467,18 @@ msgstr ""
"Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "Vous devez renseigner vos coordonnées de facturation si vous voulez payer "
"par carte bancaire" "par carte bancaire"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:111 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:112
msgid "Pay with credit card" msgid "Pay with credit card"
msgstr "Payer avec une carte bancaire" msgstr "Payer avec une carte bancaire"
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:115 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:116
msgid "" msgid ""
"AE account payment disabled because your basket contains refilling items." "AE account payment disabled because your basket contains refilling items."
msgstr "" msgstr ""
"Paiement par compte AE désactivé parce que votre panier contient des bons de " "Paiement par compte AE désactivé parce que votre panier contient des bons de "
"rechargement." "rechargement."
#: eboutic/templates/eboutic/eboutic_makecommand.jinja:120 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:121
msgid "Pay with Sith account" msgid "Pay with Sith account"
msgstr "Payer avec un compte AE" msgstr "Payer avec un compte AE"

View File

@ -22,13 +22,13 @@
# #
# #
from django.urls import re_path from django.urls import path
from matmat.views import * from matmat.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^$", SearchNormalFormView.as_view(), name="search"), path("", SearchNormalFormView.as_view(), name="search"),
re_path(r"^reverse$", SearchReverseFormView.as_view(), name="search_reverse"), path("reverse/", SearchReverseFormView.as_view(), name="search_reverse"),
re_path(r"^quick$", SearchQuickFormView.as_view(), name="search_quick"), path("quick/", SearchQuickFormView.as_view(), name="search_quick"),
re_path(r"^clear$", SearchClearFormView.as_view(), name="search_clear"), path("clear/", SearchClearFormView.as_view(), name="search_clear"),
] ]

View File

@ -732,9 +732,9 @@ class UVSearchTest(TestCase):
[ [
{ {
"id": 1, "id": 1,
"absolute_url": "/pedagogy/uv/1", "absolute_url": "/pedagogy/uv/1/",
"update_url": "/pedagogy/uv/1/edit", "update_url": "/pedagogy/uv/1/edit/",
"delete_url": "/pedagogy/uv/1/delete", "delete_url": "/pedagogy/uv/1/delete/",
"code": "PA00", "code": "PA00",
"author": 0, "author": 0,
"credit_type": "OM", "credit_type": "OM",

View File

@ -22,33 +22,33 @@
# #
# #
from django.urls import re_path from django.urls import path
from pedagogy.views import * from pedagogy.views import *
urlpatterns = [ urlpatterns = [
# Urls displaying the actual application for visitors # Urls displaying the actual application for visitors
re_path(r"^$", UVListView.as_view(), name="guide"), path("", UVListView.as_view(), name="guide"),
re_path(r"^uv/(?P<uv_id>[0-9]+)$", UVDetailFormView.as_view(), name="uv_detail"), path("uv/<int:uv_id>/", UVDetailFormView.as_view(), name="uv_detail"),
re_path( path(
r"^comment/(?P<comment_id>[0-9]+)/edit$", "comment/<int:comment_id>/edit/",
UVCommentUpdateView.as_view(), UVCommentUpdateView.as_view(),
name="comment_update", name="comment_update",
), ),
re_path( path(
r"^comment/(?P<comment_id>[0-9]+)/delete$", "comment/<int:comment_id>/delete/",
UVCommentDeleteView.as_view(), UVCommentDeleteView.as_view(),
name="comment_delete", name="comment_delete",
), ),
re_path( path(
r"^comment/(?P<comment_id>[0-9]+)/report$", "comment/<int:comment_id>/report/",
UVCommentReportCreateView.as_view(), UVCommentReportCreateView.as_view(),
name="comment_report", name="comment_report",
), ),
# Moderation # Moderation
re_path(r"^moderation$", UVModerationFormView.as_view(), name="moderation"), path("moderation/", UVModerationFormView.as_view(), name="moderation"),
# Administration : Create Update Delete Edit # Administration : Create Update Delete Edit
re_path(r"^uv/create$", UVCreateView.as_view(), name="uv_create"), path("uv/create/", UVCreateView.as_view(), name="uv_create"),
re_path(r"^uv/(?P<uv_id>[0-9]+)/delete$", UVDeleteView.as_view(), name="uv_delete"), path("uv/<int:uv_id>/delete/", UVDeleteView.as_view(), name="uv_delete"),
re_path(r"^uv/(?P<uv_id>[0-9]+)/edit$", UVUpdateView.as_view(), name="uv_update"), path("uv/<int:uv_id>/edit/", UVUpdateView.as_view(), name="uv_update"),
] ]

View File

@ -21,7 +21,212 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
from datetime import date, timedelta
from django.core.management import call_command
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
# Create your tests here. from club.models import Club
from core.models import User, RealGroup
from counter.models import Customer, Product, Selling, Counter, Refilling
from subscription.models import Subscription
class MergeUserTest(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
call_command("populate")
cls.ae = Club.objects.get(unix_name="ae")
cls.eboutic = Counter.objects.get(name="Eboutic")
cls.barbar = Product.objects.get(code="BARB")
cls.barbar.selling_price = 2
cls.barbar.save()
cls.root = User.objects.get(username="root")
def setUp(self) -> None:
super().setUp()
self.to_keep = User(username="to_keep", password="plop", email="u.1@utbm.fr")
self.to_delete = User(username="to_del", password="plop", email="u.2@utbm.fr")
self.to_keep.save()
self.to_delete.save()
self.client.login(username="root", password="plop")
def test_simple(self):
self.to_delete.first_name = "Biggus"
self.to_keep.last_name = "Dickus"
self.to_keep.nick_name = "B'ian"
self.to_keep.address = "Jerusalem"
self.to_delete.parent_address = "Rome"
self.to_delete.address = "Rome"
subscribers = RealGroup.objects.get(name="Subscribers")
mde_admin = RealGroup.objects.get(name="MDE admin")
sas_admin = RealGroup.objects.get(name="SAS admin")
self.to_keep.groups.add(subscribers.id)
self.to_delete.groups.add(mde_admin.id)
self.to_keep.groups.add(sas_admin.id)
self.to_delete.groups.add(sas_admin.id)
self.to_delete.save()
self.to_keep.save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.assertRedirects(res, self.to_keep.get_absolute_url())
self.assertFalse(User.objects.filter(pk=self.to_delete.pk).exists())
self.to_keep = User.objects.get(pk=self.to_keep.pk)
# fields of to_delete should be assigned to to_keep
# if they were not set beforehand
self.assertEqual("Biggus", self.to_keep.first_name)
self.assertEqual("Dickus", self.to_keep.last_name)
self.assertEqual("B'ian", self.to_keep.nick_name)
self.assertEqual("Jerusalem", self.to_keep.address)
self.assertEqual("Rome", self.to_keep.parent_address)
self.assertEqual(3, self.to_keep.groups.count())
groups = list(self.to_keep.groups.all())
expected = [subscribers, mde_admin, sas_admin]
self.assertCountEqual(groups, expected)
def test_both_subscribers_and_with_account(self):
Customer(user=self.to_keep, account_id="11000l", amount=0).save()
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
Refilling(
amount=10,
operator=self.root,
customer=self.to_keep.customer,
counter=self.eboutic,
).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_delete.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_keep.customer,
seller=self.root,
unit_price=2,
quantity=2,
payment_method="SITH_ACCOUNT",
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_delete.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
today = date.today()
# both subscriptions began last month and shall end in 5 months
Subscription(
member=self.to_keep,
subscription_type="un-semestre",
payment_method="EBOUTIC",
subscription_start=today - timedelta(30),
subscription_end=today + timedelta(5 * 30),
).save()
Subscription(
member=self.to_delete,
subscription_type="un-semestre",
payment_method="EBOUTIC",
subscription_start=today - timedelta(30),
subscription_end=today + timedelta(5 * 30),
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_keep had 10€ at first and bought 2 barbar worth 2€ each
# to_delete had 20€ and bought 4 barbar
# total should be 10 - 4 + 20 - 8 = 18
self.assertAlmostEqual(18, self.to_keep.customer.amount, delta=0.0001)
self.assertEqual(2, self.to_keep.customer.buyings.count())
self.assertEqual(2, self.to_keep.customer.refillings.count())
self.assertTrue(self.to_keep.is_subscribed)
# to_keep had 5 months of subscription remaining and received
# 5 more months from to_delete, so he should be subscribed for 10 months
self.assertEqual(
today + timedelta(10 * 30),
self.to_keep.subscriptions.order_by("subscription_end")
.last()
.subscription_end,
)
def test_godfathers(self):
users = list(User.objects.all()[:4])
self.to_keep.godfathers.add(users[0])
self.to_keep.godchildren.add(users[1])
self.to_delete.godfathers.add(users[2])
self.to_delete.godfathers.add(self.to_keep)
self.to_delete.godchildren.add(users[3])
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.assertRedirects(res, self.to_keep.get_absolute_url())
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertCountEqual(list(self.to_keep.godfathers.all()), [users[0], users[2]])
self.assertCountEqual(
list(self.to_keep.godchildren.all()), [users[1], users[3]]
)
def test_keep_has_no_account(self):
Customer(user=self.to_delete, account_id="12000m", amount=0).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_delete.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_delete.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_delete had 20€ and bought 4 barbar worth 2€ each
# total should be 20 - 8 = 12
self.assertTrue(hasattr(self.to_keep, "customer"))
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)
def test_delete_has_no_account(self):
Customer(user=self.to_keep, account_id="12000m", amount=0).save()
Refilling(
amount=20,
operator=self.root,
customer=self.to_keep.customer,
counter=self.eboutic,
).save()
Selling(
label="barbar",
counter=self.eboutic,
club=self.ae,
product=self.barbar,
customer=self.to_keep.customer,
seller=self.root,
unit_price=2,
quantity=4,
payment_method="SITH_ACCOUNT",
).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data)
self.to_keep = User.objects.get(pk=self.to_keep.id)
self.assertRedirects(res, self.to_keep.get_absolute_url())
# to_keep had 20€ and bought 4 barbar worth 2€ each
# total should be 20 - 8 = 12
self.assertTrue(hasattr(self.to_keep, "customer"))
self.assertAlmostEqual(12, self.to_keep.customer.amount, delta=0.0001)

View File

@ -23,16 +23,16 @@
# #
# #
from django.urls import re_path from django.urls import path
from rootplace.views import * from rootplace.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^merge$", MergeUsersView.as_view(), name="merge"), path("merge/", MergeUsersView.as_view(), name="merge"),
re_path( path(
r"^forum/messages/delete$", "forum/messages/delete/",
DeleteAllForumUserMessagesView.as_view(), DeleteAllForumUserMessagesView.as_view(),
name="delete_forum_messages", name="delete_forum_messages",
), ),
re_path(r"^logs$", OperationLogListView.as_view(), name="operation_logs"), path("logs/", OperationLogListView.as_view(), name="operation_logs"),
] ]

View File

@ -23,72 +23,114 @@
# #
# #
from django.utils.translation import gettext as _ from ajax_select.fields import AutoCompleteSelectField
from django.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse
from django import forms from django import forms
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import ListView
from django.views.generic.edit import FormView
from ajax_select.fields import AutoCompleteSelectField from core.models import User, OperationLog, SithFile
from core.views import CanEditPropMixin from core.views import CanEditPropMixin
from core.models import User, OperationLog
from counter.models import Customer from counter.models import Customer
from forum.models import ForumMessageMeta from forum.models import ForumMessageMeta
def merge_users(u1, u2): def __merge_subscriptions(u1: User, u2: User):
u1.nick_name = u1.nick_name or u2.nick_name """
u1.date_of_birth = u1.date_of_birth or u2.date_of_birth Give all the subscriptions of the second user to first one
u1.home = u1.home or u2.home If some subscriptions are still active, update their end date
u1.sex = u1.sex or u2.sex to increase the overall subscription time of the first user.
u1.pronouns = u1.pronouns or u2.pronouns
u1.tshirt_size = u1.tshirt_size or u2.tshirt_size Some examples :
u1.role = u1.role or u2.role - if u1 is not subscribed, his subscription end date become the one of u2
u1.department = u1.department or u2.department - if u1 is subscribed but not u2, nothing happen
u1.dpt_option = u1.dpt_option or u2.dpt_option - if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
u1.semester = u1.semester or u2.semester he shall then be subscribed for 5 months
u1.quote = u1.quote or u2.quote """
u1.school = u1.school or u2.school last_subscription = (
u1.promo = u1.promo or u2.promo u1.subscriptions.filter(
u1.forum_signature = u1.forum_signature or u2.forum_signature subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
u1.second_email = u1.second_email or u2.second_email )
u1.phone = u1.phone or u2.phone .order_by("subscription_end")
u1.parent_phone = u1.parent_phone or u2.parent_phone .last()
u1.address = u1.address or u2.address )
u1.parent_address = u1.parent_address or u2.parent_address if last_subscription is not None:
subscription_end = last_subscription.subscription_end
for subscription in u2.subscriptions.filter(
subscription_end__gte=timezone.now()
):
subscription.subscription_start = subscription_end
if subscription.subscription_start > timezone.now().date():
remaining = subscription.subscription_end - timezone.now().date()
else:
remaining = (
subscription.subscription_end - subscription.subscription_start
)
subscription_end += remaining
subscription.subscription_end = subscription_end
subscription.save()
u2.subscriptions.all().update(member=u1)
def __merge_pictures(u1: User, u2: User) -> None:
SithFile.objects.filter(owner=u2).update(owner=u1)
if u1.profile_pict is None and u2.profile_pict is not None:
u1.profile_pict, u2.profile_pict = u2.profile_pict, None
if u1.scrub_pict is None and u2.scrub_pict is not None:
u1.scrub_pict, u2.scrub_pict = u2.scrub_pict, None
if u1.avatar_pict is None and u2.avatar_pict is not None:
u1.avatar_pict, u2.avatar_pict = u2.avatar_pict, None
u2.save()
u1.save() u1.save()
for u in u2.godfathers.all():
u1.godfathers.add(u)
def merge_users(u1: User, u2: User) -> User:
"""
Merge u2 into u1
This means that u1 shall receive everything that belonged to u2 :
- pictures
- refills of the sith account
- purchases of any item bought on the eboutic or the counters
- subscriptions
- godfathers
- godchildren
If u1 had no account id, he shall receive the one of u2.
If u1 and u2 were both in the middle of a subscription, the remaining
durations stack
If u1 had no profile picture, he shall receive the one of u2
"""
for field in u1._meta.fields:
if not field.is_relation and not u1.__dict__[field.name]:
u1.__dict__[field.name] = u2.__dict__[field.name]
for group in u2.groups.all():
u1.groups.add(group.id)
for godfather in u2.godfathers.exclude(id=u1.id):
u1.godfathers.add(godfather)
for godchild in u2.godchildren.exclude(id=u1.id):
u1.godchildren.add(godchild)
__merge_subscriptions(u1, u2)
__merge_pictures(u1, u2)
u2.invoices.all().update(user=u1)
c_src = Customer.objects.filter(user=u2).first()
if c_src is not None:
c_dest, created = Customer.get_or_create(u1)
c_src.refillings.update(customer=c_dest)
c_src.buyings.update(customer=c_dest)
c_dest.recompute_amount()
if created:
# swap the account numbers, so that the user keep
# the id he is accustomed to
tmp_id = c_src.account_id
# delete beforehand in order not to have a unique constraint violation
c_src.delete()
c_dest.account_id = tmp_id
u1.save() u1.save()
for i in u2.invoices.all(): u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
for f in i._meta.local_fields: # I have sadly not found anything better :/
if f.name == "date":
f.auto_now = False
u1.invoices.add(i)
u1.save()
s1 = User.objects.filter(id=u1.id).first()
s2 = User.objects.filter(id=u2.id).first()
for s in s2.subscriptions.all():
s1.subscriptions.add(s)
s1.save()
c1 = Customer.objects.filter(user__id=u1.id).first()
c2 = Customer.objects.filter(user__id=u2.id).first()
if c1 and c2:
for r in c2.refillings.all():
c1.refillings.add(r)
c1.save()
for s in c2.buyings.all():
c1.buyings.add(s)
c1.save()
elif c2 and not c1:
c2.user = u1
c1 = c2
c1.save()
c1.recompute_amount()
u2.delete()
return u1 return u1
@ -128,9 +170,8 @@ class MergeUsersView(FormView):
form_class = MergeForm form_class = MergeForm
def dispatch(self, request, *arg, **kwargs): def dispatch(self, request, *arg, **kwargs):
res = super(MergeUsersView, self).dispatch(request, *arg, **kwargs)
if request.user.is_root: if request.user.is_root:
return res return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied raise PermissionDenied
def form_valid(self, form): def form_valid(self, form):
@ -140,7 +181,7 @@ class MergeUsersView(FormView):
return super(MergeUsersView, self).form_valid(form) return super(MergeUsersView, self).form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse("core:user_profile", kwargs={"user_id": self.final_user.id}) return self.final_user.get_absolute_url()
class DeleteAllForumUserMessagesView(FormView): class DeleteAllForumUserMessagesView(FormView):

View File

@ -22,40 +22,36 @@
# #
# #
from django.urls import re_path from django.urls import path
from sas.views import * from sas.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^$", SASMainView.as_view(), name="main"), path("", SASMainView.as_view(), name="main"),
re_path(r"^moderation$", ModerationView.as_view(), name="moderation"), path("moderation/", ModerationView.as_view(), name="moderation"),
re_path(r"^album/(?P<album_id>[0-9]+)$", AlbumView.as_view(), name="album"), path("album/<int:album_id>/", AlbumView.as_view(), name="album"),
re_path( path(
r"^album/(?P<album_id>[0-9]+)/upload$", "album/<int:album_id>/upload/",
AlbumUploadView.as_view(), AlbumUploadView.as_view(),
name="album_upload", name="album_upload",
), ),
re_path( path("album/<int:album_id>/edit/", AlbumEditView.as_view(), name="album_edit"),
r"^album/(?P<album_id>[0-9]+)/edit$", AlbumEditView.as_view(), name="album_edit" path("album/<int:album_id>/preview/", send_album, name="album_preview"),
), path("picture/<int:picture_id>/", PictureView.as_view(), name="picture"),
re_path(r"^album/(?P<album_id>[0-9]+)/preview$", send_album, name="album_preview"), path(
re_path(r"^picture/(?P<picture_id>[0-9]+)$", PictureView.as_view(), name="picture"), "picture/<int:picture_id>/edit/",
re_path(
r"^picture/(?P<picture_id>[0-9]+)/edit$",
PictureEditView.as_view(), PictureEditView.as_view(),
name="picture_edit", name="picture_edit",
), ),
re_path(r"^picture/(?P<picture_id>[0-9]+)/download$", send_pict, name="download"), path("picture/<int:picture_id>/download/", send_pict, name="download"),
re_path( path(
r"^picture/(?P<picture_id>[0-9]+)/download/compressed$", "picture/<int:picture_id>/download/compressed/",
send_compressed, send_compressed,
name="download_compressed", name="download_compressed",
), ),
re_path( path(
r"^picture/(?P<picture_id>[0-9]+)/download/thumb$", "picture/<int:picture_id>/download/thumb/",
send_thumb, send_thumb,
name="download_thumb", name="download_thumb",
), ),
# re_path(r'^album/new$', AlbumCreateView.as_view(), name='album_new'),
# re_path(r'^(?P<club_id>[0-9]+)/$', ClubView.as_view(), name='club_view'),
] ]

View File

@ -291,6 +291,10 @@ SITH_URL = "my.url.git.an"
SITH_NAME = "Sith website" SITH_NAME = "Sith website"
SITH_TWITTER = "@ae_utbm" SITH_TWITTER = "@ae_utbm"
# Enable experimental features
# Enable/Disable the galaxy button on user profile (urls stay activated)
SITH_ENABLE_GALAXY = False
# AE configuration # AE configuration
# 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_ID = 1

View File

@ -23,67 +23,65 @@
# #
# #
from django.urls import include, re_path from django.urls import path
from stock.views import * from stock.views import *
urlpatterns = [ urlpatterns = [
# Stock re_paths # Stock re_paths
re_path( path("new/counter/<int:counter_id>/", StockCreateView.as_view(), name="new"),
r"^new/counter/(?P<counter_id>[0-9]+)$", StockCreateView.as_view(), name="new" path("edit/<int:stock_id>/", StockEditView.as_view(), name="edit"),
), path("list/", StockListView.as_view(), name="list"),
re_path(r"^edit/(?P<stock_id>[0-9]+)$", StockEditView.as_view(), name="edit"),
re_path(r"^list$", StockListView.as_view(), name="list"),
# StockItem re_paths # StockItem re_paths
re_path(r"^(?P<stock_id>[0-9]+)$", StockItemList.as_view(), name="items_list"), path("<int:stock_id>/", StockItemList.as_view(), name="items_list"),
re_path( path(
r"^(?P<stock_id>[0-9]+)/stock_item/new_item$", "<int:stock_id>/stock_item/new_item/",
StockItemCreateView.as_view(), StockItemCreateView.as_view(),
name="new_item", name="new_item",
), ),
re_path( path(
r"^stock_item/(?P<item_id>[0-9]+)/edit$", "stock_item/<int:item_id>/edit/",
StockItemEditView.as_view(), StockItemEditView.as_view(),
name="edit_item", name="edit_item",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/stock_item/take_items$", "<int:stock_id>/stock_item/take_items/",
StockTakeItemsBaseFormView.as_view(), StockTakeItemsBaseFormView.as_view(),
name="take_items", name="take_items",
), ),
# ShoppingList re_paths # ShoppingList re_paths
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/list$", "<int:stock_id>/shopping_list/list/",
StockShoppingListView.as_view(), StockShoppingListView.as_view(),
name="shoppinglist_list", name="shoppinglist_list",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/create$", "<int:stock_id>/shopping_list/create/",
StockItemQuantityBaseFormView.as_view(), StockItemQuantityBaseFormView.as_view(),
name="shoppinglist_create", name="shoppinglist_create",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/items$", "<int:stock_id>/shopping_list/<int:shoppinglist_id>/items/",
StockShoppingListItemListView.as_view(), StockShoppingListItemListView.as_view(),
name="shoppinglist_items", name="shoppinglist_items",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/delete$", "<int:stock_id>/shopping_list/<int:shoppinglist_id>/delete/",
StockShoppingListDeleteView.as_view(), StockShoppingListDeleteView.as_view(),
name="shoppinglist_delete", name="shoppinglist_delete",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_done$", "<int:stock_id>/shopping_list/<int:shoppinglist_id>/set_done/",
StockShopppingListSetDone.as_view(), StockShopppingListSetDone.as_view(),
name="shoppinglist_set_done", name="shoppinglist_set_done",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_todo$", "<int:stock_id>/shopping_list/<int:shoppinglist_id>/set_todo/",
StockShopppingListSetTodo.as_view(), StockShopppingListSetTodo.as_view(),
name="shoppinglist_set_todo", name="shoppinglist_set_todo",
), ),
re_path( path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/update_stock$", "<int:stock_id>/shopping_list/<int:shoppinglist_id>/update_stock/",
StockUpdateAfterShopppingBaseFormView.as_view(), StockUpdateAfterShopppingBaseFormView.as_view(),
name="update_after_shopping", name="update_after_shopping",
), ),

View File

@ -24,7 +24,6 @@
from datetime import date, timedelta from datetime import date, timedelta
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -101,8 +100,8 @@ class Subscription(models.Model):
super(Subscription, self).save() super(Subscription, self).save()
from counter.models import Customer from counter.models import Customer
if not Customer.objects.filter(user=self.member).exists(): _, created = Customer.get_or_create(self.member)
Customer.new_for_user(self.member).save() if created:
form = PasswordResetForm({"email": self.member.email}) form = PasswordResetForm({"email": self.member.email})
if form.is_valid(): if form.is_valid():
form.save( form.save(
@ -166,7 +165,4 @@ class Subscription(models.Model):
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
def is_valid_now(self): def is_valid_now(self):
return ( return self.subscription_start <= date.today() <= self.subscription_end
self.subscription_start <= date.today()
and date.today() <= self.subscription_end
)

View File

@ -22,12 +22,12 @@
# #
# #
from django.urls import re_path from django.urls import path
from subscription.views import * from subscription.views import *
urlpatterns = [ urlpatterns = [
# Subscription views # Subscription views
re_path(r"^$", NewSubscription.as_view(), name="subscription"), path("", NewSubscription.as_view(), name="subscription"),
re_path(r"stats", SubscriptionsStatsView.as_view(), name="stats"), path("stats/", SubscriptionsStatsView.as_view(), name="stats"),
] ]

View File

@ -23,67 +23,65 @@
# #
# #
from django.urls import re_path from django.urls import path
from trombi.views import * from trombi.views import *
urlpatterns = [ urlpatterns = [
re_path(r"^(?P<club_id>[0-9]+)/new$", TrombiCreateView.as_view(), name="create"), path("<int:club_id>/new/", TrombiCreateView.as_view(), name="create"),
re_path( path("<int:trombi_id>/export/", TrombiExportView.as_view(), name="export"),
r"^(?P<trombi_id>[0-9]+)/export$", TrombiExportView.as_view(), name="export" path("<int:trombi_id>/edit/", TrombiEditView.as_view(), name="edit"),
), path(
re_path(r"^(?P<trombi_id>[0-9]+)/edit$", TrombiEditView.as_view(), name="edit"), "<int:trombi_id>/moderate_comments/",
re_path(
r"^(?P<trombi_id>[0-9]+)/moderate_comments$",
TrombiModerateCommentsView.as_view(), TrombiModerateCommentsView.as_view(),
name="moderate_comments", name="moderate_comments",
), ),
re_path( path(
r"^(?P<comment_id>[0-9]+)/moderate$", "<int:comment_id>/moderate/",
TrombiModerateCommentView.as_view(), TrombiModerateCommentView.as_view(),
name="moderate_comment", name="moderate_comment",
), ),
re_path( path(
r"^user/(?P<user_id>[0-9]+)/delete$", "user/<int:user_id>/delete/",
TrombiDeleteUserView.as_view(), TrombiDeleteUserView.as_view(),
name="delete_user", name="delete_user",
), ),
re_path(r"^(?P<trombi_id>[0-9]+)$", TrombiDetailView.as_view(), name="detail"), path("<int:trombi_id>/", TrombiDetailView.as_view(), name="detail"),
re_path( path(
r"^(?P<user_id>[0-9]+)/new_comment$", "<int:user_id>/new_comment/",
TrombiCommentCreateView.as_view(), TrombiCommentCreateView.as_view(),
name="new_comment", name="new_comment",
), ),
re_path( path(
r"^(?P<user_id>[0-9]+)/profile$", "<int:user_id>/profile/",
UserTrombiProfileView.as_view(), UserTrombiProfileView.as_view(),
name="user_profile", name="user_profile",
), ),
re_path( path(
r"^comment/(?P<comment_id>[0-9]+)/edit$", "comment/<int:comment_id>/edit/",
TrombiCommentEditView.as_view(), TrombiCommentEditView.as_view(),
name="edit_comment", name="edit_comment",
), ),
re_path(r"^tools$", UserTrombiToolsView.as_view(), name="user_tools"), path("tools/", UserTrombiToolsView.as_view(), name="user_tools"),
re_path(r"^profile$", UserTrombiEditProfileView.as_view(), name="profile"), path("profile/", UserTrombiEditProfileView.as_view(), name="profile"),
re_path(r"^pictures$", UserTrombiEditPicturesView.as_view(), name="pictures"), path("pictures/", UserTrombiEditPicturesView.as_view(), name="pictures"),
re_path( path(
r"^reset_memberships$", "reset_memberships/",
UserTrombiResetClubMembershipsView.as_view(), UserTrombiResetClubMembershipsView.as_view(),
name="reset_memberships", name="reset_memberships",
), ),
re_path( path(
r"^membership/(?P<membership_id>[0-9]+)/edit$", "membership/<int:membership_id>/edit/",
UserTrombiEditMembershipView.as_view(), UserTrombiEditMembershipView.as_view(),
name="edit_membership", name="edit_membership",
), ),
re_path( path(
r"^membership/(?P<membership_id>[0-9]+)/delete$", "membership/<int:membership_id>/delete/",
UserTrombiDeleteMembershipView.as_view(), UserTrombiDeleteMembershipView.as_view(),
name="delete_membership", name="delete_membership",
), ),
re_path( path(
r"^membership/(?P<user_id>[0-9]+)/create$", "membership/<int:user_id>/create/",
UserTrombiAddMembershipView.as_view(), UserTrombiAddMembershipView.as_view(),
name="create_membership", name="create_membership",
), ),