diff --git a/club/models.py b/club/models.py index b2adf128..4d22b8e5 100644 --- a/club/models.py +++ b/club/models.py @@ -127,12 +127,22 @@ class Club(models.Model): def clean(self): self.check_loop() - def _change_unixname(self, new_name): + def _change_unixname(self, old_name, new_name): c = Club.objects.filter(unix_name=new_name).first() if c is None: + # Update all the groups names + Group.objects.filter(name=old_name).update(name=new_name) + Group.objects.filter(name=old_name + settings.SITH_BOARD_SUFFIX).update( + name=new_name + settings.SITH_BOARD_SUFFIX + ) + Group.objects.filter(name=old_name + settings.SITH_MEMBER_SUFFIX).update( + name=new_name + settings.SITH_MEMBER_SUFFIX + ) + if self.home: self.home.name = new_name self.home.save() + else: raise ValidationError(_("A club with that unix_name already exists")) diff --git a/core/models.py b/core/models.py index 12f4e683..8a01c2d5 100644 --- a/core/models.py +++ b/core/models.py @@ -919,6 +919,36 @@ class SithFile(models.Model): class Meta: verbose_name = _("file") + def can_be_managed_by(self, user: User) -> bool: + """ + Tell if the user can manage the file (edit, delete, etc.) or not. + Apply the following rules: + - If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone -> return True + - If the file is in the SAS, only the SAS admins (or roots) can manage it -> return True if the user is in the SAS admin group or is a root + - If the file is in the profiles directory, only the roots can manage it -> return True if the user is a root + + :returns: True if the file is managed by the SAS or within the profiles directory, False otherwise + """ + + # If the file is not in the SAS nor in the profiles directory, it can be "managed" by anyone + profiles_dir = SithFile.objects.filter(name="profiles").first() + if not self.is_in_sas and not profiles_dir in self.get_parent_list(): + return True + + # If the file is in the SAS, only the SAS admins (or roots) can manage it + if self.is_in_sas and ( + user.is_in_group(settings.SITH_GROUP_SAS_ADMIN_ID) or user.is_root + ): + return True + + # If the file is in the profiles directory, only the roots can manage it + if profiles_dir in self.get_parent_list() and ( + user.is_root or user.is_board_member + ): + return True + + return False + def is_owned_by(self, user): if user.is_anonymous: return False @@ -996,7 +1026,7 @@ class SithFile(models.Model): def save(self, *args, **kwargs): sas = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first() - self.is_in_sas = sas in self.get_parent_list() + self.is_in_sas = sas in self.get_parent_list() or self == sas copy_rights = False if self.id is None: copy_rights = True @@ -1130,12 +1160,6 @@ class SithFile(models.Model): return Album.objects.filter(id=self.id).first() - def __str__(self): - if self.is_folder: - return _("Folder: ") + self.name - else: - return _("File: ") + self.name - def get_parent_list(self): l = [] p = self.parent diff --git a/core/static/core/navbar.scss b/core/static/core/navbar.scss index 311825d9..199ca695 100644 --- a/core/static/core/navbar.scss +++ b/core/static/core/navbar.scss @@ -130,5 +130,67 @@ nav.navbar { } } } - } + + > .menu > .head, + > .link { + color: white; + padding: 10px 20px; + box-sizing: border-box; + + @media (max-width: 500px) { + padding: 10px; + } + } + + .link:hover, + .menu:hover { + background-color: rgba(0, 0, 0, .2); + } + + > .menu:hover > .content, + > .menu > .head:hover + .content, + > .menu > .content:hover { + display: flex; + } + + > .menu { + display: flex; + position: relative; + + > .content { + z-index: 10; + display: none; + position: absolute; + top: 100%; + background-color: white; + margin: 0; + list-style-type: none; + width: 130px; + box-shadow: 3px 3px 3px 0 #dfdfdf; + flex-direction: column; + + @media (max-width: 500px) { + position: absolute; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + box-shadow: inset 3px 3px 3px 0 #dfdfdf; + } + + > li > a { + display: flex; + padding: 15px 20px; + + @media (max-width: 500px) { + padding: 10px; + } + + &:hover { + color: hsl(203, 75%, 40%); + background-color: rgba(0, 0, 0, .05); + } + } + } + } + } } \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 0e98da6b..a053e2d5 100644 --- a/core/utils.py +++ b/core/utils.py @@ -35,11 +35,13 @@ def get_git_revision_short_hash() -> str: """ Return the short hash of the current commit """ - return ( - subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) - .decode("ascii") - .strip() - ) + try: + output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + if isinstance(output, bytes): + return output.decode("ascii").strip() + return output.strip() + except subprocess.CalledProcessError: + return "" def get_start_of_semester(d=date.today()): diff --git a/core/views/files.py b/core/views/files.py index 2833dc7b..1047f381 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -23,7 +23,7 @@ from django.views.generic.detail import SingleObjectMixin from django.forms.models import modelform_factory from django.conf import settings from django.utils.translation import gettext_lazy as _ -from django.http import HttpResponse +from django.http import Http404, HttpResponse from wsgiref.util import FileWrapper from django.urls import reverse from django.core.exceptions import PermissionDenied @@ -34,7 +34,12 @@ import os from ajax_select import make_ajax_field from core.models import SithFile, RealGroup, Notification -from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, can_view, not_found +from core.views import ( + CanViewMixin, + CanEditMixin, + CanEditPropMixin, + can_view, +) from counter.models import Counter @@ -58,6 +63,11 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"): raise PermissionDenied name = f.__getattribute__(file_attr).name filepath = os.path.join(settings.MEDIA_ROOT, name) + + # check if file exists on disk + if not os.path.exists(filepath.encode("utf-8")): + raise Http404() + with open(filepath.encode("utf-8"), "rb") as filename: wrapper = FileWrapper(filename) response = HttpResponse(wrapper, content_type=f.mime_type) @@ -152,6 +162,13 @@ class FileEditView(CanEditMixin, UpdateView): template_name = "core/file_edit.jinja" context_object_name = "file" + def get(self, request, *args, **kwargs): + self.object = self.get_object() + if not self.object.can_be_managed_by(request.user): + raise PermissionDenied + + return super(FileEditView, self).get(request, *args, **kwargs) + def get_form_class(self): fields = ["name", "is_moderated"] if self.object.is_file: @@ -197,6 +214,13 @@ class FileEditPropView(CanEditPropMixin, UpdateView): context_object_name = "file" form_class = FileEditPropForm + def get(self, request, *args, **kwargs): + self.object = self.get_object() + if not self.object.can_be_managed_by(request.user): + raise PermissionDenied + + return super(FileEditPropView, self).get(request, *args, **kwargs) + def get_form(self, form_class=None): form = super(FileEditPropView, self).get_form(form_class) form.fields["parent"].queryset = SithFile.objects.filter(is_folder=True) @@ -269,6 +293,9 @@ class FileView(CanViewMixin, DetailView, FormMixin): def get(self, request, *args, **kwargs): self.form = self.get_form() + if not self.object.can_be_managed_by(request.user): + raise PermissionDenied + if "clipboard" not in request.session.keys(): request.session["clipboard"] = [] return super(FileView, self).get(request, *args, **kwargs) @@ -316,6 +343,13 @@ class FileDeleteView(CanEditPropMixin, DeleteView): template_name = "core/file_delete_confirm.jinja" context_object_name = "file" + def get(self, request, *args, **kwargs): + self.object = self.get_object() + if not self.object.can_be_managed_by(request.user): + raise PermissionDenied + + return super(FileDeleteView, self).get(request, *args, **kwargs) + def get_success_url(self): self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here if "next" in self.request.GET.keys(): diff --git a/core/views/page.py b/core/views/page.py index 94356c33..c2c7dbce 100644 --- a/core/views/page.py +++ b/core/views/page.py @@ -82,6 +82,11 @@ class PageRevView(CanViewMixin, DetailView): def dispatch(self, request, *args, **kwargs): res = super(PageRevView, self).dispatch(request, *args, **kwargs) + self.object = self.get_object() + + if self.object is None: + return redirect("core:page_create", page_name=self.kwargs["page_name"]) + if self.object.need_club_redirection: return redirect( "club:club_view_rev", club_id=self.object.club.id, rev_id=kwargs["rev"] diff --git a/core/views/site.py b/core/views/site.py index 9a6355a4..c34cf2c4 100644 --- a/core/views/site.py +++ b/core/views/site.py @@ -31,6 +31,7 @@ from django.utils import html from django.views.generic import ListView, TemplateView from django.conf import settings from django.utils.text import slugify +from django.db.models.query import QuerySet import json @@ -51,12 +52,15 @@ class NotificationList(ListView): model = Notification template_name = "core/notification_list.jinja" - def get_queryset(self): + def get_queryset(self) -> QuerySet[Notification]: + if self.request.user.is_anonymous: + return Notification.objects.none() # TODO: Bulk update in django 2.2 if "see_all" in self.request.GET.keys(): for n in self.request.user.notifications.filter(viewed=False): n.viewed = True n.save() + return self.request.user.notifications.order_by("-date")[:20] diff --git a/core/views/user.py b/core/views/user.py index eeba4845..dbd60f13 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -321,7 +321,7 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): last_album = None for picture in picture_qs: album = picture.parent - if album.id != last_album: + if album.id != last_album and album not in kwargs["albums"]: kwargs["albums"].append(album) kwargs["pictures"][album.id] = [] last_album = album.id @@ -719,8 +719,12 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): def get_context_data(self, **kwargs): kwargs = super(UserPreferencesView, self).get_context_data(**kwargs) - if not hasattr(self.object, "trombi_user"): + + if not ( + hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi + ): kwargs["trombi_form"] = UserTrombiForm() + if hasattr(self.object, "customer"): kwargs["student_card_form"] = StudentCardForm() return kwargs diff --git a/counter/views.py b/counter/views.py index 9e6df235..4d5af292 100644 --- a/counter/views.py +++ b/counter/views.py @@ -583,7 +583,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): - , where the string is the code of the product - X, where the integer is the quantity and str the code """ - string = parse_qs(request.body.decode())["code"][0].upper() + string = parse_qs(request.body.decode()).get("code", [""])[0].upper() if string == "FIN": return self.finish(request) elif string == "ANN": diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index b545b4a9..b9b1681c 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -5569,7 +5569,7 @@ msgid "German" msgstr "Allemant" #: sith/settings.py:449 -msgid "Spanich" +msgid "Spanish" msgstr "Espagnol" #: sith/settings.py:453 diff --git a/pedagogy/views.py b/pedagogy/views.py index f486aa14..13151c60 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -206,7 +206,9 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView): except TypeError: return self.model.objects.none() - return queryset.filter(id__in=([o.object.id for o in qs])) + return queryset.filter( + id__in=([o.object.id for o in qs if o.object is not None]) + ) class UVCommentReportCreateView(CanCreateMixin, CreateView): diff --git a/rootplace/tests.py b/rootplace/tests.py index b6b957db..f1bb174f 100644 --- a/rootplace/tests.py +++ b/rootplace/tests.py @@ -144,11 +144,12 @@ class MergeUserTest(TestCase): 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( + self.assertAlmostEqual( today + timedelta(10 * 30), self.to_keep.subscriptions.order_by("subscription_end") .last() .subscription_end, + delta=timedelta(1), ) def test_godfathers(self): diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index 848f0aec..711a0c6c 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -167,8 +167,10 @@ switch (e.keyCode) { case 37: $('#prev a')[0].click(); + break; case 39: $('#next a')[0].click(); + break; } }); }); diff --git a/sith/settings.py b/sith/settings.py index 9b2ed357..26a013a0 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -452,7 +452,7 @@ SITH_PEDAGOGY_UV_LANGUAGE = [ ("FR", _("French")), ("EN", _("English")), ("DE", _("German")), - ("SP", _("Spanich")), + ("SP", _("Spanish")), ] SITH_PEDAGOGY_UV_RESULT_GRADE = [ diff --git a/trombi/templates/trombi/detail.jinja b/trombi/templates/trombi/detail.jinja index 85556703..bbd27804 100644 --- a/trombi/templates/trombi/detail.jinja +++ b/trombi/templates/trombi/detail.jinja @@ -14,8 +14,8 @@

