Skip to content

Commit 226dace

Browse files
committed
Support Pensieve
1 parent 43e68e1 commit 226dace

7 files changed

Lines changed: 223 additions & 8 deletions

File tree

main.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import src.handle_email_queue as handle_email
44
import src.handle_flush_gradescope as handle_flush
5+
import src.handle_flush_pensieve as handle_flush_pensieve_module
56
import src.handle_form_submit as handle_form
7+
from src.environment import Environment
68
from src.errors import KnownError
79
from src.slack import SlackManager
8-
from src.utils import Environment, truncate
10+
from src.utils import truncate
911

1012

1113
def handle_email_queue(request):
@@ -44,6 +46,24 @@ def handle_flush_gradescope(request):
4446
Environment.clear()
4547

4648

49+
def handle_flush_pensieve(request):
50+
request_json = request.get_json()
51+
print("handle_flush_pensieve called on payload: " + json.dumps(request_json))
52+
try:
53+
handle_flush_pensieve_module.handle_flush_pensieve(request_json=request_json)
54+
return {"success": True}
55+
except KnownError as e:
56+
print("Known Error Occurred: " + str(e) + f" (Request: {request_json})")
57+
SlackManager().send_error(str(e) + f" (Request: {truncate(request_json)})")
58+
return {"success": False, "error": str(e)}
59+
except Exception as e:
60+
print("Internal Error Occurred: " + str(e) + f" (Request: {request_json})")
61+
SlackManager().send_error("Internal error: " + str(e) + f" (Request: {truncate(request_json)})")
62+
raise
63+
finally:
64+
Environment.clear()
65+
66+
4767
def handle_form_submit(request):
4868
request_json = request.get_json()
4969
print("handle_form_submit called on payload: " + json.dumps(request_json))

src/assignments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ def __init__(
2424
hard_due_date: Optional[datetime],
2525
partner: bool,
2626
gradescope: List[str],
27+
pensieve: List[str],
2728
) -> None:
2829
self.name = name
2930
self.id = id
3031
self.due_date = due_date
3132
self.hard_due_date = hard_due_date
3233
self.partner = partner
3334
self.gradescope = gradescope
35+
self.pensieve = pensieve
3436

3537
def is_past_due(self, request_time: str):
3638
"""
@@ -67,6 +69,9 @@ def is_partner_assignment(self):
6769
def get_gradescope_assignment_urls(self) -> List[str]:
6870
return self.gradescope
6971

72+
def get_pensieve_assignment_urls(self) -> List[str]:
73+
return self.pensieve
74+
7075

7176
class AssignmentList:
7277
"""
@@ -86,6 +91,7 @@ def __init__(self, sheet: Sheet) -> None:
8691
hard_due_date=cast_date(row.get("hard_due_date", ""), optional=True),
8792
partner=cast_bool(row["partner"]),
8893
gradescope=cast_list_str(row.get("gradescope", "")),
94+
pensieve=cast_list_str(row.get("pensieve", "")),
8995
)
9096
)
9197

src/environment.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Optional
33

44
from dotenv import dotenv_values
5+
56
from src.sheets import Sheet
67

78
PREFIX = "flextensions_"
@@ -19,6 +20,7 @@
1920
DEFAULT_EMAIL_SIGNATURE = "{} Staff".format(DEFAULT_COURSE_NAME)
2021

2122
DEFAULT_EXTEND_GRADESCOPE_ASSIGNMENTS = "No"
23+
DEFAULT_EXTEND_PENSIEVE_ASSIGNMENTS = "No"
2224

2325
class Environment:
2426
@staticmethod
@@ -110,6 +112,18 @@ def get_gradescope_email() -> Optional[str]:
110112
def get_gradescope_password() -> Optional[str]:
111113
return Environment._safe_get("GRADESCOPE_PASSWORD")
112114

115+
@staticmethod
116+
def get_extend_pensieve_assignments() -> Optional[str]:
117+
return Environment._safe_get("EXTEND_PENSIEVE_ASSIGNMENTS", DEFAULT_EXTEND_PENSIEVE_ASSIGNMENTS)
118+
119+
@staticmethod
120+
def get_pensieve_email() -> Optional[str]:
121+
return Environment._safe_get("PENSIEVE_EMAIL")
122+
123+
@staticmethod
124+
def get_pensieve_api_token() -> Optional[str]:
125+
return Environment._safe_get("PENSIEVE_API_TOKEN")
126+
113127
@staticmethod
114128
def get_spreadsheet_url() -> Optional[str]:
115129
return Environment._safe_get("SPREADSHEET_URL")

src/handle_email_queue.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
from src.environment import Environment
66
from src.errors import ConfigurationError
77
from src.gradescope import Gradescope
8+
from src.pensieve import Pensieve
89
from src.record import EMAIL_STATUS_IN_QUEUE, StudentRecord
9-
from src.sheets import SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES, SHEET_STUDENT_RECORDS, BaseSpreadsheet
10+
from src.sheets import (SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES,
11+
SHEET_STUDENT_RECORDS, BaseSpreadsheet)
1012
from src.slack import SlackManager
1113

