Skip to content

Commit 43e68e1

Browse files
authored
Merge pull request #7 from berkeley-cdss/development
Merging latest development commits
2 parents 15dfef2 + a2a4357 commit 43e68e1

13 files changed

Lines changed: 265 additions & 150 deletions

conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dotenv import dotenv_values
44

5-
from src.utils import PREFIX
5+
from src.environment import PREFIX
66

77
# for testing Gradescope package currently under development
88
# in production, we will use the gradescope_api from https://cs161-staff/gradescope-api

src/email.py

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,10 @@
1212
from sicp.common.rpc.mail import send_email
1313

1414
from src.assignments import AssignmentList
15+
from src.environment import Environment
1516
from src.errors import EmailError, KnownError
1617
from src.record import StudentRecord
17-
from src.utils import Environment, cast_list_str
18-
19-
ENV_EMAIL_FROM = "EMAIL_FROM"
20-
ENV_EMAIL_REPLY_TO = "EMAIL_REPLY_TO"
21-
ENV_EMAIL_SUBJECT = "EMAIL_SUBJECT"
22-
ENV_EMAIL_SIGNATURE = "EMAIL_SIGNATURE"
23-
ENV_EMAIL_CC = "EMAIL_CC"
24-
ENV_APP_MASTER_SECRET = "APP_MASTER_SECRET"
25-
18+
from src.utils import cast_list_str
2619

2720
class Email:
2821
"""
@@ -86,24 +79,25 @@ def fmt_date(dt: datetime):
8679
body += "\n\n"
8780
body += "Best,"
8881
body += "\n\n"
89-
body += Environment.get(ENV_EMAIL_SIGNATURE)
82+
body += Environment.get_email_signature()
9083
body += "\n\n"
9184
body += (
9285
"Disclaimer: This is an auto-generated email. We may follow up with you in"
9386
+ " this thread, and feel free to reply to this thread if you'd like to follow up with us!"
9487
)
9588

96-
cc_emails = cast_list_str(Environment.safe_get(ENV_EMAIL_CC, ""))
89+
cc_emails = cast_list_str(Environment.get_email_cc())
9790

9891
return cls(
9992
to_email=student.get_email(),
100-
from_email=Environment.get(ENV_EMAIL_FROM),
93+
from_email=Environment.get_email_from(),
10194
cc_emails=cc_emails,
102-
reply_to_email=Environment.get(ENV_EMAIL_REPLY_TO),
103-
subject=Environment.get(ENV_EMAIL_SUBJECT),
95+
reply_to_email=Environment.get_reply_to_email(),
96+
subject=Environment.get_email_subject(),
10497
body=body,
10598
)
10699

100+
"""
107101
def OLDsend(self) -> None:
108102
# TODO: When 162 adds HTML support, bring back HTML emails.
109103
# html_body = Markdown().convert(self.body)
@@ -130,35 +124,32 @@ def OLDsend(self) -> None:
130124
131125
except Exception as e:
132126
raise EmailError("An error occurred while sending an email:", e)
133-
127+
"""
134128

135129
def send(self) -> None:
136130
PORT = 465 # For starttls
137-
HOST = "REDACTED"
138-
username = "REDACTED"
139-
sender_email = "REDACTED"
140-
SENDERNAME = Environment.get(ENV_EMAIL_FROM)
131+
132+
SMTP_HOST = os.environ['SMTP_HOST']
133+
smtp_username = os.environ['SMTP_USERNAME']
134+
smtp_password = os.environ['SMTP_PASSWORD']
135+
sender_email = os.environ['SENDER_EMAIL']
136+
137+
SENDERNAME = Environment.get_email_from()
141138
receiver_email = self.to_email
142139
cc_emails = self.cc_emails
143140
reply_to_email = self.reply_to_email
144-
password = "REDACTED"
145141
SUBJECT = self.subject
146-
# message = """\
147-
# Subject: Hi there
148-
149-
# This message is sent from Python."""
150-
151-
142+
152143
msg = MIMEMultipart('alternative')
153144
msg['Subject'] = SUBJECT
154-
#msg['From'] = formataddr((SENDERNAME, sender_email))
155145
msg['From'] = SENDERNAME
156146
msg['To'] = receiver_email
157147
email_id = make_msgid()
158148
msg['Message-Id'] = email_id
159149
msg['References'] = email_id
160150
msg['In-Reply-To'] = email_id
161-
msg.add_header('reply-to', reply_to_email)
151+
msg['Reply-To'] = reply_to_email
152+
162153
if len(cc_emails) > 0:
163154
msg['CC'] = ",".join(cc_emails)
164155
# Comment or delete the next line if you are not using a configuration set
@@ -172,8 +163,8 @@ def send(self) -> None:
172163
# the HTML message, is best and preferred.
173164
msg.attach(part1)
174165

