Skip to content

Commit fddb029

Browse files
committed
Resource.part_hierarchy uses the exam definition
Previously, a Resource's part hierarchy was computed from the SCORM elements of attempts at that resource. That meant that the hierarchy can't be worked with until there is at least one attempt, and attempts can't differ in their structure. This replaces that routine with one that looks at the exam's definition. It still doesn't cope with randomised order or different structures resulting from things like explore or diagnostic mode, but at least it can be determined without waiting for an attempt. The form to discount parts is always available, fixing #344
1 parent 5595d36 commit fddb029

File tree

5 files changed

+55
-49
lines changed

5 files changed

+55
-49
lines changed

numbas_lti/models.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,24 @@ def resolve_feedback_setting(setting):
316316

317317
return info
318318

319+
def part_hierarchy(self):
320+
data = self.source()
321+
322+
hierarchy = {}
323+
qn = 0
324+
for qg in data.get('question_groups',[]):
325+
for q in qg.get('questions',[]):
326+
qd = hierarchy[qn] = {}
327+
for i,part in enumerate(q.get('parts',[])):
328+
p = {'gaps': [], 'steps': []}
329+
if part['type'] == 'gapfill':
330+
p['gaps'] = list(range(len(part.get('gaps',[]))))
331+
p['steps'] = list(range(len(part.get('steps',[]))))
332+
qd[i] = p
333+
qn += 1
334+
335+
return hierarchy
336+
319337

