Skip to content

Commit 0be1a43

Browse files
committed
Squashed commit of the following:
commit 91288c1 Author: Bradley Miller <[email protected]> Date: Wed Aug 30 16:03:21 2023 -0500 Show correct course on datashop dump commit 02e8102 Author: Bradley Miller <[email protected]> Date: Wed Aug 30 15:37:18 2023 -0500 Fix: make sure questions are in a runestone-sphinx container commit 1b702f0 Author: Bradley Miller <[email protected]> Date: Thu Aug 24 17:09:30 2023 -0500 New: use author server to download logs commit e1fff3b Author: Bradley Miller <[email protected]> Date: Thu Aug 24 17:09:14 2023 -0500 New: use author server to download logs commit 51a90a4 Author: Bradley Miller <[email protected]> Date: Thu Aug 24 14:59:47 2023 -0500 New: show different message to instructors commit 591cf24 Author: Bradley Miller <[email protected]> Date: Thu Aug 24 14:59:33 2023 -0500 Fix: speed up gradebook filtering of instructors commit 74addc0 Author: Bradley Miller <[email protected]> Date: Tue Aug 22 16:48:34 2023 -0500 Allow instructor to dump one class in DS format commit a4429f2 Author: Bradley Miller <[email protected]> Date: Mon Aug 21 17:21:31 2023 -0500 customize datashop dump for instructors only
1 parent 1a8cb85 commit 0be1a43

File tree

13 files changed

+229
-53
lines changed

13 files changed

+229
-53
lines changed

author.compose.yml

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
command: uvicorn rsptx.author_server_api.main:app --host 0.0.0.0 --port 8114
1414
volumes:
1515
- ${BOOK_PATH}:/books
16+
- $HOME/downloads:/usr/src/app/downloads
1617
environment:
1718
- SERVER_CONFIG=${SERVER_CONFIG}
1819
- CELERY_BROKER_URL=redis://redis:6379/0
@@ -35,6 +36,7 @@ services:
3536
volumes:
3637
- ${BOOK_PATH}:/books
3738
- ${SSH_AUTH_SOCK:-/tmp}:/ssh-agent # forward host ssh agent
39+
- $HOME/downloads:/usr/src/app/downloads
3840
environment:
3941
- SERVER_CONFIG=${SERVER_CONFIG}
4042
- CELERY_BROKER_URL=redis://redis:6379/0

bases/rsptx/author_server_api/main.py

