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 %}
+
+
+
+ | {{ article.safe_title }} |
+
+
+ | First Name |
+ Last Name |
+ Email Address |
+ Reviewer Decision |
+ Recommendation |
+ Access Code |
+ Due Date |
+ Date Complete |
+
+
+
+ {% for review in review_assignments %}
+
+ | {{ 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" }} |
+
+ {% empty %}
+
+ | No review assignments |
+
+ {% endfor %}
+
+
+ {% 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.
+
+ -
+ Workflow Stage Time to Completion
+
- Details how long an article took to complete each workflow stage.
+
+ -
+ Journal Authors Report
+
- Shows users with the author role and statistics about their submissions.
+
+ -
+ Peer Reviewers Data Report
+
- Shows statistics about each peer reviewer for this journal including total number of requests, accepted requests and declined requests.
+
+ -
+ Journal Yearly Statistics
+
- For each year this journal has a published article this report shows the total number of submissions, acceptances, rejections, publications and the number of articles from that year that are still under review.
+
+ -
+ Articles Under Review
+
- For each article lists the review assignments, reviewer information and status.
+
+ -
+ Time to First Decision
+
- For each article submitted in a given time period displays a time to first decision.
+
+
+
{% 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
+
+
+
+
+
+
+ | ID |
+ Salutation |
+ First Name |
+ Last Name |
+ Username |
+ Total Articles |
+ Accepted Articles |
+ Rejected Articles |
+ Published Articles |
+
+
+
+ {% for author in authors %}
+
+ | {{ 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 }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% 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
+
+
+
+
+
+
+ | 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 |
+
+
+
+ {% for reviewer in reviewers %}
+
+ | {{ 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 }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% 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 %}
+
+
+
+
+
+
+
+
+ | ID |
+ Title |
+ Date Submitted |
+ First Decision Date |
+ Decision |
+
+
+
+ {% for article in articles %}
+
+ | {{ article.pk }} |
+ {{ article.safe_title }} |
+ {{ article.date_submitted }} |
+ {{ article.first_decision_date }} |
+ {{ article.decision_type }} |
+
+
+ {% endfor %}
+
+
+
+
+
+{% 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
+
+
+
+
+
+
+ | Year |
+ Articles Submitted |
+ Assigned, Under Review, or Under Revision |
+ Articles Accepted |
+ Articles Rejected |
+ Articles Published |
+ Articles Archived |
+
+
+
+ {% for year, stats in yearly_stats.items %}
+
+ | {{ year }} |
+ {{ stats.articles_submitted }} |
+ {{ stats.articles_assigned_review_revision }} |
+ {{ stats.articles_accepted }} |
+ {{ stats.articles_rejected }} |
+ {{ stats.articles_published }} |
+ {{ stats.articles_archived }} |
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+{% 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.
+
+
+
+
+
+
+
+
+
+
+
Workflow Stage Time to Completion for {{ request.journal.name }}
+
+
+
+
+
+
+
+ | Article Title |
+ Date Submitted |
+ {% for element_name in workflow_times_list.0 %}
+ {% if element_name != 'article_title' and element_name != 'date_submitted' %}
+ {{ element_name|capfirst }} |
+ {% endif %}
+ {% endfor %}
+
+
+
+ {% for workflow_times in workflow_times_list %}
+
+ | {{ workflow_times.article_title }} |
+
+ {{ workflow_times.date_submitted|date:"Y-m-d" }} |
+ {% for element_name, time_in_element in workflow_times.items %}
+ {% if element_name != 'article_title' and element_name != 'date_submitted' %}
+
+ {{ time_in_element|default:"None" }} |
+ {% endif %}
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% 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
+ )