refactor: removed sketch doctype and portal page for home
Both of these will be moved to mon_school.
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
# from ..lms_sketch.livecode import livecode_to_svg
|
||||
|
||||
class Exercise(Document):
|
||||
def get_user_submission(self):
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
"""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
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('LMS Sketch', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
# -*- 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,
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
"""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)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# -*- 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,5 +1,4 @@
|
||||
"""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_sketch.lms_sketch import LMSSketch as Sketch
|
||||
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,7 +0,0 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user