diff --git a/app/controller/command/commands/__init__.py b/app/controller/command/commands/__init__.py index 69c94857..b5c8c80b 100644 --- a/app/controller/command/commands/__init__.py +++ b/app/controller/command/commands/__init__.py @@ -1,6 +1,7 @@ """Pack the modules contained in the commands directory.""" import app.controller.command.commands.team as team import app.controller.command.commands.user as user +import app.controller.command.commands.export as export import app.controller.command.commands.token as token import app.controller.command.commands.project as project import app.controller.command.commands.karma as karma @@ -9,6 +10,7 @@ TeamCommand = team.TeamCommand UserCommand = user.UserCommand +ExportCommand = export.ExportCommand TokenCommand = token.TokenCommand ProjectCommand = project.ProjectCommand KarmaCommand = karma.KarmaCommand diff --git a/app/controller/command/commands/export.py b/app/controller/command/commands/export.py new file mode 100644 index 00000000..fe5a6e35 --- /dev/null +++ b/app/controller/command/commands/export.py @@ -0,0 +1,178 @@ +"""Command parsing for user events.""" +import logging +import shlex + +from argparse import ArgumentParser, _SubParsersAction +from app.controller import ResponseTuple +from app.controller.command.commands.base import Command +from db.facade import DBFacade +from app.model import User +from db.utils import get_team_by_name, get_team_members +from utils.slack_parse import check_permissions + + +class ExportCommand(Command): + """Represent Export Command Parser.""" + + command_name = "export" + permission_error = "You do not have the sufficient " \ + "permission level for this command!" + lookup_error = "Lookup error!" + no_emails_missing_msg = "All members have emails! Nice!" + char_limit_exceed_msg = "WARNING! Could not export all emails for " \ + "exceeding slack character limits :(" + no_user_msg = "No members found for exporting emails!" + desc = f"for dealing with {command_name}s" + # Slack currently allows to send 16000 characters max + MAX_CHAR_LIMIT = 15950 + + def __init__(self, db_facade: DBFacade): + """Initialize export command.""" + logging.info("Initializing ExportCommand instance") + self.parser = ArgumentParser(prog="/rocket") + self.parser.add_argument("export") + self.subparser = self.init_subparsers() + self.help = self.get_help() + self.facade = db_facade + + def init_subparsers(self) -> _SubParsersAction: + """ + Initialize subparsers for export command. + + :meta private: + """ + subparsers = self.parser.add_subparsers(dest="which") + + # Parser for emails command + parser_view = subparsers.add_parser("emails") + parser_view.set_defaults(which="emails", + help="(Admin/Lead only) Export emails " + "of all users") + parser_view.add_argument("--team", metavar="TEAM", + type=str, action='store', + help="(Admin/Lead only) Export emails" + " by team name") + + return subparsers + + def get_help(self, subcommand: str = None) -> str: + """Return command options for user events with Slack formatting.""" + + def get_subcommand_help(sc: str) -> str: + """Return the help message of a specific subcommand.""" + message = f"\n*{sc.capitalize()}*\n" + message += self.subparser.choices[sc].format_help() + return message + + if subcommand is None or subcommand not in self.subparser.choices: + res = f"\n*{self.command_name} commands:*```" + for argument in self.subparser.choices: + res += get_subcommand_help(argument) + return res + "```" + else: + res = "\n```" + res += get_subcommand_help(subcommand) + return res + "```" + + def handle(self, + command: str, + user_id: str) -> ResponseTuple: + """Handle command by splitting into substrings and giving to parser.""" + logging.debug("Handling ExportCommand") + command_arg = shlex.split(command) + args = None + + try: + args = self.parser.parse_args(command_arg) + except SystemExit: + all_subcommands = list(self.subparser.choices.keys()) + present_subcommands = [subcommand for subcommand in + all_subcommands + if subcommand in command_arg] + present_subcommand = None + if len(present_subcommands) == 1: + present_subcommand = present_subcommands[0] + return self.get_help(subcommand=present_subcommand), 200 + + if args.which == "emails": + try: + command_user = self.facade.retrieve(User, user_id) + if not check_permissions(command_user, None): + return self.permission_error, 200 + + # Check if team name is provided + if args.team is not None: + users = self.get_team_users(args.team) + return self.export_emails_helper(users) + else: # if team name is not provided, export all emails + users = self.facade.query(User) + return self.export_emails_helper(users) + except LookupError: + return self.lookup_error, 200 + else: + return self.get_help(), 200 + + def get_team_users(self, team_name): + team = get_team_by_name(self.facade, team_name) + return get_team_members(self.facade, team) + + def export_emails_helper(self, + users: list) -> ResponseTuple: + """ + returns emails of all users + + names of the users who do not have an email + """ + + if len(users) == 0: + return self.no_user_msg, 200 + + emails = [] + ids_missing_emails = [] + + for i in range(len(users)): + if users[i].email == '': + ids_missing_emails.append(users[i].slack_id) + continue + + emails.append(users[i].email) + + emails_str = ",".join(emails) + + if len(ids_missing_emails) != 0: + ret = "```" + emails_str + "```\n\n" \ + + "\n\nMembers who don't have an " \ + "email: {}".format( + ",".join(map(lambda u: f"<@{u}>", ids_missing_emails))) + if len(ret) >= self.MAX_CHAR_LIMIT: + ret = self.handle_char_limit_exceeded(ret, "\n\nMembers who") + else: + ret = "```" + emails_str + "```" \ + + "\n\n" + self.no_emails_missing_msg + if len(ret) >= self.MAX_CHAR_LIMIT: + ret = self.handle_char_limit_exceeded( + ret, self.no_emails_missing_msg) + + return ret, 200 + + def handle_char_limit_exceeded(self, ret_str, find_str): + """ + Find the last occurrence (index) of ``find_str`` + and chop off items before that index + + Assume that items are separated by commas. + + :return: a string of emails with char limit exceed + warning message that is under ``MAX_CHAR_LIMIT`` + """ + last_find_idx = ret_str.rfind(find_str) + temp_str1 = ret_str[:last_find_idx] + temp_str2 = ret_str[last_find_idx:] + max_end_idx = \ + self.MAX_CHAR_LIMIT \ + - len(temp_str2) - len(self.char_limit_exceed_msg) + temp_str3 = temp_str1[:max_end_idx] + last_comma_idx = temp_str3.rfind(',') + temp_str3 = temp_str3[:last_comma_idx] + return \ + temp_str3 + "```\n\n" + temp_str2 \ + + "\n\n" + self.char_limit_exceed_msg diff --git a/app/controller/command/parser.py b/app/controller/command/parser.py index 6dd64854..128fea13 100644 --- a/app/controller/command/parser.py +++ b/app/controller/command/parser.py @@ -1,7 +1,8 @@ """Handle Rocket 2 commands.""" from app.controller import ResponseTuple -from app.controller.command.commands import UserCommand, TeamCommand, \ - TokenCommand, ProjectCommand, KarmaCommand, MentionCommand, IQuitCommand +from app.controller.command.commands import UserCommand, TeamCommand,\ + ExportCommand, TokenCommand, ProjectCommand, KarmaCommand,\ + MentionCommand, IQuitCommand from app.controller.command.commands.base import Command from app.controller.command.commands.token import TokenCommandConfig from db.facade import DBFacade @@ -44,6 +45,7 @@ def __init__(self, self.__github, self.__bot, gcp=self.__gcp) + self.commands["export"] = ExportCommand(self.__facade) self.commands["token"] = TokenCommand(self.__facade, token_config) self.commands["project"] = ProjectCommand(self.__facade) self.commands["karma"] = KarmaCommand(self.__facade) diff --git a/tests/app/controller/command/commands/export_test.py b/tests/app/controller/command/commands/export_test.py new file mode 100644 index 00000000..2b5f019f --- /dev/null +++ b/tests/app/controller/command/commands/export_test.py @@ -0,0 +1,97 @@ +from app.controller.command.commands import ExportCommand +from unittest import TestCase +from app.model import User, Team, Permissions +from tests.memorydb import MemoryDB +from tests.util import create_test_admin + + +class TestExportCommand(TestCase): + def setUp(self): + self.u0 = User('U0G9QF9C6') + self.u0.email = 'immabaddy@gmail.com' + self.u0.github_id = '305834954' + + self.u1 = User('Utheomadude') + self.u1.email = 'theounderstars@yahoo.com' + self.u1.github_id = '349850564' + + self.admin = create_test_admin('Uadmin') + + self.lead = User('Ualley') + self.lead.email = 'alead@ubclaunchpad.com' + self.lead.github_id = '2384858' + self.lead.permissions_level = Permissions.team_lead + + self.t0 = Team('305849', 'butter-batter', 'Butter Batters') + self.t0.add_member(self.u0.github_id) + self.t0.add_member(self.lead.github_id) + self.t0.add_team_lead(self.lead.github_id) + + self.t1 = Team('320484', 'aqua-scepter', 'Aqua Scepter') + self.t1.add_member(self.u1.github_id) + + self.db = MemoryDB(users=[self.u0, self.u1, self.admin, self.lead], + teams=[self.t0, self.t1]) + + self.cmd = ExportCommand(self.db) + + def test_get_all_emails(self): + resp, _ = self.cmd.handle('export emails', self.admin.slack_id) + self.assertIn(self.u0.email, resp) + self.assertIn(self.u1.email, resp) + self.assertIn(self.admin.email, resp) + self.assertIn(self.lead.email, resp) + self.assertIn(ExportCommand.no_emails_missing_msg, resp) + + def test_lead_get_all_emails(self): + resp, _ = self.cmd.handle('export emails', self.lead.slack_id) + self.assertIn(self.u0.email, resp) + self.assertIn(self.u1.email, resp) + self.assertIn(self.admin.email, resp) + self.assertIn(self.lead.email, resp) + self.assertIn(ExportCommand.no_emails_missing_msg, resp) + + def test_member_get_all_emails(self): + resp, _ = self.cmd.handle('export emails', self.u0.slack_id) + self.assertIn(ExportCommand.permission_error, resp) + + def test_get_team_emails(self): + resp, _ = self.cmd.handle( + f'export emails --team {self.t0.github_team_name}', + self.admin.slack_id) + self.assertIn(self.u0.email, resp) + self.assertIn(self.lead.email, resp) + self.assertIn(ExportCommand.no_emails_missing_msg, resp) + + def test_lead_get_team_emails(self): + resp, _ = self.cmd.handle( + f'export emails --team {self.t0.github_team_name}', + self.lead.slack_id) + self.assertIn(self.u0.email, resp) + self.assertIn(self.lead.email, resp) + self.assertIn(ExportCommand.no_emails_missing_msg, resp) + + def test_member_get_team_emails(self): + resp, _ = self.cmd.handle( + f'export emails --team {self.t0.github_team_name}', + self.u1.slack_id) + self.assertIn(ExportCommand.permission_error, resp) + + def test_lead_get_team_emails_one_missing(self): + self.u0.email = '' + resp, _ = self.cmd.handle( + f'export emails --team {self.t0.github_team_name}', + self.lead.slack_id) + self.assertIn(self.u0.slack_id, resp) + self.assertIn(self.lead.email, resp) + self.assertIn('Members who don\'t have an email:', resp) + self.assertNotIn(ExportCommand.no_emails_missing_msg, resp) + + def test_get_all_emails_char_limit_reached(self): + old_lim = ExportCommand.MAX_CHAR_LIMIT + ExportCommand.MAX_CHAR_LIMIT = 30 + resp, _ = self.cmd.handle('export emails', self.admin.slack_id) + self.assertIn(ExportCommand.char_limit_exceed_msg, resp) + + # reset things because python doesn't do that + ExportCommand.MAX_CHAR_LIMIT = old_lim diff --git a/tests/util.py b/tests/util.py index 795e2c68..db88feb9 100644 --- a/tests/util.py +++ b/tests/util.py @@ -15,6 +15,7 @@ def create_test_admin(slack_id: str) -> User: Email admin@ubc.ca Name Iemann Atmin Github kibbles + Github ID 123453 Image URL https://via.placeholder.com/150 Major Computer Science Permission Admin