Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add file storage #1361

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ build
.env
.idea
.env.example

ENV
.venv
venv
5 changes: 5 additions & 0 deletions deployment/compose/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ server {
expires 1d;
add_header Cache-Control "public";
}

location /usercontent/ {
internal;
alias /var/ephios/data/private/media/;
}
}
17 changes: 17 additions & 0 deletions ephios/extra/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from urllib.parse import urljoin, urlsplit

from django.conf import settings
from django.shortcuts import redirect

from ephios.core.services.notifications.types import NOTIFICATION_READ_PARAM_NAME

Expand Down Expand Up @@ -36,3 +39,17 @@ def __call__(self, request):
except Notification.DoesNotExist:
pass
return response


class EphiosMediaFileMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
response = self.get_response(request)
if (
request.get_host() == urlsplit(settings.GET_USERCONTENT_URL()).netloc
and request.resolver_match.url_name != "document"
):
return redirect(urljoin(settings.GET_SITE_URL(), request.path))
return response
Comment on lines +48 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do exactly? (Needs a comment)

Also, am I right to think this is specific to the documents plugin and wouldn't work to deliver e.g. custom logos?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah good point. We could move the list of "media views" to the settings

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand what it does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you request the usercontent domain, but on a view that does not serve usercontent (e.g. ephiosusercontent.de/accounts/login), it will redirect you to the same view but on the regular domain. This prevents the app from being served under the content domain, and therefore on the same domain as the usercontent

Empty file.
5 changes: 5 additions & 0 deletions ephios/plugins/files/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.contrib import admin

from ephios.plugins.files.models import Document

admin.site.register(Document)
15 changes: 15 additions & 0 deletions ephios/plugins/files/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.utils.translation import gettext_lazy as _

from ephios.core.plugins import PluginConfig


class PluginApp(PluginConfig):
name = "ephios.plugins.files"

class EphiosPluginMeta:
name = _("Files")
author = "Ephios Team"
description = _("This plugins allows you to upload files and link to them in events.")

def ready(self):
from . import signals # pylint: disable=unused-import
39 changes: 39 additions & 0 deletions ephios/plugins/files/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.forms import FileInput, Form, ModelForm, ModelMultipleChoiceField
from django.utils.translation import gettext as _
from django_select2.forms import Select2MultipleWidget

from ephios.core.forms.events import BasePluginFormMixin
from ephios.plugins.files.models import Document


class DocumentForm(ModelForm):
class Meta:
model = Document
fields = ["title", "file"]
widgets = {"file": FileInput(attrs={"accept": ".pdf"})}


class EventAttachedDocumentForm(BasePluginFormMixin, Form):
documents = ModelMultipleChoiceField(
queryset=Document.objects.all(), required=False, widget=Select2MultipleWidget
)

def __init__(self, *args, **kwargs):
kwargs.setdefault("prefix", "files")
self.event = kwargs.pop("event")
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
self.fields["documents"].initial = (
self.event.documents.all() if self.event and self.event.pk else []
)

def save(self):
if self.cleaned_data["documents"]:
self.event.documents.set(self.cleaned_data["documents"])

@property
def heading(self):
return _("Attach files")

def is_function_active(self):
return self.event and self.event.documents.exists()
36 changes: 36 additions & 0 deletions ephios/plugins/files/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 5.0.8 on 2024-09-23 19:05

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("core", "0029_alter_userprofile_date_of_birth"),
]

operations = [
migrations.CreateModel(
name="Document",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("title", models.CharField(max_length=255)),
(
"file",
models.FileField(
upload_to="documents/",
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
),
),
("attached_to", models.ManyToManyField(related_name="documents", to="core.event")),
],
),
]
Empty file.
13 changes: 13 additions & 0 deletions ephios/plugins/files/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.core.validators import FileExtensionValidator
from django.db import models

from ephios.core.models import Event


class Document(models.Model):
title = models.CharField(max_length=255)
file = models.FileField(upload_to="documents/", validators=[FileExtensionValidator(["pdf"])])
attached_to = models.ManyToManyField(Event, related_name="documents")

def __str__(self):
return str(self.title)
41 changes: 41 additions & 0 deletions ephios/plugins/files/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.dispatch import receiver
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import gettext as _

from ephios.core.signals import event_forms, event_info, management_settings_sections
from ephios.plugins.files.forms import EventAttachedDocumentForm


@receiver(
management_settings_sections,
dispatch_uid="ephios.plugins.pages.signals.files_settings_section",
)
def files_settings_section(sender, request, **kwargs):
return (
[
{
"label": _("Files"),
"url": reverse("files:settings_document_list"),
"active": request.resolver_match.url_name.startswith("settings_document"),
},
]
if request.user.has_perm("files.add_document")
else []
)


@receiver(
event_forms,
dispatch_uid="ephios.plugins.files.signals.files_event_forms",
)
def guests_event_forms(sender, event, request, **kwargs):
return [EventAttachedDocumentForm(request.POST or None, event=event, request=request)]


@receiver(event_info, dispatch_uid="ephios.plugins.files.signals.event_info")
def display_event_files(event, request, **kwargs):
if event.documents.exists():
return render_to_string(
"files/document_attachement.html", {"documents": event.documents.all()}, request
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div id="attached-documents" class="mb-3">
{% for document in documents %}
<a href="{% url "files:document" document.pk %}"><span class="badge bg-light text-dark"><i class="fas fa-file"></i> {{ document.title }}</span></a>
{% endfor %}
</div>
22 changes: 22 additions & 0 deletions ephios/plugins/files/templates/files/document_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}
{% load static %}