175-
with SMTP_SSL(HOST, PORT) as server:
176-
server.login(username, password)
166+
with SMTP_SSL(SMTP_HOST, PORT) as server:
167+
server.login(smtp_username, smtp_password)
177168
server.sendmail(sender_email, [receiver_email]+cc_emails, msg.as_string())
178169
server.close()
179170
print("Email sent!")

src/environment.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import os
2+
from typing import Optional
3+
4+
from dotenv import dotenv_values
5+
from src.sheets import Sheet
6+
7+
PREFIX = "flextensions_"
8+
9+
DEFAULT_COURSE_NAME = "PLEASE SET A COURSE NAME"
10+
DEFAULT_REPLY_TO_EMAIL = "PLEASE SET REPLY-TO EMAIL"
11+
12+
DEFAULT_AUTO_APPROVE_THRESHOLD = 1
13+
DEFAULT_AUTO_APPROVE_THRESHOLD_DSP = 1
14+
DEFAULT_APPROVE_ASSIGNMENT_THRESHOLD = 1
15+
DEFAULT_MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD = 3
16+
17+
DEFAULT_EMAIL_FROM = "[{}] <{}@berkeley.edu>".format(DEFAULT_COURSE_NAME, DEFAULT_COURSE_NAME)
18+
DEFAULT_EMAIL_SUBJECT = "[CS 000] Extension Request Update"
19+
DEFAULT_EMAIL_SIGNATURE = "{} Staff".format(DEFAULT_COURSE_NAME)
20+
21+
DEFAULT_EXTEND_GRADESCOPE_ASSIGNMENTS = "No"
22+
23+
class Environment:
24+
@staticmethod
25+
def clear():
26+
keys = os.environ.keys()
27+
for key in keys:
28+
if key.startswith(PREFIX):
29+
del os.environ[key]
30+
31+
@staticmethod
32+
def contains(key: str) -> bool:
33+
return os.getenv(PREFIX + key) is not None and str(os.getenv(PREFIX + key)).strip() != ""
34+
35+
@staticmethod
36+
def _safe_get(key: str, default: str = None) -> Optional[str]:
37+
if os.getenv(PREFIX + key):
38+
data = str(os.getenv(PREFIX + key)).strip()
39+
if data:
40+
return data
41+
return default
42+
43+
# @staticmethod
44+
# def get(key: str) -> Any:
45+
# if not os.getenv(PREFIX + key):
46+
# raise ConfigurationError("Environment variable not set: " + key)
47+
# return os.getenv(PREFIX + key)
48+
49+
@staticmethod
50+
def get_auto_approve_threshold() -> int:
51+
return int(Environment._safe_get("AUTO_APPROVE_THRESHOLD", DEFAULT_AUTO_APPROVE_THRESHOLD))
52+
53+
@staticmethod
54+
def get_auto_approve_threshold_dsp() -> int:
55+
return int(Environment._safe_get("AUTO_APPROVE_THRESHOLD_DSP", DEFAULT_AUTO_APPROVE_THRESHOLD_DSP))
56+
57+
@staticmethod
58+
def get_max_total_requested_extensions_threshold() -> int:
59+
return int(Environment._safe_get("MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD", DEFAULT_MAX_TOTAL_REQUESTED_EXTENSIONS_THRESHOLD))
60+
61+
@staticmethod
62+
def get_auto_approve_assignment_threshold() -> int:
63+
return int(Environment._safe_get("AUTO_APPROVE_ASSIGNMENT_THRESHOLD", DEFAULT_APPROVE_ASSIGNMENT_THRESHOLD))
64+
65+
@staticmethod
66+
def get_course_name() -> str:
67+
return Environment._safe_get("COURSE_NAME", DEFAULT_COURSE_NAME)
68+
69+
@staticmethod
70+
def get_reply_to_email() -> str:
71+
return Environment._safe_get("REPLY_TO_EMAIL", DEFAULT_REPLY_TO_EMAIL)
72+
73+
@staticmethod
74+
def get_email_from() -> str:
75+
return Environment._safe_get("EMAIL_FROM", DEFAULT_EMAIL_FROM)
76+
77+
@staticmethod
78+
def get_email_subject() -> str:
79+
return Environment._safe_get("EMAIL_SUBJECT", DEFAULT_EMAIL_SUBJECT)
80+
81+
@staticmethod
82+
def get_email_signature() -> str:
83+
return Environment._safe_get("EMAIL_SIGNATURE", DEFAULT_EMAIL_SIGNATURE)
84+
85+
@staticmethod
86+
def get_email_cc() -> Optional[str]:
87+
return Environment._safe_get("EMAIL_CC")
88+
89+
@staticmethod
90+
def get_slack_endpoint() -> Optional[str]:
91+
return Environment._safe_get("SLACK_ENDPOINT")
92+
93+
@staticmethod
94+
def get_slack_debug_endpoint() -> Optional[str]:
95+
return Environment._safe_get("SLACK_DEBUG_ENDPOINT")
96+
97+
@staticmethod
98+
def get_slack_tag_list() -> Optional[str]:
99+
return Environment._safe_get("SLACK_TAG_LIST")
100+
101+
@staticmethod
102+
def get_extend_gradescope_assignments() -> bool:
103+
return Environment._safe_get("EXTEND_GRADESCOPE_ASSIGNMENTS", DEFAULT_EXTEND_GRADESCOPE_ASSIGNMENTS)
104+
105+
@staticmethod
106+
def get_gradescope_email() -> Optional[str]:
107+
return Environment._safe_get("GRADESCOPE_EMAIL")
108+
109+
@staticmethod
110+
def get_gradescope_password() -> Optional[str]:
111+
return Environment._safe_get("GRADESCOPE_PASSWORD")
112+
113+
@staticmethod
114+
def get_spreadsheet_url() -> Optional[str]:
115+
return Environment._safe_get("SPREADSHEET_URL")
116+
117+
@staticmethod
118+
def configure_env_vars(sheet: Sheet):
119+
"""
120+
Reads environment variables from the "Environment Variables" sheet, and stores them into this process's
121+
environment variables for downstream use. Expects two columns: a "key" column, and a "value"
122+
"""
123+
records = sheet.get_all_records()
124+
for record in records:
125+
key = record.get("key")
126+
value = record.get("value")
127+
if not key:
128+
continue
129+
os.environ[PREFIX + key] = str(value)
130+
131+
# Load local environment variables now from .env, which override remote provided variables for debugging
132+
if os.path.exists(".env-pytest"):
133+
for key, value in dotenv_values(".env-pytest").items():
134+
if key == "APP_MASTER_SECRET":
135+
os.environ[key] = value
136+
else:
137+
os.environ[PREFIX + key] = value