+34-15
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
# Local App
3535
# ---------
36-
from rsptx.forms import LibraryForm, DatashopForm
36+
from rsptx.forms import LibraryForm, DatashopForm, DatashopInstForm
3737
from rsptx.author_server_api.worker import (
3838
build_runestone_book,
3939
clone_runestone_book,
@@ -50,6 +50,7 @@
5050
fetch_instructor_courses,
5151
fetch_books_by_author,
5252
fetch_course,
53+
fetch_course_by_id,
5354
fetch_library_book,
5455
update_library_book,
5556
create_course,
@@ -150,7 +151,7 @@ async def home(request: Request, user=Depends(auth_manager)):
150151
async def logfiles(request: Request, user=Depends(auth_manager)):
151152
if await is_instructor(request):
152153

153-
lf_path = pathlib.Path("logfiles", user.username)
154+
lf_path = pathlib.Path("downloads", "logfiles", user.username)
154155
logger.debug(f"WORKING DIR = {lf_path}")
155156
if lf_path.exists():
156157
ready_files = {
@@ -180,13 +181,13 @@ async def logfiles(request: Request, user=Depends(auth_manager)):
180181

181182
@app.get("/author/getfile/{fname}")
182183
async def getfile(request: Request, fname: str, user=Depends(auth_manager)):
183-
file_path = pathlib.Path("logfiles", user.username, fname)
184+
file_path = pathlib.Path("downloads", "logfiles", user.username, fname)
184185
return FileResponse(file_path)
185186

186187

187188
@app.get("/author/getdatashop/{fname}")
188189
async def _getdshop(request: Request, fname: str, user=Depends(auth_manager)):
189-
file_path = pathlib.Path("datashop", user.username, fname)
190+
file_path = pathlib.Path("downloads", "datashop", user.username, fname)
190191
return FileResponse(file_path)
191192

192193

@@ -237,7 +238,9 @@ async def dump_assignments(request: Request, course: str, user=Depends(auth_mana
237238
""",
238239
eng,
239240
)
240-
all_aq_pairs.to_csv(f"{course}_assignments.csv", index=False)
241+
all_aq_pairs.to_csv(
242+
f"downloads/logfiles/{user.username}/{course}_assignments.csv", index=False
243+
)
241244

242245
return JSONResponse({"detail": "success"})
243246

@@ -336,17 +339,22 @@ async def editlib(request: Request, book: str, user=Depends(auth_manager)):
336339
@app.post("/author/anonymize_data/{book}")
337340
async def anondata(request: Request, book: str, user=Depends(auth_manager)):
338341
# Get the book and populate the form with current data
339-
if not await verify_author(user):
340-
return RedirectResponse(url="/notauthorized")
342+
is_author = await verify_author(user)
343+
is_inst = await is_instructor(request)
344+
345+
if not (is_author or is_inst):
346+
return RedirectResponse(url="/author/notauthorized")
341347

342348
# Create a list of courses taught by this user to validate courses they
343349
# can dump directly.
344350
course = await fetch_course(user.course_name)
345351
courses = await fetch_instructor_courses(user.id)
346-
class_list = [c.id for c in courses]
347-
class_list = [str(x) for x in class_list]
352+
class_list = []
353+
for c in courses:
354+
the_course = await fetch_course_by_id(c.course)
355+
class_list.append(the_course.course_name)
348356

349-
lf_path = pathlib.Path("datashop", user.username)
357+
lf_path = pathlib.Path("downloads", "datashop", user.username)
350358
logger.debug(f"WORKING DIR = {lf_path}")
351359
if lf_path.exists():
352360
ready_files = [x for x in lf_path.iterdir()]
@@ -356,13 +364,22 @@ async def anondata(request: Request, book: str, user=Depends(auth_manager)):
356364
# this will either create the form with data from the submitted form or
357365
# from the kwargs passed if there is not form data. So we can prepopulate
358366
#
359-
form = await DatashopForm.from_formdata(
360-
request, basecourse=book, clist=",".join(class_list)
361-
)
367+
if is_author:
368+
form = await DatashopForm.from_formdata(
369+
request, basecourse=book, clist=",".join(class_list)
370+
)
371+
372+
elif is_inst:
373+
form = await DatashopInstForm.from_formdata(
374+
request,
375+
basecourse=book,
376+
clist=",".join(class_list),
377+
specific_course=course.course_name,
378+
)
379+
form.specific_course.choices = class_list
362380
if request.method == "POST" and await form.validate():
363381
print(f"Got {form.authors.data}")
364382
print(f"FORM data = {form.data}")
365-
366383
# return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
367384
return templates.TemplateResponse(
368385
"author/anonymize_data.html",
@@ -373,6 +390,8 @@ async def anondata(request: Request, book: str, user=Depends(auth_manager)):
373390
ready_files=ready_files,
374391
kind="datashop",
375392
course=course,
393+
is_author=is_author,
394+
is_instructor=is_inst,
376395
),
377396
)
378397

@@ -471,7 +490,7 @@ async def dump_code(payload=Body(...), user=Depends(auth_manager)):
471490
@app.get("/author/dlsAvailable/{kind}", status_code=201)
472491
async def check_downloads(request: Request, kind: str, user=Depends(auth_manager)):
473492
# kind will be either logfiles or datashop
474-
lf_path = pathlib.Path("logfiles", user.username)
493+
lf_path = pathlib.Path("downloads", "logfiles", user.username)
475494
logger.debug(f"WORKING DIR = {lf_path}")
476495
if lf_path.exists():
477496
ready_files = [x.name for x in lf_path.iterdir()]

bases/rsptx/author_server_api/worker.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def useinfo_to_csv(self, classname, username):
277277
params=dict(cname=classname),
278278
con=eng,
279279
)
280-
p = pathlib.Path("logfiles", username)
280+
p = pathlib.Path("downloads","logfiles", username)
281281
p.mkdir(parents=True, exist_ok=True)
282282
p = p / f"{classname}_useinfo.csv.zip"
283283
self.update_state(state="WRITING", meta={"current": "creating csv.zip file"})
@@ -299,7 +299,7 @@ def code_to_csv(self, classname, username):
299299
params=dict(cname=classname),
300300
con=eng,
301301
)
302-
p = pathlib.Path("logfiles", username)
302+
p = pathlib.Path("downloads", "logfiles", username)
303303
p.mkdir(parents=True, exist_ok=True)
304304
p = p / f"{classname}_code.csv.zip"
305305
self.update_state(state="WRITING", meta={"current": "creating csv.zip file"})
@@ -335,7 +335,7 @@ def anonymize_data_dump(self, **kwargs):
335335
)
336336
a.create_datashop_data()
337337
self.update_state(state="WORKING", meta={"current": "Writing datashop file"})
338-
p = pathlib.Path("datashop", username)
338+
p = pathlib.Path("downloads", "datashop", username)
339339
p.mkdir(parents=True, exist_ok=True)
340340
a.write_datashop(path=p)
341341
self.update_state(state="SUCCESS", meta={"current": "Ready for download"})

bases/rsptx/web2py_server/applications/runestone/controllers/dashboard.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,14 @@ def grades():
451451
session.flash = "Your course does not exist"
452452
redirect(URL("dashboard", "index"))
453453

454+
iset = set()
455+
instructors = db(db.course_instructor.course == course.id).select(
456+
db.course_instructor.ALL
457+
)
458+
if instructors:
459+
for i in instructors:
460+
iset.add(i.instructor)
461+
454462
assignments = db(db.assignments.course == course.id).select(
455463
db.assignments.ALL, orderby=(db.assignments.duedate, db.assignments.id)
456464
)
@@ -487,7 +495,7 @@ def grades():
487495
rows = []
488496
for row in trows:
489497
# remove instructor rows from trows
490-
if not verifyInstructorStatus(auth.user.course_id, row[3]):
498+
if row[3] not in iset:
491499
rows.append(row)
492500

493501
studentinfo = {}
@@ -498,7 +506,7 @@ def grades():
498506
total_possible_points = 0
499507
students = []
500508
for s in allstudents:
501-
if verifyInstructorStatus(auth.user.course_id, s.id):
509+
if s.id in iset:
502510
# filter out instructors from the gradebook
503511
continue
504512
students.append(s)

bases/rsptx/web2py_server/applications/runestone/static/motd.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ <h4>Please Support Runestone</h4>
2626
already.
2727
</p>
2828

29-
<div style="width: 50%; margin-left: auto; margin-right: auto;">
30-
<img src="/runestone/static/_images/SPP_Logo_Horizontal_Final2020.png" />
29+
<div style="width: 25%; margin-left: auto; margin-right: auto; margin-bottom: 10px;">
30+
<img src="/runestone/static/_images/SPP_Logo_Horizontal_Final2020.png" width="150" />
3131
</div>

bases/rsptx/web2py_server/applications/runestone/views/admin/admin.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ <h4 style="text-align: center" class="list-group-item-heading">Manage Students</
2424
<a id="AddInstructorTab" data-toggle="tab" href="#AddInstructor" class="list-group-item">
2525
<h4 style="text-align: center" class="list-group-item-heading">Add TA</h4>
2626
</a>
27-
<a id="courselog" href="/{{=request.application}}/admin/courselog" class="list-group-item">
28-
<h4 style="text-align: center" class="list-group-item-heading">Download Log</h4>
27+
<a id="courselog" href="/author/anonymize_data/{{=course.base_course}}" class="list-group-item">
28+
<h4 style="text-align: center" class="list-group-item-heading">Download Course Data</h4>
2929
</a>
3030
<a id="activeLink" href="/{{=request.application}}/dashboard/active" class="list-group-item">
3131
<h4 style="text-align: center" class="list-group-item-heading">Students Online</h4>

components/rsptx/data_extract/anonymizeCourseData.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
# coding: utf-8
33

44
import os
5+
import datetime
56
import random
67
import re
78
import pandas as pd
89
import pathlib
10+
import pdb
911
from sqlalchemy import create_engine
1012
from tqdm import tqdm
1113

@@ -96,19 +98,24 @@ def __init__(
9698
dburl,
9799
with_assess=False,
98100
start_date="2019-01-01",
99-
end_date="2022-05-16",
101+
end_date=datetime.date.today(),
100102
sample_size=10,
101103
include_basecourse=False,
102104
specific_course="",
105+
preserve_user_ids=False,
103106
):
104107
self.eng = create_engine(dburl)
105108
self.BASECOURSE = basecourse
106109
self.WITH_ASSESS = with_assess
107110
self.START_DATE = start_date
108111
self.END_DATE = end_date
109-
self.SAMPLE_SIZE = int(sample_size)
112+
if sample_size:
113+
self.SAMPLE_SIZE = int(sample_size)
114+
else:
115+
self.SAMPLE_SIZE = 10
110116
print(f"include basecourse = {include_basecourse}")
111117
self.include_basecourse = include_basecourse
118+
self.preserve_username = preserve_user_ids
112119
if specific_course:
113120
self.COURSE_LIST = [specific_course]
114121
else:
@@ -257,8 +264,11 @@ def anonymize_user(self, id):
257264
if id in self.inst_set:
258265
self.user_map[id] = "REMOVEME"
259266
else:
260-
self.user_map[id] = self.user_num
261-
self.user_num += 1
267+
if self.preserve_username:
268+
self.user_map[id] = id
269+
else:
270+
self.user_map[id] = self.user_num
271+
self.user_num += 1
262272
return self.user_map[id]
263273

264274
def anonymize_course(self, id):
@@ -435,7 +445,12 @@ def sessionize_data(self):
435445
# The below is very expensive for long operations. Maybe we could rewrite it with a rolling function
436446

437447
self.useinfo["tdiff"] = self.useinfo.timestamp.diff()
438-
self.useinfo["sdiff"] = self.useinfo.sid.diff()
448+
if self.preserve_username:
449+
self.useinfo["sdiff"] = (
450+
self.useinfo.sid.ne(self.useinfo.sid.shift()).bfill().astype(int)
451+
)
452+
else:
453+
self.useinfo["sdiff"] = self.useinfo.sid.diff()
439454
self.sess_count = 0
440455
self.useinfo["session"] = self.useinfo.progress_apply(self.sessionize, axis=1)
441456
self.student_problem_ct = {}
@@ -502,7 +517,6 @@ def create_datashop_data(self):
502517
"anon_institution",
503518
]
504519
]
505-
506520
useinfo_w_answers.columns = [
507521
"Time",
508522
"Anon Student Id",
@@ -648,10 +662,11 @@ def write_datashop(self, path="./"):
648662

649663
if __name__ == "__main__":
650664
a = Anonymizer(
651-
"py4e-int",
665+
"thinkcspy",
652666
os.environ["DBURL"],
653667
sample_size=3,
654-
cl=["Win22-SI206", "Win21-SI206"],
668+
specific_course="bl_thinkcspy_summer23",
669+
preserve_user_ids=True,
655670
)
656671
print("Choosing Courses")
657672
a.choose_courses()

components/rsptx/db/crud.py

+19
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,25 @@ async def fetch_course(course_name: str) -> CoursesValidator:
397397
return CoursesValidator.from_orm(course)
398398

399399

400+
async def fetch_course_by_id(course_id: int) -> CoursesValidator:
401+
"""
402+
Fetches a course by its id.
403+
404+
:param course_name: The id of the course to be fetched.
405+
:type course_name: int
406+
:return: A CoursesValidator instance representing the fetched course.
407+
:rtype: CoursesValidator
408+
"""
409+
query = select(Courses).where(Courses.id == course_id)
410+
async with async_session() as session:
411+
res = await session.execute(query)
412+
# When selecting ORM entries it is useful to use the ``scalars`` method
413+
# This modifies the result so that you are getting the ORM object
414+
# instead of a Row object. `See <https://docs.sqlalchemy.org/en/14/orm/queryguide.html#selecting-orm-entities-and-attributes>`_
415+
course = res.scalars().one_or_none()
416+
return CoursesValidator.from_orm(course)
417+
418+
400419
async def fetch_base_course(base_course: str) -> CoursesValidator:
401420
"""
402421
Fetches a base course by its name.

components/rsptx/forms/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from rsptx.forms import core
2-
from rsptx.forms.author import LibraryForm, DatashopForm
2+
from rsptx.forms.author import LibraryForm, DatashopForm, DatashopInstForm
33

4-
__all__ = ["core", "LibraryForm", "DatashopForm"]
4+
__all__ = ["core", "LibraryForm", "DatashopForm", "DatashopInstForm"]

components/rsptx/forms/author.py

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
DateTimeField,
88
DateField,
99
HiddenField,
10+
SelectField,
1011
IntegerRangeField,
1112
)
1213

@@ -47,3 +48,10 @@ class DatashopForm(StarletteForm):
4748
include_basecourse = BooleanField("Include data from the open course")
4849
specific_course = StringField("Create data shop file for this course")
4950
clist = HiddenField()
51+
52+
53+
class DatashopInstForm(StarletteForm):
54+
preserve_user_ids = BooleanField("Preserve User IDs")
55+
specific_course = SelectField("Class Name")
56+
basecourse = HiddenField()
57+
clist = HiddenField()

0 commit comments

Comments
 (0)