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 *
urlpatterns = [
# Accounting types
re_path(
r"^simple_type$",
path(
"simple_type/",
SimplifiedAccountingTypeListView.as_view(),
name="simple_type_list",
),
re_path(
r"^simple_type/create$",
path(
"simple_type/create/",
SimplifiedAccountingTypeCreateView.as_view(),
name="simple_type_new",
),
re_path(
r"^simple_type/(?P<type_id>[0-9]+)/edit$",
path(
"simple_type/<int:type_id>/edit/",
SimplifiedAccountingTypeEditView.as_view(),
name="simple_type_edit",
),
# Accounting types
re_path(r"^type$", AccountingTypeListView.as_view(), name="type_list"),
re_path(r"^type/create$", AccountingTypeCreateView.as_view(), name="type_new"),
re_path(
r"^type/(?P<type_id>[0-9]+)/edit$",
path("type/", AccountingTypeListView.as_view(), name="type_list"),
path("type/create/", AccountingTypeCreateView.as_view(), name="type_new"),
path(
"type/<int:type_id>/edit/",
AccountingTypeEditView.as_view(),
name="type_edit",
),
# Bank accounts
re_path(r"^$", BankAccountListView.as_view(), name="bank_list"),
re_path(r"^bank/create$", BankAccountCreateView.as_view(), name="bank_new"),
re_path(
r"^bank/(?P<b_account_id>[0-9]+)$",
path("", BankAccountListView.as_view(), name="bank_list"),
path("bank/create", BankAccountCreateView.as_view(), name="bank_new"),
path(
"bank/<int:b_account_id>/",
BankAccountDetailView.as_view(),
name="bank_details",
),
re_path(
r"^bank/(?P<b_account_id>[0-9]+)/edit$",
path(
"bank/<int:b_account_id>/edit/",
BankAccountEditView.as_view(),
name="bank_edit",
),
re_path(
r"^bank/(?P<b_account_id>[0-9]+)/delete$",
path(
"bank/<int:b_account_id>/delete/",
BankAccountDeleteView.as_view(),
name="bank_delete",
),
# Club accounts
re_path(r"^club/create$", ClubAccountCreateView.as_view(), name="club_new"),
re_path(
r"^club/(?P<c_account_id>[0-9]+)$",
path("club/create/", ClubAccountCreateView.as_view(), name="club_new"),
path(
"club/<int:c_account_id>/",
ClubAccountDetailView.as_view(),
name="club_details",
),
re_path(
r"^club/(?P<c_account_id>[0-9]+)/edit$",
path(
"club/<int:c_account_id>/edit/",
ClubAccountEditView.as_view(),
name="club_edit",
),
re_path(
r"^club/(?P<c_account_id>[0-9]+)/delete$",
path(
"club/<int:c_account_id>/delete/",
ClubAccountDeleteView.as_view(),
name="club_delete",
),
# Journals
re_path(r"^journal/create$", JournalCreateView.as_view(), name="journal_new"),
re_path(
r"^journal/(?P<j_id>[0-9]+)$",
path("journal/create/", JournalCreateView.as_view(), name="journal_new"),
path(
"journal/<int:j_id>/",
JournalDetailView.as_view(),
name="journal_details",
),
re_path(
r"^journal/(?P<j_id>[0-9]+)/edit$",
path(
"journal/<int:j_id>/edit/",
JournalEditView.as_view(),
name="journal_edit",
),
re_path(
r"^journal/(?P<j_id>[0-9]+)/delete$",
path(
"journal/<int:j_id>/delete/",
JournalDeleteView.as_view(),
name="journal_delete",
),
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/nature$",
path(
"journal/<int:j_id>/statement/nature/",
JournalNatureStatementView.as_view(),
name="journal_nature_statement",
),
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/person$",
path(
"journal/<int:j_id>/statement/person/",
JournalPersonStatementView.as_view(),
name="journal_person_statement",
),
re_path(
r"^journal/(?P<j_id>[0-9]+)/statement/accounting$",
path(
"journal/<int:j_id>/statement/accounting/",
JournalAccountingStatementView.as_view(),
name="journal_accounting_statement",
),
# Operations
re_path(
r"^operation/create/(?P<j_id>[0-9]+)$",
path(
"operation/create/<int:j_id>/",
OperationCreateView.as_view(),
name="op_new",
),
re_path(
r"^operation/(?P<op_id>[0-9]+)$", OperationEditView.as_view(), name="op_edit"
),
re_path(
r"^operation/(?P<op_id>[0-9]+)/pdf$", OperationPDFView.as_view(), name="op_pdf"
),
path("operation/<int:op_id>/", OperationEditView.as_view(), name="op_edit"),
path("operation/<int:op_id>/pdf/", OperationPDFView.as_view(), name="op_pdf"),
# Companies
re_path(r"^company/list$", CompanyListView.as_view(), name="co_list"),
re_path(r"^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/list/", CompanyListView.as_view(), name="co_list"),
path("company/create/", CompanyCreateView.as_view(), name="co_new"),
path("company/<int:co_id>/", CompanyEditView.as_view(), name="co_edit"),
# Labels
re_path(r"^label/new$", LabelCreateView.as_view(), name="label_new"),
re_path(
r"^label/(?P<clubaccount_id>[0-9]+)$",
path("label/new/", LabelCreateView.as_view(), name="label_new"),
path(
"label/<int:clubaccount_id>/",
LabelListView.as_view(),
name="label_list",
),
re_path(
r"^label/(?P<label_id>[0-9]+)/edit$", LabelEditView.as_view(), name="label_edit"
),
re_path(
r"^label/(?P<label_id>[0-9]+)/delete$",
path("label/<int:label_id>/edit/", LabelEditView.as_view(), name="label_edit"),
path(
"label/<int:label_id>/delete/",
LabelDeleteView.as_view(),
name="label_delete",
),
# 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 *
urlpatterns = [
re_path(r"^$", ClubListView.as_view(), name="club_list"),
re_path(r"^new$", ClubCreateView.as_view(), name="club_new"),
re_path(r"^stats$", ClubStatView.as_view(), name="club_stats"),
re_path(r"^(?P<club_id>[0-9]+)/$", ClubView.as_view(), name="club_view"),
re_path(
r"^(?P<club_id>[0-9]+)/rev/(?P<rev_id>[0-9]+)/$",
path("", ClubListView.as_view(), name="club_list"),
path("new/", ClubCreateView.as_view(), name="club_new"),
path("stats/", ClubStatView.as_view(), name="club_stats"),
path("<int:club_id>/", ClubView.as_view(), name="club_view"),
path(
"<int:club_id>/rev/<int:rev_id>/",
ClubRevView.as_view(),
name="club_view_rev",
),
re_path(
r"^(?P<club_id>[0-9]+)/hist$", ClubPageHistView.as_view(), name="club_hist"
),
re_path(r"^(?P<club_id>[0-9]+)/edit$", ClubEditView.as_view(), name="club_edit"),
re_path(
r"^(?P<club_id>[0-9]+)/edit/page$",
path("<int:club_id>/hist/", ClubPageHistView.as_view(), name="club_hist"),
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
path(
"<int:club_id>/edit/page/",
ClubPageEditView.as_view(),
name="club_edit_page",
),
re_path(
r"^(?P<club_id>[0-9]+)/members$", ClubMembersView.as_view(), name="club_members"
),
re_path(
r"^(?P<club_id>[0-9]+)/elderlies$",
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
path(
"<int:club_id>/elderlies/",
ClubOldMembersView.as_view(),
name="club_old_members",
),
re_path(
r"^(?P<club_id>[0-9]+)/sellings$",
path(
"<int:club_id>/sellings/",
ClubSellingView.as_view(),
name="club_sellings",
),
re_path(
r"^(?P<club_id>[0-9]+)/sellings/csv$",
path(
"<int:club_id>/sellings/csv/",
ClubSellingCSVView.as_view(),
name="sellings_csv",
),
re_path(
r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"
),
re_path(r"^(?P<club_id>[0-9]+)/tools$", ClubToolsView.as_view(), name="tools"),
re_path(
r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"
),
re_path(
r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
path("<int:club_id>/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"),
path(
"<int:mailing_id>/mailing/generate/",
MailingAutoGenerationView.as_view(),
name="mailing_generate",
),
re_path(
r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
path(
"<int:mailing_id>/mailing/delete/",
MailingDeleteView.as_view(),
name="mailing_delete",
),
re_path(
r"^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$",
path(
"<int:mailing_subscription_id>/mailing/delete/subscription/",
MailingSubscriptionDeleteView.as_view(),
name="mailing_subscription_delete",
),
re_path(
r"^membership/(?P<membership_id>[0-9]+)/set_old$",
path(
"membership/<int:membership_id>/set_old/",
MembershipSetOldView.as_view(),
name="membership_set_old",
),
re_path(
r"^membership/(?P<membership_id>[0-9]+)/delete$",
path(
"membership/<int:membership_id>/delete/",
MembershipDeleteView.as_view(),
name="membership_delete",
),
re_path(
r"^(?P<club_id>[0-9]+)/poster$", PosterListView.as_view(), name="poster_list"
),
re_path(
r"^(?P<club_id>[0-9]+)/poster/create$",
path("<int:club_id>/poster/", PosterListView.as_view(), name="poster_list"),
path(
"<int:club_id>/poster/create/",
PosterCreateView.as_view(),
name="poster_create",
),
re_path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/edit$",
path(
"<int:club_id>/poster/<int:poster_id>/edit/",
PosterEditView.as_view(),
name="poster_edit",
),
re_path(
r"^(?P<club_id>[0-9]+)/poster/(?P<poster_id>[0-9]+)/delete$",
path(
"<int:club_id>/poster/<int:poster_id>/delete/",
PosterDeleteView.as_view(),
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 com.views import *
urlpatterns = [
re_path(r"^sith/edit/alert$", AlertMsgEditView.as_view(), name="alert_edit"),
re_path(r"^sith/edit/info$", InfoMsgEditView.as_view(), name="info_edit"),
re_path(
r"^sith/edit/weekmail_destinations$",
path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"),
path("sith/edit/info/", InfoMsgEditView.as_view(), name="info_edit"),
path(
"sith/edit/weekmail_destinations/",
WeekmailDestinationEditView.as_view(),
name="weekmail_destinations",
),
re_path(r"^weekmail$", WeekmailEditView.as_view(), name="weekmail"),
re_path(
r"^weekmail/preview$", WeekmailPreviewView.as_view(), name="weekmail_preview"
),
re_path(
r"^weekmail/new_article$",
path("weekmail/", WeekmailEditView.as_view(), name="weekmail"),
path("weekmail/preview/", WeekmailPreviewView.as_view(), name="weekmail_preview"),
path(
"weekmail/new_article/",
WeekmailArticleCreateView.as_view(),
name="weekmail_article",
),
re_path(
r"^weekmail/article/(?P<article_id>[0-9]+)/delete$",
path(
"weekmail/article/<int:article_id>/delete/",
WeekmailArticleDeleteView.as_view(),
name="weekmail_article_delete",
),
re_path(
r"^weekmail/article/(?P<article_id>[0-9]+)/edit$",
path(
"weekmail/article/<int:article_id>/edit/",
WeekmailArticleEditView.as_view(),
name="weekmail_article_edit",
),
re_path(r"^news$", NewsListView.as_view(), name="news_list"),
re_path(r"^news/admin$", NewsAdminListView.as_view(), name="news_admin_list"),
re_path(r"^news/create$", NewsCreateView.as_view(), name="news_new"),
re_path(
r"^news/(?P<news_id>[0-9]+)/delete$",
path("news/", NewsListView.as_view(), name="news_list"),
path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"),
path("news/create/", NewsCreateView.as_view(), name="news_new"),
path(
"news/<int:news_id>/delete/",
NewsDeleteView.as_view(),
name="news_delete",
),
re_path(
r"^news/(?P<news_id>[0-9]+)/moderate$",
path(
"news/<int:news_id>/moderate/",
NewsModerateView.as_view(),
name="news_moderate",
),
re_path(
r"^news/(?P<news_id>[0-9]+)/edit$", NewsEditView.as_view(), name="news_edit"
),
re_path(
r"^news/(?P<news_id>[0-9]+)$", NewsDetailView.as_view(), name="news_detail"
),
re_path(r"^mailings$", MailingListAdminView.as_view(), name="mailing_admin"),
re_path(
r"^mailings/(?P<mailing_id>[0-9]+)/moderate$",
path("news/<int:news_id>/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"),
path(
"mailings/<int:mailing_id>/moderate/",
MailingModerateView.as_view(),
name="mailing_moderate",
),
re_path(
r"^mailings/(?P<mailing_id>[0-9]+)/delete$",
path(
"mailings/<int:mailing_id>/delete/",
MailingDeleteView.as_view(redirect_page="com:mailing_admin"),
name="mailing_delete",
),
re_path(r"^poster$", PosterListView.as_view(), name="poster_list"),
re_path(r"^poster/create$", PosterCreateView.as_view(), name="poster_create"),
re_path(
r"^poster/(?P<poster_id>[0-9]+)/edit$",
path("poster/", PosterListView.as_view(), name="poster_list"),
path("poster/create/", PosterCreateView.as_view(), name="poster_create"),
path(
"poster/<int:poster_id>/edit/",
PosterEditView.as_view(),
name="poster_edit",
),
re_path(
r"^poster/(?P<poster_id>[0-9]+)/delete$",
path(
"poster/<int:poster_id>/delete/",
PosterDeleteView.as_view(),
name="poster_delete",
),
re_path(
r"^poster/moderate$",
path(
"poster/moderate/",
PosterModerateListView.as_view(),
name="poster_moderate_list",
),
re_path(
r"^poster/(?P<object_id>[0-9]+)/moderate$",
path(
"poster/<int:object_id>/moderate/",
PosterModerateView.as_view(),
name="poster_moderate",
),
re_path(r"^screen$", ScreenListView.as_view(), name="screen_list"),
re_path(r"^screen/create$", ScreenCreateView.as_view(), name="screen_create"),
re_path(
r"^screen/(?P<screen_id>[0-9]+)/slideshow$",
path("screen/", ScreenListView.as_view(), name="screen_list"),
path("screen/create/", ScreenCreateView.as_view(), name="screen_create"),
path(
"screen/<int:screen_id>/slideshow/",
ScreenSlideshowView.as_view(),
name="screen_slideshow",
),
re_path(
r"^screen/(?P<screen_id>[0-9]+)/edit$",
path(
"screen/<int:screen_id>/edit/",
ScreenEditView.as_view(),
name="screen_edit",
),
re_path(
r"^screen/(?P<screen_id>[0-9]+)/delete$",
path(
"screen/<int:screen_id>/delete/",
ScreenDeleteView.as_view(),
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;
}
#bar_ui {
#bar-ui {
padding: 0.4em;
display: flex;
flex-wrap: wrap;

View File

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

View File

@ -65,9 +65,15 @@
<div id="header_bar">
<ul id="header_bars_infos">
{% 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>
<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(): %}
<i class="fa fa-question" style="color: #f39c12"></i>
{% elif bar.is_open(): %}

View File

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

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

View File

@ -207,9 +207,7 @@ class UserTabsMixin(TabedViewMixin):
"name": _("Pictures"),
},
]
if (
False and self.request.user.was_subscribed
): # TODO: display galaxy once it's ready
if settings.SITH_ENABLE_GALAXY and self.request.user.was_subscribed:
tab_list.append(
{
"url": reverse("galaxy:user", kwargs={"user_id": user.id}),
@ -330,16 +328,16 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
return kwargs
def DeleteUserGodfathers(request, user_id, godfather_id, is_father):
user = User.objects.get(id=user_id)
if (user == request.user) or request.user.is_root or request.user.is_board_member:
ud = get_object_or_404(User, id=godfather_id)
if is_father == "True":
user.godfathers.remove(ud)
def delete_user_godfather(request, user_id, godfather_id, is_father):
user_is_admin = request.user.is_root or request.user.is_board_member
if user_id != request.user.id and not user_is_admin:
raise PermissionDenied()
user = get_object_or_404(User, id=user_id)
to_remove = get_object_or_404(User, id=godfather_id)
if is_father:
user.godfathers.remove(to_remove)
else:
user.godchildren.remove(ud)
else:
raise PermissionDenied
user.godchildren.remove(to_remove)
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.
#
#
from __future__ import annotations
from django.db.models import Sum, F
from typing import Tuple
from django.db import models
from django.db.models import OuterRef, Exists
from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
@ -57,7 +62,7 @@ class Customer(models.Model):
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
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)
class Meta:
@ -86,20 +91,32 @@ class Customer(models.Model):
about the relation between a User (not a Customer,
don't mix them) and a Product.
"""
return self.user.subscriptions.last() and (
date.today()
- self.user.subscriptions.order_by("subscription_end")
.last()
.subscription_end
) < timedelta(days=90)
subscription = self.user.subscriptions.order_by("subscription_end").last()
time_diff = date.today() - subscription.subscription_end
return subscription is not None and time_diff < timedelta(days=90)
@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
The account if is automatically generated and the amount set at 0
Work in pretty much the same way as the usual get_or_create method,
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 = (
Customer.objects.order_by(Length("account_id"), "account_id")
.values("account_id")
@ -107,14 +124,19 @@ class Customer(models.Model):
)
if account_id is None:
# legacy from the old site
return cls(user=user, account_id="1504a", amount=0)
account_id = account_id["account_id"]
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)
account = cls.objects.create(user=user, account_id="1504a")
return account, True
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):
"""
@ -127,11 +149,15 @@ class Customer(models.Model):
super(Customer, self).save(*args, **kwargs)
def recompute_amount(self):
self.amount = 0
for r in self.refillings.all():
self.amount += r.amount
for s in self.buyings.filter(payment_method="SITH_ACCOUNT"):
self.amount -= s.quantity * s.unit_price
refillings = self.refillings.aggregate(sum=Sum(F("amount")))["sum"]
self.amount = refillings if refillings is not None else 0
purchases = (
self.buyings.filter(payment_method="SITH_ACCOUNT")
.annotate(amount=F("quantity") * F("unit_price"))
.aggregate(sum=Sum(F("amount")))
)["sum"]
if purchases is not None:
self.amount -= purchases
self.save()
def get_absolute_url(self):
@ -313,6 +339,32 @@ class Product(models.Model):
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):
name = models.CharField(_("name"), max_length=30)
club = models.ForeignKey(
@ -337,6 +389,8 @@ class Counter(models.Model):
)
token = models.CharField(_("token"), max_length=30, null=True, blank=True)
objects = CounterQuerySet.as_manager()
class Meta:
verbose_name = _("counter")
@ -451,11 +505,11 @@ class Counter(models.Model):
Show if the counter authorize the refilling with physic money
"""
if (
self.id in SITH_COUNTER_OFFICES
): # If the counter is the counters 'AE' or 'BdF', the refiling are authorized
if self.type != "BAR":
return False
if self.id in SITH_COUNTER_OFFICES:
# If the counter is either 'AE' or 'BdF', refills are authorized
return True
is_ae_member = False
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
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

@ -5,16 +5,22 @@
{{ 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 %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %}
<h4 id="click_interface">{{ counter }}</h4>
<div id="bar_ui">
<div id="bar-ui" x-data="counter">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
@ -28,7 +34,7 @@
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% 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'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
@ -49,59 +55,67 @@
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5>
<div>
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %}
{% raw %}
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</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">
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
<form method="post" action=""
class="code_form" @submit.prevent="handle_code">
{% csrf_token %}
<input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/>
<label for="code_field"></label>
<input type="text" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<template x-for="error in errors">
<div class="alert alert-red" x-text="error">
</div>
</template>
<p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul>
<li v-for="p_info,p_id in basket">
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<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">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> - </button>
<input type="hidden" name="product_id" :value="id">
<input type="submit" value="-"/>
</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">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<form method="post" action="" class="inline add_product_form"
@submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> + </button>
<input type="hidden" name="product_id" :value="id">
<input type="submit" value="+">
</form>
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
</li>
<span x-text="products[id].name"></span> :
<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>
<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>
<div class="important">
<p v-for="error in errors"><strong>{{ error }}</strong></p>
</div>
{% endraw %}
<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 %}
<input type="hidden" name="action" value="finish">
<input type="submit" value="{% trans %}Finish{% endtrans %}"/>
</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 %}
<input type="hidden" name="action" value="cancel">
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
@ -110,7 +124,8 @@
{% if (counter.type == 'BAR' and barmens_can_refill) %}
<h5>{% trans %}Refilling{% endtrans %}</h5>
<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 %}
{{ refill_form.as_p() }}
<input type="hidden" name="action" value="refill">
@ -130,95 +145,45 @@
<div id="cat_{{ category|slugify }}">
<h5>{{ category }}</h5>
{% for p in categories[category] -%}
{% set file = None %}
{% if p.icon %}
{% set file = p.icon.url %}
{% 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">
<form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
class="form_button add_product_form" @submit.prevent="handle_action">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
<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>
{%- endfor %}
</div>
{%- endfor %}
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script>
$( function() {
/* Vue.JS dynamic form */
const click_form_vue = Vue.createApp({
data() {
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
const csrf_token = "{{ csrf_token }}";
const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const basket = {{ request.session["basket"]|tojson }};
const products = {
{%- for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}",
special_selling_price: "{{ p.special_selling_price }}",
price: {{ p.price }},
},
{%- endfor %}
},
basket: {{ request.session["basket"]|tojson }},
errors: [],
}
},
methods: {
sum_basket() {
var vm = this;
var total = 0;
for(idx in vm.basket) {
var item = vm.basket[idx];
console.log(item);
total += item["qty"] * item["price"];
}
return total / 100;
},
handle_code(event) {
var vm = this;
var code = $(event.target).find("#code_field").val().toUpperCase();
console.log("Code:");
console.log(code);
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
$(event.target).submit();
} else {
vm.handle_action(event);
}
},
handle_action(event) {
var vm = this;
var payload = $(event.target).serialize();
$.ajax({
type: 'post',
dataType: 'json',
data: payload,
success: function(response) {
vm.basket = response.basket;
vm.errors = [];
},
error: function(error) {
vm.basket = error.responseJSON.basket;
vm.errors = error.responseJSON.errors;
}
});
$('form.code_form #code_field').val("").focus();
}
}
}).mount('#bar_ui');
/* Autocompletion in the code field */
var products_autocomplete = [
{%- endfor -%}
};
const products_autocomplete = [
{% for p in products -%}
{
value: "{{ p.code }}",
@ -227,41 +192,5 @@ $( function() {
},
{%- endfor %}
];
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 %}

View File

@ -23,6 +23,7 @@
#
import json
import re
import string
from django.test import TestCase
from django.urls import reverse
@ -42,7 +43,7 @@ class CounterTest(TestCase):
self.foyer = Counter.objects.get(id=2)
def test_full_click(self):
response = self.client.post(
self.client.post(
reverse("counter:login", kwargs={"counter_id": self.mde.id}),
{"username": self.skia.username, "password": "plop"},
)
@ -62,13 +63,12 @@ class CounterTest(TestCase):
reverse("counter:details", kwargs={"counter_id": self.mde.id}),
{"code": "4000k", "counter_token": counter_token},
)
location = response.get("location")
counter_url = response.get("location")
response = self.client.get(response.get("location"))
self.assertTrue(">Richard Batsbak</" in str(response.content))
self.client.post(
location,
counter_url,
{
"action": "refill",
"amount": "5",
@ -76,17 +76,27 @@ class CounterTest(TestCase):
"bank": "OTHER",
},
)
self.client.post(location, {"action": "code", "code": "BARB"})
self.client.post(location, {"action": "add_product", "product_id": "4"})
self.client.post(location, {"action": "del_product", "product_id": "4"})
self.client.post(location, {"action": "code", "code": "2xdeco"})
self.client.post(location, {"action": "code", "code": "1xbarb"})
response = self.client.post(location, {"action": "code", "code": "fin"})
self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
self.client.post(
counter_url, "action=add_product&product_id=4", content_type="text/xml"
)
self.client.post(
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_content = response_get.content.decode("utf-8")
self.assertTrue("<li>2 x Barbar" in str(response_content))
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue("2 x Barbar" in str(response_content))
self.assertTrue("2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue(
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
in str(response_content)
@ -98,7 +108,7 @@ class CounterTest(TestCase):
)
response = self.client.post(
location,
counter_url,
{
"action": "refill",
"amount": "5",
@ -108,7 +118,7 @@ class CounterTest(TestCase):
)
self.assertTrue(response.status_code == 200)
response = self.client.post(
self.client.post(
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
{"username": self.krophil.username, "password": "plop"},
)
@ -125,10 +135,10 @@ class CounterTest(TestCase):
reverse("counter:details", kwargs={"counter_id": self.foyer.id}),
{"code": "4000k", "counter_token": counter_token},
)
location = response.get("location")
counter_url = response.get("location")
response = self.client.post(
location,
counter_url,
{
"action": "refill",
"amount": "5",
@ -138,13 +148,27 @@ class CounterTest(TestCase):
)
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):
def setUp(self):
call_command("populate")
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
response = self.client.get(reverse("counter:stats", args=[self.counter.id]))
self.assertTrue(response.status_code == 403)
@ -745,18 +769,30 @@ class StudentCardTest(TestCase):
self.assertEqual(response.status_code, 403)
class AccountIdTest(TestCase):
class CustomerAccountIdTest(TestCase):
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_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_c, amount=0, account_id="12345f")
def test_create_customer(self):
user_d = User.objects.create(username="d", password="plop")
customer_d = Customer.new_for_user(user_d)
customer_d.save()
number = customer_d.account_id[:-1]
customer, created = Customer.get_or_create(user_d)
account_id = customer.account_id
number = account_id[:-1]
self.assertTrue(created)
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 *
urlpatterns = [
re_path(r"^(?P<counter_id>[0-9]+)$", CounterMain.as_view(), name="details"),
re_path(
r"^(?P<counter_id>[0-9]+)/click/(?P<user_id>[0-9]+)$",
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
path(
"<int:counter_id>/click/<int:user_id>/",
CounterClick.as_view(),
name="click",
),
re_path(
r"^(?P<counter_id>[0-9]+)/last_ops$",
path(
"<int:counter_id>/last_ops/",
CounterLastOperationsView.as_view(),
name="last_ops",
),
re_path(
r"^(?P<counter_id>[0-9]+)/cash_summary$",
path(
"<int:counter_id>/cash_summary/",
CounterCashSummaryView.as_view(),
name="cash_summary",
),
re_path(
r"^(?P<counter_id>[0-9]+)/activity$",
path(
"<int:counter_id>/activity/",
CounterActivityView.as_view(),
name="activity",
),
re_path(r"^(?P<counter_id>[0-9]+)/stats$", CounterStatView.as_view(), name="stats"),
re_path(r"^(?P<counter_id>[0-9]+)/login$", CounterLogin.as_view(), name="login"),
re_path(r"^(?P<counter_id>[0-9]+)/logout$", CounterLogout.as_view(), name="logout"),
re_path(
r"^eticket/(?P<selling_id>[0-9]+)/pdf$",
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
path("<int:counter_id>/login/", CounterLogin.as_view(), name="login"),
path("<int:counter_id>/logout/", CounterLogout.as_view(), name="logout"),
path(
"eticket/<int:selling_id>/pdf/",
EticketPDFView.as_view(),
name="eticket_pdf",
),
re_path(
r"^customer/(?P<customer_id>[0-9]+)/card/add$",
path(
"customer/<int:customer_id>/card/add/",
StudentCardFormView.as_view(),
name="add_student_card",
),
re_path(
r"^customer/(?P<customer_id>[0-9]+)/card/delete/(?P<card_id>[0-9]+)/$",
path(
"customer/<int:customer_id>/card/delete/<int:card_id>/",
StudentCardDeleteView.as_view(),
name="delete_student_card",
),
@ -76,76 +76,76 @@ urlpatterns = [
edit_billing_info,
name="edit_billing_info",
),
re_path(r"^admin/(?P<counter_id>[0-9]+)$", CounterEditView.as_view(), name="admin"),
re_path(
r"^admin/(?P<counter_id>[0-9]+)/prop$",
path("admin/<int:counter_id>/", CounterEditView.as_view(), name="admin"),
path(
"admin/<int:counter_id>/prop/",
CounterEditPropView.as_view(),
name="prop_admin",
),
re_path(r"^admin$", CounterListView.as_view(), name="admin_list"),
re_path(r"^admin/new$", CounterCreateView.as_view(), name="new"),
re_path(
r"^admin/delete/(?P<counter_id>[0-9]+)$",
path("admin/", CounterListView.as_view(), name="admin_list"),
path("admin/new/", CounterCreateView.as_view(), name="new"),
path(
"admin/delete/<int:counter_id>/",
CounterDeleteView.as_view(),
name="delete",
),
re_path(r"^admin/invoices_call$", InvoiceCallView.as_view(), name="invoices_call"),
re_path(
r"^admin/cash_summary/list$",
path("admin/invoices_call/", InvoiceCallView.as_view(), name="invoices_call"),
path(
"admin/cash_summary/list/",
CashSummaryListView.as_view(),
name="cash_summary_list",
),
re_path(
r"^admin/cash_summary/(?P<cashsummary_id>[0-9]+)$",
path(
"admin/cash_summary/<int:cashsummary_id>/",
CashSummaryEditView.as_view(),
name="cash_summary_edit",
),
re_path(r"^admin/product/list$", ProductListView.as_view(), name="product_list"),
re_path(
r"^admin/product/list_archived$",
path("admin/product/list/", ProductListView.as_view(), name="product_list"),
path(
"admin/product/list_archived/",
ProductArchivedListView.as_view(),
name="product_list_archived",
),
re_path(r"^admin/product/create$", ProductCreateView.as_view(), name="new_product"),
re_path(
r"^admin/product/(?P<product_id>[0-9]+)$",
path("admin/product/create/", ProductCreateView.as_view(), name="new_product"),
path(
"admin/product/<int:product_id>/",
ProductEditView.as_view(),
name="product_edit",
),
re_path(
r"^admin/producttype/list$",
path(
"admin/producttype/list/",
ProductTypeListView.as_view(),
name="producttype_list",
),
re_path(
r"^admin/producttype/create$",
path(
"admin/producttype/create/",
ProductTypeCreateView.as_view(),
name="new_producttype",
),
re_path(
r"^admin/producttype/(?P<type_id>[0-9]+)$",
path(
"admin/producttype/<int:type_id>/",
ProductTypeEditView.as_view(),
name="producttype_edit",
),
re_path(r"^admin/eticket/list$", EticketListView.as_view(), name="eticket_list"),
re_path(r"^admin/eticket/new$", EticketCreateView.as_view(), name="new_eticket"),
re_path(
r"^admin/eticket/(?P<eticket_id>[0-9]+)$",
path("admin/eticket/list/", EticketListView.as_view(), name="eticket_list"),
path("admin/eticket/new/", EticketCreateView.as_view(), name="new_eticket"),
path(
"admin/eticket/<int:eticket_id>/",
EticketEditView.as_view(),
name="edit_eticket",
),
re_path(
r"^admin/selling/(?P<selling_id>[0-9]+)/delete$",
path(
"admin/selling/<int:selling_id>/delete/",
SellingDeleteView.as_view(),
name="selling_delete",
),
re_path(
r"^admin/refilling/(?P<refilling_id>[0-9]+)/delete$",
path(
"admin/refilling/<int:refilling_id>/delete/",
RefillingDeleteView.as_view(),
name="refilling_delete",
),
re_path(
r"^admin/(?P<counter_id>[0-9]+)/refillings$",
path(
"admin/<int:counter_id>/refillings/",
CounterRefillingListView.as_view(),
name="refilling_list",
),

View File

@ -22,8 +22,10 @@
#
#
import json
from urllib.parse import parse_qs
from django.contrib.auth.decorators import login_required
from django.db.models import F
from django.shortcuts import get_object_or_404
from django.http import Http404
from django.core.exceptions import PermissionDenied
@ -300,7 +302,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
current_tab = "counter"
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": []}
status = HTTPStatus.OK
@ -395,42 +397,40 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["not_valid_student_card_uid"] = False
if self.object.type != "BAR":
self.operator = request.user
elif self.is_barman_price():
elif self.customer_is_barman():
self.operator = self.customer.user
else:
self.operator = self.object.get_random_barman()
if "add_product" in request.POST["action"]:
action = self.request.POST.get("action", None)
if action is None:
action = parse_qs(request.body.decode()).get("action", [""])[0]
if action == "add_product":
self.add_product(request)
elif "add_student_card" in request.POST["action"]:
elif action == "add_student_card":
self.add_student_card(request)
elif "del_product" in request.POST["action"]:
elif action == "del_product":
self.del_product(request)
elif "refill" in request.POST["action"]:
elif action == "refill":
self.refill(request)
elif "code" in request.POST["action"]:
elif action == "code":
return self.parse_code(request)
elif "cancel" in request.POST["action"]:
elif action == "cancel":
return self.cancel(request)
elif "finish" in request.POST["action"]:
elif action == "finish":
return self.finish(request)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def is_barman_price(self):
if self.object.type == "BAR" and self.customer.user.id in [
s.id for s in self.object.get_barmen_list()
]:
return True
else:
return False
def customer_is_barman(self) -> bool:
barmen = self.object.barmen_list
return self.object.type == "BAR" and self.customer.user in barmen
def get_product(self, pid):
return Product.objects.filter(pk=int(pid)).first()
def get_price(self, pid):
p = self.get_product(pid)
if self.is_barman_price():
if self.customer_is_barman():
price = p.special_selling_price
else:
price = p.selling_price
@ -475,13 +475,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
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):
"""
Add a product to the basket
q is the quantity passed as 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)
price = self.get_price(pid)
total = self.sum_basket(request)
@ -563,7 +572,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def del_product(self, request):
"""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)
if pid in request.session["basket"]:
if (
@ -576,30 +585,29 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
request.session["basket"][pid]["qty"] -= 1
if request.session["basket"][pid]["qty"] <= 0:
del request.session["basket"][pid]
else:
request.session["basket"][pid] = None
request.session.modified = True
def parse_code(self, request):
"""Parse the string entered by the barman"""
string = str(request.POST["code"]).upper()
if string == _("END"):
"""
Parse the string entered by the barman
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)
elif string == _("CAN"):
elif string == "ANN":
return self.cancel(request)
regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
m = regex.match(string)
if m is not None:
nb = m.group("nb")
code = m.group("code")
if nb is None:
nb = 1
else:
nb = int(nb)
nb = int(nb) if nb is not None else 1
p = self.object.products.filter(code=code).first()
if p is not None:
while nb > 0 and not self.add_product(request, nb, p.id):
nb -= 1
self.add_product(request, nb, p.id)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
@ -613,7 +621,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
for pid, infos in request.session["basket"].items():
# This duplicates code for DB optimization (prevent to load many times the same object)
p = Product.objects.filter(pk=pid).first()
if self.is_barman_price():
if self.customer_is_barman():
uprice = p.special_selling_price
else:
uprice = p.selling_price
@ -665,7 +673,8 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
def refill(self, request):
"""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)
if form.is_valid():
form.instance.counter = self.object
@ -674,13 +683,16 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
form.instance.save()
else:
self.refill_form = form
else:
raise PermissionDenied
def get_context_data(self, **kwargs):
"""Add customer to the context"""
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"] = {}
for product in kwargs["products"]:
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"):
raise PermissionDenied()
user = get_object_or_404(User, pk=user_id)
if not hasattr(user, "customer"):
customer = Customer.new_for_user(user)
customer.save()
else:
customer = get_object_or_404(Customer, user_id=user_id)
customer, _ = Customer.get_or_create(user)
BillingInfo.objects.create(customer=customer)
return __manage_billing_info_req(request, user_id, True)

View File

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

View File

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

View File

@ -1,57 +1,49 @@
from django.urls import re_path
from django.urls import path
from election.views import *
urlpatterns = [
re_path(r"^$", ElectionsListView.as_view(), name="list"),
re_path(r"^archived$", ElectionListArchivedView.as_view(), name="list_archived"),
re_path(r"^add$", ElectionCreateView.as_view(), name="create"),
re_path(
r"^(?P<election_id>[0-9]+)/edit$", ElectionUpdateView.as_view(), name="update"
),
re_path(
r"^(?P<election_id>[0-9]+)/delete$", ElectionDeleteView.as_view(), name="delete"
),
re_path(
r"^(?P<election_id>[0-9]+)/list/add$",
path("", ElectionsListView.as_view(), name="list"),
path("archived/", ElectionListArchivedView.as_view(), name="list_archived"),
path("add/", ElectionCreateView.as_view(), name="create"),
path("<int:election_id>/edit/", ElectionUpdateView.as_view(), name="update"),
path("<int:election_id>/delete/", ElectionDeleteView.as_view(), name="delete"),
path(
"<int:election_id>/list/add/",
ElectionListCreateView.as_view(),
name="create_list",
),
re_path(
r"^(?P<list_id>[0-9]+)/list/delete$",
path(
"<int:list_id>/list/delete/",
ElectionListDeleteView.as_view(),
name="delete_list",
),
re_path(
r"^(?P<election_id>[0-9]+)/role/create$",
path(
"<int:election_id>/role/create/",
RoleCreateView.as_view(),
name="create_role",
),
re_path(
r"^(?P<role_id>[0-9]+)/role/edit$", RoleUpdateView.as_view(), name="update_role"
),
re_path(
r"^(?P<role_id>[0-9]+)/role/delete$",
path("<int:role_id>/role/edit/", RoleUpdateView.as_view(), name="update_role"),
path(
"<int:role_id>/role/delete/",
RoleDeleteView.as_view(),
name="delete_role",
),
re_path(
r"^(?P<election_id>[0-9]+)/candidate/add$",
path(
"<int:election_id>/candidate/add/",
CandidatureCreateView.as_view(),
name="candidate",
),
re_path(
r"^(?P<candidature_id>[0-9]+)/candidate/edit$",
path(
"<int:candidature_id>/candidate/edit/",
CandidatureUpdateView.as_view(),
name="update_candidate",
),
re_path(
r"^(?P<candidature_id>[0-9]+)/candidate/delete$",
path(
"<int:candidature_id>/candidate/delete/",
CandidatureDeleteView.as_view(),
name="delete_candidate",
),
re_path(r"^(?P<election_id>[0-9]+)/vote$", VoteFormView.as_view(), name="vote"),
re_path(
r"^(?P<election_id>[0-9]+)/detail$", ElectionDetailView.as_view(), name="detail"
),
path("<int:election_id>/vote/", VoteFormView.as_view(), name="vote"),
path("<int:election_id>/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 *
urlpatterns = [
re_path(r"^$", ForumMainView.as_view(), name="main"),
re_path(r"^search/$", ForumSearchView.as_view(), name="search"),
re_path(r"^new_forum$", ForumCreateView.as_view(), name="new_forum"),
re_path(
r"^mark_all_as_read$", ForumMarkAllAsRead.as_view(), name="mark_all_as_read"
),
re_path(r"^last_unread$", ForumLastUnread.as_view(), name="last_unread"),
re_path(
r"^favorite_topics$", ForumFavoriteTopics.as_view(), name="favorite_topics"
),
re_path(r"^(?P<forum_id>[0-9]+)$", ForumDetailView.as_view(), name="view_forum"),
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$",
path("", ForumMainView.as_view(), name="main"),
path("search/", ForumSearchView.as_view(), name="search"),
path("new_forum/", ForumCreateView.as_view(), name="new_forum"),
path("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"),
path("<int:forum_id>/", ForumDetailView.as_view(), name="view_forum"),
path("<int:forum_id>/edit/", ForumEditView.as_view(), name="edit_forum"),
path("<int:forum_id>/delete/", ForumDeleteView.as_view(), name="delete_forum"),
path(
"<int:forum_id>/new_topic/",
ForumTopicCreateView.as_view(),
name="new_topic",
),
re_path(
r"^topic/(?P<topic_id>[0-9]+)$",
path(
"topic/<int:topic_id>/",
ForumTopicDetailView.as_view(),
name="view_topic",
),
re_path(
r"^topic/(?P<topic_id>[0-9]+)/edit$",
path(
"topic/<int:topic_id>/edit/",
ForumTopicEditView.as_view(),
name="edit_topic",
),
re_path(
r"^topic/(?P<topic_id>[0-9]+)/new_message$",
path(
"topic/<int:topic_id>/new_message/",
ForumMessageCreateView.as_view(),
name="new_message",
),
re_path(
r"^topic/(?P<topic_id>[0-9]+)/toggle_subscribe$",
path(
"topic/<int:topic_id>/toggle_subscribe/",
ForumTopicSubscribeView.as_view(),
name="toggle_subscribe_topic",
),
re_path(
r"^message/(?P<message_id>[0-9]+)$",
path(
"message/<int:message_id>/",
ForumMessageView.as_view(),
name="view_message",
),
re_path(
r"^message/(?P<message_id>[0-9]+)/edit$",
path(
"message/<int:message_id>/edit/",
ForumMessageEditView.as_view(),
name="edit_message",
),
re_path(
r"^message/(?P<message_id>[0-9]+)/delete$",
path(
"message/<int:message_id>/delete/",
ForumMessageDeleteView.as_view(),
name="delete_message",
),
re_path(
r"^message/(?P<message_id>[0-9]+)/undelete$",
path(
"message/<int:message_id>/undelete/",
ForumMessageUndeleteView.as_view(),
name="undelete_message",
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,33 +22,33 @@
#
#
from django.urls import re_path
from django.urls import path
from pedagogy.views import *
urlpatterns = [
# Urls displaying the actual application for visitors
re_path(r"^$", UVListView.as_view(), name="guide"),
re_path(r"^uv/(?P<uv_id>[0-9]+)$", UVDetailFormView.as_view(), name="uv_detail"),
re_path(
r"^comment/(?P<comment_id>[0-9]+)/edit$",
path("", UVListView.as_view(), name="guide"),
path("uv/<int:uv_id>/", UVDetailFormView.as_view(), name="uv_detail"),
path(
"comment/<int:comment_id>/edit/",
UVCommentUpdateView.as_view(),
name="comment_update",
),
re_path(
r"^comment/(?P<comment_id>[0-9]+)/delete$",
path(
"comment/<int:comment_id>/delete/",
UVCommentDeleteView.as_view(),
name="comment_delete",
),
re_path(
r"^comment/(?P<comment_id>[0-9]+)/report$",
path(
"comment/<int:comment_id>/report/",
UVCommentReportCreateView.as_view(),
name="comment_report",
),
# Moderation
re_path(r"^moderation$", UVModerationFormView.as_view(), name="moderation"),
path("moderation/", UVModerationFormView.as_view(), name="moderation"),
# Administration : Create Update Delete Edit
re_path(r"^uv/create$", UVCreateView.as_view(), name="uv_create"),
re_path(r"^uv/(?P<uv_id>[0-9]+)/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/create/", UVCreateView.as_view(), name="uv_create"),
path("uv/<int:uv_id>/delete/", UVDeleteView.as_view(), name="uv_delete"),
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.
#
#
from datetime import date, timedelta
from django.core.management import call_command
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 *
urlpatterns = [
re_path(r"^merge$", MergeUsersView.as_view(), name="merge"),
re_path(
r"^forum/messages/delete$",
path("merge/", MergeUsersView.as_view(), name="merge"),
path(
"forum/messages/delete/",
DeleteAllForumUserMessagesView.as_view(),
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 django.views.generic.edit import FormView
from django.views.generic import ListView
from django.urls import reverse
from ajax_select.fields import AutoCompleteSelectField
from django import forms
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.models import User, OperationLog
from counter.models import Customer
from forum.models import ForumMessageMeta
def merge_users(u1, u2):
u1.nick_name = u1.nick_name or u2.nick_name
u1.date_of_birth = u1.date_of_birth or u2.date_of_birth
u1.home = u1.home or u2.home
u1.sex = u1.sex or u2.sex
u1.pronouns = u1.pronouns or u2.pronouns
u1.tshirt_size = u1.tshirt_size or u2.tshirt_size
u1.role = u1.role or u2.role
u1.department = u1.department or u2.department
u1.dpt_option = u1.dpt_option or u2.dpt_option
u1.semester = u1.semester or u2.semester
u1.quote = u1.quote or u2.quote
u1.school = u1.school or u2.school
u1.promo = u1.promo or u2.promo
u1.forum_signature = u1.forum_signature or u2.forum_signature
u1.second_email = u1.second_email or u2.second_email
u1.phone = u1.phone or u2.phone
u1.parent_phone = u1.parent_phone or u2.parent_phone
u1.address = u1.address or u2.address
u1.parent_address = u1.parent_address or u2.parent_address
def __merge_subscriptions(u1: User, u2: User):
"""
Give all the subscriptions of the second user to first one
If some subscriptions are still active, update their end date
to increase the overall subscription time of the first user.
Some examples :
- if u1 is not subscribed, his subscription end date become the one of u2
- if u1 is subscribed but not u2, nothing happen
- if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months,
he shall then be subscribed for 5 months
"""
last_subscription = (
u1.subscriptions.filter(
subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()
)
.order_by("subscription_end")
.last()
)
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()
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()
for i in u2.invoices.all():
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()
u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE
return u1
@ -128,9 +170,8 @@ class MergeUsersView(FormView):
form_class = MergeForm
def dispatch(self, request, *arg, **kwargs):
res = super(MergeUsersView, self).dispatch(request, *arg, **kwargs)
if request.user.is_root:
return res
return super().dispatch(request, *arg, **kwargs)
raise PermissionDenied
def form_valid(self, form):
@ -140,7 +181,7 @@ class MergeUsersView(FormView):
return super(MergeUsersView, self).form_valid(form)
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):

View File

@ -22,40 +22,36 @@
#
#
from django.urls import re_path
from django.urls import path
from sas.views import *
urlpatterns = [
re_path(r"^$", SASMainView.as_view(), name="main"),
re_path(r"^moderation$", ModerationView.as_view(), name="moderation"),
re_path(r"^album/(?P<album_id>[0-9]+)$", AlbumView.as_view(), name="album"),
re_path(
r"^album/(?P<album_id>[0-9]+)/upload$",
path("", SASMainView.as_view(), name="main"),
path("moderation/", ModerationView.as_view(), name="moderation"),
path("album/<int:album_id>/", AlbumView.as_view(), name="album"),
path(
"album/<int:album_id>/upload/",
AlbumUploadView.as_view(),
name="album_upload",
),
re_path(
r"^album/(?P<album_id>[0-9]+)/edit$", AlbumEditView.as_view(), name="album_edit"
),
re_path(r"^album/(?P<album_id>[0-9]+)/preview$", send_album, name="album_preview"),
re_path(r"^picture/(?P<picture_id>[0-9]+)$", PictureView.as_view(), name="picture"),
re_path(
r"^picture/(?P<picture_id>[0-9]+)/edit$",
path("album/<int:album_id>/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"),
path(
"picture/<int:picture_id>/edit/",
PictureEditView.as_view(),
name="picture_edit",
),
re_path(r"^picture/(?P<picture_id>[0-9]+)/download$", send_pict, name="download"),
re_path(
r"^picture/(?P<picture_id>[0-9]+)/download/compressed$",
path("picture/<int:picture_id>/download/", send_pict, name="download"),
path(
"picture/<int:picture_id>/download/compressed/",
send_compressed,
name="download_compressed",
),
re_path(
r"^picture/(?P<picture_id>[0-9]+)/download/thumb$",
path(
"picture/<int:picture_id>/download/thumb/",
send_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_TWITTER = "@ae_utbm"
# Enable experimental features
# Enable/Disable the galaxy button on user profile (urls stay activated)
SITH_ENABLE_GALAXY = False
# AE configuration
# TODO: keep only that first setting, with the ID, and do the same for the other clubs
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 *
urlpatterns = [
# Stock re_paths
re_path(
r"^new/counter/(?P<counter_id>[0-9]+)$", StockCreateView.as_view(), name="new"
),
re_path(r"^edit/(?P<stock_id>[0-9]+)$", StockEditView.as_view(), name="edit"),
re_path(r"^list$", StockListView.as_view(), name="list"),
path("new/counter/<int:counter_id>/", StockCreateView.as_view(), name="new"),
path("edit/<int:stock_id>/", StockEditView.as_view(), name="edit"),
path("list/", StockListView.as_view(), name="list"),
# StockItem re_paths
re_path(r"^(?P<stock_id>[0-9]+)$", StockItemList.as_view(), name="items_list"),
re_path(
r"^(?P<stock_id>[0-9]+)/stock_item/new_item$",
path("<int:stock_id>/", StockItemList.as_view(), name="items_list"),
path(
"<int:stock_id>/stock_item/new_item/",
StockItemCreateView.as_view(),
name="new_item",
),
re_path(
r"^stock_item/(?P<item_id>[0-9]+)/edit$",
path(
"stock_item/<int:item_id>/edit/",
StockItemEditView.as_view(),
name="edit_item",
),
re_path(
r"^(?P<stock_id>[0-9]+)/stock_item/take_items$",
path(
"<int:stock_id>/stock_item/take_items/",
StockTakeItemsBaseFormView.as_view(),
name="take_items",
),
# ShoppingList re_paths
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/list$",
path(
"<int:stock_id>/shopping_list/list/",
StockShoppingListView.as_view(),
name="shoppinglist_list",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/create$",
path(
"<int:stock_id>/shopping_list/create/",
StockItemQuantityBaseFormView.as_view(),
name="shoppinglist_create",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/items$",
path(
"<int:stock_id>/shopping_list/<int:shoppinglist_id>/items/",
StockShoppingListItemListView.as_view(),
name="shoppinglist_items",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/delete$",
path(
"<int:stock_id>/shopping_list/<int:shoppinglist_id>/delete/",
StockShoppingListDeleteView.as_view(),
name="shoppinglist_delete",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_done$",
path(
"<int:stock_id>/shopping_list/<int:shoppinglist_id>/set_done/",
StockShopppingListSetDone.as_view(),
name="shoppinglist_set_done",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/set_todo$",
path(
"<int:stock_id>/shopping_list/<int:shoppinglist_id>/set_todo/",
StockShopppingListSetTodo.as_view(),
name="shoppinglist_set_todo",
),
re_path(
r"^(?P<stock_id>[0-9]+)/shopping_list/(?P<shoppinglist_id>[0-9]+)/update_stock$",
path(
"<int:stock_id>/shopping_list/<int:shoppinglist_id>/update_stock/",
StockUpdateAfterShopppingBaseFormView.as_view(),
name="update_after_shopping",
),

View File

@ -24,7 +24,6 @@
from datetime import date, timedelta
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core.exceptions import ValidationError
@ -101,8 +100,8 @@ class Subscription(models.Model):
super(Subscription, self).save()
from counter.models import Customer
if not Customer.objects.filter(user=self.member).exists():
Customer.new_for_user(self.member).save()
_, created = Customer.get_or_create(self.member)
if created:
form = PasswordResetForm({"email": self.member.email})
if form.is_valid():
form.save(
@ -166,7 +165,4 @@ class Subscription(models.Model):
return user.is_in_group(settings.SITH_MAIN_BOARD_GROUP) or user.is_root
def is_valid_now(self):
return (
self.subscription_start <= date.today()
and date.today() <= self.subscription_end
)
return self.subscription_start <= 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 *
urlpatterns = [
# Subscription views
re_path(r"^$", NewSubscription.as_view(), name="subscription"),
re_path(r"stats", SubscriptionsStatsView.as_view(), name="stats"),
path("", NewSubscription.as_view(), name="subscription"),
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 *
urlpatterns = [
re_path(r"^(?P<club_id>[0-9]+)/new$", TrombiCreateView.as_view(), name="create"),
re_path(
r"^(?P<trombi_id>[0-9]+)/export$", TrombiExportView.as_view(), name="export"
),
re_path(r"^(?P<trombi_id>[0-9]+)/edit$", TrombiEditView.as_view(), name="edit"),
re_path(
r"^(?P<trombi_id>[0-9]+)/moderate_comments$",
path("<int:club_id>/new/", TrombiCreateView.as_view(), name="create"),
path("<int:trombi_id>/export/", TrombiExportView.as_view(), name="export"),
path("<int:trombi_id>/edit/", TrombiEditView.as_view(), name="edit"),
path(
"<int:trombi_id>/moderate_comments/",
TrombiModerateCommentsView.as_view(),
name="moderate_comments",
),
re_path(
r"^(?P<comment_id>[0-9]+)/moderate$",
path(
"<int:comment_id>/moderate/",
TrombiModerateCommentView.as_view(),
name="moderate_comment",
),
re_path(
r"^user/(?P<user_id>[0-9]+)/delete$",
path(
"user/<int:user_id>/delete/",
TrombiDeleteUserView.as_view(),
name="delete_user",
),
re_path(r"^(?P<trombi_id>[0-9]+)$", TrombiDetailView.as_view(), name="detail"),
re_path(
r"^(?P<user_id>[0-9]+)/new_comment$",
path("<int:trombi_id>/", TrombiDetailView.as_view(), name="detail"),
path(
"<int:user_id>/new_comment/",
TrombiCommentCreateView.as_view(),
name="new_comment",
),
re_path(
r"^(?P<user_id>[0-9]+)/profile$",
path(
"<int:user_id>/profile/",
UserTrombiProfileView.as_view(),
name="user_profile",
),
re_path(
r"^comment/(?P<comment_id>[0-9]+)/edit$",
path(
"comment/<int:comment_id>/edit/",
TrombiCommentEditView.as_view(),
name="edit_comment",
),
re_path(r"^tools$", UserTrombiToolsView.as_view(), name="user_tools"),
re_path(r"^profile$", UserTrombiEditProfileView.as_view(), name="profile"),
re_path(r"^pictures$", UserTrombiEditPicturesView.as_view(), name="pictures"),
re_path(
r"^reset_memberships$",
path("tools/", UserTrombiToolsView.as_view(), name="user_tools"),
path("profile/", UserTrombiEditProfileView.as_view(), name="profile"),
path("pictures/", UserTrombiEditPicturesView.as_view(), name="pictures"),
path(
"reset_memberships/",
UserTrombiResetClubMembershipsView.as_view(),
name="reset_memberships",
),
re_path(
r"^membership/(?P<membership_id>[0-9]+)/edit$",
path(
"membership/<int:membership_id>/edit/",
UserTrombiEditMembershipView.as_view(),
name="edit_membership",
),
re_path(
r"^membership/(?P<membership_id>[0-9]+)/delete$",
path(
"membership/<int:membership_id>/delete/",
UserTrombiDeleteMembershipView.as_view(),
name="delete_membership",
),
re_path(
r"^membership/(?P<user_id>[0-9]+)/create$",
path(
"membership/<int:user_id>/create/",
UserTrombiAddMembershipView.as_view(),
name="create_membership",
),