diff --git a/community/lms/doctype/lms_sketch/__init__.py b/community/lms/doctype/lms_sketch/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/community/lms/doctype/lms_sketch/livecode.py b/community/lms/doctype/lms_sketch/livecode.py new file mode 100644 index 00000000..1eb804ad --- /dev/null +++ b/community/lms/doctype/lms_sketch/livecode.py @@ -0,0 +1,44 @@ +"""Utilities to work with livecode service. +""" +import websocket +import json +import drawSvg as draw + +def livecode_to_svg(livecode_ws_url, code, *, timeout=1): + """Renders the code as svg. + """ + print("livecode_to_svg") + ws = websocket.WebSocket() + ws.settimeout(timeout) + ws.connect(livecode_ws_url) + + msg = { + "msgtype": "exec", + "runtime": "python-canvas", + "code": code + } + ws.send(json.dumps(msg)) + + messages = _read_messages(ws) + commands = [m['cmd'] for m in messages if m['msgtype'] == 'draw'] + img = draw_image(commands) + return img.asSvg() + +def _read_messages(ws): + messages = [] + try: + while True: + msg = ws.recv() + if not msg: + break + messages.append(json.loads(msg)) + except websocket.WebSocketTimeoutException: + pass + return messages + +def draw_image(commands): + img = draw.Drawing(300, 300, fill='none', stroke='black') + for c in commands: + if c['function'] == 'circle': + img.append(draw.Circle(cx=c['x'], cy=c['y'], r=c['d']/2)) + return img diff --git a/community/lms/doctype/lms_sketch/lms_sketch.js b/community/lms/doctype/lms_sketch/lms_sketch.js new file mode 100644 index 00000000..020c5909 --- /dev/null +++ b/community/lms/doctype/lms_sketch/lms_sketch.js @@ -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) { + + // } +}); diff --git a/community/lms/doctype/lms_sketch/lms_sketch.json b/community/lms/doctype/lms_sketch/lms_sketch.json new file mode 100644 index 00000000..c236db10 --- /dev/null +++ b/community/lms/doctype/lms_sketch/lms_sketch.json @@ -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 +} \ No newline at end of file diff --git a/community/lms/doctype/lms_sketch/lms_sketch.py b/community/lms/doctype/lms_sketch/lms_sketch.py new file mode 100644 index 00000000..21257e49 --- /dev/null +++ b/community/lms/doctype/lms_sketch/lms_sketch.py @@ -0,0 +1,103 @@ +# -*- 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 + +class LMSSketch(Document): + def get_owner_name(self): + return get_userinfo(self.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) + cache.set(key, value) + return value + + def __repr__(self): + return f"" + +@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() + 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, + } + +def get_recent_sketches(): + """Returns the recent sketches. + + The return value will be a list of dicts with each entry containing + the following fields: + - name + - title + - owner + - owner_name + - modified + """ + sketches = frappe.get_all( + "LMS Sketch", + fields='*', + order_by='modified desc', + page_length=100 + ) + for s in sketches: + s['owner_name'] = get_userinfo(s['owner'])['full_name'] + return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches] + +def get_userinfo(email): + """Returns the username and fullname of a user. + + Please note that the email could be "Administrator" or "Guest" + as a special case to denote the system admin and guest user respectively. + """ + user = frappe.get_doc("User", email) + return { + "full_name": user.full_name, + "username": user.username + } diff --git a/community/lms/doctype/lms_sketch/test_lms_sketch.py b/community/lms/doctype/lms_sketch/test_lms_sketch.py new file mode 100644 index 00000000..b07262a6 --- /dev/null +++ b/community/lms/doctype/lms_sketch/test_lms_sketch.py @@ -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 diff --git a/community/public/build.json b/community/public/build.json new file mode 100644 index 00000000..14ac7230 --- /dev/null +++ b/community/public/build.json @@ -0,0 +1,5 @@ +{ + "css/lms.css": [ + "public/css/lms.css" + ] +} diff --git a/community/public/css/lms.css b/community/public/css/lms.css new file mode 100644 index 00000000..590f8160 --- /dev/null +++ b/community/public/css/lms.css @@ -0,0 +1,64 @@ + +.canvas-wrapper { + position: relative; + padding: 10px; +} + +.canvas-editor canvas { + position: relative; + z-index: 0; + border: 1px solid #ddd; + height: 300px; + width: 300px; +} +.canvas-wrapper .output { + position: absolute; + z-index: 1; + width: 100%; + left: 0px; + top: 0px; + background-color: rgba(255, 255, 255, 0); + margin: 15px; +} + +.canvas-editor .code { + width: 100%; + padding: 5px; + min-height: 330px; + resize: none; +} +.canvas-editor .output { + padding: 5px; +} + +.heading { + background: #eee; + padding: 10px; + clear: both; + color: #212529; + border: 1px solid #ddd; +} + +.canvas-editor h2 { + font-size: 1.2em; + text-transform: uppercase; + margin: 0px; + font-weight: normal; +} + +.canvas-editor .run { + float: right; +} + +.canvas-editor .col-sm { + border: 1px solid #ddd; +} + +.sketch-header h1 { + font-size: 1.5em; + margin-bottom: 0px; +} + +.sketch-header { + margin-bottom: 1em; +} diff --git a/community/www/courses/topic.html b/community/www/courses/topic.html index b527a444..0cc30d38 100644 --- a/community/www/courses/topic.html +++ b/community/www/courses/topic.html @@ -1,4 +1,5 @@ {% extends "templates/base.html" %} +{% from "www/macros/livecode.html" import LiveCodeEditor with context %} {% block title %}{{topic.title}} ({{course.title}}){% endblock %} {% block head_include %} @@ -7,6 +8,7 @@ + @@ -42,7 +44,7 @@ {% if s.type == "text" %} {{ render_section_text(s) }} {% elif s.type == "code" %} - {{ render_section_code(s) }} + {{ LiveCodeEditor(s.name, s.code) }} {% else %}
Unknown section type: {{s.type}}
{% endif %} @@ -52,28 +54,6 @@ {{ s.contents | markdown }} {% endmacro %} -{% macro render_section_code(s) %} -
-
-
- -

Editor

-
- -
-
-
-

Output

-
-
- Dashed box -

-    
- -
-
-{% endmacro %} - {%- block script %} {{ super() }} @@ -89,73 +69,3 @@ }) {%- endblock %} - -{%- block style %} - {{ super() }} - - -{%- endblock %} diff --git a/community/www/macros/livecode.html b/community/www/macros/livecode.html new file mode 100644 index 00000000..8035cbd7 --- /dev/null +++ b/community/www/macros/livecode.html @@ -0,0 +1,40 @@ + +{% macro LiveCodeEditor(name, code) %} +
+
+
+ +

