Compare commits
1 Commits
sketch-cle
...
reviews
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c64d46e99 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,3 +5,6 @@
|
|||||||
tags
|
tags
|
||||||
community/docs/current
|
community/docs/current
|
||||||
community/public/dist
|
community/public/dist
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ whitelist = [
|
|||||||
"/add-a-new-batch",
|
"/add-a-new-batch",
|
||||||
"/new-sign-up",
|
"/new-sign-up",
|
||||||
"/message",
|
"/message",
|
||||||
"/about"
|
"/about",
|
||||||
|
"/lms-course-review"
|
||||||
]
|
]
|
||||||
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
# from ..lms_sketch.livecode import livecode_to_svg
|
||||||
|
|
||||||
class Exercise(Document):
|
class Exercise(Document):
|
||||||
def get_user_submission(self):
|
def get_user_submission(self):
|
||||||
|
|||||||
@@ -2,47 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Lesson', {
|
frappe.ui.form.on('Lesson', {
|
||||||
setup: function (frm) {
|
// refresh: function(frm) {
|
||||||
frm.trigger('setup_help');
|
|
||||||
},
|
|
||||||
setup_help(frm) {
|
|
||||||
frm.get_field('help').html(`
|
|
||||||
<p>You can add some more additional content to the lesson using a special syntax. The table below mentions all types of dynamic content that you can add to the lessons and the syntax for the same.</p>
|
|
||||||
<div class="row font-weight-bold mb-3">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
Content Type
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
Syntax
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
// }
|
||||||
<div class="col-sm-4">
|
|
||||||
YouTube Video
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
{{ YouTubeVideo("unique_embed_id") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
Exercise
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
{{ Exercise("exercise_name") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
Quiz
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
{{ Quiz("lms_quiz_name") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,9 +13,7 @@
|
|||||||
"index_",
|
"index_",
|
||||||
"index_label",
|
"index_label",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"body",
|
"body"
|
||||||
"help_section",
|
|
||||||
"help"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -62,20 +60,11 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "help_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Help"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "help",
|
|
||||||
"fieldtype": "HTML"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-29 13:34:49.077363",
|
"modified": "2021-06-23 17:59:52.946515",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Lesson",
|
"name": "Lesson",
|
||||||
|
|||||||
@@ -71,38 +71,3 @@ def save_message(message, batch):
|
|||||||
"message": message
|
"message": message
|
||||||
})
|
})
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
def switch_batch(course_name, email, batch_name):
|
|
||||||
"""Switches the user from the current batch of the course to a new batch.
|
|
||||||
"""
|
|
||||||
membership = frappe.get_last_doc(
|
|
||||||
"LMS Batch Membership",
|
|
||||||
filters={"course": course_name, "member": email})
|
|
||||||
|
|
||||||
batch = frappe.get_doc("LMS Batch", batch_name)
|
|
||||||
if not batch:
|
|
||||||
raise ValueError(f"Invalid Batch: {batch_name}")
|
|
||||||
|
|
||||||
if batch.course != course_name:
|
|
||||||
raise ValueError("Can not switch batches across courses")
|
|
||||||
|
|
||||||
if batch.is_member(email):
|
|
||||||
print(f"{email} is already a member of {batch.title}")
|
|
||||||
return
|
|
||||||
|
|
||||||
old_batch = frappe.get_doc("LMS Batch", membership.batch)
|
|
||||||
|
|
||||||
print("updating membership", membership.name)
|
|
||||||
membership.batch = batch_name
|
|
||||||
membership.save()
|
|
||||||
|
|
||||||
# update exercise submissions
|
|
||||||
filters = {
|
|
||||||
"owner": email,
|
|
||||||
"batch": old_batch.name
|
|
||||||
}
|
|
||||||
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck='name'):
|
|
||||||
doc = frappe.get_doc("Exercise Submission", name)
|
|
||||||
print("updating exercise submission", name)
|
|
||||||
doc.batch = batch_name
|
|
||||||
doc.save()
|
|
||||||
|
|||||||
@@ -199,7 +199,12 @@ class LMSCourse(Document):
|
|||||||
}
|
}
|
||||||
if batch:
|
if batch:
|
||||||
filters["batch"] = 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:
|
if membership and membership.batch:
|
||||||
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
|
||||||
return membership
|
return membership
|
||||||
@@ -241,6 +246,32 @@ class LMSCourse(Document):
|
|||||||
member_names = [m['member'] for m in memberships]
|
member_names = [m['member'] for m in memberships]
|
||||||
return find_all("User", name=["IN", member_names])
|
return find_all("User", name=["IN", member_names])
|
||||||
|
|
||||||
|
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_eligibleto_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):
|
def get_outline(self):
|
||||||
return CourseOutline(self)
|
return CourseOutline(self)
|
||||||
|
|
||||||
|
|||||||
0
community/lms/doctype/lms_course_review/__init__.py
Normal file
0
community/lms/doctype/lms_course_review/__init__.py
Normal 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) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestLMSCourseReview(unittest.TestCase):
|
||||||
|
pass
|
||||||
0
community/lms/doctype/lms_sketch/__init__.py
Normal file
0
community/lms/doctype/lms_sketch/__init__.py
Normal file
126
community/lms/doctype/lms_sketch/livecode.py
Normal file
126
community/lms/doctype/lms_sketch/livecode.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""Utilities to work with livecode service.
|
||||||
|
"""
|
||||||
|
import websocket
|
||||||
|
import json
|
||||||
|
from .svg import SVG
|
||||||
|
import frappe
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# Files to pass to livecode server
|
||||||
|
# The same code is part of livecode-canvas.js
|
||||||
|
# TODO: generate livecode-canvas.js from this file
|
||||||
|
START = '''
|
||||||
|
import sketch
|
||||||
|
code = open("main.py").read()
|
||||||
|
env = dict(sketch.__dict__)
|
||||||
|
exec(code, env)
|
||||||
|
'''
|
||||||
|
|
||||||
|
SKETCH = '''
|
||||||
|
import json
|
||||||
|
|
||||||
|
def sendmsg(msgtype, function, args):
|
||||||
|
"""Sends a message to the frontend.
|
||||||
|
|
||||||
|
The frontend will receive the specified message whenever
|
||||||
|
this function is called. The frontend can decide to some
|
||||||
|
action on each of these messages.
|
||||||
|
"""
|
||||||
|
msg = dict(msgtype=msgtype, function=function, args=args)
|
||||||
|
print("--MSG--", json.dumps(msg))
|
||||||
|
|
||||||
|
def _draw(func, **kwargs):
|
||||||
|
sendmsg(msgtype="draw", function=func, args=kwargs)
|
||||||
|
|
||||||
|
def circle(x, y, d):
|
||||||
|
"""Draws a circle of diameter d with center (x, y).
|
||||||
|
"""
|
||||||
|
_draw("circle", x=x, y=y, d=d)
|
||||||
|
|
||||||
|
def line(x1, y1, x2, y2):
|
||||||
|
"""Draws a line from point (x1, y1) to point (x2, y2).
|
||||||
|
"""
|
||||||
|
_draw("line", x1=x1, y1=y1, x2=x2, y2=y2)
|
||||||
|
|
||||||
|
def rect(x, y, w, h):
|
||||||
|
"""Draws a rectangle on the canvas.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
x: x coordinate of the top-left corner of the rectangle
|
||||||
|
y: y coordinate of the top-left corner of the rectangle
|
||||||
|
w: width of the rectangle
|
||||||
|
h: height of the rectangle
|
||||||
|
"""
|
||||||
|
_draw("rect", x=x, y=y, w=w, h=h)
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
_draw("clear")
|
||||||
|
|
||||||
|
# clear the canvas on start
|
||||||
|
clear()
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_livecode_url():
|
||||||
|
doc = frappe.get_cached_doc("LMS Settings")
|
||||||
|
return doc.livecode_url
|
||||||
|
|
||||||
|
def get_livecode_ws_url():
|
||||||
|
url = urlparse(get_livecode_url())
|
||||||
|
protocol = "wss" if url.scheme == "https" else "ws"
|
||||||
|
return protocol + "://" + url.netloc + "/livecode"
|
||||||
|
|
||||||
|
def livecode_to_svg(livecode_ws_url, code, *, timeout=3):
|
||||||
|
"""Renders the code as svg.
|
||||||
|
"""
|
||||||
|
if livecode_ws_url is None:
|
||||||
|
livecode_ws_url = get_livecode_ws_url()
|
||||||
|
|
||||||
|
try:
|
||||||
|
ws = websocket.WebSocket()
|
||||||
|
ws.settimeout(timeout)
|
||||||
|
ws.connect(livecode_ws_url)
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
"msgtype": "exec",
|
||||||
|
"runtime": "python",
|
||||||
|
"code": code,
|
||||||
|
"files": [
|
||||||
|
{"filename": "start.py", "contents": START},
|
||||||
|
{"filename": "sketch.py", "contents": SKETCH},
|
||||||
|
],
|
||||||
|
"command": ["python", "start.py"]
|
||||||
|
}
|
||||||
|
ws.send(json.dumps(msg))
|
||||||
|
|
||||||
|
messages = _read_messages(ws)
|
||||||
|
commands = [m for m in messages if m['msgtype'] == 'draw']
|
||||||
|
img = draw_image(commands)
|
||||||
|
return img.tostring()
|
||||||
|
except websocket.WebSocketException as e:
|
||||||
|
frappe.log_error(frappe.get_traceback(), 'livecode_to_svg failed')
|
||||||
|
|
||||||
|
def _read_messages(ws):
|
||||||
|
messages = []
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = ws.recv()
|
||||||
|
if not msg:
|
||||||
|
break
|
||||||
|
messages.append(json.loads(msg))
|
||||||
|
except websocket.WebSocketTimeoutException as e:
|
||||||
|
print("Error:", e)
|
||||||
|
pass
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def draw_image(commands):
|
||||||
|
img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black')
|
||||||
|
for c in commands:
|
||||||
|
args = c['args']
|
||||||
|
if c['function'] == 'circle':
|
||||||
|
img.circle(cx=args['x'], cy=args['y'], r=args['d']/2)
|
||||||
|
elif c['function'] == 'line':
|
||||||
|
img.line(x1=args['x1'], y1=args['y1'], x2=args['x2'], y2=args['y2'])
|
||||||
|
elif c['function'] == 'rect':
|
||||||
|
img.rect(x=args['x'], y=args['y'], width=args['w'], height=args['h'])
|
||||||
|
return img
|
||||||
8
community/lms/doctype/lms_sketch/lms_sketch.js
Normal file
8
community/lms/doctype/lms_sketch/lms_sketch.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, FOSS United and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('LMS Sketch', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
62
community/lms/doctype/lms_sketch/lms_sketch.json
Normal file
62
community/lms/doctype/lms_sketch/lms_sketch.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "format:SKETCH-{#}",
|
||||||
|
"creation": "2021-03-09 16:31:50.523524",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"title",
|
||||||
|
"runtime",
|
||||||
|
"code",
|
||||||
|
"svg"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "runtime",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Runtime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "code",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "svg",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "SVG",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-03-12 08:42:56.671658",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "LMS",
|
||||||
|
"name": "LMS Sketch",
|
||||||
|
"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,
|
||||||
|
"track_views": 1
|
||||||
|
}
|
||||||
105
community/lms/doctype/lms_sketch/lms_sketch.py
Normal file
105
community/lms/doctype/lms_sketch/lms_sketch.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, FOSS United and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import hashlib
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from . import livecode
|
||||||
|
|
||||||
|
DEFAULT_IMAGE = """
|
||||||
|
<svg viewBox="0 0 300 300" width="300" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class LMSSketch(Document):
|
||||||
|
@property
|
||||||
|
def sketch_id(self):
|
||||||
|
"""Returns the numeric part of the name.
|
||||||
|
|
||||||
|
For example, the skech_id will be "123" for sketch with name "SKETCH-123".
|
||||||
|
"""
|
||||||
|
return self.name.replace("SKETCH-", "")
|
||||||
|
|
||||||
|
def get_owner(self):
|
||||||
|
"""Returns the owner of this sketch as a document.
|
||||||
|
"""
|
||||||
|
return frappe.get_doc("User", self.owner)
|
||||||
|
|
||||||
|
def get_owner_name(self):
|
||||||
|
return self.get_owner().full_name
|
||||||
|
|
||||||
|
def get_livecode_url(self):
|
||||||
|
doc = frappe.get_cached_doc("LMS Settings")
|
||||||
|
return doc.livecode_url
|
||||||
|
|
||||||
|
def get_livecode_ws_url(self):
|
||||||
|
url = urlparse(self.get_livecode_url())
|
||||||
|
protocol = "wss" if url.scheme == "https" else "ws"
|
||||||
|
return protocol + "://" + url.netloc + "/livecode"
|
||||||
|
|
||||||
|
def to_svg(self):
|
||||||
|
return self.svg or self.render_svg()
|
||||||
|
|
||||||
|
def render_svg(self):
|
||||||
|
h = hashlib.md5(self.code.encode('utf-8')).hexdigest()
|
||||||
|
cache = frappe.cache()
|
||||||
|
key = "sketch-" + h
|
||||||
|
value = cache.get(key)
|
||||||
|
if value:
|
||||||
|
value = value.decode('utf-8')
|
||||||
|
else:
|
||||||
|
ws_url = self.get_livecode_ws_url()
|
||||||
|
value = livecode.livecode_to_svg(ws_url, self.code)
|
||||||
|
if value:
|
||||||
|
cache.set(key, value)
|
||||||
|
return value or DEFAULT_IMAGE
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_recent_sketches(limit=100, owner=None):
|
||||||
|
"""Returns the recent sketches.
|
||||||
|
"""
|
||||||
|
filters = {}
|
||||||
|
if owner:
|
||||||
|
filters = {"owner": owner}
|
||||||
|
sketches = frappe.get_all(
|
||||||
|
"LMS Sketch",
|
||||||
|
filters=filters,
|
||||||
|
fields='*',
|
||||||
|
order_by='modified desc',
|
||||||
|
page_length=limit
|
||||||
|
)
|
||||||
|
return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LMSSketch {self.name}>"
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def save_sketch(name, title, code):
|
||||||
|
if not name or name == "new":
|
||||||
|
doc = frappe.new_doc('LMS Sketch')
|
||||||
|
doc.title = title
|
||||||
|
doc.code = code
|
||||||
|
doc.runtime = 'python-canvas'
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
status = "created"
|
||||||
|
else:
|
||||||
|
doc = frappe.get_doc("LMS Sketch", name)
|
||||||
|
|
||||||
|
if doc.owner != frappe.session.user:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"error": "Permission Denied"
|
||||||
|
}
|
||||||
|
doc.title = title
|
||||||
|
doc.code = code
|
||||||
|
doc.svg = ''
|
||||||
|
doc.save()
|
||||||
|
status = "updated"
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"status": status,
|
||||||
|
"name": doc.name,
|
||||||
|
}
|
||||||
143
community/lms/doctype/lms_sketch/svg.py
Normal file
143
community/lms/doctype/lms_sketch/svg.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""SVG rendering library.
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
from svg import SVG
|
||||||
|
|
||||||
|
svg = SVG(width=200, height=200)
|
||||||
|
svg.circle(cx=100, cy=200, r=50)
|
||||||
|
print(svg.tostring())
|
||||||
|
"""
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
TAGNAMES = set([
|
||||||
|
"circle", "ellipse",
|
||||||
|
"line", "path", "rect", "polygon", "polyline",
|
||||||
|
"text", "textPath", "title",
|
||||||
|
"marker", "defs",
|
||||||
|
"g"
|
||||||
|
])
|
||||||
|
|
||||||
|
class Node:
|
||||||
|
"""SVG Node"""
|
||||||
|
def __init__(self, tag, **attrs):
|
||||||
|
self.tag = tag
|
||||||
|
self.attrs = dict((k.replace('_', '-'), str(v)) for k, v in attrs.items())
|
||||||
|
self.children = []
|
||||||
|
|
||||||
|
def node(self, tag, **attrs):
|
||||||
|
n = Node(tag, **attrs)
|
||||||
|
self.children.append(n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
def apply(self, func):
|
||||||
|
"""Applies a function to this node and
|
||||||
|
all the children recursively.
|
||||||
|
"""
|
||||||
|
func(self)
|
||||||
|
for n in self.children:
|
||||||
|
n.apply(func)
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
node = Node(self.tag, **self.attrs)
|
||||||
|
node.children = [n.clone() for n in self.children]
|
||||||
|
return node
|
||||||
|
|
||||||
|
def add_node(self, node):
|
||||||
|
if not isinstance(node, Node):
|
||||||
|
node = Text(node)
|
||||||
|
self.children.append(node)
|
||||||
|
|
||||||
|
def __getattr__(self, tag):
|
||||||
|
if tag not in TAGNAMES:
|
||||||
|
raise AttributeError(tag)
|
||||||
|
return lambda **attrs: self.node(tag, **attrs)
|
||||||
|
|
||||||
|
def translate(self, x, y):
|
||||||
|
return self.g(transform="translate(%s, %s)" % (x, y))
|
||||||
|
|
||||||
|
def scale(self, *args):
|
||||||
|
return self.g(transform="scale(%s)" % ", ".join(str(a) for a in args))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s .../>" % self.tag
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def build_tree(self, builder):
|
||||||
|
builder.start(self.tag, self.attrs)
|
||||||
|
for node in self.children:
|
||||||
|
node.build_tree(builder)
|
||||||
|
return builder.end(self.tag)
|
||||||
|
|
||||||
|
def _indent(self, elem, level=0):
|
||||||
|
"""Indent etree node for prettyprinting."""
|
||||||
|
|
||||||
|
i = "\n" + level*" "
|
||||||
|
if len(elem):
|
||||||
|
if not elem.text or not elem.text.strip():
|
||||||
|
elem.text = i + " "
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
for elem in elem:
|
||||||
|
self._indent(elem, level+1)
|
||||||
|
if not elem.tail or not elem.tail.strip():
|
||||||
|
elem.tail = i
|
||||||
|
else:
|
||||||
|
if level and (not elem.tail or not elem.tail.strip()):
|
||||||
|
elem.tail = i
|
||||||
|
|
||||||
|
def save(self, filename, encoding='utf-8'):
|
||||||
|
f = open(filename, 'w')
|
||||||
|
f.write(self.tostring())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
def tostring(self, encoding='utf-8'):
|
||||||
|
builder = ElementTree.TreeBuilder()
|
||||||
|
self.build_tree(builder)
|
||||||
|
e = builder.close()
|
||||||
|
self._indent(e)
|
||||||
|
return ElementTree.tostring(e, encoding).decode(encoding)
|
||||||
|
|
||||||
|
class Text(Node):
|
||||||
|
"""Text Node
|
||||||
|
|
||||||
|
>>> p = Node("p")
|
||||||
|
>>> p.add_node("hello, world!")
|
||||||
|
>>> p.tostring()
|
||||||
|
'<p>hello, world!</p>'
|
||||||
|
"""
|
||||||
|
def __init__(self, text):
|
||||||
|
Node.__init__(self, "__text__")
|
||||||
|
self._text = text
|
||||||
|
|
||||||
|
def build_tree(self, builder):
|
||||||
|
builder.data(str(self._text))
|
||||||
|
|
||||||
|
class SVG(Node):
|
||||||
|
"""
|
||||||
|
>>> svg = SVG(width=200, height=200)
|
||||||
|
>>> svg.rect(x=0, y=0, width=200, height=200, fill="blue")
|
||||||
|
<rect .../>
|
||||||
|
>>> with svg.translate(-50, -50) as g:
|
||||||
|
... g.rect(x=0, y=0, width=50, height=100, fill="red")
|
||||||
|
... g.rect(x=50, y=0, width=50, height=100, fill="green")
|
||||||
|
<rect .../>
|
||||||
|
<rect .../>
|
||||||
|
>>> print(svg.tostring())
|
||||||
|
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0" y="0" width="200" height="200" fill="blue" />
|
||||||
|
<g transform="translate(-50, -50)">
|
||||||
|
<rect x="0" y="0" width="50" height="100" fill="red" />
|
||||||
|
<rect x="50" y="0" width="50" height="100" fill="green" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, **attrs):
|
||||||
|
attrs['xmlns'] = "http://www.w3.org/2000/svg"
|
||||||
|
Node.__init__(self, 'svg', **attrs)
|
||||||
|
|
||||||
10
community/lms/doctype/lms_sketch/test_lms_sketch.py
Normal file
10
community/lms/doctype/lms_sketch/test_lms_sketch.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, FOSS United and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestLMSSketch(unittest.TestCase):
|
||||||
|
pass
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Handy module to make access to all doctypes from a single place.
|
"""Handy module to make access to all doctypes from a single place.
|
||||||
"""
|
"""
|
||||||
from .doctype.lms_course.lms_course import LMSCourse as Course
|
from .doctype.lms_course.lms_course import LMSCourse as Course
|
||||||
|
from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch
|
||||||
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership
|
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership
|
||||||
|
|||||||
61
community/lms/widgets/Reviews.html
Normal file
61
community/lms/widgets/Reviews.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<div>
|
||||||
|
<div class="reviews-heading">
|
||||||
|
<div class="reviews-title">Review</div>
|
||||||
|
{% if course.is_eligibleto_review(membership) %}
|
||||||
|
<a href="/lms-course-review?new=1">
|
||||||
|
Provide your Feedback
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% for review in course.get_reviews() %}
|
||||||
|
<div class="review-card">
|
||||||
|
<div class="review-content"> {{ review.review }} </div>
|
||||||
|
<div class="review-card-footer">
|
||||||
|
<div>
|
||||||
|
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }} <a
|
||||||
|
href="/{{review.owner_details.username}}">{{ review.owner_details.full_name }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="rating">
|
||||||
|
{% 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">×</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>
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
{
|
|
||||||
"category": "Modules",
|
|
||||||
"charts": [],
|
|
||||||
"creation": "2021-06-29 13:05:28.741459",
|
|
||||||
"developer_mode_only": 0,
|
|
||||||
"disable_user_customization": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workspace",
|
|
||||||
"extends_another_page": 0,
|
|
||||||
"hide_custom": 0,
|
|
||||||
"icon": "education",
|
|
||||||
"idx": 0,
|
|
||||||
"is_default": 0,
|
|
||||||
"is_standard": 1,
|
|
||||||
"label": "LMS",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Course",
|
|
||||||
"link_to": "LMS Course",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Batch",
|
|
||||||
"link_to": "LMS Batch",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Batch Membership",
|
|
||||||
"link_to": "LMS Batch Membership",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Course Mentor Mapping",
|
|
||||||
"link_to": "LMS Course Mentor Mapping",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Content",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Chapter",
|
|
||||||
"link_to": "Chapter",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Lesson",
|
|
||||||
"link_to": "Lesson",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Exercise",
|
|
||||||
"link_to": "Exercise",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Exercise Submission",
|
|
||||||
"link_to": "Exercise Submission",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Quiz",
|
|
||||||
"link_to": "LMS Quiz",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "LMS Quiz Submission",
|
|
||||||
"link_to": "LMS Quiz Submission",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modified": "2021-06-29 15:11:07.324651",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "LMS",
|
|
||||||
"name": "LMS",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"pin_to_bottom": 0,
|
|
||||||
"pin_to_top": 1,
|
|
||||||
"shortcuts": [
|
|
||||||
{
|
|
||||||
"color": "#29CD42",
|
|
||||||
"doc_view": "List",
|
|
||||||
"format": "{} Published",
|
|
||||||
"label": "Courses",
|
|
||||||
"link_to": "LMS Course",
|
|
||||||
"stats_filter": "{\"is_published\": 1}",
|
|
||||||
"type": "DocType"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#29CD42",
|
|
||||||
"doc_view": "List",
|
|
||||||
"format": "{} Active ",
|
|
||||||
"label": "Batches",
|
|
||||||
"link_to": "LMS Batch",
|
|
||||||
"stats_filter": "{\"status\": \"Active\"}",
|
|
||||||
"type": "DocType"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#39E4A5",
|
|
||||||
"doc_view": "List",
|
|
||||||
"format": "{} Students",
|
|
||||||
"label": "Memberships",
|
|
||||||
"link_to": "LMS Batch Membership",
|
|
||||||
"stats_filter": "{\"member_type\": \"Student\"}",
|
|
||||||
"type": "DocType"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
37
community/templates/pages/reviews.js
Normal file
37
community/templates/pages/reviews.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
frappe.ready(() => {
|
||||||
|
$("#provide-feedback").click((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
let d = new frappe.ui.Dialog({
|
||||||
|
title: 'Provide your Feedback',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Rating',
|
||||||
|
fieldname: 'rating',
|
||||||
|
fieldtype: 'Rating'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Review',
|
||||||
|
fieldname: 'review',
|
||||||
|
fieldtype: 'Small Text'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action_label: 'Submit',
|
||||||
|
primary_action(values) {
|
||||||
|
console.log(values);
|
||||||
|
frappe.call({
|
||||||
|
method: "community.lms.doctype.lms_course.lms_course",
|
||||||
|
args: {
|
||||||
|
course: "{{ course.name }}",
|
||||||
|
rating: values.rating,
|
||||||
|
review: values.review
|
||||||
|
},
|
||||||
|
callback: (data) => {
|
||||||
|
d.hide();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
})
|
||||||
|
})
|
||||||
0
community/www/__init__.py
Normal file
0
community/www/__init__.py
Normal file
@@ -61,11 +61,6 @@
|
|||||||
|
|
||||||
{%- block script %}
|
{%- block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var page_context = {{ page_context | tojson }};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% for ext in page_extensions %}
|
{% for ext in page_extensions %}
|
||||||
{{ ext.render_footer() }}
|
{{ ext.render_footer() }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ def get_context(context):
|
|||||||
|
|
||||||
context.page_extensions = get_page_extensions()
|
context.page_extensions = get_page_extensions()
|
||||||
|
|
||||||
context.page_context = {
|
|
||||||
"course": context.course.name,
|
|
||||||
"batch": context.get("batch") and context.batch.name,
|
|
||||||
"lesson": context.lesson.name
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_chapter_title(course_name, lesson_number):
|
def get_chapter_title(course_name, lesson_number):
|
||||||
if not lesson_number:
|
if not lesson_number:
|
||||||
return
|
return
|
||||||
|
|||||||
0
community/www/courses/__init__.py
Normal file
0
community/www/courses/__init__.py
Normal file
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between align-items-end">
|
<div class="d-flex justify-content-between align-items-end">
|
||||||
<h2 id="course-title" data-course="{{course.name}}">{{course.title}}</h2>
|
<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>
|
<div>
|
||||||
<button class="btn btn-primary join-batch" data-course="{{ course.name | urlencode }}"> Start Learning </button>
|
<button class="btn btn-primary join-batch" data-course="{{ course.name | urlencode }}"> Start Learning </button>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
|
{{ widgets.InstructorSection(instructor=course.get_instructor()) }}
|
||||||
{{ BatchSection(course) }}
|
{{ BatchSection(course) }}
|
||||||
{{ widgets.CourseOutline(course=course, show_link=membership) }}
|
{{ widgets.CourseOutline(course=course, show_link=membership) }}
|
||||||
|
{{ widgets.Reviews(course=course, membership=membership) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,5 +20,5 @@ def get_context(context):
|
|||||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||||
context.membership = membership
|
context.membership = membership
|
||||||
if not course.is_mentor(frappe.session.user) and membership:
|
if not course.is_mentor(frappe.session.user) and membership:
|
||||||
frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
|
""" frappe.local.flags.redirect_location = f"/courses/{course.name}/learn"
|
||||||
raise frappe.Redirect
|
raise frappe.Redirect """
|
||||||
|
|||||||
0
community/www/dashboard/__init__.py
Normal file
0
community/www/dashboard/__init__.py
Normal file
181
community/www/dashboard/index.html
Normal file
181
community/www/dashboard/index.html
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
{% extends "templates/base.html" %}
|
||||||
|
{% block title %}{{ _("Community") }}{% endblock %}
|
||||||
|
{% block head_include %}
|
||||||
|
<meta name="description" content="{{ 'Community' }}" />
|
||||||
|
<meta name="keywords" content="An app that supports Communities." />
|
||||||
|
<style>
|
||||||
|
section {
|
||||||
|
padding: 2rem;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__parent {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__name {
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__details {
|
||||||
|
padding-top: 2rem;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__course {
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
width: 48%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__courseHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 50px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__badge {
|
||||||
|
background: #D6D6FF;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #1712FE;
|
||||||
|
padding: 0.5rem;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__description {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section>
|
||||||
|
<div class="dashboard__parent">
|
||||||
|
<div class="mr-5">
|
||||||
|
{{ widgets.Avatar(member=member, avatar_class="avatar-xl") }}
|
||||||
|
</div>
|
||||||
|
<div class="dashboard__details">
|
||||||
|
<div class="dashboard__name">
|
||||||
|
<a class="anchor_style" href="/{{member.username}}">{{ member.full_name }}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab"
|
||||||
|
aria-controls="home" aria-selected="true">Activity</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="sketches-tab" data-toggle="tab" href="#sketches" role="tab"
|
||||||
|
aria-controls="sketches" aria-selected="false">Sketches</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="courses-tab" data-toggle="tab" href="#courses" role="tab"
|
||||||
|
aria-controls="courses" aria-selected="false">Courses</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
|
||||||
|
<div class='container'>
|
||||||
|
{% if activity %}
|
||||||
|
{% for message in activity %}
|
||||||
|
<div class="dashboard__message border m-5 p-3">
|
||||||
|
<a class="anchor_style bold" href="/{{message.member.username}}">{{ message.member.full_name }}</a>
|
||||||
|
<div class="text-muted float-right">
|
||||||
|
{{ message.course }} ({{message.batch}})
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center w-100">
|
||||||
|
<div>
|
||||||
|
{{ widgets.Avatar(member=message.member, avatar_class="avatar-medium") }}
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 mt-5">{{ frappe.utils.md_to_html(message.message) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="text-muted float-right">
|
||||||
|
{{ frappe.utils.pretty_date(message.creation) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center">You have not received any messages yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade py-4" role="tabpanel" id="sketches">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/sketches/new">Create a New Sketch</a>
|
||||||
|
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2">
|
||||||
|
{% if sketches %}
|
||||||
|
{% for sketch in sketches %}
|
||||||
|
<div class="col mb-4">
|
||||||
|
<div class="card sketch-card" style="width: 200px;">
|
||||||
|
<div class="card-img-top">
|
||||||
|
<a href="/sketches/{{sketch.sketch_id}}">
|
||||||
|
{{ sketch.to_svg() }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="sketch-title">
|
||||||
|
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="sketch-author">
|
||||||
|
by {{sketch.get_owner_name()}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if not sketches %}
|
||||||
|
<p class="text-center">You have not created any sketches.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade py-4" role="tabpanel" id="courses">
|
||||||
|
{% if courses %}
|
||||||
|
<p class="ml-3">Your Courses</p>
|
||||||
|
<div class="d-flex flex-wrap">
|
||||||
|
{% for course in courses %}
|
||||||
|
<div class="dashboard__course">
|
||||||
|
<div class="dashboard__courseHeader">
|
||||||
|
<a class="text-decoration-none" target="_blank" href="/courses/{{course.name}}">
|
||||||
|
<h5 class="w-75">{{ course.title }}</h5>
|
||||||
|
</a>
|
||||||
|
{% if course.member_type %}
|
||||||
|
<div class="dashboard__badge">
|
||||||
|
{{ course.member_type }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="dashboard__description">
|
||||||
|
{{ frappe.utils.md_to_html(course.description) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted"> Joined {{ frappe.utils.pretty_date(course.joining) }} </div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center">You are not a member of any course yet. Checkout our <a
|
||||||
|
href="/courses" target="_blank">Courses</a>.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
42
community/www/dashboard/index.py
Normal file
42
community/www/dashboard/index.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import frappe
|
||||||
|
from community.lms.models import Sketch
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
context.no_cache = 1
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
frappe.local.flags.redirect_location = "/login"
|
||||||
|
raise frappe.Redirect
|
||||||
|
context.member = frappe.get_doc("User", frappe.session.user)
|
||||||
|
context.memberships = get_memberships()
|
||||||
|
context.courses = get_courses(context.memberships)
|
||||||
|
context.activity = get_activity(context.memberships)
|
||||||
|
context.sketches = list(filter(lambda x: x.owner == frappe.session.user, Sketch.get_recent_sketches(owner=context.member.email)))
|
||||||
|
|
||||||
|
def get_memberships():
|
||||||
|
return frappe.get_all("LMS Batch Membership", {"member": frappe.session.user}, ["batch", "member_type", "creation"])
|
||||||
|
|
||||||
|
def get_courses(memberships):
|
||||||
|
courses = []
|
||||||
|
for membership in memberships:
|
||||||
|
course = frappe.db.get_value("LMS Batch", membership.batch, "course")
|
||||||
|
course_details = frappe.get_doc("LMS Course", course)
|
||||||
|
course_in_list = list(filter(lambda x: x.name == course_details.name, courses))
|
||||||
|
if not len(course_in_list):
|
||||||
|
course_details.description = course_details.description[0:100] + "..."
|
||||||
|
course_details.joining = membership.creation
|
||||||
|
if membership.member_type != "Student":
|
||||||
|
course_details.member_type = membership.member_type
|
||||||
|
courses.append(course_details)
|
||||||
|
return courses
|
||||||
|
|
||||||
|
def get_activity(memberships):
|
||||||
|
messages, courses = [], {}
|
||||||
|
batches = [x.batch for x in memberships]
|
||||||
|
for batch in batches:
|
||||||
|
courses[batch] = frappe.db.get_value("LMS Batch", batch, "course")
|
||||||
|
messages = frappe.get_all("LMS Message", {"batch": ["in", ",".join(batches)]}, ["message", "author", "creation", "batch"], order_by='creation desc')
|
||||||
|
for message in messages:
|
||||||
|
message.course = courses[message.batch]
|
||||||
|
message.member = frappe.get_doc("User", message.author)
|
||||||
|
return messages
|
||||||
0
community/www/hackathons/__init__.py
Normal file
0
community/www/hackathons/__init__.py
Normal file
0
community/www/hackathons/macros/__init__.py
Normal file
0
community/www/hackathons/macros/__init__.py
Normal file
0
community/www/home/__init__.py
Normal file
0
community/www/home/__init__.py
Normal file
49
community/www/home/index.html
Normal file
49
community/www/home/index.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% extends "templates/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ HeroSection() }}
|
||||||
|
{{ ExploreCourses(courses) }}
|
||||||
|
{{ RecentSketches(recent_sketches) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% macro HeroSection() %}
|
||||||
|
<section id="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1 class="display-4">Guided online programming courses, with a <br />mentor at your back.</h1>
|
||||||
|
<p class="lead">Hands-on programming courses designed by experts, delivered by passionate mentors.</p>
|
||||||
|
{{ widgets.RequestInvite() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro ExploreCourses(courses) %}
|
||||||
|
<section id="explore-courses" class="lightgray">
|
||||||
|
<div class="container lightgray">
|
||||||
|
<h2>Explore Courses</h2>
|
||||||
|
<div class="row">
|
||||||
|
{% for course in courses %}
|
||||||
|
<div class="col-md-6">
|
||||||
|
{{ widgets.CourseTeaser(course=course) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro RecentSketches(sketches) %}
|
||||||
|
<section id="recet-sketches">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Recent Sketches</h2>
|
||||||
|
<div class="row">
|
||||||
|
{% for sketch in sketches %}
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ widgets.SketchTeaser(sketch=sketch) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endmacro %}
|
||||||
7
community/www/home/index.py
Normal file
7
community/www/home/index.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
from community.lms.models import Course, Sketch
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
context.no_cache = 1
|
||||||
|
context.courses = Course.find_all()
|
||||||
|
context.recent_sketches = Sketch.get_recent_sketches(limit=8)
|
||||||
0
community/www/macros/__init__.py
Normal file
0
community/www/macros/__init__.py
Normal file
0
community/www/my-courses/__init__.py
Normal file
0
community/www/my-courses/__init__.py
Normal file
0
community/www/profiles/__init__.py
Normal file
0
community/www/profiles/__init__.py
Normal file
0
community/www/sketches/__init__.py
Normal file
0
community/www/sketches/__init__.py
Normal file
Reference in New Issue
Block a user