mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-05 07:39:21 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dabbce4df | |||
| e811aeaecd | |||
| 549a778be0 | |||
| 78b24dc1e7 | |||
| ebf0196bef | |||
| 362b9eea06 | |||
| 3b3e33ed80 |
@@ -392,6 +392,30 @@ class ClubRoleForm(forms.ModelForm):
|
|||||||
self.instance.order = cleaned_data["ORDER"] - 1
|
self.instance.order = cleaned_data["ORDER"] - 1
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
def save(self, commit=True): # noqa: FBT002
|
||||||
|
instance: ClubRole = super().save(commit=commit)
|
||||||
|
if commit and "is_board" in self.changed_data:
|
||||||
|
# if the role was moved from board to simple member,
|
||||||
|
# remove all users with that role from the club board group.
|
||||||
|
# If the role became a board role, add users with
|
||||||
|
# that role to the club board group.
|
||||||
|
group_id = instance.club.board_group_id
|
||||||
|
if self.cleaned_data["is_board"]:
|
||||||
|
User.groups.through.objects.bulk_create(
|
||||||
|
[
|
||||||
|
User.groups.through(user_id=u, group_id=group_id)
|
||||||
|
for u in Membership.objects.ongoing()
|
||||||
|
.filter(role=instance)
|
||||||
|
.values_list("user_id", flat=True)
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
User.groups.through.objects.filter(
|
||||||
|
user__memberships__role=instance, group_id=group_id
|
||||||
|
).delete()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class ClubRoleCreateForm(forms.ModelForm):
|
class ClubRoleCreateForm(forms.ModelForm):
|
||||||
"""Form to create a club role.
|
"""Form to create a club role.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pytest
|
|||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
from model_bakery import baker, seq
|
from model_bakery import baker, seq
|
||||||
from model_bakery.recipe import Recipe
|
from model_bakery.recipe import Recipe
|
||||||
from pytest_django.asserts import assertRedirects
|
from pytest_django.asserts import assertRedirects
|
||||||
@@ -239,7 +240,7 @@ class TestClubRoleUpdate(TestCase):
|
|||||||
|
|
||||||
def test_president_moves_itself_out_of_the_presidency(self):
|
def test_president_moves_itself_out_of_the_presidency(self):
|
||||||
"""Test that if the user moves its own role out of the presidency,
|
"""Test that if the user moves its own role out of the presidency,
|
||||||
then it's redirected to another page and loses access to the update page."""
|
then it loses access to the update page."""
|
||||||
self.payload["roles-0-is_presidency"] = False
|
self.payload["roles-0-is_presidency"] = False
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
res = self.client.post(self.url, data=self.payload)
|
res = self.client.post(self.url, data=self.payload)
|
||||||
@@ -251,3 +252,29 @@ class TestClubRoleUpdate(TestCase):
|
|||||||
|
|
||||||
res = self.client.get(self.url)
|
res = self.client.get(self.url)
|
||||||
assert res.status_code == 403
|
assert res.status_code == 403
|
||||||
|
|
||||||
|
def test_role_stops_being_board(self):
|
||||||
|
"""Test that if a role stops being a board role,
|
||||||
|
its users lose the club board group."""
|
||||||
|
self.payload["roles-0-is_board"] = False
|
||||||
|
self.payload["roles-0-is_presidency"] = False
|
||||||
|
self.payload["roles-1-is_board"] = False
|
||||||
|
formset = ClubRoleFormSet(data=self.payload, instance=self.club)
|
||||||
|
assert formset.is_valid()
|
||||||
|
formset.save()
|
||||||
|
assert not self.user.groups.contains(self.club.board_group)
|
||||||
|
|
||||||
|
def test_role_becomes_board(self):
|
||||||
|
"""Test that if a role becomes a board role,
|
||||||
|
its active users get the club board group"""
|
||||||
|
members = [
|
||||||
|
baker.make(Membership, club=self.club, role=self.roles[0], end_date=None),
|
||||||
|
baker.make(Membership, club=self.club, role=self.roles[0], end_date=now()),
|
||||||
|
]
|
||||||
|
self.payload["roles-2-is_board"] = True
|
||||||
|
formset = ClubRoleFormSet(data=self.payload, instance=self.club)
|
||||||
|
assert formset.is_valid()
|
||||||
|
formset.save()
|
||||||
|
# the second membership is finished, so its user shouldn't get the role
|
||||||
|
assert members[0].user.groups.contains(self.club.board_group)
|
||||||
|
assert not members[1].user.groups.contains(self.club.board_group)
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ details.accordion>.accordion-content {
|
|||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
padding: .75em 1.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin animation($selector) {
|
@mixin animation($selector) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { RecursivePartial, TomSettings } from "tom-select/dist/types/types";
|
import type { RecursivePartial, TomSettings } from "tom-select/src/types";
|
||||||
import { AutoCompleteSelectBase } from "#core:core/components/ajax-select-base.ts";
|
import { AutoCompleteSelectBase } from "#core:core/components/ajax-select-base";
|
||||||
import { registerComponent } from "#core:utils/web-components.ts";
|
import { registerComponent } from "#core:utils/web-components";
|
||||||
|
|
||||||
const productParsingRegex = /^(\d+x)?(.*)/i;
|
const productParsingRegex = /^(\d+x)?(.*)/i;
|
||||||
const codeParsingRegex = / \((\w+)\)$/;
|
const codeParsingRegex = / \((\w+)\)$/;
|
||||||
@@ -63,13 +63,6 @@ export class CounterProductSelect extends AutoCompleteSelectBase {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.widget.hook("after", "onOptionSelect", () => {
|
|
||||||
/* Focus the next element if it's an input */
|
|
||||||
if (this.nextElementSibling.nodeName === "INPUT") {
|
|
||||||
(this.nextElementSibling as HTMLInputElement).focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
protected tomSelectSettings(): RecursivePartial<TomSettings> {
|
||||||
/* We disable the dropdown on focus because we're going to always autofocus the widget */
|
/* We disable the dropdown on focus because we're going to always autofocus the widget */
|
||||||
@@ -80,9 +73,7 @@ export class CounterProductSelect extends AutoCompleteSelectBase {
|
|||||||
// We need to manually set weights or it results on an inconsistent
|
// We need to manually set weights or it results on an inconsistent
|
||||||
// behavior between production and development environment
|
// behavior between production and development environment
|
||||||
searchField: [
|
searchField: [
|
||||||
// @ts-expect-error documentation says it's fine, specified type is wrong
|
|
||||||
{ field: "code", weight: 2 },
|
{ field: "code", weight: 2 },
|
||||||
// @ts-expect-error documentation says it's fine, specified type is wrong
|
|
||||||
{ field: "text", weight: 0.5 },
|
{ field: "text", weight: 0.5 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.codeField = this.$refs.codeField;
|
this.codeField = this.$refs.codeField;
|
||||||
|
this.codeField.widget.hook("after", "onOptionSelect", () => {
|
||||||
|
this.handleCode();
|
||||||
|
});
|
||||||
this.codeField.widget.focus();
|
this.codeField.widget.focus();
|
||||||
|
|
||||||
// It's quite tricky to manually apply attributes to the management part
|
// It's quite tricky to manually apply attributes to the management part
|
||||||
@@ -154,6 +157,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.addToBasket(code, quantity);
|
this.addToBasket(code, quantity);
|
||||||
}
|
}
|
||||||
this.codeField.widget.clear();
|
this.codeField.widget.clear();
|
||||||
|
this.codeField.widget.setTextboxValue("");
|
||||||
this.codeField.widget.focus();
|
this.codeField.widget.focus();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -42,7 +42,28 @@
|
|||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
.basket-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
flex: 1 2 0;
|
||||||
|
min-width: 0;
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-top: .5rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,15 @@
|
|||||||
<div class="accordion-content">
|
<div class="accordion-content">
|
||||||
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
|
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
|
||||||
|
|
||||||
<form method="post" action=""
|
<form method="post" action="" @submit.prevent="handleCode">
|
||||||
class="code_form" @submit.prevent="handleCode">
|
|
||||||
|
|
||||||
<counter-product-select name="code" x-ref="codeField" autofocus required placeholder="{% trans %}Select a product...{% endtrans %}">
|
<counter-product-select
|
||||||
|
name="code"
|
||||||
|
x-ref="codeField"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
placeholder="{% trans %}Select a product...{% endtrans %}"
|
||||||
|
>
|
||||||
<option value=""></option>
|
<option value=""></option>
|
||||||
<optgroup label="{% trans %}Operations{% endtrans %}">
|
<optgroup label="{% trans %}Operations{% endtrans %}">
|
||||||
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
||||||
@@ -68,13 +73,11 @@
|
|||||||
{%- for category, prices in categories.items() -%}
|
{%- for category, prices in categories.items() -%}
|
||||||
<optgroup label="{{ category }}">
|
<optgroup label="{{ category }}">
|
||||||
{%- for price in prices -%}
|
{%- for price in prices -%}
|
||||||
<option value="{{ price.id }}">{{ price.full_label }}</option>
|
<option value="{{ price.id }}">{{ price.full_label }} ({{ price.product.code }})</option>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</counter-product-select>
|
</counter-product-select>
|
||||||
|
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% for error in form.non_form_errors() %}
|
{% for error in form.non_form_errors() %}
|
||||||
@@ -102,7 +105,9 @@
|
|||||||
{{ form.management_form }}
|
{{ form.management_form }}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li x-show="getBasketSize() === 0">{% trans %}This basket is empty{% endtrans %}</li>
|
<li x-show="getBasketSize() === 0">
|
||||||
|
<em>{% trans %}This basket is empty{% endtrans %}</em>
|
||||||
|
</li>
|
||||||
<template x-for="(item, index) in Object.values(basket)" :key="item.product.price.id">
|
<template x-for="(item, index) in Object.values(basket)" :key="item.product.price.id">
|
||||||
<li>
|
<li>
|
||||||
<template x-for="error in item.errors">
|
<template x-for="error in item.errors">
|
||||||
@@ -110,19 +115,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button @click.prevent="addToBasket(item.product.price.id, -1)">-</button>
|
<div class="basket-row">
|
||||||
<span class="quantity" x-text="item.quantity"></span>
|
<div>
|
||||||
<button @click.prevent="addToBasket(item.product.price.id, 1)">+</button>
|
<button @click.prevent="addToBasket(item.product.price.id, -1)">-</button>
|
||||||
|
<span class="quantity" x-text="item.quantity"></span>
|
||||||
|
<button @click.prevent="addToBasket(item.product.price.id, 1)">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span x-text="item.product.name"></span> :
|
<span class="product-name" x-text="item.product.name"></span>
|
||||||
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
<span x-text="`${item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })} €`"></span>
|
||||||
<span x-show="item.getBonusQuantity() > 0"
|
<span x-show="item.getBonusQuantity() > 0"
|
||||||
x-text="`${item.getBonusQuantity()} x P`"></span>
|
x-text="`${item.getBonusQuantity()} x P`"></span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="remove-item"
|
class="remove-item"
|
||||||
@click.prevent="removeFromBasket(item.product.price.id)"
|
@click.prevent="removeFromBasket(item.product.price.id)"
|
||||||
><i class="fa fa-trash-can delete-action"></i></button>
|
><i class="fa fa-trash-can delete-action"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ dependencies = [
|
|||||||
"django-honeypot>=1.3.0,<2",
|
"django-honeypot>=1.3.0,<2",
|
||||||
"pydantic-extra-types>=2.11.1,<3.0.0",
|
"pydantic-extra-types>=2.11.1,<3.0.0",
|
||||||
"ical>=12.0.0,<14.0.0",
|
"ical>=12.0.0,<14.0.0",
|
||||||
"redis[hiredis]>=3.3.1,<8.0.0",
|
"redis[hiredis]>=8.0.0,<9.0.0",
|
||||||
"environs[django]>=15.0.1,<16",
|
"environs[django]>=15.0.1,<16",
|
||||||
"requests>=2.34.2,<3.0.0",
|
"requests>=2.34.2,<3.0.0",
|
||||||
"honcho>=2.0.0",
|
"honcho>=2.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user