{% trans %}Add user{% endtrans %}

- {% csrf_token %} - {{ form.as_p() }} + {% csrf_token %} + {{ form.as_p() }}

diff --git a/trombi/templates/trombi/user_tools.jinja b/trombi/templates/trombi/user_tools.jinja index 19056111..0a45742b 100644 --- a/trombi/templates/trombi/user_tools.jinja +++ b/trombi/templates/trombi/user_tools.jinja @@ -38,18 +38,18 @@
{{ u.user.get_display_name() }}
{% if trombi.show_profiles %} -
- {% trans %}Profile{% endtrans %} -
+
+ {% trans %}Profile{% endtrans %} +
{% endif %}
{% if can_comment %} - {% set comment = u.received_comments.filter(author__id=user.trombi_user.id).first() %} - {% if comment %} - {% trans %}Edit comment{% endtrans %} - {% else %} - {% trans %}Comment{% endtrans %} - {% endif %} + {% set comment = u.received_comments.filter(author__id=user.trombi_user.id).first() %} + {% if comment %} + {% trans %}Edit comment{% endtrans %} + {% else %} + {% trans %}Comment{% endtrans %} + {% endif %} {% endif %}
diff --git a/trombi/views.py b/trombi/views.py index 590d98fc..98bf5fc2 100644 --- a/trombi/views.py +++ b/trombi/views.py @@ -462,6 +462,10 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView): def get(self, request, *args, **kwargs): self.object = self.get_object() + + if request.user.is_anonymous: + raise PermissionDenied() + if ( self.object.trombi.id != request.user.trombi_user.trombi.id or self.object.user.id == request.user.id