diff --git a/.env.example b/.env.example index 0f0ca32848..50220306f4 100644 --- a/.env.example +++ b/.env.example @@ -170,6 +170,10 @@ GRADIENT_85 = 'rgba(42, 47, 52, 0.85)' # maximum number of emails that can be associated to a user model MAX_EMAILS_PER_USER = 10 +# maximum number of active projects that can be created by a submitting author at any time. +# if MAX_SUBMITTABLE_PROJECTS is reached, the user must wait for a project to be archived or published before starting another. +MAX_SUBMITTABLE_PROJECTS = 10 + # Max training report size in bytes MAX_TRAINING_REPORT_UPLOAD_SIZE = 1048576 ENABLE_LIGHTWAVE=True diff --git a/physionet-django/console/forms.py b/physionet-django/console/forms.py index a24cc5c876..e1ec6393a4 100644 --- a/physionet-django/console/forms.py +++ b/physionet-django/console/forms.py @@ -223,9 +223,6 @@ def save(self): # Reject if edit_log.decision == 0: project.reject() - # Have to reload this object which is changed by the reject - # function - edit_log = EditLog.objects.get(id=edit_log.id) # Resubmit with revisions elif edit_log.decision == 1: project.submission_status = SubmissionStatus.NEEDS_RESUBMISSION diff --git a/physionet-django/console/templates/console/console_navbar.html b/physionet-django/console/templates/console/console_navbar.html index c282399175..e25f14d351 100644 --- a/physionet-django/console/templates/console/console_navbar.html +++ b/physionet-django/console/templates/console/console_navbar.html @@ -20,7 +20,7 @@ - {% if perms.project.change_activeproject or perms.project.change_publishedproject or perms.project.change_archivedproject %} + {% if perms.project.change_activeproject or perms.project.change_publishedproject %}
  • +
    +
    {{ project.archive_datetime|date }}
    +
    + The project was archived. +
    +
    +
  • + {% endif %} + {# At this point, there may have been any number of submissions #} {% for e in edit_logs reversed %} @@ -139,7 +150,7 @@
    {{ e.decision_datetime|date }}
    {% if e.decision == 0 %} -

    : The editor rejected the submission.

    +

    The editor rejected the submission.

    {% elif e.decision == 1 %}

    The editor requested a resubmission with revisions.

    {% elif e.decision == 2 %} diff --git a/physionet-django/project/templates/project/static_submission_timeline.html b/physionet-django/project/templates/project/static_submission_timeline.html index 06e6d069e1..52db7566d8 100644 --- a/physionet-django/project/templates/project/static_submission_timeline.html +++ b/physionet-django/project/templates/project/static_submission_timeline.html @@ -51,26 +51,23 @@ {% endfor %} {% endif %} - - {# There may have been any number of submissions #} - {% for e in edit_logs %} - {% if e.is_resubmission %} -
  • -
  • +
    +
    {{ project.archive_datetime|date }}
    +
    + The project was archived.
    -
  • - {% endif %} +
    + + {% endif %} + + {# At this point, there may have been any number of submissions #} + {% for e in edit_logs reversed %}
  • + {% if e.decision_datetime %}
    {{ e.decision_datetime|date }}
    {% if e.decision == 0 %} @@ -86,12 +83,31 @@
  • {{ result }}
  • {% endfor %} -
    -

    The editor comments regarding the submission are as follows:

    - {{ e.editor_comments|linebreaks }} - +
    +

    The editor comments regarding the submission are as follows:

    + {{ e.editor_comments|linebreaks }} - + {# No decision yet #} + {% else %} +
    Currently
    +
    Waiting for editor decision
    + {% endif %} + + + {% if e.is_resubmission %} +
  • +
    +
    {{ e.submission_datetime|date }}
    +
    +

    The project was resubmitted for review.

    + {% if e.author_comments %} +

    The submitting author included the following comments:

    + {{ e.author_comments|linebreaks }} + {% endif %} +
    +
    +
  • + {% endif %} {% endfor %} diff --git a/physionet-django/project/test_views.py b/physionet-django/project/test_views.py index 83a9f8f0cc..a31d89c565 100644 --- a/physionet-django/project/test_views.py +++ b/physionet-django/project/test_views.py @@ -13,7 +13,6 @@ from project.models import ( AccessPolicy, ActiveProject, - ArchivedProject, Author, AuthorInvitation, DataAccessRequest, @@ -22,6 +21,7 @@ PublishedAuthor, PublishedProject, StorageRequest, + SubmissionStatus ) from user.models import User from user.test_views import TestMixin, prevent_request_warnings @@ -898,15 +898,20 @@ def test_archive(self): """ self.client.login(username='rgmark@mit.edu', password='Tester11!') project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database') + self.assertTrue(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.UNSUBMITTED)) author_id = project.authors.all().first().id abstract = project.abstract + # 'Delete' (archive) the project response = self.client.post(reverse('project_overview', args=(project.slug,)), data={'delete_project':''}) - # The ActiveProject model should be replaced, and all its - # related objects should point to the new ArchivedProject - self.assertFalse(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database')) - project = ArchivedProject.objects.get(title='MIT-BIH Arrhythmia Database') + + # The ActiveProject model should be set to "Archived" status + self.assertFalse(ActiveProject.objects.filter(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.UNSUBMITTED)) + project = ActiveProject.objects.get(title='MIT-BIH Arrhythmia Database', + submission_status=SubmissionStatus.ARCHIVED) self.assertTrue(Author.objects.get(id=author_id).project == project) self.assertEqual(project.abstract, abstract) diff --git a/physionet-django/project/views.py b/physionet-django/project/views.py index e8968e0523..ecbe385bd5 100644 --- a/physionet-django/project/views.py +++ b/physionet-django/project/views.py @@ -33,7 +33,6 @@ ActiveProject, Affiliation, AnonymousAccess, - ArchivedProject, Author, AuthorInvitation, DataAccess, @@ -120,6 +119,13 @@ def view_wrapper(request, *args, **kwargs): else: allow = False + # Authors cannot view archived projects + if ( + project.submission_status == SubmissionStatus.ARCHIVED + and not user.has_perm("project.change_activeproject") + ): + allow = False + # Post authentication if request.method == 'POST': if post_auth_mode == 1: @@ -221,19 +227,15 @@ def project_home(request): InvitationResponseFormSet = modelformset_factory(AuthorInvitation, form=forms.InvitationResponseForm, extra=0) - active_authors = Author.objects.filter(user=user, - content_type=ContentType.objects.get_for_model(ActiveProject)) - archived_authors = Author.objects.filter(user=user, - content_type=ContentType.objects.get_for_model(ArchivedProject)) - published_authors = PublishedAuthor.objects.filter(user=user, - project__is_latest_version=True) request_reviewers = DataAccessRequestReviewer.objects.filter(reviewer=user, is_revoked=False, project__is_latest_version=True) + published_projects = PublishedProject.objects.filter(authors__user=user, + is_latest_version=True).order_by("-publish_datetime") + # Get the various projects. - projects = [a.project for a in active_authors] - published_projects = [a.project for a in published_authors] + [ a.project for a in request_reviewers] + published_projects = [a for a in published_projects] + [a.project for a in request_reviewers] for p in published_projects: p.new_button = p.can_publish_new(user) p.requests_button = p.can_approve_requests(user) @@ -251,7 +253,11 @@ def project_home(request): pending_author_approvals = [] missing_affiliations = [] pending_revisions = [] - for p in projects: + + active_projects = ActiveProject.objects.filter(authors__user=user).exclude( + submission_status=SubmissionStatus.ARCHIVED).order_by("-creation_datetime") + + for p in active_projects: if (p.submission_status == SubmissionStatus.NEEDS_APPROVAL and not p.all_authors_approved()): if p.authors.get(user=user).is_submitting: @@ -263,7 +269,9 @@ def project_home(request): pending_revisions.append(p) if p.submission_status == SubmissionStatus.UNSUBMITTED and p.authors.get(user=user).affiliations.count() == 0: missing_affiliations.append([p, p.authors.get(user=user).creation_date]) - archived_projects = [a.project for a in archived_authors] + + archived_projects = ActiveProject.objects.filter( + authors__user=user, submission_status=SubmissionStatus.ARCHIVED).order_by("-creation_datetime") invitation_response_formset = InvitationResponseFormSet( queryset=AuthorInvitation.get_user_invitations(user)) @@ -279,7 +287,7 @@ def project_home(request): request, 'project/project_home.html', { - 'projects': projects, + 'projects': active_projects, 'published_projects': published_projects, 'archived_projects': archived_projects, 'missing_affiliations': missing_affiliations, @@ -298,11 +306,22 @@ def create_project(request): user = request.user - n_submitting = Author.objects.filter(user=user, is_submitting=True, - content_type=ContentType.objects.get_for_model(ActiveProject)).count() - if n_submitting >= ActiveProject.MAX_SUBMITTING_PROJECTS: - return render(request, 'project/project_limit_reached.html', - {'max_projects':ActiveProject.MAX_SUBMITTING_PROJECTS}) + # Filter ActiveProject instances (excluding ARCHIVED) + # Get Author objects related to these ActiveProjects + active_projects = ActiveProject.objects.exclude(submission_status=SubmissionStatus.ARCHIVED) + n_submitting = Author.objects.filter( + user=user, + is_submitting=True, + content_type=ContentType.objects.get_for_model(ActiveProject), + object_id__in=active_projects.values_list('id', flat=True) + ).count() + + if n_submitting >= settings.MAX_SUBMITTABLE_PROJECTS: + return render( + request, + "project/project_limit_reached.html", + {"max_projects": settings.MAX_SUBMITTABLE_PROJECTS}, + ) if request.method == 'POST': form = forms.CreateProjectForm(user=user, data=request.POST) @@ -327,11 +346,22 @@ def new_project_version(request, project_slug): user = request.user - n_submitting = Author.objects.filter(user=user, is_submitting=True, - content_type=ContentType.objects.get_for_model(ActiveProject)).count() - if n_submitting >= ActiveProject.MAX_SUBMITTING_PROJECTS: - return render(request, 'project/project_limit_reached.html', - {'max_projects':ActiveProject.MAX_SUBMITTING_PROJECTS}) + # Filter ActiveProject instances (excluding ARCHIVED) + # Get Author objects related to these ActiveProjects + active_projects = ActiveProject.objects.exclude(submission_status=SubmissionStatus.ARCHIVED) + n_submitting = Author.objects.filter( + user=user, + is_submitting=True, + content_type=ContentType.objects.get_for_model(ActiveProject), + object_id__in=active_projects.values_list('id', flat=True) + ).count() + + if n_submitting >= settings.MAX_SUBMITTABLE_PROJECTS: + return render( + request, + "project/project_limit_reached.html", + {"max_projects": settings.MAX_SUBMITTABLE_PROJECTS}, + ) previous_projects = PublishedProject.objects.filter( slug=project_slug).order_by('-version_order') @@ -370,7 +400,7 @@ def project_overview(request, project_slug, **kwargs): under_submission = project.under_submission() if request.method == 'POST' and 'delete_project' in request.POST and is_submitting and not under_submission: - project.fake_delete() + project.archive(archive_reason=1, clear_files=True) return redirect('delete_project_success') return render(request, 'project/project_overview.html', @@ -1509,10 +1539,14 @@ def archived_submission_history(request, project_slug): user = request.user try: # Checks if the user is an author - project = ArchivedProject.objects.get(slug=project_slug, authors__user=user) - except ArchivedProject.DoesNotExist: - if user.has_perm('project.change_archivedproject'): - project = get_object_or_404(ArchivedProject, slug=project_slug) + project = ActiveProject.objects.get(slug=project_slug, + submission_status=SubmissionStatus.ARCHIVED, + authors__user=user) + except ActiveProject.DoesNotExist: + if user.has_perm('project.change_activeproject'): + project = get_object_or_404(ActiveProject, + slug=project_slug, + submission_status=SubmissionStatus.ARCHIVED) else: raise Http404() diff --git a/physionet-django/user/fixtures/demo-user.json b/physionet-django/user/fixtures/demo-user.json index 23fc251e0d..39070d6642 100644 --- a/physionet-django/user/fixtures/demo-user.json +++ b/physionet-django/user/fixtures/demo-user.json @@ -14389,11 +14389,6 @@ "project", "activeproject" ], - [ - "change_archivedproject", - "project", - "archivedproject" - ], [ "add_dua", "project", @@ -14482,11 +14477,6 @@ "project", "activeproject" ], - [ - "change_archivedproject", - "project", - "archivedproject" - ], [ "can_view_access_logs", "project", diff --git a/physionet-django/user/management/commands/resetdb.py b/physionet-django/user/management/commands/resetdb.py index 96a8ebdfa5..29238248cb 100644 --- a/physionet-django/user/management/commands/resetdb.py +++ b/physionet-django/user/management/commands/resetdb.py @@ -19,7 +19,7 @@ from django.core.management.base import BaseCommand from lightwave.views import DBCAL_FILE -from project.models import ActiveProject, PublishedProject, ArchivedProject +from project.models import ActiveProject, PublishedProject from user.models import User, CredentialApplication diff --git a/physionet-django/user/models.py b/physionet-django/user/models.py index 2ad2e396f2..a126ad1106 100644 --- a/physionet-django/user/models.py +++ b/physionet-django/user/models.py @@ -21,9 +21,9 @@ from django.utils.crypto import constant_time_compare from django.utils.translation import gettext as _ -from project.validators import validate_version from project.modelcomponents.access import AccessPolicy from project.modelcomponents.fields import SafeHTMLField +from project.validators import validate_version from user import validators from user.userfiles import UserFiles from user.enums import TrainingStatus, RequiredField