14+
1215
def handle_email_queue(request_json):
1316
if "spreadsheet_url" not in request_json:
1417
raise ConfigurationError("handle_email_queue expects spreadsheet_url parameter")
@@ -55,6 +58,12 @@ def handle_email_queue(request_json):
5558
for warning in warnings:
5659
slack.add_warning(warning)
5760

61+
if Pensieve.is_enabled():
62+
pensieve_client = Pensieve()
63+
warnings = student.apply_extensions_pensieve(assignments=assignments, pensieve=pensieve_client)
64+
for warning in warnings:
65+
slack.add_warning(warning)
66+
5867
if len(emails) == 0:
5968
slack.send_message("Sent zero emails from the queue...was it empty?")
6069
else:

src/handle_flush_pensieve.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from src.assignments import AssignmentList
2+
from src.environment import Environment
3+
from src.errors import ConfigurationError
4+
from src.pensieve import Pensieve
5+
from src.record import StudentRecord
6+
from src.sheets import (SHEET_ASSIGNMENTS, SHEET_ENVIRONMENT_VARIABLES,
7+
SHEET_STUDENT_RECORDS, BaseSpreadsheet)
8+
from src.slack import SlackManager
9+
10+
11+
def handle_flush_pensieve(request_json):
12+
if "spreadsheet_url" not in request_json:
13+
raise ConfigurationError("handle_flush_pensieve expects spreadsheet_url parameter")
14+
15+
# Get pointers to sheets.
16+
base = BaseSpreadsheet(spreadsheet_url=request_json["spreadsheet_url"])
17+
sheet_assignments = base.get_sheet(SHEET_ASSIGNMENTS)
18+
sheet_records = base.get_sheet(SHEET_STUDENT_RECORDS)
19+
sheet_env_vars = base.get_sheet(SHEET_ENVIRONMENT_VARIABLES)
20+
21+
# Set up environment variables.
22+
Environment.configure_env_vars(sheet_env_vars)
23+
24+
# Fetch assignments.
25+
assignments = AssignmentList(sheet=sheet_assignments)
26+
27+
# Fetch records.
28+
records = sheet_records.get_all_records()
29+
30+
slack = SlackManager()
31+
32+
pensieve = Pensieve()
33+
34+
all_warnings = []
35+
successes = []
36+
failures = []
37+
for i, table_record in enumerate(records):
38+
student = StudentRecord(table_index=i, table_record=table_record, sheet=sheet_records)
39+
if student.should_flush_pensieve():
40+
w = student.apply_extensions_pensieve(assignments=assignments, pensieve=pensieve)
41+
if len(w) > 0:
42+
failures.append(student.get_email())
43+
all_warnings.extend(w)
44+
else:
45+
successes.append(student.get_email())
46+
student.set_flush_pensieve_status_success()
47+
48+
student.flush()
49+
50+
for warning in all_warnings:
51+
slack.add_warning(warning)
52+
53+
summary = "Flush Pensieve Summary:" + "\n"
54+
if len(successes) > 0:
55+
summary += "\n" + "*Successes:* " + ", ".join(successes)
56+
if len(failures) > 0:
57+
summary += "\n" + "*Failures:* " + ", ".join(failures)
58+
if len(successes) + len(failures) == 0:
59+
summary += (
60+
"\n"
61+
+ "No student records processed. To process a student record, create a `flush_pensieve` column on the Roster sheet, and set the value to TRUE for each record you would like to flush to Pensieve."
62+
)
63+
64+
slack.send_message(summary)

src/pensieve.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import List
2+
3+
import requests
4+
5+
from src.environment import Environment
6+
from src.errors import KnownError
7+
from src.utils import truncate
8+
9+
10+
class Pensieve:
11+
"""
12+
Pensieve client to apply extensions for a student on an assignment.
13+
"""
14+
15+
def __init__(self) -> None:
16+
email = Environment.get_pensieve_email()
17+
token = Environment.get_pensieve_api_token()
18+
19+
if not email:
20+
raise KnownError("PENSIEVE_EMAIL must be set to use Pensieve.")
21+
if not token:
22+
raise KnownError("PENSIEVE_API_TOKEN must be set to use Pensieve.")
23+
24+
self.base_url = "https://api.pensieve.co"
25+
self.email = email
26+
self.token = token
27+
28+
@staticmethod
29+
def is_enabled():
30+
return Environment.get_extend_pensieve_assignments() in ["Yes", "TRUE"]
31+
32+
def apply_extension(self, assignment_name: str, assignment_urls: List[str], email: str, num_days: int) -> List[str]:
33+
warnings = []
34+
course_name = Environment.get_course_name()
35+
36+
for assignment_url in assignment_urls:
37+
prefix = '[{}] [{}{}] [{}] [{}] '.format(
38+
email, course_name + ' ', assignment_name, assignment_url, num_days)
39+
try:
40+
resp = requests.post(
41+
f"{self.base_url}/api/b2s/v1/external-client/grant-extension",
42+
headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"},
43+
json={
44+
"assignment_url": assignment_url,
45+
"student_email": email,
46+
"num_days": num_days,
47+
},
48+
timeout=30,
49+
)
50+
if resp.status_code != 200:
51+
raise Exception(f"HTTP {resp.status_code}: {truncate(resp.text)}")
52+
data = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
53+
if not data or not data.get("success", False):
54+
raise Exception(truncate(data))
55+
except Exception as err:
56+
warnings.append(
57+
prefix
58+
+ f"failed to extend assignment in Pensieve: internal Pensieve error occurred ({truncate(err)})"
59+
)
60+
return warnings