{% block title %}
{% translate "Delete file" %}
{% endblock %}

{% block settings_content %}
<div class="page-header">
<h2>{% translate "Delete file" %}</h2>
</div>

<form method="post">{% csrf_token %}
<p>{% blocktranslate trimmed with title=document.title %}
Are you sure you want to delete the file "{{ title }}"?
{% endblocktranslate %}</p>
<a role="button" class="btn btn-secondary" href="{% url "files:settings_document_list" %}">{% translate "Back" %}</a>
<button type="submit" class="btn btn-danger">{% translate "Delete" %}</button>
</form>

{% endblock %}
19 changes: 19 additions & 0 deletions ephios/plugins/files/templates/files/document_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "core/settings/settings_base.html" %}
{% load crispy_forms_filters %}
{% load i18n %}

{% block settings_content %}
<div class="page-header">
{% if page %}
<h2>{% translate "Edit file" %}</h2>
{% else %}
<h2>{% translate "Upload new file" %}</h2>
{% endif %}
</div>

<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary" value="Save">
</form>
{% endblock %}
33 changes: 33 additions & 0 deletions ephios/plugins/files/templates/files/document_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends "core/settings/settings_base.html" %}
{% load i18n %}

{% block settings_content %}
<a class="btn btn-secondary" href="{% url "files:settings_document_create" %}"><span
class="fa fa-plus"></span> {% translate "Add file" %}</a>
<table class="table table-striped display mt-2">
<thead>
<tr>
<th>{% translate "Title" %}</th>
<th>{% translate "Action" %}</th>
</tr>
</thead>
<tbody>
{% for document in document_list %}
<tr>
<td class="break-word">{{ document.title }}</td>
<td class="d-flex">
<a class="btn btn-secondary btn-sm text-nowrap me-1"
href="{% url "files:document" document.pk %}"><span
class="fa fa-download"></span> <span class="d-none d-md-inline">{% translate "Download" %}</span></a>
<a class="btn btn-secondary btn-sm text-nowrap me-1"
href="{% url "files:settings_document_edit" document.pk %}"><span
class="fa fa-edit"></span> <span class="d-none d-md-inline">{% translate "Edit" %}</span></a>
<a class="btn btn-secondary btn-sm text-nowrap me-1"
href="{% url "files:settings_document_delete" document.pk %}"><span
class="fa fa-trash"></span> <span class="d-none d-md-inline">{% translate "Delete" %}</span></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
28 changes: 28 additions & 0 deletions ephios/plugins/files/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.urls import path

from ephios.plugins.files.views import (
DocumentCreateView,
DocumentDeleteView,
DocumentListView,
DocumentUpdateView,
DocumentView,
)

app_name = "files"
urlpatterns = [
path("document/<int:pk>/", DocumentView.as_view(), name="document"),
path("settings/documents/", DocumentListView.as_view(), name="settings_document_list"),
path(
"settings/documents/create/", DocumentCreateView.as_view(), name="settings_document_create"
),
path(
"settings/documents/<int:pk>/edit/",
DocumentUpdateView.as_view(),
name="settings_document_edit",
),
path(
"settings/documents/<int:pk>/delete/",
DocumentDeleteView.as_view(),
name="settings_document_delete",
),
]
60 changes: 60 additions & 0 deletions ephios/plugins/files/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os
from urllib.parse import urlsplit

from django.conf import settings
from django.contrib.messages.views import SuccessMessageMixin
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from guardian.mixins import LoginRequiredMixin

from ephios.extra.mixins import CustomPermissionRequiredMixin
from ephios.plugins.files.forms import DocumentForm
from ephios.plugins.files.models import Document


class DocumentView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
if (loc := urlsplit(settings.GET_USERCONTENT_URL()).netloc) and request.get_host() != loc:
return redirect(settings.GET_USERCONTENT_URL() + request.path)
document = get_object_or_404(Document, id=kwargs["pk"])
if settings.FALLBACK_MEDIA_SERVING:
response = FileResponse(document.file)
else:
response = HttpResponse()
response["X-Accel-Redirect"] = document.file.url
response["Content-Disposition"] = (
"attachment; filename=" + os.path.split(document.file.name)[1]
)
Comment on lines +27 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would need a comment as well. Maybe we can craft this into an AcceleratedMediaResponse class to make it reusable?

return response


class DocumentListView(CustomPermissionRequiredMixin, ListView):
model = Document
permission_required = "files.add_document"


class DocumentCreateView(CustomPermissionRequiredMixin, SuccessMessageMixin, CreateView):
model = Document
permission_required = "files.add_document"
form_class = DocumentForm
success_url = reverse_lazy("files:settings_document_list")
success_message = _("File saved successfully.")


class DocumentUpdateView(CustomPermissionRequiredMixin, SuccessMessageMixin, UpdateView):
model = Document
permission_required = "files.change_document"
form_class = DocumentForm
success_url = reverse_lazy("files:settings_document_list")
success_message = _("File saved successfully.")


class DocumentDeleteView(CustomPermissionRequiredMixin, SuccessMessageMixin, DeleteView):
model = Document
permission_required = "files.delete_document"
success_url = reverse_lazy("files:settings_document_list")
success_message = _("File deleted successfully.")
Loading