diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 668f81d9..ccabe84a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.16.10 + rev: v0.17.2 hooks: - id: ruff-check # just check the code, and print the errors - id: ruff-check # actually fix the fixable errors, but print nothing @@ -12,7 +12,7 @@ repos: rev: v0.6.1 hooks: - id: biome-check - additional_dependencies: ["@biomejs/biome@2.5.15"] + additional_dependencies: ["@biomejs/biome@2.6.3"] - repo: https://github.com/rtts/djhtml rev: 3.0.11 hooks: diff --git a/club/models.py b/club/models.py index 6c2baec8..e9ad358e 100644 --- a/club/models.py +++ b/club/models.py @@ -238,7 +238,7 @@ class Club(models.Model): def can_be_edited_by(self, user: User) -> bool: """Method to see if that object can be edited by the given user.""" - return self.has_rights_in_club(user) + return user.has_perm("club.change_club") or self.has_rights_in_club(user) def can_roles_be_edited_by(self, user: User) -> bool: """Return True if the given user can edit the roles of this club""" diff --git a/club/templates/club/club_detail.jinja b/club/templates/club/club_detail.jinja index bc1a1c0b..8b63998b 100644 --- a/club/templates/club/club_detail.jinja +++ b/club/templates/club/club_detail.jinja @@ -26,6 +26,15 @@ {% endblock %} {% block content %} + {% if user.is_authenticated and user.can_edit(club) %} + + {% trans %}Edit{% endtrans %} + + {% endif %} +

{{ club.name }}

diff --git a/club/templates/club/club_news_list.jinja b/club/templates/club/club_news_list.jinja new file mode 100644 index 00000000..2087bcc3 --- /dev/null +++ b/club/templates/club/club_news_list.jinja @@ -0,0 +1,79 @@ +{% extends "core/base.jinja" %} + +{% block content %} + {% if club.has_rights_in_club(user) %} + + {% trans %}Create news{% endtrans %} + + {% endif %} + + {% for semester_code, semester_events in events %} +

{{ semester_code }}

