[FIX] Correction de bugs (#617)

* Fix #600

* Fix #602

* Fixes & améliorations du nouveau CSS (#616)

* Fix #604

* should fix #605

* Fix #608

* Update core/views/site.py

Co-Authored-By: thomas girod <56346771+imperosol@users.noreply.github.com>

* Added back the permission denied

* Should fix #609

* Fix failing test when 2 user are merged

* Should fix #610

* Should fix #627

* Should fix #109

Block les URLs suivantes lorsque le fichier se trouve dans le dir `profiles` ou `SAS` :
- `/file/<id>/`
- `/file/<id>/[delete|prop|edit]`

> Les urls du SAS restent accessiblent pour les roots & les admins SAS
> Les urls de profiles sont uniquement accessiblent aux roots

* Fix root dir of SAS being unnaccessible for sas admins

⚠️ need to edit the SAS directory & save it (no changes required in sas directory properties)

* Remove overwritten code

* Should fix duplicated albums in user profile (wtf)

* Fix typo

* Extended profiles picture access to board members

* Should fix #607

* Fix keyboard navigation not working properly

* Fix user tagged pictures section inside python rather than in the template

* Update utils.py

* Apply suggested changes

* Fix #604

* Fix #608

* Added back the permission denied

* Should fix duplicated albums in user profile (wtf)

* Fix user tagged pictures section inside python rather than in the template

* Apply suggested changes

---------

Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com>
This commit is contained in:
Julien Constant 2023-05-02 13:07:36 +02:00 committed by GitHub
parent ef968f3673
commit b30ee0a27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 189 additions and 35 deletions

View File

@ -127,12 +127,22 @@ class Club(models.Model):
def clean(self): def clean(self):
self.check_loop() 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() c = Club.objects.filter(unix_name=new_name).first()
if c is None: 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: if self.home:
self.home.name = new_name self.home.name = new_name
self.home.save() self.home.save()
else: else:
raise ValidationError(_("A club with that unix_name already exists")) raise ValidationError(_("A club with that unix_name already exists"))

View File

@ -919,6 +919,36 @@ class SithFile(models.Model):
class Meta: class Meta:
verbose_name = _("file") 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): def is_owned_by(self, user):
if user.is_anonymous: if user.is_anonymous:
return False return False
@ -996,7 +1026,7 @@ class SithFile(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
sas = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first() 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 copy_rights = False
if self.id is None: if self.id is None:
copy_rights = True copy_rights = True
@ -1130,12 +1160,6 @@ class SithFile(models.Model):
return Album.objects.filter(id=self.id).first() 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): def get_parent_list(self):
l = [] l = []
p = self.parent p = self.parent

View File

@ -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);
}
}
}
}
}
} }

View File

@ -35,11 +35,13 @@ def get_git_revision_short_hash() -> str:
""" """
Return the short hash of the current commit Return the short hash of the current commit
""" """
return ( try:
subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) output = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
.decode("ascii") if isinstance(output, bytes):
.strip() return output.decode("ascii").strip()
) return output.strip()
except subprocess.CalledProcessError:
return ""
def get_start_of_semester(d=date.today()): def get_start_of_semester(d=date.today()):

View File

