Skip to content

Commit 54ac8f6

Browse files
committed
CHG update course to use theia image table for default image field
1 parent 20c1971 commit 54ac8f6

File tree

11 files changed

+126
-41
lines changed

11 files changed

+126
-41
lines changed

api/Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ yeetdb:
2424
cleanyeetdb:
2525
make -C .. cleanyeetdb
2626

27+
test: venv
28+
./tests/test.sh
29+
2730
.PHONY: migrations # Run alembic migrations
2831
migrations: venv
2932
./venv/bin/alembic upgrade head

api/anubis/lms/assignments.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def assignment_sync(assignment_data: dict) -> Tuple[Union[dict, str], bool]:
207207
# Check if it exists
208208
if assignment is None:
209209
assignment = Assignment(
210-
theia_image=course.theia_default_image,
210+
theia_image_id=course.theia_default_image_id,
211211
theia_options=course.theia_default_options,
212212
unique_code=assignment_data["unique_code"],
213213
course=course,

api/anubis/models/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class Course(db.Model):
104104
default="https://github.com/os3224/anubis-assignment-tests",
105105
)
106106
github_repo_required = db.Column(db.Boolean, default=True)
107-
theia_default_image = db.Column(db.TEXT, nullable=False, default="registry.digitalocean.com/anubis/theia-xv6")
107+
theia_default_image_id = db.Column(db.String(128), db.ForeignKey('theia_image.id'), nullable=True)
108108
theia_default_options = db.Column(MutableJson, default=lambda: copy.deepcopy(THEIA_DEFAULT_OPTIONS))
109109
github_org = db.Column(db.TEXT, default="os3224")
110110
join_code = db.Column(db.String(256), unique=True)
@@ -213,7 +213,7 @@ class Assignment(db.Model):
213213

214214
# IDE
215215
ide_enabled = db.Column(db.Boolean, default=True)
216-
theia_image_id = db.Column(db.String(128), db.ForeignKey('theia_image.id'))
216+
theia_image_id = db.Column(db.String(128), db.ForeignKey('theia_image.id'), default=None)
217217
theia_options = db.Column(MutableJson, default=lambda: copy.deepcopy(THEIA_DEFAULT_OPTIONS))
218218

219219
# Github
@@ -654,6 +654,7 @@ class TheiaImage(db.Model):
654654
label = db.Column(db.String(1024), nullable=False, default='')
655655
public = db.Column(db.Boolean, nullable=False, default=False)
656656

657+
courses = db.relationship('Course', backref='theia_default_image')
657658
assignments = db.relationship('Assignment', backref='theia_image')
658659
sessions = db.relationship('TheiaSession', backref='image')
659660

api/anubis/rpc/seed.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,9 @@ def seed():
5656
student_user = User(netid="student", github_username="student", name="student")
5757
db.session.add_all([superuser, professor_user, ta_user, student_user])
5858

59-
xv6_image = TheiaImage(
60-
image="registry.digitalocean.com/anubis/theia-xv6"
61-
)
62-
db.session.add(xv6_image)
59+
xv6_image = TheiaImage(image="registry.digitalocean.com/anubis/theia-xv6", label="theia-xv6")
60+
admin_image = TheiaImage(image="registry.digitalocean.com/anubis/theia-admin", label="theia-admin")
61+
db.session.add_all([xv6_image, admin_image])
6362

6463
db.session.commit()
6564

api/anubis/views/admin/assignments.py

+3
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ def admin_assignments_save(assignment: dict):
415415
db_assignment.theia_image_id = None
416416
continue
417417

418+
if key == 'theia_image_id':
419+
continue
420+
418421
setattr(db_assignment, key, value)
419422

420423
# Attempt to commit

