feat: integrated exercises into lessons
- an exercise can now be added to a lesson - it is rendered using livecode editor with submit button - remembers the submitted code and shows the submission time Issue #90
This commit is contained in:
@@ -21,3 +21,13 @@ def get_section(name):
|
|||||||
"""
|
"""
|
||||||
doc = frappe.get_doc("LMS Section", name)
|
doc = frappe.get_doc("LMS Section", name)
|
||||||
return doc and doc.as_dict()
|
return doc and doc.as_dict()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def submit_solution(exercise, code):
|
||||||
|
"""Submits a solution.
|
||||||
|
"""
|
||||||
|
ex = frappe.get_doc("Exercise", exercise)
|
||||||
|
if not ex:
|
||||||
|
return
|
||||||
|
doc = ex.submit(code)
|
||||||
|
return {"name": doc.name, "creation": doc.creation}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Lesson(Document):
|
|||||||
def make_lms_section(self, index, section):
|
def make_lms_section(self, index, section):
|
||||||
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
|
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
|
||||||
s.type = section.type
|
s.type = section.type
|
||||||
|
s.id = section.id
|
||||||
s.label = section.label
|
s.label = section.label
|
||||||
s.contents = section.contents
|
s.contents = section.contents
|
||||||
s.index = index
|
s.index = index
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"contents",
|
"contents",
|
||||||
"code",
|
"code",
|
||||||
"attrs",
|
"attrs",
|
||||||
"index"
|
"index",
|
||||||
|
"id"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -43,12 +44,17 @@
|
|||||||
"fieldname": "index",
|
"fieldname": "index",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Index"
|
"label": "Index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "id"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-12 17:56:23.118854",
|
"modified": "2021-05-19 18:55:26.019625",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Section",
|
"name": "LMS Section",
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ class LMSSection(Document):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<LMSSection {self.label!r}>"
|
return f"<LMSSection {self.label!r}>"
|
||||||
|
|
||||||
|
def get_exercise(self):
|
||||||
|
if self.type == "exercise":
|
||||||
|
return frappe.get_doc("Exercise", self.id)
|
||||||
|
|
||||||
def get_latest_code_for_user(self):
|
def get_latest_code_for_user(self):
|
||||||
"""Returns the latest code for the logged in user.
|
"""Returns the latest code for the logged in user.
|
||||||
"""
|
"""
|
||||||
|
|||||||
14
community/lms/widgets/Exercise.html
Normal file
14
community/lms/widgets/Exercise.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||||
|
|
||||||
|
<div class="exercise">
|
||||||
|
<h2>{{ exercise.title }}</h2>
|
||||||
|
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
|
||||||
|
|
||||||
|
{% set submission = exercise.get_user_submission() %}
|
||||||
|
|
||||||
|
{{ LiveCodeEditor(exercise.name,
|
||||||
|
code=exercise.code,
|
||||||
|
reset_code=exercise.code,
|
||||||
|
is_exercise=True,
|
||||||
|
last_submitted=submission and submission.creation) }}
|
||||||
|
</div>
|
||||||
@@ -55,8 +55,14 @@
|
|||||||
{% macro render_section(s) %}
|
{% macro render_section(s) %}
|
||||||
{% if s.type == "text" %}
|
{% if s.type == "text" %}
|
||||||
{{ render_section_text(s) }}
|
{{ render_section_text(s) }}
|
||||||
{% elif s.type == "example" or s.type == "code" or s.type == "exercise" %}
|
{% elif s.type == "example" or s.type == "code" %}
|
||||||
{{ LiveCodeEditor(s.name, s.get_latest_code_for_user(), s.type=="exercise", "2 hours ago") }}
|
{{ LiveCodeEditor(s.name,
|
||||||
|
code=s.get_latest_code_for_user(),
|
||||||
|
reset_code=s.contents,
|
||||||
|
is_exercise=False)
|
||||||
|
}}
|
||||||
|
{% elif s.type == "exercise" %}
|
||||||
|
{{ widgets.Exercise(exercise=s.get_exercise())}}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>Unknown section type: {{s.type}}</div>
|
<div>Unknown section type: {{s.type}}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro LiveCodeEditor(name, code, is_exercise, last_submitted) %}
|
{% macro LiveCodeEditor(name, code, reset_code, is_exercise=False, last_submitted=None) %}
|
||||||
<div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
|
<div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8 col-md-6">
|
<div class="col-lg-8 col-md-6">
|
||||||
@@ -34,10 +34,13 @@
|
|||||||
{% if is_exercise %}
|
{% if is_exercise %}
|
||||||
<button class="submit pull-right btn-primary">Submit</button>
|
<button class="submit pull-right btn-primary">Submit</button>
|
||||||
{% if last_submitted %}
|
{% if last_submitted %}
|
||||||
<span class="pull-right" style="padding-right: 10px;">Submitted <span class="human-time" data-timestamp="{{last_submitted}}">on {{last_submitted}}</span></span>
|
<span class="pull-right" style="padding-right: 10px;"><span class="human-time" data-timestamp="{{last_submitted}}"></span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: none">
|
||||||
|
<pre class="reset-code">{{reset_code}}</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-editor">
|
<div class="code-editor">
|
||||||
@@ -59,20 +62,67 @@
|
|||||||
|
|
||||||
|
|
||||||
{% macro LiveCodeEditorJS(name, code) %}
|
{% macro LiveCodeEditorJS(name, code) %}
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// comment_when is failing because of this
|
||||||
|
if (!frappe.sys_defaults) {
|
||||||
|
frappe.sys_defaults = {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
||||||
<script type="text/javascript" src="/assets/community/js/livecode-canvas.js"></script>
|
<script type="text/javascript" src="/assets/community/js/livecode-canvas.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var livecodeEditors = [];
|
var livecodeEditors = [];
|
||||||
|
var livecodeEditorsMap = {};
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$(".livecode-editor").each((i, e) => {
|
$(".livecode-editor").each((i, e) => {
|
||||||
|
var name = e.id.replace("editor-", "");
|
||||||
var editor = new LiveCodeEditor(e, {
|
var editor = new LiveCodeEditor(e, {
|
||||||
base_url: "{{ livecode_url }}",
|
base_url: "{{ livecode_url }}",
|
||||||
...getLiveCodeOptions()
|
...getLiveCodeOptions()
|
||||||
})
|
})
|
||||||
livecodeEditors.push(editor);
|
livecodeEditors.push(editor);
|
||||||
})
|
livecodeEditorsMap[e.id] = editor;
|
||||||
})
|
|
||||||
|
$(e).find(".reset").on('click', function() {
|
||||||
|
let code = $(e).find(".reset-code").html();
|
||||||
|
editor.codemirror.doc.setValue(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(e).find(".submit").on('click', function() {
|
||||||
|
let code = editor.codemirror.doc.getValue();
|
||||||
|
console.log("submit", name, code);
|
||||||
|
frappe.call("community.lms.api.submit_solution", {
|
||||||
|
"exercise": name,
|
||||||
|
"code": code
|
||||||
|
}).then(r => {
|
||||||
|
if (r.message.name) {
|
||||||
|
frappe.msgprint("Submitted successfully!");
|
||||||
|
|
||||||
|
let d = r.message.creation;
|
||||||
|
$(e).find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateSubmitTimes() {
|
||||||
|
$(".human-time").each(function(i, e) {
|
||||||
|
var d = $(e).data().timestamp;
|
||||||
|
$(e).html(__("Submitted {0}", [comment_when(d)]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSubmitTimes();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
Reference in New Issue
Block a user