Skip to content

Commit 6b32c41

Browse files
authored
V3.0.1 chg patch lms (#85)
* FIX remove git hook container escape * FIX remove git hook container escape * FIX migration issue, theia options for course and superuser course context * CHG improve caching issues * CHG take theia images out of deploy script * ADD app context aware with_context wrapper
1 parent d0c284e commit 6b32c41

24 files changed

+226
-117
lines changed

api/anubis/models/__init__.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@ class User(db.Model):
5656
def data(self):
5757
professor_for = [pf.data for pf in self.professor_for]
5858
ta_for = [taf.data for taf in self.ta_for]
59-
extra_for = []
59+
super_for = None
6060
if self.is_superuser:
61+
super_for = []
6162
courses = Course.query.all()
6263
for course in courses:
63-
extra_for.append({'id': course.id, 'name': course.name})
64+
super_for.append({'id': course.id, 'name': course.name})
6465
return {
6566
"id": self.id,
6667
"netid": self.netid,
@@ -70,7 +71,7 @@ def data(self):
7071
"is_admin": len(professor_for) > 0 or len(ta_for) > 0 or self.is_superuser,
7172
"professor_for": professor_for,
7273
"ta_for": ta_for,
73-
"admin_for": professor_for + ta_for + extra_for,
74+
"admin_for": super_for or (professor_for + ta_for),
7475
}
7576

7677
def __repr__(self):
@@ -93,7 +94,7 @@ class Course(db.Model):
9394
section = db.Column(db.TEXT, nullable=True)
9495
professor = db.Column(db.TEXT, nullable=False)
9596
theia_default_image = db.Column(db.TEXT, nullable=False, default='registry.osiris.services/anubis/xv6')
96-
theia_default_options = db.Column(MutableJson, default=lambda: {"limits": {"cpu": "4", "memory": "4Gi"}})
97+
theia_default_options = db.Column(MutableJson, default=lambda: {"limits": {"cpu": "2", "memory": "500Mi"}})
9798

9899
@property
99100
def total_assignments(self):

api/anubis/rpc/theia.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ def create_theia_pod_obj(theia_session: TheiaSession):
1919
name = get_theia_pod_name(theia_session)
2020
containers = []
2121

22+
# Get the theia session options
23+
limits = theia_session.options.get('limits', {"cpu": "2", "memory": "500Mi"})
24+
requests = theia_session.options.get('requests', {"cpu": "250m", "memory": "100Mi"})
25+
autosave = theia_session.options.get('autosave', True)
26+
credentials = theia_session.options.get('credentials', False)
27+
2228
# PVC
2329
volume_name = name + "-volume"
2430
pvc = client.V1PersistentVolumeClaim(
@@ -66,12 +72,8 @@ def create_theia_pod_obj(theia_session: TheiaSession):
6672
],
6773
)
6874

69-
limits = theia_session.options.get('limits', {"cpu": "2", "memory": "500Mi"})
70-
requests = theia_session.options.get('requests', {"cpu": "250m", "memory": "100Mi"})
71-
autosave = theia_session.options.get('autosave', True)
72-
7375
extra_env = []
74-
if theia_session.options.get('credentials', False):
76+
if credentials:
7577
extra_env.append(client.V1EnvVar(
7678
name='INCLUSTER',
7779
value=base64.b64encode(create_token(theia_session.owner.netid).encode()).decode(),

api/anubis/utils/data.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from smtplib import SMTP
88
from typing import Union, Tuple
99

10-
from flask import Response
10+
from flask import Response, has_app_context, has_request_context
1111

1212
from anubis.config import config
1313

@@ -302,11 +302,15 @@ def with_context(function):
302302

303303
@functools.wraps(function)
304304
def wrapper(*args, **kwargs):
305-
306305
# Do the import here to avoid circular
307306
# import issues.
308307
from anubis.app import create_app
309308

309+
# Only create an app context if
310+
# there is not already one
311+
if has_app_context() or has_request_context():
312+
return function(*args, **kwargs)
313+
310314
# Create a fresh app
311315
app = create_app()
312316

api/anubis/utils/lms/assignments.py

+13-55
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Union, List, Dict, Tuple
44

55
from dateutil.parser import parse as date_parse, ParserError
6-
from sqlalchemy import or_, and_
6+
from sqlalchemy import or_
77

88
from anubis.models import (
99
db,
@@ -25,7 +25,7 @@
2525
from anubis.utils.services.logger import logger
2626

2727

28-
@cache.memoize(timeout=5, unless=is_debug)
28+
@cache.memoize(timeout=60, unless=is_debug)
2929
def get_courses(netid: str):
3030
"""
3131
Get all classes a given netid is in
@@ -122,48 +122,6 @@ def get_assignments(netid: str, course_id=None) -> Union[List[Dict[str, str]], N
122122
return response
123123

124124

125-
@cache.memoize(timeout=3, unless=is_debug)
126-
def get_submissions(
127-
user_id=None, course_id=None, assignment_id=None
128-
) -> Union[List[Dict[str, str]], None]:
129-
"""
130-
Get all submissions for a given netid. Cache the results. Optionally specify
131-
a class_name and / or assignment_name for additional filtering.
132-
133-
:param user_id:
134-
:param course_id:
135-
:param assignment_id: id of assignment
136-
:return:
137-
"""
138-
139-
# Load user
140-
owner = User.query.filter(User.id == user_id).first()
141-
142-
# Verify user exists
143-
if owner is None:
144-
return None
145-
146-
# Build filters
147-
filters = []
148-
if course_id is not None and course_id != "":
149-
filters.append(Course.id == course_id)
150-
if user_id is not None and user_id != "":
151-
filters.append(User.id == user_id)
152-
if assignment_id is not None:
153-
filters.append(Assignment.id == assignment_id)
154-
155-
submissions = (
156-
Submission.query.join(Assignment)
157-
.join(Course)
158-
.join(InCourse)
159-
.join(User)
160-
.filter(Submission.owner_id == owner.id, *filters)
161-
.all()
162-
)
163-
164-
return [s.full_data for s in submissions]
165-
166-
167125
def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
168126
"""
169127
Take an assignment_data dictionary from a assignment meta.yaml
@@ -181,24 +139,24 @@ def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
181139

182140
# Attempt to find the class
183141
course_name = assignment_data.get('class', None) or assignment_data.get('course', None)
184-
c: Course = Course.query.filter(
142+
course: Course = Course.query.filter(
185143
or_(
186144
Course.name == course_name,
187145
Course.course_code == course_name,
188146
)
189147
).first()
190-
if c is None:
148+
if course is None:
191149
return "Unable to find class", False
192150

193-
assert_course_admin(c.id)
151+
assert_course_admin(course.id)
194152

195153
# Check if it exists
196154
if assignment is None:
197155
assignment = Assignment(
198-
theia_image=c.theia_default_image,
199-
theia_options=c.theia_default_options,
156+
theia_image=course.theia_default_image,
157+
theia_options=course.theia_default_options,
200158
unique_code=assignment_data["unique_code"],
201-
course=c,
159+
course=course,
202160
)
203161

204162
# Update fields
@@ -220,10 +178,8 @@ def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
220178
# Go through assignment tests, and delete those that are now
221179
# not in the assignment data.
222180
for assignment_test in AssignmentTest.query.filter(
223-
and_(
224-
AssignmentTest.assignment_id == assignment.id,
225-
AssignmentTest.name.notin_(assignment_data["tests"]),
226-
)
181+
AssignmentTest.assignment_id == assignment.id,
182+
AssignmentTest.name.notin_(assignment_data["tests"]),
227183
).all():
228184
# Delete any and all submission test results that are still outstanding
229185
# for an assignment test that will be deleted.
@@ -257,7 +213,7 @@ def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
257213

258214
# Sync the questions in the assignment data
259215
question_message = None
260-
if 'questions' in assignment_data:
216+
if 'questions' in assignment_data and isinstance(assignment_data['questions'], list):
261217
accepted, ignored, rejected = ingest_questions(
262218
assignment_data["questions"], assignment
263219
)
@@ -266,3 +222,5 @@ def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
266222
db.session.commit()
267223

268224
return {"assignment": assignment.data, "questions": question_message}, True
225+
226+

api/anubis/utils/lms/repos.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import List
2+
3+
from anubis.models import AssignmentRepo, Assignment
4+
from anubis.utils.services.cache import cache
5+
6+
7+
@cache.memoize(timeout=3600)
8+
def get_repos(user_id: str):
9+
repos: List[AssignmentRepo] = (
10+
AssignmentRepo.query.join(Assignment)
11+
.filter(AssignmentRepo.owner_id == user_id)
12+
.distinct(AssignmentRepo.repo_url)
13+
.order_by(Assignment.release_date.desc())
14+
.all()
15+
)
16+
17+
return [repo.data for repo in repos]

api/anubis/utils/lms/submissions.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from datetime import datetime
2-
from typing import List, Union
2+
from typing import List, Union, Dict
33

4-
from anubis.models import Submission, AssignmentRepo, User, db
4+
from anubis.models import Submission, AssignmentRepo, User, db, Course, Assignment, InCourse
5+
from anubis.utils.data import is_debug
56
from anubis.utils.http.https import error_response, success_response
7+
from anubis.utils.services.cache import cache
68
from anubis.utils.services.rpc import enqueue_autograde_pipeline
79

810

@@ -146,3 +148,45 @@ def fix_dangling():
146148
enqueue_autograde_pipeline(s.id)
147149

148150
return fixed
151+
152+
153+
@cache.memoize(timeout=3600, unless=is_debug)
154+
def get_submissions(
155+
user_id=None, course_id=None, assignment_id=None
156+
) -> Union[List[Dict[str, str]], None]:
157+
"""
158+
Get all submissions for a given netid. Cache the results. Optionally specify
159+
a class_name and / or assignment_name for additional filtering.
160+
161+
:param user_id:
162+
:param course_id:
163+
:param assignment_id: id of assignment
164+
:return:
165+
"""
166+
167+
# Load user
168+
owner = User.query.filter(User.id == user_id).first()
169+
170+
# Verify user exists
171+
if owner is None:
172+
return None
173+
174+
# Build filters
175+
filters = []
176+
if course_id is not None and course_id != "":
177+
filters.append(Course.id == course_id)
178+
if user_id is not None and user_id != "":
179+
filters.append(User.id == user_id)
180+
if assignment_id is not None:
181+
filters.append(Assignment.id == assignment_id)
182+
183+
submissions = (
184+
Submission.query.join(Assignment)
185+
.join(Course)
186+
.join(InCourse)
187+
.join(User)
188+
.filter(Submission.owner_id == owner.id, *filters)
189+
.all()
190+
)
191+
192+
return [s.full_data for s in submissions]

api/anubis/utils/lms/webhook.py

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from anubis.models import db, User, AssignmentRepo
2+
from anubis.utils.services.cache import cache
3+
from anubis.utils.lms.repos import get_repos
24

35

46
def parse_webhook(webhook):
@@ -99,5 +101,8 @@ def check_repo(assignment, repo_url, github_username, user=None) -> AssignmentRe
99101
db.session.add(repo)
100102
db.session.commit()
101103

104+
if user is not None:
105+
cache.delete_memoized(get_repos, user.id)
106+
102107
# Return the repo object
103108
return repo

api/anubis/utils/visuals/usage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def get_theia_sessions() -> pd.DataFrame:
8181
return theia_sessions
8282

8383

84-
@cache.memoize(timeout=10)
84+
@cache.memoize(timeout=360)
8585
def get_raw_submissions() -> List[Dict[str, Any]]:
8686
submissions_df = get_submissions()
8787
data = submissions_df.groupby(['assignment_id', 'created'])['id'].count() \

api/anubis/views/admin/autograde.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
from anubis.utils.lms.course import assert_course_admin, assert_course_context, get_course_context
99
from anubis.utils.lms.questions import get_assigned_questions
1010
from anubis.utils.services.elastic import log_endpoint
11+
from anubis.utils.services.cache import cache
1112

1213
autograde_ = Blueprint("admin-autograde", __name__, url_prefix="/admin/autograde")
1314

1415

1516
@autograde_.route("/assignment/<assignment_id>")
1617
@require_admin()
18+
@cache.memoize(timeout=60)
1719
@json_response
18-
def admin_autograde_assignment_assignment_id(assignment_id, netid=None):
20+
def admin_autograde_assignment_assignment_id(assignment_id):
1921
"""
2022
Calculate result statistics for an assignment. This endpoint is
2123
potentially very IO and computationally expensive. We basically
@@ -29,7 +31,6 @@ def admin_autograde_assignment_assignment_id(assignment_id, netid=None):
2931
timeout. *
3032
3133
:param assignment_id:
32-
:param netid:
3334
:return:
3435
"""
3536

@@ -58,6 +59,7 @@ def admin_autograde_assignment_assignment_id(assignment_id, netid=None):
5859

5960
@autograde_.route("/for/<assignment_id>/<user_id>")
6061
@require_admin()
62+
@cache.memoize(timeout=60)
6163
@json_response
6264
def admin_autograde_for_assignment_id_user_id(assignment_id, user_id):
6365
"""
@@ -68,9 +70,6 @@ def admin_autograde_for_assignment_id_user_id(assignment_id, user_id):
6870
:return:
6971
"""
7072

71-
# Get the course context
72-
course = get_course_context()
73-
7473
# Pull the assignment object
7574
assignment = Assignment.query.filter(
7675
Assignment.id == assignment_id
@@ -101,6 +100,7 @@ def admin_autograde_for_assignment_id_user_id(assignment_id, user_id):
101100
@autograde_.route("/submission/<string:assignment_id>/<string:netid>")
102101
@require_admin()
103102
@log_endpoint("cli", lambda: "submission-stats")
103+
@cache.memoize(timeout=60)
104104
@json_response
105105
def private_submission_stats_id(assignment_id: str, netid: str):
106106
"""

api/anubis/views/admin/regrade.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from anubis.utils.http.decorators import json_response
1212
from anubis.utils.http.decorators import load_from_id
1313
from anubis.utils.http.https import error_response, success_response, get_number_arg
14-
from anubis.utils.lms.course import assert_course_admin, get_course_context, assert_course_context
14+
from anubis.utils.lms.course import assert_course_context
1515
from anubis.utils.services.elastic import log_endpoint
1616
from anubis.utils.services.rpc import enqueue_autograde_pipeline, rpc_enqueue
1717

@@ -62,7 +62,7 @@ def admin_regrade_status(assignment: Assignment):
6262
@require_admin()
6363
@log_endpoint("cli", lambda: "regrade-commit")
6464
@json_response
65-
def admin_regrade_submission_commit(commit):
65+
def admin_regrade_submission_commit(commit: str):
6666
"""
6767
Regrade a specific submission via the unique commit hash.
6868

0 commit comments

Comments
 (0)