Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 21 additions & 30 deletions src/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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!")
137 changes: 137 additions & 0 deletions src/environment.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions src/gradescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/handle_email_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/handle_flush_gradescope.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
3 changes: 1 addition & 2 deletions src/policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand Down
24 changes: 18 additions & 6 deletions src/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading