Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.

Commit 82ce8e0

Browse files
committed
merge master
2 parents 187c4c1 + a403c57 commit 82ce8e0

File tree

16 files changed

+279
-48
lines changed

16 files changed

+279
-48
lines changed

controllers/admin.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -782,10 +782,14 @@ def createAssignment():
782782
response.headers['content-type'] = 'application/json'
783783
due = None
784784
logger.debug(type(request.vars['name']))
785-
786785
try:
787-
logger.debug("Adding new assignment {} for course".format(request.vars['name'], auth.user.course_id))
788-
newassignID = db.assignments.insert(course=auth.user.course_id, name=request.vars['name'], duedate=datetime.datetime.utcnow() + datetime.timedelta(days=7))
786+
name=request.vars['name']
787+
course=auth.user.course_id
788+
logger.debug("Adding new assignment {} for course".format(request.vars['name'], course))
789+
name_existsQ = len(db((db.assignments.name == name) & (db.assignments.course == course)).select())
790+
if name_existsQ>0:
791+
return json.dumps("EXISTS")
792+
newassignID = db.assignments.insert(course=course, name=name, duedate=datetime.datetime.utcnow() + datetime.timedelta(days=7))
789793
except Exception as ex:
790794
logger.error(ex)
791795
return json.dumps('ERROR')
@@ -796,6 +800,27 @@ def createAssignment():
796800
logger.error(ex)
797801
return json.dumps('ERROR')
798802

803+
@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True)
804+
def renameAssignment():
805+
response.headers['content-type'] = 'application/json'
806+
try:
807+
logger.debug("Renaming {} to {} for course {}.".format(request.vars['original'],request.vars['name'],auth.user.course_id))
808+
assignment_id=request.vars['original']
809+
name=request.vars['name']
810+
course=auth.user.course_id
811+
name_existsQ = len(db((db.assignments.name == name) & (db.assignments.course == course)).select())
812+
if name_existsQ>0:
813+
return json.dumps("EXISTS")
814+
db(db.assignments.id == assignment_id).update(name=name)
815+
except Exception as ex:
816+
logger.error(ex)
817+
return json.dumps('ERROR')
818+
try:
819+
returndict={name: assignment_id}
820+
return json.dumps(returndict)
821+
except Exception as ex:
822+
logger.error(ex)
823+
return json.dumps('ERROR')
799824

800825
@auth.requires(lambda: verifyInstructorStatus(auth.user.course_name, auth.user), requires_login=True)
801826
def questionBank():

controllers/ajax.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,13 @@ def gethist():
302302
codetbl = db.code
303303
acid = request.vars.acid
304304

