From 24386f361c4460ec73d4e8640fef08d80c7a45b1 Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Fri, 29 Nov 2024 14:30:02 +0100
Subject: [PATCH] add individual budget CRUD views

---
 scipost_django/finances/forms.py              |  4 +
 scipost_django/funders/forms.py               | 51 +++++++++++-
 scipost_django/funders/models.py              |  5 ++
 .../funders/individual_budget_delete.html     | 69 ++++++++++++++++
 .../funders/individual_budget_detail.html     | 80 +++++++++++++++++++
 .../funders/individual_budget_form.html       | 40 ++++++++++
 .../funders/individual_budget_list.html       | 44 ++++++++++
 scipost_django/funders/urls.py                | 34 +++++++-
 scipost_django/funders/views.py               | 51 +++++++++++-
 9 files changed, 375 insertions(+), 3 deletions(-)
 create mode 100644 scipost_django/funders/templates/funders/individual_budget_delete.html
 create mode 100644 scipost_django/funders/templates/funders/individual_budget_detail.html
 create mode 100644 scipost_django/funders/templates/funders/individual_budget_form.html
 create mode 100644 scipost_django/funders/templates/funders/individual_budget_list.html

diff --git a/scipost_django/finances/forms.py b/scipost_django/finances/forms.py
index 8d9cc9999..d131b70f8 100644
--- a/scipost_django/finances/forms.py
+++ b/scipost_django/finances/forms.py
@@ -89,6 +89,10 @@ class SubsidyForm(forms.ModelForm):
         self.fields["collective"].help_text = (
             f"If missing, <a href='{subsidy_collective_create}'>create a new one</a>."
         )
+        individual_budget_create = reverse_lazy("funders:individual_budget_create")
+        self.fields["individual_budget"].help_text = (
+            f"If missing, <a href='{individual_budget_create}'>create a new one</a>."
+        )
 
     def clean(self):
         cleaned_data = super().clean()
diff --git a/scipost_django/funders/forms.py b/scipost_django/funders/forms.py
index c8b24b6db..fbed8a842 100644
--- a/scipost_django/funders/forms.py
+++ b/scipost_django/funders/forms.py
@@ -3,10 +3,14 @@ __license__ = "AGPL v3"
 
 
 from django import forms
+from django.urls import reverse_lazy
 
 from common.forms import HTMXDynSelWidget
 
-from .models import Funder, Grant
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit
+
+from .models import Funder, Grant, IndividualBudget
 
 from dal import autocomplete
 
@@ -73,3 +77,48 @@ class GrantSelectForm(forms.Form):
         queryset=Grant.objects.all(),
         widget=HTMXDynSelWidget(url="/funders/grant-autocomplete"),
     )
+
+
+class IndividualBudgetForm(forms.ModelForm):
+    required_css_class = "required-asterisk"
+
+    class Meta:
+        model = IndividualBudget
+        fields = [
+            "organization",
+            "description",
+            "holder",
+            "budget_number",
+            "fundref_id",
+        ]
+        widgets = {
+            "organization": autocomplete.ModelSelect2(
+                url=reverse_lazy("organizations:organization-autocomplete"),
+                attrs={
+                    "data-html": True,
+                    "style": "width: 100%",
+                },
+            ),
+            "holder": autocomplete.ModelSelect2(
+                url=reverse_lazy("profiles:profile-autocomplete"),
+                attrs={
+                    "data-html": True,
+                    "style": "width: 100%",
+                },
+            ),
+        }
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Div(
+                Div(Field("organization"), css_class="col-12 col-md-6"),
+                Div(Field("holder"), css_class="col-12 col-md-6"),
+                Div(Field("description"), css_class="col-12"),
+                Div(Field("budget_number"), css_class="col-12 col-md"),
+                Div(Field("fundref_id"), css_class="col-12 col-md"),
+                css_class="row",
+            ),
+            ButtonHolder(Submit("submit", "Submit", css_class="btn-sm")),
+        )
diff --git a/scipost_django/funders/models.py b/scipost_django/funders/models.py
index 5c99776ce..d15c97a93 100644
--- a/scipost_django/funders/models.py
+++ b/scipost_django/funders/models.py
@@ -127,6 +127,11 @@ class IndividualBudget(models.Model):
             ),
         ]
 