src/gradescope.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from gradescope_api.client import GradescopeClient
44

5+
from src.environment import Environment
56
from src.errors import GradescopeError
6-
from src.utils import Environment, cast_bool, truncate
7+
from src.utils import cast_bool, truncate
78

89

910
class Gradescope:
@@ -23,12 +24,15 @@ def __init__(self) -> None:
2324

2425
@staticmethod
2526
def is_enabled():
26-
return cast_bool(Environment.safe_get("EXTEND_GRADESCOPE_ASSIGNMENTS", "No"))
27+
return cast_bool(Environment.get_extend_gradescope_assignments())
2728

28-
def apply_extension(self, assignment_urls: List[str], email: str, num_days: int) -> List[str]:
29+
def apply_extension(self, assignment_name: str, assignment_urls: List[str], email: str, num_days: int) -> List[str]:
2930
warnings = []
31+
course_name = Environment.safe_get('COURSE_NAME', '')
32+
3033
for assignment_url in assignment_urls:
31-
prefix = f"[{email}] [{assignment_url}] [{num_days}] "
34+
prefix = '[{}] [{}{}] [{}] [{}] '.format(
35+
email, course_name + ' ', assignment_name, assignment_url, num_days)
3236
print("Extending: " + prefix)
3337
try:
3438
course = self.client.get_course(course_url=assignment_url)