@ -23,7 +23,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _ 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 wsgiref.util import FileWrapper
from django.urls import reverse from django.urls import reverse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -34,7 +34,12 @@ import os
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
from core.models import SithFile, RealGroup, Notification 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 from counter.models import Counter
@ -58,6 +63,11 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
raise PermissionDenied raise PermissionDenied
name = f.__getattribute__(file_attr).name name = f.__getattribute__(file_attr).name
filepath = os.path.join(settings.MEDIA_ROOT, 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: with open(filepath.encode("utf-8"), "rb") as filename:
wrapper = FileWrapper(filename) wrapper = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type) response = HttpResponse(wrapper, content_type=f.mime_type)
@ -152,6 +162,13 @@ class FileEditView(CanEditMixin, UpdateView):
template_name = "core/file_edit.jinja" template_name = "core/file_edit.jinja"
context_object_name = "file" 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): def get_form_class(self):
fields = ["name", "is_moderated"] fields = ["name", "is_moderated"]
if self.object.is_file: if self.object.is_file:
@ -197,6 +214,13 @@ class FileEditPropView(CanEditPropMixin, UpdateView):
context_object_name = "file" context_object_name = "file"
form_class = FileEditPropForm 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): def get_form(self, form_class=None):
form = super(FileEditPropView, self).get_form(form_class) form = super(FileEditPropView, self).get_form(form_class)
form.fields["parent"].queryset = SithFile.objects.filter(is_folder=True) 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): def get(self, request, *args, **kwargs):
self.form = self.get_form() self.form = self.get_form()
if not self.object.can_be_managed_by(request.user):
raise PermissionDenied
if "clipboard" not in request.session.keys(): if "clipboard" not in request.session.keys():
request.session["clipboard"] = [] request.session["clipboard"] = []
return super(FileView, self).get(request, *args, **kwargs) return super(FileView, self).get(request, *args, **kwargs)
@ -316,6 +343,13 @@ class FileDeleteView(CanEditPropMixin, DeleteView):
template_name = "core/file_delete_confirm.jinja" template_name = "core/file_delete_confirm.jinja"
context_object_name = "file" 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): def get_success_url(self):
self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here 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(): if "next" in self.request.GET.keys():

View File

@ -82,6 +82,11 @@ class PageRevView(CanViewMixin, DetailView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
res = super(PageRevView, self).dispatch(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: if self.object.need_club_redirection:
return redirect( return redirect(
"club:club_view_rev", club_id=self.object.club.id, rev_id=kwargs["rev"] "club:club_view_rev", club_id=self.object.club.id, rev_id=kwargs["rev"]

View File

@ -31,6 +31,7 @@ from django.utils import html
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
from django.conf import settings from django.conf import settings
from django.utils.text import slugify from django.utils.text import slugify
from django.db.models.query import QuerySet
import json import json
@ -51,12 +52,15 @@ class NotificationList(ListView):
model = Notification model = Notification
template_name = "core/notification_list.jinja" 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 # TODO: Bulk update in django 2.2
if "see_all" in self.request.GET.keys(): if "see_all" in self.request.GET.keys():
for n in self.request.user.notifications.filter(viewed=False): for n in self.request.user.notifications.filter(viewed=False):
n.viewed = True n.viewed = True
n.save() n.save()
return self.request.user.notifications.order_by("-date")[:20] return self.request.user.notifications.order_by("-date")[:20]

View File

@ -321,7 +321,7 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
last_album = None last_album = None
for picture in picture_qs: for picture in picture_qs:
album = picture.parent album = picture.parent
if album.id != last_album: if album.id != last_album and album not in kwargs["albums"]:
kwargs["albums"].append(album) kwargs["albums"].append(album)
kwargs["pictures"][album.id] = [] kwargs["pictures"][album.id] = []
last_album = album.id last_album = album.id
@ -719,8 +719,12 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(UserPreferencesView, self).get_context_data(**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() kwargs["trombi_form"] = UserTrombiForm()
if hasattr(self.object, "customer"): if hasattr(self.object, "customer"):
kwargs["student_card_form"] = StudentCardForm() kwargs["student_card_form"] = StudentCardForm()
return kwargs return kwargs

View File

@ -583,7 +583,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
- <str>, where the string is the code of the product - <str>, where the string is the code of the product
- <int>X<str>, where the integer is the quantity and str the code - <int>X<str>, 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": if string == "FIN":
return self.finish(request) return self.finish(request)
elif string == "ANN": elif string == "ANN":

View File

@ -5569,7 +5569,7 @@ msgid "German"
msgstr "Allemant" msgstr "Allemant"
#: sith/settings.py:449 #: sith/settings.py:449
msgid "Spanich" msgid "Spanish"
msgstr "Espagnol" msgstr "Espagnol"
#: sith/settings.py:453 #: sith/settings.py:453

View File

@ -206,7 +206,9 @@ class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
except TypeError: except TypeError:
return self.model.objects.none() 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): class UVCommentReportCreateView(CanCreateMixin, CreateView):

View File

@ -144,11 +144,12 @@ class MergeUserTest(TestCase):
self.assertTrue(self.to_keep.is_subscribed) self.assertTrue(self.to_keep.is_subscribed)
# to_keep had 5 months of subscription remaining and received # to_keep had 5 months of subscription remaining and received
# 5 more months from to_delete, so he should be subscribed for 10 months # 5 more months from to_delete, so he should be subscribed for 10 months
self.assertEqual( self.assertAlmostEqual(
today + timedelta(10 * 30), today + timedelta(10 * 30),
self.to_keep.subscriptions.order_by("subscription_end") self.to_keep.subscriptions.order_by("subscription_end")
.last() .last()
.subscription_end, .subscription_end,
delta=timedelta(1),
) )
def test_godfathers(self): def test_godfathers(self):

View File

@ -167,8 +167,10 @@
switch (e.keyCode) { switch (e.keyCode) {
case 37: case 37:
$('#prev a')[0].click(); $('#prev a')[0].click();
break;
case 39: case 39:
$('#next a')[0].click(); $('#next a')[0].click();
break;
} }
}); });
}); });

