diff --git a/scipost_django/colleges/forms.py b/scipost_django/colleges/forms.py index a749a82ec35713cd5eb17ee7a5f19f6b12050313..38e304c7182b9a8adfb2cd9b247f30e06792d466 100644 --- a/scipost_django/colleges/forms.py +++ b/scipost_django/colleges/forms.py @@ -109,6 +109,7 @@ class FellowshipDynSelForm(forms.Form): action_url_name = forms.CharField() action_url_base_kwargs = forms.JSONField(required=False) action_target_element_id = forms.CharField() + action_target_swap = forms.CharField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -118,6 +119,7 @@ class FellowshipDynSelForm(forms.Form): Field("action_url_name", type="hidden"), Field("action_url_base_kwargs", type="hidden"), Field("action_target_element_id", type="hidden"), + Field("action_target_swap", type="hidden"), ) def search_results(self): diff --git a/scipost_django/colleges/templates/colleges/_hx_fellowship_dynsel_list.html b/scipost_django/colleges/templates/colleges/_hx_fellowship_dynsel_list.html index 85ef7102fd0fb76acce34d3c27835118b39cf5b0..b78b1e56b4ebdbff35a80dddc7d26d9ec9618ea4 100644 --- a/scipost_django/colleges/templates/colleges/_hx_fellowship_dynsel_list.html +++ b/scipost_django/colleges/templates/colleges/_hx_fellowship_dynsel_list.html @@ -1,6 +1,6 @@ {% load colleges_extras %} -<ul class="list list-unstyled"> +<ul class="list list-unstyled dynsel-list"> {% for fellowship in fellowships|slice:":11" %} {% if forloop.counter == 11 %} <li> ...</li> @@ -9,6 +9,8 @@ <a hx-get="{% fellowship_dynsel_action_url fellowship %}" hx-target="#{{ action_target_element_id }}" + hx-swap="{{ action_target_swap }}" + hx-indicator="#{{ action_target_element_id }}-indicator" > {{ fellowship }} </a> diff --git a/scipost_django/journals/templates/journals/_publication_li_content-alt.html b/scipost_django/journals/templates/journals/_publication_li_content-alt.html index 4d112e20a18f226eeea21e0784560b7d724442e8..ad874fb0404c66cfe429664f8b741963e28c71a7 100644 --- a/scipost_django/journals/templates/journals/_publication_li_content-alt.html +++ b/scipost_django/journals/templates/journals/_publication_li_content-alt.html @@ -14,7 +14,7 @@ </div> <div class="citation">{{ publication.citation }}</div> <span class="">published {{ publication.publication_date|date:'j F Y' }}</span> - {% for collection in publication.collection_set.all %} + {% for collection in publication.collections.all %} <p class="m-1"><em>Part of the <a href="{{ collection.get_absolute_url }}">{{ collection }}</a> Collection in the <a href="{{ collection.series.get_absolute_url }}">{{ collection.series }}</a> Series.</em></p> {% endfor %} </div> diff --git a/scipost_django/journals/templates/journals/_publication_li_content.html b/scipost_django/journals/templates/journals/_publication_li_content.html index 8143df7e429ba25d973b8f935d613fccbadb1c4f..d81d4588a5c42810d358da5dcc863646e470c2bd 100644 --- a/scipost_django/journals/templates/journals/_publication_li_content.html +++ b/scipost_django/journals/templates/journals/_publication_li_content.html @@ -12,7 +12,7 @@ <p class="meta mb-0"> {{ publication.citation }} · <span class="fw-light">published {{ publication.publication_date|date:'j F Y' }}</span> </p> - {% for collection in publication.collection_set.all %} + {% for collection in publication.collections.all %} <p class="m-1"><em>Part of the <a href="{{ collection.get_absolute_url }}">{{ collection }}</a> Collection in the <a href="{{ collection.series.get_absolute_url }}">{{ collection.series }}</a> Series.</em></p> {% endfor %} </div> diff --git a/scipost_django/journals/templates/journals/_publication_summary.html b/scipost_django/journals/templates/journals/_publication_summary.html index cc43d2d3582294ad158bb909993f3340d23c425e..ed44077694f1985fe90ca582616b64a08e49ee6a 100644 --- a/scipost_django/journals/templates/journals/_publication_summary.html +++ b/scipost_django/journals/templates/journals/_publication_summary.html @@ -12,7 +12,7 @@ {% if publication.cc_license != 'CC BY 4.0' %} · licensed under {{ publication.get_cc_license_display }} {% endif %} - {% for collection in publication.collection_set.all %} + {% for collection in publication.collections.all %} <p class="m-2"><em>Part of the <a href="{{ collection.get_absolute_url }}">{{ collection }}</a> Collection in the <a href="{{ collection.series.get_absolute_url }}">{{ collection.series }}</a> Series.</em></p> {% endfor %} diff --git a/scipost_django/journals/templates/journals/about.html b/scipost_django/journals/templates/journals/about.html index e54f3b0f7ecf274ef9c9a086b57426a2574287ff..2e1e2fa36ad9948da883d645d7d77a6b0f510eaf 100644 --- a/scipost_django/journals/templates/journals/about.html +++ b/scipost_django/journals/templates/journals/about.html @@ -112,10 +112,10 @@ Content </h2> {% automarkup journal.content %} - {% if journal.series_set.all|length > 0 %} + {% if journal.contained_series.all|length > 0 %} Series hosted in this Journal: <ul> - {% for series in journal.series_set.all %} + {% for series in journal.contained_series.all %} <li> <a href="{{ series.get_absolute_url }}" target="_blank">{{ series }}</a> </li> diff --git a/scipost_django/journals/templates/journals/journal_detail.html b/scipost_django/journals/templates/journals/journal_detail.html index 143ed590f1b15b5efeeca101b34620ac5496472f..4d751d1facdd28d25c9aa80b21f5b8e8c8889e75 100644 --- a/scipost_django/journals/templates/journals/journal_detail.html +++ b/scipost_django/journals/templates/journals/journal_detail.html @@ -20,7 +20,7 @@ <li class="nav-item"> <a class="nav-link" id="accepted-tab" data-bs-toggle="tab" href="#accepted" role="tab" aria-controls="accepted" aria-selected="true">Accepted Submissions</a> </li> - {% if journal.series_set.all|length > 0 %} + {% if journal.contained_series.all|length > 0 %} <li class="nav-item"> <a class="nav-link" id="series-tab" data-bs-toggle="tab" href="#series" role="tab" aria-controls="series" aria-selected="true">Series</a> </li> @@ -64,11 +64,11 @@ {% endfor %} </ul> </div> - {% if journal.series_set.all|length > 0 %} + {% if journal.contained_series.all|length > 0 %} <div class="tab-pane pt-4" id="series" role="tabpanel" aria-labelledby="series-tab"> <h3 class="highlight">Series contained in this Journal</h3> <ul> - {% for series in journal.series_set.all %} + {% for series in journal.contained_series.all %} <li><a href="{{ series.get_absolute_url }}" target="_blank">{{ series }}</a></li> {% endfor %} </ul> diff --git a/scipost_django/journals/templates/journals/journal_list.html b/scipost_django/journals/templates/journals/journal_list.html index b5f9ab43029f7ad9e5fabd16401a1c826724aad2..2d677223b58b6ff9efde0442fdd32347605fb3d6 100644 --- a/scipost_django/journals/templates/journals/journal_list.html +++ b/scipost_django/journals/templates/journals/journal_list.html @@ -63,11 +63,11 @@ <div class="card-body"> {% automarkup journal.blurb %} </div> - {% if journal.series_set.all|length > 0 %} + {% if journal.contained_series.all|length > 0 %} <div class="card-footer"> Series hosted in this Journal: <ul> - {% for series in journal.series_set.all %} + {% for series in journal.contained_series.all %} <li> <a href="{{ series.get_absolute_url }}" target="_blank">{{ series }}</a> </li> diff --git a/scipost_django/production/admin.py b/scipost_django/production/admin.py index 191991474634f31e30458f52907baebabc549d61..0df3196c38b40efc72a9e6350b03d48faf776eba 100644 --- a/scipost_django/production/admin.py +++ b/scipost_django/production/admin.py @@ -109,12 +109,15 @@ class ProofsRepositoryAdmin(GuardedModelAdmin): list_filter = ["status"] list_display = ["name", "status", "gitlab_link"] - readonly_fields = ["stream", "template_path", "gitlab_link"] + readonly_fields = ["stream", "template_paths", "gitlab_link"] def gitlab_link(self, obj): return format_html( '<a href="{1}" target="_blank">{0}</a>', obj.git_path, obj.git_url ) + def template_paths(self, obj): + return format_html("<br>".join(obj.template_paths)) + admin.site.register(ProofsRepository, ProofsRepositoryAdmin) diff --git a/scipost_django/production/management/commands/advance_git_repos.py b/scipost_django/production/management/commands/advance_git_repos.py index ae1ec0bae52cc3d30872751488fc22ffaa89ffc9..42da5e38216fa7a2c9acaa03b417939795a4adf7 100644 --- a/scipost_django/production/management/commands/advance_git_repos.py +++ b/scipost_django/production/management/commands/advance_git_repos.py @@ -161,9 +161,17 @@ class Command(BaseCommand): """ Return a list of gitlab actions required to fully clone a project. """ - filenames = list( - map(lambda x: x["path"], project.repository_tree(get_all=True)) - ) + try: + filenames = list( + map(lambda x: x["path"], project.repository_tree(get_all=True)) + ) + except: + self.stdout.write( + self.style.WARNING( + f"Could not get the files of {project.path_with_namespace}, it may be empty" + ) + ) + return [] actions = [] for filename in filenames: @@ -192,24 +200,23 @@ class Command(BaseCommand): """ project = self.GL.projects.get(repo.git_path) - journal_template_project = self.GL.projects.get(repo.template_path) - base_template_project = self.GL.projects.get( - "{ROOT}/Templates/Base".format(ROOT=settings.GITLAB_ROOT) - ) + # Get the cloning actions for each template project + actions = [ + self._get_project_cloning_actions(self.GL.projects.get(template_path)) + for template_path in repo.template_paths + ] + actions = list(chain(*actions)) # Flatten the list of lists - all_actions = [] - # Add "Base" and Journal specific templates to the repo - all_actions.append(self._get_project_cloning_actions(base_template_project)) - all_actions.append(self._get_project_cloning_actions(journal_template_project)) + # Keep the last action if there are multiple actions for the same file + # (i.e. the same file_path key in the dictionary) + non_duplicate_actions = [] + file_paths_to_clone = [] - # Add the "Selected" template if the submission has been accepted in Selections - if "Selections" in repo.stream.submission.editorial_decision.for_journal.name: - selected_template_project = self.GL.projects.get( - "{ROOT}/Templates/Selected".format(ROOT=settings.GITLAB_ROOT) - ) - all_actions.append( - self._get_project_cloning_actions(selected_template_project) - ) + for action in reversed(actions): + file_path = action.get("file_path", None) + if (file_path is not None) and (file_path not in file_paths_to_clone): + file_paths_to_clone.append(file_path) + non_duplicate_actions.append(action) # Add some delays to avoid: # - Commiting the files before the branch has finished being created @@ -220,7 +227,7 @@ class Command(BaseCommand): { "branch": "main", "commit_message": "copy pure templates", - "actions": list(chain(*all_actions)), + "actions": non_duplicate_actions, } ) sleep(3) @@ -396,6 +403,21 @@ class Command(BaseCommand): selections_logo_img = r"[width=34.55mm]{logo_select.pdf}" replacements_dict[default_logo_img] = (lambda _: selections_logo_img, None) + # Add collection specific information if the submission is part of a collection + if repo.stream.submission.collections.exists(): + collection = repo.stream.submission.collections.first() + series = collection.series + + collection_replacements_dict = { + "<|COLLECTION_NAME|>": (lambda _: collection.name, None), + "<|COLLECTION_URL|>": (lambda _: collection.get_absolute_url(), None), + "<|EVENT_DETAILS|>": (lambda _: collection.event_details, None), + "<|SERIES_NAME|>": (lambda _: series.name, None), + "<|SERIES_URL|>": (lambda _: series.get_absolute_url(), None), + } + + replacements_dict.update(collection_replacements_dict) + # Define a helper function to try to format and replace a placeholder # which catches any errors and prints them to the console non-intrusively def try_format_replace( diff --git a/scipost_django/production/models.py b/scipost_django/production/models.py index 2b6fbd92542f7d53b0398dc791217382874f0721..44fe27351ecc62edd33abe82cd2dab680ee794e1 100644 --- a/scipost_django/production/models.py +++ b/scipost_django/production/models.py @@ -2,6 +2,7 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" +from typing import List from django.db import models from django.contrib.contenttypes.fields import GenericRelation from django.urls import reverse @@ -410,22 +411,53 @@ class ProofsRepository(models.Model): git_path=self.git_path, ) - @property - def template_path(self) -> str: + @cached_property + def template_paths(self) -> List[str]: """ - Return the path to the template repository. + Return the list of paths to the various templates used for the proofs. """ + paths = ["{ROOT}/Templates/Base".format(ROOT=settings.GITLAB_ROOT)] + + # Determine whether to add the proceedings template or of some other journal if self.stream.submission.proceedings is not None: - return "{ROOT}/Templates/{journal_subdivision}".format( - ROOT=settings.GITLAB_ROOT, - journal_subdivision=self.journal_subdivision, + paths.append( + "{ROOT}/Templates/{journal_subdivision}".format( + ROOT=settings.GITLAB_ROOT, + journal_subdivision=self.journal_subdivision, + ) ) + # Add extra paths for any collections associated with the submission + # First add the base template for the series and then the collection + elif collections := self.stream.submission.collections.all(): + for collection in collections: + paths.append( + "{ROOT}/Templates/Series/{series}/Base".format( + ROOT=settings.GITLAB_ROOT, + series=collection.series.slug, + collection=collection.slug, + ) + ) + paths.append( + "{ROOT}/Templates/Series/{series}/{collection}".format( + ROOT=settings.GITLAB_ROOT, + series=collection.series.slug, + collection=collection.slug, + ) + ) else: - return "{ROOT}/Templates/{journal}".format( - ROOT=settings.GITLAB_ROOT, - journal=self.journal_abbrev, + paths.append( + "{ROOT}/Templates/{journal}".format( + ROOT=settings.GITLAB_ROOT, + journal=self.journal_abbrev, + ) ) + # Add the selected template if the submission is a Selections paper + if "Selections" in self.stream.submission.editorial_decision.for_journal.name: + paths.append("{ROOT}/Templates/Selected".format(ROOT=settings.GITLAB_ROOT)) + + return paths + def __str__(self) -> str: return f"Proofs repo for {self.stream}" diff --git a/scipost_django/production/templates/production/_productionstream_details_summary_contents.html b/scipost_django/production/templates/production/_productionstream_details_summary_contents.html index b6d3530c45569321eb90a8ca91e5589f9c756a94..c3ec77e4f99af300cb14e9995097836cd789dad0 100644 --- a/scipost_django/production/templates/production/_productionstream_details_summary_contents.html +++ b/scipost_django/production/templates/production/_productionstream_details_summary_contents.html @@ -46,24 +46,30 @@ </div> <div class="row"> - <div class="col col-sm-6 col-md text-nowrap"> + <div class="col text-nowrap text-truncate"> <small class="text-muted">To be published in</small> <br> {% if productionstream.submission.proceedings %} - {% if productionstream.submission.proceedings.event_suffix %} - {{ productionstream.submission.proceedings.event_suffix }} - {% else %} - {{ productionstream.submission.proceedings.event_name }} - {% endif %} + {% if productionstream.submission.proceedings.event_suffix %} + <span title="{{ productionstream.submission.proceedings.event_name }}">{{ productionstream.submission.proceedings.event_suffix }}</span> + {% else %} + <span title="{{ productionstream.submission.proceedings.event_name }}">{{ productionstream.submission.proceedings.event_name }}</span> + {% endif %} + {% elif productionstream.submission.collections.all %} + {% for collection in productionstream.submission.collections.all %} + <span title="{{ collection.series.name }}">{{ collection.series.name }}</span> + <br> + <span title="{{ collection.name }}">{{ collection.name }}</span> + {% endfor %} {% else %} - {{ productionstream.submission.editorial_decision.for_journal }} - {% if "Selections" in productionstream.submission.editorial_decision.for_journal.name %} - - {{ productionstream.submission.acad_field }} - {% endif %} + {{ productionstream.submission.editorial_decision.for_journal }} + {% if "Selections" in productionstream.submission.editorial_decision.for_journal.name %} + - {{ productionstream.submission.acad_field }} + {% endif %} {% endif %} </div> - <div class="col col-sm-6 col-md text-nowrap"> + <div class="col-auto text-nowrap"> <small class="text-muted">Submitter</small> <br> {% if productionstream.submission.submitted_by.profile.email %} @@ -74,7 +80,7 @@ <a href="{% url 'scipost:contributor_info' productionstream.submission.submitted_by.id %}">{{ productionstream.submission.submitted_by.formal_str }}</a> </div> - <div class="col-auto pe-auto"> + <div class="col-12 col-sm-auto col-md-12 col-xl-auto pe-auto"> <small class="text-muted">Go to page:</small> <br> <div class="d-inline-flex"> diff --git a/scipost_django/production/tests/test_models.py b/scipost_django/production/tests/test_models.py index e82a62c0c875eeb3c317423cfd82d783e7d618fb..dcaee0bdb0f8f11d94cc8699c0d4a064b70877ae 100644 --- a/scipost_django/production/tests/test_models.py +++ b/scipost_django/production/tests/test_models.py @@ -207,8 +207,8 @@ class TestProofRepository(TestCase): "ProjectRoot/Proofs/SciPostPhys/2021/01/scipost_202101_00001v1_User", ) - self.assertEqual( - proofs_repo.template_path, + self.assertIn( + proofs_repo.template_paths, "ProjectRoot/Templates/SciPostPhys", ) @@ -254,7 +254,7 @@ class TestProofRepository(TestCase): "ProjectRoot/Proofs/SciPostPhysProc/2021/ProcName21/scipost_202101_00001v1_User", ) - self.assertEqual( - proofs_repo.template_path, + self.assertIn( + proofs_repo.template_paths, "ProjectRoot/Templates/SciPostPhysProc/2021/ProcName21", ) diff --git a/scipost_django/profiles/models.py b/scipost_django/profiles/models.py index a5a70141b4615802154ca6ace4c625d7463802e2..16fde268cd75b01aad22e84e860083ecb330b100 100644 --- a/scipost_django/profiles/models.py +++ b/scipost_django/profiles/models.py @@ -90,6 +90,10 @@ class Profile(models.Model): self.first_name, ) + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + @property def roles(self): try: diff --git a/scipost_django/scipost/templates/scipost/portal/_hx_journals.html b/scipost_django/scipost/templates/scipost/portal/_hx_journals.html index 64b64dc8b98f09badc5f65a874c391dd460d4f9e..23a78cd1bb53696ed3c00b5fa681fe055c005ab5 100644 --- a/scipost_django/scipost/templates/scipost/portal/_hx_journals.html +++ b/scipost_django/scipost/templates/scipost/portal/_hx_journals.html @@ -20,11 +20,11 @@ <div class="card-body"> {% automarkup journal.blurb %} </div> - {% if journal.series_set.all|length > 0 %} + {% if journal.contained_series.all|length > 0 %} <div class="card-footer"> Series hosted in this Journal: <ul> - {% for series in journal.series_set.all %} + {% for series in journal.contained_series.all %} <li> <a href="{{ series.get_absolute_url }}" target="_blank">{{ series }}</a> </li> diff --git a/scipost_django/scipost/views.py b/scipost_django/scipost/views.py index 56f8684738251cbfa46e1eabd50451a04be590c0..b5f72a54844d76ce99f33e4da48df1901b67f34d 100644 --- a/scipost_django/scipost/views.py +++ b/scipost_django/scipost/views.py @@ -210,9 +210,9 @@ def portal_hx_home(request): "publications": Publication.objects.published() .exclude(doi_label__contains="Proc") .order_by("-publication_date", "-paper_nr") - .prefetch_related( - "in_issue__in_journal", "specialties", "collection_set__series" - )[:5], + .prefetch_related("in_issue__in_journal", "specialties", "collections__series")[ + :5 + ], } return render(request, "scipost/portal/_hx_home.html", context) @@ -241,7 +241,7 @@ def portal_hx_journals(request): if session_acad_field_slug and session_acad_field_slug != "all": journals = journals.filter( college__acad_field__slug=session_acad_field_slug, - ).prefetch_related("series_set") + ).prefetch_related("contained_series") context["journals"] = journals else: # build a dictionary of journals per branch / acad_field journals_dict = {} @@ -289,7 +289,7 @@ def portal_hx_publications_page(request): "in_issue__in_journal", "in_issue__in_volume__in_journal", "specialties", - "collection_set__series", + "collections__series", ) paginator = Paginator(publications, 10) page_nr = request.GET.get("page") diff --git a/scipost_django/series/admin.py b/scipost_django/series/admin.py index ba0ab82179bf4ea60a5c93d00c9412dd95286f23..a644eec5de0cb4d1e2891772551c1c9251fda8b9 100644 --- a/scipost_django/series/admin.py +++ b/scipost_django/series/admin.py @@ -36,6 +36,8 @@ class CollectionAdmin(admin.ModelAdmin): "series", "submissions", "publications", + "expected_authors", + "expected_editors", ] diff --git a/scipost_django/series/migrations/0007_collection_expected_editors.py b/scipost_django/series/migrations/0007_collection_expected_editors.py new file mode 100644 index 0000000000000000000000000000000000000000..2dfb931757d09c0fd088c1175e3633b66ef34987 --- /dev/null +++ b/scipost_django/series/migrations/0007_collection_expected_editors.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.18 on 2023-09-19 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0035_alter_profile_title'), + ('colleges', '0039_nomination_add_events'), + ('series', '0006_collection_expected_authors'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='expected_editors', + field=models.ManyToManyField(blank=True, related_name='collections_editing', to='colleges.Fellowship'), + ), + migrations.AlterField( + model_name='collection', + name='expected_authors', + field=models.ManyToManyField(blank=True, related_name='collections_authoring', to='profiles.Profile'), + ), + ] diff --git a/scipost_django/series/migrations/0008_alter_collections_related_names.py b/scipost_django/series/migrations/0008_alter_collections_related_names.py new file mode 100644 index 0000000000000000000000000000000000000000..1c3e8c1254d5cf9da5c97c003c710be175686675 --- /dev/null +++ b/scipost_django/series/migrations/0008_alter_collections_related_names.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.18 on 2023-09-20 16:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0128_populate_submission_object_types'), + ('submissions', '0142_alter_submission_author_list'), + ('series', '0007_collection_expected_editors'), + ] + + operations = [ + migrations.AlterField( + model_name='collection', + name='publications', + field=models.ManyToManyField(blank=True, related_name='collections', through='series.CollectionPublicationsTable', to='journals.Publication'), + ), + migrations.AlterField( + model_name='collection', + name='series', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collections', to='series.series'), + ), + migrations.AlterField( + model_name='collection', + name='submissions', + field=models.ManyToManyField(blank=True, related_name='collections', to='submissions.Submission'), + ), + migrations.AlterField( + model_name='series', + name='container_journals', + field=models.ManyToManyField(blank=True, related_name='contained_series', to='journals.Journal'), + ), + ] diff --git a/scipost_django/series/migrations/0009_collection_event_details.py b/scipost_django/series/migrations/0009_collection_event_details.py new file mode 100644 index 0000000000000000000000000000000000000000..ee35e4ce65a524d1aba19613b2df15516d4faec5 --- /dev/null +++ b/scipost_django/series/migrations/0009_collection_event_details.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-09-21 15:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('series', '0008_alter_collections_related_names'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='event_details', + field=models.TextField(blank=True, help_text='The details of the event, to be displayed as information about the collection in the paper.'), + ), + ] diff --git a/scipost_django/series/models.py b/scipost_django/series/models.py index b3b0394a0ed604d2ec39f16d651b50614e815603..dd0c2e20a0687df13f8ebcdfe21f8a567502b8e4 100644 --- a/scipost_django/series/models.py +++ b/scipost_django/series/models.py @@ -27,7 +27,9 @@ class Series(models.Model): blank=True, ) image = models.ImageField(upload_to="series/images/", blank=True) - container_journals = models.ManyToManyField("journals.Journal", blank=True) + container_journals = models.ManyToManyField( + "journals.Journal", blank=True, related_name="contained_series" + ) class Meta: verbose_name_plural = "series" @@ -45,7 +47,11 @@ class Collection(models.Model): """ series = models.ForeignKey( - "series.Series", blank=True, null=True, on_delete=models.CASCADE + "series.Series", + blank=True, + null=True, + on_delete=models.CASCADE, + related_name="collections", ) name = models.CharField(max_length=256, blank=True) slug = models.SlugField(unique=True, allow_unicode=True) @@ -58,17 +64,38 @@ class Collection(models.Model): ) event_start_date = models.DateField(null=True, blank=True) event_end_date = models.DateField(null=True, blank=True) + event_details = models.TextField( + blank=True, + help_text=( + "The details of the event, to be displayed as information " + "about the collection in the paper." + ), + ) image = models.ImageField(upload_to="series/collections/images/", blank=True) - expected_authors = models.ManyToManyField("profiles.Profile", blank=True) - submissions = models.ManyToManyField("submissions.Submission", blank=True) + expected_authors = models.ManyToManyField( + "profiles.Profile", blank=True, related_name="collections_authoring" + ) + expected_editors = models.ManyToManyField( + "colleges.Fellowship", blank=True, related_name="collections_editing" + ) + submissions = models.ManyToManyField( + "submissions.Submission", blank=True, related_name="collections" + ) publications = models.ManyToManyField( - "journals.Publication", through="series.CollectionPublicationsTable", blank=True + "journals.Publication", + through="series.CollectionPublicationsTable", + blank=True, + related_name="collections", ) def __str__(self): return self.name + @property + def name_with_series(self): + return f"{self.series.name} - {self.name}" + def get_absolute_url(self): return reverse("series:collection_detail", kwargs={"slug": self.slug}) diff --git a/scipost_django/series/templates/series/_hx_collection_expected_authors.html b/scipost_django/series/templates/series/_hx_collection_expected_authors.html index 48536a9394f624d796b2e2e0ce7ac2eb5ef66661..69f401aac66df0a0ea4b6e573d7895457f5c24e4 100644 --- a/scipost_django/series/templates/series/_hx_collection_expected_authors.html +++ b/scipost_django/series/templates/series/_hx_collection_expected_authors.html @@ -5,39 +5,40 @@ {% include 'scipost/messages.html' %} <table class="table"> <thead> - <tr> - <th>Profile</th> - <th>Actions</th> - </tr> + <tr> + <th>Profile</th> + <th>Actions</th> + </tr> </thead> + {% for profile in collection.expected_authors.all %} - <tr> - <td><a href="{{ profile.get_absolute_url }}">{{ profile }}</a></td> - <td> - <a - class="btn btn-sm btn-outline-danger" - hx-get="{% url 'series:_hx_collection_expected_author_action' slug=collection.slug profile_id=profile.id action='remove' %}" - hx-target="#profiles" - hx-confirm="Are you sure you want to remove {{ profile }} from expected authors in this Collection?" - ><small>Remove</small></a> - </td> - </tr> + <tr> + <td> + <a href="{{ profile.get_absolute_url }}">{{ profile }}</a> + </td> + <td> + <a class="btn btn-sm btn-outline-danger" + hx-get="{% url 'series:_hx_collection_expected_author_action' slug=collection.slug profile_id=profile.id action='remove' %}" + hx-target="#author_profiles" + hx-confirm="Are you sure you want to remove {{ profile }} from expected authors in this Collection?"><small>Remove</small></a> + </td> + </tr> {% empty %} - <tr> - <td colspan="4">No expected authors yet</td> - </tr> + <tr> + <td colspan="4">No expected authors yet</td> + </tr> {% endfor %} + </table> </div> <div class="col-md-4 p-4"> <h4>Add an expected author</h4> - <form - hx-post="{% url 'profiles:_hx_profile_dynsel_list' %}" - hx-trigger="keyup delay:200ms, change" - hx-target="#profile_search_results" - > - <div id="profile_search_form">{% crispy profile_search_form %}</div> + <form hx-post="{% url 'profiles:_hx_profile_dynsel_list' %}" + hx-trigger="keyup delay:200ms, change" + hx-target="#author_profile_search_results"> + <div id="author_profile_search_form">{% crispy author_profile_search_form %}</div> </form> - <div id="profile_search_results" class="border border-light m-2 p-1"></div> + <div id="author_profile_search_results" + class="border border-light m-2 p-1"></div> </div> </div> diff --git a/scipost_django/series/templates/series/_hx_collection_expected_editors.html b/scipost_django/series/templates/series/_hx_collection_expected_editors.html new file mode 100644 index 0000000000000000000000000000000000000000..bdd964de47563cd6224a853f52c898658f911016 --- /dev/null +++ b/scipost_django/series/templates/series/_hx_collection_expected_editors.html @@ -0,0 +1,46 @@ +{% load crispy_forms_tags %} + +<div class="row"> + <div class="col-md-8"> + {% include 'scipost/messages.html' %} + <table class="table"> + <thead> + <tr> + <th>Fellow</th> + <th>Actions</th> + </tr> + </thead> + + {% for editor in collection.expected_editors.all %} + {% with profile=editor.contributor.profile %} + <tr> + <td> + <a href="{{ profile.get_absolute_url }}">{{ profile }}</a> + </td> + <td> + <a class="btn btn-sm btn-outline-danger" + hx-get="{% url 'series:_hx_collection_expected_editor_action' slug=collection.slug fellowship_id=editor.id action='remove' %}" + hx-target="#editor_fellowships" + hx-confirm="Are you sure you want to remove {{ profile }} from expected editors in this Collection?"><small>Remove</small></a> + </td> + </tr> + {% endwith %} + {% empty %} + <tr> + <td colspan="4">No expected editors yet</td> + </tr> + {% endfor %} + + </table> + </div> + <div class="col-md-4 p-4"> + <h4>Add an expected editor</h4> + <form hx-post="{% url 'colleges:_hx_fellowship_dynsel_list' %}" + hx-trigger="keyup delay:200ms, change" + hx-target="#editor_fellowship_search_results"> + <div id="editor_fellowship_search_form">{% crispy editor_fellowship_search_form %}</div> + </form> + <div id="editor_fellowship_search_results" + class="border border-light m-2 p-1"></div> + </div> +</div> diff --git a/scipost_django/series/templates/series/collection_detail.html b/scipost_django/series/templates/series/collection_detail.html index 0811be7e2a207658f50f69d1aa9d8dcb1515ddfc..6043ca82745190eae529e0c9a0eae5ac61395af7 100644 --- a/scipost_django/series/templates/series/collection_detail.html +++ b/scipost_django/series/templates/series/collection_detail.html @@ -12,8 +12,13 @@ <span class="breadcrumb-item">{{ collection.name }}</span> {% endblock %} -{% block meta_description %}{{ block.super }} Collection detail {{ collection.series.name }} {{ collection.name }}{% endblock meta_description %} -{% block pagetitle %}: Collection detail{% endblock pagetitle %} +{% block meta_description %} + {{ block.super }} Collection detail {{ collection.series.name }} {{ collection.name }} +{% endblock meta_description %} + +{% block pagetitle %} + : Collection detail +{% endblock pagetitle %} {% block content %} @@ -22,42 +27,47 @@ <div class="row"> <div class="col-12"> <h1 class="highlight"> - <a href="{{ collection.series.get_absolute_url }}">{{ collection.series.name }}</a> - {% if collection.series.container_journals %} - <br> - <small><em> - <ul class="list list-inline mt-2 mb-0"> - <li class="list-inline-item mx-0">a series contained in</li> - {% for container in collection.series.container_journals.all %} - <li class="list-inline-item"> - <a href="{{ container.get_absolute_url }}">{{ container }}</a> - </li> - {% endfor %} - </ul> - </em></small> - {% endif %} + <a href="{{ collection.series.get_absolute_url }}">{{ collection.series.name }}</a> + + {% if collection.series.container_journals %} + <br /> + <small><em> + <ul class="list list-inline mt-2 mb-0"> + <li class="list-inline-item mx-0">a series contained in</li> + + {% for container in collection.series.container_journals.all %} + <li class="list-inline-item"> + <a href="{{ container.get_absolute_url }}">{{ container }}</a> + </li> + {% endfor %} + + </ul> + </em></small> + {% endif %} + </h1> - <h2 class="highlight"> - Collection {{ collection.name }} - </h2> + <h2 class="highlight">Collection {{ collection.name }}</h2> </div> </div> <div class="row"> <div class="col-12"> - <div class="p-2"> - {% automarkup collection.description %} - </div> + <div class="p-2">{% automarkup collection.description %}</div> + {% if collection.event_start_date and collection.event_end_date %} - <p class="p-2"> - Dates: from {{ collection.event_start_date }} to {{ collection.event_end_date }}. - </p> + <p class="p-2">Dates: from {{ collection.event_start_date }} to {{ collection.event_end_date }}.</p> {% endif %} + {% if collection.image %} - <div class="p-2"> - <img class="d-flex me-3 p-2" style="max-height: 350px; max-width: 100%;" alt="image" src="{{ collection.image.url }}"> - </div> + <div class="p-2"> + <img class="d-flex me-3 p-2" + style="max-height: 350px; + max-width: 100%" + alt="image" + src="{{ collection.image.url }}" /> + </div> {% endif %} + </div> </div> @@ -66,77 +76,88 @@ <h3>Editorial Administration</h3> <div class="card my-4"> - <div class="card-header"> - Expected authors for this Collection - </div> - <div class="card-body"> - <div - id="profiles" - hx-get="{% url 'series:_hx_collection_expected_authors' slug=collection.slug %}" - hx-trigger="load" - > - </div> - </div> + <div class="card-header">Expected authors for this Collection</div> + <div class="card-body"> + <div id="author_profiles" + hx-get="{% url 'series:_hx_collection_expected_authors' slug=collection.slug %}" + hx-trigger="load"></div> + </div> + </div> + + <div class="card my-4"> + <div class="card-header">Expected editors for this Collection</div> + <div class="card-body"> + <div id="editor_fellowships" + hx-get="{% url 'series:_hx_collection_expected_editors' slug=collection.slug %}" + hx-trigger="load"></div> + </div> </div> <div class="card my-4"> - <div class="card-header"> - Publications - </div> - <div class="card-body"> - <div - id="publications" - hx-get="{% url 'series:_hx_collection_publications' slug=collection.slug %}" - hx-trigger="load" - > - </div> - </div> + <div class="card-header">Publications</div> + <div class="card-body"> + <div id="publications" + hx-get="{% url 'series:_hx_collection_publications' slug=collection.slug %}" + hx-trigger="load"></div> + </div> </div> </div> {% endif %} {% with active_submissions=collection.submissions.under_consideration %} + {% if active_submissions|length > 0 %} <div class="row"> - <div class="col-12"> - <h3 class="highlight">Submissions to this Collection</h3> - <ul> - {% for submission in active_submissions.accepted %} - <li><strong class="text-success">accepted:</strong> - <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> - </li> - {% endfor %} - {% for submission in active_submissions.revision_requested %} - <li><strong class="text-primary">awaiting resubmission:</strong> - <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> - </li> - {% endfor %} - {% for submission in active_submissions.in_refereeing %} - <li><strong class="text-warning">under refereeing:</strong> - <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> - </li> - {% endfor %} - </ul> - </div> + <div class="col-12"> + <h3 class="highlight">Submissions to this Collection</h3> + <ul> + + {% for submission in active_submissions.accepted %} + <li> + <strong class="text-success">accepted:</strong> + <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> + </li> + {% endfor %} + + {% for submission in active_submissions.revision_requested %} + <li> + <strong class="text-primary">awaiting resubmission:</strong> + <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> + </li> + {% endfor %} + + {% for submission in active_submissions.in_refereeing %} + <li> + <strong class="text-warning">under refereeing:</strong> + <a href="{{ submission.get_absolute_url }}" target="_blank">{{ submission }}</a> + </li> + {% endfor %} + + </ul> + </div> </div> {% endif %} + {% endwith %} <div class="row"> <div class="col-12"> <h3 class="highlight">Publications in this Collection</h3> <ul> - {% for publication in collection.publications.all %} - <li><a href="{{ publication.get_absolute_url }}">{{ publication }}</a></li> - {% empty %} - <li>No Publication has yet been associated to this Collection</li> - {% endfor %} + + {% for publication in collection.publications.all %} + <li> + <a href="{{ publication.get_absolute_url }}">{{ publication }}</a> + </li> + {% empty %} + <li>No Publication has yet been associated to this Collection</li> + {% endfor %} + </ul> </div> </div> {% endblock content %} -{% block footer_script %} - {{ expected_author_form.media }} -{% endblock %} + +{% block footer_script %}{{ expected_author_form.media }}{% endblock %} diff --git a/scipost_django/series/urls.py b/scipost_django/series/urls.py index e6e1b4867a7e776fab8d9bfaf0f22c75e7a5b579..ecd8c4dc37ff5e2c1f252a22d728611f5193c3c9 100644 --- a/scipost_django/series/urls.py +++ b/scipost_django/series/urls.py @@ -24,11 +24,21 @@ urlpatterns = [ views._hx_collection_expected_authors, name="_hx_collection_expected_authors", ), + path( + "_hx_collection_expected_editors", + views._hx_collection_expected_editors, + name="_hx_collection_expected_editors", + ), path( "_hx_collection_expected_author_action/<int:profile_id>/<str:action>", views._hx_collection_expected_author_action, name="_hx_collection_expected_author_action", ), + path( + "_hx_collection_expected_editor_action/<int:fellowship_id>/<str:action>", + views._hx_collection_expected_editor_action, + name="_hx_collection_expected_editor_action", + ), path( "_hx_collection_publications", views._hx_collection_publications, diff --git a/scipost_django/series/views.py b/scipost_django/series/views.py index d342bcc59aba46faaa5bd5b88ce3ce7d84399151..f98e907bbc55c03ce003a5359bd1055c337bf047 100644 --- a/scipost_django/series/views.py +++ b/scipost_django/series/views.py @@ -13,6 +13,8 @@ from journals.models import Publication from journals.forms import PublicationDynSelForm from profiles.models import Profile from profiles.forms import ProfileSelectForm, ProfileDynSelForm +from colleges.forms import FellowshipDynSelForm +from colleges.models import Fellowship from .models import Series, Collection, CollectionPublicationsTable @@ -36,7 +38,7 @@ class SeriesDetailView(DetailView): context = super().get_context_data(*args, **kwargs) # Sort collections in series by event start date - context["collections"] = self.object.collection_set.all().order_by( + context["collections"] = self.object.collections.all().order_by( "event_start_date" ) return context @@ -62,11 +64,11 @@ def _hx_collection_expected_authors(request, slug): initial={ "action_url_name": "series:_hx_collection_expected_author_action", "action_url_base_kwargs": {"slug": collection.slug, "action": "add"}, - "action_target_element_id": "profiles", + "action_target_element_id": "author_profiles", "action_target_swap": "innerHTML", } ) - context = {"collection": collection, "profile_search_form": form} + context = {"collection": collection, "author_profile_search_form": form} return render(request, "series/_hx_collection_expected_authors.html", context) @@ -91,6 +93,36 @@ def _hx_collection_expected_author_action(request, slug, profile_id, action): ) +@permission_required("scipost.can_manage_series") +def _hx_collection_expected_editors(request, slug): + collection = get_object_or_404(Collection, slug=slug) + form = FellowshipDynSelForm( + initial={ + "action_url_name": "series:_hx_collection_expected_editor_action", + "action_url_base_kwargs": {"slug": collection.slug, "action": "add"}, + "action_target_element_id": "editor_fellowships", + "action_target_swap": "innerHTML", + } + ) + context = {"collection": collection, "editor_fellowship_search_form": form} + return render(request, "series/_hx_collection_expected_editors.html", context) + + +@permission_required("scipost.can_manage_series") +def _hx_collection_expected_editor_action(request, slug, fellowship_id, action): + collection = get_object_or_404(Collection, slug=slug) + fellowship = get_object_or_404(Fellowship, pk=fellowship_id) + if action == "add": + collection.expected_editors.add(fellowship) + if action == "remove": + collection.expected_editors.remove(fellowship) + return redirect( + reverse( + "series:_hx_collection_expected_editors", kwargs={"slug": collection.slug} + ) + ) + + @permission_required("scipost.can_manage_series") def _hx_collection_publications(request, slug): collection = get_object_or_404(Collection, slug=slug) diff --git a/scipost_django/submissions/admin.py b/scipost_django/submissions/admin.py index 44fc4388bc966887351479af1a195576cea4a3bb..672646c135dadc7ca61f8c512948a11df409d8b7 100644 --- a/scipost_django/submissions/admin.py +++ b/scipost_django/submissions/admin.py @@ -117,6 +117,14 @@ class SubmissionTieringInline(admin.StackedInline): ] +class CollectionInline(admin.StackedInline): + model = Submission.collections.through + extra = 0 + autocomplete_fields = [ + "collection", + ] + + class SubmissionAdmin(GuardedModelAdmin): date_hierarchy = "submission_date" list_display = ( @@ -162,6 +170,7 @@ class SubmissionAdmin(GuardedModelAdmin): ReadinessInline, SubmissionClearanceInline, SubmissionTieringInline, + CollectionInline, ] # Admin fields should be added in the fieldsets diff --git a/scipost_django/submissions/forms/__init__.py b/scipost_django/submissions/forms/__init__.py index 3c6276b36e72c6122f1a5cedabffcd435df9f9a6..4dfec8347d7979402f0e681a4603edd15d5b24f6 100644 --- a/scipost_django/submissions/forms/__init__.py +++ b/scipost_django/submissions/forms/__init__.py @@ -10,7 +10,9 @@ import datetime from django import forms from django.conf import settings from django.db import transaction -from django.db.models import Q, Count +from django.db.models import Q, Count, Value +from django.db.models.functions import Concat +from django.shortcuts import get_object_or_404 from django.forms.formsets import ORDERING_FIELD_NAME from django.utils import timezone @@ -79,6 +81,7 @@ from proceedings.models import Proceedings from profiles.models import Profile from scipost.services import DOICaller, ArxivCaller, FigshareCaller, OSFPreprintsCaller from scipost.models import Contributor, Remark +from series.models import Collection import strings import iThenticate @@ -1229,6 +1232,18 @@ class SubmissionForm(forms.ModelForm): Form to submit a new (re)Submission. """ + collection = forms.ChoiceField( + choices=[(None, "None")] + + list( + Collection.objects.all() + .order_by("-event_start_date") + # Short name is `event_suffix` if set, otherwise `event_name` + .annotate(name_with_series=Concat("series__name", Value(" - "), "name")) + .values_list("id", "name_with_series") + ), + help_text="If your submission is part of a collection (e.g. Les Houches), please select it from the list.<br>If your target collection is missing, please contact techsupport.", + required=False, + ) specialties = forms.ModelMultipleChoiceField( queryset=Specialty.objects.all(), widget=autocomplete.ModelSelect2Multiple( @@ -1384,9 +1399,11 @@ class SubmissionForm(forms.ModelForm): + str(kwargs["initial"].get("acad_field").id) ) - # Proceedings submission fields + # Proceedings & Collection submission fields if "Proc" not in self.submitted_to_journal.doi_label: del self.fields["proceedings"] + elif "LectNotes" not in self.submitted_to_journal.doi_label: + del self.fields["collection"] else: qs = self.fields["proceedings"].queryset.open_for_submission() self.fields["proceedings"].queryset = qs @@ -1468,6 +1485,51 @@ class SubmissionForm(forms.ModelForm): self.add_error("author_list", error_message) return author_list + def clean_collection(self): + """ + Check that the collection is part of a series in the target journal and that + at least one of the authors in the list is an expected author of the collection. + """ + # Check if no collection is selected or fetch the object + collection_id = self.cleaned_data.get("collection", "") + if collection_id == "": + return collection_id + collection = get_object_or_404(Collection, id=collection_id) + + # Check that the collection is part of a series in the target journal + if not self.submitted_to_journal in collection.series.container_journals.all(): + self.add_error( + "collection", + "This collection is not part of a series in the target journal. " + "Please check that the collection and journal are correct before contacting techsupport.", + ) + + # Check that the author list is not empty + str_author_list = self.cleaned_data.get("author_list", "") + if str_author_list == "": + self.add_error( + "collection", + "The author list is empty, so the collection may not be validated.", + ) + + clean_author_list = [name.strip() for name in str_author_list.split(",")] + expected_author_list = [a.full_name for a in collection.expected_authors.all()] + + # Check that the collection has defined expected authors + if len(expected_author_list) == 0: + self.add_error( + "collection", + "This collection has no specified authors yet, please contact techsupport.", + ) + # Check that at least one of the authors in the list is an expected author of the collection + elif not any(author in expected_author_list for author in clean_author_list): + self.add_error( + "collection", + "None of the authors in the author list match any of the expected authors of this collection. " + "Please check that the author list and collection are correct before contacting techsupport.", + ) + return collection_id + def clean_code_repository_url(self): """ Prevent having well-known servers in list. @@ -1593,6 +1655,10 @@ class SubmissionForm(forms.ModelForm): if self.is_resubmission(): self.process_resubmission(submission) + # Add the Collection if applicable + if collection := self.cleaned_data.get("collection", None): + submission.collections.add(collection) + # Gather first known author and Fellows. submission.authors.add(self.requested_by.contributor) self.set_fellowship(submission) @@ -1650,14 +1716,24 @@ class SubmissionForm(forms.ModelForm): Set the default set of (guest) Fellows for this Submission. """ qs = Fellowship.objects.active() + fellows = None + if submission.proceedings: # Add only Proceedings-related Fellowships fellows = qs.filter( proceedings=submission.proceedings ).return_active_for_submission(submission) - submission.fellows.set(fellows) - - else: + elif len(submission.collections.all()) > 0: + # Add the Fellowships of the collections + fellows = set([ + fellow + for collection in submission.collections.all() + for fellow in collection.expected_editors.all() + ]) + + # Check if neither a Proceedings nor a Collection is set + # or whether the above queries returned no results + if fellows is None or len(fellows) == 0: fellows = ( qs.regular_or_senior() .filter( @@ -1666,7 +1742,8 @@ class SubmissionForm(forms.ModelForm): ) .return_active_for_submission(submission) ) - submission.fellows.set(fellows) + + submission.fellows.set(fellows) class SubmissionReportsForm(forms.ModelForm): diff --git a/scipost_django/submissions/templates/submissions/_submission_summary.html b/scipost_django/submissions/templates/submissions/_submission_summary.html index 072a08083c9ac0111029bff634759f47ae79823b..33f2a6f1af5229aa8c2f597e4f3bdf561f83f476 100644 --- a/scipost_django/submissions/templates/submissions/_submission_summary.html +++ b/scipost_django/submissions/templates/submissions/_submission_summary.html @@ -86,16 +86,16 @@ <td>{{ submission.proceedings }}</td> </tr> {% endif %} - {% with ncollections=submission.collection_set.all|length %} + {% with ncollections=submission.collections.all|length %} {% if ncollections > 0 %} <tr> <td></td> <td> for consideration in Collection{{ ncollections|pluralize }}: <ul class="mb-0 pb-0"> - {% for collection in submission.collection_set.all %} + {% for collection in submission.collections.all %} <li> - <a href="{{ collection.get_absolute_url }}" target="_blank">{{ collection }}</a> + <a href="{{ collection.get_absolute_url }}" target="_blank">{{ collection.name_with_series }}</a> </li> {% endfor %} </ul>