+ {% for event in semester_events %} +
+ {{ event.title }} +
+ + {# Events that occur weekly from the start to the end of the semester + would take a lot of space if shown by default. + Thus, if the event has more than one date, those are + hidden by a details element. + If there is only one date, it is directly displayed. #} + {% set dates = event.dates.all()|list %} + {% if dates|length == 1 %} + + - + + + {% else %} +
+ + {% trans trimmed nb_occurrences=dates|length %} + Dates ({{ nb_occurrences }} occurrences) + {% endtrans %} + +
    + {% for d in dates %} +
  • + - + +
  • + {% endfor %} +
+
+ {% endif %} + +

{% trans %}Permalink{% endtrans %}

+

{{ request.build_absolute_uri(event.get_absolute_url())|urlize }}

+ +

{% trans %}Summary{% endtrans %}

+ {% if event.summary %} + {{ event.summary|markdown }} + {% else %} +

{% trans %}No summary{% endtrans %}

+ {% endif %} + +

{% trans %}Description{% endtrans %}

+ {% if event.content %} + {{ event.content|markdown }} + {% else %} +

{% trans %}No detailed description{% endtrans %}

+ {% endif %} +
+
+ {% endfor %} +
+ {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/club/tests/test_club_news.py b/club/tests/test_club_news.py new file mode 100644 index 00000000..75d58cb8 --- /dev/null +++ b/club/tests/test_club_news.py @@ -0,0 +1,60 @@ +from datetime import datetime + +from django.test import RequestFactory, TestCase +from django.urls import reverse +from django.utils.timezone import get_current_timezone +from model_bakery import baker + +from club.models import Club +from club.views import ClubNewsView +from com.models import News, NewsDate +from core.baker_recipes import subscriber_user +from core.models import User + + +class TestClubNews(TestCase): + @classmethod + def setUpTestData(cls): + cls.club = baker.make(Club) + cls.news = baker.make( + News, club=cls.club, is_published=True, author=baker.make(User), _quantity=5 + ) + d = datetime(year=2025, month=10, day=1, tzinfo=get_current_timezone()) + dates = [ + baker.prepare(NewsDate, news=cls.news[0], start_date=d), + baker.prepare(NewsDate, news=cls.news[0], start_date=d.replace(day=8)), + baker.prepare(NewsDate, news=cls.news[0], start_date=d.replace(day=15)), + baker.prepare(NewsDate, news=cls.news[1], start_date=d.replace(month=5)), + baker.prepare(NewsDate, news=cls.news[2], start_date=d.replace(month=4)), + baker.prepare(NewsDate, news=cls.news[3], start_date=d.replace(year=2024)), + baker.prepare(NewsDate, news=cls.news[4], start_date=d.replace(year=2020)), + ] + NewsDate.objects.bulk_create(dates) + cls.url = reverse("club:news_list", kwargs={"club_id": cls.club.id}) + + def test_load_page(self): + """Just test that the page loads.""" + self.client.force_login(subscriber_user.make()) + res = self.client.get(self.url) + assert res.status_code == 200 + + def test_page_data(self): + """Test that we display the good events.""" + # For this test, the object we want to test + # is built with `itertools.groupby`. + # However, it is consumed when rendering the template, + # making it unusable for later testing purposes, + # so we must use lower level django APIs in this test. + request = RequestFactory().get(self.url) + request.user = subscriber_user.make() + response = ClubNewsView.as_view()(request, club_id=self.club.id) + events = [ + (semester_code, list(dates)) + for semester_code, dates in response.context_data["events"] + ] + assert events == [ + ("A25", [self.news[0]]), + ("P25", [self.news[1], self.news[2]]), + ("A24", [self.news[3]]), + ("A20", [self.news[4]]), + ] diff --git a/club/tests/test_page.py b/club/tests/test_page.py index ce5746e7..7cefa677 100644 --- a/club/tests/test_page.py +++ b/club/tests/test_page.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable import pytest from aemark import markdown from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission from django.urls import reverse from model_bakery import baker from pytest_django.asserts import assertHTMLEqual, assertRedirects @@ -16,13 +17,20 @@ if TYPE_CHECKING: @pytest.mark.django_db -def test_page_display_on_club_main_page(client: Client): +@pytest.mark.parametrize("user_factory", [lambda: None, subscriber_user.make]) +def test_page_display_on_club_main_page( + client: Client, user_factory: Callable[[], User | None] +): """Test the club Page is properly displayed on the club main view""" club = baker.make(Club) + # create another previous revision, to test that we display the most recent one + baker.make(PageRev, page=club.page, revision=1) content = "# foo\nLorem ipsum dolor sit amet" - baker.make(PageRev, page=club.page, revision=1, content=content) - res = client.get(reverse("club:club_view", kwargs={"club_id": club.id})) + baker.make(PageRev, page=club.page, revision=2, content=content) + if user := user_factory(): + client.force_login(user) + res = client.get(reverse("club:club_view", kwargs={"club_id": club.id})) assert res.status_code == 200 soup = BeautifulSoup(res.text, "lxml") detail_html = soup.find(id="club-page").find(class_="markdown") @@ -30,20 +38,60 @@ def test_page_display_on_club_main_page(client: Client): @pytest.mark.django_db -def test_club_main_page_without_content(client: Client): - """Test the club view works, even if the club page is empty""" +@pytest.mark.parametrize( + ("user_factory", "can_view_tabs", "can_view_edit_button"), + [ + (lambda _: None, False, False), + (lambda _: baker.make(User), False, False), + (lambda _: subscriber_user.make(), True, False), + ( + # admin user + lambda _: subscriber_user.make( + user_permissions=[Permission.objects.get(codename="change_club")] + ), + True, + True, + ), + ( + # club board member + lambda club: ( + baker.make( + Membership, + club=club, + role=baker.make(ClubRole, is_board=True), + user=subscriber_user.make(), + ).user + ), + True, + True, + ), + ], + ids=["anonymous", "public", "subscriber", "admin", "club board"], +) +@pytest.mark.django_db +def test_club_main_page_displayed_elements( + client: Client, user_factory, can_view_tabs, can_view_edit_button +): + """Test that the appropriate elements are displayed depending on user status.""" club = baker.make(Club) - club.page.revisions.all().delete() + if user := user_factory(club): + client.force_login(user) res = client.get(reverse("club:club_view", kwargs={"club_id": club.id})) assert res.status_code == 200 soup = BeautifulSoup(res.text, "lxml") - detail_html = soup.find(id="club-page") - assert detail_html.find_all("markdown") == [] + tabs = soup.find(class_="tool_bar") + assert tabs is not None if can_view_tabs else tabs is None + edit_btn = soup.find( + "a", + href=reverse("core:page_edit", kwargs={"page_name": club.page.get_full_name()}), + ) + assert edit_btn is not None if can_view_edit_button else edit_btn is None @pytest.mark.django_db def test_page_revision(client: Client): + """Test that accessing a previous revision of the club page works.""" club = baker.make(Club) revisions = baker.make( PageRev, page=club.page, _quantity=3, content=iter(["foo", "bar", "baz"]) diff --git a/club/urls.py b/club/urls.py index 6a08bbe5..d5b9f1a5 100644 --- a/club/urls.py +++ b/club/urls.py @@ -31,6 +31,7 @@ from club.views import ( ClubListView, ClubMailingView, ClubMembersView, + ClubNewsView, ClubOldMembersView, ClubPageEditView, ClubPageHistView, @@ -136,4 +137,5 @@ urlpatterns = [ PosterDeleteView.as_view(), name="poster_delete", ), + path("/news/", ClubNewsView.as_view(), name="news_list"), ] diff --git a/club/views.py b/club/views.py index 6f2c7716..fcf63f6a 100644 --- a/club/views.py +++ b/club/views.py @@ -37,6 +37,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.paginator import InvalidPage, Paginator from django.db.models import F, Prefetch, Q, Sum +from django.db.models.expressions import OuterRef, Subquery from django.db.models.functions import Length from django.http import Http404, StreamingHttpResponse from django.shortcuts import get_object_or_404, redirect @@ -71,7 +72,7 @@ from club.models import ( MailingSubscription, Membership, ) -from com.models import Poster +from com.models import NewsDate, Poster from com.views import ( PosterCreateBaseView, PosterDeleteBaseView, @@ -80,6 +81,7 @@ from com.views import ( ) from core.auth.mixins import CanEditMixin, PermissionOrClubBoardRequiredMixin from core.models import Page, PageRev +from core.utils import get_semester_code from core.views import BasePageEditView, DetailFormView, UseFragmentsMixin from core.views.mixins import ( AllowFragment, @@ -106,42 +108,40 @@ class ClubTabsMixin(TabedViewMixin): return self.object.get_display_name() def get_list_of_tabs(self): + if not self.request.user.has_perm("club.view_club"): + # The user will still be able to see the club main page, + # but nothing else. + # It's useless to display a tab with only one item, so return nothing. + return [] tab_list = [ { "url": reverse("club:club_view", kwargs={"club_id": self.object.id}), "slug": "infos", "name": _("Infos"), - } + }, + { + "url": reverse("club:club_members", kwargs={"club_id": self.object.id}), + "slug": "members", + "name": _("Members"), + }, + { + "url": reverse( + "club:club_old_members", kwargs={"club_id": self.object.id} + ), + "slug": "elderlies", + "name": _("Old members"), + }, + { + "url": reverse("club:club_hist", kwargs={"club_id": self.object.id}), + "slug": "history", + "name": _("History"), + }, + { + "url": reverse("club:news_list", kwargs={"club_id": self.object.id}), + "slug": "events", + "name": _("Events"), + }, ] - if self.request.user.has_perm("club.view_club"): - tab_list.extend( - [ - { - "url": reverse( - "club:club_members", kwargs={"club_id": self.object.id} - ), - "slug": "members", - "name": _("Members"), - }, - { - "url": reverse( - "club:club_old_members", kwargs={"club_id": self.object.id} - ), - "slug": "elderlies", - "name": _("Old members"), - }, - ] - ) - if self.object.page: - tab_list.append( - { - "url": reverse( - "club:club_hist", kwargs={"club_id": self.object.id} - ), - "slug": "history", - "name": _("History"), - } - ) if self.request.user.can_edit(self.object): tab_list.extend( [ @@ -159,21 +159,6 @@ class ClubTabsMixin(TabedViewMixin): "slug": "edit", "name": _("Edit"), }, - ] - ) - if self.object.page and self.request.user.can_edit(self.object.page): - tab_list.append( - { - "url": reverse( - "core:page_edit", - kwargs={"page_name": self.object.page.get_full_name()}, - ), - "slug": "page_edit", - "name": _("Edit club page"), - } - ) - tab_list.extend( - [ { "url": reverse( "club:club_sellings", kwargs={"club_id": self.object.id} @@ -957,3 +942,30 @@ class PosterDeleteView(ClubTabsMixin, PosterDeleteBaseView): def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) + + +class ClubNewsView(PermissionRequiredMixin, ClubTabsMixin, DetailView): + pk_url_kwarg = "club_id" + model = Club + permission_required = "club.view_club" + template_name = "club/club_news_list.jinja" + current_tab = "events" + + def get_context_data(self, **kwargs: Any): + events = list( + self.object.news.viewable_by(self.request.user) + .annotate( + first_date=Subquery( + NewsDate.objects.filter(news_id=OuterRef("pk")) + .order_by("start_date") + .values("start_date")[:1] + ), + ) + .prefetch_related("dates") + .order_by("-first_date") + ) + return super().get_context_data(**kwargs) | { + "events": itertools.groupby( + events, key=lambda event: get_semester_code(event.first_date.date()) + ), + } diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss index c0d633fb..6f2b9944 100644 --- a/com/static/com/css/news-detail.scss +++ b/com/static/com/css/news-detail.scss @@ -5,7 +5,6 @@ margin-top: 20px; padding: 0.4em; width: 80%; - background: $white-color; h4 { margin-top: 1em; diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index e53fbc5c..f8f8d79e 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -32,7 +32,7 @@ h3 { --box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 3px 7px 2px; - background: lighten($second-color, 5%); + background: var(--bg-yellow); box-shadow: var(--box-shadow); padding: .75rem; margin: 0 0 0.5em 0; @@ -85,7 +85,7 @@ #links_content { overflow: auto; - box-shadow: $shadow-color 1px 1px 1px; + box-shadow: var(--bg-darkened-off-white) 1px 1px 1px; min-height: 20em; padding: 1em; @@ -111,7 +111,7 @@ } #birthdays_content { - box-shadow: $shadow-color 1px 1px 1px; + box-shadow: var(--bg-darkened-off-white) 1px 1px 1px; padding: 1em; ul.birthdays_year { @@ -123,7 +123,7 @@ padding: 0.5em; &:nth-child(even) { - background: $secondary-neutral-light-color; + background: var(--bg-off-white); } } @@ -141,7 +141,7 @@ /* EVENTS TODAY AND NEXT FEW DAYS */ .news_events_group { - box-shadow: $shadow-color 1px 1px 1px; + box-shadow: var(--bg-darkened-off-white) 1px 1px 1px; margin-left: 0; margin-bottom: 0.5em; @@ -153,8 +153,8 @@ display: table-cell; padding: 0.6em; vertical-align: middle; - background: $primary-neutral-dark-color; - color: $white-color; + background: var(--deepblue); + color: var(--text-white); text-transform: uppercase; text-align: center; font-weight: bold; @@ -180,7 +180,7 @@ } .news_event:nth-of-type(even) { - background: $primary-neutral-light-color; + background: var(--bg-white); } .news_event { diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index de803d66..d3c880c7 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -864,7 +864,11 @@ class Command(BaseCommand): subscribers = Group.objects.create(name="Cotisants") subscribers.permissions.add( - *list(perms.filter(codename__in=["add_news", "add_uecomment"])) + *list( + perms.filter( + codename__in=["add_news", "add_uecomment", "add_self_timetable"] + ) + ) ) old_subscribers = Group.objects.create(name="Anciens cotisants") old_subscribers.permissions.add( @@ -893,18 +897,16 @@ class Command(BaseCommand): accounting_admin.permissions.add( *list( perms.filter( - Q( - codename__in=[ - "view_customer", - "view_product", - "change_product", - "add_product", - "view_producttype", - "change_producttype", - "add_producttype", - "delete_selling", - ] - ) + codename__in=[ + "view_customer", + "view_product", + "change_product", + "add_product", + "view_producttype", + "change_producttype", + "add_producttype", + "delete_selling", + ] ).values_list("pk", flat=True) ) ) diff --git a/core/migrations/0052_preferences_show_my_timetable.py b/core/migrations/0052_preferences_show_my_timetable.py new file mode 100644 index 00000000..eb9f37bf --- /dev/null +++ b/core/migrations/0052_preferences_show_my_timetable.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.17 on 2026-09-24 14:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("core", "0051_user_cgu_approved_at")] + + operations = [ + migrations.AddField( + model_name="preferences", + name="show_my_timetable", + field=models.BooleanField( + default=True, + help_text=( + "Allow subscribers to see your timetable and " + "to compare it to theirs (if you have filled it)" + ), + verbose_name="show your timetable to others", + ), + ), + ] diff --git a/core/models.py b/core/models.py index 8b057a03..7baec8d1 100644 --- a/core/models.py +++ b/core/models.py @@ -799,6 +799,14 @@ class Preferences(models.Model): ), default=False, ) + show_my_timetable = models.BooleanField( + _("show your timetable to others"), + help_text=_( + "Allow subscribers to see your timetable and " + "to compare it to theirs (if you have filled it)" + ), + default=True, + ) notify_on_click = models.BooleanField( _("get a notification for every click"), default=False ) diff --git a/core/static/bundled/core/components/easymde-index.ts b/core/static/bundled/core/components/easymde-index.ts index adc01bb4..54f1a134 100644 --- a/core/static/bundled/core/components/easymde-index.ts +++ b/core/static/bundled/core/components/easymde-index.ts @@ -60,6 +60,7 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => { }); easymde.codemirror.replaceSelection("\n"); }, + previewClass: ["editor-preview", "markdown"], // default class + our own markdown class previewRender: (plainText) => markdown(plainText), forceSync: true, // Avoid validation error on generic create view imageTexts: { diff --git a/core/static/core/base.css b/core/static/core/base.css deleted file mode 100644 index d9bd6af1..00000000 --- a/core/static/core/base.css +++ /dev/null @@ -1,123 +0,0 @@ -/*--------------------------------RESET--------------------------------*/ -/*--------------------------------RESET--------------------------------*/ -html, -body, -div, -span, -applet, -object, -iframe, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -abbr, -acronym, -address, -big, -cite, -code, -del, -dfn, -em, -img, -ins, -kbd, -q, -s, -samp, -small, -strike, -sub, -sup, -tt, -var, -b, -u, -i, -center, -dl, -dt, -dd, -ol, -ul, -li, -fieldset, -form, -label, -legend, -table, -caption, -tbody, -tfoot, -thead, -tr, -th, -td, -article, -aside, -canvas, -details, -embed, -figure, -figcaption, -footer, -header, -hgroup, -menu, -nav, -output, -ruby, -section, -summary, -time, -mark, -audio, -video { - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - vertical-align: baseline; -} -/* HTML5 display-role reset for older browsers */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -menu, -nav, -section { - display: block; -} -body { - line-height: 1; -} -ol, -ul { - /* list-style: none;*/ -} -blockquote, -q { - quotes: none; -} -blockquote:before, -blockquote:after, -q:before, -q:after { - content: none; -} -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss index 490690d9..74335cbc 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -1,41 +1,37 @@ -$first-color: hsl(220, 100%, 50%); -$second-color: hsl(48, 100%, 67%); -$primary-color: hsl(219.9, 53.7%, 50%); -$secondary-color: hsl(204, 64%, 44%); -$primary-color-text: hsl(0, 0%, 100%); -$secondary-color-text: hsla(0, 0%, 0%, 0.87); +/** + * Sith theme variables + */ +:root { + --bg-yellow: #ffe270; + --bg-white: #f9fafb; + --bg-off-white: #f0f0f0; + --bg-darkened-off-white: #e3e3e3; + --bg-lightgrey: #d4d4d4; -$primary-light-color: hsl(219.8, 46.4%, 64.9%); -$primary-dark-color: hsl(203, 75%, 40%); + --text-white: #f9fafb; + --text-black: #2b2b2b; + --text-red: #eb2f06; -$secondary-light-color: hsl(40, 68%, 65%); -$secondary-dark-color: hsl(40, 68%, 35%); - -$primary-neutral-color: hsl(219.6, 20.8%, 50%); -$primary-neutral-light-color: hsl(0, 0%, 94%); -$primary-neutral-dark-color: hsl(210, 29%, 29%); - -$secondary-neutral-color: hsl(204, 64%, 44%); -$secondary-neutral-light-color: hsl(0, 0%, 91%); -$secondary-neutral-dark-color: hsl(40, 57.6%, 17%); - -$white-color: hsl(219.6, 20.8%, 98%); -$black-color: hsl(0, 0%, 17%); -$red-text-color: #eb2f06; -$hovered-red-text-color: #ff4d4d; + --blue: #3b69c4; + --lightblue: #1a78b3; + // darkened-deepblue is 10% darker than deepblue + // This is explicitely declared, because the `darken` scss function + // isn't called at compile-time somehow. + --deepblue: #354a5f; + --darkened-deepblue: #23313e; +} +/** + * Brand colours. + * Those are officially defined by their respective companies, + * and so are more appropriate as scss variables. + */ $faceblue: hsl(221, 44%, 41%); $twitblue: hsl(206, 82%, 63%); $discordblurple: #7289da; $instagradient: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); $githubblack: rgb(22, 22, 20); -$shadow-color: rgb(223, 223, 223); - -$background-button-color: hsl(0, 0%, 95%); - -$deepblue: #354a5f; - @mixin shadow { box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0, rgba(60, 64, 67, 0.15) 0 4px 8px 3px; diff --git a/core/static/core/components/card.scss b/core/static/core/components/card.scss index 2fe346a2..602ab3f3 100644 --- a/core/static/core/components/card.scss +++ b/core/static/core/components/card.scss @@ -17,7 +17,7 @@ } .card { - background-color: $primary-neutral-light-color; + background-color: var(--bg-off-white); border-radius: 5px; position: relative; box-sizing: border-box; @@ -30,12 +30,12 @@ gap: 20px; &:disabled { - background-color: darken($primary-neutral-light-color, 5%); + background-color: var(--bg-darkened-off-white); opacity: 65%; } &.clickable:not(:disabled):hover { - background-color: darken($primary-neutral-light-color, 5%); + background-color: var(--bg-darkened-off-white); } &.selected { diff --git a/core/static/core/components/tabs.scss b/core/static/core/components/tabs.scss index e8665967..c44dec96 100644 --- a/core/static/core/components/tabs.scss +++ b/core/static/core/components/tabs.scss @@ -11,7 +11,7 @@ ui-tab-group { .tab-headers { display: flex; flex-flow: row wrap; - background-color: $primary-neutral-light-color; + background-color: var(--bg-off-white); padding: 3px 12px 12px; column-gap: 20px; border-top-left-radius: 5px; @@ -25,23 +25,27 @@ ui-tab-group { background-color: unset; position: relative; + &:hover { + background-color: var(--bg-lightgrey); + } + &:after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; - border-bottom: 4px solid darken($primary-neutral-light-color, 10%); + border-bottom: 4px solid var(--bg-lightgrey); border-radius: 2px; transition: all 0.2s ease-in-out; } &:hover:after { - border-bottom-color: darken($primary-neutral-light-color, 20%); + filter: brightness(85%); } &.active:after { - border-bottom-color: $primary-dark-color; + border-bottom-color: var(--lightblue); } } } diff --git a/core/static/core/footer.scss b/core/static/core/footer.scss index aa2e048f..4e2b9ae6 100644 --- a/core/static/core/footer.scss +++ b/core/static/core/footer.scss @@ -2,92 +2,75 @@ @import "devices"; footer.bottom-links { - >section>a { + > section > a { text-align: center; } + text-align: center; + + hr { + border: none; + } + @media (max-width: $small-devices) { margin-top: 0.6em; padding: 1.25em; - background-color: $primary-neutral-dark-color; + background-color: var(--deepblue); display: flex; flex-direction: column; - align-items: center; gap: 1.25em; - >section { - + > section { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 0.8em; - } a { - color: $white-color; - width: auto; + color: var(--text-white); &:hover { - color: $white-color; - text-shadow: 0.5px 0.5px 0.5px $shadow-color; + text-shadow: 0.5px 0.5px 0.5px var(--bg-darkened-off-white); } } .fa-github { - color: $white-color; + color: var(--text-white); } hr { - width: 100%; - height: 0px; - border: none; - border-top: 0.5px solid $white-color; + border-top: var(--bg-white) 1px solid; } } + @media (min-width: $small-devices) { width: 90%; margin: 2em auto; - font-size: 90%; - text-align: center; - vertical-align: middle; section:first-of-type { margin: 0.6em 0; - color: $white-color; border-radius: 5px; display: flex; flex-wrap: wrap; align-items: center; - background-color: $primary-neutral-dark-color; - box-shadow: $shadow-color 0 0 15px; + background-color: var(--deepblue); a { - color: $white-color; - width: auto; padding: 0.8em; flex: 1; font-weight: bold; + color: var(--text-white); &:hover { - color: $white-color; - text-shadow: 0.5px 0.5px 0.5px $shadow-color; + text-shadow: 0.5px 0.5px 0.5px var(--bg-darkened-off-white); } } } - - .fa-github { - color: $githubblack; - } - - hr { - border: none; - height: 5px; - } } } \ No newline at end of file diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss index 511190b6..3283a352 100644 --- a/core/static/core/forms.scss +++ b/core/static/core/forms.scss @@ -16,7 +16,6 @@ input[type="file"] { border: none; text-decoration: none; - background-color: $background-button-color; padding: 0.4em; margin: 0.1em; font-size: 1.2em; @@ -24,7 +23,7 @@ color: black; &:not(.link-like):not(:disabled):hover { - background: hsl(0, 0%, 83%); + background: var(--bg-lightgrey); } } @@ -53,7 +52,7 @@ .ts-control { border: none; text-decoration: none; - background-color: $background-button-color; + //background-color: $background-button-color; padding: 0.4em; margin: 0.1em; font-size: 1.2em; @@ -64,7 +63,7 @@ textarea { border: none; text-decoration: none; - background-color: $background-button-color; + //background-color: $background-button-color; padding: 7px; font-size: 1.2em; border-radius: 5px; @@ -75,7 +74,7 @@ border: none; text-decoration: none; font-size: 1.2em; - background-color: $background-button-color; + //background-color: $background-button-color; padding: 10px; border-radius: 5px; cursor: pointer; @@ -83,14 +82,14 @@ a:not(.button) { text-decoration: none; - color: $primary-dark-color; + color: var(--lightblue); &:hover { - color: $primary-light-color; + color: var(--blue); } &:active { - color: $primary-color; + color: var(--blue); } } } @@ -161,6 +160,7 @@ form { fieldset { margin-bottom: 1rem; + border: 0; } .row > label { @@ -762,16 +762,13 @@ form { // a submit input that should look like a regular input[type="submit"], button { &.link-like { - color: $primary-dark-color; + color: var(--lightblue); &:hover { - color: $primary-light-color; + color: var(--blue); } &.link-red { - color: $red-text-color; - &:hover { - color: $hovered-red-text-color; - } + color: var(--text-red); } font-weight: normal; font-size: 100%; diff --git a/core/static/core/header.scss b/core/static/core/header.scss index e935a27b..95409c2b 100644 --- a/core/static/core/header.scss +++ b/core/static/core/header.scss @@ -7,7 +7,7 @@ $background-color-hovered: #283747; .header { box-sizing: border-box; - background-color: $deepblue; + background-color: var(--deepblue); box-shadow: 3px 3px 3px 0 #dfdfdf; border-radius: 0; width: 100%; @@ -96,7 +96,7 @@ $background-color-hovered: #283747; border-radius: 0; margin: 0; box-sizing: border-box; - background-color: $deepblue; + background-color: var(--deepblue); width: 45px; height: 25px; padding: 0; @@ -212,7 +212,7 @@ $background-color-hovered: #283747; background-position: center center; background-size: cover; background-repeat: no-repeat; - background-color: $deepblue; + background-color: var(--deepblue); } >.options { @@ -298,7 +298,7 @@ $background-color-hovered: #283747; width: 10px; height: 10px; padding: 5px; - background-color: $red-text-color; + background-color: var(--text-red); border-radius: 50%; position: absolute; top: -50%; diff --git a/core/static/core/markdown.scss b/core/static/core/markdown.scss index 098d86be..2a4a5aaa 100644 --- a/core/static/core/markdown.scss +++ b/core/static/core/markdown.scss @@ -1,7 +1,4 @@ .markdown { - --bg-lightgrey: rgb(240, 241, 245); - --border-lightgrey: rgb(215, 216, 220); - h1, h2, h3, @@ -28,11 +25,20 @@ font-size: 14px; tab-size: 4; border-radius: 4px; - border: 1px solid var(--border-lightgrey); - background-color: var(--bg-lightgrey); + border: 1px solid var(--bg-lightgrey); + background-color: var(--bg-off-white); padding: 1px 2px; } + /** + * Some restyling rules for the cases where our own md css + * conflicts with the easymde editor preview css + */ + &.editor-preview { + pre { + background: unset; + } + } pre code { // code that is not inlined margin: 15px; diff --git a/core/static/core/navbar.scss b/core/static/core/navbar.scss index 9b77bf77..810b113a 100644 --- a/core/static/core/navbar.scss +++ b/core/static/core/navbar.scss @@ -3,7 +3,7 @@ $desktop-size: 500px; nav.navbar { - background-color: $primary-dark-color; + background-color: var(--lightblue); margin: 1em; color: white; border-radius: 0.6em; diff --git a/core/static/core/pagination.scss b/core/static/core/pagination.scss index 911018a4..958a11d3 100644 --- a/core/static/core/pagination.scss +++ b/core/static/core/pagination.scss @@ -6,27 +6,21 @@ margin: 30px; button { - background-color: $secondary-neutral-light-color; + background-color: var(--bg-off-white); min-width: 37px; &:hover { - background-color: darken($secondary-neutral-light-color, 10%); - } - - &:disabled { - color: #fff; - background-color: darken($secondary-neutral-light-color, 5%); + background-color: var(--bg-lightgrey); } &.active, &.active:hover { - background-color: $primary-neutral-color; + background-color: var(--deepblue); color: white; border-radius: 50%; } - &:first-of-type, - &:last-of-type { + .pagination-arrow { // previous and next buttons &:disabled { // Hide the arrows when they are disabled, without diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 2a1857b1..80ee165f 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -2,12 +2,21 @@ @import "forms"; @import "devices"; +/*----- RESET -----*/ +* { + margin: 0; + padding: 0; +} + + /*--------------------------------GENERAL------------------------------*/ body { position: relative; width: 100%; font-family: sans-serif; + background-color: var(--bg-white); + line-height: 1; } [aria-busy] { @@ -57,7 +66,7 @@ body { .collapse-header { color: white; - background-color: $deepblue; + background-color: var(--deepblue); padding: 5px 10px; display: flex; align-items: center; @@ -109,6 +118,12 @@ body { display: none !important; } +hr { + display: block; + margin: .5em auto; + color: gray; +} + /*--------------------------------HEADER-------------------------------*/ #info_boxes { @@ -141,7 +156,7 @@ body { } #info_box { - background: $primary-neutral-light-color; + background: var(--bg-off-white); &:before { content: "\f05a"; @@ -150,11 +165,11 @@ body { } #alert_box { - background: $second-color; + background: var(--bg-yellow); &:before { content: "\f06a"; - color: $white-color; + color: var(--text-white); } } } @@ -189,10 +204,10 @@ body { border-radius: 5px; &.btn-blue { - background-color: $deepblue; + background-color: var(--deepblue); &:not(:disabled):hover { - background-color: darken($deepblue, 10%); + background-color: var(--darkened-deepblue); } &:disabled { @@ -215,7 +230,7 @@ body { &.btn-green { $bg-color: rgba(0, 210, 83, 0.4); background-color: $bg-color; - color: $black-color; + color: var(--text-black); &:not(:disabled):hover { background-color: darken($bg-color, 15%); @@ -272,8 +287,8 @@ body { /*--------------------------------CONTENT------------------------------*/ #content { padding: 1.5em 3%; - box-shadow: $shadow-color 0 5px 10px; - background: $white-color; + box-shadow: var(--bg-darkened-off-white) 0 5px 10px; + background: var(--bg-white); overflow: auto; } @@ -343,7 +358,7 @@ body { display: flex; flex-wrap: wrap; float: right; - background: $primary-neutral-light-color; + background: var(--bg-off-white); padding: 5px; border-radius: 6px; text-align: center; @@ -351,7 +366,7 @@ body { a { padding: 7px; display: inline-block; - color: $black-color; + color: var(--text-black); font-weight: bold; margin: 2px; flex: 1; @@ -359,14 +374,14 @@ body { white-space: nowrap; &.selected_tab { - background: $primary-color; - color: $white-color; + background: var(--blue); + color: var(--text-white); border-radius: 6px; } &:hover { - background: $primary-color; - color: $white-color; + background: var(--blue); + color: var(--text-white); border-radius: 6px; } } @@ -398,28 +413,33 @@ body { } } - /* Fontawesome icons */ - .fa-brands, .fa-link { - color: black; - } +} - .fa-facebook { - color: $faceblue; - } +/* Fontawesome icons */ +.fa-brands, .fa-link { + color: black; +} - .fa-discord { - color: $discordblurple; - } +.fa-facebook { + color: $faceblue; +} - .fa-square-instagram::before, .fa-instagram::before { - background: $instagradient; - background-clip: text; - -webkit-text-fill-color: transparent; - } +.fa-discord { + color: $discordblurple; +} - .fa-bluesky, .fa-square-bluesky { - color: #0f73ff; - } +.fa-square-instagram::before, .fa-instagram::before { + background: $instagradient; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.fa-bluesky, .fa-square-bluesky { + color: #0f73ff; +} + +.fa-github { + color: $githubblack; } @media screen and (max-width: $small-devices) { @@ -481,16 +501,6 @@ dt { margin-top: 25px; } -.edit-bar { - display: block; - margin: 4px; - - a { - display: inline-block; - margin: 4px; - } -} - .important { font-size: 1.2em; font-weight: bold; @@ -503,7 +513,6 @@ table { border-collapse: collapse; border-spacing: 0; border-radius: 5px; - -moz-border-radius: 5px; overflow: hidden; box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0, rgba(60, 64, 67, 0.15) 0 4px 8px 3px; @@ -515,25 +524,21 @@ table { } } -th { - padding: 4px; -} - td, th { vertical-align: middle; text-align: center; padding: 5px 10px; - >input[type="checkbox"] { + > input[type="checkbox"] { padding: unset; } - >ul { + > ul { margin-top: 0; } - >input[type="checkbox"] { + > input[type="checkbox"] { padding: unset; } } @@ -543,10 +548,6 @@ td { border-collapse: collapse; overflow: hidden; text-overflow: ellipsis; - - >ul { - margin-top: 0; - } } th, @@ -556,23 +557,23 @@ thead td { } thead { - background-color: $deepblue; + background-color: var(--deepblue); color: white; } -tbody>tr { +tbody > tr { &:nth-child(even):not(.highlight) { - background: $primary-neutral-light-color; + background: var(--bg-off-white); } &.clickable:hover { cursor: pointer; - background: $secondary-neutral-light-color; + background: var(--bg-lightgrey); width: 100%; } &.highlight { - color: $primary-dark-color; + color: var(--lightblue); font-style: italic; } } @@ -598,7 +599,7 @@ em { } .highlight { - background: $second-color; + background: var(--bg-yellow); font-weight: bold; } @@ -700,15 +701,15 @@ u, height: 18em; overflow: hidden; border: 1px solid black; - box-shadow: $shadow-color 1px 1px 1px; + box-shadow: var(--bg-darkened-off-white) 1px 1px 1px; &:hover { - box-shadow: 1px 1px 5px $second-color; + box-shadow: 1px 1px 5px var(--bg-yellow); } } .matmat_user a { - color: $primary-neutral-dark-color; + color: var(--deepblue); height: 100%; width: 100%; margin: 0; @@ -723,7 +724,6 @@ u, display: block; margin: 10px; padding: 10px; - background: $white-color; } textarea { @@ -731,43 +731,7 @@ textarea { margin-top: 10px; } - - -.ui-dialog .ui-dialog-buttonpane { - bottom: 0; - position: absolute; - width: 97%; -} - #cash_summary_form label, .inline { display: inline; } - -.form_button { - width: 150px; - height: 120px; - padding: 2px; - display: inline-block; - font-size: 0.8em; - - span { - width: 70px; - float: right; - } - - img { - max-width: 50px; - max-height: 50px; - float: left; - } - - strong { - font-weight: bold; - font-size: 1.2em; - } - - button { - vertical-align: middle; - } -} diff --git a/core/static/user/login.scss b/core/static/user/login.scss index 97814e12..45afd269 100644 --- a/core/static/user/login.scss +++ b/core/static/user/login.scss @@ -21,7 +21,6 @@ body { flex-direction: column; padding: 10px; box-shadow: none; - background-color: white; margin: 0; .alert { diff --git a/core/static/user/user_detail.scss b/core/static/user/user_detail.scss index 60985f16..2191b773 100644 --- a/core/static/user/user_detail.scss +++ b/core/static/user/user_detail.scss @@ -71,7 +71,7 @@ main { border-radius: 50%; justify-content: center; align-items: center; - background-color: $primary-neutral-light-color; + background-color: var(--bg-white); > span { font-size: small; diff --git a/core/static/user/user_godfathers.scss b/core/static/user/user_godfathers.scss index d4cdd304..a9a665f3 100644 --- a/core/static/user/user_godfathers.scss +++ b/core/static/user/user_godfathers.scss @@ -62,7 +62,7 @@ } &:enabled>.fa { - background-color: #354a5f; + background-color: var(--deepblue); color: white; } diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 57cae31d..be260d35 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -23,7 +23,6 @@ {% endblock %} - diff --git a/core/templates/core/fragment/user_visibility.jinja b/core/templates/core/fragment/user_visibility.jinja index 82e4f0ea..370665e5 100644 --- a/core/templates/core/fragment/user_visibility.jinja +++ b/core/templates/core/fragment/user_visibility.jinja @@ -29,5 +29,13 @@ {{ form.show_my_stats.errors }} +
+ {{ form.show_my_timetable }} + {{ form.show_my_timetable.label_tag() }} + + {{ form.show_my_timetable.help_text }} + + {{ form.show_my_timetable.errors }} +
\ No newline at end of file diff --git a/core/templates/core/macros.jinja b/core/templates/core/macros.jinja index 303c670e..d5083f00 100644 --- a/core/templates/core/macros.jinja +++ b/core/templates/core/macros.jinja @@ -102,6 +102,7 @@ @click.prevent="{{ page }}--" :disabled="{{ page }} <= 1" @keyup.right.window="{{ page }} = Math.min({{ nb_pages }}, {{ page }} + 1)" + class="pagination-arrow" > @@ -112,6 +113,7 @@ @click.prevent="{{ page }}++" :disabled="{{ page }} >= {{ nb_pages }}" @keyup.left.window="{{ page }} = Math.max(1, {{ page }} - 1)" + class="pagination-arrow" > @@ -163,12 +165,14 @@ href="?{{ querystring(request, page=current_page.previous_page_number()) }}" {%- endif -%} > -
{% else %} - + {% endif %} {% for i in paginator.get_elided_page_range(current_page.number) %} {% if i == current_page.number %} @@ -201,12 +205,14 @@ {%- else -%} href="?{{querystring(request, page=current_page.next_page_number())}}" {%- endif -%} - > {% else %} - + {% endif %} {% endmacro %} diff --git a/core/templates/core/user_stats.jinja b/core/templates/core/user_stats.jinja index 621c4956..28c59cad 100644 --- a/core/templates/core/user_stats.jinja +++ b/core/templates/core/user_stats.jinja @@ -31,10 +31,10 @@ {% for sum in purchase_sums %}
{{ sum["counter__name"] }} - {{ sum["total"] }} € + {{ "%.2f"|format(sum["total"]) }} €
{% endfor %} -
Total : {{ total_purchases }} €
+
Total : {{ "%.2f"|format(total_purchases) }} €
diff --git a/core/urls.py b/core/urls.py index 0dccdc2e..64930df4 100644 --- a/core/urls.py +++ b/core/urls.py @@ -84,6 +84,7 @@ from core.views import ( notification, send_file, ) +from timetable.views import UserTimetableView register_converter(FourDigitYearConverter, "yyyy") register_converter(TwoDigitMonthConverter, "mm") @@ -184,6 +185,11 @@ urlpatterns = [ name="user_account_detail", ), path("user//stats/", UserStatsView.as_view(), name="user_stats"), + path( + "user//edt/", + UserTimetableView.as_view(), + name="user_timetable", + ), path( "user//gift/create/", GiftCreateView.as_view(), diff --git a/core/views/files.py b/core/views/files.py index 8b7b4f42..c97b3427 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -21,7 +21,7 @@ from wsgiref.util import FileWrapper from django import forms from django.conf import settings -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Exists, OuterRef from django.forms.models import modelform_factory from django.http import Http404, HttpRequest, HttpResponse @@ -233,6 +233,12 @@ class FileEditPropForm(forms.ModelForm): recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False) + def clean_parent(self): + parent = self.cleaned_data["parent"] + if not self.instance.owner.can_edit(parent): + raise ValidationError(_("You cannot move this file there")) + return parent + class FileEditPropView(CanEditPropMixin, UpdateView): model = SithFile @@ -253,10 +259,7 @@ class FileEditPropView(CanEditPropMixin, UpdateView): return ret def get_success_url(self): - return reverse( - "core:file_detail", - kwargs={"file_id": self.object.id}, - ) + return reverse("core:file_detail", kwargs={"file_id": self.object.id}) class FileView(CanViewMixin, DetailView, FormMixin): @@ -269,7 +272,7 @@ class FileView(CanViewMixin, DetailView, FormMixin): form_class = AddFilesForm @staticmethod - def handle_clipboard(request, obj): + def handle_clipboard(request, obj: SithFile): """Handle the clipboard in the view. This method can fail, since it does not catch the exceptions coming from @@ -283,10 +286,9 @@ class FileView(CanViewMixin, DetailView, FormMixin): where you want to paste the clipboard """ if "delete" in request.POST: - for f_id in request.POST.getlist("file_list"): - file = SithFile.objects.filter(id=f_id).first() - if file: - file.delete() + file_ids = request.POST.getlist("file_list") + for file in obj.children.filter(id__in=file_ids): + file.delete() if "clear" in request.POST: request.session["clipboard"] = [] if "cut" in request.POST: diff --git a/core/views/forms.py b/core/views/forms.py index c0510c7d..cc235a2e 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -336,10 +336,14 @@ class UserVisibilityForm(forms.ModelForm): __preferences_fields = forms.fields_for_model( Preferences, - ["show_my_stats"], - widgets={"show_my_stats": forms.CheckboxInput(attrs={"class": "switch"})}, + ["show_my_stats", "show_my_timetable"], + widgets={ + "show_my_stats": forms.CheckboxInput(attrs={"class": "switch"}), + "show_my_timetable": forms.CheckboxInput(attrs={"class": "switch"}), + }, ) show_my_stats = __preferences_fields["show_my_stats"] + show_my_timetable = __preferences_fields["show_my_timetable"] def __init__( self, *args, initial: dict | None = None, instance: User | None = None, **kwargs @@ -347,12 +351,16 @@ class UserVisibilityForm(forms.ModelForm): if instance: initial = initial or {} initial["show_my_stats"] = instance.preferences.show_my_stats + initial["show_my_timetable"] = instance.preferences.show_my_timetable super().__init__(*args, initial=initial, instance=instance, **kwargs) def save(self, commit=True) -> User: # noqa: FBT002 instance = super().save(commit=commit) if commit: instance.preferences.show_my_stats = self.cleaned_data["show_my_stats"] + instance.preferences.show_my_timetable = self.cleaned_data[ + "show_my_timetable" + ] instance.preferences.save() return instance diff --git a/core/views/mixins.py b/core/views/mixins.py index f0403662..974ea91f 100644 --- a/core/views/mixins.py +++ b/core/views/mixins.py @@ -1,6 +1,14 @@ import copy import inspect -from typing import TYPE_CHECKING, Any, ClassVar, LiteralString, Protocol, Unpack +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + LiteralString, + Protocol, + TypedDict, + Unpack, +) from django.core.exceptions import ImproperlyConfigured from django.template.loader import render_to_string @@ -12,11 +20,17 @@ if TYPE_CHECKING: from django.utils.safestring import SafeString +class TabItem(TypedDict): + url: str + slug: str + name: str + + class TabedViewMixin(View): """Basic functions for displaying tabs in the template.""" current_tab: ClassVar[str | None] = None - list_of_tabs: ClassVar[list | None] = None + list_of_tabs: ClassVar[list[TabItem] | None] = None tabs_title: ClassVar[str | None] = None def get_tabs_title(self): diff --git a/core/views/user.py b/core/views/user.py index 4fad761f..cfcaeff0 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -413,7 +413,7 @@ class UserStatsView(UserTabsMixin, UserPassesTestMixin, DetailView): kwargs = super().get_context_data(**kwargs) kwargs["perm_time"] = list( - self.object.permanencies.filter(end__isnull=False, counter__type="BAR") + self.object.permanencies.filter(end__isnull=False) .values("counter", "counter__name") .annotate(total=Sum(F("end") - F("start"), default=timedelta(seconds=0))) .order_by("-total") @@ -422,8 +422,7 @@ class UserStatsView(UserTabsMixin, UserPassesTestMixin, DetailView): [perm["total"] for perm in kwargs["perm_time"]], start=timedelta(seconds=0) ) kwargs["purchase_sums"] = list( - self.object.customer.buyings.filter(counter__type="BAR") - .values("counter", "counter__name") + self.object.customer.buyings.values("counter", "counter__name") .annotate(total=Sum(F("unit_price") * F("quantity"))) .order_by("-total") ) diff --git a/counter/models.py b/counter/models.py index 1ccf1d8f..12059c94 100644 --- a/counter/models.py +++ b/counter/models.py @@ -647,13 +647,6 @@ class Counter(models.Model): or user in self.sellers.all() ) - def gen_token(self) -> None: - """Generate a new token for this counter.""" - self.token = "".join( - random.choice(string.ascii_letters + string.digits) for _ in range(30) - ) - self.save() - @cached_property def barmen_list(self) -> list[User]: """Returns the barman list as list of User.""" @@ -661,10 +654,6 @@ class Counter(models.Model): p.user for p in self.permanencies.filter(end=None).select_related("user") ] - def get_random_barman(self) -> User: - """Return a random user being currently a barman.""" - return random.choice(self.barmen_list) - def update_activity(self) -> None: """Update the barman activity to prevent timeout.""" self.permanencies.filter(end=None).update(activity=timezone.now()) diff --git a/counter/static/counter/css/counter-click.scss b/counter/static/counter/css/counter-click.scss index 64645cde..6098f182 100644 --- a/counter/static/counter/css/counter-click.scss +++ b/counter/static/counter/css/counter-click.scss @@ -71,7 +71,7 @@ padding: 0.5em; margin: 0.2em; height: 100%; - background: $secondary-neutral-light-color; + background: var(--bg-off-white); img { max-width: 70%; diff --git a/counter/templates/counter/product_form.jinja b/counter/templates/counter/product_form.jinja index be1fd850..c30b3e86 100644 --- a/counter/templates/counter/product_form.jinja +++ b/counter/templates/counter/product_form.jinja @@ -83,8 +83,17 @@ {% endmacro %} {% block content %} +
+
+ {%- trans trimmed -%} + You can see product details. + However, you cannot edit it, thus you won't be able + to submit this form. + {%- endtrans -%} +
+
{% if object %} -

{% trans name=object %}Edit product {{ name }}{% endtrans %}

+

{{ object }}

{% trans %}Creation date{% endtrans %} : {{ object.created_at|date }}

{% trans %}Last update{% endtrans %} : {{ object.updated_at|date }}

{% else %} @@ -127,19 +136,17 @@ {{ form.price_formset.management_form }}
{%- for form in form.price_formset.forms -%} -
- {{ price_form(form) }} -
+
{{ price_form(form) }}
{%- endfor -%}
- + {% if user.has_perm("counter.change_product") %} + + {% endif %}
@@ -165,11 +172,16 @@ - + {% if user.has_perm("counter.change_product") %} + + {% endif %}
{{ form.archived.as_field_group() }}
-

+ + {% if user.has_perm("counter.change_product") %} +

+ {% endif %} {% endblock %} diff --git a/counter/templates/counter/product_list.jinja b/counter/templates/counter/product_list.jinja index ca2e4c4d..b5c3c7fa 100644 --- a/counter/templates/counter/product_list.jinja +++ b/counter/templates/counter/product_list.jinja @@ -80,9 +80,11 @@

{% trans %}Product list{% endtrans %}

- - {% trans %}New product{% endtrans %} - + {% if user.has_perm("counter.add_product") %} + + {% trans %}New product{% endtrans %} + + {% endif %}