-{% endblock %}
diff --git a/com/tests/test_views.py b/com/tests/test_views.py
index 02081597..0e48f033 100644
--- a/com/tests/test_views.py
+++ b/com/tests/test_views.py
@@ -17,7 +17,9 @@ from unittest.mock import patch
import pytest
from django.conf import settings
+from django.contrib.auth.models import Permission
from django.contrib.sites.models import Site
+from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import html
@@ -27,9 +29,10 @@ from model_bakery import baker
from pytest_django.asserts import assertNumQueries, assertRedirects
from club.models import Club, Membership
-from com.models import News, NewsDate, Sith, Weekmail, WeekmailArticle
+from com.models import News, NewsDate, Poster, Sith, Weekmail, WeekmailArticle
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User
+from core.utils import RED_PIXEL_PNG
@pytest.fixture()
@@ -314,7 +317,6 @@ def test_feed(client: Client):
[
reverse("com:poster_list"),
reverse("com:poster_create"),
- reverse("com:poster_moderate_list"),
],
)
def test_poster_management_views_crash_test(client: Client, url: str):
@@ -325,3 +327,37 @@ def test_poster_management_views_crash_test(client: Client, url: str):
client.force_login(user)
res = client.get(url)
assert res.status_code == 200
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+ "referer",
+ [
+ None,
+ reverse("com:poster_list"),
+ reverse("club:poster_list", kwargs={"club_id": settings.SITH_MAIN_CLUB_ID}),
+ ],
+)
+def test_moderate_poster(client: Client, referer: str | None):
+ poster = baker.make(
+ Poster,
+ is_moderated=False,
+ file=SimpleUploadedFile("test.png", content=RED_PIXEL_PNG),
+ club_id=settings.SITH_MAIN_CLUB_ID,
+ )
+ user = baker.make(
+ User,
+ user_permissions=Permission.objects.filter(
+ codename__in=["view_poster", "moderate_poster"]
+ ),
+ )
+ client.force_login(user)
+ headers = {"REFERER": f"https://{settings.SITH_URL}{referer}"} if referer else {}
+ response = client.post(
+ reverse("com:poster_moderate", kwargs={"object_id": poster.id}), headers=headers
+ )
+ result_url = referer or reverse("com:poster_list")
+ assertRedirects(response, result_url)
+ poster.refresh_from_db()
+ assert poster.is_moderated
+ assert poster.moderator == user
diff --git a/com/urls.py b/com/urls.py
index 8afbfd12..51861316 100644
--- a/com/urls.py
+++ b/com/urls.py
@@ -33,7 +33,6 @@ from com.views import (
PosterDeleteView,
PosterEditView,
PosterListView,
- PosterModerateListView,
PosterModerateView,
ScreenCreateView,
ScreenDeleteView,
@@ -102,11 +101,6 @@ urlpatterns = [
PosterDeleteView.as_view(),
name="poster_delete",
),
- path(
- "poster/moderate/",
- PosterModerateListView.as_view(),
- name="poster_moderate_list",
- ),
path(
"poster//moderate/",
PosterModerateView.as_view(),
diff --git a/com/views.py b/com/views.py
index a1897b12..2d5045d9 100644
--- a/com/views.py
+++ b/com/views.py
@@ -25,6 +25,7 @@ import itertools
from datetime import date, timedelta
from smtplib import SMTPRecipientsRefused
from typing import Any
+from urllib.parse import urlparse
from dateutil.relativedelta import relativedelta
from django.conf import settings
@@ -34,7 +35,7 @@ from django.contrib.auth.mixins import (
)
from django.contrib.syndication.views import Feed
from django.core.exceptions import PermissionDenied, ValidationError
-from django.db.models import Max
+from django.db.models import Exists, Max, OuterRef, Value
from django.forms.models import modelform_factory
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
@@ -45,7 +46,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, TemplateView, View
from django.views.generic.edit import CreateView, DeleteView, UpdateView
-from club.models import Club, Mailing
+from club.models import Club, Mailing, Membership
from com.forms import NewsDateForm, NewsForm, PosterForm
from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle
@@ -561,16 +562,26 @@ class MailingModerateView(View):
raise PermissionDenied
-class PosterListBaseView(PermissionOrClubBoardRequiredMixin, ListView):
+class PosterListBaseView(ListView):
"""List communication posters."""
model = Poster
template_name = "com/poster_list.jinja"
permission_required = "com.view_poster"
- ordering = ["-date_begin"]
- def get_context_data(self, **kwargs):
- return super().get_context_data(**kwargs) | {"club": self.club}
+ def get_queryset(self):
+ qs = Poster.objects.prefetch_related("screens")
+ if self.request.user.has_perm("com.edit_poster"):
+ qs = qs.annotate(is_editable=Value(value=True))
+ else:
+ qs = qs.annotate(
+ is_editable=Exists(
+ Membership.objects.ongoing()
+ .board()
+ .filter(user=self.request.user, club=OuterRef("club_id"))
+ )
+ )
+ return qs.order_by("-date_begin")
class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView):
@@ -633,21 +644,17 @@ class PosterDeleteBaseView(
permission_required = "com.delete_poster"
-class PosterListView(ComTabsMixin, PosterListBaseView):
+class PosterListView(PermissionRequiredMixin, ComTabsMixin, PosterListBaseView):
"""List communication posters."""
current_tab = "posters"
-
- def get_queryset(self):
- qs = super().get_queryset()
- if self.request.user.has_perm("com.view_poster"):
- return qs
- return qs.filter(club=self.club.id)
-
- def get_context_data(self, **kwargs):
- kwargs = super().get_context_data(**kwargs)
- kwargs["app"] = "com"
- return kwargs
+ extra_context = {
+ "create_url": reverse_lazy("com:poster_create"),
+ "get_edit_url": lambda poster: reverse(
+ "com:poster_edit", kwargs={"poster_id": poster.id}
+ ),
+ }
+ permission_required = "com.view_poster"
class PosterCreateView(ComTabsMixin, PosterCreateBaseView):
@@ -672,17 +679,6 @@ class PosterDeleteView(PosterDeleteBaseView):
success_url = reverse_lazy("com:poster_list")
-class PosterModerateListView(PermissionRequiredMixin, ComTabsMixin, ListView):
- """Moderate list communication poster."""
-
- current_tab = "posters"
- model = Poster
- template_name = "com/poster_moderate.jinja"
- queryset = Poster.objects.filter(is_moderated=False).all()
- permission_required = "com.moderate_poster"
- extra_context = {"app": "com"}
-
-
class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
"""Moderate communication poster."""
@@ -690,12 +686,21 @@ class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View):
permission_required = "com.moderate_poster"
extra_context = {"app": "com"}
- def get(self, request, *args, **kwargs):
+ def post(self, request, *args, **kwargs):
obj = get_object_or_404(Poster, pk=kwargs["object_id"])
obj.is_moderated = True
obj.moderator = request.user
obj.save()
- return redirect("com:poster_moderate_list")
+ # The moderation request may be originated from a club context (/club/poster)
+ # or a global context (/com/poster),
+ # so the redirection URL will be the URL of the page that called this view,
+ # as long as the latter belongs to the sith.
+ referer = self.request.META.get("HTTP_REFERER")
+ if referer:
+ parsed = urlparse(referer)
+ if parsed.netloc == settings.SITH_URL:
+ return redirect(parsed.path)
+ return redirect("com:poster_list")
class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView):
diff --git a/core/api.py b/core/api.py
index af4daff5..ab69f86e 100644
--- a/core/api.py
+++ b/core/api.py
@@ -99,7 +99,7 @@ class SithFileController(ControllerBase):
@route.get(
"/search",
response=PaginatedResponseSchema[SithFileSchema],
- auth=[SessionAuth(), ApiKeyAuth()],
+ auth=[ApiKeyAuth(), SessionAuth()],
permissions=[CanAccessLookup],
)
@paginate(PageNumberPaginationExtra, page_size=50)
@@ -112,7 +112,7 @@ class GroupController(ControllerBase):
@route.get(
"/search",
response=PaginatedResponseSchema[GroupSchema],
- auth=[SessionAuth(), ApiKeyAuth()],
+ auth=[ApiKeyAuth(), SessionAuth()],
permissions=[CanAccessLookup],
)
@paginate(PageNumberPaginationExtra, page_size=50)
diff --git a/core/auth/mixins.py b/core/auth/mixins.py
index c4e603a9..917200ed 100644
--- a/core/auth/mixins.py
+++ b/core/auth/mixins.py
@@ -24,7 +24,6 @@
from __future__ import annotations
import types
-import warnings
from typing import TYPE_CHECKING, Any, LiteralString
from django.contrib.auth.mixins import AccessMixin, PermissionRequiredMixin
@@ -147,45 +146,6 @@ class GenericContentPermissionMixinBuilder(View):
return super().dispatch(request, *arg, **kwargs)
-class CanCreateMixin(View):
- """Protect any child view that would create an object.
-
- Raises:
- PermissionDenied:
- If the user has not the necessary permission
- to create the object of the view.
- """
-
- def __init_subclass__(cls, **kwargs):
- warnings.warn(
- f"{cls.__name__} is deprecated and should be replaced "
- "by other permission verification mecanism.",
- DeprecationWarning,
- stacklevel=2,
- )
- super().__init_subclass__(**kwargs)
-
- def __init__(self, *args, **kwargs):
- warnings.warn(
- f"{self.__class__.__name__} is deprecated and should be replaced "
- "by other permission verification mecanism.",
- DeprecationWarning,
- stacklevel=2,
- )
- super().__init__(*args, **kwargs)
-
- def dispatch(self, request, *arg, **kwargs):
- if not request.user.is_authenticated:
- raise PermissionDenied
- return super().dispatch(request, *arg, **kwargs)
-
- def form_valid(self, form):
- obj = form.instance
- if can_edit_prop(obj, self.request.user):
- return super().form_valid(form)
- raise PermissionDenied
-
-
class CanEditPropMixin(GenericContentPermissionMixinBuilder):
"""Ensure the user has owner permissions on the child view object.
diff --git a/core/management/commands/check_fs.py b/core/management/commands/check_fs.py
deleted file mode 100644
index 8e970ced..00000000
--- a/core/management/commands/check_fs.py
+++ /dev/null
@@ -1,40 +0,0 @@
-#
-# Copyright 2018
-# - Skia
-#
-# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
-# http://ae.utbm.fr.
-#
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the GNU General Public License a published by the Free Software
-# Foundation; either version 3 of the License, or (at your option) any later
-# version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-# details.
-#
-# You should have received a copy of the GNU General Public License along with
-# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
-# Place - Suite 330, Boston, MA 02111-1307, USA.
-#
-#
-
-from django.core.management.base import BaseCommand
-
-from core.models import SithFile
-
-
-class Command(BaseCommand):
- help = "Recursively check the file system with respect to the DB"
-
- def add_arguments(self, parser):
- parser.add_argument(
- "ids", metavar="ID", type=int, nargs="+", help="The file IDs to process"
- )
-
- def handle(self, *args, **options):
- files = SithFile.objects.filter(id__in=options["ids"]).all()
- for f in files:
- f._check_fs()
diff --git a/core/management/commands/repair_fs.py b/core/management/commands/repair_fs.py
deleted file mode 100644
index cf88d108..00000000
--- a/core/management/commands/repair_fs.py
+++ /dev/null
@@ -1,41 +0,0 @@
-#
-# Copyright 2018
-# - Skia
-#
-# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
-# http://ae.utbm.fr.
-#
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the GNU General Public License a published by the Free Software
-# Foundation; either version 3 of the License, or (at your option) any later
-# version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-# details.
-#
-# You should have received a copy of the GNU General Public License along with
-# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
-# Place - Suite 330, Boston, MA 02111-1307, USA.
-#
-#
-
-
-from django.core.management.base import BaseCommand
-
-from core.models import SithFile
-
-
-class Command(BaseCommand):
- help = "Recursively repair the file system with respect to the DB"
-
- def add_arguments(self, parser):
- parser.add_argument(
- "ids", metavar="ID", type=int, nargs="+", help="The file IDs to process"
- )
-
- def handle(self, *args, **options):
- files = SithFile.objects.filter(id__in=options["ids"]).all()
- for f in files:
- f._repair_fs()
diff --git a/core/models.py b/core/models.py
index 0506364a..a624ebf6 100644
--- a/core/models.py
+++ b/core/models.py
@@ -23,14 +23,12 @@
#
from __future__ import annotations
-import logging
-import os
import string
import unicodedata
from datetime import timedelta
from io import BytesIO
from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Self
+from typing import TYPE_CHECKING, Self
from uuid import uuid4
from django.conf import settings
@@ -97,48 +95,6 @@ def validate_promo(value: int) -> None:
)
-def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None:
- """Search for a group by its primary key or its name.
- Either one of the two must be set.
-
- The result is cached for the default duration (should be 5 minutes).
-
- Args:
- pk: The primary key of the group
- name: The name of the group
-
- Returns:
- The group if it exists, else None
-
- Raises:
- ValueError: If no group matches the criteria
- """
- if pk is None and name is None:
- raise ValueError("Either pk or name must be set")
-
- # replace space characters to hide warnings with memcached backend
- pk_or_name: str | int = pk if pk is not None else name.replace(" ", "_")
- group = cache.get(f"sith_group_{pk_or_name}")
-
- if group == "not_found":
- # Using None as a cache value is a little bit tricky,
- # so we use a special string to represent None
- return None
- elif group is not None:
- return group
- # if this point is reached, the group is not in cache
- if pk is not None:
- group = Group.objects.filter(pk=pk).first()
- else:
- group = Group.objects.filter(name=name).first()
- if group is not None:
- name = group.name.replace(" ", "_")
- cache.set_many({f"sith_group_{group.id}": group, f"sith_group_{name}": group})
- else:
- cache.set(f"sith_group_{pk_or_name}", "not_found")
- return group
-
-
class BanGroup(AuthGroup):
"""An anti-group, that removes permissions instead of giving them.
@@ -382,19 +338,18 @@ class User(AbstractUser):
Returns:
True if the user is the group, else False
"""
- if pk is not None:
- group: Optional[Group] = get_group(pk=pk)
- elif name is not None:
- group: Optional[Group] = get_group(name=name)
- else:
+ if not pk and not name:
raise ValueError("You must either provide the id or the name of the group")
- if group is None:
+ group_id: int | None = (
+ pk or Group.objects.filter(name=name).values_list("id", flat=True).first()
+ )
+ if group_id is None:
return False
- if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:
+ if group_id == settings.SITH_GROUP_SUBSCRIBERS_ID:
return self.is_subscribed
- if group.id == settings.SITH_GROUP_ROOT_ID:
+ if group_id == settings.SITH_GROUP_ROOT_ID:
return self.is_root
- return group in self.cached_groups
+ return any(g.id == group_id for g in self.cached_groups)
@cached_property
def cached_groups(self) -> list[Group]:
@@ -454,14 +409,6 @@ class User(AbstractUser):
else:
raise ValidationError(_("A user with that username already exists"))
- def get_profile(self):
- return {
- "last_name": self.last_name,
- "first_name": self.first_name,
- "nick_name": self.nick_name,
- "date_of_birth": self.date_of_birth,
- }
-
def get_short_name(self):
"""Returns the short name for the user."""
if self.nick_name:
@@ -689,8 +636,8 @@ class AnonymousUser(AuthAnonymousUser):
if pk is not None:
return pk == allowed_id
elif name is not None:
- group = get_group(name=name)
- return group is not None and group.id == allowed_id
+ group = Group.objects.get(id=allowed_id)
+ return group.name == name
else:
raise ValueError("You must either provide the id or the name of the group")
@@ -1016,63 +963,6 @@ class SithFile(models.Model):
self.clean()
self.save()
- def _repair_fs(self):
- """Rebuilds recursively the filesystem as it should be regarding the DB tree."""
- if self.is_folder:
- for c in self.children.all():
- c._repair_fs()
- return
- elif not self._check_path_consistence():
- # First get future parent path and the old file name
- # Prepend "." so that we match all relative handling of Django's
- # file storage
- parent_path = "." + self.parent.get_full_path()
- parent_full_path = settings.MEDIA_ROOT + parent_path
- os.makedirs(parent_full_path, exist_ok=True)
- old_path = self.file.name # Should be relative: "./users/skia/bleh.jpg"
- new_path = "." + self.get_full_path()
- try:
- # Make this atomic, so that a FS problem rolls back the DB change
- with transaction.atomic():
- # Set the new filesystem path
- self.file.name = new_path
- self.save()
- # Really move at the FS level
- if os.path.exists(parent_full_path):
- os.rename(
- settings.MEDIA_ROOT + old_path,
- settings.MEDIA_ROOT + new_path,
- )
- # Empty directories may remain, but that's not really a
- # problem, and that can be solved with a simple shell
- # command: `find . -type d -empty -delete`
- except Exception as e:
- logging.error(e)
-
- def _check_path_consistence(self):
- file_path = str(self.file)
- file_full_path = settings.MEDIA_ROOT + file_path
- db_path = ".%s" % self.get_full_path()
- if not os.path.exists(file_full_path):
- print("%s: WARNING: real file does not exists!" % self.id) # noqa T201
- print("file path: %s" % file_path, end="") # noqa T201
- print(" db path: %s" % db_path) # noqa T201
- return False
- if file_path != db_path:
- print("%s: " % self.id, end="") # noqa T201
- print("file path: %s" % file_path, end="") # noqa T201
- print(" db path: %s" % db_path) # noqa T201
- return False
- return True
-
- def _check_fs(self):
- if self.is_folder:
- for c in self.children.all():
- c._check_fs()
- return
- else:
- self._check_path_consistence()
-
@property
def is_file(self):
return not self.is_folder
@@ -1157,8 +1047,6 @@ class QuickUploadImage(models.Model):
identifier = str(uuid4())
name = Path(image.name).stem[: cls.IMAGE_NAME_SIZE - 1]
file = File(convert_image(image), name=f"{identifier}.webp")
- width, height = Image.open(file).size
-
return cls.objects.create(
uuid=identifier,
name=name,
diff --git a/core/static/bundled/alpine-index.js b/core/static/bundled/alpine-index.js
index daed0b27..ef273300 100644
--- a/core/static/bundled/alpine-index.js
+++ b/core/static/bundled/alpine-index.js
@@ -1,8 +1,9 @@
+import { limitedChoices } from "#core:alpine/limited-choices";
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications";
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
-Alpine.plugin(sort);
+Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
window.Alpine = Alpine;
diff --git a/core/static/bundled/alpine/limited-choices.ts b/core/static/bundled/alpine/limited-choices.ts
new file mode 100644
index 00000000..211441d0
--- /dev/null
+++ b/core/static/bundled/alpine/limited-choices.ts
@@ -0,0 +1,69 @@
+import type { Alpine as AlpineType } from "alpinejs";
+
+export function limitedChoices(Alpine: AlpineType) {
+ /**
+ * Directive to limit the number of elements
+ * that can be selected in a group of checkboxes.
+ *
+ * When the max numbers of selectable elements is reached,
+ * new elements will still be inserted, but oldest ones will be deselected.
+ * For example, if checkboxes A, B and C have been selected and the max
+ * number of selections is 3, then selecting D will result in having
+ * B, C and D selected.
+ *
+ * # Example in template
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ */
+ Alpine.directive(
+ "limited-choices",
+ (el, { expression }, { evaluateLater, effect }) => {
+ const getMaxChoices = evaluateLater(expression);
+ let maxChoices: number;
+ const inputs: HTMLInputElement[] = Array.from(
+ el.querySelectorAll("input[type='checkbox']"),
+ );
+ const checked = [] as HTMLInputElement[];
+
+ const manageDequeue = () => {
+ if (checked.length <= maxChoices) {
+ // There isn't too many checkboxes selected. Nothing to do
+ return;
+ }
+ const popped = checked.splice(0, checked.length - maxChoices);
+ for (const p of popped) {
+ p.checked = false;
+ }
+ };
+
+ for (const input of inputs) {
+ input.addEventListener("change", (_e) => {
+ if (input.checked) {
+ checked.push(input);
+ } else {
+ checked.splice(checked.indexOf(input), 1);
+ }
+ manageDequeue();
+ });
+ }
+ effect(() => {
+ getMaxChoices((value: string) => {
+ const previousValue = maxChoices;
+ maxChoices = Number.parseInt(value);
+ if (maxChoices < previousValue) {
+ // The maximum number of selectable items has been lowered.
+ // Some currently selected elements may need to be removed
+ manageDequeue();
+ }
+ });
+ });
+ },
+ );
+}
diff --git a/core/static/core/footer.scss b/core/static/core/footer.scss
index aa2e048f..3c0306e0 100644
--- a/core/static/core/footer.scss
+++ b/core/static/core/footer.scss
@@ -65,7 +65,7 @@ footer.bottom-links {
flex-wrap: wrap;
align-items: center;
background-color: $primary-neutral-dark-color;
- box-shadow: $shadow-color 0 0 15px;
+ box-shadow: black 0 8px 15px;
a {
color: $white-color;
diff --git a/core/static/core/header.scss b/core/static/core/header.scss
index 7eca52f9..f43819c3 100644
--- a/core/static/core/header.scss
+++ b/core/static/core/header.scss
@@ -11,7 +11,8 @@ $hovered-red-text-color: #ff4d4d;
.header {
box-sizing: border-box;
background-color: $deepblue;
- box-shadow: 3px 3px 3px 0 #dfdfdf;
+ box-shadow: black 0 1px 3px 0,
+ black 0 4px 8px 3px;
border-radius: 0;
width: 100%;
display: flex;
@@ -99,7 +100,7 @@ $hovered-red-text-color: #ff4d4d;
border-radius: 0;
margin: 0;
box-sizing: border-box;
- background-color: $deepblue;
+ background-color: transparent;
width: 45px;
height: 25px;
padding: 0;
@@ -331,7 +332,7 @@ $hovered-red-text-color: #ff4d4d;
padding: 10px;
z-index: 100;
border-radius: 10px;
- box-shadow: 3px 3px 3px 0 #767676;
+ @include shadow;
>ul {
list-style-type: none;
diff --git a/core/static/core/img/gala25_background.webp b/core/static/core/img/gala25_background.webp
new file mode 100644
index 00000000..978e9946
Binary files /dev/null and b/core/static/core/img/gala25_background.webp differ
diff --git a/core/static/core/img/gala25_logo.webp b/core/static/core/img/gala25_logo.webp
new file mode 100644
index 00000000..3cbdb6f7
Binary files /dev/null and b/core/static/core/img/gala25_logo.webp differ
diff --git a/core/static/core/style.scss b/core/static/core/style.scss
index 7522666b..c23303a7 100644
--- a/core/static/core/style.scss
+++ b/core/static/core/style.scss
@@ -271,8 +271,9 @@ body {
/*--------------------------------CONTENT------------------------------*/
#content {
- padding: 1em 1%;
- box-shadow: $shadow-color 0 5px 10px;
+ padding: 1.5em 2%;
+ border-radius: 5px;
+ box-shadow: black 0 8px 15px;
background: $white-color;
overflow: auto;
}
diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja
index 2be6cd54..356abdff 100644
--- a/core/templates/core/base.jinja
+++ b/core/templates/core/base.jinja
@@ -4,12 +4,22 @@
{% block head %}
{% block title %}Association des Étudiants de l'UTBM{% endblock %}
-
+
+
+ {% block metatags %}
+
+
+
+
+ {% endblock %}
@@ -34,6 +44,18 @@
{% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %}
+
{% endblock %}
diff --git a/core/templates/core/base/header.jinja b/core/templates/core/base/header.jinja
index cb47a47c..65a15968 100644
--- a/core/templates/core/base/header.jinja
+++ b/core/templates/core/base/header.jinja
@@ -1,6 +1,6 @@