Merge branch 'main' of https://github.com/frappe/community into main

This commit is contained in:
pateljannat
2021-03-12 12:22:19 +05:30
15 changed files with 546 additions and 94 deletions

View File

@@ -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

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 Sketch', {
// refresh: function(frm) {
// }
});

View 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
}

View File

@@ -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"<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()
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
}

View 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

View File

@@ -0,0 +1,5 @@
{
"css/lms.css": [
"public/css/lms.css"
]
}

View File

@@ -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;
}

View File

@@ -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 %}
<meta name="description" content="Topic {{topic.title}} of the course {{course.title}}" />
@@ -7,6 +8,7 @@
</style>
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
@@ -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 %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
@@ -52,28 +54,6 @@
{{ s.contents | markdown }}
{% endmacro %}
{% macro render_section_code(s) %}
<div class="canvas-editor row no-gutters" id="editor-{{s.name}}">
<div class="col-sm">
<div class="heading">
<button class="run">Run</button>
<h2>Editor</h2>
</div>
<textarea class="code">{{s.code}}</textarea>
</div>
<div class="col-sm">
<div class="heading">
<h2>Output</h2>
</div>
<div class="canvas-wrapper">
<canvas class="canvas" width="300" height="300">Dashed box</canvas>
<pre class="output"></pre>
</div>
</pre>
</div>
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
@@ -89,73 +69,3 @@
})
</script>
{%- endblock %}
{%- block style %}
{{ super() }}
<style type="text/css">
.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;
}
/* .canvas-editor canvas {
float: left;
border: 1px solid #ddd;
padding: 5px;
margin: 10px;
} */
</style>
{%- endblock %}

View File

@@ -0,0 +1,40 @@
{% macro LiveCodeEditor(name, code) %}
<div class="canvas-editor row no-gutters" id="editor-{{name}}">
<div class="col-sm">
<div class="heading">
<button class="run">Run</button>
<h2>Editor</h2>
</div>
<textarea class="code">{{code}}</textarea>
</div>
<div class="col-sm">
<div class="heading">
<h2>Output</h2>
</div>
<div class="canvas-wrapper">
<canvas class="canvas" width="300" height="300"></canvas>
<pre class="output"></pre>
</div>
</div>
</div>
{% endmacro %}
{% macro LiveCodeEditorJS(name, code) %}
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript">
var livecodeEditors = [];
$(function() {
$(".canvas-editor").each((i, e) => {
var editor = new LiveCodeEditor(e, {
runtime: "python-canvas",
base_url: "{{ livecode_url }}",
codemirror: true
})
livecodeEditors.push(editor);
})
})
</script>
{% endmacro %}

View File

@@ -0,0 +1,47 @@
{% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditor, LiveCodeEditorJS %}
{% block title %}Sketches{% endblock %}
{% block head_include %}
<meta name="description" content="Sketches" />
<meta name="keywords" content="sketches" />
<link rel="stylesheet" href="/assets/css/lms.css">
{% endblock %}
{% block content %}
<section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'>
<h1>Recent Sketches</h1>
<a href="/sketches/sketch?sketch=new">Create a New Sketch</a>
</div>
<div class='container'>
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2 ">
{% for sketch in sketches %}
<div class="col mb-4">
<div class="card" style="width: 200px;">
<div class="card-img-top">
<a href="/sketches/sketch?sketch={{sketch.name}}">
{{ sketch.to_svg() }}
</a>
</div>
<div class="card-footer">
By {{sketch.get_owner_name()}}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</section>
{% endblock %}
{% block style %}
{{super()}}
<style type="text/css">
svg {
width: 200px;
height: 200px;
}
</style>
{% endblock %}

View File

@@ -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()

View File

@@ -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 %}
<meta name="description" content="Sketch {{sketch.title}}" />
<meta name="keywords" content="sketch {{sketch.title}}" />
<style>
</style>
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.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>
{% endblock %}
{% block content %}
<section class="top-section" style="padding: 1rem 0rem;">
<div class='container pb-5'>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item" aria-current="page"><a href="/sketches">Sketches</a></li>
</ol>
</nav>
<div class="sketch-header">
{% if editable %}
<input type="button" class="pull-right" id="sketch-save" value="Save"/>
<h1 class="sketch-title">
<input type="text" name="title" id="sketch-title" value="{{ sketch.title }}" />
</h1>
{% else %}
<h1 class="sketch-title">{{sketch.title}}</h1>
{% endif %}
<div class="sketch-owner-wrapper">By <span class="sketch-owner">{{sketch.get_owner_name()}}</span></div>
</div>
{% if sketch.is_new() and not editable %}
<div class="alert alert-warning">
Please login to save this sketch.
</div>
{% endif %}
<div class="sketch-editor">
{{LiveCodeEditor(sketch.name, sketch.code) }}
</div>
</section>
{% endblock %}
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
<script type="text/javascript">
var sketch_name = {{ sketch.name | tojson }};
function saveSketch() {
var title = $("#sketch-title").val()
var code = livecodeEditors[0].codemirror.doc.getValue()
frappe.call('community.lms.doctype.lms_sketch.lms_sketch.save_sketch', {
name: sketch_name,
title: title,
code: code
})
.then(r => {
var msg = r.message;
if (!msg.ok) {
var error = msg.error || "Save failed."
frappe.msgprint({
"title": "Error",
"indicator": "red",
"message": error
});
}
else if (msg.status == "created") {
var path = "/sketches/sketch?sketch=" + msg.name;
var url = window.location.protocol + "//" + window.location.host + path
window.history.pushState({path: url}, '', url);
sketch_name = name;
frappe.msgprint({
"title": "Notification",
"indicator": "green",
"message": "New sketch has been saved!"
});
}
else if (msg.status == "updated") {
frappe.msgprint({
"title": "Notification",
"indicator": "green",
"message": "The sketch has been saved!"
});
}
})
}
$(function() {
$("#sketch-save").click(function() {
saveSketch();
});
})
</script>
{%- endblock %}

View File

@@ -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

View File

@@ -1 +1,3 @@
frappe
frappe
websocket_client
drawSvg