From d8bd066335f7c1fced08f5ef986ba06eba2fc831 Mon Sep 17 00:00:00 2001 From: "J.-S. Caux" <J.S.Caux@uva.nl> Date: Thu, 14 Feb 2019 05:23:13 +0100 Subject: [PATCH] Add model finances.SubsidyAttachment --- finances/forms.py | 44 ++++++++++++++++++- finances/migrations/0012_subsidyattachment.py | 27 ++++++++++++ finances/models.py | 25 +++++++++++ .../templates/finances/subsidy_detail.html | 13 ++++++ finances/urls.py | 2 + finances/views.py | 13 ++++++ partners/forms.py | 2 + scipost/storage.py | 2 +- 8 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 finances/migrations/0012_subsidyattachment.py diff --git a/finances/forms.py b/finances/forms.py index 4320a7d73..9142a9a9e 100644 --- a/finances/forms.py +++ b/finances/forms.py @@ -15,7 +15,7 @@ from dateutil.rrule import rrule, MONTHLY from common.forms import MonthYearWidget from scipost.fields import UserModelChoiceField -from .models import Subsidy, WorkLog +from .models import Subsidy, SubsidyAttachment, WorkLog class SubsidyForm(forms.ModelForm): @@ -28,6 +28,48 @@ class SubsidyForm(forms.ModelForm): 'date', 'date_until'] +class SubsidyAttachmentForm(forms.ModelForm): + class Meta: + model = SubsidyAttachment + fields = ( + 'name', + 'attachment', + 'publicly_visible', + ) + + def save(self, to_object, commit=True): + """ + This custom save method will automatically assign the file to the object + given when it is a valid instance type. + """ + attachment = super().save(commit=False) + + # Formset might save an empty Instance + if not attachment.name or not attachment.attachment: + return None + + if isinstance(to_object, Subsidy): + attachment.subsidy = to_object + else: + raise forms.ValidationError('You cannot save Attachments to this type of object.') + if commit: + attachment.save() + return attachment + + +class SubsidyAttachmentFormSet(forms.BaseModelFormSet): + def save(self, to_object, commit=True): + """ + This custom save method will automatically assign the file to the object + given when it is a valid instance type. + """ + returns = [] + for form in self.forms: + returns.append(form.save(to_object)) + return returns + + + class WorkLogForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.types = kwargs.pop('log_types', False) diff --git a/finances/migrations/0012_subsidyattachment.py b/finances/migrations/0012_subsidyattachment.py new file mode 100644 index 000000000..95ad7cb04 --- /dev/null +++ b/finances/migrations/0012_subsidyattachment.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2019-02-14 04:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import scipost.storage + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0011_auto_20190214_0224'), + ] + + operations = [ + migrations.CreateModel( + name='SubsidyAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(storage=scipost.storage.SecureFileStorage(), upload_to='UPLOADS/FINANCES/SUBSIDIES/ATTACHMENTS')), + ('name', models.CharField(max_length=128)), + ('publicly_visible', models.BooleanField(default=False)), + ('subsidy', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='finances.Subsidy')), + ], + ), + ] diff --git a/finances/models.py b/finances/models.py index 4c2a85bc1..cd3c7012e 100644 --- a/finances/models.py +++ b/finances/models.py @@ -13,6 +13,8 @@ from django.utils.html import format_html from .constants import SUBSIDY_TYPES, SUBSIDY_STATUS from .utils import id_to_slug +from scipost.storage import SecureFileStorage + class Subsidy(models.Model): """ @@ -53,6 +55,29 @@ class Subsidy(models.Model): return reverse('finances:subsidy_details', args=(self.id,)) +class SubsidyAttachment(models.Model): + """ + A document related to a Subsidy. + """ + attachment = models.FileField(upload_to='UPLOADS/FINANCES/SUBSIDIES/ATTACHMENTS', + storage=SecureFileStorage()) + name = models.CharField(max_length=128) + subsidy = models.ForeignKey('finances.Subsidy', related_name='attachments', + blank=True) + publicly_visible = models.BooleanField(default=False) + + + def get_absolute_url(self): + if self.subsidy: + return reverse('finances:subsidy_attachment', args=(self.subsidy.id, self.id)) + + + +########################### +# Work hours registration # +########################### + + class WorkLog(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL) comments = models.TextField(blank=True) diff --git a/finances/templates/finances/subsidy_detail.html b/finances/templates/finances/subsidy_detail.html index ab90b4673..2dd44bbbb 100644 --- a/finances/templates/finances/subsidy_detail.html +++ b/finances/templates/finances/subsidy_detail.html @@ -19,4 +19,17 @@ </div> </div> +<div class="row"> + <div class="col-12"> + <h3>Attachments</h3> + <ul> + {% for file in subsidy.attachments.all %} + <li><a href="{{ file.get_absolute_url }}" target="_blank">{{ file.name }}</a></li> + {% empty %} + <li>No attachment found</li> + {% endfor %} + </ul> + </div> +</div> + {% endblock content %} diff --git a/finances/urls.py b/finances/urls.py index 31bd7da08..0181a1fc9 100644 --- a/finances/urls.py +++ b/finances/urls.py @@ -18,6 +18,8 @@ urlpatterns = [ url(r'^subsidies/(?P<pk>[0-9]+)/delete/$', views.SubsidyDeleteView.as_view(), name='subsidy_delete'), url(r'^subsidies/(?P<pk>[0-9]+)/$', views.SubsidyDetailView.as_view(), name='subsidy_details'), + url(r'^subsidies/(?P<subsidy_id>[0-9]+)/attachments/(?P<attachment_id>[0-9]+)$', + views.subsidy_attachment, name='subsidy_attachment'), # Timesheets url(r'^timesheets$', views.timesheets, name='timesheets'), diff --git a/finances/views.py b/finances/views.py index 90f6c2103..6b45cb3a1 100644 --- a/finances/views.py +++ b/finances/views.py @@ -78,6 +78,19 @@ class SubsidyDetailView(DetailView): model = Subsidy +def subsidy_attachment(request, subsidy_id, attachment_id): + attachment = get_object_or_404(SubsidyAttachment.objects, + subsidy__id=subsidy_id, id=attachment_id) + + content_type, encoding = mimetypes.guess_type(attachment.attachment.path) + content_type = content_type or 'application/octet-stream' + response = HttpResponse(attachment.attachment.read(), content_type=content_type) + response["Content-Encoding"] = encoding + response['Content-Disposition'] = ('filename=%s' % attachment.name) + return response + + + ############################ # Timesheets and Work Logs # ############################ diff --git a/partners/forms.py b/partners/forms.py index 7696a6b10..daf2ed44d 100644 --- a/partners/forms.py +++ b/partners/forms.py @@ -472,6 +472,7 @@ class MembershipQueryForm(forms.Form): captcha = ReCaptchaField(attrs={'theme': 'clean'}, label='*Please verify to continue:') +#done class PartnersAttachmentForm(forms.ModelForm): class Meta: model = PartnersAttachment @@ -500,6 +501,7 @@ class PartnersAttachmentForm(forms.ModelForm): return attachment +# done class PartnersAttachmentFormSet(forms.BaseModelFormSet): def save(self, to_object, commit=True): """ diff --git a/scipost/storage.py b/scipost/storage.py index c755aa8b4..d2f3e0daf 100644 --- a/scipost/storage.py +++ b/scipost/storage.py @@ -10,7 +10,7 @@ from django.utils.functional import cached_property class SecureFileStorage(FileSystemStorage): """ Inherit default FileStorage system to prevent files from being publicly accessible - from an server location that is permitted to be opened without explicit permissions. + from a server location that is opened without this permission having been explicitly given. """ @cached_property def location(self): -- GitLab