From 3c5f058a02ccb8cfb368954f7de116061881a8e3 Mon Sep 17 00:00:00 2001 From: Marc Sommerhalder Date: Fri, 23 Sep 2016 09:40:17 +0200 Subject: [PATCH] Rework notifications --- onegov/election_day/collection.py | 54 +++++++------- onegov/election_day/models/__init__.py | 10 ++- onegov/election_day/models/notification.py | 71 ++++++++++++++++++- onegov/election_day/utils.py | 30 -------- onegov/election_day/views/manage_elections.py | 23 +++--- onegov/election_day/views/manage_votes.py | 25 +++---- 6 files changed, 120 insertions(+), 93 deletions(-) diff --git a/onegov/election_day/collection.py b/onegov/election_day/collection.py index c1077128..76905456 100644 --- a/onegov/election_day/collection.py +++ b/onegov/election_day/collection.py @@ -1,5 +1,4 @@ -from onegov.ballot.models import Election, Vote -from onegov.election_day.models import Notification +from onegov.election_day.models import Notification, WebhookNotification class NotificationCollection(object): @@ -7,38 +6,37 @@ class NotificationCollection(object): def __init__(self, session): self.session = session - def add(self, url, last_change, election_or_vote): - """ Adds a new notification. """ - - notification = Notification() - notification.url = url - notification.last_change = last_change - if isinstance(election_or_vote, Election): - notification.election_id = election_or_vote.id - if isinstance(election_or_vote, Vote): - notification.vote_id = election_or_vote.id - - self.session.add(notification) - self.session.flush() - - return notification - def query(self): return self.session.query(Notification) - def by_election(self, election, url, last_change): - """ Returns the notification specified by given parameters. """ + def by_election(self, election): + """ Returns the notification for the given election and its + modification times. + + """ + return self.query().filter( Notification.election_id == election.id, - Notification.url == url, - Notification.last_change == last_change - ).first() + Notification.last_change == election.last_result_change + ).all() + + def by_vote(self, vote): + """ Returns the notification for the given vote and its modification + time. - def by_vote(self, vote, url, last_change): - """ Returns the notification specified by given parameters. """ + """ return self.query().filter( Notification.vote_id == vote.id, - Notification.url == url, - Notification.last_change == last_change - ).first() + Notification.last_change == vote.last_result_change + ).all() + + def trigger(self, request, model): + """ Triggers and adds all notifications. """ + + if request.app.principal.webhooks: + notification = WebhookNotification() + notification.trigger(request, model) + self.session.add(notification) + + self.session.flush() diff --git a/onegov/election_day/models/__init__.py b/onegov/election_day/models/__init__.py index 6e58cafc..968d9013 100644 --- a/onegov/election_day/models/__init__.py +++ b/onegov/election_day/models/__init__.py @@ -1,6 +1,12 @@ from onegov.election_day.models.archive import Archive -from onegov.election_day.models.principal import Principal from onegov.election_day.models.notification import Notification +from onegov.election_day.models.notification import WebhookNotification +from onegov.election_day.models.principal import Principal -__all__ = ['Archive', 'Principal', 'Notification'] +__all__ = [ + 'Archive', + 'Notification', + 'Principal', + 'WebhookNotification' +] diff --git a/onegov/election_day/models/notification.py b/onegov/election_day/models/notification.py index ccad4979..0ca54a21 100644 --- a/onegov/election_day/models/notification.py +++ b/onegov/election_day/models/notification.py @@ -1,13 +1,22 @@ +import json +import logging +import urllib.request + +from _thread import start_new_thread from onegov.ballot.models import Election, Vote from onegov.core.orm import Base from onegov.core.orm.mixins import TimestampMixin from onegov.core.orm.types import UTCDateTime from onegov.core.orm.types import UUID -from sqlalchemy import Text +from onegov.election_day.utils import get_summary from sqlalchemy import Column, ForeignKey +from sqlalchemy import Text from uuid import uuid4 +log = logging.getLogger('onegov.election_day') # noqa + + class Notification(Base, TimestampMixin): """ Stores triggered notifications. """ @@ -16,8 +25,8 @@ class Notification(Base, TimestampMixin): #: Identifies the notification id = Column(UUID, primary_key=True, default=uuid4) - #: The URL to call - url = Column(Text, nullable=False) + #: The action made (e.g. the URL called) + action = Column(Text, nullable=False) #: The corresponding election election_id = Column(Text, ForeignKey(Election.id), nullable=True) @@ -27,3 +36,59 @@ class Notification(Base, TimestampMixin): #: The last update of the corresponding election/vote last_change = Column(UTCDateTime) + + def update_from_model(self, model): + """ Copy """ + self.last_change = model.last_result_change + if isinstance(model, Election): + self.election_id = model.id + if isinstance(model, Vote): + self.vote_id = model.id + + def trigger(self, request, model): + """ Trigger the custom actions. """ + + raise NotImplementedError + + +def send_post_request(url, data, headers, timeout=30): + """ """ + try: + request = urllib.request.Request(url) + for header in headers: + request.add_header(header[0], header[1]) + urllib.request.urlopen(request, data, timeout) + except Exception as e: + log.error( + 'Error while sending a POST request to {}: {}'.format( + url, str(e) + ) + ) + + +class WebhookNotification(Notification): + + def trigger(self, request, model): + """ Posts the summary of the given vote or election to the webhook + URL defined for this principal. + + This only works for external URL. If posting to server itself is + needed, use a process instead of the thread: + + process = Process(target=send_post_request, args=(urls, data)) + process.start() + + """ + urls = request.app.principal.webhooks + if urls: + self.update_from_model(model) + self.action = 'webhooks' + + summary = get_summary(model, request) + data = json.dumps(summary).encode('utf-8') + headers = ( + ('Content-Type', 'application/json; charset=utf-8'), + ('Content-Length', len(data)) + ) + for url in urls: + start_new_thread(send_post_request, (url, data, headers)) diff --git a/onegov/election_day/utils.py b/onegov/election_day/utils.py index 88a68474..2350a7ee 100644 --- a/onegov/election_day/utils.py +++ b/onegov/election_day/utils.py @@ -1,14 +1,6 @@ -import _thread -import json -import logging -import urllib.request - from onegov.ballot import Election, Vote -log = logging.getLogger('onegov.election_day') # noqa - - def add_last_modified_header(response, last_modified): """ Adds the give date to the response as Last-Modified header. """ @@ -98,25 +90,3 @@ def get_archive_links(archive, request): str(year): request.link(archive.for_date(year)) for year in archive.get_years() } - - -def _post_to(url, data, timeout=30): - try: - data = json.dumps(data).encode('utf-8') - request = urllib.request.Request(url) - request.add_header('Content-Type', 'application/json; charset=utf-8') - request.add_header('Content-Length', len(data)) - urllib.request.urlopen(request, data, timeout) - except Exception as e: - log.error( - 'Error while sending a POST request to {}: {}'.format(url, e.msg) - ) - - -def post_to(url, data): - try: - _thread.start_new_thread(_post_to, (url, data)) - except: - return False - - return True diff --git a/onegov/election_day/views/manage_elections.py b/onegov/election_day/views/manage_elections.py index 311dbd94..c88ca378 100644 --- a/onegov/election_day/views/manage_elections.py +++ b/onegov/election_day/views/manage_elections.py @@ -105,8 +105,7 @@ def trigger_notifications(self, request, form): layout = ManageElectionsLayout(self, request) if form.submitted(request): - for url in request.app.principal.webhooks: - notifications.add(url, self.last_result_change, self) + notifications.trigger(request, self) return morepath.redirect(layout.manage_model_link) callout = None @@ -114,19 +113,15 @@ def trigger_notifications(self, request, form): title = _("Trigger notifications") button_class = 'primary' - for url in request.app.principal.webhooks: - existing = notifications.by_election( - self, url, self.last_result_change + if notifications.by_election(self): + callout = _( + "There are no changes since the last time the notifications " + "have been triggered!" ) - if existing is not None: - callout = _( - "There are no changes since the last time the notifications " - "have been triggered!" - ) - message = _( - "Do you really want to retrigger the notfications?", - ) - button_class = 'alert' + message = _( + "Do you really want to retrigger the notfications?", + ) + button_class = 'alert' return { 'message': message, diff --git a/onegov/election_day/views/manage_votes.py b/onegov/election_day/views/manage_votes.py index 2ff10757..010cd776 100644 --- a/onegov/election_day/views/manage_votes.py +++ b/onegov/election_day/views/manage_votes.py @@ -11,7 +11,6 @@ from onegov.election_day.forms import TriggerNotificationForm from onegov.election_day.forms import VoteForm from onegov.election_day.layout import ManageVotesLayout -from onegov.election_day.utils import get_vote_summary, post_to @ElectionDayApp.html(model=VoteCollection, template='manage_votes.pt', @@ -105,9 +104,7 @@ def trigger_notifications(self, request, form): layout = ManageVotesLayout(self, request) if form.submitted(request): - for url in request.app.principal.webhooks: - if post_to(url, get_vote_summary(self, request)): - notifications.add(url, self.last_result_change, self) + notifications.trigger(request, self) return morepath.redirect(layout.manage_model_link) callout = None @@ -115,19 +112,15 @@ def trigger_notifications(self, request, form): title = _("Trigger notifications") button_class = 'primary' - for url in request.app.principal.webhooks: - existing = notifications.by_vote( - self, url, self.last_result_change + if notifications.by_vote(self): + callout = _( + "There are no changes since the last time the notifications " + "have been triggered!" ) - if existing is not None: - callout = _( - "There are no changes since the last time the notifications " - "have been triggered!" - ) - message = _( - "Do you really want to retrigger the notfications?", - ) - button_class = 'alert' + message = _( + "Do you really want to retrigger the notfications?", + ) + button_class = 'alert' return { 'message': message,