305+
# if vars.sid then we know this is being called from the grading interface
305306
if request.vars.sid:
306307
sid = request.vars.sid
307-
course_id = db(db.auth_user.username == sid).select(db.auth_user.course_id).first().course_id
308+
if auth.user and verifyInstructorStatus(auth.user.course_name, auth.user.id):
309+
course_id = auth.user.course_id
310+
else:
311+
course_id = None
308312
elif auth.user:
309313
sid = auth.user.username
310314
course_id = auth.user.course_id
@@ -982,8 +986,11 @@ def preview_question():
982986
# Prevent any changes to the database when building a preview question.
983987
del env['DBURL']
984988
# Run a runestone build.
989+
# We would like to use sys.executable But when we run web2py
990+
# in uwsgi then sys.executable is uwsgi which doesn't work.
991+
# Why not just run runestone?
985992
popen_obj = subprocess.Popen(
986-
[sys.executable, '-m', 'runestone', 'build'],
993+
[settings.python_interpreter, '-m', 'runestone', 'build'],
987994
# The build must be run from the directory containing a ``conf.py`` and all the needed support files.
988995
cwd='applications/{}/build/preview'.format(request.application),
989996
# Capture the build output in case of an error.

controllers/books.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def _route_book(is_published=True):
5252

5353
# Ensure the base course in the URL agrees with the base course in ``course``. If not, ask the user to select a course.
5454
if not course or course.base_course != base_course:
55+
session.flash = "{} is not the course your are currently in, switch to or add it to go there".format(base_course)
5556
redirect(URL(c='default', f='courses'))
5657

5758
# Ensure the user has access to this book.

controllers/dashboard.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,55 @@
2323

2424
# select acid, sid from code as T where timestamp = (select max(timestamp) from code where sid=T.sid and acid=T.acid);
2525

26+
class ChapterGet:
27+
# chapnum_map={}
28+
# sub_chapters={}
29+
# subchap_map={}
30+
# subchapnum_map={}
31+
# subchapNum_map={}
32+
def __init__(self,chapters):
33+
34+
self.Cmap={}
35+
self.Smap={} #dictionary organized by chapter and section labels
36+
self.SAmap={} #organized just by section label
37+
for chapter in chapters:
38+
label=chapter.chapter_label
39+
self.Cmap[label]=chapter
40+
sub_chapters=db(db.sub_chapters.chapter_id==chapter.id).select(db.sub_chapters.ALL) #FIX: get right course_id, too
41+
#NOTE: sub_chapters table doesn't have a course name column in it, kind of a problem
42+
self.Smap[label]={}
43+
44+
for sub_chapter in sub_chapters:
45+
self.Smap[label][sub_chapter.sub_chapter_label]=sub_chapter
46+
self.SAmap[sub_chapter.sub_chapter_label]=sub_chapter
47+
def ChapterNumber(self,label):
48+
"""Given the label of a chapter, return its number"""
49+
try:
50+
return self.Cmap[label].chapter_num
51+
except KeyError:
52+
return ""
53+
def ChapterName(self,label):
54+
try:
55+
return self.Cmap[label].chapter_name
56+
except KeyError:
57+
return label
58+
def SectionName(self,chapter,section):
59+
try:
60+
return self.Smap[chapter][section].sub_chapter_name
61+
except KeyError:
62+
return section
63+
def SectionNumber(self,chapter,section=None):
64+
try:
65+
if section==None:
66+
lookup=self.SAmap
67+
section=chapter
68+
else:
69+
lookup=self.Smap[chapter]
70+
71+
return lookup[section].sub_chapter_num
72+
except KeyError:
73+
return 999
74+
2675
@auth.requires_login()
2776
def index():
2877
selected_chapter = None
@@ -36,11 +85,10 @@ def index():
3685

3786
course = db(db.courses.id == auth.user.course_id).select().first()
3887
assignments = db(db.assignments.course == course.id).select(db.assignments.ALL, orderby=db.assignments.name)
88+
chapters = db(db.chapters.course_id == course.base_course).select(orderby=db.chapters.chapter_num)
89+
3990
logger.debug("getting chapters for {}".format(auth.user.course_name))
40-
chapters = db(db.chapters.course_id == course.base_course).select()
41-
chap_map = {}
42-
for chapter in chapters:
43-
chap_map[chapter.chapter_label] = chapter.chapter_name
91+
chapget = ChapterGet(chapters)
4492
for chapter in chapters.find(lambda chapter: chapter.chapter_label==request.vars['chapter']):
4593
selected_chapter = chapter
4694
if selected_chapter is None:
@@ -61,12 +109,16 @@ def index():
61109

62110
if data_analyzer.questions[problem_id]:
63111
chtmp = data_analyzer.questions[problem_id].chapter
112+
schtmp = data_analyzer.questions[problem_id].subchapter
64113
entry = {
65114
"id": problem_id,
66115
"text": metric.problem_text,
67116
"chapter": chtmp,
68-
"chapter_title": chap_map.get(chtmp,chtmp),
69-
"sub_chapter": data_analyzer.questions[problem_id].subchapter,
117+
"chapter_title": chapget.ChapterName(chtmp),
118+
"chapter_number": chapget.ChapterNumber(chtmp),
119+
"sub_chapter": schtmp,
120+
"sub_chapter_number": chapget.SectionNumber(chtmp,schtmp),
121+
"sub_chapter_title": chapget.SectionName(chtmp,schtmp),
70122
"correct": stats[2],
71123
"correct_mult_attempt": stats[3],
72124
"incomplete": stats[1],
@@ -79,6 +131,8 @@ def index():
79131
"text": metric.problem_text,
80132
"chapter": "unknown",
81133
"sub_chapter": "unknown",
134+
"sub_chapter_number": 0,
135+
"sub_chapter_title":"unknown",
82136
"chapter_title": "unknown",
83137
"correct": stats[2],
84138
"correct_mult_attempt": stats[3],
@@ -89,14 +143,20 @@ def index():
89143
questions.append(entry)
90144
logger.debug("ADDING QUESTION %s ", entry["chapter"])
91145

92-
logger.debug("getting questsions")
93-
questions = sorted(questions, key=itemgetter("chapter"))
146+
logger.debug("getting questions")
147+
try:
148+
questions = sorted(questions, key=itemgetter("chapter","sub_chapter_number"))
149+
except:
150+
logger.error("FAILED TO SORT {}".format(questions))
94151
logger.debug("starting sub_chapter loop")
95152
for sub_chapter, metric in six.iteritems(progress_metrics.sub_chapters):
96153
sections.append({
97154
"id": metric.sub_chapter_label,
98155
"text": metric.sub_chapter_text,
99156
"name": metric.sub_chapter_name,
157+
"number": chapget.SectionNumber(selected_chapter.chapter_label,metric.sub_chapter_label),
158+
#FIX: Using selected_chapter here might be a kludge
159+
#Better if metric contained chapter numbers associated with sub_chapters
100160
"readPercent": metric.get_completed_percent(),
101161
"startedPercent": metric.get_started_percent(),
102162
"unreadPercent": metric.get_not_started_percent()

controllers/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def donate():
457457
@auth.requires_login()
458458
def delete():
459459
if request.vars['deleteaccount']:
460-
logger.error("deleting account {} for {}".format(auth.user.id, auth.user.username))
460+
logger.error("deleting account {} for {}".format(auth.user.id, auth.user.username))
461461
session.flash = "Account Deleted"
462462
db(db.auth_user.id == auth.user.id).delete()
463463
db(db.useinfo.sid == auth.user.username).delete()

models/0.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from gluon.storage import Storage
99
import logging
1010
from os import environ
11+
import sys
1112

1213
settings = Storage()
1314

@@ -61,3 +62,4 @@
6162
settings.logger = "web2py.app.runestone"
6263
settings.sched_logger = settings.logger # works for production where sending log to syslog but not for dev.
6364
settings.log_level = logging.DEBUG
65+
settings.python_interpreter = sys.executable

models/db.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
#auth.settings.auth_two_factor_enabled = True
8282
#auth.settings.two_factor_methods = [lambda user, auth_two_factor: 'password_here']
8383

84+
if os.environ.get("WEB2PY_CONFIG","") == 'production':
85+
SELECT_CACHE = dict(cache=(cache.ram, 3600), cacheable=True)
86+
COUNT_CACHE = dict(cache=(cache.ram, 3600))
87+
else:
88+
SELECT_CACHE = {}
89+
COUNT_CACHE = {}
8490

8591
## create all tables needed by auth if not custom tables
8692
db.define_table('courses',
@@ -131,11 +137,11 @@ def verifyInstructorStatus(course, instructor):
131137
given course.
132138
"""
133139
if type(course) == str:
134-
course = db(db.courses.course_name == course).select(db.courses.id).first()
140+
course = db(db.courses.course_name == course).select(db.courses.id, **SELECT_CACHE).first()
135141

136142
return db((db.course_instructor.course == course) &
137143
(db.course_instructor.instructor == instructor)
138-
).count() > 0
144+
).count(**COUNT_CACHE) > 0
139145

140146
class IS_COURSE_ID:
141147
''' used to validate that a course name entered (e.g. devcourse) corresponds to a

static/js/admin.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ function gradeIndividualItem() {
4949
}
5050
}
5151
if(!sid){continue}
52-
var newid="Q"+question+"S"+sid;
53-
52+
var newid= "Q" + question.replace(/[#@+:>~.\/ ]/g,'_') +
53+
"S" + sid.replace(/[#@+:>~.\/]/g,'_');
5454
//This creates the equivalent of outerRightDiv for each question and student
5555
var divstring='<div style="border:1px solid;padding:5px;margin:5px;" id="'+newid+'">';
5656
divstring+='<h4 id="rightTitle"></h4><div id="questiondisplay">Question Display</div>'
@@ -1053,6 +1053,31 @@ function menu_from_editable(
10531053
}
10541054

10551055

1056+
function fillinAssignmentName(target){
1057+
//On the assignments tab, fill in the target with the name of the current assignment
1058+
//Only used by the rename assignment button for now
1059+
select=$("#assignlist")[0]
1060+
$("#"+target).html(select.options[select.selectedIndex].innerHTML)
1061+
}
1062+
//Invoked by the "Rename" button of the "Rename Assignment" dialog
1063+
function renameAssignment(form) {
1064+
var select=$("#assignlist")[0]
1065+
var id=select[select.selectedIndex].value
1066+
var name = form['rename-name'].value;
1067+
data={'name':name,'original':id}
1068+
url='/runestone/admin/renameAssignment';
1069+
jQuery.post(url,data,function(iserror,textStatus,whatever){
1070+
if (iserror=="EXISTS"){
1071+
alert('There already is an assignment called "'+name+'".') //FIX: reopen the dialog box?
1072+
} else if (iserror!='ERROR'){
1073+
//find the assignment
1074+
select=$('#assignlist')[0];
1075+
select.options[select.selectedIndex].innerHTML=name
1076+
} else {
1077+
alert('Error in renaming assignment '+id)
1078+
}
1079+
},'json')
1080+
}
10561081
// Invoked by the "Create" button of the "Create Assignment" dialog.
10571082
function createAssignment(form) {
10581083
var name = form.name.value;
@@ -1061,7 +1086,9 @@ function createAssignment(form) {
10611086
data = {'name': name}
10621087
url = '/runestone/admin/createAssignment';
10631088
jQuery.post(url, data, function (iserror, textStatus, whatever) {
1064-
if (iserror != 'ERROR') {
1089+
if (iserror=="EXISTS"){
1090+
alert('There already is an assignment called "'+name+'".') //FIX: reopen the dialog box?
1091+
} else if (iserror!='ERROR'){
10651092
select = document.getElementById('assignlist');
10661093
newopt = document.createElement('option');
10671094
newopt.value = iserror[name];

tests/test_ajax2.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,6 @@ def test_GetHist(test_client, test_user_1):
384384
kwargs['code'] = 'test_code_{}'.format(x)
385385
test_client.post('ajax/runlog', data = kwargs)
386386

387-
388387
kwargs = dict(
389388
acid = 'test_activecode_1',
390389
sid = 'test_user_1'
@@ -393,6 +392,17 @@ def test_GetHist(test_client, test_user_1):
393392
print(test_client.text)
394393
res = json.loads(test_client.text)
395394

395+
assert len(res['timestamps']) == 0
396+
assert len(res['history']) == 0
397+
398+
kwargs = dict(
399+
acid = 'test_activecode_1',
400+
#sid = 'test_user_1'
401+
)
402+
test_client.post('ajax/gethist', data = kwargs)
403+
print(test_client.text)
404+
res = json.loads(test_client.text)
405+
396406
assert len(res['timestamps']) == 10
397407
assert len(res['history']) == 10
398408

tests/test_server.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -523,21 +523,50 @@ def test_assignments(test_client, runestone_db_tools, test_user):
523523
test_instructor_1.make_instructor()
524524
test_instructor_1.login()
525525
db = runestone_db_tools.db
526-
526+
name_1 = 'test_assignment_1'
527+
name_2 = 'test_assignment_2'
528+
name_3 = 'test_assignment_3'
529+
527530
# Create an assignment -- using createAssignment
528531
test_client.post('admin/createAssignment',
529-
data=dict(name='test_assignment_1'))
532+
data=dict(name=name_1))
530533

531-
assign = db(
532-
(db.assignments.name == 'test_assignment_1') &
534+
assign1 = db(
535+
(db.assignments.name == name_1) &
533536
(db.assignments.course == test_instructor_1.course.course_id)
534537
).select().first()
535-
assert assign
538+
assert assign1
536539

537-
# Delete an assignment -- using removeassignment
538-
test_client.post('admin/removeassign', data=dict(assignid=assign.id))
539-
assert not db(db.assignments.name == 'test_assignment_1').select().first()
540+
# Make sure you can't create two assignments with the same name
541+
test_client.post('admin/createAssignment',
542+
data=dict(name=name_1))
543+
assert "EXISTS" in test_client.text
540544

545+
# Rename assignment
546+
test_client.post('admin/createAssignment',
547+
data=dict(name=name_2))
548+
assign2 = db(
549+
(db.assignments.name == name_2) &
550+
(db.assignments.course == test_instructor_1.course.course_id)
551+
).select().first()
552+
assert assign2
553+
554+
test_client.post('admin/renameAssignment',
555+
data=dict(name=name_3,original=assign2.id))
556+
assert db(db.assignments.name == name_3).select().first()
557+
assert not db(db.assignments.name == name_2).select().first()
558+
559+
# Make sure you can't rename an assignment to an already used assignment
560+
test_client.post('admin/renameAssignment',
561+
data=dict(name=name_3,original=assign1.id))
562+
assert "EXISTS" in test_client.text
563+
564+
# Delete an assignment -- using removeassignment
565+
test_client.post('admin/removeassign', data=dict(assignid=assign1.id))
566+
assert not db(db.assignments.name == name_1).select().first()
567+
test_client.post('admin/removeassign', data=dict(assignid=assign2.id))
568+
assert not db(db.assignments.name == name_3).select().first()
569+
541570
test_client.post('admin/removeassign', data=dict(assignid=9999999))
542571
assert "Error" in test_client.text
543572

0 commit comments

Comments
 (0)