api/anubis/views/admin/courses.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ def admin_courses_list():
2323
:return:
2424
"""
2525

26+
course_data = row2dict(course_context)
27+
if course_context.theia_default_image_id is not None:
28+
course_data['theia_default_image'] = course_context.theia_default_image.data
29+
else:
30+
course_data['theia_default_image'] = None
31+
2632
# Return the course context broken down
2733
return success_response(
2834
{
29-
"course": row2dict(course_context),
35+
"course": course_data,
3036
}
3137
)
3238

@@ -101,10 +107,20 @@ def admin_courses_save_id(course: dict):
101107
for column in Course.__table__.columns:
102108
if column.name in course:
103109
key, value = column.name, course[column.name]
110+
104111
if isinstance(value, str):
105112
value = value.strip()
113+
114+
if key == 'theia_default_image_id':
115+
continue
116+
106117
setattr(db_course, key, value)
107118

119+
if course['theia_default_image'] is not None:
120+
db_course.theia_default_image_id = course['theia_default_image']['id']
121+
else:
122+
db_course.theia_default_image_id = None
123+
108124
# Commit the changes
109125
try:
110126
db.session.commit()

api/anubis/views/admin/ide.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,19 @@ def admin_ide_initialize_custom(settings: dict, **_):
9494
message="Starting new IDEs is currently disabled by an Anubis administrator. " "Please try again later.",
9595
)
9696

97+
image_db = TheiaImage.query.filter(
98+
TheiaImage.image == image
99+
).first()
100+
101+
if image_db is None:
102+
return error_response('Theia IDE Image is not yet registered')
103+
97104
# Create a new session
98105
session = TheiaSession(
99106
owner_id=current_user.id,
100107
assignment_id=None,
101108
course_id=course_context.id,
102-
image=image,
109+
image_id=image_db.id,
103110
repo_url=repo_url,
104111
active=True,
105112
state="Initializing",

api/migrations/versions/08c5273b4713_add_theia_image_table.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,23 @@ def upgrade():
7676
op.drop_column("theia_session", "image")
7777
op.create_foreign_key(
7878
None, "theia_session", "theia_image", ["image_id"], ["id"]
79-
)
79+
)
80+
81+
# Handle course table
82+
op.add_column(
83+
"course",
84+
sa.Column("theia_default_image_id", sa.String(length=128), nullable=True),
85+
)
86+
for i in images:
87+
conn.execute(
88+
sa.text('update course set theia_default_image_id = :id where theia_default_image = :image;'),
89+
id=i['id'], image=i['image'],
90+
)
91+
op.drop_column("course", "theia_default_image")
92+
op.create_foreign_key(
93+
None, "course", "theia_image", ["theia_default_image_id"], ["id"]
94+
)
95+
8096
# ### end Alembic commands ###
8197

8298

api/tests/test_config_admin.py api/tests/test_config_super.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55

66
def test_config_admin():
7-
permission_test("/admin/config/list")
7+
permission_test("/super/config/list")
88
permission_test(
9-
"/admin/config/save",
9+
"/super/config/save",
1010
method="post",
1111
json={"config": sample_config},
1212
fail_for=[

old-web/src/Components/Admin/Course/CourseCard.jsx

+67-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import React from 'react';
1+
import React, {useState} from 'react';
2+
import axios from 'axios';
3+
import {useSnackbar} from 'notistack';
24
import {Link} from 'react-router-dom';
35

46
import Grid from '@material-ui/core/Grid';
@@ -11,9 +13,12 @@ import CardContent from '@material-ui/core/CardContent';
1113
import yellow from '@material-ui/core/colors/yellow';
1214
import EditIcon from '@material-ui/icons/Edit';
1315
import Switch from '@material-ui/core/Switch';
16+
import FormControlLabel from '@material-ui/core/FormControlLabel';
17+
import Autocomplete from '@material-ui/lab/Autocomplete';
1418

1519
import AuthContext from '../../../Contexts/AuthContext';
16-
import FormControlLabel from '@material-ui/core/FormControlLabel';
20+
import standardStatusHandler from '../../../Utils/standardStatusHandler';
21+
import standardErrorHandler from '../../../Utils/standardErrorHandler';
1722

1823

1924
const useStyles = makeStyles((theme) => ({
@@ -24,35 +29,72 @@ const useStyles = makeStyles((theme) => ({
2429

2530
export default function CourseCard({course, _disabled, editableFields, updateField, saveCourse}) {
2631
const classes = useStyles();
32+
const {enqueueSnackbar} = useSnackbar();
33+
const [images, setImages] = useState([]);
34+
35+
React.useEffect(() => {
36+
axios.get(`/api/admin/ide/images/list`).then((response) => {
37+
const data = standardStatusHandler(response, enqueueSnackbar);
38+
if (data.images) {
39+
setImages(data.images);
40+
}
41+
}).catch(standardErrorHandler(enqueueSnackbar));
42+
}, []);
2743

2844
return (
2945
<Card>
3046
<CardContent>
3147
<Grid container spacing={2}>
32-
{editableFields.map(({field, label, type = 'text', disabled = false}) => (
33-
<Grid item xs={12} key={field}>
34-
{type === 'text' && <TextField
35-
fullWidth
36-
disabled={disabled || _disabled}
37-
variant={'outlined'}
38-
label={label}
39-
value={course[field]}
40-
onChange={updateField(course.id, field)}
41-
/>}
42-
{type === 'boolean' && <FormControlLabel
43-
value={course[field]}
44-
label={label}
45-
labelPlacement="end"
46-
control={
47-
<Switch
48-
checked={course[field]}
49-
color={'primary'}
50-
onClick={updateField(course.id, field, true)}
48+
{editableFields.map(({field, label, type = 'text', disabled = false}) => {
49+
switch (field) {
50+
case 'theia_default_image':
51+
return (
52+
<Grid item xs={12} key={field}>
53+
<Autocomplete
54+
fullWidth
55+
value={course[field]}
56+
onChange={(_, v) => updateField(course.id, field, false, false, true)(v)}
57+
options={images}
58+
getOptionLabel={(option) => option.label}
59+
renderInput={(params) => <TextField {...params} label={label} variant="outlined" />}
60+
/>
61+
</Grid>
62+
);
63+
}
64+
switch (type) {
65+
case 'text':
66+
return (
67+
<Grid item xs={12} key={field}>
68+
<TextField
69+
fullWidth
70+
disabled={disabled || _disabled}
71+
variant={'outlined'}
72+
label={label}
73+
value={course[field]}
74+
onChange={updateField(course.id, field)}
75+
/>
76+
</Grid>
77+
);
78+
case 'boolean':
79+
return (
80+
<Grid item xs={12} key={field}>
81+
<FormControlLabel
82+
value={course[field]}
83+
label={label}
84+
labelPlacement="end"
85+
control={
86+
<Switch
87+
checked={course[field]}
88+
color={'primary'}
89+
onClick={updateField(course.id, field, true)}
90+
/>
91+
}
5192
/>
52-
}
53-
/>}
54-
</Grid>
55-
))}
93+
</Grid>
94+
);
95+
}
96+
return null;
97+
})}
5698
</Grid>
5799
</CardContent>
58100
{!_disabled ? (

old-web/src/Pages/Admin/Course.jsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,13 @@ export default function Course() {
6565
}, [reset]);
6666

6767
const updateField = (id, field, toggle = false, datetime = false, json = false) => (e) => {
68-
if (!e) {
69-
return;
70-
}
71-
7268
if (course.id === id) {
7369
if (toggle) {
7470
course[field] = !course[field];
7571
} else if (datetime) {
7672
course[field] = format(e, 'yyyy-MM-dd HH:mm:ss');
73+
} else if (json) {
74+
course[field] = e;
7775
} else {
7876
course[field] = e.target.value.toString();
7977
}

0 commit comments

Comments
 (0)