Editor

+
+ +
+
+
+

Output

+
+
+ +

+    
+
+
+{% endmacro %} + + +{% macro LiveCodeEditorJS(name, code) %} + + +{% endmacro %} diff --git a/community/www/sketches/index.html b/community/www/sketches/index.html new file mode 100644 index 00000000..5da1037e --- /dev/null +++ b/community/www/sketches/index.html @@ -0,0 +1,47 @@ +{% extends "templates/base.html" %} +{% from "www/macros/livecode.html" import LiveCodeEditor, LiveCodeEditorJS %} + +{% block title %}Sketches{% endblock %} +{% block head_include %} + + + +{% endblock %} + +{% block content %} +
+
+

Recent Sketches

+ + Create a New Sketch +
+
+
+ {% for sketch in sketches %} +
+
+ + +
+
+ {% endfor %} +
+
+
+{% endblock %} + +{% block style %} + {{super()}} + +{% endblock %} diff --git a/community/www/sketches/index.py b/community/www/sketches/index.py new file mode 100644 index 00000000..3ab688e1 --- /dev/null +++ b/community/www/sketches/index.py @@ -0,0 +1,7 @@ +import frappe +from ...lms.doctype.lms_sketch.lms_sketch import get_recent_sketches + +def get_context(context): + context.no_cache = 1 + context.sketches = get_recent_sketches() + diff --git a/community/www/sketches/sketch.html b/community/www/sketches/sketch.html new file mode 100644 index 00000000..a0509ef1 --- /dev/null +++ b/community/www/sketches/sketch.html @@ -0,0 +1,109 @@ +{% extends "templates/base.html" %} +{% from "www/macros/livecode.html" import LiveCodeEditor, LiveCodeEditorJS with context %} + +{% block title %}{{sketch.title}}{% endblock %} +{% block head_include %} + + + + + + + + + + + + + +{% endblock %} + +{% block content %} +
+
+ + +
+ {% if editable %} + +

+ +

+ {% else %} +

{{sketch.title}}

+ {% endif %} +
By {{sketch.get_owner_name()}}
+
+ + {% if sketch.is_new() and not editable %} +
+ Please login to save this sketch. +
+ {% endif %} + +
+ {{LiveCodeEditor(sketch.name, sketch.code) }} +
+
+{% endblock %} + +{%- block script %} + {{ super() }} + {{ LiveCodeEditorJS() }} + + +{%- endblock %} + diff --git a/community/www/sketches/sketch.py b/community/www/sketches/sketch.py new file mode 100644 index 00000000..e94f76e7 --- /dev/null +++ b/community/www/sketches/sketch.py @@ -0,0 +1,41 @@ +import frappe + +def get_context(context): + context.no_cache = 1 + course_name = get_queryparam("sketch", '/sketches') + context.sketch = get_sketch(course_name) + context.livecode_url = get_livecode_url() + context.editable = is_editable(context.sketch, frappe.session.user) + +def is_editable(sketch, user): + if sketch.is_new(): + # new sketches can be editable by any logged in user + return user != "Guest" + else: + # existing sketches are editable by the owner + return sketch.owner == user + +def get_livecode_url(): + doc = frappe.get_doc("LMS Settings") + return doc.livecode_url + +def get_queryparam(name, redirect_when_not_found): + try: + return frappe.form_dict[name] + except KeyError: + frappe.local.flags.redirect_location = redirect_when_not_found + raise frappe.Redirect + +def get_sketch(name): + if name == 'new': + sketch = frappe.new_doc('LMS Sketch') + sketch.name = "new" + sketch.title = "New Sketch" + sketch.code = "circle(100, 100, 50)" + return sketch + + try: + return frappe.get_doc('LMS Sketch', name) + except frappe.exceptions.DoesNotExistError: + raise frappe.NotFound + diff --git a/requirements.txt b/requirements.txt index 5ac1c812..6da7c51b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -frappe \ No newline at end of file +frappe +websocket_client +drawSvg