From 3349e6f4afce2ab612d38834b453d60b799b3718 Mon Sep 17 00:00:00 2001 From: "J.-S. Caux" <J.S.Caux@uva.nl> Date: Sat, 8 May 2021 17:23:11 +0200 Subject: [PATCH] Fire up basic affiliates app --- SciPost_v1/settings/base.py | 1 + SciPost_v1/urls.py | 4 ++ affiliates/__init__.py | 0 affiliates/admin.py | 38 +++++++++++ affiliates/apps.py | 9 +++ affiliates/converters.py | 11 +++ affiliates/migrations/0001_initial.py | 68 +++++++++++++++++++ .../migrations/0002_auto_20210508_1629.py | 20 ++++++ .../migrations/0003_auto_20210508_1653.py | 19 ++++++ affiliates/migrations/__init__.py | 0 affiliates/models/__init__.py | 11 +++ affiliates/models/journal.py | 51 ++++++++++++++ affiliates/models/pubfraction.py | 40 +++++++++++ affiliates/models/publication.py | 41 +++++++++++ affiliates/models/publisher.py | 23 +++++++ affiliates/regexes.py | 5 ++ .../affiliates/affiliatejournal_detail.html | 22 ++++++ .../affiliates/affiliatejournal_list.html | 17 +++++ .../affiliatepublication_detail.html | 23 +++++++ affiliates/urls.py | 30 ++++++++ affiliates/validators.py | 12 ++++ affiliates/views.py | 23 +++++++ 22 files changed, 468 insertions(+) create mode 100644 affiliates/__init__.py create mode 100644 affiliates/admin.py create mode 100644 affiliates/apps.py create mode 100644 affiliates/converters.py create mode 100644 affiliates/migrations/0001_initial.py create mode 100644 affiliates/migrations/0002_auto_20210508_1629.py create mode 100644 affiliates/migrations/0003_auto_20210508_1653.py create mode 100644 affiliates/migrations/__init__.py create mode 100644 affiliates/models/__init__.py create mode 100644 affiliates/models/journal.py create mode 100644 affiliates/models/pubfraction.py create mode 100644 affiliates/models/publication.py create mode 100644 affiliates/models/publisher.py create mode 100644 affiliates/regexes.py create mode 100644 affiliates/templates/affiliates/affiliatejournal_detail.html create mode 100644 affiliates/templates/affiliates/affiliatejournal_list.html create mode 100644 affiliates/templates/affiliates/affiliatepublication_detail.html create mode 100644 affiliates/urls.py create mode 100644 affiliates/validators.py create mode 100644 affiliates/views.py diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 47b1c76fd..e0f5c8fb3 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -86,6 +86,7 @@ INSTALLED_APPS = ( 'django_countries', 'django_extensions', 'haystack', + 'affiliates', 'api', 'apimail', 'careers', diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index cb15a9fb9..33de07773 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -28,6 +28,10 @@ urlpatterns = [ url(r'^sitemap.xml$', scipost_views.sitemap_xml, name='sitemap_xml'), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', admin.site.urls), + path( + 'affiliates/', + include('affiliates.urls', namespace='affiliates') + ), url(r'^api/', include('api.urls', namespace='api')), path( 'mail/', diff --git a/affiliates/__init__.py b/affiliates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/affiliates/admin.py b/affiliates/admin.py new file mode 100644 index 000000000..550598ea9 --- /dev/null +++ b/affiliates/admin.py @@ -0,0 +1,38 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.contrib import admin + +from .models import ( + AffiliatePublisher, AffiliateJournal, + AffiliatePublication, AffiliatePubFraction +) + + +admin.site.register(AffiliatePublisher) + + +class AffiliateJournalAdmin(admin.ModelAdmin): + search_fields = ['name'] + list_display = ['name', 'publisher'] + +admin.site.register(AffiliateJournal, AffiliateJournalAdmin) + + +class AffiliatePubFractionInline(admin.TabularInline): + model = AffiliatePubFraction + list_display = ('organization', 'publication', 'fraction') + autocomplete_fields = [ + 'organization', + ] + +class AffiliatePublicationAdmin(admin.ModelAdmin): + search_fields = ['doi', 'journal',] + list_display = [ + 'journal', + 'doi', + ] + inlines = [AffiliatePubFractionInline,] + +admin.site.register(AffiliatePublication, AffiliatePublicationAdmin) diff --git a/affiliates/apps.py b/affiliates/apps.py new file mode 100644 index 000000000..dd56ccff1 --- /dev/null +++ b/affiliates/apps.py @@ -0,0 +1,9 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.apps import AppConfig + + +class AffiliatesConfig(AppConfig): + name = 'affiliates' diff --git a/affiliates/converters.py b/affiliates/converters.py new file mode 100644 index 000000000..c5621a88c --- /dev/null +++ b/affiliates/converters.py @@ -0,0 +1,11 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.urls.converters import StringConverter + +from .regexes import DOI_AFFILIATEPUBLICATION_REGEX + + +class Crossref_DOI_converter(StringConverter): + regex = DOI_AFFILIATEPUBLICATION_REGEX diff --git a/affiliates/migrations/0001_initial.py b/affiliates/migrations/0001_initial.py new file mode 100644 index 000000000..2678619a9 --- /dev/null +++ b/affiliates/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 2.2.16 on 2021-05-08 14:20 + +from decimal import Decimal +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import re + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('organizations', '0012_auto_20210310_2026'), + ] + + operations = [ + migrations.CreateModel( + name='AffiliateJournal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('short_name', models.CharField(default='', max_length=256)), + ('slug', models.SlugField(max_length=128, validators=[django.core.validators.RegexValidator(re.compile('^[-\\w]+\\Z'), "Enter a valid 'slug' consisting of Unicode letters, numbers, underscores, or hyphens.", 'invalid')])), + ], + options={ + 'ordering': ['publisher', 'name'], + }, + ), + migrations.CreateModel( + name='AffiliatePublisher', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='AffiliatePublication', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('doi', models.CharField(db_index=True, max_length=256, unique=True, validators=[django.core.validators.RegexValidator('^10.\\d{4,9}/[-._;()/:a-zA-Z0-9]+$', 'Only expressions with regex 10.\\d{4,9}/[-._;()/:a-zA-Z0-9]+ are allowed.')])), + ('_metadata_crossref', django.contrib.postgres.fields.jsonb.JSONField(default=dict)), + ('journal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='affiliates.AffiliateJournal')), + ], + ), + migrations.AddField( + model_name='affiliatejournal', + name='publisher', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='journals', to='affiliates.AffiliatePublisher'), + ), + migrations.CreateModel( + name='AffiliatePubFraction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('fraction', models.DecimalField(decimal_places=3, default=Decimal('0.000'), max_digits=4)), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='affiliate_pubfractions', to='organizations.Organization')), + ('publication', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pubfractions', to='affiliates.AffiliatePublication')), + ], + options={ + 'unique_together': {('organization', 'publication')}, + }, + ), + ] diff --git a/affiliates/migrations/0002_auto_20210508_1629.py b/affiliates/migrations/0002_auto_20210508_1629.py new file mode 100644 index 000000000..e683662f4 --- /dev/null +++ b/affiliates/migrations/0002_auto_20210508_1629.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.16 on 2021-05-08 14:29 + +import django.core.validators +from django.db import migrations, models +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliates', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='affiliatejournal', + name='slug', + field=models.SlugField(max_length=128, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-\\w]+\\Z'), "Enter a valid 'slug' consisting of Unicode letters, numbers, underscores, or hyphens.", 'invalid')]), + ), + ] diff --git a/affiliates/migrations/0003_auto_20210508_1653.py b/affiliates/migrations/0003_auto_20210508_1653.py new file mode 100644 index 000000000..893701aef --- /dev/null +++ b/affiliates/migrations/0003_auto_20210508_1653.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.16 on 2021-05-08 14:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('affiliates', '0002_auto_20210508_1629'), + ] + + operations = [ + migrations.AlterField( + model_name='affiliatepublication', + name='journal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='affiliates.AffiliateJournal'), + ), + ] diff --git a/affiliates/migrations/__init__.py b/affiliates/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/affiliates/models/__init__.py b/affiliates/models/__init__.py new file mode 100644 index 000000000..991e72420 --- /dev/null +++ b/affiliates/models/__init__.py @@ -0,0 +1,11 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from .publisher import AffiliatePublisher + +from .journal import AffiliateJournal + +from .publication import AffiliatePublication + +from .pubfraction import AffiliatePubFraction diff --git a/affiliates/models/journal.py b/affiliates/models/journal.py new file mode 100644 index 000000000..989f257e6 --- /dev/null +++ b/affiliates/models/journal.py @@ -0,0 +1,51 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.validators import validate_unicode_slug +from django.db import models +from django.urls import reverse + + +class AffiliateJournal(models.Model): + """ + A Journal which piggybacks on SciPost's services. + """ + + publisher = models.ForeignKey( + 'affiliates.AffiliatePublisher', + on_delete=models.CASCADE, + related_name='journals' + ) + + name = models.CharField( + max_length=256 + ) + + # Note that the short name can be just as long as the full name. This is because not all + # journals have abbreviated names in Crossref, and instead return the full journal name. + short_name = models.CharField( + max_length=256, + default="" + ) + + slug = models.SlugField( + max_length=128, + validators=[validate_unicode_slug,], + unique=True + ) + + class Meta: + ordering = [ + 'publisher', + 'name' + ] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse( + 'affiliates:journal_detail', + kwargs={'slug': self.slug} + ) diff --git a/affiliates/models/pubfraction.py b/affiliates/models/pubfraction.py new file mode 100644 index 000000000..92d87a913 --- /dev/null +++ b/affiliates/models/pubfraction.py @@ -0,0 +1,40 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from decimal import Decimal + +from django.db import models + + +class AffiliatePubFraction(models.Model): + """ + PubFraction for an AffiliatePublication. + """ + organization = models.ForeignKey( + 'organizations.Organization', + on_delete=models.CASCADE, + related_name='affiliate_pubfractions' + ) + publication = models.ForeignKey( + 'affiliates.AffiliatePublication', + on_delete=models.CASCADE, + related_name='pubfractions' + ) + fraction = models.DecimalField( + max_digits=4, + decimal_places=3, + default=Decimal('0.000') + ) + + class Meta: + unique_together = ( + ('organization', 'publication'), + ) + + def __str__(self): + return 'PubFraction of %s for %s: %d' % ( + self.organization, + self.publication, + self.fraction + ) diff --git a/affiliates/models/publication.py b/affiliates/models/publication.py new file mode 100644 index 000000000..a46385053 --- /dev/null +++ b/affiliates/models/publication.py @@ -0,0 +1,41 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.contrib.postgres.fields import JSONField +from django.db import models +from django.urls import reverse + +from ..validators import doi_affiliatepublication_validator + + +class AffiliatePublication(models.Model): + """ + Publication item from an affiliate Publisher/Journal. + """ + + doi = models.CharField( + max_length=256, + unique=True, + db_index=True, + validators=[doi_affiliatepublication_validator] + ) + _metadata_crossref = JSONField( + default=dict, + + ) + + journal = models.ForeignKey( + 'affiliates.AffiliateJournal', + on_delete=models.CASCADE, + related_name='publications' + ) + + def __str__(self): + return self.doi + + def get_absolute_url(self): + return reverse( + 'affiliates:publication_detail', + kwargs={'doi': self.doi} + ) diff --git a/affiliates/models/publisher.py b/affiliates/models/publisher.py new file mode 100644 index 000000000..c8305c638 --- /dev/null +++ b/affiliates/models/publisher.py @@ -0,0 +1,23 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.db import models + + +class AffiliatePublisher(models.Model): + """ + A Publisher which piggybacks on SciPost's services. + """ + + name = models.CharField( + max_length=256 + ) + + class Meta: + ordering = [ + 'name' + ] + + def __str__(self): + return self.name diff --git a/affiliates/regexes.py b/affiliates/regexes.py new file mode 100644 index 000000000..c334c8f57 --- /dev/null +++ b/affiliates/regexes.py @@ -0,0 +1,5 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +DOI_AFFILIATEPUBLICATION_REGEX = r'10.\d{4,9}/[-._;()/:a-zA-Z0-9]+' diff --git a/affiliates/templates/affiliates/affiliatejournal_detail.html b/affiliates/templates/affiliates/affiliatejournal_detail.html new file mode 100644 index 000000000..b30a23998 --- /dev/null +++ b/affiliates/templates/affiliates/affiliatejournal_detail.html @@ -0,0 +1,22 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Affiliate Journal: {{ object }}{% endblock %} + +{% block content %} + + <h2 class="highlight">Affiliate Journal: {{ object }}</h2> + + <h3 class="highlight">Publications</h3> + <table class="table"> + {% for pub in object.publications.all %} + <tr> + <td><a href="{{ pub.get_absolute_url }}">{{ pub }}</a></td> + </tr> + {% empty %} + <tr> + <td>No publications yet</td> + </tr> + {% endfor %} + </table> + +{% endblock content %} diff --git a/affiliates/templates/affiliates/affiliatejournal_list.html b/affiliates/templates/affiliates/affiliatejournal_list.html new file mode 100644 index 000000000..cc71786c1 --- /dev/null +++ b/affiliates/templates/affiliates/affiliatejournal_list.html @@ -0,0 +1,17 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Affiliate Journals{% endblock %} + +{% block content %} + + <h2 class="highlight">Affiliate Journals</h2> + + <ul> + {% for journal in object_list %} + <li><a href="{{ journal.get_absolute_url }}">{{ journal }}</a></li> + {% empty %} + <li>There are no affiliate journals at this moment</li> + {% endfor %} + </ul> + +{% endblock content %} diff --git a/affiliates/templates/affiliates/affiliatepublication_detail.html b/affiliates/templates/affiliates/affiliatepublication_detail.html new file mode 100644 index 000000000..4c180f8a8 --- /dev/null +++ b/affiliates/templates/affiliates/affiliatepublication_detail.html @@ -0,0 +1,23 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Publication: {{ object }}{% endblock %} + +{% block content %} + + <h2 class="highlight">Publication: {{ object }} </h2> + <p>(in affiliate journal {{ object.journal }})</p> + + <h3 class="highlight">PubFractions</h3> + <table class="table"> + {% for pubfrac in object.pufractions.all %} + <tr> + <td>{{ pubfrac }}</td> + </tr> + {% empty %} + <tr> + <td>No PubFractions yet</td> + </tr> + {% endfor %} + </table> + +{% endblock content %} diff --git a/affiliates/urls.py b/affiliates/urls.py new file mode 100644 index 000000000..91ce17930 --- /dev/null +++ b/affiliates/urls.py @@ -0,0 +1,30 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.urls import path, register_converter + +from . import views +from .converters import Crossref_DOI_converter + +app_name='affiliates' + +register_converter(Crossref_DOI_converter, 'doi') + +urlpatterns = [ + path( # /affiliates/journals + 'journals', + views.AffiliateJournalListView.as_view(), + name='journals' + ), + path( # /affiliates/journals/<slug> + 'journals/<slug:slug>', + views.AffiliateJournalDetailView.as_view(), + name='journal_detail' + ), + path( # /affiliates/publications/<doi:doi> + 'publications/<doi:doi>', + views.AffiliatePublicationDetailView.as_view(), + name='publication_detail' + ), +] diff --git a/affiliates/validators.py b/affiliates/validators.py new file mode 100644 index 000000000..eb13135b7 --- /dev/null +++ b/affiliates/validators.py @@ -0,0 +1,12 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.core.validators import RegexValidator + +from .regexes import DOI_AFFILIATEPUBLICATION_REGEX + + +doi_affiliatepublication_validator = RegexValidator( + r'^{regex}$'.format(regex=DOI_AFFILIATEPUBLICATION_REGEX), + 'Only expressions with regex %s are allowed.' % DOI_AFFILIATEPUBLICATION_REGEX) diff --git a/affiliates/views.py b/affiliates/views.py new file mode 100644 index 000000000..0a6661eb4 --- /dev/null +++ b/affiliates/views.py @@ -0,0 +1,23 @@ +__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" +__license__ = "AGPL v3" + + +from django.shortcuts import render +from django.views.generic.detail import DetailView +from django.views.generic.list import ListView + +from .models import AffiliateJournal, AffiliatePublication + + +class AffiliateJournalListView(ListView): + model = AffiliateJournal + + +class AffiliateJournalDetailView(DetailView): + model = AffiliateJournal + + +class AffiliatePublicationDetailView(DetailView): + model = AffiliatePublication + slug_field = 'doi' + slug_url_kwarg = 'doi' -- GitLab