src/handle_email_queue.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
from src.assignments import AssignmentList
44
from src.email import Email
5+
from src.environment import Environment
56
from src.errors import ConfigurationError
67
from src.gradescope import Gradescope
78
from src.record import EMAIL_STATUS_IN_QUEUE, StudentRecord
89
from src.sheets import SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES, SHEET_STUDENT_RECORDS, BaseSpreadsheet
910
from src.slack import SlackManager
10-
from src.utils import Environment
11-
1211

1312
def handle_email_queue(request_json):
1413
if "spreadsheet_url" not in request_json:

src/handle_flush_gradescope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from src.assignments import AssignmentList
2+
from src.environment import Environment
23
from src.errors import ConfigurationError
34
from src.gradescope import Gradescope
45
from src.record import StudentRecord
56
from src.sheets import SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES, SHEET_STUDENT_RECORDS, BaseSpreadsheet
67
from src.slack import SlackManager
7-
from src.utils import Environment
88

99

1010
def handle_flush_gradescope(request_json):

src/policy.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
from src.assignments import AssignmentList
44
from src.email import Email
5+
from src.environment import Environment
56
from src.gradescope import Gradescope
67
from src.record import StudentRecord
78
from src.sheets import Sheet
89
from src.slack import SlackManager
910
from src.submission import FormSubmission
10-
from src.utils import Environment
11-
1211

1312
class Policy:
1413
def __init__(

src/record.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
from pytz import timezone
88

99
from src.assignments import AssignmentList
10+
from src.environment import Environment
1011
from src.errors import StudentRecordError
1112
from src.gradescope import Gradescope
1213
from src.sheets import Sheet
13-
from src.utils import cast_bool
14+
from src.utils import cast_bool
15+
16+
import json
1417

1518
APPROVAL_STATUS_REQUESTED_MEETING = "Requested Meeting"
1619
APPROVAL_STATUS_PENDING = "Pending"
@@ -137,6 +140,8 @@ def flush(self):
137140

138141
if self.table_index == -1:
139142
values = [self.write_queue.get(header) for header in headers]
143+
# minus 1 to account for header row
144+
self.table_index = self.sheet.num_entries - 1
140145
self.sheet.append_row(values=values, value_input_option="USER_ENTERED")
141146

142147
# Update local table_record object for email.
@@ -155,30 +160,37 @@ def flush(self):
155160
self.sheet.update_cells(cells=cells)
156161

157162
def apply_extensions(self, assignments: AssignmentList, gradescope: Gradescope) -> List[str]:
163+
158164
warnings = []
159165
for assignment in assignments:
160166
num_days = self.get_request(assignment_id=assignment.get_id())
167+
course_name = Environment.safe_get("COURSE_NAME", "")
168+
161169
if num_days:
170+
162171
if len(assignment.get_gradescope_assignment_urls()) == 0:
163172
print(
164-
f"[{assignment.get_name()}] could not extend assignment deadline for {self.get_email()} (assignment URL's not set)."
165-
)
173+
"[{}{}] could not extend assignment deadline for {} (assignment URL's not set).".format(
174+
course_name + " ", assignment.get_name(), self.get_email()))
166175
continue
167176

168177
elif not assignment.get_due_date():
169178
warnings.append(
170-
f"[{assignment.get_name()}] could not extend assignment deadline for {self.get_email()} (deadline not set)."
171-
)
179+
"[{} {}] could not extend assignment deadline for {} (deadline not set).".format(
180+
course_name + " ", assignment.get_name(), self.get_email()))
172181
continue
173182

174183
else:
175-
print("Extending assignments: " + str(assignment.get_gradescope_assignment_urls()))
184+
print("Extending assignments: [{}{}] {}".format(
185+
course_name + " ", assignment.get_name(), str(assignment.get_gradescope_assignment_urls())))
176186
warnings = gradescope.apply_extension(
187+
assignment_name=assignment.get_name(),
177188
assignment_urls=assignment.get_gradescope_assignment_urls(),
178189
email=self.get_email(),
179190
num_days=num_days,
180191
)
181192
warnings.extend(warnings)
193+
182194
return warnings
183195

184196
@staticmethod

0 commit comments

Comments
 (0)