View File

@ -452,7 +452,7 @@ SITH_PEDAGOGY_UV_LANGUAGE = [
("FR", _("French")), ("FR", _("French")),
("EN", _("English")), ("EN", _("English")),
("DE", _("German")), ("DE", _("German")),
("SP", _("Spanich")), ("SP", _("Spanish")),
] ]
SITH_PEDAGOGY_UV_RESULT_GRADE = [ SITH_PEDAGOGY_UV_RESULT_GRADE = [

View File

@ -14,8 +14,8 @@
<hr> <hr>
<h4>{% trans %}Add user{% endtrans %}</h4> <h4>{% trans %}Add user{% endtrans %}</h4>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.as_p() }}
<input type="submit" value="{% trans %}Add{% endtrans %}" /> <input type="submit" value="{% trans %}Add{% endtrans %}" />
</form> </form>
<hr> <hr>

View File

@ -38,18 +38,18 @@
</div> </div>
<div>{{ u.user.get_display_name() }}</div> <div>{{ u.user.get_display_name() }}</div>
{% if trombi.show_profiles %} {% if trombi.show_profiles %}
<div> <div>
<a href="{{ url("trombi:user_profile", user_id=u.id) }}">{% trans %}Profile{% endtrans %}</a> <a href="{{ url('trombi:user_profile', user_id=u.id) }}">{% trans %}Profile{% endtrans %}</a>
</div> </div>
{% endif %} {% endif %}
<div> <div>
{% if can_comment %} {% if can_comment %}
{% set comment = u.received_comments.filter(author__id=user.trombi_user.id).first() %} {% set comment = u.received_comments.filter(author__id=user.trombi_user.id).first() %}
{% if comment %} {% if comment %}
<a href="{{ url("trombi:edit_comment", comment_id=comment.id) }}">{% trans %}Edit comment{% endtrans %}</a> <a href="{{ url('trombi:edit_comment', comment_id=comment.id) }}">{% trans %}Edit comment{% endtrans %}</a>
{% else %} {% else %}
<a href="{{ url("trombi:new_comment", user_id=u.id) }}">{% trans %}Comment{% endtrans %}</a> <a href="{{ url('trombi:new_comment', user_id=u.id) }}">{% trans %}Comment{% endtrans %}</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -462,6 +462,10 @@ class UserTrombiProfileView(TrombiTabsMixin, DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
if request.user.is_anonymous:
raise PermissionDenied()
if ( if (
self.object.trombi.id != request.user.trombi_user.trombi.id self.object.trombi.id != request.user.trombi_user.trombi.id
or self.object.user.id == request.user.id or self.object.user.id == request.user.id