From 973c2975ac79527c2b572712dd0244faed47bca0 Mon Sep 17 00:00:00 2001 From: Didi Kohen Date: Fri, 19 Feb 2021 11:47:45 +0200 Subject: [PATCH] Multiple fixes and small features Support for multiple same day meeting Avoid matching the same people in a different order Don't ignore repeats if failed Post attendence reply deadline according to the setting and not hardcoded Support multiple, comma seperated email domains --- check_attendance.py | 40 ++++++++++++++++++++++++++++++++++++++-- config.py | 2 +- generate_meeting.py | 36 ++++++++++++++++++++++++++++++------ utils.py | 17 +++++++++++++++-- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/check_attendance.py b/check_attendance.py index 17cc4e2..348e80a 100755 --- a/check_attendance.py +++ b/check_attendance.py @@ -10,6 +10,41 @@ from config import ATTENDANCE_TIME_LIMIT from utils import YES, NO, initialize, nostdout, download_shelve_from_s3, upload_shelve_to_s3 +def create_time_limit_string(): + """Create a string according to the time limit in the config + """ + if (ATTENDANCE_TIME_LIMIT < 60): + return "%d seconds" % ATTENDANCE_TIME_LIMIT + if (ATTENDANCE_TIME_LIMIT >= 60 and ATTENDANCE_TIME_LIMIT < 120): # Singular + mins = ATTENDANCE_TIME_LIMIT // 60 + secs = ATTENDANCE_TIME_LIMIT % 60 + time_string = "1 minute" + if secs > 0: + time_string += " and %d second" % secs + if secs > 1: + time_string += "s" + return time_string + hours = ATTENDANCE_TIME_LIMIT // (60*60) + mins = (ATTENDANCE_TIME_LIMIT // 60) - (hours * 60) + secs = ATTENDANCE_TIME_LIMIT % 60 + time_string = "" + if hours > 0: + time_string = "%d hour" % hours + if hours > 1: + time_string += "s" + if mins > 0 and secs > 0: + time_string += ", " + elif mins > 0: + time_string += " and " + if mins > 0: + time_string += "%d minute" % mins + if mins > 1: + time_string += "s" + if secs > 0: + time_string += " and %d second" % secs + if secs > 1: + time_string += "s" + return time_string def check_attendance(store, sc, users=None): """Pings all slack users with the email address stored in config.py. @@ -29,6 +64,7 @@ def check_attendance(store, sc, users=None): users = store["everyone"] user_len = len(users) messages_sent = {} + time_string = create_time_limit_string() if sc.rtm_connect(): for user in users: @@ -37,8 +73,8 @@ def check_attendance(store, sc, users=None): "chat.postMessage", channel="@" + user, as_user=True, - text="Will you be available for today's ({:%Y-%m-%d}) :coffee: shuffle? [yes/no] - Please reply within 1 hour!".format( - todays_meeting["date"] + text="Will you be available for today's ({:%Y-%m-%d}) :coffee: shuffle? [yes/no] - Please reply within {}!".format( + todays_meeting["date"], time_string ), ) message["user"] = user diff --git a/config.py b/config.py index e2066e5..af3e1c0 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,7 @@ import os from datetime import timedelta -EMAIL_DOMAIN = "example.com" +EMAIL_DOMAINS = "example.com" SLACK_TOKEN = "yourtoken" SLACK_CHANNEL = "#general" SHELVE_FILE = "meetings.shelve" diff --git a/generate_meeting.py b/generate_meeting.py index 9d826a5..c1f9c25 100755 --- a/generate_meeting.py +++ b/generate_meeting.py @@ -18,9 +18,14 @@ def get_google_hangout_url(): """Get a Google Hangouts URL with a unique identifier appended.""" return "{}{}".format(GOOGLE_HANGOUT_URL, uuid4()) +def get_pairings_stings(pairings): + pairings_strings = [] + for pair in pairings: + pairings_strings.append(",".join(sorted(pair))) + return pairings_strings def create_meetings( - store, sc, size=PAIRING_SIZE, whos_out=None, pairs=None, force_create=False, any_pair=False + store, sc, size=PAIRING_SIZE, whos_out=None, pairs=None, force_create=False, any_pair=False, same_day=False ): """Randomly generates sets of pairs for (usually) 1 on 1 meetings for a Slack team. @@ -35,6 +40,7 @@ def create_meetings( pairs (list): List of slack users explictly pair up (elements of list are in the form of 'username+username') force_create (Optional[bool]): If True, generate the meeting and write it to storage without asking if it should. any_pair (Optional[bool]): If True, generate any pairing - regardless if it's happened in the past or not + same_day (Optional[bool]): If True, don't delete the planned meeting to create multiple meetings in the same day Returns: bool: True if successful, False otherwise. @@ -104,7 +110,7 @@ def create_meetings( if "history" in store else [] ) - + previous_pairings = get_pairings_stings(previous_pairings) # == Handle Random Pairs == attempts = 1 max_attempts = 25 @@ -133,14 +139,15 @@ def create_meetings( if not any_pair: pairing = frozenset(pairing) - if pairing in previous_pairings and attempts <= max_attempts: + pairing_string = ",".join(sorted(pairing)) + if pairing_string in previous_pairings and attempts <= max_attempts: logging.info( "Generated already existing pair, going to try again (%s attempt(s) so far)", attempts, ) attempts += 1 continue - elif attempts > max_attempts: + if attempts > max_attempts: logging.warning("Max randomizing attempts reached, Got to start over again!!!!") return False @@ -169,7 +176,7 @@ def create_meetings( answer = input("\nAccept and write to shelf storage? (y/n) ").lower() if answer in YES: - if found_upcoming: + if found_upcoming and not same_day: del store["upcoming"] if "history" not in store: @@ -269,14 +276,19 @@ def main(args): whos_out=args.whos_out, pairs=args.pairs, force_create=args.force_create, - any_pair=attempt > max_attempts, + any_pair=args.any_pair, + same_day=args.same_day ) attempt += 1 + if attempt > max_attempts and not success: + break finally: store.close() if args.s3_sync: upload_shelve_to_s3() + if not success: + logging.error("Failed to find pairs!") if __name__ == "__main__": import argparse @@ -284,6 +296,18 @@ def main(args): parser = argparse.ArgumentParser( description="Generate random Coffee & Bagel meetups to promote synergy!" ) + parser.add_argument( + "--any-pair", + action="store_true", + required=False, + help="Ignore previous pairings", + ) + parser.add_argument( + "--same-day", + action="store_true", + required=False, + help="Don't delete the planned meeting upon creation, to allow multiple meetings with the same attendence", + ) parser.add_argument( "--out", "-o", diff --git a/utils.py b/utils.py index 0152039..16080ed 100644 --- a/utils.py +++ b/utils.py @@ -11,7 +11,7 @@ from botocore.exceptions import ClientError from slackclient import SlackClient -from config import EMAIL_DOMAIN, S3_BUCKET, S3_PREFIX, SLACK_TOKEN, SHELVE_FILE, SLACK_CHANNEL_ID +from config import EMAIL_DOMAINS, S3_BUCKET, S3_PREFIX, SLACK_TOKEN, SHELVE_FILE, SLACK_CHANNEL_ID logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s") @@ -117,6 +117,18 @@ def nostdout(): sys.stdout = save_stdout +def check_domain(domains, email): + """Alows filtering multiple domains + + Args: + domains: a list of domains + email: an email address to test + """ + for domain in domains: + if email.endswith("@" + domain): + return True + return False + def update_everyone_from_slack(store, sc): """Updates our store's list of `everyone`. @@ -136,6 +148,7 @@ def update_everyone_from_slack(store, sc): sc.api_call( "users.info",user=member) for member in users["members"] ] + domains=EMAIL_DOMAINS.split(",") store["everyone"] = [ m["user"]["name"] for m in fullusers @@ -143,5 +156,5 @@ def update_everyone_from_slack(store, sc): and not m["user"]["is_restricted"] and not m["user"]["is_bot"] and m["user"]["profile"].get("email") - and m["user"]["profile"]["email"].endswith("@" + EMAIL_DOMAIN) + and check_domain(domains, m["user"]["profile"]["email"]) ]