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