diff --git a/scipost_django/affiliates/admin.py b/scipost_django/affiliates/admin.py index f3edb28d98c8506bd7e225dcd40372e205313d4f..23fa6e8120fced4f74ab642f999d1ae08ecab422 100644 --- a/scipost_django/affiliates/admin.py +++ b/scipost_django/affiliates/admin.py @@ -9,6 +9,7 @@ from .models import ( AffiliateJournal, AffiliatePublication, AffiliatePubFraction, + AffiliateJournalYearSubsidy, ) @@ -23,6 +24,13 @@ class AffiliateJournalAdmin(admin.ModelAdmin): admin.site.register(AffiliateJournal, AffiliateJournalAdmin) +class AffiliateJournalYearSubsidyAdmin(admin.ModelAdmin): + search_fields = ["journal", "organization", "year"] + list_display =["journal", "year", "amount", "organization"] + +admin.site.register(AffiliateJournalYearSubsidy, AffiliateJournalYearSubsidyAdmin) + + class AffiliatePubFractionInline(admin.TabularInline): model = AffiliatePubFraction list_display = ("organization", "publication", "fraction") diff --git a/scipost_django/affiliates/forms.py b/scipost_django/affiliates/forms.py index 42269dce542875576283bf7eb80095a384d68b0c..561624e0cdb20e836f845325cc0e4594b8a21491 100644 --- a/scipost_django/affiliates/forms.py +++ b/scipost_django/affiliates/forms.py @@ -12,7 +12,12 @@ from scipost.services import DOICaller, extract_publication_date_from_Crossref_d from organizations.models import Organization -from .models import AffiliatePublication, AffiliatePubFraction +from .models import ( + AffiliateJournalYearSubsidy, + AffiliatePublication, + AffiliatePubFraction, +) + from .regexes import DOI_AFFILIATEPUBLICATION_REGEX @@ -117,3 +122,24 @@ class AffiliatePublicationAddPubFractionForm(forms.ModelForm): elif input_fraction > 1: raise forms.ValidationError("An individual PubFraction cannot exceed 1!") return input_fraction + + +class AffiliateJournalAddYearSubsidyForm(forms.ModelForm): + organization = forms.ModelChoiceField( + queryset=Organization.objects.all(), + widget=autocomplete.ModelSelect2( + url="/organizations/organization-autocomplete", attrs={"data-html": True} + ), + required=True, + ) + + class Meta: + model = AffiliateJournalYearSubsidy + fields = [ + "journal", + "organization", + "description", + "amount", + "year", + ] + widgets = {"journal": forms.HiddenInput()} diff --git a/scipost_django/affiliates/migrations/0009_auto_20220226_2037.py b/scipost_django/affiliates/migrations/0009_auto_20220226_2037.py new file mode 100644 index 0000000000000000000000000000000000000000..47d0e6c84d87135d743273ffc4c0add676b78b34 --- /dev/null +++ b/scipost_django/affiliates/migrations/0009_auto_20220226_2037.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.12 on 2022-02-26 19:37 + +import affiliates.models.journal +import affiliates.models.subsidy +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0018_auto_20220223_0737'), + ('affiliates', '0008_affiliatejournal_homepage'), + ] + + operations = [ + migrations.AddField( + model_name='affiliatejournal', + name='cost_info', + field=models.JSONField(default=affiliates.models.journal.cost_default_value), + ), + migrations.CreateModel( + name='AffiliateJournalYearSubsidy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField()), + ('amount', models.PositiveIntegerField(help_text='in € (rounded)')), + ('year', models.PositiveSmallIntegerField(default=affiliates.models.subsidy.get_current_year)), + ('journal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='affiliates.affiliatejournal')), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='organizations.organization')), + ], + options={ + 'verbose_name_plural': 'year_subsidies', + 'ordering': ['journal', '-year', 'organization'], + }, + ), + ] diff --git a/scipost_django/affiliates/models/__init__.py b/scipost_django/affiliates/models/__init__.py index 991e724208301c249374ddd480b7cadc5994a244..19eca5cfafb31459b5b5f1e3effa68f04c90f4ba 100644 --- a/scipost_django/affiliates/models/__init__.py +++ b/scipost_django/affiliates/models/__init__.py @@ -9,3 +9,5 @@ from .journal import AffiliateJournal from .publication import AffiliatePublication from .pubfraction import AffiliatePubFraction + +from .subsidy import AffiliateJournalYearSubsidy diff --git a/scipost_django/affiliates/models/journal.py b/scipost_django/affiliates/models/journal.py index 9df8948a89ee85adf75c85a7495521d6373fe924..6f2ffd5126c82c74401d0539bb78e92f2e037a31 100644 --- a/scipost_django/affiliates/models/journal.py +++ b/scipost_django/affiliates/models/journal.py @@ -7,6 +7,10 @@ from django.db import models from django.urls import reverse +def cost_default_value(): + return {"default": 400} + + class AffiliateJournal(models.Model): """ A Journal which piggybacks on SciPost's services. @@ -32,6 +36,9 @@ class AffiliateJournal(models.Model): homepage = models.URLField(max_length=256, blank=True) + # Cost per publication information + cost_info = models.JSONField(default=cost_default_value) + class Meta: ordering = ["publisher", "name"] permissions = (("manage_journal_content", "Manage Journal content"),) diff --git a/scipost_django/affiliates/models/subsidy.py b/scipost_django/affiliates/models/subsidy.py new file mode 100644 index 0000000000000000000000000000000000000000..5e954d110ffbd60ff0ba99e5bd8c1510fbad1c96 --- /dev/null +++ b/scipost_django/affiliates/models/subsidy.py @@ -0,0 +1,38 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +import datetime + +from django.db import models +from django.utils.html import format_html + + +def get_current_year(): + return datetime.date.today().year + + +class AffiliateJournalYearSubsidy(models.Model): + """ + A subsidy given to an AffiliateJournal in a particular year. + """ + + journal = models.ForeignKey( + "affiliates.AffiliateJournal", on_delete=models.CASCADE + ) + organization = models.ForeignKey( + "organizations.Organization", on_delete=models.CASCADE + ) + description = models.TextField() + amount = models.PositiveIntegerField(help_text="in € (rounded)") + year = models.PositiveSmallIntegerField(default=get_current_year) + + class Meta: + verbose_name_plural = "year_subsidies" + ordering = ["journal", "-year", "organization"] + + def __str__(self): + return format_html( + f"{self.year}: €{self.amount} from {self.organization}, " + f"for {self.description}" + ) diff --git a/scipost_django/affiliates/templates/affiliates/affiliatejournal_detail.html b/scipost_django/affiliates/templates/affiliates/affiliatejournal_detail.html index 5a0fffceb9a136bafa162eaee2a9549322aac2a2..08e84b113ae30fc147a802b399687c55bd56f919 100644 --- a/scipost_django/affiliates/templates/affiliates/affiliatejournal_detail.html +++ b/scipost_django/affiliates/templates/affiliates/affiliatejournal_detail.html @@ -142,6 +142,42 @@ </div> </div> + + <div class="row p-2"> + <div class="col"> + <h3 class="highlight">Current Subsidies + <a class="btn btn-sm btn-primary ms-5" href="{% url 'affiliates:journal_subsidies' slug=object.slug %}"> + {% include 'bi/arrow-right.html' %} View all Subsidies</a> + </h3> + + <table class="table"> + <thead> + <tr> + <th>Organization</th> + <th align="right">Subsidy (€)</th> + </tr> + </thead> + <tbody> + {% for subsidy in subsidies_current_year %} + <tr> + <td> + <a href="{% url 'affiliates:journal_organization_detail' journal_slug=object.slug organization_id=subsidy.organization.id %}"> + {{ subsidy.organization }} + </a> + </td> + <td align="right">{{ subsidy.amount }}</td> + </tr> + {% empty %} + <tr> + <td>No subsidies defined</td> + </tr> + {% endfor %} + </tbody> + </table> + + </div> + </div> + {% endblock content %} {% block footer_script %} diff --git a/scipost_django/affiliates/templates/affiliates/affiliatejournal_subsidy_list.html b/scipost_django/affiliates/templates/affiliates/affiliatejournal_subsidy_list.html new file mode 100644 index 0000000000000000000000000000000000000000..6bf75e4594c5b6c059215a1c25403fd0ebf8435d --- /dev/null +++ b/scipost_django/affiliates/templates/affiliates/affiliatejournal_subsidy_list.html @@ -0,0 +1,73 @@ +{% extends 'affiliates/base.html' %} + +{% load guardian_tags %} + +{% block pagetitle %}: Affiliate Journals: Subsidies{% endblock %} + +{% block breadcrumb_items %} + <span class="breadcrumb-item">Affiliates</span> + <span class="breadcrumb-item"><a href="{% url 'affiliates:journals' %}">Journals</a></span> + <span class="breadcrumb-item"><a href="{% url 'affiliates:journal_detail' slug=journal.slug %}">{{ journal }}</a></span> + <span class="breadcrumb-item">Subsidies</span> +{% endblock %} + +{% block content %} + + {% get_obj_perms request.user for journal as "user_perms" %} + + <h2 class="highlight">{{ journal }}: Subsidies</h2> + + {% if 'manage_journal_content' in user_perms %} + <div class="row p-2"> + <div class="col"> + <div class="border border-warning mb-2 p-2"> + <strong class="text-warning">Management</strong> + <h4>Add a Subsidy</h4> + <form action="{% url 'affiliates:journal_add_subsidy' slug=journal.slug %}" method="post"> + {% csrf_token %} + {{ add_subsidy_form.as_p }} + <input type="submit" value="Submit" class="btn btn-primary"> + </form> + </div> + </div> + </div> + {% endif %} + + <table class="table"> + <thead> + <tr> + <th>Organization</th> + <th>Country</th> + <th>Year</th> + <th align="right">Subsidy (€)</th> + </tr> + </thead> + <tbody> + {% for subsidy in object_list %} + <tr> + <td> + <a href="{% url 'affiliates:journal_organization_detail' journal_slug=journal.slug organization_id=subsidy.organization.id %}">{{ subsidy.organization }}</a> + </td> + <td><img src="{{ subsidy.organization.country.flag }}" alt="{{ subsidy.organization.country }} flag"/> <span class="text-muted"><small>[{{ subsidy.organization.country }}]</small></span> {{ subsidy.organization.get_country_display }}</td> + <td>{{ subsidy.year }}</td> + <td align="right">{{ subsidy.amount }}</td> + </tr> + {% empty %} + <tr> + <td>No items at this time</td> + </tr> + {% endfor %} + </tbody> + </table> + + {% if is_paginated %} + <div class="col-12"> + {% include '_pagination.html' with page_obj=page_obj %} + </div> + {% endif %} + +{% endblock content %} + +{% block footer_script %} + {{ add_subsidy_form.media }} +{% endblock footer_script %} diff --git a/scipost_django/affiliates/urls.py b/scipost_django/affiliates/urls.py index 193e666e30c183e159629d6747362e077dab1844..5df37d9b7d783f95651ed700737b42c20c2deb04 100644 --- a/scipost_django/affiliates/urls.py +++ b/scipost_django/affiliates/urls.py @@ -71,4 +71,15 @@ urlpatterns = [ views.affiliatejournal_organization_detail, name="journal_organization_detail", ), + # AffiliateJournalYearSubsidy-related + path( # /affiliates/journals/<slug:slug>/subsidies + "journals/<slug:slug>/subsidies", + views.AffiliateJournalYearSubsidyListView.as_view(), + name="journal_subsidies", + ), + path( # /affiliates/journals/<slug:slug>/subsidies/add + "journals/<slug:slug>/subsidies/add", + views.journal_add_subsidy, + name="journal_add_subsidy", + ), ] diff --git a/scipost_django/affiliates/views.py b/scipost_django/affiliates/views.py index b21b8b720e74475052d0f6caa163feae46286eea..e6923ca546f5f6629509b9786fc0fb4dd48e6c97 100644 --- a/scipost_django/affiliates/views.py +++ b/scipost_django/affiliates/views.py @@ -2,6 +2,8 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +import datetime + from django.contrib import messages from django.contrib.auth.models import User from django.db.models import Q, Sum @@ -16,11 +18,17 @@ from guardian.shortcuts import assign_perm, remove_perm, get_users_with_perms from scipost.mixins import PaginationMixin from organizations.models import Organization -from .models import AffiliateJournal, AffiliatePublication, AffiliatePubFraction +from .models import ( + AffiliateJournal, + AffiliateJournalYearSubsidy, + AffiliatePublication, + AffiliatePubFraction, +) from .forms import ( AffiliateJournalAddManagerForm, AffiliateJournalAddPublicationForm, AffiliatePublicationAddPubFractionForm, + AffiliateJournalAddYearSubsidyForm, ) from .services import get_affiliatejournal_publications_from_Crossref @@ -59,6 +67,9 @@ class AffiliateJournalDetailView(DetailView): ) ).order_by("-sum_affiliate_pubfractions") context["top_benefitting_organizations"] = organizations[:10] + context["subsidies_current_year"] = AffiliateJournalYearSubsidy.objects.filter( + journal=self.object, year=datetime.date.today().year + ) return context @@ -107,11 +118,9 @@ def affiliatejournal_update_publications_from_Crossref(request, slug): class AffiliatePublicationListView(PaginationMixin, ListView): + model = AffiliatePublication paginate_by = 25 - class Meta: - model = AffiliatePublication - def get_queryset(self): queryset = AffiliatePublication.objects.all() if self.request.GET.get("journal", None): @@ -212,3 +221,33 @@ def affiliatejournal_organization_detail(request, journal_slug, organization_id) "affiliates/affiliatejournal_organization_detail.html", context, ) + + +class AffiliateJournalYearSubsidyListView(PaginationMixin, ListView): + model = AffiliateJournalYearSubsidy + template_name = "affiliates/affiliatejournal_subsidy_list.html" + paginate_by = 25 + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["journal"] = get_object_or_404( + AffiliateJournal, slug=self.kwargs["slug"] + ) + context["add_subsidy_form"] = AffiliateJournalAddYearSubsidyForm( + initial={"journal": context["journal"]} + ) + return context + + +@permission_required_or_403( + "affiliates.change_affiliatejournal", (AffiliateJournal, "slug", "slug") +) +def journal_add_subsidy(request, slug): + journal = get_object_or_404(AffiliateJournal, slug=slug) + form = AffiliateJournalAddYearSubsidyForm(request.POST or None) + if form.is_valid(): + form.save() + else: + for error_messages in form.errors.values(): + messages.warning(request, *error_messages) + return redirect(reverse("affiliates:journal_subsidies", kwargs={"slug": slug}))