diff --git a/conftest.py b/conftest.py index 67d8d56f..c4355565 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,7 @@ from dotenv import dotenv_values -from src.utils import PREFIX +from src.environment import PREFIX # for testing Gradescope package currently under development # in production, we will use the gradescope_api from https://cs161-staff/gradescope-api diff --git a/src/email.py b/src/email.py index 2f68b8d5..c2c6acc1 100644 --- a/src/email.py +++ b/src/email.py @@ -12,17 +12,10 @@ from sicp.common.rpc.mail import send_email from src.assignments import AssignmentList +from src.environment import Environment from src.errors import EmailError, KnownError from src.record import StudentRecord -from src.utils import Environment, cast_list_str - -ENV_EMAIL_FROM = "EMAIL_FROM" -ENV_EMAIL_REPLY_TO = "EMAIL_REPLY_TO" -ENV_EMAIL_SUBJECT = "EMAIL_SUBJECT" -ENV_EMAIL_SIGNATURE = "EMAIL_SIGNATURE" -ENV_EMAIL_CC = "EMAIL_CC" -ENV_APP_MASTER_SECRET = "APP_MASTER_SECRET" - +from src.utils import cast_list_str class Email: """ @@ -86,24 +79,25 @@ def fmt_date(dt: datetime): body += "\n\n" body += "Best," body += "\n\n" - body += Environment.get(ENV_EMAIL_SIGNATURE) + body += Environment.get_email_signature() body += "\n\n" body += ( "Disclaimer: This is an auto-generated email. We may follow up with you in" + " this thread, and feel free to reply to this thread if you'd like to follow up with us!" ) - cc_emails = cast_list_str(Environment.safe_get(ENV_EMAIL_CC, "")) + cc_emails = cast_list_str(Environment.get_email_cc()) return cls( to_email=student.get_email(), - from_email=Environment.get(ENV_EMAIL_FROM), + from_email=Environment.get_email_from(), cc_emails=cc_emails, - reply_to_email=Environment.get(ENV_EMAIL_REPLY_TO), - subject=Environment.get(ENV_EMAIL_SUBJECT), + reply_to_email=Environment.get_reply_to_email(), + subject=Environment.get_email_subject(), body=body, ) + """ def OLDsend(self) -> None: # TODO: When 162 adds HTML support, bring back HTML emails. # html_body = Markdown().convert(self.body) @@ -130,35 +124,32 @@ def OLDsend(self) -> None: except Exception as e: raise EmailError("An error occurred while sending an email:", e) - + """ def send(self) -> None: PORT = 465 # For starttls - HOST = "REDACTED" - username = "REDACTED" - sender_email = "REDACTED" - SENDERNAME = Environment.get(ENV_EMAIL_FROM) + + SMTP_HOST = os.environ['SMTP_HOST'] + smtp_username = os.environ['SMTP_USERNAME'] + smtp_password = os.environ['SMTP_PASSWORD'] + sender_email = os.environ['SENDER_EMAIL'] + + SENDERNAME = Environment.get_email_from() receiver_email = self.to_email cc_emails = self.cc_emails reply_to_email = self.reply_to_email - password = "REDACTED" SUBJECT = self.subject - # message = """\ - # Subject: Hi there - - # This message is sent from Python.""" - - + msg = MIMEMultipart('alternative') msg['Subject'] = SUBJECT - #msg['From'] = formataddr((SENDERNAME, sender_email)) msg['From'] = SENDERNAME msg['To'] = receiver_email email_id = make_msgid() msg['Message-Id'] = email_id msg['References'] = email_id msg['In-Reply-To'] = email_id - msg.add_header('reply-to', reply_to_email) + msg['Reply-To'] = reply_to_email + if len(cc_emails) > 0: msg['CC'] = ",".join(cc_emails) # Comment or delete the next line if you are not using a configuration set @@ -172,8 +163,8 @@ def send(self) -> None: # the HTML message, is best and preferred. msg.attach(part1) - with SMTP_SSL(HOST, PORT) as server: - server.login(username, password) + with SMTP_SSL(SMTP_HOST, PORT) as server: + server.login(smtp_username, smtp_password) server.sendmail(sender_email, [receiver_email]+cc_emails, msg.as_string()) server.close() print("Email sent!") \ No newline at end of file diff --git a/src/environment.py b/src/environment.py new file mode 100644 index 00000000..d1c10390 --- /dev/null +++ b/src/environment.py @@ -0,0 +1,137 @@ +import os +from typing import Optional + +from dotenv import dotenv_values +from src.sheets import Sheet + +PREFIX = "flextensions_" + +DEFAULT_COURSE_NAME = "PLEASE SET A COURSE NAME" +DEFAULT_REPLY_TO_EMAIL = "PLEASE SET REPLY-TO EMAIL" + +DEFAULT_AUTO_APPROVE_THRESHOLD = 1 +DEFAULT_AUTO_APPROVE_THRESHOLD_DSP = 1 +DEFAULT_APPROVE_ASSIGNMENT_THRESHOLD = 1 +DEFAULT_MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD = 3 + +DEFAULT_EMAIL_FROM = "[{}] <{}@berkeley.edu>".format(DEFAULT_COURSE_NAME, DEFAULT_COURSE_NAME) +DEFAULT_EMAIL_SUBJECT = "[CS 000] Extension Request Update" +DEFAULT_EMAIL_SIGNATURE = "{} Staff".format(DEFAULT_COURSE_NAME) + +DEFAULT_EXTEND_GRADESCOPE_ASSIGNMENTS = "No" + +class Environment: + @staticmethod + def clear(): + keys = os.environ.keys() + for key in keys: + if key.startswith(PREFIX): + del os.environ[key] + + @staticmethod + def contains(key: str) -> bool: + return os.getenv(PREFIX + key) is not None and str(os.getenv(PREFIX + key)).strip() != "" + + @staticmethod + def _safe_get(key: str, default: str = None) -> Optional[str]: + if os.getenv(PREFIX + key): + data = str(os.getenv(PREFIX + key)).strip() + if data: + return data + return default + + # @staticmethod + # def get(key: str) -> Any: + # if not os.getenv(PREFIX + key): + # raise ConfigurationError("Environment variable not set: " + key) + # return os.getenv(PREFIX + key) + + @staticmethod + def get_auto_approve_threshold() -> int: + return int(Environment._safe_get("AUTO_APPROVE_THRESHOLD", DEFAULT_AUTO_APPROVE_THRESHOLD)) + + @staticmethod + def get_auto_approve_threshold_dsp() -> int: + return int(Environment._safe_get("AUTO_APPROVE_THRESHOLD_DSP", DEFAULT_AUTO_APPROVE_THRESHOLD_DSP)) + + @staticmethod + def get_max_total_requested_extensions_threshold() -> int: + return int(Environment._safe_get("MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD", DEFAULT_MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD)) + + @staticmethod + def get_auto_approve_assignment_threshold() -> int: + return int(Environment._safe_get("AUTO_APPROVE_ASSIGNMENT_THRESHOLD", DEFAULT_APPROVE_ASSIGNMENT_THRESHOLD)) + + @staticmethod + def get_course_name() -> str: + return Environment._safe_get("COURSE_NAME", DEFAULT_COURSE_NAME) + + @staticmethod + def get_reply_to_email() -> str: + return Environment._safe_get("REPLY_TO_EMAIL", DEFAULT_REPLY_TO_EMAIL) + + @staticmethod + def get_email_from() -> str: + return Environment._safe_get("EMAIL_FROM", DEFAULT_EMAIL_FROM) + + @staticmethod + def get_email_subject() -> str: + return Environment._safe_get("EMAIL_SUBJECT", DEFAULT_EMAIL_SUBJECT) + + @staticmethod + def get_email_signature() -> str: + return Environment._safe_get("EMAIL_SIGNATURE", DEFAULT_EMAIL_SIGNATURE) + + @staticmethod + def get_email_cc() -> Optional[str]: + return Environment._safe_get("EMAIL_CC") + + @staticmethod + def get_slack_endpoint() -> Optional[str]: + return Environment._safe_get("SLACK_ENDPOINT") + + @staticmethod + def get_slack_debug_endpoint() -> Optional[str]: + return Environment._safe_get("SLACK_DEBUG_ENDPOINT") + + @staticmethod + def get_slack_tag_list() -> Optional[str]: + return Environment._safe_get("SLACK_TAG_LIST") + + @staticmethod + def get_extend_gradescope_assignments() -> bool: + return Environment._safe_get("EXTEND_GRADESCOPE_ASSIGNMENTS", DEFAULT_EXTEND_GRADESCOPE_ASSIGNMENTS) + + @staticmethod + def get_gradescope_email() -> Optional[str]: + return Environment._safe_get("GRADESCOPE_EMAIL") + + @staticmethod + def get_gradescope_password() -> Optional[str]: + return Environment._safe_get("GRADESCOPE_PASSWORD") + + @staticmethod + def get_spreadsheet_url() -> Optional[str]: + return Environment._safe_get("SPREADSHEET_URL") + + @staticmethod + def configure_env_vars(sheet: Sheet): + """ + Reads environment variables from the "Environment Variables" sheet, and stores them into this process's + environment variables for downstream use. Expects two columns: a "key" column, and a "value" + """ + records = sheet.get_all_records() + for record in records: + key = record.get("key") + value = record.get("value") + if not key: + continue + os.environ[PREFIX + key] = str(value) + + # Load local environment variables now from .env, which override remote provided variables for debugging + if os.path.exists(".env-pytest"): + for key, value in dotenv_values(".env-pytest").items(): + if key == "APP_MASTER_SECRET": + os.environ[key] = value + else: + os.environ[PREFIX + key] = value \ No newline at end of file diff --git a/src/gradescope.py b/src/gradescope.py index 30604a68..76a3ebd9 100644 --- a/src/gradescope.py +++ b/src/gradescope.py @@ -2,8 +2,9 @@ from gradescope_api.client import GradescopeClient +from src.environment import Environment from src.errors import GradescopeError -from src.utils import Environment, cast_bool, truncate +from src.utils import cast_bool, truncate class Gradescope: @@ -23,12 +24,15 @@ def __init__(self) -> None: @staticmethod def is_enabled(): - return cast_bool(Environment.safe_get("EXTEND_GRADESCOPE_ASSIGNMENTS", "No")) + return cast_bool(Environment.get_extend_gradescope_assignments()) - def apply_extension(self, assignment_urls: List[str], email: str, num_days: int) -> List[str]: + def apply_extension(self, assignment_name: str, assignment_urls: List[str], email: str, num_days: int) -> List[str]: warnings = [] + course_name = Environment.safe_get('COURSE_NAME', '') + for assignment_url in assignment_urls: - prefix = f"[{email}] [{assignment_url}] [{num_days}] " + prefix = '[{}] [{}{}] [{}] [{}] '.format( + email, course_name + ' ', assignment_name, assignment_url, num_days) print("Extending: " + prefix) try: course = self.client.get_course(course_url=assignment_url) diff --git a/src/handle_email_queue.py b/src/handle_email_queue.py index e0641eb2..09fb75d8 100644 --- a/src/handle_email_queue.py +++ b/src/handle_email_queue.py @@ -2,13 +2,12 @@ from src.assignments import AssignmentList from src.email import Email +from src.environment import Environment from src.errors import ConfigurationError from src.gradescope import Gradescope from src.record import EMAIL_STATUS_IN_QUEUE, StudentRecord from src.sheets import SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES, SHEET_STUDENT_RECORDS, BaseSpreadsheet from src.slack import SlackManager -from src.utils import Environment - def handle_email_queue(request_json): if "spreadsheet_url" not in request_json: diff --git a/src/handle_flush_gradescope.py b/src/handle_flush_gradescope.py index 11e96128..6baeb8dd 100644 --- a/src/handle_flush_gradescope.py +++ b/src/handle_flush_gradescope.py @@ -1,10 +1,10 @@ from src.assignments import AssignmentList +from src.environment import Environment from src.errors import ConfigurationError from src.gradescope import Gradescope from src.record import StudentRecord from src.sheets import SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES, SHEET_STUDENT_RECORDS, BaseSpreadsheet from src.slack import SlackManager -from src.utils import Environment def handle_flush_gradescope(request_json): diff --git a/src/policy.py b/src/policy.py index 48caffb0..fb9c917f 100644 --- a/src/policy.py +++ b/src/policy.py @@ -2,13 +2,12 @@ from src.assignments import AssignmentList from src.email import Email +from src.environment import Environment from src.gradescope import Gradescope from src.record import StudentRecord from src.sheets import Sheet from src.slack import SlackManager from src.submission import FormSubmission -from src.utils import Environment - class Policy: def __init__( diff --git a/src/record.py b/src/record.py index 8bdc241c..b2fc2de9 100644 --- a/src/record.py +++ b/src/record.py @@ -7,10 +7,13 @@ from pytz import timezone from src.assignments import AssignmentList +from src.environment import Environment from src.errors import StudentRecordError from src.gradescope import Gradescope from src.sheets import Sheet -from src.utils import cast_bool +from src.utils import cast_bool + +import json APPROVAL_STATUS_REQUESTED_MEETING = "Requested Meeting" APPROVAL_STATUS_PENDING = "Pending" @@ -137,6 +140,8 @@ def flush(self): if self.table_index == -1: values = [self.write_queue.get(header) for header in headers] + # minus 1 to account for header row + self.table_index = self.sheet.num_entries - 1 self.sheet.append_row(values=values, value_input_option="USER_ENTERED") # Update local table_record object for email. @@ -155,30 +160,37 @@ def flush(self): self.sheet.update_cells(cells=cells) def apply_extensions(self, assignments: AssignmentList, gradescope: Gradescope) -> List[str]: + warnings = [] for assignment in assignments: num_days = self.get_request(assignment_id=assignment.get_id()) + course_name = Environment.safe_get("COURSE_NAME", "") + if num_days: + if len(assignment.get_gradescope_assignment_urls()) == 0: print( - f"[{assignment.get_name()}] could not extend assignment deadline for {self.get_email()} (assignment URL's not set)." - ) + "[{}{}] could not extend assignment deadline for {} (assignment URL's not set).".format( + course_name + " ", assignment.get_name(), self.get_email())) continue elif not assignment.get_due_date(): warnings.append( - f"[{assignment.get_name()}] could not extend assignment deadline for {self.get_email()} (deadline not set)." - ) + "[{} {}] could not extend assignment deadline for {} (deadline not set).".format( + course_name + " ", assignment.get_name(), self.get_email())) continue else: - print("Extending assignments: " + str(assignment.get_gradescope_assignment_urls())) + print("Extending assignments: [{}{}] {}".format( + course_name + " ", assignment.get_name(), str(assignment.get_gradescope_assignment_urls()))) warnings = gradescope.apply_extension( + assignment_name=assignment.get_name(), assignment_urls=assignment.get_gradescope_assignment_urls(), email=self.get_email(), num_days=num_days, ) warnings.extend(warnings) + return warnings @staticmethod diff --git a/src/sheets.py b/src/sheets.py index 92fb5b18..e93c8883 100644 --- a/src/sheets.py +++ b/src/sheets.py @@ -22,6 +22,7 @@ def __init__(self, sheet: Worksheet) -> None: self.all_values = self.sheet.get_all_values() self.all_records = self.sheet.get_all_records() self.headers = self.all_values[0] + self.num_entries = len(self.all_values) def get_headers(self) -> List[str]: return self.headers @@ -65,12 +66,12 @@ class BaseSpreadsheet: A pointer to the master spreadsheet. """ - def __init__(self, spreadsheet_url: str) -> None: - if not os.path.exists("service-account.json"): - raise SheetError("Could not find Google Service Account at service-account.json.") + def __init__(self, spreadsheet_url: str, credentials_path: str = "/var/secrets/service-account-json") -> None: + if not os.path.exists(credentials_path): + raise SheetError("Could not find Google Service Account at service-account-json.") self.spreadsheet_url = spreadsheet_url - self.spreadsheet = gspread.service_account("service-account.json").open_by_url(spreadsheet_url) + self.spreadsheet = gspread.service_account(credentials_path).open_by_url(spreadsheet_url) def get_sheet(self, sheet_name: str) -> Sheet: return Sheet(sheet=self.spreadsheet.worksheet(sheet_name)) diff --git a/src/slack.py b/src/slack.py index 0c6d9bd2..734f08d5 100644 --- a/src/slack.py +++ b/src/slack.py @@ -4,10 +4,11 @@ from tabulate import tabulate from src.assignments import AssignmentList +from src.environment import Environment from src.errors import SlackError from src.record import StudentRecord from src.submission import FormSubmission -from src.utils import Environment, cast_list_str +from src.utils import cast_list_str class SlackManager: @@ -17,13 +18,20 @@ class SlackManager: def __init__(self) -> None: self.webhooks: List[WebhookClient] = [] - self.webhooks.append(WebhookClient(Environment.get("SLACK_ENDPOINT"))) self.warnings = [] self.silent = False - if Environment.contains("SLACK_ENDPOINT_DEBUG"): - if Environment.get("SLACK_ENDPOINT_DEBUG") != Environment.get("SLACK_ENDPOINT"): - self.webhooks.append(WebhookClient(Environment.get("SLACK_ENDPOINT_DEBUG"))) + webhook = Environment.get_slack_endpoint() + debug_webhook = Environment.get_slack_debug_endpoint() + + if webhook: + self.webhooks.append(WebhookClient(webhook)) + if debug_webhook and debug_webhook != webhook: + self.webhooks.append(WebhookClient(debug_webhook)) + + # after Slack configuration if there is no webhook provided then suppress any Slack action + if not self.webhooks: + self.silent = True def suppress(self): self.silent = True @@ -40,6 +48,7 @@ def _get_submission_details_knows_assignments(self): text += "> *DSP Accomodations for Extensions*: " + self.submission.dsp_status() + "\n" if self.submission.has_partner(): text += "> *Partner Email(s)*: " + ", ".join(self.submission.get_partner_emails()) + "\n" + text += "> *Documentation*: " + self.submission.get_documentation() + "\n" return text def _get_submission_details_unknown_assignments(self): @@ -76,7 +85,7 @@ def send_message(self, message: str) -> None: @staticmethod def get_tags() -> str: - slack_tags = Environment.safe_get("SLACK_TAG_LIST") + slack_tags = Environment.get_slack_tag_list() prefix = "" if slack_tags: uids = cast_list_str(slack_tags) @@ -129,7 +138,7 @@ def send_student_update(self, message: str, autoapprove: bool = False) -> None: { "type": "button", "text": {"type": "plain_text", "text": "View Spreadsheet"}, - "url": Environment.get("SPREADSHEET_URL"), + "url": Environment.get_spreadsheet_url(), }, ], }, diff --git a/src/submission.py b/src/submission.py index d4119f05..98cd7085 100644 --- a/src/submission.py +++ b/src/submission.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Tuple from src.assignments import Assignment, AssignmentList +from src.environment import Environment from src.errors import ConfigurationError, FormInputError from src.sheets import Sheet from src.utils import cast_list_int, cast_list_str @@ -32,6 +33,7 @@ def __init__(self, form_payload: Dict[str, Any], question_sheet: Sheet, assignme self.responses[key] = str(form_payload[question][0]) self.responses["Timestamp"] = form_payload["Timestamp"][0] + self.responses["Course Name"] = Environment.get_course_name() print(self.responses) def get_timestamp(self) -> str: @@ -103,6 +105,9 @@ def get_reason(self) -> str: assert self.knows_assignments() return self.responses["reason"] + def get_documentation(self) -> str: + return self.responses["documentation"] + def has_partner(self) -> bool: # Encoding default behavior: if the form doesn't contain a has_partner field, then we assume # the student is working alone (e.g. the class has all solo assignments). diff --git a/src/utils.py b/src/utils.py index c1ca3d1f..38ebbc28 100644 --- a/src/utils.py +++ b/src/utils.py @@ -51,77 +51,6 @@ def cast_list_int(cell: str) -> List[int]: return items -PREFIX = "cs161extensions_" - - -class Environment: - @staticmethod - def clear(): - keys = os.environ.keys() - for key in keys: - if key.startswith(PREFIX): - del os.environ[key] - - @staticmethod - def contains(key: str) -> bool: - return os.getenv(PREFIX + key) is not None and str(os.getenv(PREFIX + key)).strip() != "" - - @staticmethod - def safe_get(key: str, default: str = None) -> Optional[str]: - if os.getenv(PREFIX + key): - data = str(os.getenv(PREFIX + key)).strip() - if data: - return data - return default - - @staticmethod - def get(key: str) -> Any: - if not os.getenv(PREFIX + key): - raise ConfigurationError("Environment variable not set: " + key) - return os.getenv(PREFIX + key) - - @staticmethod - def get_auto_approve_threshold() -> int: - return int(Environment.get("AUTO_APPROVE_THRESHOLD")) - - @staticmethod - def get_auto_approve_threshold_dsp() -> int: - return int(Environment.get("AUTO_APPROVE_THRESHOLD_DSP")) - - @staticmethod - def get_max_total_requested_extensions_threshold() -> int: - # If this number is -1, then assume this flag is disabled. - # If this number is 0, then reject all extensions. - # If this number is > 0, then reject extensions if the total number of extensions requested exceeds this number. - return int(Environment.safe_get("MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD", default=-1)) - - @staticmethod - def get_auto_approve_assignment_threshold() -> int: - return int(Environment.get("AUTO_APPROVE_ASSIGNMENT_THRESHOLD")) - - @staticmethod - def configure_env_vars(sheet: Sheet): - """ - Reads environment variables from the "Environment Variables" sheet, and stores them into this process's - environment variables for downstream use. Expects two columns: a "key" column, and a "value" - """ - records = sheet.get_all_records() - for record in records: - key = record.get("key") - value = record.get("value") - if not key: - continue - os.environ[PREFIX + key] = str(value) - - # Load local environment variables now from .env, which override remote provided variables for debugging - if os.path.exists(".env-pytest"): - for key, value in dotenv_values(".env-pytest").items(): - if key == "APP_MASTER_SECRET": - os.environ[key] = value - else: - os.environ[PREFIX + key] = value - - def truncate(s, amount=300): s = str(s) if len(s) > amount: diff --git a/tests/test_integration.py b/tests/test_integration.py index 4e08ba76..c2d80c38 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -17,27 +17,36 @@ from tests.MockSheet import MockSheet -DEV_URL = "https://docs.google.com/spreadsheets/d/1BabID1n6fPgeuuO4-1r3mkoQ9Nx5dquNwdsET75In1E/edit#gid=1214799044" +# TODO: Paste Spreadsheet URL +DEV_URL = "SPREADSHEET URL HERE" class TestIntegration: @classmethod def setup_class(cls): - ss = gspread.service_account("service-account.json").open_by_url(DEV_URL) + + credentials_path = '../service-account.json' + ss = gspread.service_account(credentials_path).open_by_url(DEV_URL) roster = ss.worksheet("Roster") headers = roster.get_all_values()[0] roster.clear() roster.update_cells([gspread.Cell(row=1, col=i + 1, value=header) for i, header in enumerate(headers)]) - base = BaseSpreadsheet(spreadsheet_url=DEV_URL) - cls.sheet_assignments = MockSheet.from_live(base.get_sheet(SHEET_ASSIGNMENTS).sheet) - cls.sheet_form_questions = MockSheet.from_live(base.get_sheet(SHEET_FORM_QUESTIONS).sheet) - cls.sheet_env_vars = MockSheet.from_live(base.get_sheet(SHEET_ENVIRONMENT_VARIABLES).sheet) - cls.sheet_records = MockSheet.from_live(base.get_sheet(SHEET_STUDENT_RECORDS).sheet) + base = BaseSpreadsheet(spreadsheet_url=DEV_URL, credentials_path=credentials_path) + cls.sheet_assignments = base.get_sheet(SHEET_ASSIGNMENTS) + cls.sheet_form_questions = base.get_sheet(SHEET_FORM_QUESTIONS) + cls.sheet_env_vars = base.get_sheet(SHEET_ENVIRONMENT_VARIABLES) + cls.sheet_records = base.get_sheet(SHEET_STUDENT_RECORDS) + + Environment.configure_env_vars(cls.sheet_env_vars) + cls.auto_approve_threshold = Environment.get_auto_approve_threshold() + cls.auto_approve_threshold_dsp = Environment.get_auto_approve_threshold_dsp() + cls.auto_approve_assignment_threshold = Environment.get_auto_approve_assignment_threshold() + cls.max_total_requested_extensions_threshold =Environment.get_max_total_requested_extensions_threshold() @classmethod def teardown_class(cls): - cls.sheet_records.flush() + pass def get_request( self, @@ -46,6 +55,7 @@ def get_request( assignments: str, is_dsp: str = "No", reason: str = "Test reason.", + documentation: str = "Test documentation.", has_partner: str = "No", partner_email: str = "", ): @@ -57,6 +67,7 @@ def get_request( "assignments": assignments, "days": days, "reason": reason, + "documentation": documentation, "has_partner": has_partner, "partner_email": partner_email, "partner_sid": "123456", @@ -257,20 +268,25 @@ def test_retroactive_single_student_multiple_assignments(self): # [C] MANUAL APPROVALS: Request # days > allowed # days. ######################################################################################################### def test_flag_request_too_many_days(self): + auto_approve_threshold_exceeded = self.auto_approve_threshold + 1 policy = self.get_policy( mock_request=self.get_request( - email="C1@berkeley.edu", assignments="Homework 1", days="10", reason="test_flag_request_too_many_days" + email="C1@berkeley.edu", + assignments="Homework 1", + days=str(auto_approve_threshold_exceeded), + reason="test_flag_request_too_many_days" ), timestamp="2022-01-27T20:46:42.125Z", ) assert not policy.apply(silent=True) def test_flag_request_too_many_days_with_partner(self): + auto_approve_threshold_exceeded = self.auto_approve_threshold + 1 policy = self.get_policy( mock_request=self.get_request( email="C1@berkeley.edu", assignments="Project 1 Checkpoint", - days="10", + days=str(auto_approve_threshold_exceeded), has_partner="Yes", partner_email="C1.5@berkeley.edu", reason="test_flag_request_too_many_days_with_partner", @@ -280,11 +296,12 @@ def test_flag_request_too_many_days_with_partner(self): assert not policy.apply(silent=True) def test_flag_request_too_many_days_dsp(self): + auto_approve_threshold_dsp_exceeded = self.auto_approve_threshold_dsp + 1 policy = self.get_policy( mock_request=self.get_request( email="C2@berkeley.edu", assignments="Homework 1", - days="10", + days=str(auto_approve_threshold_dsp_exceeded), is_dsp="Yes", reason="test_flag_request_too_many_days_dsp", ), @@ -293,36 +310,46 @@ def test_flag_request_too_many_days_dsp(self): assert not policy.apply(silent=True) def test_flag_request_too_many_days_multiple_assignments(self): + auto_approve_threshold_exceeded = self.auto_approve_threshold + 1 policy = self.get_policy( mock_request=self.get_request( email="C3@berkeley.edu", assignments="Homework 1, Homework 2", - days="10, 2", + days=f"{auto_approve_threshold_exceeded}, {auto_approve_threshold_exceeded}", reason="test_flag_request_too_many_days_multiple_assignments", ), timestamp="2022-01-27T20:46:42.125Z", ) assert not policy.apply(silent=True) - def test_flag_too_many_submissions_in_one_request(self): + def test_flag_too_many_assignments_in_one_request(self): + auto_approve_assignment_threshold_exceeded = self.auto_approve_assignment_threshold + 1 + assignment_list = [f'Homework {i + 1}' for i in range(auto_approve_assignment_threshold_exceeded)] + assignment_list_str = ", ".join(assignment_list) + policy = self.get_policy( mock_request=self.get_request( email="C4@berkeley.edu", - assignments="Homework 1, Homework 2, Homework 3, Homework 4, Homework 5, Homework 6", - days="10", - reason="test_flag_too_many_submissions_in_one_request", + assignments=assignment_list_str, + days="2", + reason="test_flag_too_many_assignments_in_one_request", ), timestamp="2022-01-27T20:46:42.125Z", ) assert not policy.apply(silent=True) - def test_flag_too_many_total_submissions_non_dsp(self): + def test_flag_too_many_total_extensions_non_dsp(self): # Note: based on environment variables, the threshold for # of assignments allowed is 6. - for i in range(1, 7): + + # BUG: this test case is failing because it simulates multiple form submissions + # at different times using for-loop. This prevents Google Sheet from propagating + # data correctly thus one email has multiple rows in the Roster sheet + + for i in range(self.max_total_requested_extensions_threshold): policy = self.get_policy( mock_request=self.get_request( email="C4.5@berkeley.edu", - assignments=f"Homework {i}", + assignments=f"Homework {i + 1}", days="2", reason="test_flag_too_many_submissions", ), @@ -330,11 +357,11 @@ def test_flag_too_many_total_submissions_non_dsp(self): ) assert policy.apply(silent=True) - # The 7th request should trigger manual approval. + # The next request should trigger manual approval. policy = self.get_policy( mock_request=self.get_request( email="C4.5@berkeley.edu", - assignments="Homework 7", + assignments=f"Homework {self.max_total_requested_extensions_threshold + 1}", days="2", reason="test_flag_too_many_submissions", ), @@ -343,11 +370,13 @@ def test_flag_too_many_total_submissions_non_dsp(self): assert not policy.apply(silent=True) def test_flag_request_too_many_days_with_multiple_partners(self): + auto_approve_threshold_exceeded = self.auto_approve_threshold + 1 + policy = self.get_policy( mock_request=self.get_request( email="C5@berkeley.edu", assignments="Project 1 Checkpoint", - days="10", + days=str(auto_approve_threshold_exceeded), has_partner="Yes", partner_email="C5.5@berkeley.edu, C5.6@berkeley.edu, C5.7@berkeley.edu", reason="test_flag_request_too_many_days_with_multiple_partners",