320338
GRADING_METHODS = [
321339
('highest',_('Highest score')),
@@ -672,33 +690,9 @@ def user_data(self,user):
672690
return LTIUserData.objects.filter(resource=self,user=user).last()
673691

674692
def part_hierarchy(self):
675-
"""
676-
Returns an object
677-
{
678-
question_num: {
679-
part_num: {
680-
gaps: [list of gap indices],
681-
steps: [list of step indices]
682-
}
683-
}
684-
}
685-
"""
686-
paths = sorted(set(e['value'] for e in ScormElement.objects.filter(attempt__resource=self,key__regex=r'cmi.interactions.[0-9]+.id').values('value')),key=lambda x:(len(x),x))
687-
re_path = re.compile(r'q([0-9]+)p([0-9]+)(?:g([0-9]+)|s([0-9]+))?')
688-
out = defaultdict(lambda: defaultdict(lambda: {'gaps':[],'steps':[]}))
689-
for path in paths:
690-
m = re_path.match(path)
691-
question_index = m.group(1)
692-
part_index = m.group(2)
693-
gap_index = m.group(3)
694-
step_index = m.group(4)
695-
p = out[question_index][part_index]
696-
if m.group(3):
697-
p['gaps'].append(gap_index)
698-
elif m.group(4):
699-
p['steps'].append(step_index)
700-
701-
return out
693+
if self.exam is None:
694+
return {}
695+
return self.exam.part_hierarchy()
702696

703697
def last_activity(self):
704698
if self.attempts.exists():
@@ -1362,6 +1356,7 @@ def part_discount(self,part):
13621356
return self.resource.discounted_parts.filter(part=part).first()
13631357

13641358
def part_paths(self):
1359+
""" Paths to all parts in this attempt. """
13651360
return set(e['value'] for e in self.scormelements.filter(key__regex='cmi.interactions.[0-9]+.id').values('value').distinct())
13661361

13671362
def part_hierarchy(self):

numbas_lti/templates/numbas_lti/management/dashboard.html

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ <h1>{% translate "Dashboard" %}
3737
{% endif %}
3838
</section>
3939

40-
{% if resource.attempts.count %}
40+
{% if has_attempts %}
4141

4242
<section id="attempts-count">
4343
<p><strong>{{resource.unbroken_attempts.count}}</strong> {% blocktranslate count counter=resource.unbroken_attempts.count %}attempt{% plural %}attempts{% endblocktranslate %} {% translate "by" %} <strong>{{students.count}}</strong> {% blocktranslate count counter=students.count %}student{% plural %}students{% endblocktranslate %}.</p>
@@ -66,8 +66,15 @@ <h1>{% translate "Dashboard" %}
6666
</section>
6767
{% endif %}
6868

69-
<section>
70-
<ul class="list-unstyled actions">
69+
{% else %}
70+
<section id="attempts-count">
71+
<p>{% translate "No students have attempted this exam yet. Information about scores will appear here once a student attempts this exam." %}</p>
72+
</section>
73+
{% endif %}
74+
75+
<section>
76+
<ul class="list-unstyled actions">
77+
{% if has_attempts %}
7178
<li><form method="POST" action="{% url_with_lti 'scores_csv' resource.pk %}">{% csrf_token %}<button type="submit" class="button info">{% icon 'save' %} {% translate "Download scores as CSV" %}</button></form></li>
7279
{% if not last_report_process %}
7380
<li>
@@ -77,22 +84,19 @@ <h1>{% translate "Dashboard" %}
7784
{% endif %}
7885
</li>
7986
{% endif %}
80-
<li><a class="button" href="{% url_with_lti 'student_progress' resource.pk %}">{% icon 'user' %} {% translate "View individual student progress and grant access tokens" %}</a></li>
81-
<li>
82-
<a class="button danger" href="{% url_with_lti 'discount_parts' resource.pk %}">{% icon 'minus-sign' %} {% translate "Discount question parts" %}</a>
83-
<span class="help-block">{% translate "You can remark individual attempts on the attempts page." %}</span>
84-
</li>
85-
<li>
86-
<a class="button info" href="{% url_with_lti 'validate_receipt' resource.pk %}">{% icon 'ok' %} {% translate "Validate a receipt code" %}</a>
87-
</li>
88-
</ul>
89-
</section>
90-
91-
{% else %}
92-
<section id="attempts-count">
93-
<p>{% translate "No students have attempted this exam yet. Information about scores will appear here once a student attempts this exam." %}</p>
94-
</section>
95-
{% endif %}
87+
{% endif %}
88+
<li><a class="button" href="{% url_with_lti 'student_progress' resource.pk %}">{% icon 'user' %} {% translate "View individual student progress and grant access tokens" %}</a></li>
89+
<li>
90+
<a class="button danger" href="{% url_with_lti 'discount_parts' resource.pk %}">{% icon 'minus-sign' %} {% translate "Discount question parts" %}</a>
91+
<span class="help-block">{% translate "You can remark individual attempts on the attempts page." %}</span>
92+
</li>
93+
{% if has_attempts %}
94+
<li>
95+
<a class="button info" href="{% url_with_lti 'validate_receipt' resource.pk %}">{% icon 'ok' %} {% translate "Validate a receipt code" %}</a>
96+
</li>
97+
{% endif %}
98+
</ul>
99+
</section>
96100

97101
{% if exam_info %}
98102
<hr>

numbas_lti/templates/numbas_lti/management/discount.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ <h1>{% translate "Discount question parts" %}</h1>
3939
</thead>
4040
<tbody>
4141
{% for part in parts %}
42-
<tr class="{% if part.p == None %}info{% endif %} {% if part.discount %}warning{% endif %}">
43-
<td class="{% if part.p == None %}question-id{% endif %} {% if part.p %}muted not-first-appearance{% endif %}">{{part.q}}</td>
42+
<tr class="{% if part.p is None %}info{% endif %} {% if part.discount %}warning{% endif %}">
43+
<td class="{% if part.p is None %}question-id{% endif %} {% if part.p %}muted not-first-appearance{% endif %}">{{part.q}}</td>
4444
<td class="{% if part.p and part.g %}muted not-first-appearance{% endif %}">{% if part.p %}{{part.p}}{% endif %}</td>
45-
<td>{% if part.g %}{{part.g}}{% endif %}</td>
45+
<td>{% if part.g is not None %}{{part.g}}{% endif %}</td>
4646
<td class="part-control">
4747
{% if part.p %}
4848
<select name="discount-{{part.path}}">

numbas_lti/util.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def hierarchy_key(x):
2424
return key
2525

2626
def transform_part_hierarchy(hierarchy,transform,hierarchy_key=hierarchy_key):
27+
"""
28+
Transform the part hierarchy of an exam.
29+
``hierarchy`` is a dictionary Dict[question_number:int -> Dict[part_number:int -> Dict["gaps" -> list[int], "steps" -> list[int]]]].
30+
For each part in the hierarchy, the function ``transform`` is called with information about that part.
31+
"""
2732
out = []
2833

2934
def row(q,p=None,g=None,parent=None,has_gaps=False):

numbas_lti/views/resource.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ def get_context_data(self,*args,**kwargs):
211211

212212
context['num_unbroken_attempts'] = resource.attempts.exclude(broken=True).count()
213213

214+
context['has_attempts'] = resource.attempts.exists()
215+
214216
if not resource.lineitem_unwanted and resource.lti_13_links.exists():
215217
lti_context = resource.lti_13_contexts().first()
216218
if lti_context:

0 commit comments

Comments
 (0)