Sith/core/views/files.py

364 lines
13 KiB
Python
Raw Normal View History

# -*- coding:utf-8 -*
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
2016-08-10 03:48:06 +00:00
# This file contains all the views that concern the page model
from django.shortcuts import redirect, get_object_or_404
from django.utils.http import http_date
2016-11-09 08:13:57 +00:00
from django.views.generic import ListView, DetailView, TemplateView
2017-06-12 07:42:03 +00:00
from django.views.generic.edit import UpdateView, FormMixin, DeleteView
2016-11-09 08:13:57 +00:00
from django.views.generic.detail import SingleObjectMixin
2016-08-10 03:48:06 +00:00
from django.forms.models import modelform_factory
from django.conf import settings
from django.utils.translation import gettext_lazy as _
2016-08-10 03:48:06 +00:00
from django.http import HttpResponse
2018-04-18 23:13:05 +00:00
from wsgiref.util import FileWrapper
from django.urls import reverse
2017-06-12 07:42:03 +00:00
from django.core.exceptions import PermissionDenied
2016-08-10 03:48:06 +00:00
from django import forms
import os
2017-06-12 07:42:03 +00:00
from ajax_select import make_ajax_field
2016-12-18 16:59:08 +00:00
2016-12-08 18:47:28 +00:00
from core.models import SithFile, RealGroup, Notification
2017-06-12 07:42:03 +00:00
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, can_view, not_found
2016-12-18 10:55:45 +00:00
from counter.models import Counter
2016-08-10 03:48:06 +00:00
2017-06-12 07:42:03 +00:00
2016-11-20 10:56:33 +00:00
def send_file(request, file_id, file_class=SithFile, file_attr="file"):
2016-08-10 03:48:06 +00:00
"""
Send a file through Django without loading the whole file into
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
"""
f = get_object_or_404(file_class, id=file_id)
2018-10-04 19:29:19 +00:00
if not (
can_view(f, request.user)
or (
"counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
)
):
2016-08-10 03:48:06 +00:00
raise PermissionDenied
2016-11-20 10:56:33 +00:00
name = f.__getattribute__(file_attr).name
2017-03-30 17:13:47 +00:00
filepath = os.path.join(settings.MEDIA_ROOT, name)
2018-10-04 19:29:19 +00:00
with open(filepath.encode("utf-8"), "rb") as filename:
2016-08-10 03:48:06 +00:00
wrapper = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type)
response["Last-Modified"] = http_date(f.date.timestamp())
2018-10-04 19:29:19 +00:00
response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
"utf-8"
)
2016-08-10 03:48:06 +00:00
return response
2017-06-12 07:42:03 +00:00
class AddFilesForm(forms.Form):
2018-10-04 19:29:19 +00:00
folder_name = forms.CharField(
label=_("Add a new folder"), max_length=30, required=False
)
file_field = forms.FileField(
widget=forms.ClearableFileInput(attrs={"multiple": True}),
label=_("Files"),
required=False,
)
2016-08-10 03:48:06 +00:00
def process(self, parent, owner, files):
2016-12-08 18:47:28 +00:00
notif = False
2016-08-10 03:48:06 +00:00
try:
2018-10-04 19:29:19 +00:00
if self.cleaned_data["folder_name"] != "":
folder = SithFile(
parent=parent, name=self.cleaned_data["folder_name"], owner=owner
)
2016-08-10 03:48:06 +00:00
folder.clean()
folder.save()
2016-12-08 18:47:28 +00:00
notif = True
2016-08-10 03:48:06 +00:00
except Exception as e:
2018-10-04 19:29:19 +00:00
self.add_error(
None,
_("Error creating folder %(folder_name)s: %(msg)s")
% {"folder_name": self.cleaned_data["folder_name"], "msg": repr(e)},
)
2016-08-10 03:48:06 +00:00
for f in files:
2018-10-04 19:29:19 +00:00
new_file = SithFile(
parent=parent,
name=f.name,
file=f,
owner=owner,
is_folder=False,
mime_type=f.content_type,
size=f.size,
2018-10-04 19:29:19 +00:00
)
2016-08-10 03:48:06 +00:00
try:
new_file.clean()
new_file.save()
2016-12-08 18:47:28 +00:00
notif = True
2016-08-10 03:48:06 +00:00
except Exception as e:
2018-10-04 19:29:19 +00:00
self.add_error(
None,
_("Error uploading file %(file_name)s: %(msg)s")
% {"file_name": f, "msg": repr(e)},
)
2016-12-08 18:47:28 +00:00
if notif:
2018-10-04 19:29:19 +00:00
for u in (
RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID)
.first()
.users.all()
):
if not u.notifications.filter(
type="FILE_MODERATION", viewed=False
).exists():
Notification(
user=u,
url=reverse("core:file_moderation"),
type="FILE_MODERATION",
).save()
2016-12-08 18:47:28 +00:00
2017-06-12 07:42:03 +00:00
2016-08-10 14:23:12 +00:00
class FileListView(ListView):
2018-10-04 19:29:19 +00:00
template_name = "core/file_list.jinja"
2016-08-10 03:48:06 +00:00
context_object_name = "file_list"
def get_queryset(self):
return SithFile.objects.filter(parent=None)
def get_context_data(self, **kwargs):
kwargs = super(FileListView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["popup"] = ""
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
kwargs["popup"] = "popup"
2016-08-10 03:48:06 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-08-10 03:48:06 +00:00
class FileEditView(CanEditMixin, UpdateView):
model = SithFile
pk_url_kwarg = "file_id"
2018-10-04 19:29:19 +00:00
template_name = "core/file_edit.jinja"
2016-08-10 03:48:06 +00:00
context_object_name = "file"
def get_form_class(self):
2018-10-04 19:29:19 +00:00
fields = ["name", "is_moderated"]
2016-08-10 03:48:06 +00:00
if self.object.is_file:
2018-10-04 19:29:19 +00:00
fields = ["file"] + fields
2016-08-10 03:48:06 +00:00
return modelform_factory(SithFile, fields=fields)
def get_success_url(self):
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
return reverse(
"core:file_detail", kwargs={"file_id": self.object.id, "popup": "popup"}
)
return reverse(
"core:file_detail", kwargs={"file_id": self.object.id, "popup": ""}
)
2016-08-10 03:48:06 +00:00
def get_context_data(self, **kwargs):
kwargs = super(FileEditView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["popup"] = ""
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
kwargs["popup"] = "popup"
2016-08-10 03:48:06 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-12-18 16:59:08 +00:00
class FileEditPropForm(forms.ModelForm):
class Meta:
model = SithFile
2018-10-04 19:29:19 +00:00
fields = ["parent", "owner", "edit_groups", "view_groups"]
parent = make_ajax_field(SithFile, "parent", "files", help_text="")
edit_groups = make_ajax_field(
SithFile, "edit_groups", "groups", help_text="", label=_("edit group")
)
view_groups = make_ajax_field(
SithFile, "view_groups", "groups", help_text="", label=_("view group")
)
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
2016-12-18 16:59:08 +00:00
2017-06-12 07:42:03 +00:00
2016-08-10 03:48:06 +00:00
class FileEditPropView(CanEditPropMixin, UpdateView):
model = SithFile
pk_url_kwarg = "file_id"
2018-10-04 19:29:19 +00:00
template_name = "core/file_edit.jinja"
2016-08-10 03:48:06 +00:00
context_object_name = "file"
2016-12-18 16:59:08 +00:00
form_class = FileEditPropForm
2016-08-10 03:48:06 +00:00
def get_form(self, form_class=None):
form = super(FileEditPropView, self).get_form(form_class)
2018-10-04 19:29:19 +00:00
form.fields["parent"].queryset = SithFile.objects.filter(is_folder=True)
2016-08-10 03:48:06 +00:00
return form
def form_valid(self, form):
ret = super(FileEditPropView, self).form_valid(form)
2018-10-04 19:29:19 +00:00
if form.cleaned_data["recursive"]:
self.object.apply_rights_recursively()
return ret
2016-08-10 03:48:06 +00:00
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse(
"core:file_detail",
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
2018-10-04 19:29:19 +00:00
)
2016-08-10 03:48:06 +00:00
def get_context_data(self, **kwargs):
kwargs = super(FileEditPropView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["popup"] = ""
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
kwargs["popup"] = "popup"
2016-08-10 03:48:06 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-08-10 12:48:18 +00:00
class FileView(CanViewMixin, DetailView, FormMixin):
2016-08-10 03:48:06 +00:00
"""This class handle the upload of new files into a folder"""
2018-10-04 19:29:19 +00:00
2016-08-10 03:48:06 +00:00
model = SithFile
pk_url_kwarg = "file_id"
2018-10-04 19:29:19 +00:00
template_name = "core/file_detail.jinja"
2016-08-10 03:48:06 +00:00
context_object_name = "file"
form_class = AddFilesForm
2016-08-10 03:48:06 +00:00
2016-12-13 16:17:58 +00:00
def handle_clipboard(request, object):
"""
This method handles the clipboard in the view.
This method can fail, since it does not catch the exceptions coming from
below, allowing proper handling in the calling view.
Use this method like this:
FileView.handle_clipboard(request, self.object)
`request` is usually the self.request object in your view
`object` is the SithFile object you want to put in the clipboard, or
where you want to paste the clipboard
"""
2018-10-04 19:29:19 +00:00
if "delete" in request.POST.keys():
for f_id in request.POST.getlist("file_list"):
2016-12-13 16:17:58 +00:00
sf = SithFile.objects.filter(id=f_id).first()
if sf:
sf.delete()
2018-10-04 19:29:19 +00:00
if "clear" in request.POST.keys():
request.session["clipboard"] = []
if "cut" in request.POST.keys():
for f_id in request.POST.getlist("file_list"):
2016-12-13 16:17:58 +00:00
f_id = int(f_id)
2018-10-04 19:29:19 +00:00
if (
f_id in [c.id for c in object.children.all()]
and f_id not in request.session["clipboard"]
):
request.session["clipboard"].append(f_id)
if "paste" in request.POST.keys():
for f_id in request.session["clipboard"]:
2016-12-13 16:17:58 +00:00
sf = SithFile.objects.filter(id=f_id).first()
if sf:
sf.move_to(object)
2018-10-04 19:29:19 +00:00
request.session["clipboard"] = []
2016-12-13 16:17:58 +00:00
request.session.modified = True
2016-08-10 03:48:06 +00:00
def get(self, request, *args, **kwargs):
self.form = self.get_form()
2018-10-04 19:29:19 +00:00
if "clipboard" not in request.session.keys():
request.session["clipboard"] = []
2016-08-10 03:48:06 +00:00
return super(FileView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if "clipboard" not in request.session.keys():
request.session["clipboard"] = []
2016-12-12 23:45:20 +00:00
if request.user.can_edit(self.object):
# XXX this call can fail!
2016-12-13 16:17:58 +00:00
FileView.handle_clipboard(request, self.object)
2017-06-12 07:42:03 +00:00
self.form = self.get_form() # The form handle only the file upload
2018-10-04 19:29:19 +00:00
files = request.FILES.getlist("file_field")
if (
request.user.is_authenticated
2018-10-04 19:29:19 +00:00
and request.user.can_edit(self.object)
and self.form.is_valid()
):
2016-08-10 03:48:06 +00:00
self.form.process(parent=self.object, owner=request.user, files=files)
if self.form.is_valid():
return super(FileView, self).form_valid(self.form)
return self.form_invalid(self.form)
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse(
"core:file_detail",
kwargs={"file_id": self.object.id, "popup": self.kwargs.get("popup", "")},
2018-10-04 19:29:19 +00:00
)
2016-08-10 03:48:06 +00:00
def get_context_data(self, **kwargs):
kwargs = super(FileView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["popup"] = ""
kwargs["form"] = self.form
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
kwargs["popup"] = "popup"
kwargs["clipboard"] = SithFile.objects.filter(
id__in=self.request.session["clipboard"]
)
2016-08-10 03:48:06 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-08-10 03:48:06 +00:00
class FileDeleteView(CanEditPropMixin, DeleteView):
model = SithFile
pk_url_kwarg = "file_id"
2018-10-04 19:29:19 +00:00
template_name = "core/file_delete_confirm.jinja"
2016-08-10 03:48:06 +00:00
context_object_name = "file"
def get_success_url(self):
2017-06-12 07:42:03 +00:00
self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here
2018-10-04 19:29:19 +00:00
if "next" in self.request.GET.keys():
return self.request.GET["next"]
2016-08-10 03:48:06 +00:00
if self.object.parent is None:
2018-10-04 19:29:19 +00:00
return reverse(
"core:file_list", kwargs={"popup": self.kwargs.get("popup", "")}
2018-10-04 19:29:19 +00:00
)
return reverse(
"core:file_detail",
kwargs={
"file_id": self.object.parent.id,
"popup": self.kwargs.get("popup", ""),
2018-10-04 19:29:19 +00:00
},
)
2016-08-10 03:48:06 +00:00
def get_context_data(self, **kwargs):
kwargs = super(FileDeleteView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["popup"] = ""
if self.kwargs.get("popup") is not None:
2018-10-04 19:29:19 +00:00
kwargs["popup"] = "popup"
2016-08-10 03:48:06 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-11-09 08:13:57 +00:00
class FileModerationView(TemplateView):
template_name = "core/file_moderation.jinja"
def get_context_data(self, **kwargs):
kwargs = super(FileModerationView, self).get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["files"] = SithFile.objects.filter(is_moderated=False)[:100]
2016-11-09 08:13:57 +00:00
return kwargs
2017-06-12 07:42:03 +00:00
2016-11-09 08:13:57 +00:00
class FileModerateView(CanEditPropMixin, SingleObjectMixin):
model = SithFile
pk_url_kwarg = "file_id"
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.is_moderated = True
2016-12-12 16:23:06 +00:00
self.object.moderator = request.user
2016-11-09 08:13:57 +00:00
self.object.save()
2018-10-04 19:29:19 +00:00
if "next" in self.request.GET.keys():
return redirect(self.request.GET["next"])
return redirect("core:file_moderation")