+    def get_absolute_url(self):
+        return reverse(
+            "funders:individual_budget_details", kwargs={"budget_id": self.id}
+        )
+
     @property
     def name(self):
         if self.budget_number:
diff --git a/scipost_django/funders/templates/funders/individual_budget_delete.html b/scipost_django/funders/templates/funders/individual_budget_delete.html
new file mode 100644
index 000000000..7d9427c74
--- /dev/null
+++ b/scipost_django/funders/templates/funders/individual_budget_delete.html
@@ -0,0 +1,69 @@
+{% extends 'finances/base.html' %}
+
+{% load bootstrap %}
+{% load scipost_extras %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item">Individual Budgets</span>
+  <span class="breadcrumb-item"><a href="{{ object.get_absolute_url }}">{{ object }}</a></span>
+  <span class="breadcrumb-item"><a href="#" class="active">Delete</a></span>
+{% endblock %}
+
+{% block pagetitle %}
+  : Delete Individual Budget
+{% endblock pagetitle %}
+
+{% block content %}
+
+  <hgroup class="highlight p-3 mb-3">
+    <h1>Delete {{ object|object_name }}</h1>
+    <p class="m-0 fs-4">
+      <a href="{{ object.get_absolute_url }}">{{ object }}</a>
+    </p>
+  </hgroup>
+
+  <div class="row">
+    <div class="col-12">
+      <h2>
+        <a href="{{ object.get_absolute_url }}">{{ object }}</a>
+      </h2>
+ 
+
+      {% for field in object|get_fields %}
+        {% with object|get_field_value:field.name as field_value %}
+
+          {% if not field_value or field.name == 'id' or field.name == 'subsidies_funded' %}
+          {% else %}
+            <div class="fs-5 fw-bold">{{ field.verbose_name|title }}</div>
+            <p>
+
+              {% if field.is_relation %}
+                <a href="{{ field_value.get_absolute_url }}">{{ field_value }}</a>
+              {% else %}
+                {{ field_value }}
+              {% endif %}
+
+            </p>
+          {% endif %}
+
+        {% endwith %}
+      {% endfor %}
+
+ 
+      <div class="col-12">
+        <form method="post">
+          {% csrf_token %}
+          <div class="fs-5 mb-2">
+            Are you sure you want to delete this {{ object|object_name }}?
+          </div>
+          <p>
+            Deleting this {{ object|object_name }} will <strong>not</strong> delete the subsidies associated with it.
+          </p>
+          <input type="submit" class="btn btn-danger" value="Yes, delete it" />
+          <a href="{{ object.get_absolute_url }}" class="btn btn-secondary">Cancel</a>
+        </form>
+      </div>
+    </div>
+
+  {% endblock content %}
diff --git a/scipost_django/funders/templates/funders/individual_budget_detail.html b/scipost_django/funders/templates/funders/individual_budget_detail.html
new file mode 100644
index 000000000..bea071f1d
--- /dev/null
+++ b/scipost_django/funders/templates/funders/individual_budget_detail.html
@@ -0,0 +1,80 @@
+{% extends 'funders/base.html' %}
+
+{% load bootstrap %}
+{% load scipost_extras %}
+
+{% block meta_description %}
+  {{ block.super }} {{ object|object_name }} Detail
+{% endblock meta_description %}
+
+{% block pagetitle %}
+  : {{ object|object_name }} details
+{% endblock pagetitle %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="{% url 'funders:individual_budgets' %}">Individual Budgets</a></span>
+  <span class="breadcrumb-item"><a href="#" class="active">{{ object }}</a></span>
+{% endblock %}
+
+{% block content %}
+
+  <div class="highlight p-3 d-flex flex-row justify-content-between align-items-center mb-3">
+    <h1>{{ object }}</h1>
+    <div class="dropdown">
+      <button class="btn btn-sm btn-light dropdown-toggle"
+              type="button"
+              data-bs-toggle="dropdown"
+              aria-expanded="false">
+        <span>Actions</span>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-end">
+        <li>
+          <a href="{% url 'funders:individual_budget_update' budget_id=object.id %}"
+             class="dropdown-item">Edit</a>
+        </li>
+        <li>
+          <a href="{% url 'funders:individual_budget_delete' budget_id=object.id %}"
+             class="dropdown-item">Delete</a>
+        </li>
+      </ul>
+    </div>
+  </div>
+ 
+
+  {% for field in object|get_fields %}
+    {% with object|get_field_value:field.name as field_value %}
+
+      {% if not field_value or field.name == 'id' or field.name == 'subsidies_funded' %}
+      {% else %}
+        <div class="fs-5 fw-bold">{{ field.verbose_name|title }}</div>
+        <p>
+
+          {% if field.is_relation %}
+            <a href="{{ field_value.get_absolute_url }}">{{ field_value }}</a>
+          {% else %}
+            {{ field_value }}
+          {% endif %}
+
+        </p>
+      {% endif %}
+
+    {% endwith %}
+  {% endfor %}
+
+  <h2>Subsidies funded from this {{ object|object_name }}</h2>
+  <ul class="list-unstyled">
+
+    {% for subsidy in object.subsidies_funded.all %}
+      <li>
+        <a href="{{ subsidy.get_absolute_url }}">{{ subsidy }}</a>
+      </li>
+    {% empty %}
+      <li>No subsidies funded.</li>
+    {% endfor %}
+
+
+
+  </ul>
+
+{% endblock content %}
diff --git a/scipost_django/funders/templates/funders/individual_budget_form.html b/scipost_django/funders/templates/funders/individual_budget_form.html
new file mode 100644
index 000000000..1a0f8b190
--- /dev/null
+++ b/scipost_django/funders/templates/funders/individual_budget_form.html
@@ -0,0 +1,40 @@
+{% extends 'finances/base.html' %}
+
+{% load bootstrap %}
+{% load scipost_extras %}
+{% load crispy_forms_tags %}
+
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="{% url 'funders:individual_budgets' %}">Individual Budgets</a></span>
+  {% if form.instance.id %}
+    <span class="breadcrumb-item"><a href="{{ form.instance.get_absolute_url }}">{{ form.instance }}</a></span>
+    <span class="breadcrumb-item"><a href="#" class="active">Update</a></span>
+  {% else %}
+    <span class="breadcrumb-item"><a href="#" class="active">Create</a></span>
+  {% endif %}
+
+{% endblock %}
+
+{% block pagetitle %}
+  : Individual Budgets
+{% endblock pagetitle %}
+
+
+{% block content %}
+<hgroup class="highlight p-3 mb-3">
+  <h1>{% if form.instance.id %}Update{% else %}Create {{ form.instance|object_name }}{% endif %} Form</h1>
+  {% if form.instance.id %}
+    <p class="m-0 fs-4"><a href="{{ form.instance.get_absolute_url }}">{{ form.instance }}</a></p>
+  {% endif %}
+</hgroup>
+
+  {% crispy form %}
+{% endblock content %}
+
+
+{% block footer_script %}
+  {{ block.super }}
+  {{ form.media }}
+{% endblock footer_script %}
diff --git a/scipost_django/funders/templates/funders/individual_budget_list.html b/scipost_django/funders/templates/funders/individual_budget_list.html
new file mode 100644
index 000000000..a5aa13cb7
--- /dev/null
+++ b/scipost_django/funders/templates/funders/individual_budget_list.html
@@ -0,0 +1,44 @@
+{% extends 'funders/base.html' %}
+{% load crispy_forms_tags %}
+
+{% block meta_description %}
+  {{ block.super }} Individual Budget List
+{% endblock meta_description %}
+
+{% block pagetitle %}
+  : Individual Budget
+{% endblock pagetitle %}
+
+{% load static %}
+{% load bootstrap %}
+
+{% block breadcrumb_items %}
+  {{ block.super }}
+  <span class="breadcrumb-item"><a href="#" class="active">Individual Budgets</a></span>
+{% endblock %}
+
+{% block content %}
+ 
+  <div class="highlight p-3 d-flex flex-row justify-content-between align-items-center mb-3">
+    <h1>Individual Budgets</h1>
+    <a href="{% url 'funders:individual_budget_create' %}"
+       class="btn btn-primary">Create</a>
+  </div>
+ 
+  <div class="row">
+    <div class="col">
+      <ul class="list-unstyled">
+
+        {% for budget in budgets %}
+          <li>
+            <a href="{{ budget.get_absolute_url }}">{{ budget }}</a>
+          </li>
+        {% empty %}
+          <li>No Individual Budgets</li>
+        {% endfor %}
+
+      </ul>
+    </div>
+  </div>
+
+{% endblock content %}
diff --git a/scipost_django/funders/urls.py b/scipost_django/funders/urls.py
index c4a13b431..385f58f32 100644
--- a/scipost_django/funders/urls.py
+++ b/scipost_django/funders/urls.py
@@ -2,7 +2,7 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
-from django.urls import path
+from django.urls import include, path
 
 from . import views
 
@@ -34,4 +34,36 @@ urlpatterns = [
         views.LinkFunderToOrganizationView.as_view(),
         name="link_to_organization",
     ),
+    path(
+        "budgets/<int:budget_id>/",
+        include(
+            [
+                path(
+                    "",
+                    views.IndividualBudgetDetailView.as_view(),
+                    name="individual_budget_details",
+                ),
+                path(
+                    "delete/",
+                    views.IndividualBudgetDeleteView.as_view(),
+                    name="individual_budget_delete",
+                ),
+                path(
+                    "update/",
+                    views.IndividualBudgetUpdateView.as_view(),
+                    name="individual_budget_update",
+                ),
+            ]
+        ),
+    ),
+    path(
+        "budgets/create/",
+        views.IndividualBudgetCreateView.as_view(),
+        name="individual_budget_create",
+    ),
+    path(
+        "budgets/",
+        views.IndividualBudgetListView.as_view(),
+        name="individual_budgets",
+    ),
 ]
diff --git a/scipost_django/funders/views.py b/scipost_django/funders/views.py
index c50f49388..49e07b940 100644
--- a/scipost_django/funders/views.py
+++ b/scipost_django/funders/views.py
@@ -2,6 +2,7 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
+from django.views.generic import DeleteView, DetailView, ListView
 import requests
 import json
 
@@ -17,12 +18,13 @@ from django.shortcuts import get_object_or_404, render, redirect
 
 from common.views import HXDynselAutocomplete
 
-from .models import Funder, Grant
+from .models import Funder, Grant, IndividualBudget
 from .forms import (
     FunderRegistrySearchForm,
     FunderForm,
     FunderOrganizationSelectForm,
     GrantForm,
+    IndividualBudgetForm,
 )
 
 from scipost.mixins import PermissionsMixin
@@ -163,3 +165,50 @@ class CreateGrantView(PermissionsMixin, HttpRefererMixin, CreateView):
     model = Grant
     form_class = GrantForm
     success_url = reverse_lazy("funders:funders_dashboard")
+
+
+#######################
+# Individual Budgets #
+#######################
+
+
+class IndividualBudgetListView(PermissionsMixin, ListView):
+    model = IndividualBudget
+    template_name = "funders/individual_budget_list.html"
+    permission_required = "scipost.can_manage_subsidies"
+    context_object_name = "budgets"
+
+
+class IndividualBudgetDetailView(PermissionsMixin, DetailView):
+    model = IndividualBudget
+    template_name = "funders/individual_budget_detail.html"
+    permission_required = "scipost.can_manage_subsidies"
+    pk_url_kwarg = "budget_id"
+    context_object_name = "budget"
+
+
+class IndividualBudgetDeleteView(PermissionsMixin, DeleteView):
+    model = IndividualBudget
+    template_name = "funders/individual_budget_delete.html"
+    success_url = reverse_lazy("finances:subsidies")
+    permission_required = "scipost.can_manage_subsidies"
+    pk_url_kwarg = "budget_id"
+    context_object_name = "budget"
+
+
+class IndividualBudgetCreateView(PermissionsMixin, CreateView):
+    model = IndividualBudget
+    form_class = IndividualBudgetForm
+    template_name = "funders/individual_budget_form.html"
+    permission_required = "scipost.can_manage_subsidies"
+    pk_url_kwarg = "budget_id"
+    context_object_name = "budget"
+
+
+class IndividualBudgetUpdateView(PermissionsMixin, UpdateView):
+    model = IndividualBudget
+    form_class = IndividualBudgetForm
+    template_name = "funders/individual_budget_form.html"
+    permission_required = "scipost.can_manage_subsidies"
+    pk_url_kwarg = "budget_id"
+    context_object_name = "budget"
-- 
GitLab