diff --git a/scipost_django/finances/forms.py b/scipost_django/finances/forms.py index 8d9cc9999cb6f913e6094192339c473a259376b1..d131b70f8362fa0a447851ecbb9a7877a4f4b1ac 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 c8b24b6dbaea71661582e1bdb9e3ec79fc6e5fd7..fbed8a8427a6435eb2c3d880a62e46dbbed92df5 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 5c99776cea1e53d2091442704528164b79d5bc58..d15c97a93d2f76e63f2abf6a6acf1bbfa9e195dc 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 0000000000000000000000000000000000000000..7d9427c744258e33398e9a2e3d61869b15b5a81f --- /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 0000000000000000000000000000000000000000..bea071f1d91214e406ba6d1fdcdbb14dcec6e9a5 --- /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 0000000000000000000000000000000000000000..1a0f8b19097740e5d95490b4a91261bc33fb1ffd --- /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 0000000000000000000000000000000000000000..a5aa13cb774234ea5260a199305c93ee0aeb119f --- /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 c4a13b431ab3e24da62b0eb04f02e656b3a01cc5..385f58f322bac5674f8e4cdecf2b18d951a363b0 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 c50f4938800ee927596a27d46eb2ceb980343918..49e07b940fddb1fd24e314d7a7076228ce08b680 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"