diff --git a/logic.py b/logic.py index cd86b0f..ac70eca 100644 --- a/logic.py +++ b/logic.py @@ -2,7 +2,7 @@ from io import StringIO from itertools import chain import os -from datetime import datetime, date, timedelta +from datetime import date, timedelta from dateutil.relativedelta import relativedelta @@ -17,23 +17,29 @@ F, IntegerField, Min, + Max, Case, Count, Q, Subquery, Func, When, - OuterRef + OuterRef, + Avg, + fields, + Value, + CharField, + DateTimeField, ) from django.db.models.functions import TruncMonth -from django.contrib import messages +from django.utils.text import capfirst from submission import models as sm from core.files import serve_temp_file from core import models as core_models from utils.function_cache import cache from journal import models as jm -from review import models as rm +from review import models as rm, logic as rl from metrics import models as mm from identifiers import models as id_models from plugins.reporting.templatetags import timedelta as td_tag @@ -217,10 +223,14 @@ def stream_csv(headers, iterable, filename=None): def response_streamer(): """Writes each row to an in-memory file that is yielded immediately""" - # Headers file_like = StringIO() - csv_writer = csv.writer(file_like) + csv_writer = csv.writer( + file_like, + delimiter=',', + quotechar='"', + quoting=csv.QUOTE_ALL, + ) csv_writer.writerow(headers) yield file_like.getvalue() @@ -1003,6 +1013,345 @@ def export_workflow_report(article_list, averages): return export_csv(all_rows) +@cache(600) +def get_report_reviewers_data(journal): + return rm.ReviewAssignment.objects.filter( + article__journal=journal + ).values( + 'reviewer', + 'reviewer__first_name', + 'reviewer__last_name', + ).annotate( + total_assignments=Count('id'), + accepted_assignments=Count( + 'id', + filter=Q(date_accepted__isnull=False) + ), + declined_assignments=Count( + 'id', + filter=Q( + date_declined__isnull=False, + decision__isnull=True, + ) + ), + withdrawn_assignments=Count( + 'id', + filter=Q( + decision='withdrawn', + ) + ), + completed_assignments=Count( + 'id', + filter=Q( + date_declined__isnull=True, + decision__isnull=False, + date_complete__isnull=False, + is_complete=True, + ) + ), + assignments_awaiting_response=Count( + 'id', + filter=Q( + decision__isnull=True, + date_accepted__isnull=True, + date_declined__isnull=True, + ), + ), + earliest_completed_review=Min( + 'date_complete', + filter=Q( + is_complete=True, + date_declined__isnull=True, + ), + ), + latest_completed_review=Max( + 'date_complete', + filter=Q( + is_complete=True, + date_declined__isnull=True, + ), + ), + average_rating=Avg('reviewerrating__rating'), + average_time_to_complete=Avg( + Case( + When( + date_requested__lte=F('date_complete'), + then=F('date_complete') - F('date_requested') + ), + default=None, + output_field=fields.DurationField(), + ), + filter=Q(date_complete__isnull=False, date_declined__isnull=True) + ), + ) + + +def get_report_author_data(journal): + return core_models.Account.objects.filter( + accountrole__role__slug="author", + accountrole__journal=journal, + ).values( + 'id', + 'username', + 'first_name', + 'last_name', + 'salutation', + ).annotate( + total_articles=Count( + 'authors__id', + filter=Q(authors__date_submitted__isnull=False, + authors__journal=journal), + ), + accepted_articles=Count( + 'authors__id', + filter=Q(authors__date_submitted__isnull=False, + authors__date_accepted__isnull=False, + authors__journal=journal), + ), + declined_articles=Count( + 'authors__id', + filter=Q(authors__date_submitted__isnull=False, + authors__date_declined__isnull=False, + authors__journal=journal), + ), + published_articles=Count( + 'authors__id', filter=Q( + authors__date_submitted__isnull=False, + authors__date_published__isnull=False, + authors__journal=journal), + ), + ) + + +@cache(600) +def get_workflow_times(journal, date_parts): + start = timezone.make_aware(timezone.datetime( + int(date_parts["start_month_y"]), + int(date_parts["start_month_m"]), + 1 + )) + end = timezone.make_aware(timezone.datetime( + int(date_parts["end_month_y"]), + int(date_parts["end_month_m"]), + 1 + # get first day of next month at 00:00:00 + ) + relativedelta(months=1)) + articles = sm.Article.objects.filter( + journal=journal, + date_submitted__range=[ + start, + end, + ], + date_published__isnull=False, + ) + + workflow_elements = core_models.WorkflowElement.objects.filter( + journal=journal, + element_name__in=core_models.WorkflowLog.objects.filter( + article__journal=journal, + article__date_submitted__range=[ + start, + end, + ], + article__date_published__isnull=False, + ).values('element__element_name') + ).order_by('order') + + workflow_element_names = [element.element_name for element in workflow_elements] + + workflow_logs = core_models.WorkflowLog.objects.filter( + article__journal=journal, + article__date_submitted__range=[ + start, + end, + ], + article__date_published__isnull=False, + ).select_related('article', 'element').order_by('article', 'timestamp') + + workflow_times_dict = {} + for article in articles: + workflow_times_dict[article.id] = {element_name: None for element_name in workflow_element_names} + + article_logs = workflow_logs.filter(article=article) + for index, workflow_log in enumerate(article_logs): + element_name = workflow_log.element.element_name + + try: + next_workflow_log = article_logs[index + 1] + time_in_element = next_workflow_log.timestamp - workflow_log.timestamp + except IndexError: + # For the last entry, compare with date_published + time_in_element = article.date_published - workflow_log.timestamp + + workflow_times_dict[article.pk][element_name] = time_in_element + + workflow_times_list = [] + for article in articles: + article_id = article.id + workflow_times_list.append({ + 'article_title': article.title, + 'date_submitted': article.date_submitted, + **workflow_times_dict[article_id] + }) + + return workflow_times_list + + +@cache(600) +def get_yearly_stats(journal): + # Get the earliest year of publication + earliest_year = sm.Article.objects.filter(journal=journal).order_by('date_submitted').values('date_submitted__year').first()['date_submitted__year'] + + yearly_stats = {} + + # Loop through each year from the earliest year to the current year + current_year = timezone.now().year + for year in range(earliest_year, current_year + 1): + yearly_stats[year] = sm.Article.objects.filter( + journal=journal, date_submitted__year=year + ).aggregate( + articles_submitted=Count('id'), + articles_assigned_review_revision=Count( + Case( + When(stage__in=['Assigned', 'Under Review', 'Under Revision'], then='id'), + default=None, + output_field=IntegerField() + ) + ), + articles_accepted=Count( + Case( + When(date_accepted__isnull=False, then='id'), + default=None, + output_field=IntegerField() + ) + ), + articles_rejected=Count( + Case( + When(date_declined__isnull=False, then='id'), + default=None, + output_field=IntegerField() + ) + ), + articles_published=Count( + Case( + When(date_published__isnull=False, then='id'), + default=None, + output_field=IntegerField() + ) + ), + articles_archived=Count( + Case( + When(stage='Archived', then='id'), + default=None, + output_field=IntegerField() + ) + ), + ) + return yearly_stats + + +def yearly_stats_iterable(yearly_stats): + iterable = list() + for year, stats in yearly_stats.items(): + iterable.append([ + year, + stats.get('articles_submitted'), + stats.get('articles_assigned_review_revision'), + stats.get('articles_accepted'), + stats.get('articles_rejected'), + stats.get('articles_published'), + stats.get('articles_archived'), + ]) + return iterable + + +def articles_under_review_iterable(review_assignments): + iterable = list() + for i, review in enumerate(review_assignments): + iterable.append([ + review.article.title, + review.reviewer.first_name, + review.reviewer.last_name, + review.reviewer.email, + review.request_decision_status(), + review.decision, + review.article.journal.site_url( + path=rl.generate_access_code_url('do_review', review, review.access_code) + ), + review.date_due, + review.date_complete + ]) + return iterable + + +def get_workflow_times_export(workflow_times_list): + headers = ['Article Title', 'Date Submitted'] + iterable = list() + for i, article_data in enumerate(workflow_times_list): + row = [] + for stage, time in article_data.items(): + if i == 0 and stage not in ['article_title', 'date_submitted']: # first in list + headers.append(capfirst(stage)) + row.append(time) + iterable.append(row) + + return headers, iterable + + +def get_report_author_export(authors): + headers = list() + iterable = list() + for i, author_data in enumerate(authors): + row = [] + for k, v in author_data.items(): + if i == 0: + headers.append(capfirst(k)) + row.append(v) + iterable.append(row) + + return headers, iterable + + +def get_reviewers_export(reviewers): + headers = [ + 'ID', + 'First Name', + 'Last Name', + 'Total Requests', + 'Accepted Requests', + 'Declined Requests', + 'Withdrawn Requests', + 'Completed Requests', + 'Requests Awaiting Response', + 'Earliest Completed Review', + 'Latest Completed Review', + 'Average Time to Completion', + 'Average Rating', + ] + iterable = list() + for reviewer in reviewers: + iterable.append( + [ + reviewer.get('reviewer'), + reviewer.get('reviewer__first_name'), + reviewer.get('reviewer__last_name'), + reviewer.get('total_assignments'), + reviewer.get('accepted_assignments'), + reviewer.get('declined_assignments'), + reviewer.get('withdrawn_assignments'), + reviewer.get('completed_assignments'), + reviewer.get('assignments_awaiting_response'), + reviewer.get('earliest_completed_review'), + reviewer.get('latest_completed_review'), + reviewer.get('average_time_to_complete'), + reviewer.get('average_rating'), + + ] + ) + + return headers, iterable + + def manager_metrics_summary(repository, start_date, end_date): preprints = repository_models.Preprint.objects.filter( repository=repository, @@ -1027,3 +1376,37 @@ def manager_metrics_summary(repository, start_date, end_date): ) ) return preprints + + +def get_time_to_first_decision(journal, start_date, end_date): + return sm.Article.objects.filter( + journal=journal, + date_submitted__gte=start_date, + date_submitted__lte=end_date, + ).annotate( + first_decision_date=ExpressionWrapper( + Func( + F("date_accepted"), + F("date_declined"), + F("revisionrequest__date_requested"), + function="LEAST", + ), + output_field=DateTimeField(), + ), + decision_type=Case( + When( + date_accepted=F("first_decision_date"), + then=Value("accept"), + ), + When( + date_declined=F("first_decision_date"), + then=Value("decline"), + ), + When( + revisionrequest__date_requested=F("first_decision_date"), + then=Value("revision"), + ), + default=Value("unknown"), + output_field=CharField(), + ), + ) diff --git a/templates/reporting/articles_under_review.html b/templates/reporting/articles_under_review.html new file mode 100644 index 0000000..b2b9089 --- /dev/null +++ b/templates/reporting/articles_under_review.html @@ -0,0 +1,94 @@ +{% extends "admin/core/base.html" %} +{% load timedelta %} + +{% block title %}Articles Under Review{% endblock %} +{% block title-section %} + Articles Under Review +{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Articles Under Review
  • +{% endblock %} + +{% block body %} + {% regroup review_assignments by article as article_list %} + +
    +
    +
    +
    +

    Context

    +
    +
    +

    Displays all of the articles currently under review for + this journal including their reviewers, recommendations + and due dates.

    +
    +
    +
    +
    +
    +
    +
    +

    Articles Under Review + in {{ request.journal.name|safe }}

    + Export to CSV +
    +
    +
    + {% regroup review_assignments by article as article_list %} + {% for article, review_assignments in article_list %} + + + + + + + + + + + + + + + + + + {% for review in review_assignments %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {{ article.safe_title }}
    First NameLast NameEmail AddressReviewer DecisionRecommendationAccess CodeDue DateDate Complete
    {{ review.reviewer.first_name }}{{ review.reviewer.last_name }}{{ review.reviewer.email }}{% if review.decision == 'withdrawn' %} + {% trans 'Withdrawn' %} + {{ review.date_complete|date:"Y-m-d" }} + {% elif review.date_accepted %}{% trans 'Accepted' %} + {{ review.date_accepted|date:"Y-m-d" }} + {% elif review.date_declined %}{% trans 'Declined' %} + {{ review.date_declined|date:"Y-m-d" }} + {% else %} + {% trans 'Awaiting acknowledgement' %}{% endif %} + {{ review.get_decision_display }}{% journal_url 'do_review' review.pk %}?access_code={{ review.access_code }}{{ review.date_due|date:"Y-m-d" }}{{ review.date_complete|date:"Y-m-d" }}
    No review assignments
    + {% endfor %} +
    +
    +
    +
    + + +{% endblock %} diff --git a/templates/reporting/index.html b/templates/reporting/index.html index 2ff59b6..32c05d7 100644 --- a/templates/reporting/index.html +++ b/templates/reporting/index.html @@ -139,6 +139,38 @@

    Data is presented in a series of averages followed by a table of data.

    View Report + +
  • + Experimental Reports +
    +

    Listed below are some experimental reports intended to be fully released as part of v1.6.

    + +
  • {% if request.repository %}
  • diff --git a/templates/reporting/report_authors_data.html b/templates/reporting/report_authors_data.html new file mode 100644 index 0000000..ac02ecb --- /dev/null +++ b/templates/reporting/report_authors_data.html @@ -0,0 +1,77 @@ +{% extends "admin/core/base.html" %} + +{% block title %}Journal Author Data Report{% endblock %} +{% block title-section %}Journal Author Data Report{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Journal Author Data Report
  • +{% endblock %} + +{% block body %} +
    +
    +
    +
    +

    Context

    +
    +
    +

    This report shows a journal's authors and their statistics. + Authors are limited to users with the author role on this + journal. Row totals may not add up where the author has + an article in workflow that is not accepted, rejected + or published.

    +
    +
    +
    +
    +
    +
    +

    Journal Author Data Report for {{ request.journal.name }}

    + Export to CSV +
    +
    +
    + + + + + + + + + + + + + + + + {% for author in authors %} + + + + + + + + + + + + {% endfor %} + +
    IDSalutationFirst NameLast NameUsernameTotal ArticlesAccepted ArticlesRejected ArticlesPublished Articles
    {{ author.id }} + {{ author.salutation|default_if_none:"" }} + {{ author.first_name }}{{ author.last_name }}{{ author.username }}{{ author.total_articles }}{{ author.accepted_articles }}{{ author.declined_articles }}{{ author.published_articles }}
    +
    +
    +
    +
    +
    +{% endblock %} + +{% block js %} + {% include "elements/datatables.html" with target="#reviewers_report" %} +{% endblock js %} diff --git a/templates/reporting/report_reviewers.html b/templates/reporting/report_reviewers.html new file mode 100644 index 0000000..8083b50 --- /dev/null +++ b/templates/reporting/report_reviewers.html @@ -0,0 +1,88 @@ +{% extends "admin/core/base.html" %} + +{% block title %}Peer Reviewers Data Report{% endblock %} +{% block title-section %}Peer Reviewers Data Report{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Peer Reviewers Data Report
  • +{% endblock %} + +{% block body %} +
    +
    +
    +
    +

    Context

    +
    +
    +

    This report displays totals for each reviewer including + total request, accepted, declined and withdrawn + reviews. Note that for the Average Time to + Completion column all review assignments where + date_requested is greater than date_completed are + ignored. This is usually due to an import where the + date_requested is not actually known. +

    +
    +
    +
    +
    +
    +
    +

    Peer Reviewers Data Report for {{ request.journal.name }}

    + Export to CSV +
    +
    +
    + + + + + + + + + + + + + + + + + + + + {% for reviewer in reviewers %} + + + + + + + + + + + + + + + + {% endfor %} + +
    IDFirst NameLast NameTotal RequestsAccepted RequestsDeclined RequestsWithdrawn RequestsCompleted RequestsRequests Awaiting ResponseEarliest Completed ReviewLatest Completed ReviewAverage Time to CompletionAverage Rating
    {{ reviewer.reviewer }}{{ reviewer.reviewer__first_name }}{{ reviewer.reviewer__last_name }}{{ reviewer.total_assignments }}{{ reviewer.accepted_assignments }}{{ reviewer.declined_assignments }}{{ reviewer.withdrawn_assignments }}{{ reviewer.completed_assignments }}{{ reviewer.assignments_awaiting_response }} + {{ reviewer.earliest_completed_review|date:"Y-m-d" }} + {{ reviewer.latest_completed_review|date:"Y-m-d" }}{{ reviewer.average_time_to_complete }}{{ reviewer.average_rating }}
    +
    +
    +
    +
    +
    +{% endblock %} + +{% block js %} + {% include "elements/datatables.html" with target="#reviewers_report" %} +{% endblock js %} diff --git a/templates/reporting/report_time_to_first_decision.html b/templates/reporting/report_time_to_first_decision.html new file mode 100644 index 0000000..9bbfef3 --- /dev/null +++ b/templates/reporting/report_time_to_first_decision.html @@ -0,0 +1,71 @@ +{% extends "admin/core/base.html" %} + +{% block title %}Reports{% endblock %} +{% block title-section %}Time to First Decision{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Time to First Decision
  • +{% endblock %} + +{% block body %} + +
    +
    +

    Filter by Dates

    +
    {% csrf_token %} + Export to CSV +
    +
    +
    +
    +
    + {{ date_form.errors|safe }} +
    + {{ date_form.start_date }} +
    +
    + {{ date_form.end_date }} +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + {% for article in articles %} + + + + + + + + + {% endfor %} + +
    IDTitleDate SubmittedFirst Decision DateDecision
    {{ article.pk }}{{ article.safe_title }}{{ article.date_submitted }}{{ article.first_decision_date }}{{ article.decision_type }}
    +
    +
    +
    +{% endblock %} + +{% block js %} + {% include "elements/datatables.html" with target="#first-decision-report" %} +{% endblock js %} diff --git a/templates/reporting/report_yearly_stats.html b/templates/reporting/report_yearly_stats.html new file mode 100644 index 0000000..b0fc584 --- /dev/null +++ b/templates/reporting/report_yearly_stats.html @@ -0,0 +1,68 @@ +{% extends "admin/core/base.html" %} +{% load timedelta %} + +{% block title %}Journal Yearly Statistics{% endblock %} +{% block title-section %} +Journal Yearly Statistics +{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Journal Yearly Statistics
  • +{% endblock %} + +{% block body %} +
    +
    +
    +
    +

    Context

    +
    +
    +

    For each year displays a series of journal statistics. Assigned, Under Review or Under Revision shows articles that were submitted in the given year that are still in that workflow element.

    +
    +
    +
    +
    +
    +
    +
    +

    Statistics by Year for {{ request.journal.name }}

    + Export to CSV +
    +
    +
    + + + + + + + + + + + + + + {% for year, stats in yearly_stats.items %} + + + + + + + + + + {% endfor %} + +
    YearArticles SubmittedAssigned, Under Review, or Under RevisionArticles AcceptedArticles RejectedArticles PublishedArticles Archived
    {{ year }}{{ stats.articles_submitted }}{{ stats.articles_assigned_review_revision }}{{ stats.articles_accepted }}{{ stats.articles_rejected }}{{ stats.articles_published }}{{ stats.articles_archived }}
    +
    +
    +
    +
    + + +{% endblock %} diff --git a/templates/reporting/workflow_timings_report.html b/templates/reporting/workflow_timings_report.html new file mode 100644 index 0000000..535b5f6 --- /dev/null +++ b/templates/reporting/workflow_timings_report.html @@ -0,0 +1,100 @@ +{% extends "admin/core/base.html" %} +{% load timedelta %} + +{% block title %}Workflow Stage Completion Time{% endblock %} +{% block title-section %}Workflow Stage Completion Time{% endblock %} + +{% block breadcrumbs %} + {{ block.super }} +
  • Reporting Index
  • +
  • Workflow Times Report
  • +{% endblock %} + +{% block body %} +
    +
    +
    +
    +

    Context

    +
    +
    +

    For published articles this report displays how long it was in each of the workflow stages.

    +
    +
    +

    Date Filters

    +
    +
    +

    Choose a + year and month using the YYYY-MM format. The report + will cover articles that have been published during + this specific time period.

    +
    +
    + {{ month_form.errors|safe }} +
    + {{ month_form.start_month }} +
    +
    + {{ month_form.end_month }} +
    +
    + +
    +
    +


    +
    + +


    +
    +
    +
    +
    +
    +
    +
    +

    Workflow Stage Time to Completion for {{ request.journal.name }}

    +
    + {% csrf_token %} + Export to CSV +
    +
    +
    +
    + + + + + + {% for element_name in workflow_times_list.0 %} + {% if element_name != 'article_title' and element_name != 'date_submitted' %} + + {% endif %} + {% endfor %} + + + + {% for workflow_times in workflow_times_list %} + + + + {% for element_name, time_in_element in workflow_times.items %} + {% if element_name != 'article_title' and element_name != 'date_submitted' %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
    Article TitleDate Submitted{{ element_name|capfirst }}
    {{ workflow_times.article_title }} + {{ workflow_times.date_submitted|date:"Y-m-d" }} + {{ time_in_element|default:"None" }}
    + +
    +
    +
    +
    +{% endblock %} + +{% block js %} + {% include "elements/datatables.html" with target="#workflow_timings" %} +{% endblock js %} diff --git a/urls.py b/urls.py index 945995f..d4f0786 100644 --- a/urls.py +++ b/urls.py @@ -60,9 +60,24 @@ re_path(r'^licenses/$', views.report_licenses, name='reporting_license'), - re_path(r'^workflow/$', + re_path(r'^workflow/submssion_to_publication/$', views.report_workflow, name='reporting_workflow'), + re_path(r'^reviewers/$', + views.report_reviewers, + name='report_reviewers'), + re_path(r'^authors/data/$', + views.report_authors_data, + name='report_authors_data'), + re_path(r'^workflow/stage_averages/$', + views.report_workflow_stage, + name='report_workflow_stages'), + re_path(r'^yearly_stats/$', + views.report_yearly_stats, + name='report_yearly_stats'), + re_path(r'^under-review/$', + views.report_articles_under_review, + name='report_articles_under_review'), re_path(r'^repository/metrics/$', views.report_preprints_metrics, name='report_preprints_metrics'), @@ -71,4 +86,9 @@ views.geographical_data, name='api_geographical_data' ), +re_path( + r'^first-decision/$', + views.report_time_to_first_decision, + name='report_time_to_first_decision' + ), ] diff --git a/views.py b/views.py index 64c25a1..22873b0 100644 --- a/views.py +++ b/views.py @@ -15,10 +15,11 @@ from core import models as core_models from journal import models from production import models as pm -from security.decorators import editor_user_required, is_repository_manager +from security.decorators import editor_user_required, has_journal, is_repository_manager from submission import models as sm from journal import models as jm from metrics import models as mm +from review import models as rm from api import permissions as api_permissions from utils import plugins from repository import models as repository_models @@ -626,6 +627,210 @@ def report_authors(request): return render(request, template, context) +@editor_user_required +def report_reviewers(request): + """ + Displays information about Peer Reviewers. + :param request: HttpRequest object + :return: HttpResponse or HttpRedirect + """ + reviewers = logic.get_report_reviewers_data( + journal=request.journal, + ) + if 'csv' in request.GET: + headers, iterable = logic.get_reviewers_export( + reviewers, + ) + return logic.stream_csv( + headers, + iterable, + filename=f'{request.journal.code}_reviewer_report.csv' + ) + template = 'reporting/report_reviewers.html' + context = { + 'reviewers': reviewers, + } + return render( + request, + template, + context, + ) + + +@editor_user_required +def report_authors_data(request): + """ + Displays information about a Journal's authors. + :param request: HttpRequest object + :return: HttpResponse or HttpRedirect + """ + authors = logic.get_report_author_data( + journal=request.journal, + ) + if 'csv' in request.GET: + headers, iterable = logic.get_report_author_export( + authors, + ) + return logic.stream_csv( + headers, + iterable, + f'{request.journal.code}_author_report.csv', + ) + template = 'reporting/report_authors_data.html' + context = { + 'authors': authors, + } + return render( + request, + template, + context, + ) + + +@editor_user_required +def report_workflow_stage(request): + start_month, end_month, date_parts = logic.get_start_and_end_months( + request, + ) + month_form = forms.MonthForm( + initial={ + 'start_month': start_month, 'end_month': end_month, + } + ) + workflow_times_list = logic.get_workflow_times( + request.journal, + date_parts, + ) + if 'csv' in request.GET: + headers, iterable = logic.get_workflow_times_export( + workflow_times_list, + ) + return logic.stream_csv( + headers, + iterable, + filename=f'{request.journal.code}_workflow_stage_completion.csv', + ) + + context = { + 'month_form': month_form, + 'workflow_times_list': workflow_times_list, + } + + template = 'reporting/workflow_timings_report.html', + return render( + request, + template, + context, + ) + + +@editor_user_required +def report_yearly_stats(request): + yearly_stats = logic.get_yearly_stats(request.journal) + + if 'csv' in request.GET: + return logic.stream_csv( + ['Year', 'Articles Submitted', 'In Review', ' Articles Accepted', + 'Artcicles Rejected', 'Articles Published', 'Articles Archived'], + logic.yearly_stats_iterable(yearly_stats), + filename=f'{request.journal.code}_yearly_stats.csv' + ) + + template = 'reporting/report_yearly_stats.html' + context = { + 'yearly_stats': yearly_stats + } + return render( + request, + template, + context, + ) + + +@has_journal +@editor_user_required +def report_articles_under_review(request): + review_assignments = rm.ReviewAssignment.objects.filter( + article__stage=sm.STAGE_UNDER_REVIEW, + article__journal=request.journal, + ).select_related( + 'article', + 'article__journal', + 'reviewer', + ).order_by( + 'article__title', + ) + if 'csv' in request.GET: + return logic.stream_csv( + [ + 'Title', 'First Name', 'Last Name', 'Email Address', + 'Reviewer Decision', 'Recommendation', 'Access Code', + 'Due Date', 'Date Complete' + ], + logic.articles_under_review_iterable(review_assignments), + filename=f'{request.journal.code}_articles_under_review.csv' + ) + template = 'reporting/articles_under_review.html' + context = { + 'review_assignments': review_assignments, + } + return render( + request, + template, + context, + ) + + +@editor_user_required +def report_time_to_first_decision(request): + """ + A report that shows for articles submitted during a time period + when their first decision was and the time to the first decision. + """ + start_date, end_date = logic.get_start_and_end_date(request) + date_form = forms.DateForm( + request.GET, + ) + articles = sm.Article.objects.none() + if date_form.is_valid(): + articles = logic.get_time_to_first_decision( + journal=request.journal, + start_date=start_date, + end_date=end_date, + ) + if "csv" in request.GET: + return logic.stream_csv( + headers=[ + 'ID', + 'Title', + 'Date Submitted', + 'First Decision Date', + 'Decision', + ], + iterable=[ + [ + article.pk, + article.title, + article.date_submitted, + article.first_decision_date, + article.decision_type, + ] + for article in articles + ], + filename=f'{request.journal.code}_time_to_first_decision.csv' + ) + template = 'reporting/report_time_to_first_decision.html' + context = { + 'articles': articles, + 'date_form': date_form, + } + return render( + request, + template, + context, + ) + + @is_repository_manager def report_preprints_metrics(request): form = forms.DateRangeForm( @@ -658,4 +863,4 @@ def report_preprints_metrics(request): request, template, context, - ) \ No newline at end of file + )