diff --git a/README.md b/README.md index e2cd9e8..f1b5360 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Welcome to the MakerSpace Leiden Aggregator. This software aggregates and distri - [Installation](#installation) - [Running Locally](#running-locally) - [Testing](#testing) -- [For Developers](#for-developers) - [Production Environment](#production-environment) ## Features @@ -71,12 +70,6 @@ Run the test suite with: python run-tests.py ``` -## For Developers - -Please refer to the feature documentation in the source directories for detailed information on specific components: -- [Chores](./src/aggregator/chores/README.md) - - ### Production Environment The server runs in production using systemd. diff --git a/server-dev.py b/server-dev.py index 2eac184..07c9b13 100755 --- a/server-dev.py +++ b/server-dev.py @@ -48,11 +48,6 @@ # }, # 'signal_bot': { # }, - "chores": { - "timeframe_in_days": 90, - "warnings_check_window_in_hours": 2, # Window to check for warnings to be sent (i.e. if server is down for longer, warnings might be lost) - "message_users_seen_no_later_than_days": 14, - }, } diff --git a/server-prod.py b/server-prod.py index 2fad6e1..5596b35 100755 --- a/server-prod.py +++ b/server-prod.py @@ -68,11 +68,6 @@ "signal_bot": { "some": "config", }, - "chores": { - "timeframe_in_days": 90, - "warnings_check_window_in_hours": 2, # Window to check for warnings to be sent (i.e. if server is down for longer, warnings might be lost) - "message_users_seen_no_later_than_days": 14, - }, } diff --git a/src/aggregator/chores/README.md b/src/aggregator/chores/README.md deleted file mode 100644 index 4645bbe..0000000 --- a/src/aggregator/chores/README.md +++ /dev/null @@ -1,58 +0,0 @@ -## Chores feature - -A "chore" is a one-shot or recurring (configurable in different ways) event that requires a certain -number of members to be carried out. - -The aggregator is where the logic lives, and mijn.makerspaceleiden.nl only provides a way to create the DB records, -show the upcoming chores and allow volunteering. - -### State - -The "Chores" table contains the chores. The actual events are not stored in the DB, but computed -on the fly by the aggregator. A given event is uniquely identified by the chore "name" and the -timestamp. The "Chore volunteers" table contains the volunteering sign ups, and contains the above -pair identifying the chore event + the user. In this regard, the DB schema is not properly normalized, -so, in case a chore was changed (like periodicity or starting day), it might be that some volunteering -signups would remain "orphan". But that's ok, as it simplifies the management of "events", that -don't need to be stored in the DB, updated, and so forth. - -### Lifecycle - -Each chore can have multiple "reminders" (configured via JSON). A reminder is a way to help users -engage and follow up on the given chore, either by signing up for volunteering, or being reminded -of the chore itself, to which they have signed up. The reminder type "missing_volunteers" is meant -to gather volunteers, by asking on the mailing list, or via the BOT. The type "volunteers_who_signed_up" -is meant to remind those who signed up to honour the committment. - -Each reminder can have multiple "nudges" (configured via JSON). A nudge has a unique key, so that the -system can remember (via Redis) whether the nudge was excercised, and can be of different types. - -An "email" nudge is performed by sending an email. - -A "volunteer_via_chat_bot" nudge is performed by sending messages via Telegram or Signal, to the -relevant users. - -### Algorithm - -The aggregator periodically executes the function "send_warnings_for_chores", which computes nudges to -send out by considering a time window configured in the main entry points (a couple of hours or so). -By looking into every chore, it finds the one that has reminders set for the most number of days in -the past. It then calculates every event for that many days in the future, so that every potentially -relevant reminder is taken into consideration, and filters those that actually have reminders falling -into the given time window. For each event it finally calculates what reminders have set off (i.e. fall -into the past). By looking into Redis it sends out those nudges that haven't been processed yet. - -The 2 main ideas behind this algorithm are: - -1. The future lookup: considering that for every chore there is a potentially infinite sequence of events, -looking up for a number of days equal to the earliest of the reminders is a way to -effectively limit the search to the only events that could generate a relevant reminder. - -2. The time window: because the system is asynchronous, we don't send out nudges at the exact moment -when it should happen, but we only find out later (when the "send_warnings_for_chores" is executed) that -a given nudge has transitioned to the past. So, in order to not send nudges twice, we store the nudge -key inside Redis for those that have been processed already. But the number of these keys would grow -indefinitely as time passes. Hence the time window idea: we only look as far back in the past as the -time window (a couple of hours), and we let the Redis keys expire after some time (twice the time window). -This way we effectively limit the usage of Redis and contain the complexity of the search algorithm. The -trade-off is that the system should never be down for a longer period than the configured time window. diff --git a/src/aggregator/chores/__init__.py b/src/aggregator/chores/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/aggregator/chores/chores_logic.py b/src/aggregator/chores/chores_logic.py deleted file mode 100644 index c610664..0000000 --- a/src/aggregator/chores/chores_logic.py +++ /dev/null @@ -1,316 +0,0 @@ -from collections import namedtuple -from datetime import datetime - -from ..clock import Time -from ..messages import AskForVolunteeringNotification, VolunteeringReminderNotification - -NudgesParams = namedtuple( - "NudgesParams", "volunteers now urls message_users_seen_no_later_than_days" -) - - -class ChoreEvent(object): - def __init__(self, chore, ts): - self.chore = chore - self.ts = ts - - def get_object_key(self): - return { - "chore_id": self.chore.chore_id, - "ts": self.ts.as_int_timestamp(), - } - - def for_json(self): - return { - "chore": self.chore.for_json(), - "when": { - "timestamp": self.ts.as_int_timestamp(), - "human_str": self.ts.human_str(), - }, - } - - def iter_nudges(self, params): - for reminder in self.chore.reminders: - for nudge in reminder.iter_nudges(self, params): - yield nudge - - -class RecurrentEventGenerator(object): - def __init__(self, starting_time, crontab, take_one_every): - self.starting_time = Time.from_datetime( - datetime.strptime(starting_time, "%d/%m/%Y %H:%M") - ) - self.crontab = crontab - self.take_one_every = take_one_every - - def iter_events_from_to(self, aggregator, ts_from, ts_to): - for idx, ts in enumerate(Time.iter_crontab(self.crontab, self.starting_time)): - if ts > ts_to: - break - if ts >= ts_from and idx % self.take_one_every == 0: - yield ChoreEvent(aggregator, ts) - - -class SingleOccurrenceEventGenerator(object): - def __init__(self, event_time): - self.event_time = Time.from_datetime( - datetime.strptime(event_time, "%d/%m/%Y %H:%M") - ) - - def iter_events_from_to(self, aggregator, ts_from, ts_to): - if ts_from <= self.event_time <= ts_to: - yield ChoreEvent(aggregator, self.event_time) - - -class BasicChore(object): - def __init__( - self, - chore_id, - name, - description, - min_required_people, - events_generation, - reminders, - ): - self.chore_id = chore_id - self.name = name - self.description = description - self.min_required_people = min_required_people - - self.events_generator = None - event_type = events_generation["event_type"] - data = dict(events_generation) - del data["event_type"] - if event_type == "recurrent": - self.events_generator = RecurrentEventGenerator(**data) - elif event_type == "single_occurrence": - self.events_generator = SingleOccurrenceEventGenerator(**data) - - self.reminders = [ - build_reminder(self.min_required_people, **reminder) - for reminder in reminders - ] - - def iter_events_from_to(self, ts_from, ts_to): - if self.events_generator: - for event in self.events_generator.iter_events_from_to( - self, ts_from, ts_to - ): - yield event - - def for_json(self): - return { - "chore_id": self.chore_id, - "name": self.name, - "description": self.description, - "min_required_people": self.min_required_people, - } - - def get_num_days_of_earliest_reminder(self): - return max([0] + [reminder.when["days_before"] for reminder in self.reminders]) - - -ALL_CHORE_TYPES = [ - BasicChore, -] - - -class EmailNudge(object): - def __init__(self, event, nudge_key, destination, subject, body): - self.event = event - self.nudge_key = nudge_key - self.destination = destination - self.subject = subject - self.body = body - - def __str__(self): - return "Email nudge. Chore: {0}, to: {1}, subject: {2}".format( - self.event.chore.name, self.destination, self.subject - ) - - def get_string_key(self): - event_key = self.event.get_object_key() - return "{0}-{1}-{2}".format( - self.nudge_key, event_key["chore_id"], event_key["ts"] - ) - - def send(self, aggregator, logger): - logger = logger.getLogger(subsystem="chores") - logger.info("Sending email nudge to: {0}".format(self.destination)) - aggregator.email_adapter.send_email( - self.destination, self.destination, self, logger - ) - - # Honour the Message API (see messages.py) - def get_subject_for_email(self): - return self.subject - - # Honour the Message API (see messages.py) - def get_email_text(self): - return self.body + "\n" - - -class VolunteerViaChatBotNudge(object): - def __init__(self, event, nudge, params): - self.event = event - self.nudge = nudge - self.message_users_seen_no_later_than_days = ( - params.message_users_seen_no_later_than_days - ) - self.urls = params.urls - - def __str__(self): - return "Chat BOT nudge: {0}".format(self.event.chore.name) - - def get_string_key(self): - event_key = self.event.get_object_key() - return "{0}-{1}-{2}".format( - self.nudge["nudge_key"], event_key["chore_id"], event_key["ts"] - ) - - def send(self, aggregator, logger): - logger = logger.getLogger(subsystem="chores") - users = aggregator.get_users_seen_no_later_than_days( - self.message_users_seen_no_later_than_days, logger - ) - logger.info( - "Sending Chat BOT nudge to: {0}".format( - ", ".join(["{0}".format(u.full_name) for u in users]) - ) - ) - for user in users: - aggregator.send_user_notification( - user, - AskForVolunteeringNotification(user, self.event, self.urls), - logger, - ) - - -class VolunteerReminderViaChatBotNudge(object): - def __init__(self, event, params): - self.event = event - self.volunteers = params.volunteers - - def __str__(self): - return "Volunteer reminder via Chat BOT: {0}".format(self.event.chore.name) - - def get_string_key(self): - event_key = self.event.get_object_key() - return "volunteer-reminder-{0}-{1}".format( - event_key["chore_id"], event_key["ts"] - ) - - def send(self, aggregator, logger): - logger = logger.getLogger(subsystem="chores") - logger.info( - "Sending volunteering reminder to: {0}".format( - ", ".join(["{0}".format(u.full_name) for u in self.volunteers]) - ) - ) - for user in self.volunteers: - aggregator.send_user_notification( - user, VolunteeringReminderNotification(user, self.event), logger - ) - - -class MissingVolunteersReminder(object): - def __init__(self, min_required_people, when, nudges): - self.min_required_people = min_required_people - self.when = when - self.nudges = nudges - - def iter_nudges(self, event, params): - reminder_time = calculate_reminder_time(event, self.when) - if params.now > reminder_time and len(params.volunteers) == 0: - for nudge in self.nudges: - if nudge["nudge_type"] == "email": - yield self._build_email_nudge(event, nudge, params) - if nudge["nudge_type"] == "volunteer_via_chat_bot": - yield VolunteerViaChatBotNudge(event, nudge, params) - - def _build_email_nudge(self, event, nudge, params): - template_data = { - "event_day": event.ts.strftime("%a %d/%m/%Y %H:%M"), - "chore_description": event.chore.description, - "num_volunteers_needed": self.min_required_people - len(params.volunteers), - "signup_url": params.urls.chores(), - } - return EmailNudge( - event=event, - nudge_key=nudge["nudge_key"], - destination=nudge["destination"], - subject=nudge["subject_template"].format(**template_data), - body=nudge["body_template"].format(**template_data), - ) - - -class VolunteersReminder(object): - def __init__(self, when): - self.when = when - - def iter_nudges(self, event, params): - reminder_time = calculate_reminder_time(event, self.when) - if params.now > reminder_time: - yield VolunteerReminderViaChatBotNudge(event, params) - - -class ChoresLogic(object): - def __init__(self, chores): - self.chores = [build_chore_instance(chore) for chore in chores] - - def get_events_from_to(self, ts_from, ts_to): - events = [] - for chore in self.chores: - events.extend(chore.iter_events_from_to(ts_from, ts_to)) - events.sort(key=lambda c: c.ts) - return events - - def iter_events_with_reminders_from_to(self, ts_from, ts_to): - num_days_of_earliest_reminder = max( - [0] + [chore.get_num_days_of_earliest_reminder() for chore in self.chores] - ) - for event in self.get_events_from_to( - ts_from, ts_to.add(num_days_of_earliest_reminder + 1, "days") - ): - for reminder in event.chore.reminders: - reminder_time = calculate_reminder_time(event, reminder.when) - if ts_from <= reminder_time <= ts_to: - yield event - - -# -- Utility functions ---- - - -def get_chore_type_class(chore): - for chore_class in ALL_CHORE_TYPES: - if chore_class.__name__ == chore.class_type: - return chore_class - raise Exception(f'Cannot find Python class for chore of type "{chore.class_type}"') - - -def build_chore_instance(chore): - chore_class = get_chore_type_class(chore) - return chore_class( - chore.chore_id, chore.name, chore.description, **chore.configuration - ) - - -def build_reminder(min_required_people, reminder_type, when, nudges=None): - if reminder_type == "missing_volunteers": - return MissingVolunteersReminder(min_required_people, when, nudges) - if reminder_type == "volunteers_who_signed_up": - return VolunteersReminder(when) - raise Exception(f"Unknown reminder type {repr(reminder_type)}") - - -def parse_hhmm(hhmm_str): - hh, mm = hhmm_str.split(":") - return int(hh), int(mm) - - -def calculate_reminder_time(event, when): - hh, mm = parse_hhmm(when["time"]) - reminder_time = event.ts.add(-when["days_before"], "days").replace( - hour=hh, minute=mm - ) - return reminder_time diff --git a/src/aggregator/chores/chores_tests.py b/src/aggregator/chores/chores_tests.py deleted file mode 100644 index 223d7b4..0000000 --- a/src/aggregator/chores/chores_tests.py +++ /dev/null @@ -1,82 +0,0 @@ -from ..testing_utils import STEFANO, AggregatorBaseTestSuite - - -class TestChores(AggregatorBaseTestSuite): - def test_sending_all_reminders(self): - self.clock.set_day_and_time("20/2/2019 8:0") - self.aggregator.user_entered_space(STEFANO.user_id, self.logger) - - # No nudges before the gentle reminder threshold - self.clock.set_day_and_time("23/2/2019 16:59") - self.aggregator.send_warnings_for_chores(self.logger) - self.assertEqual(self.emails_sent, []) - - # Email nudge after the gentle reminder threshold - self.clock.set_day_and_time("23/2/2019 17:01") - self.aggregator.send_warnings_for_chores(self.logger) - self.assertEqual( - self.emails_sent, - [("deelnemers@mailing.list", "deelnemers@mailing.list", "EmailNudge")], - ) - self.emails_sent = [] - - # No double nudges - self.aggregator.send_warnings_for_chores(self.logger) - self.assertEqual(self.emails_sent, []) - - # Email nudge after the hard reminder threshold - self.clock.set_day_and_time("24/2/2019 17:01") - self.aggregator.send_warnings_for_chores(self.logger) - self.assertEqual( - self.emails_sent, - [ - ("deelnemers@mailing.list", "deelnemers@mailing.list", "EmailNudge"), - (1, "AskForVolunteeringNotification"), - ], - ) - self.emails_sent = [] - self.assertEqual( - self.bot_messages, - [ - (1, "AskForVolunteeringNotification"), - ], - ) - self.bot_messages = [] - - # Wrong confirmation via chat-bot - self.send_bot_message(STEFANO, "wefwef") - self.assertEqual( - self.bot_messages, - [ - (1, "MessageUnknown"), - ], - ) - self.bot_messages = [] - - # Confirm volunteering via chat-bot - self.send_bot_message(STEFANO, "yes") - self.assertEqual( - self.bot_messages, - [ - (1, "MessageConfirmedVolunteering"), - ], - ) - self.bot_messages = [] - - # Reminder the day before - self.clock.set_day_and_time("25/2/2019 19:01") - self.aggregator.send_warnings_for_chores(self.logger) - self.assertEqual( - self.emails_sent, - [ - (1, "VolunteeringReminderNotification"), - ], - ) - self.emails_sent = [] - self.assertEqual( - self.bot_messages, - [ - (1, "VolunteeringReminderNotification"), - ], - ) - self.bot_messages = [] diff --git a/src/aggregator/database.py b/src/aggregator/database.py index 3ea1a58..9a9075a 100644 --- a/src/aggregator/database.py +++ b/src/aggregator/database.py @@ -1,9 +1,8 @@ -import json from contextlib import contextmanager import mysql.connector -from aggregator.model import Chore, Machine, Tag, User +from aggregator.model import Machine, Tag, User class MySQLAdapter(object): @@ -26,19 +25,6 @@ def get_all_users(self, logger): ) return [User(*row) for row in mycursor] - def get_all_chores(self, logger): - logger = logger.getLogger(subsystem="mysql") - logger.info("Reading all chores") - with self._connection() as db: - mycursor = db.cursor() - mycursor.execute( - "SELECT id, name, description, class_type, configuration FROM chores_chore" - ) - rows = [list(row) for row in mycursor] - for row in rows: - row[-1] = json.loads(row[-1]) - return [Chore(*row) for row in rows] - def get_all_machines(self, logger): logger = logger.getLogger(subsystem="mysql") logger.info("Reading all machines") @@ -95,35 +81,3 @@ def delete_telegram_user_id_for_user_id(self, user_id, logger): (user_id,), ) db.commit() - - def get_chore_volunteers_for_event(self, event, logger): - logger = logger.getLogger(subsystem="mysql") - logger.info( - f"Reading Chore volunteers for event {event.chore.name}-{event.ts.as_int_timestamp()}" - ) - with self._connection() as db: - mycursor = db.cursor() - mycursor.execute( - """ - SELECT members_user.id, members_user.first_name, members_user.last_name, members_user.email, members_user.telegram_user_id, members_user.phone_number, members_user.uses_signal, members_user.always_uses_email - FROM chores_chorevolunteer LEFT JOIN members_user ON (user_id = members_user.id) - WHERE chore_id = %s AND timestamp = %s - """, - (event.chore.chore_id, event.ts.as_int_timestamp()), - ) - return [User(*row) for row in mycursor] - - def add_chore_volunteer_for_event(self, event, user, logger): - logger = logger.getLogger(subsystem="mysql") - logger.info( - f"Adding user ID {user.user_id} as volunteer for chore {event.chore.name}-{event.ts.as_int_timestamp()}" - ) - with self._connection() as db: - mycursor = db.cursor() - mycursor.execute( - """ - INSERT INTO chores_chorevolunteer(timestamp, user_id, chore_id, created_at) VALUES (%s, %s, %s, NOW()) - """, - (event.ts.as_int_timestamp(), user.user_id, event.chore.chore_id), - ) - db.commit() diff --git a/src/aggregator/http_server.py b/src/aggregator/http_server.py index 32039e1..ce1ca9e 100644 --- a/src/aggregator/http_server.py +++ b/src/aggregator/http_server.py @@ -166,14 +166,6 @@ async def space_checkout(): ) return Response("Ok", mimetype="text/plain") - @app.route("/chores/overview", methods=["POST", "GET"]) - # @with_basic_auth - async def chores_overview(): - data = await worker_input_queue.add_task_with_result_future( - aggregator.get_chores_for_json, request.logger - ) - return jsonify(data) - # -- Web Socket ----- async def ws_sending(): diff --git a/src/aggregator/logic.py b/src/aggregator/logic.py index 968f12e..3f72539 100644 --- a/src/aggregator/logic.py +++ b/src/aggregator/logic.py @@ -5,7 +5,6 @@ from functools import partial from .bots.bot_logic import BotLogic -from .chores.chores_logic import ChoresLogic, NudgesParams from .messages import ( BASIC_COMMANDS, MachineLeftOnNotification, @@ -37,22 +36,12 @@ def __init__( email_adapter, task_scheduler, checkin_stale_after_hours, - chores_timeframe_in_days, - chores_warnings_check_window_in_hours, - chores_message_users_seen_no_later_than_days, ): self.database_adapter = database_adapter self.redis_adapter = redis_adapter self.notifications_queue = notifications_queue self.clock = clock self.checkin_stale_after_hours = checkin_stale_after_hours - self.chores_timeframe_in_days = chores_timeframe_in_days - self.chores_warnings_check_window_in_hours = ( - chores_warnings_check_window_in_hours - ) - self.chores_message_users_seen_no_later_than_days = ( - chores_message_users_seen_no_later_than_days - ) self.email_adapter = email_adapter self.task_scheduler = task_scheduler self.bot_logic = BotLogic(self) @@ -462,30 +451,6 @@ def _warn_user_of_machine_left_on(self, machine_name, user_id, logger): user, MachineLeftOnNotification(machine), logger ) - def _get_chores_logic(self, logger): - try: - logger.info("ok 1") - return ChoresLogic(self.database_adapter.get_all_chores(logger)) - logger.info("ok 2") - except Exception as e: - logger.exception("Unexpected exception {}".format(e)) - - def get_chores_for_json(self, logger): - try: - logger.info("ok 3") - chores_logic = self._get_chores_logic(logger) - now = self.clock.now() - events = chores_logic.get_events_from_to( - now, now.add(self.chores_timeframe_in_days, "days") - ) - except Exception as e: - logger.exception("Unexpected exception {}".format(e)) - - logger.info(events) - return { - "events": [event.for_json() for event in events], - } - def get_users_seen_no_later_than_days(self, num_days, logger): ts_and_users = self.redis_adapter.get_users_last_in_space(logger) threshold = self.clock.now().add(-num_days, "days") @@ -494,36 +459,3 @@ def get_users_seen_no_later_than_days(self, num_days, logger): for ts, user_id in ts_and_users if ts > threshold ] - - def send_warnings_for_chores(self, logger): - logger = logger.getLogger(subsystem="aggregator") - chores_logic = self._get_chores_logic(logger) - now = self.clock.now() - for event in chores_logic.iter_events_with_reminders_from_to( - now.add(-self.chores_warnings_check_window_in_hours, "hours"), now - ): - volunteers = self.database_adapter.get_chore_volunteers_for_event( - event, logger - ) - params = NudgesParams( - volunteers, - now, - self.urls, - self.chores_message_users_seen_no_later_than_days, - ) - for nudge in event.iter_nudges(params): - logger.info("Processing Chore nudge: {0}".format(nudge)) - if not self.redis_adapter.nudge_has_been_processed(nudge, logger): - nudge.send(self, logger) - self.redis_adapter.store_nudge_marker(nudge, logger) - - def user_volunteers_for_event(self, user_id, event, logger): - user = self._get_user_by_id(user_id, logger) - if not user: - raise Exception(f"User not found with ID {user_id}") - volunteers = self.database_adapter.get_chore_volunteers_for_event(event, logger) - if len(volunteers) >= event.chore.min_required_people: - # No need for more volunteers - return False - self.database_adapter.add_chore_volunteer_for_event(event, user, logger) - return True diff --git a/src/aggregator/logic_tests.py b/src/aggregator/logic_tests.py index cbe7b57..e08e1b1 100644 --- a/src/aggregator/logic_tests.py +++ b/src/aggregator/logic_tests.py @@ -55,76 +55,6 @@ def test_enter_and_leave_space(self): ], ) - def test_chores(self): - chores_state = self.aggregator.get_chores_for_json(self.logger) - self.assertEqual( - chores_state, - { - "events": [ - { - "chore": { - "chore_id": 1, - "description": "Empty trash every 2 weeks", - "min_required_people": 2, - "name": "Empty trash", - }, - "when": { - "human_str": "07:30:00 26/02/2019", - "timestamp": 1551166200, - }, - }, - { - "chore": { - "chore_id": 1, - "description": "Empty trash every 2 weeks", - "min_required_people": 2, - "name": "Empty trash", - }, - "when": { - "human_str": "07:30:00 12/03/2019", - "timestamp": 1552375800, - }, - }, - { - "chore": { - "chore_id": 1, - "description": "Empty trash every 2 weeks", - "min_required_people": 2, - "name": "Empty trash", - }, - "when": { - "human_str": "07:30:00 26/03/2019", - "timestamp": 1553585400, - }, - }, - { - "chore": { - "chore_id": 1, - "description": "Empty trash every 2 weeks", - "min_required_people": 2, - "name": "Empty trash", - }, - "when": { - "human_str": "07:30:00 09/04/2019", - "timestamp": 1554795000, - }, - }, - { - "chore": { - "chore_id": 1, - "description": "Empty trash every 2 weeks", - "min_required_people": 2, - "name": "Empty trash", - }, - "when": { - "human_str": "07:30:00 23/04/2019", - "timestamp": 1556004600, - }, - }, - ] - }, - ) - def test_stale_checkout_detection(self): # Check in at 11pm self.clock.set_time_of_day("23:00") diff --git a/src/aggregator/main.py b/src/aggregator/main.py index 8cbcf80..6a71833 100644 --- a/src/aggregator/main.py +++ b/src/aggregator/main.py @@ -36,7 +36,6 @@ def _main(config): from aggregator.redis import RedisAdapter from aggregator.timed_tasks import ( TaskScheduler, - start_checking_for_chores, start_checking_for_off_machines, start_checking_for_stale_checkins, ) @@ -83,9 +82,7 @@ def _main(config): # Application logic aggregator = Aggregator( MySQLAdapter(**config["mysql"]), - RedisAdapter( - clock, config["chores"]["warnings_check_window_in_hours"], **config["redis"] - ), + RedisAdapter(clock, **config["redis"]), http_server_input_message_queue, clock, email_adapter, @@ -93,9 +90,6 @@ def _main(config): config["check_stale_checkins"]["stale_after_hours"] if "check_stale_checkins" in config else 0, - config["chores"]["timeframe_in_days"], - config["chores"]["warnings_check_window_in_hours"], - config["chores"]["message_users_seen_no_later_than_days"], ) # Start MQTT listener @@ -144,7 +138,6 @@ def _main(config): config["check_stale_checkins"]["crontab"], logger, ) - start_checking_for_chores(aggregator, worker_input_queue, logger) start_checking_for_off_machines(aggregator, worker_input_queue, logger) task_scheduler.start_running_scheduled_tasks(worker_input_queue) diff --git a/src/aggregator/messages.py b/src/aggregator/messages.py index b70f78b..b3259f9 100644 --- a/src/aggregator/messages.py +++ b/src/aggregator/messages.py @@ -255,36 +255,6 @@ def get_subject_for_email(self): return "Forgotten when leaving the space" -class AskForVolunteeringNotification(BaseBotMessage): - next_commands = YES_NO_COMMANDS - - def __init__(self, user, event, urls): - self.user = user - self.event = event - self.urls = urls - - def get_text(self): - chore_description = self.event.chore.description - chore_event_ts_human = self.event.ts.strftime("%a %d/%m/%Y %H:%M") - return f"Hello {self.user.first_name}, your faithful Chat BOT here. We need help for {chore_description} at {chore_event_ts_human}. Would you like to volunteer?" - - def get_email_text(self): - chore_description = self.event.chore.description - chore_event_ts_human = self.event.ts.strftime("%a %d/%m/%Y %H:%M") - return f"Hello {self.user.first_name}, your faithful Chat BOT here.\n\nWe need help for {chore_description} at {chore_event_ts_human}.\n\nWould you like to volunteer?\n\nIf so, please sign up in: {self.urls.chores()}" - - def get_subject_for_email(self): - return "Volunteer needed" - - def set_chat_state(self, chat_id, bot_logic): - bot_logic.chat_states.set( - chat_id, - STATE_CONFIRM_VOLUNTEERING, - expiration_in_min=30, # Half hour - metadata={"user_id": self.user.user_id, "event": self.event}, - ) - - class MessageConfirmedVolunteering(BaseBotMessage): def get_text(self): return "Volunteering confirmed!" @@ -301,20 +271,6 @@ def get_subject_for_email(self): return "Volunteering not necessary anymore" -class VolunteeringReminderNotification(BaseBotMessage): - def __init__(self, user, event): - self.user = user - self.event = event - - def get_text(self): - chore_description = self.event.chore.description - chore_event_ts_human = self.event.ts.strftime("%a %d/%m/%Y %H:%M") - return f"{self.user.first_name}, here's a friendly reminder that you signed up for {chore_description} at {chore_event_ts_human}. Don't forget!" - - def get_subject_for_email(self): - return "Volunteering reminder" - - # -- Problems (used to compose notifications) ---- diff --git a/src/aggregator/model.py b/src/aggregator/model.py index 113435b..8fb4b80 100644 --- a/src/aggregator/model.py +++ b/src/aggregator/model.py @@ -59,11 +59,6 @@ def for_json(self): ] -class Chore(namedtuple("Chore", "chore_id name description class_type configuration")): - def for_json(self): - return dict(self._asdict()) - - # -- History lines ---- diff --git a/src/aggregator/redis.py b/src/aggregator/redis.py index df4c127..b7a075c 100644 --- a/src/aggregator/redis.py +++ b/src/aggregator/redis.py @@ -13,7 +13,6 @@ class RedisAdapter(object): def __init__( self, clock, - chores_warnings_check_window_in_hours, host, port, db, @@ -25,9 +24,6 @@ def __init__( history_lines_expiration_in_days, ): self.clock = clock - self.chores_warnings_check_window_in_hours = ( - chores_warnings_check_window_in_hours - ) self.redis = redis.Redis(host=host, port=port, db=db) self.key_prefix = key_prefix self.users_expiration_time_in_sec = users_expiration_time_in_sec @@ -298,23 +294,6 @@ def get_all_history_lines(self, logger): self.redis.srem(self._k_history_lines(), *ids_to_remove) return result - def nudge_has_been_processed(self, nudge, logger): - logger = logger.getLogger(subsystem="redis") - nudge_key = nudge.get_string_key() - logger.info(f"Checking nudge has been processed: {nudge_key}") - return self.redis.exists(self._k_nudge(nudge_key)) - - def store_nudge_marker(self, nudge, logger): - logger = logger.getLogger(subsystem="redis") - nudge_key = nudge.get_string_key() - logger.info(f"Checking nudge has been processed: {nudge_key}") - self.redis.setex( - self._k_nudge(nudge_key), - # The time to live of the logged record of the nudge in redis set to 10 days (2 x 120 x 3600 seconds). - self.chores_warnings_check_window_in_hours * 120 * 3600, - "processed", - ) - # -- Keys ---- def _k_history_line(self, hl_id): diff --git a/src/aggregator/testing_utils.py b/src/aggregator/testing_utils.py index 0dcf281..1f8241f 100644 --- a/src/aggregator/testing_utils.py +++ b/src/aggregator/testing_utils.py @@ -4,7 +4,7 @@ from .clock import MockClock from .logging import configure_logging_for_tests from .logic import Aggregator -from .model import Chore, Machine, User +from .model import Machine, User from .redis import RedisAdapter from .timed_tasks import TaskScheduler @@ -42,75 +42,6 @@ ] -EMPTY_TRASH = Chore( - 1, - "Empty trash", - "Empty trash every 2 weeks", - "BasicChore", - { - "min_required_people": 2, - "events_generation": { - "event_type": "recurrent", - "starting_time": "26/2/2019 7:00", - "crontab": "30 7 * * tue", # Every Tuesday at 7:30 - "take_one_every": 2, # Every other 2 events, i.e. every other Tuesday at 7:30 - }, - "reminders": [ - { - "reminder_type": "missing_volunteers", - "when": { - "days_before": 3, - "time": "17:00", - }, - "nudges": [ - { - "nudge_type": "email", - "nudge_key": "gentle_email_reminder", - "destination": "deelnemers@mailing.list", - "subject_template": "Volunteers needed for {event_day}, {chore_description}", - "body_template": "Hello, we need {num_volunteers_needed} volunteers for {event_day}, {chore_description}. Click here {signup_url}", - } - ], - }, - { - "reminder_type": "missing_volunteers", - "when": { - "days_before": 2, - "time": "17:00", - }, - "nudges": [ - { - "nudge_type": "email", - "nudge_key": "hard_email_reminder", - "destination": "deelnemers@mailing.list", - "subject_template": "Volunteers WANTED for {event_day}, {chore_description}", - "body_template": "Hello, we need {num_volunteers_needed} volunteers for {event_day}, {chore_description}. Click here {signup_url}", - }, - { - "nudge_type": "volunteer_via_chat_bot", - "nudge_key": "volunteer_via_chat_bot", - }, - ], - }, - { - "reminder_type": "volunteers_who_signed_up", - "when": { - "days_before": 1, - "time": "19:00", - }, - }, - ], - }, -) - -# import json -# print(json.dumps(EMPTY_TRASH.configuration, indent=2)) - -ALL_CHORES = [ - EMPTY_TRASH, -] - - class MockeHttpServerInputMessageQueue(object): def send_message(self, **kwargs): pass @@ -126,15 +57,6 @@ def get_all_users(self, logger): def get_all_machines(self, logger): return ALL_MACHINES - def get_all_chores(self, logger): - return ALL_CHORES - - def get_chore_volunteers_for_event(self, event, logger): - return self.test_suite.get_chore_volunteers_for_event(event) - - def add_chore_volunteer_for_event(self, event, user, logger): - self.test_suite.add_chore_volunteer_for_event(event, user) - class AggregatorBaseTestSuite(unittest.TestCase): def setUp(self): @@ -144,7 +66,6 @@ def setUp(self): http_server_input_message_queue = MockeHttpServerInputMessageQueue() self.redis_adapter = RedisAdapter( self.clock, - 2, "127.0.0.1", 6379, 0, @@ -166,9 +87,6 @@ def setUp(self): self, self.task_scheduler, 5, - 90, - 2, - 14, ) self.aggregator.signal_bot = self self.bot_messages = [] @@ -184,14 +102,6 @@ def _delete_all_redis_keys(self): for key in keys: self.redis_adapter.redis.delete(key) - def get_chore_volunteers_for_event(self, event): - key = "{chore_id}-{ts}".format(**event.get_object_key()) - return self.volunteers[key] - - def add_chore_volunteer_for_event(self, event, user): - key = "{chore_id}-{ts}".format(**event.get_object_key()) - return self.volunteers[key].append(user) - def send_notification(self, user, notification, logger): self.assertNotEqual(notification.get_text(), "") self.assertNotEqual(notification.get_markdown(), "") diff --git a/src/aggregator/timed_tasks.py b/src/aggregator/timed_tasks.py index e3a60ee..7bdd517 100644 --- a/src/aggregator/timed_tasks.py +++ b/src/aggregator/timed_tasks.py @@ -10,13 +10,6 @@ def early_in_the_morning(): worker_input_queue.add_task(aggregator.clean_stale_user_checkins, logger) -def start_checking_for_chores(aggregator, worker_input_queue, logger): - @aiocron.crontab("*/5 * * * *") # Every five minutes - @asyncio.coroutine - def early_in_the_morning(): - worker_input_queue.add_task(aggregator.send_warnings_for_chores, logger) - - def start_checking_for_off_machines(aggregator, worker_input_queue, logger): @aiocron.crontab("*/5 * * * *") # Every five minutes @asyncio.coroutine diff --git a/src/aggregator/urls.py b/src/aggregator/urls.py index cb27582..8b8c7cc 100644 --- a/src/aggregator/urls.py +++ b/src/aggregator/urls.py @@ -4,6 +4,3 @@ def notification_settings(self): def space_state(self): return "https://mijn.makerspaceleiden.nl/space_state" - - def chores(self): - return "https://mijn.makerspaceleiden.nl/chores/"