feat: review card style

This commit is contained in:
pateljannat
2021-06-29 12:58:12 +05:30
18 changed files with 460 additions and 23 deletions

View File

@@ -165,7 +165,8 @@ whitelist = [
"/add-a-new-batch",
"/new-sign-up",
"/message",
"/about"
"/about",
"/lms-course-review"
]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -187,6 +188,10 @@ update_website_context = 'community.widgets.update_website_context'
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
community_lesson_page_extensions = [
"community.plugins.LiveCodeExtension"
]
## Markdown Macros for Lessons
community_markdown_macro_renderers = {
"Exercise": "community.plugins.exercise_renderer",

View File

@@ -6,12 +6,17 @@
"engine": "InnoDB",
"field_order": [
"exercise",
"solution",
"status",
"batch",
"column_break_4",
"exercise_title",
"course",
"batch",
"lesson",
"image"
"section_break_8",
"solution",
"image",
"test_results",
"comments"
],
"fields": [
{
@@ -21,12 +26,6 @@
"label": "Exercise",
"options": "Exercise"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
@@ -61,11 +60,41 @@
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-21 11:28:45.833018",
"modified": "2021-06-24 16:22:50.570845",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",

View File

@@ -199,7 +199,12 @@ class LMSCourse(Document):
}
if batch:
filters["batch"] = batch
membership = frappe.db.get_value("LMS Batch Membership", filters, ["name","batch", "current_lesson"], as_dict=True)
membership = frappe.db.get_value("LMS Batch Membership",
filters,
["name", "batch", "current_lesson", "member_type"],
as_dict=True)
if membership and membership.batch:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return membership
@@ -244,6 +249,32 @@ class LMSCourse(Document):
def get_tags(self):
return self.tags.split(",") if self.tags else []
def get_reviews(self):
reviews = frappe.get_all("LMS Course Review",
{
"course": self.name
},
["review", "rating", "owner"],
order_by= "creation desc")
for review in reviews:
review.owner_details = frappe.get_doc("User", review.owner)
return reviews
def is_eligible_to_review(self, membership):
""" Checks if user is eligible to review the course """
if not membership:
return False
if frappe.db.count("LMS Course Review",
{
"course": self.name,
"owner": frappe.session.user
}):
return False
return True
def get_outline(self):
return CourseOutline(self)

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Course Review', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,54 @@
{
"actions": [],
"creation": "2021-06-28 13:36:36.146718",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"review",
"rating",
"course"
],
"fields": [
{
"fieldname": "review",
"fieldtype": "Small Text",
"label": "Review"
},
{
"fieldname": "rating",
"fieldtype": "Rating",
"label": "Rating"
},
{
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-28 15:00:35.146196",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Review",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSCourseReview(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSCourseReview(unittest.TestCase):
pass

View File

@@ -0,0 +1,64 @@
<div>
<div class="reviews-heading">
<div class="reviews-title mb-5">Review</div>
{% if course.is_eligible_to_review(membership) %}
<a href="/lms-course-review?new=1">
Provide your Feedback
</a>
{% endif %}
</div>
<div>
{% for review in course.get_reviews() %}
<div class="common-card-style review-card">
<div class="review-content"> {{ review.review }} </div>
<div class="card-divider"></div>
<div class="review-card-footer">
<div>
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-small") }}
<span class="course-instructor">
{{ course.get_instructor().full_name }}
</span>
</div>
<div class="rating pull-right">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="review-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<iframe src="/lms-course-review?new=1"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<script>
frappe.ready(() => {
frappe.provide('frappe.ui');
console.log(frappe.ui)
})
</script>

View File

@@ -67,6 +67,25 @@ class ProfileTab:
"""
raise NotImplementedError()
class LiveCodeExtension(PageExtension):
def render_header(self):
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
context = {
"livecode_url": livecode_url
}
return frappe.render_template(
"templates/livecode/extension_header.html",
context)
def render_footer(self):
livecode_url = frappe.get_value("LMS Settings", None, "livecode_url")
context = {
"livecode_url": livecode_url
}
return frappe.render_template(
"templates/livecode/extension_footer.html",
context)
def quiz_renderer(quiz_name):
quiz = frappe.get_doc("LMS Quiz", quiz_name)
context = dict(quiz=quiz)

View File

@@ -296,10 +296,13 @@ input[type=checkbox] {
align-items: flex-start;
background: #FFFFFF;
border-radius: 8px;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);
}
.course-card {
width: 352px;
height: 380px;
margin: 0px 16px 32px;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);
}
@media (max-width: 768px) {
@@ -317,13 +320,14 @@ input[type=checkbox] {
.course-card-meta {
font-size: 12px;
line-height: 135%;
margin: 12px 16px 8px;
margin: 16px 0px 8px;
color: var(--muted-text);
height: 15px;
}
.course-card-content {
width: 100%;
padding: 0px 24px 20px;
}
.course-card-title {
@@ -333,17 +337,17 @@ input[type=checkbox] {
letter-spacing: -0.014em;
color: var(--text-color);
align-self: stretch;
margin: 0px 16px 16px;
margin-bottom: 16px;
height: 45px;
}
.card-divider {
border: 1px solid #E2E6E9;
margin: 0px 16px 16px;
margin-bottom: 16px;
}
.course-card-meta-2 {
margin: 0px 16px 16px;
margin-bottom: 16px;
}
.course-instructor {
@@ -362,7 +366,6 @@ input[type=checkbox] {
.view-course-link {
height: 32px;
margin: 0px 16px 16px;
background: var(--button-background);
border-radius: 4px;
font-size: 12px;
@@ -432,3 +435,32 @@ input[type=checkbox] {
.small-margin {
margin-left: 10px;
}
.reviews-heading {
display: flex;
justify-content: space-between;
}
.reviews-title {
font-weight: 600;
font-size: 22px;
line-height: 145%;
}
.review-card {
width: 300px;
height: 200px;
margin: 0px 16px 32px;
padding: 16px;
}
.review-card-footer {
width: 100%;
display: flex;
justify-content: space-between;
}
.review-content {
font-size: 15px;
line-height: 135%;
}

View File

@@ -0,0 +1,168 @@
<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="/assets/mon_school/js/livecode-files.js"></script>
<template id="livecode-template">
<div class="livecode-editor livecode-editor-inline">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="controls">
<button class="run">Run</button>
<div class="exercise-controls pull-right">
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
<button class="submit btn-primary">Submit</button>
</div>
</div>
</div>
</div>
<div class="code-editor">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="code-wrapper">
<textarea class="code"></textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<div class="svg-image" width="300" height="300"></div>
<pre class="output"></pre>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
function getLiveCodeOptions() {
return {
base_url: "{{ livecode_url }}",
runtime: "python",
files: LIVECODE_FILES, // loaded from livecode-files.js
command: ["python", "start.py"],
codemirror: true,
onMessage: {
image: function(editor, msg) {
const element = editor.parent.querySelector(".svg-image");
element.innerHTML = msg.image;
}
}
}
}
$(function() {
var editorLookup = {};
$("pre.example, pre.exercise").each((i, e) => {
var code = $(e).text();
var template = document.querySelector('#livecode-template');
var clone = template.content.cloneNode(true);
$(e)
.wrap('<div></div>')
.hide()
.parent()
.append(clone)
.find("textarea.code")
.val(code);
if ($(e).hasClass("exercise")) {
var last_submitted = $(e).data("last-submitted");
if (last_submitted) {
$(e).parent().find(".last-submitted")
.data("timestamp", last_submitted)
.html(__("Submitted {0}", [comment_when(last_submitted)]));
}
}
else {
$(e).parent().find(".exercise-controls").remove();
}
var editor = new LiveCodeEditor(e.parentElement, {
...getLiveCodeOptions(),
codemirror: true,
onMessage: {
image: function(editor, msg) {
const canvasElement = editor.parent.querySelector("div.svg-image");
canvasElement.innerHTML = msg.image;
}
}
});
$(e).parent().find(".submit").on('click', function() {
var name = $(e).data("name");
let code = editor.codemirror.doc.getValue();
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).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
}
});
});
});
$(".exercise-image").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).html(svg);
});
$("pre.exercise").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).parent().find(".svg-image").html(svg);
});
});
</script>
<style type="text/css">
.svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 310px;
height: 310px;
}
.livecode-editor {
margin-bottom: 30px;
}
.livecode-editor-small .svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 210px;
height: 210px;
}
/* work-in-progress styles for showing admonition */
.admonition {
border: 1px solid #aaa;
border-left: .5rem solid #888;
border-radius: .3em;
font-size: 0.9em;
margin: 1.5em 0;
padding: 0 0.5em;
}
.admonition-title {
padding: 0.5em 0px;
font-weight: bold;
padding-top:
}
</style>

View File

@@ -0,0 +1,8 @@
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>

View File

@@ -15,7 +15,7 @@
</div>
<div class="d-flex justify-content-between align-items-end">
<h2 id="course-title" data-course="{{course.name}}">{{course.title}}</h2>
{% if not course.disable_self_learning and not course.is_mentor(frappe.session.user) %}
{% if not course.disable_self_learning and not membership %}
<div>
<button class="btn btn-primary join-batch" data-course="{{ course.name | urlencode }}"> Start Learning </button>
</div>
@@ -32,6 +32,7 @@
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
{{ BatchSection(course) }}
{{ widgets.CourseOutline(course=course, show_link=membership) }}
{{ widgets.Reviews(course=course, membership=membership) }}
</div>
</div>
</div>

View File

@@ -20,5 +20,5 @@ def get_context(context):
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
context.membership = membership
if not course.is_mentor(frappe.session.user) and membership:
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
raise frappe.Redirect
""" frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
raise frappe.Redirect """

View File

@@ -28,7 +28,7 @@
{% macro course_card(course) %}
<div class="common-card-style">
<div class="common-card-style course-card">
<div class="course-image" style="background-image: url({{ course.image }});">
<div class="course-tags">
{% for tag in course.get_tags() %}

View File

@@ -1,4 +1,3 @@
{% macro LiveCodeEditorLarge(name, code) %}
<div class="livecode-editor livecode-editor-large" id="editor-{{name}}">
<div class="row">