diff --git a/cms/data/notifications.yml b/cms/data/notifications.yml index 6aca840e49..1a67c6c7c2 100644 --- a/cms/data/notifications.yml +++ b/cms/data/notifications.yml @@ -142,3 +142,8 @@ update_request:publisher:rejected:notify: short: Your update request was rejected +journal:assed:discontinuing_soon:notify: + long: | + Journal "{title}" (id: {id}) will discontinue in {days} days. + short: + Journal discontinuing \ No newline at end of file diff --git a/doajtest/testbook/public_site/ToC.yml b/doajtest/testbook/public_site/ToC.yml new file mode 100644 index 0000000000..b481085cf9 --- /dev/null +++ b/doajtest/testbook/public_site/ToC.yml @@ -0,0 +1,15 @@ +suite: Public Site +testset: ToC +tests: +- title: Test Correctly Displayed Discontinued Date + context: + role: anonymous + steps: + - step: To prepare to do this test make sure there are 3 journals publically available in DOAJ + one with discontinued date in the past + one with discontinued date in the future + one with discontinued date today + - step: Search for every journal from the list above + results: + - On the ToC of the journal with discontinued date in the past or today - the discontinued date is displayed + - On the ToC of the journal with discontinued date in the future - the discontinued date is not displayed diff --git a/doajtest/unit/api_tests/test_apiv3_discovery.py b/doajtest/unit/api_tests/test_apiv3_discovery.py index b847c48b5b..ffc94feacc 100644 --- a/doajtest/unit/api_tests/test_apiv3_discovery.py +++ b/doajtest/unit/api_tests/test_apiv3_discovery.py @@ -241,7 +241,7 @@ def test_03_applications(self): for i in range(5): a = models.Suggestion() a.set_owner("owner") - a.set_created(dates.format(dates.after(now, i))) + a.set_created(dates.format(dates.seconds_after(now, i))) bj = a.bibjson() bj.title = "Test Suggestion {x}".format(x=i) bj.add_identifier(bj.P_ISSN, "{x}000-0000".format(x=i)) @@ -256,7 +256,7 @@ def test_03_applications(self): for i in range(5): a = models.Suggestion() a.set_owner("stranger") - a.set_created(dates.format(dates.after(now, i + 5))) + a.set_created(dates.format(dates.seconds_after(now, i + 5))) bj = a.bibjson() bj.title = "Test Suggestion {x}".format(x=i) bj.add_identifier(bj.P_ISSN, "{x}000-0000".format(x=i)) diff --git a/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py new file mode 100644 index 0000000000..f4f70f2f78 --- /dev/null +++ b/doajtest/unit/event_consumers/test_journal_discontinuing_soon_notify.py @@ -0,0 +1,86 @@ +from portality import models +from portality import constants +from portality.bll import exceptions +from doajtest.helpers import DoajTestCase +from doajtest.fixtures import JournalFixtureFactory, ApplicationFixtureFactory +from portality.events.consumers.journal_discontinuing_soon_notify import JournalDiscontinuingSoonNotify +from doajtest.fixtures import BackgroundFixtureFactory +import time + +# Mock required to make application lookup work +@classmethod +def pull_application(cls, id): + app = models.Application(**ApplicationFixtureFactory.make_application_source()) + return app + +@classmethod +def pull_by_key(cls, key, value): + ed = models.EditorGroup() + acc = models.Account() + acc.set_id('testuser') + acc.set_email("test@example.com") + acc.save(blocking=True) + ed.set_maned(acc.id) + ed.save(blocking=True) + + return ed + +class TestJournalDiscontinuingSoonNotify(DoajTestCase): + def setUp(self): + super(TestJournalDiscontinuingSoonNotify, self).setUp() + self.pull_application = models.Application.pull + models.Application.pull = pull_application + self.pull_by_key = models.EditorGroup.pull_by_key + models.EditorGroup.pull_by_key = pull_by_key + + def tearDown(self): + super(TestJournalDiscontinuingSoonNotify, self).tearDown() + models.Application.pull = self.pull_application + models.EditorGroup.pull_by_key = self.pull_by_key + + def test_consumes(self): + + event = models.Event("test:event", context={"data" : {"1234"}}) + assert not JournalDiscontinuingSoonNotify.consumes(event) + + event = models.Event("test:event", context={"data": {}}) + assert not JournalDiscontinuingSoonNotify.consumes(event) + + event = models.Event(constants.EVENT_JOURNAL_DISCONTINUING_SOON) + assert not JournalDiscontinuingSoonNotify.consumes(event) + + event = models.Event(constants.EVENT_JOURNAL_DISCONTINUING_SOON, context = {"journal": {"1234"}, "discontinue_date": "2002-22-02"}) + assert JournalDiscontinuingSoonNotify.consumes(event) + + def test_consume_success(self): + self._make_and_push_test_context("/") + + source = BackgroundFixtureFactory.example() + bj = models.BackgroundJob(**source) + # bj.save(blocking=True) + + acc = models.Account() + acc.set_id('testuser') + acc.set_email("test@example.com") + acc.add_role('admin') + acc.save(blocking=True) + + source = JournalFixtureFactory.make_journal_source() + journal = models.Journal(**source) + journal.save(blocking=True) + + event = models.Event(constants.BACKGROUND_JOB_FINISHED, context={"job" : bj.data, "journal" : journal.id}) + JournalDiscontinuingSoonNotify.consume(event) + + time.sleep(2) + ns = models.Notification.all() + assert len(ns) == 1 + + n = ns[0] + assert n.who == acc.id + assert n.created_by == JournalDiscontinuingSoonNotify.ID + assert n.classification == constants.NOTIFICATION_CLASSIFICATION_STATUS + assert n.long is not None + assert n.short is not None + assert n.action is not None + assert not n.is_seen() diff --git a/doajtest/unit/test_task_discontinued_soon.py b/doajtest/unit/test_task_discontinued_soon.py new file mode 100644 index 0000000000..3c9e4bcd1d --- /dev/null +++ b/doajtest/unit/test_task_discontinued_soon.py @@ -0,0 +1,77 @@ +import unittest +import datetime + +from doajtest.helpers import DoajTestCase + +from portality.core import app +from portality import models +from portality.tasks import find_discontinued_soon +from portality.ui.messages import Messages +from doajtest.fixtures import JournalFixtureFactory + +DELTA = app.config.get('DISCONTINUED_DATE_DELTA',1) + +class TestDiscontinuedSoon(DoajTestCase): + + def _date_to_found(self): + return (datetime.datetime.today() + datetime.timedelta(days=DELTA)).strftime('%Y-%m-%d') + + def _date_too_late(self): + return (datetime.datetime.today() + datetime.timedelta(days=DELTA+1)).strftime('%Y-%m-%d') + + def test_discontinued_soon_found(self): + + # Both these should be found + journal_discontinued_to_found_1 = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True)) + journal_discontinued_to_found_1.set_id("1") + jbib = journal_discontinued_to_found_1.bibjson() + jbib.title = "Discontinued Tomorrow 1" + jbib.discontinued_date = self._date_to_found() + journal_discontinued_to_found_1.save(blocking=True) + + journal_discontinued_to_found_2 = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True)) + journal_discontinued_to_found_2.set_id("2") + jbib = journal_discontinued_to_found_2.bibjson() + jbib.title = "Discontinued Tomorrow 2" + jbib.discontinued_date = self._date_to_found() + journal_discontinued_to_found_2.save(blocking=True) + + # that shouldn't be found + journal_discontinued_too_late = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True)) + journal_discontinued_too_late.set_id("3") + jbib = journal_discontinued_too_late.bibjson() + jbib.title = "Discontinued In 2 days" + jbib.discontinued_date = self._date_too_late() + journal_discontinued_too_late.save(blocking=True) + + job = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask.prepare("system") + task = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask(job) + task.run() + + assert len(job.audit) == 2 + assert job.audit[0]["message"] == Messages.DISCONTINUED_JOURNAL_FOUND_LOG.format(id="1") + assert job.audit[1]["message"] == Messages.DISCONTINUED_JOURNAL_FOUND_LOG.format(id="2") + + def test_discontinued_soon_not_found(self): + + # None of these should be found - this one discontinues in 2 days + journal_discontinued_too_late = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=True)) + journal_discontinued_too_late.set_id("1") + jbib = journal_discontinued_too_late.bibjson() + jbib.title = "Discontinued In 2 days" + jbib.discontinued_date = self._date_too_late() + journal_discontinued_too_late.save(blocking=True) + + # this one is not in doaj + journal_not_in_doaj = models.Journal(**JournalFixtureFactory.make_journal_source(in_doaj=False)) + journal_not_in_doaj.set_id("2") + jbib = journal_not_in_doaj.bibjson() + jbib.discontinued_date = self._date_to_found() + journal_not_in_doaj.save(blocking=True) + + job = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask.prepare("system") + task = find_discontinued_soon.FindDiscontinuedSoonBackgroundTask(job) + task.run() + + assert len(job.audit) == 1 + assert job.audit[0]["message"] == Messages.NO_DISCONTINUED_JOURNALS_FOUND_LOG diff --git a/portality/app.py b/portality/app.py index 4d1bf787c8..9f6b4d2466 100644 --- a/portality/app.py +++ b/portality/app.py @@ -285,6 +285,10 @@ def form_diff_table_subject_expand(val): return ", ".join(results) +@app.template_filter("is_in_the_past") +def is_in_the_past(dttm): + return dates.is_before(dttm, dates.today()) + ####################################################### diff --git a/portality/bll/services/events.py b/portality/bll/services/events.py index 2b27b85beb..3c5e96473c 100644 --- a/portality/bll/services/events.py +++ b/portality/bll/services/events.py @@ -21,6 +21,7 @@ from portality.events.consumers.journal_editor_group_assigned_notify import JournalEditorGroupAssignedNotify from portality.events.consumers.application_publisher_inprogress_notify import ApplicationPublisherInprogressNotify from portality.events.consumers.update_request_publisher_rejected_notify import UpdateRequestPublisherRejectedNotify +from portality.events.consumers.journal_discontinuing_soon_notify import JournalDiscontinuingSoonNotify class EventsService(object): @@ -44,7 +45,8 @@ class EventsService(object): JournalEditorGroupAssignedNotify, UpdateRequestPublisherAcceptedNotify, UpdateRequestPublisherAssignedNotify, - UpdateRequestPublisherRejectedNotify + UpdateRequestPublisherRejectedNotify, + JournalDiscontinuingSoonNotify ] def __init__(self): diff --git a/portality/constants.py b/portality/constants.py index ec908a4e82..ce7ed5e406 100644 --- a/portality/constants.py +++ b/portality/constants.py @@ -53,7 +53,9 @@ EVENT_APPLICATION_EDITOR_GROUP_ASSIGNED = "application:editor_group:assigned" EVENT_JOURNAL_ASSED_ASSIGNED = "journal:assed:assigned" EVENT_JOURNAL_EDITOR_GROUP_ASSIGNED = "journal:editor_group:assigned" +EVENT_JOURNAL_DISCONTINUING_SOON = "journal:discontinuing_soon" +NOTIFICATION_CLASSIFICATION_STATUS = "alert" NOTIFICATION_CLASSIFICATION_STATUS_CHANGE = "status_change" NOTIFICATION_CLASSIFICATION_ASSIGN = "assign" NOTIFICATION_CLASSIFICATION_CREATE = "create" diff --git a/portality/events/consumers/journal_discontinuing_soon_notify.py b/portality/events/consumers/journal_discontinuing_soon_notify.py new file mode 100644 index 0000000000..11da31bb96 --- /dev/null +++ b/portality/events/consumers/journal_discontinuing_soon_notify.py @@ -0,0 +1,55 @@ +# ~~JournalDiscontinuingSoonNotify:Consumer~~ +import json +import urllib.parse + +from portality.util import url_for +from portality.events.consumer import EventConsumer +from portality.core import app +from portality import constants +from portality import models +from portality.bll import DOAJ, exceptions +from portality.lib import edges +from portality import dao + +class JournalDiscontinuingSoonNotify(EventConsumer): + ID = "journal:assed:discontinuing_soon:notify" + + @classmethod + def consumes(cls, event): + return event.id == constants.EVENT_JOURNAL_DISCONTINUING_SOON and \ + event.context.get("journal") is not None and \ + event.context.get("discontinue_date") is not None + + @classmethod + def consume(cls, event): + journal_id = event.context.get("journal") + discontinued_date = event.context.get("discontinue_date") + + journal = models.Journal.pull(journal_id) + if journal is None: + return + + if not journal.editor_group: + return + + eg = models.EditorGroup.pull_by_key("name", journal.editor_group) + managing_editor = eg.maned + if not managing_editor: + return + + # ~~-> Notifications:Service ~~ + svc = DOAJ.notificationsService() + + notification = models.Notification() + notification.who = managing_editor + notification.created_by = cls.ID + notification.classification = constants.NOTIFICATION_CLASSIFICATION_STATUS + notification.long = svc.long_notification(cls.ID).format( + days=app.config.get('DISCONTINUED_DATE_DELTA',0), + title=journal.bibjson().title, + id=journal.id + ) + notification.short = svc.short_notification(cls.ID) + notification.action = url_for("admin.journal_page", journal_id=journal.id) + + svc.notify(notification) diff --git a/portality/lib/dates.py b/portality/lib/dates.py index a1af162b0b..52f6b0a809 100644 --- a/portality/lib/dates.py +++ b/portality/lib/dates.py @@ -119,15 +119,27 @@ def before_now(seconds: int) -> datetime: return before(now(), seconds) -def after(timestamp, seconds) -> datetime: +def seconds_after(timestamp, seconds) -> datetime: return timestamp + timedelta(seconds=seconds) +def seconds_after_now(seconds: int): + return seconds_after(datetime.utcnow(), seconds) + + +def days_after(timestamp, days): + return timestamp + timedelta(days=days) + + +def days_after_now(days: int): + return days_after(datetime.utcnow(), days) + + def eta(since, sofar, total) -> str: td = (now() - since).total_seconds() spr = float(td) / float(sofar) alltime = int(math.ceil(total * spr)) - fin = after(since, alltime) + fin = seconds_after(since, alltime) return format(fin) @@ -163,3 +175,13 @@ def day_ranges(fro: datetime, to: datetime) -> 'list[str]': def human_date(stamp, string_format=FMT_DATE_HUMAN) -> str: return reformat(stamp, out_format=string_format) + +def is_before(mydate, comparison=None): + if comparison is None: + comparison = datetime.utcnow() + if isinstance(mydate, str): + mydate = parse(mydate) + if isinstance(comparison, str): + comparison = parse(comparison) + return mydate < comparison + diff --git a/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py b/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py index adb85410aa..4315618d51 100644 --- a/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py +++ b/portality/migrate/20180106_1463_ongoing_updates/sync_journals_applications.py @@ -52,7 +52,7 @@ app_created = application.created_timestamp for journal in related_journals: almu = application.last_manual_update_timestamp - almu_adjusted = dates.after(almu, 3600) + almu_adjusted = dates.seconds_after(almu, 3600) # do a load of reporting prep jc_ac_diff = int((journal.created_timestamp - app_created).total_seconds()) diff --git a/portality/models/v2/journal.py b/portality/models/v2/journal.py index 41c50a7ce9..ac1ce42585 100644 --- a/portality/models/v2/journal.py +++ b/portality/models/v2/journal.py @@ -1131,4 +1131,4 @@ def query(self): "sort" : [ {"created_date" : {"order" : "desc"}} ] - } \ No newline at end of file + } diff --git a/portality/settings.py b/portality/settings.py index 9bd5a61d92..15d99834c4 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -439,6 +439,7 @@ "anon_export": {"month": "*", "day": "10", "day_of_week": "*", "hour": "6", "minute": "30"}, "old_data_cleanup": {"month": "*", "day": "12", "day_of_week": "*", "hour": "6", "minute": "30"}, "monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"}, + "find_discontinued_soon": {"month": "*", "day": "*", "day_of_week": "*", "hour": "0", "minute": "3"} } HUEY_TASKS = { @@ -1385,3 +1386,6 @@ # Pages under maintenance PRESERVATION_PAGE_UNDER_MAINTENANCE = False + +# report journals that discontinue in ... days (eg. 1 = tomorrow) +DISCONTINUED_DATE_DELTA = 0 \ No newline at end of file diff --git a/portality/static/js/edges/notifications.edge.js b/portality/static/js/edges/notifications.edge.js index 65788540ad..4b8c00e083 100644 --- a/portality/static/js/edges/notifications.edge.js +++ b/portality/static/js/edges/notifications.edge.js @@ -10,6 +10,10 @@ $.extend(true, doaj, { seen_url: "/dashboard/notifications/{notification_id}/seen", icons: { + alert: ``, finished: `
- {% if bibjson.discontinued_date %} + {% if bibjson.discontinued_date is not none and bibjson.discontinued_date | is_in_the_past %}Ceased publication on {{ bibjson.discontinued_datestamp.strftime("%d %B %Y") }}
{% endif %} diff --git a/portality/templates/email/discontinue_soon.jinja2 b/portality/templates/email/discontinue_soon.jinja2 new file mode 100644 index 0000000000..25debb60a8 --- /dev/null +++ b/portality/templates/email/discontinue_soon.jinja2 @@ -0,0 +1,7 @@ +{# +~~FindDiscontinuedSoonBackgroundTask:Email~~ +#} + +Following journals will discontinue in {{ days }} days. + +{{ data }} \ No newline at end of file diff --git a/portality/ui/messages.py b/portality/ui/messages.py index 094751c97c..ac7f9163bc 100644 --- a/portality/ui/messages.py +++ b/portality/ui/messages.py @@ -110,6 +110,12 @@ class Messages(object): NOTIFY__DEFAULT_SHORT_NOTIFICATION = "You have a new notification" + DISCONTINUED_JOURNAL_FOUND_LOG = "Journal discontinuing soon found: {id}" + DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_SENT_LOG = "Notification with journals discontinuing soon sent." + DISCONTINUED_JOURNALS_FOUND_NOTIFICATION_ERROR_LOG = "Error sending notification with journals discontinuing soon." + NO_DISCONTINUED_JOURNALS_FOUND_LOG = "No journals discontinuing soon found" + + @classmethod def flash(cls, tup): if isinstance(tup, tuple): diff --git a/test.cfg b/test.cfg index b6b18f3b15..210b69789b 100644 --- a/test.cfg +++ b/test.cfg @@ -55,7 +55,8 @@ HUEY_SCHEDULE = { "harvest": {"month": "*", "day": "*", "day_of_week": "*", "hour": "5", "minute": "30"}, "anon_export": CRON_NEVER, "old_data_cleanup": {"month": "*", "day": "*", "day_of_week": "3", "hour": "12", "minute": "0"}, - "monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"} + "monitor_bgjobs": {"month": "*", "day": "*/6", "day_of_week": "*", "hour": "10", "minute": "0"}, + "find_discontinued_soon": {"month": "*", "day": "*", "day_of_week": "*", "hour": "0", "minute": "3"} } # =======================