src/record.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@
1010
from src.environment import Environment
1111
from src.errors import StudentRecordError
1212
from src.gradescope import Gradescope
13+
from src.pensieve import Pensieve
1314
from src.sheets import Sheet
14-
from src.utils import cast_bool
15-
16-
import json
15+
from src.utils import cast_bool
1716

1817
APPROVAL_STATUS_REQUESTED_MEETING = "Requested Meeting"
1918
APPROVAL_STATUS_PENDING = "Pending"
@@ -101,7 +100,16 @@ def set_flush_gradescope_status_success(self):
101100
if "flush_gradescope" in self.sheet.get_headers():
102101
self.queue_write_back(col_key="flush_gradescope", col_value=False)
103102

104-
def count_requests(self, assignments=AssignmentList):
103+
def should_flush_pensieve(self):
104+
if "flush_pensieve" in self.sheet.get_headers():
105+
return cast_bool(self.table_record["flush_pensieve"])
106+
return False
107+
108+
def set_flush_pensieve_status_success(self):
109+
if "flush_pensieve" in self.sheet.get_headers():
110+
self.queue_write_back(col_key="flush_pensieve", col_value=False)
111+
112+
def count_requests(self, assignments: AssignmentList) -> int:
105113
count = 0
106114
for assignment_id in assignments.get_all_ids():
107115
if self.get_request(assignment_id=assignment_id) is not None:
@@ -141,7 +149,7 @@ def flush(self):
141149
if self.table_index == -1:
142150
values = [self.write_queue.get(header) for header in headers]
143151
# minus 1 to account for header row
144-
self.table_index = self.sheet.num_entries - 1
152+
self.table_index = self.sheet.num_entries - 1
145153
self.sheet.append_row(values=values, value_input_option="USER_ENTERED")
146154

147155
# Update local table_record object for email.
@@ -164,7 +172,7 @@ def apply_extensions(self, assignments: AssignmentList, gradescope: Gradescope)
164172
warnings = []
165173
for assignment in assignments:
166174
num_days = self.get_request(assignment_id=assignment.get_id())
167-
course_name = Environment.safe_get("COURSE_NAME", "")
175+
course_name = Environment.get_course_name()
168176

169177
if num_days:
170178

@@ -193,6 +201,40 @@ def apply_extensions(self, assignments: AssignmentList, gradescope: Gradescope)
193201

194202
return warnings
195203

204+
def apply_extensions_pensieve(self, assignments: AssignmentList, pensieve: Pensieve) -> List[str]:
205+
206+
warnings = []
207+
for assignment in assignments:
208+
num_days = self.get_request(assignment_id=assignment.get_id())
209+
course_name = Environment.get_course_name()
210+
211+
if num_days:
212+
213+
if len(assignment.get_pensieve_assignment_urls()) == 0:
214+
print(
215+
"[{}{}] could not extend assignment deadline for {} (assignment URL's not set).".format(
216+
course_name + " ", assignment.get_name(), self.get_email()))
217+
continue
218+
219+
elif not assignment.get_due_date():
220+
warnings.append(
221+
"[{} {}] could not extend assignment deadline for {} (deadline not set).".format(
222+
course_name + " ", assignment.get_name(), self.get_email()))
223+
continue
224+
225+
else:
226+
print("Extending assignments (Pensieve): [{}{}] {}".format(
227+
course_name + " ", assignment.get_name(), str(assignment.get_pensieve_assignment_urls())))
228+
pensieve_warnings = pensieve.apply_extension(
229+
assignment_name=assignment.get_name(),
230+
assignment_urls=assignment.get_pensieve_assignment_urls(),
231+
email=self.get_email(),
232+
num_days=num_days,
233+
)
234+
warnings.extend(pensieve_warnings)
235+
236+
return warnings
237+
196238
@staticmethod
197239
def from_email(email: str, sheet_records: Sheet) -> StudentRecord:
198240
email = email.lower()

0 commit comments

Comments
 (0)