Compare commits

...

32 Commits

Author SHA1 Message Date
Anand Chitipothu
9e0476fd00 chore: upgraded node to v14 in github actions
- the new changes to frappe required node v14
- also added a build setup before runing tests to make sure the assets are built
2021-05-19 10:05:59 +05:30
Anand Chitipothu
d9ea02667d refactor: removed the branding and customization for mon school
They have been moved to a new mon_school app
2021-05-18 21:25:29 +05:30
Anand Chitipothu
bdabf32124 Merge pull request #86 from fossunited/learn
Implemented learning section for batches
2021-05-14 15:36:44 +05:30
Anand Chitipothu
8b657f2f40 feat: added next/prev links to learn pages 2021-05-14 15:29:44 +05:30
Anand Chitipothu
49b41749e8 feat: added learn page
- added sections to the lesson to handle multiple sesions like examples and exercises
- added livecode integration to lesson pages
- autosave and submiting the answers is not done yet
2021-05-14 12:11:45 +05:30
Anand Chitipothu
1cf57c4823 feat: redirect the course page to learn page when the visitor is a student of a batch 2021-05-14 12:06:37 +05:30
Anand Chitipothu
5cfb72a731 style: tweaks
- made hero h1 black
- fixed the styles of lesson teasers
2021-05-08 13:44:34 +05:30
Anand Chitipothu
49d5ca4292 fix: added index to lesson doctype to fix the display order 2021-05-08 13:44:34 +05:30
Anand Chitipothu
a3e53efcc1 fix: fixed course description on the course page
The small intro was being shown in its place.
2021-05-08 13:44:34 +05:30
Anand Chitipothu
be7814b4fe Merge pull request #81 from fossunited/flow-fixes
fix: username and email validations
2021-05-07 18:53:58 +05:30
Anand Chitipothu
c40ab9a726 style: style tweaks 2021-05-07 18:32:35 +05:30
Anand Chitipothu
64cf14ed92 fix: fail gracefully when livecode_to_svg crashes
That seems to be happening in some cases and couldn't really figure out
the reason. Handling the error to gracefully to show an empty image in
those cases.
2021-05-07 18:32:34 +05:30
Anand Chitipothu
dbaa896fcc fix: fixed the issue of users unable to save sketches
Inserting new sketches is failing because the web user role doesn't have
permission to create sketches. Fixed it by adding
ignore_permission=True.
2021-05-07 18:32:34 +05:30
pateljannat
911c85bfc8 fix: redirect after login 2021-05-07 18:27:18 +05:30
pateljannat
5edceb2562 fix: photo from user 2021-05-07 17:14:33 +05:30
Anand Chitipothu
5d14dce320 Merge pull request #80 from fossunited/add-new-batch-form-enhancements
fix: Add new batch form enhancements
2021-05-07 16:53:21 +05:30
Anand Chitipothu
eec9e57dd2 fix: fixed thumbnail generation from livecode
Due to the recent changes to livecode, the code to generate svg from
code got broken. Fixed that now.
2021-05-07 16:48:52 +05:30
Anand Chitipothu
503b922074 style: improved the sketch page 2021-05-07 16:48:25 +05:30
pateljannat
d4c19932d5 fix: replaced slug with name and removed whitelist 2021-05-07 16:28:12 +05:30
Anand Chitipothu
3c8cffc5ad fix: fixed the issue of unable to create sketches.
The livecode API has been generalized and there was some
backwackward-incompatible changes in that proces. Added
livecode-canvas.js with the required options to fix the issue.
2021-05-07 13:59:38 +05:30
pateljannat
b3c67a3f34 fix: username and email validations 2021-05-07 13:47:33 +05:30
Anand Chitipothu
84b4833fed style: tweaked the styles of profile page 2021-05-07 12:33:43 +05:30
pateljannat
28ef7e5def fix: conflicts 2021-05-07 12:08:51 +05:30
pateljannat
9981baa13b fix: add new batch form enhancements 2021-05-07 12:04:11 +05:30
Anand Chitipothu
c764aa6c20 fix: fixed the error in saving a new course 2021-05-07 05:18:06 +05:30
Anand Chitipothu
bc11730697 style: showing the message as alert elements on the course page 2021-05-06 21:05:36 +05:30
Anand Chitipothu
92c4a86e8b refactor: fixed the accidentally removed code in RequestInvite 2021-05-06 21:05:08 +05:30
Anand Chitipothu
358724bf1c style: fixed the hero section on mobile
The email textbox was becoming too small.
2021-05-06 20:53:14 +05:30
Anand Chitipothu
bb80d988d7 refactor: using SketchTeaser widget to show sketch in the profile page 2021-05-06 20:43:04 +05:30
Anand Chitipothu
0ad03a3fb5 style: made the avatar and name of the person a link 2021-05-06 20:41:52 +05:30
Anand Chitipothu
3382de0ecb refactor: removed slug from course page. 2021-05-06 15:47:47 +05:30
Anand Chitipothu
08bb7b4490 Merge pull request #73 from fossunited/mentor-request-email-templates
fix: mentor request flow and emails #30, #69, #70
2021-05-06 15:39:58 +05:30
41 changed files with 762 additions and 393 deletions

View File

@@ -36,7 +36,7 @@ jobs:
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '12' node-version: '14'
check-latest: true check-latest: true
- name: setup cache for bench - name: setup cache for bench
uses: actions/cache@v2 uses: actions/cache@v2
@@ -69,6 +69,9 @@ jobs:
- name: allow tests - name: allow tests
working-directory: /home/runner/frappe-bench working-directory: /home/runner/frappe-bench
run: bench --site frappe.local set-config allow_tests true run: bench --site frappe.local set-config allow_tests true
- name: bench build
working-directory: /home/runner/frappe-bench
run: bench --site frappe.local build
- name: run tests - name: run tests
working-directory: /home/runner/frappe-bench working-directory: /home/runner/frappe-bench
run: bench --site frappe.local run-tests --app community run: bench --site frappe.local run-tests --app community

View File

@@ -47,6 +47,9 @@ class CommunityMember(Document):
'member_type': 'Mentor' 'member_type': 'Mentor'
}) })
def get_photo_url(self):
return frappe.db.get_value("User", self.email, ["user_image"])
def get_palette(self): def get_palette(self):
palette = [ palette = [
['--orange-avatar-bg', '--orange-avatar-color'], ['--orange-avatar-bg', '--orange-avatar-color'],
@@ -74,7 +77,7 @@ def create_member_from_user(doc, method):
if ( doc.username and username_exists(doc.username)) or not doc.username: if ( doc.username and username_exists(doc.username)) or not doc.username:
username = create_username_from_email(doc.email) username = create_username_from_email(doc.email)
elif len(doc.username) < 4: elif len(doc.username) < 4 and doc.send_welcome_email == 1:
username = adjust_username(doc.username) username = adjust_username(doc.username)
if username_exists(username): if username_exists(username):

View File

@@ -1,7 +1,8 @@
{% set color = member.get_palette() %} {% set color = member.get_palette() %}
<a href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}"> <span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.photo %} {% if member.get_photo_url() %}
<img class="avatar-frame standard-image" src="{{ member.photo }}" title="{{ member.full_name }}"> <img class="avatar-frame standard-image" src="{{ member.get_photo_url() }}" title="{{ member.full_name }}">
</img> </img>
{% else %} {% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}" <span class="avatar-frame standard-image" title="{{ member.full_name }}"
@@ -9,4 +10,5 @@
{{ member.abbr }} {{ member.abbr }}
</span> </span>
{% endif %} {% endif %}
</a>
</span> </span>

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from . import __version__ as app_version from . import __version__ as app_version
from .install import APP_LOGO_URL
app_name = "community" app_name = "community"
app_title = "Community" app_title = "Community"
@@ -12,8 +11,6 @@ app_color = "grey"
app_email = "jannat@erpnext.com" app_email = "jannat@erpnext.com"
app_license = "AGPL" app_license = "AGPL"
app_logo_url = APP_LOGO_URL
# Includes in <head> # Includes in <head>
# ------------------ # ------------------
@@ -63,7 +60,7 @@ web_include_css = "/assets/css/community.css"
# ------------ # ------------
# before_install = "community.install.before_install" # before_install = "community.install.before_install"
after_install = "community.install.after_install" # after_install = "community.install.after_install"
# Desk Notifications # Desk Notifications
# ------------------ # ------------------
@@ -145,6 +142,7 @@ primary_rules = [
{"from_route": "/dashboard", "to_route": ""}, {"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"}, {"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "courses/learn"}, {"from_route": "/courses/<course>/<batch>/learn", "to_route": "courses/learn"},
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "courses/learn"},
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "courses/schedule"}, {"from_route": "/courses/<course>/<batch>/schedule", "to_route": "courses/schedule"},
{"from_route": "/courses/<course>/<batch>/members", "to_route": "courses/members"}, {"from_route": "/courses/<course>/<batch>/members", "to_route": "courses/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "courses/discuss"}, {"from_route": "/courses/<course>/<batch>/discuss", "to_route": "courses/discuss"},

View File

@@ -1,42 +0,0 @@
"""Hooks that are executed during and after install.
"""
import os
import frappe
APP_LOGO_URL = os.getenv("APP_LOGO_URL") or "/files/logo.png"
def after_install():
set_app_name()
disable_signup()
add_header_items()
add_footer_items()
def set_app_name():
app_name = os.getenv("FRAPPE_APP_NAME")
if app_name:
frappe.db.set_value('System Settings', None, 'app_name', app_name)
def disable_signup():
frappe.db.set_value("Website Settings", None, "disable_signup", 1)
def add_header_items():
items = [
{"label": "Sketches", "url": "/sketches"},
]
doc = frappe.get_doc("Website Settings", None)
doc.update({
"top_bar_items": items
})
doc.save()
def add_footer_items():
items = [
{"label": "About", "url": "/about"},
{"label": "Blog", "url": "/blog"},
{"label": "Github", "url": "https://github.com/fossunited/community"}
]
doc = frappe.get_doc("Website Settings", None)
doc.update({
"footer_items": items
})
doc.save()

View File

@@ -9,7 +9,8 @@
"course", "course",
"title", "title",
"description", "description",
"locked" "locked",
"index_"
], ],
"fields": [ "fields": [
{ {
@@ -35,6 +36,12 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"options": "LMS Course" "options": "LMS Course"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -45,7 +52,7 @@
"link_fieldname": "chapter" "link_fieldname": "chapter"
} }
], ],
"modified": "2021-05-03 06:52:10.894328", "modified": "2021-05-13 21:05:20.531890",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Chapter", "name": "Chapter",

View File

@@ -10,5 +10,6 @@ class Chapter(Document):
def get_lessons(self): def get_lessons(self):
rows = frappe.db.get_all("Lesson", rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name}, filters={"chapter": self.name},
fields='*') fields='*',
order_by="index_")
return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows] return [frappe.get_doc(dict(row, doctype='Lesson')) for row in rows]

View File

@@ -47,6 +47,9 @@ class InviteRequest(Document):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def create_invite_request(invite_email): def create_invite_request(invite_email):
if not frappe.utils.validate_email_address(invite_email):
return "invalid email"
if frappe.db.exists("User", invite_email): if frappe.db.exists("User", invite_email):
return "user" return "user"

View File

@@ -8,7 +8,10 @@
"field_order": [ "field_order": [
"chapter", "chapter",
"lesson_type", "lesson_type",
"title" "title",
"index_",
"body",
"sections"
], ],
"fields": [ "fields": [
{ {
@@ -31,11 +34,28 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Title" "label": "Title"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
"label": "Body"
},
{
"fieldname": "sections",
"fieldtype": "Table",
"label": "Sections",
"options": "LMS Section"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-03 06:51:43.588969", "modified": "2021-05-13 20:03:51.510605",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Lesson", "name": "Lesson",

View File

@@ -3,8 +3,37 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from ..lms_topic.section_parser import SectionParser
class Lesson(Document): class Lesson(Document):
pass def before_save(self):
sections = SectionParser().parse(self.body or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
def get_sections(self):
return sorted(self.get('sections'), key=lambda s: s.index)
def make_lms_section(self, index, section):
s = frappe.new_doc('LMS Section', parent_doc=self, parentfield='sections')
s.type = section.type
s.label = section.label
s.contents = section.contents
s.index = index
return s
def get_next(self):
"""Returns the number for the next lesson.
The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson.
"""
def get_prev(self):
"""Returns the number for the prev lesson.
The return value would be like 1.2, 2.1 etc.
It will be None if there is no next lesson.
"""

View File

@@ -6,20 +6,30 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from community.www.courses.utils import get_member_with_email from community.www.courses.utils import get_member_with_email
from frappe import _
from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
from community.query import find, find_all from community.query import find, find_all
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
self.validate_if_mentor()
if not self.code: if not self.code:
self.generate_code() self.generate_code()
def validate_if_mentor(self):
course = frappe.get_doc("LMS Course", self.course)
if not course.is_mentor(frappe.session.user):
frappe.throw(_("You are not a mentor of the course {0}").format(course.title))
def after_insert(self):
create_membership(batch=self.name, member_type="Mentor")
def generate_code(self): def generate_code(self):
short_code = frappe.db.get_value("LMS Course", self.course, "short_code") short_code = frappe.db.get_value("LMS Course", self.course, "short_code")
course_batches = frappe.get_all("LMS Batch",{"course":self.course}) course_batches = frappe.get_all("LMS Batch",{"course":self.course})
self.code = short_code + str(len(course_batches) + 1) self.code = short_code + str(len(course_batches) + 1)
def get_mentors(self): def get_mentors(self):
mentors = []
memberships = frappe.get_all( memberships = frappe.get_all(
"LMS Batch Membership", "LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"}, {"batch": self.name, "member_type": "Mentor"},

View File

@@ -28,8 +28,7 @@ class LMSBatchMembership(Document):
member_name = frappe.db.get_value("Community Member", self.member, "full_name") member_name = frappe.db.get_value("Community Member", self.member, "full_name")
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch)) frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch))
@frappe.whitelist() def create_membership(batch, member=None, member_type="Student", role="Member"):
def create_membership(batch, course=None, member=None, member_type="Student", role="Member"):
if not member: if not member:
member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name") member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name")
frappe.get_doc({ frappe.get_doc({
@@ -39,7 +38,4 @@ def create_membership(batch, course=None, member=None, member_type="Student", ro
"member_type": member_type, "member_type": member_type,
"member": member "member": member
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
if course:
course_slug = frappe.db.get_value("LMS Course", {"title": course}, ["name"])
return course_slug
return "OK" return "OK"

View File

@@ -8,7 +8,6 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title", "title",
"slug",
"is_published", "is_published",
"column_break_3", "column_break_3",
"short_code", "short_code",
@@ -42,14 +41,6 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Short Code" "label": "Short Code"
}, },
{
"description": "The slug of the course. Autogenerated from the title if not specified.",
"fieldname": "slug",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slug",
"unique": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -93,7 +84,7 @@
"link_fieldname": "course" "link_fieldname": "course"
} }
], ],
"modified": "2021-05-06 11:15:45.728976", "modified": "2021-05-06 13:37:03.318829",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Course", "name": "LMS Course",
@@ -112,7 +103,7 @@
"write": 1 "write": 1
} }
], ],
"search_fields": "slug", "search_fields": "title",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",

View File

@@ -28,8 +28,8 @@ class LMSCourse(Document):
def generate_slug(self, title): def generate_slug(self, title):
result = frappe.get_all( result = frappe.get_all(
'LMS Course', 'LMS Course',
fields=['slug']) fields=['name'])
slugs = set([row['slug'] for row in result]) slugs = set([row['name'] for row in result])
return slugify(title, used_slugs=slugs) return slugify(title, used_slugs=slugs)
def __repr__(self): def __repr__(self):
@@ -114,6 +114,35 @@ class LMSCourse(Document):
"mentor": member "mentor": member
}) })
def get_student_batch(self, email):
"""Returns the batch the given student is part of.
Returns None if the student is not part of any batch.
"""
if not email:
return False
member = self.get_community_member(email)
result = frappe.db.get_all(
"LMS Batch Membership",
filters={
"member": member,
"member_type": "Student",
},
fields=['batch']
)
batches = [row['batch'] for row in result]
# filter the batches that are for this course
result = frappe.db.get_all(
"LMS Batch",
filters={
"course": self.name,
"name": ["IN", batches]
})
batches = [row['name'] for row in result]
if batches:
return frappe.get_doc("LMS Batch", batches[0])
def get_instructor(self): def get_instructor(self):
member_name = self.get_community_member(self.owner) member_name = self.get_community_member(self.owner)
return frappe.get_doc("Community Member", member_name) return frappe.get_doc("Community Member", member_name)
@@ -148,3 +177,59 @@ class LMSCourse(Document):
visibility="Public") visibility="Public")
return batches return batches
def get_chapter(self, index):
return find("Chapter", course=self.name, index_=index)
def get_lesson(self, chapter_index, lesson_index):
chapter_name = frappe.get_value(
"Chapter",
{"course": self.name, "index_": chapter_index},
"name")
lesson_name = chapter_name and frappe.get_value(
"Lesson",
{"chapter": chapter_name, "index_": lesson_index},
"name")
return lesson_name and frappe.get_doc("Lesson", lesson_name)
def get_outline(self):
return CourseOutline(self)
class CourseOutline:
def __init__(self, course):
self.course = course
self.chapters = self.get_chapters()
self.lessons = self.get_lessons()
def get_next(self, current):
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
return numbers[index+1]
except IndexError:
return None
def get_prev(self, current):
numbers = sorted(lesson['number'] for lesson in self.lessons)
try:
index = numbers.index(current)
if index == 0:
return None
return numbers[index-1]
except IndexError:
return None
def get_chapters(self):
return frappe.db.get_all("Chapter",
filters={"course": self.course.name},
fields=["name", "title", "index_"])
def get_lessons(self):
chapters = [c['name'] for c in self.chapters]
lessons = frappe.db.get_all("Lesson",
filters={"chapter": ["IN", chapters]},
fields=["name", "title", "chapter", "index_"])
chapter_numbers = {c['name']: c['index_'] for c in self.chapters}
for lesson in lessons:
lesson['number'] = "{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_'])
return lessons

View File

@@ -3,26 +3,89 @@
import websocket import websocket
import json import json
from .svg import SVG from .svg import SVG
import frappe
def livecode_to_svg(livecode_ws_url, code, *, timeout=1): # 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 livecode_to_svg(livecode_ws_url, code, *, timeout=3):
"""Renders the code as svg. """Renders the code as svg.
""" """
print("livecode_to_svg") try:
ws = websocket.WebSocket() ws = websocket.WebSocket()
ws.settimeout(timeout) ws.settimeout(timeout)
ws.connect(livecode_ws_url) ws.connect(livecode_ws_url)
msg = { msg = {
"msgtype": "exec", "msgtype": "exec",
"runtime": "python-canvas", "runtime": "python",
"code": code "code": code,
} "files": [
ws.send(json.dumps(msg)) {"filename": "start.py", "contents": START},
{"filename": "sketch.py", "contents": SKETCH},
],
"command": ["python", "start.py"]
}
ws.send(json.dumps(msg))
messages = _read_messages(ws) messages = _read_messages(ws)
commands = [m['cmd'] for m in messages if m['msgtype'] == 'draw'] commands = [m for m in messages if m['msgtype'] == 'draw']
img = draw_image(commands) img = draw_image(commands)
return img.tostring() return img.tostring()
except websocket.WebSocketException as e:
frappe.log_error(frappe.get_traceback(), 'livecode_to_svg failed')
def _read_messages(ws): def _read_messages(ws):
messages = [] messages = []
@@ -32,17 +95,19 @@ def _read_messages(ws):
if not msg: if not msg:
break break
messages.append(json.loads(msg)) messages.append(json.loads(msg))
except websocket.WebSocketTimeoutException: except websocket.WebSocketTimeoutException as e:
print("Error:", e)
pass pass
return messages return messages
def draw_image(commands): def draw_image(commands):
img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black') img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black')
for c in commands: for c in commands:
args = c['args']
if c['function'] == 'circle': if c['function'] == 'circle':
img.circle(cx=c['x'], cy=c['y'], r=c['d']/2) img.circle(cx=args['x'], cy=args['y'], r=args['d']/2)
elif c['function'] == 'line': elif c['function'] == 'line':
img.line(x1=c['x1'], y1=c['y1'], x2=c['x2'], y2=c['y2']) img.line(x1=args['x1'], y1=args['y1'], x2=args['x2'], y2=args['y2'])
elif c['function'] == 'rect': elif c['function'] == 'rect':
img.rect(x=c['x'], y=c['y'], width=c['w'], height=c['h']) img.rect(x=args['x'], y=args['y'], width=args['w'], height=args['h'])
return img return img

View File

@@ -9,6 +9,11 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from . import livecode from . import livecode
DEFAULT_IMAGE = """
<svg viewBox="0 0 300 300" width="300" xmlns="http://www.w3.org/2000/svg">
</svg>
"""
class LMSSketch(Document): class LMSSketch(Document):
@property @property
def sketch_id(self): def sketch_id(self):
@@ -48,8 +53,9 @@ class LMSSketch(Document):
else: else:
ws_url = self.get_livecode_ws_url() ws_url = self.get_livecode_ws_url()
value = livecode.livecode_to_svg(ws_url, self.code) value = livecode.livecode_to_svg(ws_url, self.code)
cache.set(key, value) if value:
return value cache.set(key, value)
return value or DEFAULT_IMAGE
@staticmethod @staticmethod
def get_recent_sketches(limit=100, owner=None): def get_recent_sketches(limit=100, owner=None):
@@ -77,7 +83,7 @@ def save_sketch(name, title, code):
doc.title = title doc.title = title
doc.code = code doc.code = code
doc.runtime = 'python-canvas' doc.runtime = 'python-canvas'
doc.insert() doc.insert(ignore_permissions=True)
status = "created" status = "created"
else: else:
doc = frappe.get_doc("LMS Sketch", name) doc = frappe.get_doc("LMS Sketch", name)

View File

@@ -1,4 +1,9 @@
"""Utility to split the text in the topic into multiple sections. """Utility to split the text in the topic into multiple sections.
{{ section(type="example", id="foo") }}
circle(100, 100, 50)
{{ end }}
""" """
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -1,19 +1,14 @@
frappe.ready(function () { frappe.ready(function () {
frappe.web_form.after_save = () => { frappe.web_form.after_save = () => {
let data = frappe.web_form.get_values(); let data = frappe.web_form.get_values();
frappe.call({ let slug = new URLSearchParams(window.location.search).get("slug")
"method": "community.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership", frappe.msgprint({
"args": { message: __("Batch {0} has been successfully created!", [data.title]),
"batch": data.title, clear: true
"member_type": "Mentor", });
"course": data.course setTimeout(function () {
}, window.location.href = `courses/${slug}`;
"callback": (data) => { }, 2000);
if (data.message) {
window.location.href = `courses/${data.message}`
}
}
})
} }
frappe.web_form.validate = () => { frappe.web_form.validate = () => {
@@ -21,8 +16,13 @@ frappe.ready(function () {
let time_format = sysdefaults && sysdefaults.time_format ? sysdefaults.time_format : 'HH:mm:ss'; let time_format = sysdefaults && sysdefaults.time_format ? sysdefaults.time_format : 'HH:mm:ss';
let data = frappe.web_form.get_values(); let data = frappe.web_form.get_values();
data.start_time = moment(data.start_time,time_format).format(time_format) data.start_time = moment(data.start_time, time_format).format(time_format)
data.end_time = moment(data.end_time,time_format).format(time_format) data.end_time = moment(data.end_time, time_format).format(time_format)
if (data.start_date < frappe.datetime.nowdate()) {
frappe.msgprint(__('Start date cannot be a past date.'))
return false;
}
if (!frappe.datetime.validate(data.start_time) || !frappe.datetime.validate(data.end_time)) { if (!frappe.datetime.validate(data.start_time) || !frappe.datetime.validate(data.end_time)) {
frappe.msgprint(__('Invalid Start or End Time.')); frappe.msgprint(__('Invalid Start or End Time.'));
@@ -34,10 +34,6 @@ frappe.ready(function () {
return false; return false;
} }
if (data.start_date < date.nowdate()) {
frappe.msgprint(__('Start date cannot be a past date.'))
return false;
}
return true; return true;
}; };
}) })

View File

@@ -7,7 +7,9 @@
</div> </div>
<div class="course-footer"> <div class="course-footer">
<div class="course-author"> <div class="course-author">
<img class="course-author-avatar" src="{{ course.get_instructor().avatar }}" />{{ course.get_instructor().full_name }} {% with author = course.get_instructor() %}
{{ widgets.Avatar(member=author, avatar_class="avatar-medium") }} <a href="/{{author.username}}">{{ author.full_name }}</a>
{% endwith %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,13 @@
<form id="invite-request-form"> <form id="invite-request-form">
<input class="form-control field-width mr-5" id="invite_email" type="email" placeholder="Email Address"> <div class="row">
<a type="submit" id="submit-invite-request" class="btn btn-primary btn-lg" href="#" role="button">Request Invite</a> <div class="col-md">
<input class="form-control w-100 mr-5 mb-5 mt-2" id="invite_email" type="email" placeholder="Email Address">
</div>
<div class="col-md">
<a type="submit" id="submit-invite-request" class="btn btn-primary btn-lg" role="button">Request
Invite</a>
</div>
</div>
</form> </form>
<script> <script>
frappe.ready(() => { frappe.ready(() => {
@@ -12,27 +19,44 @@
invite_email: invite_email invite_email: invite_email
}, },
callback: (data) => { callback: (data) => {
$("#invite-request-form").hide(); if (data.message == "invalid email") {
$(".email-validation") && $(".email-validation").remove();
if (data.message == "OK") { if (invite_email) {
var message = `<div> var message = `<div>
<p class="lead">Thanks for your interest in Mon School. We have recorded your interest and we will get back to you shortly.</p> <small class="email-validation" style="color: red;">${invite_email} is not a valid email address.</small>
</div>`; </div>`;
} }
else {
else if (data.message == "invite") { var message = `<div>
var message = `<div> <small class="email-validation" style="color: red;">Please enter an email address.</small>
<p class="lead">Email ${invite_email} has already been used to request an invitation.</p>
</div>`; </div>`;
} }
else if (data.message == "user") { $("#invite-request-form").append(message);
var message = `<div> }
<p class="lead">Looks like there is already an account with email ${invite_email}. Would you like to <a href="/login">login</a>.</p> else {
$("#invite-request-form").hide();
if (data.message == "OK") {
var message = `<div>
<p class="lead alert alert-secondary">Thanks for your interest in Mon School. We have recorded your interest and we will get back to you shortly.</p>
</div>`; </div>`;
} }
$(".jumbotron").append(message); else if (data.message == "invite") {
var message = `<div>
<p class="lead alert alert-secondary">Email ${invite_email} has already been used to request an invitation.</p>
</div>`;
}
else if (data.message == "user") {
var message = `<div>
<p class="lead alert alert-secondary">Looks like there is already an account with email ${invite_email}. Would you like to <a href="/login">login</a>?</p>
</div>`;
}
$(".jumbotron").append(message);
}
} }
}) })
}) })

View File

@@ -9,7 +9,8 @@
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a> <a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
</div> </div>
<div class="sketch-author"> <div class="sketch-author">
by {{sketch.get_owner().full_name}} {% set owner = sketch.get_owner() %}
by <a href="/{{owner.username}}">{{owner.full_name}}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,5 +6,9 @@
"public/css/style.css", "public/css/style.css",
"public/css/vars.css", "public/css/vars.css",
"public/css/style.less" "public/css/style.less"
],
"js/livecode-canvas.js": [
"public/js/livecode-canvas.js"
] ]
} }

View File

@@ -1,57 +1,4 @@
.livecode-editor-large .canvas-wrapper {
position: relative;
padding: 10px;
}
.livecode-editor-large canvas {
position: relative;
z-index: 0;
border: 1px solid #ddd;
height: 300px;
width: 300px;
}
.livecode-editor-large .code {
width: 100%;
padding: 5px;
min-height: 330px;
resize: none;
}
.livecode-editor-large .output {
padding: 5px;
}
.livecode-editor-large .CodeMirror {
height: 320px;
}
.canvas-editor canvas {
position: relative;
z-index: 0;
border: 1px solid #ddd;
}
.canvas-wrapper .output {
position: absolute;
z-index: 1;
width: 100%;
left: 0px;
top: 0px;
background-color: rgba(255, 255, 255, 0);
margin: 15px;
max-height: 200px;
}
.canvas-editor .code {
width: 100%;
padding: 5px;
/* min-height: 330px; */
resize: none;
}
/* .canvas-editor .output {
padding: 5px;
} */
.heading { .heading {
background: #eee; background: #eee;
padding: 10px; padding: 10px;
@@ -60,21 +7,6 @@
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.livecode-editor-large h2 {
font-size: 1.2em;
text-transform: uppercase;
margin: 0px;
font-weight: normal;
}
.livecode-editor-large .run {
float: right;
}
.livecode-editor-large .col-sm {
border: 1px solid #ddd;
}
.sketch-header h1 { .sketch-header h1 {
font-size: 1.5em; font-size: 1.5em;
margin-bottom: 0px; margin-bottom: 0px;
@@ -89,48 +21,6 @@
color: inherit; color: inherit;
} }
.canvas-editor .CodeMirror {
background: #ffe;
border: 1px solid #eed;
margin-bottom: 10px;
border-radius: 10px;
height: 100%;
}
canvas {
border: 2px solid #eee;
}
@media (min-width: 768px) {
.canvas-wrapper {
margin-bottom: -200px;
}
}
@media (max-width: 768px) {
.canvas-wrapper {
padding-top: 20px;
margin-left: 2em;
}
.canvas-wrapper .output {
left: 2em;
}
}
.hidden { .hidden {
display: none; display: none;
} }
.livecode-controls {
margin-left: 2em;
}
.livecode-controls a {
margin-left: 10px;
color: #666;
}
.canvas-editor {
margin: 10px 0px;
}

View File

@@ -77,7 +77,7 @@ body {
.lessons { .lessons {
padding-left: 20px; padding-left: 20px;
} }
.lesson { .lessons .lesson {
margin: 5px 0px; margin: 5px 0px;
font-weight: bold; font-weight: bold;
} }
@@ -167,7 +167,7 @@ img.profile-photo {
} }
.msger-inputarea { .msger-inputarea {
position: fixed; position: absolute;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -232,3 +232,77 @@ section.lightgray {
color: inherit; color: inherit;
text-decoration: underline; text-decoration: underline;
} }
// LiveCode editor
.livecode-editor {
.CodeMirror {
border: 1px solid #ddd;
background: #ffe;
height: auto;
}
.CodeMirror-scroll {
max-height: 310px;
min-height: 310px;
}
.controls {
padding: 10px 0px;
}
canvas {
border: 5px solid #ddd;
position: relative;
z-index: 0;
}
.output {
position: absolute;
z-index: 1;
width: 300px;
left: 0px;
top: 0px;
background-color: rgba(255, 255, 255, 0);
max-height: 300px;
white-space: pre-wrap;
margin: 0px;
margin-left: 20px;
padding: 4px;
color: #888;
}
@media (max-width: 768px) {
.canvas-wrapper {
padding-top: 10px;
}
.code-wrapper {
min-height: 50px;
}
.CodeMirror {
min-height: 50px;
}
}
}
.sketch-header {
input#sketch-title {
font-weight: bold;
}
}
.chapter-description {
margin-bottom: 10px;
}
.lesson-teaser {
font-weight: bold;
color: black;
padding-left: 20px;
}
#hero h1 {
color: black !important;
}
.lesson-page {
margin: 20px 0px;
}

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="104px" height="81px" viewBox="0 0 104 81" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>Group 2</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="site" transform="translate(-160.803147, -89.000000)">
<g id="Group-2" transform="translate(163.323899, 91.260768)">
<g id="g1593" transform="translate(10.217829, 8.684369)" fill="black" fill-rule="nonzero">
<g id="text1571" transform="translate(0.248104, 0.000000)">
<polygon id="path1919" points="6.20610364 30.6588166 0.0881167018 30.6588166 0.0881167018 0.711614981 15.1331308 0.711614981 15.1331308 6.82960641 6.20610364 6.82960641 6.20610364 12.6143164 15.1331308 12.6143164 15.1331308 18.7323078 6.20610364 18.7323078"></polygon>
<path d="M24.0601557,24.5170177 L29.8448702,24.5170177 L29.8448702,6.82960641 L24.0601557,6.82960641 L24.0601557,24.5170177 Z M20.917843,30.6588166 L20.917843,27.6593507 L17.9421688,27.6593507 L17.9421688,3.68729595 L20.917843,3.68729595 L20.917843,0.711614981 L32.9871829,0.711614981 L32.9871829,3.68729595 L35.9628571,3.68729595 L35.9628571,27.6593507 L32.9871829,27.6593507 L32.9871829,30.6588166 L20.917843,30.6588166 Z M18.108805,0.806844685 L20.917843,0.806844685 L20.917843,3.68729595 L18.108805,3.68729595 L18.108805,0.806844685 Z M32.9871829,0.711525057 L35.9628571,0.711525057 L35.9628571,3.68720602 L32.9871829,3.68720602 L32.9871829,0.711525057 Z M32.9871829,27.6592608 L35.9628571,27.6592608 L35.9628571,30.5635195 L32.9871829,30.5635195 L32.9871829,27.6592608 Z M17.9421688,27.6592608 L20.917843,27.6592608 L20.917843,30.6587267 L17.9421688,30.6587267 L17.9421688,27.6592608 Z" id="path1921"></path>
<path d="M53.8169092,30.6588166 L38.7718951,30.6588166 L38.7718951,24.5170177 L50.6745942,24.5170177 L50.6745942,18.7323078 L41.7475693,18.7323078 L41.7475693,15.7566493 L38.7718951,15.7566493 L38.7718951,3.68729595 L41.7475693,3.68729595 L41.7475693,0.711614981 L56.7925834,0.711614981 L56.7925834,6.82960641 L44.889882,6.82960641 L44.889882,12.6143164 L53.8169092,12.6143164 L53.8169092,15.5899973 L56.7925834,15.5899973 L56.7925834,27.6593507 L53.8169092,27.6593507 L53.8169092,30.6588166 Z M38.9385313,0.806844685 L41.7475693,0.806844685 L41.7475693,3.68729595 L38.9385313,3.68729595 L38.9385313,0.806844685 Z M38.7719018,15.7566493 L41.747576,15.7566493 L41.747576,18.7323078 L38.7719018,18.7323078 L38.7719018,15.7566493 Z M53.8169159,12.6143164 L56.7925901,12.6143164 L56.7925901,15.5899973 L53.8169159,15.5899973 L53.8169159,12.6143164 Z M53.8169159,27.6593507 L56.7925901,27.6593507 L56.7925901,30.5636094 L53.8169159,30.5636094 L53.8169159,27.6593507 Z" id="path1923"></path>
<path d="M74.64664,30.6588166 L59.6016259,30.6588166 L59.6016259,24.5170177 L71.5043273,24.5170177 L71.5043273,18.7323078 L62.5773023,18.7323078 L62.5773023,15.7566493 L59.6016259,15.7566493 L59.6016259,3.68729595 L62.5773023,3.68729595 L62.5773023,0.711614981 L77.6223164,0.711614981 L77.6223164,6.82960641 L65.7196151,6.82960641 L65.7196151,12.6143164 L74.64664,12.6143164 L74.64664,15.5899973 L77.6223164,15.5899973 L77.6223164,27.6593507 L74.64664,27.6593507 L74.64664,30.6588166 Z M59.7682644,0.806844685 L62.5773023,0.806844685 L62.5773023,3.68729595 L59.7682644,3.68729595 L59.7682644,0.806844685 Z M59.6016349,15.7566493 L62.5773113,15.7566493 L62.5773113,18.7323078 L59.6016349,18.7323078 L59.6016349,15.7566493 Z M74.646649,12.6143164 L77.6223254,12.6143164 L77.6223254,15.5899973 L74.646649,15.5899973 L74.646649,12.6143164 Z M74.646649,27.6593507 L77.6223254,27.6593507 L77.6223254,30.5636094 L74.646649,30.5636094 L74.646649,27.6593507 Z" id="path1925"></path>
</g>
<g id="text1575" transform="translate(0.127883, 35.473579)">
<path d="M2.35734791,22.5432945 L2.35734791,20.3720424 L0.206884567,20.3720424 L0.206884567,0.865355398 L4.62823747,0.865355398 L4.62823747,18.0974048 L8.80873794,18.0974048 L8.80873794,0.865355398 L13.2300908,0.865355398 L13.2300908,20.3720424 L11.0796275,20.3720424 L11.0796275,22.5432945 L2.35734791,22.5432945 Z M0.206884567,20.3720424 L2.35734791,20.3720424 L2.35734791,22.5432945 L0.206884567,22.5432945 L0.206884567,20.3720424 Z M11.0796275,20.3720424 L13.2300908,20.3720424 L13.2300908,22.4743558 L11.0796275,22.4743558 L11.0796275,20.3720424 Z" id="path1928"></path>
<polygon id="path1930" points="19.6814809 22.5432945 15.260128 22.5432945 15.260128 0.865355398 28.2833343 0.865355398 28.2833343 22.5432945 23.8619814 22.5432945 23.8619814 5.29401037 19.6814809 5.29401037"></polygon>
<polygon id="path1932" points="34.7347243 22.5432945 30.3133714 22.5432945 30.3133714 0.865355398 34.7347243 0.865355398"></polygon>
<polygon id="path1934" points="45.4870433 22.5432945 41.0656904 22.5432945 41.0656904 5.29401037 36.7647637 5.29401037 36.7647637 0.865355398 49.7879699 0.865355398 49.7879699 5.29401037 45.4870433 5.29401037"></polygon>
<polygon id="path1936" points="62.6907478 22.5432945 51.8180048 22.5432945 51.8180048 0.865355398 62.6907478 0.865355398 62.6907478 5.29401037 56.2393577 5.29401037 56.2393577 9.48138012 62.6907478 9.48138012 62.6907478 13.9100351 56.2393577 13.9100351 56.2393577 18.0974048 62.6907478 18.0974048"></polygon>
<path d="M69.1421378,18.0974048 L73.3226383,18.0974048 L73.3226383,5.29401037 L69.1421378,5.29401037 L69.1421378,18.0974048 Z M64.7207849,22.5432945 L64.7207849,0.865355398 L75.5935278,0.865355398 L75.5935278,3.01937283 L77.7439912,3.01937283 L77.7439912,20.3720424 L75.5935278,20.3720424 L75.5935278,22.5432945 L64.7207849,22.5432945 Z" id="path1938"></path>
</g>
</g>
<rect id="rect1579" stroke="black" stroke-width="3.99998745" x="0" y="0" width="98.6425638" height="76.0972583"></rect>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,106 @@
function getLiveCodeOptions() {
var START = `
import sketch
code = open("main.py").read()
env = dict(sketch.__dict__)
exec(code, env)
`
var 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()
`
const CANVAS_FUNCTIONS = {
circle: function(ctx, args) {
ctx.beginPath();
ctx.arc(args.x, args.y, args.d/2, 0, 2*Math.PI);
ctx.stroke();
},
line: function(ctx, args) {
ctx.beginPath();
ctx.moveTo(args.x1, args.y1);
ctx.lineTo(args.x2, args.y2);
ctx.stroke();
},
rect: function(ctx, args) {
ctx.beginPath();
ctx.rect(args.x, args.y, args.w, args.h);
ctx.stroke();
},
clear: function(ctx, args) {
var width = 300;
var height = 300;
ctx.clearRect(0, 0, width, height);
}
}
function drawOnCanvas(canvasElement, funcName, args) {
var ctx = canvasElement.getContext('2d');
var func = CANVAS_FUNCTIONS[funcName];
var scalex = canvasElement.width/300;
var scaley = canvasElement.height/300;
ctx.save();
ctx.scale(scalex, scaley);
func(ctx, args);
ctx.restore();
}
return {
runtime: "python",
files: [
{filename: "start.py", contents: START},
{filename: "sketch.py", contents: SKETCH},
],
command: ["python", "start.py"],
codemirror: true,
onMessage: {
draw: function(editor, msg) {
const canvasElement = editor.parent.querySelector("canvas");
drawOnCanvas(canvasElement, msg.function, msg.args);
}
}
}
}

View File

@@ -1,14 +0,0 @@
<div class="footer-info">
<div class="row">
<div class="footer-col-left col-sm-6 col-12">
<img src="/assets/community/images/fossunited-logo.svg" style="width: 50px;">
</div>
{# powered #}
<div class="footer-col-right col-sm-6 col-12 footer-powered">
{% block powered %}
{% include "templates/includes/footer/footer_powered.html" %}
{% endblock %}
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
<div class="footer-logo-extension">
<div class="row">
<div class="text-left col-6">
<h3>Mon School</h3>
</div>
<div class="text-right col-6">
{% block extension %}
{% include "templates/includes/footer/footer_extension.html" %}
{% endblock %}
</div>
</div>
</div>

View File

@@ -36,8 +36,9 @@ class Widgets:
'<div>Hello, World!</div>' '<div>Hello, World!</div>'
""" """
def __getattr__(self, name): def __getattr__(self, name):
widget_globals = {"widgets": self}
if not name.startswith("__"): if not name.startswith("__"):
return Widget(name) return Widget(name, widget_globals)
else: else:
raise AttributeError(name) raise AttributeError(name)
@@ -51,11 +52,13 @@ class Widget:
>>> w(name="World!") >>> w(name="World!")
'<div>Hello, World!</div>' '<div>Hello, World!</div>'
""" """
def __init__(self, name): def __init__(self, name, widget_globals={}):
self.widget_globals = widget_globals
self.name = name self.name = name
def __call__(self, **kwargs): def __call__(self, **kwargs):
# the widget could be in any of the modules # the widget could be in any of the modules
paths = [f"{module}/widgets/{self.name}.html" for module in MODULES] paths = [f"{module}/widgets/{self.name}.html" for module in MODULES]
env = get_jenv() env = get_jenv()
kwargs.update(self.widget_globals)
return env.get_or_select_template(paths).render(kwargs) return env.get_or_select_template(paths).render(kwargs)

View File

@@ -46,9 +46,7 @@
{% for m in batch.get_mentors() %} {% for m in batch.get_mentors() %}
<div> <div>
{% if m.photo_url %} {{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<img class="profile-photo" src="{{m.photo_url}}">
{% endif %}
<span class="instructor-title">{{m.full_name}}</span> <span class="instructor-title">{{m.full_name}}</span>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -55,7 +55,7 @@
<h2>Course Description</h2> <h2>Course Description</h2>
<div class="course-description"> <div class="course-description">
{{ course.short_introduction }} {{ frappe.utils.md_to_html(course.description) }}
</div> </div>
{% endmacro %} {% endmacro %}
@@ -88,7 +88,8 @@
{% if can_manage %} {% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a> <a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a>
{% else %} {% else %}
<button class="join-batch" data-batch="{{ batch.name | urlencode }}">Join this Batch</button> <button class="join-batch" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -99,7 +100,7 @@
<h2>Your Batches</h2> <h2>Your Batches</h2>
{% if mentor_batches %} {% if mentor_batches %}
<div class="message"> <div class="alert alert-secondary">
You are a mentor for this course. Manage your batches or create a new batch from here. You are a mentor for this course. Manage your batches or create a new batch from here.
</div> </div>
@@ -111,11 +112,11 @@
{% endfor %} {% endfor %}
</div> </div>
<a class="btn btn-primary add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.title}}">Add a new batch</a> <a class="btn btn-primary add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}">Add a new batch</a>
{% else %} {% else %}
<div class="mentor_message"> <div class="mentor_message">
<p> You are a mentor for this course. </p> <p> You are a mentor for this course. </p>
<a class="btn btn-primary" href="/add-a-new-batch?new=1&course={{course.title}}" >Create your first batch</a> <a class="btn btn-primary" href="/add-a-new-batch?new=1&course={{course.title}}&slug={{course.name}}" >Create your first batch</a>
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

View File

@@ -17,7 +17,7 @@ frappe.ready(() => {
$("#apply-now").click((e) => { $("#apply-now").click((e) => {
e.preventDefault(); e.preventDefault();
if (frappe.session.user == "Guest") { if (frappe.session.user == "Guest") {
window.location.href = "/login"; window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
return; return;
} }
frappe.call({ frappe.call({
@@ -53,7 +53,7 @@ frappe.ready(() => {
$(".join-batch").click((e) => { $(".join-batch").click((e) => {
e.preventDefault() e.preventDefault()
if (frappe.session.user == "Guest") { if (frappe.session.user == "Guest") {
window.location.href = "/login"; window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
return; return;
} }
batch = decodeURIComponent($(e.currentTarget).attr("data-batch")) batch = decodeURIComponent($(e.currentTarget).attr("data-batch"))
@@ -70,4 +70,3 @@ frappe.ready(() => {
}) })
}) })
}) })

View File

@@ -19,3 +19,8 @@ def get_context(context):
context.course = course context.course = course
batch = course.get_student_batch(frappe.session.user)
if batch:
frappe.local.flags.redirect_location = f"/courses/{course.name}/{batch.name}/learn"
raise frappe.Redirect

View File

@@ -14,10 +14,12 @@
<div class="batch-header"> <div class="batch-header">
{{ BatchHearder(course.name, member_count) }} {{ BatchHearder(course.name, member_count) }}
</div> </div>
<div class="message-section"> <div class="messages">
{{ Messages(messages) }} <div class="message-section">
{{ Messages(messages) }}
</div>
{{ TextArea() }}
</div> </div>
{{ TextArea() }}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,13 +1,122 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %} {% from "www/macros/sidebar.html" import Sidebar %}
{% block title %}Learn{% endblock %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ lesson.title }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" /> <meta name="description" content="{{lesson.title}} - {{course.title}}" />
<meta name="keywords" content="" /> <meta name="keywords" content="{{lesson.title}} - {{course.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 %} {% endblock %}
{% block content %} {% block content %}
{{ Sidebar(course.name, batch.name) }} {{ Sidebar(course.name, batch.name) }}
<div class="container"> <div class="container">
<div class="lesson-page">
<div class="lesson-pagination">
{% if prev_url %}
<a href="{{prev_url}}" class="btn">&larr; Prev</a>
{% endif %}
{% if next_url %}
<a href="{{next_url}}" class="btn pull-right">Next &rarr;</a>
{% endif %}
</div>
<h2>{{ lesson.title }}</h2>
{% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}">
{{ render_section(s) }}
</div>
{% endfor %}
<div class="lesson-pagination">
<a href="#" class="btn">&larr; Prev</a>
<a href="#" class="btn pull-right">Next &rarr;</a>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% macro render_section(s) %}
{% if s.type == "text" %}
{{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" or s.type == "exercise" %}
{{ LiveCodeEditor(s.name, s.get_latest_code_for_user(), s.type=="exercise", "2 hours ago") }}
{% else %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
{% endmacro %}
{% macro render_section_text(s) %}
<div class="row">
<div class="col-md-9">
{{ frappe.utils.md_to_html(s.contents) }}
</div>
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
<!-- <script type="text/javascript">
$(function() {
var editorLookup = {};
$(".canvas-editor").each((i, e) => {
var data = $(e).data();
var editor = new LiveCodeEditor(e, {
runtime: "python-canvas",
base_url: "{{ livecode_url }}",
codemirror: true,
userdata: data,
autosave: function(editor, code) {
// can't autosave when user is Guest
if (frappe.session.user == "Guest") {
return;
}
var data = editor.options.userdata;
var code = editor.codemirror.doc.getValue();
// console.log("autosaving...")
frappe.call("community.lms.api.autosave_section", {
section: data.section,
code: code
}).then((r) => {
// TODO: verify
})
}
})
editorLookup[data.section] = editor;
})
$(".canvas-editor .reset").each((i, e) => {
$(e).on("click", function(event) {
var data = $(this).parents(".canvas-editor").data();
var section = data.section;
frappe.call("community.lms.api.get_section", {
name: section
}).then(r => {
var editor = editorLookup[data.section];
editor.codemirror.doc.setValue(r.message.contents);
})
})
})
})
</script> -->
{%- endblock %}

View File

@@ -6,6 +6,9 @@ def get_context(context):
course_name = frappe.form_dict["course"] course_name = frappe.form_dict["course"]
batch_name = frappe.form_dict["batch"] batch_name = frappe.form_dict["batch"]
chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}"
course = Course.find(course_name) course = Course.find(course_name)
if not course: if not course:
@@ -17,5 +20,27 @@ def get_context(context):
frappe.local.flags.redirect_location = "/courses/" + course_name frappe.local.flags.redirect_location = "/courses/" + course_name
raise frappe.Redirect raise frappe.Redirect
if not chapter_index or not lesson_index:
frappe.local.flags.redirect_location = f"/courses/{course_name}/{batch_name}/learn/1.1"
raise frappe.Redirect
context.course = course context.course = course
context.batch = batch context.batch = batch
context.lesson = course.get_lesson(chapter_index, lesson_index)
context.lesson_index = lesson_index
context.chapter_index = chapter_index
context.livecode_url = get_livecode_url()
outline = course.get_outline()
next_ = outline.get_next(lesson_number)
prev_ = outline.get_prev(lesson_number)
context.next_url = get_learn_url(course_name, batch_name, next_)
context.prev_url = get_learn_url(course_name, batch_name, prev_)
def get_learn_url(course_name, batch_name, lesson_number):
if not lesson_number:
return
return f"/courses/{course_name}/{batch_name}/learn/{lesson_number}"
def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url")

View File

@@ -1,63 +1,75 @@
{% macro LiveCodeEditorLarge(name, code) %} {% macro LiveCodeEditorLarge(name, code) %}
<div class="livecode-editor livecode-editor-large row no-gutters" id="editor-{{name}}"> <div class="livecode-editor livecode-editor-large" id="editor-{{name}}">
<div class="col-sm"> <div class="row">
<div class="heading"> <div class="col-lg-8 col-md-6">
<button class="run">Run</button> <div class="controls">
<h2>Editor</h2> <button class="run">Run</button>
</div>
</div> </div>
<textarea class="code">{{code}}</textarea>
</div> </div>
<div class="col-sm"> <div class="code-editor">
<div class="heading"> <div class="row">
<h2>Output</h2> <div class="col-lg-8 col-md-6">
</div> <div class="code-wrapper">
<div class="canvas-wrapper"> <textarea class="code">{{code}}</textarea>
<canvas class="canvas" width="300" height="300"></canvas> </div>
<pre class="output"></pre> </div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<canvas width="300" height="300"></canvas>
<pre class="output"></pre>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro LiveCodeEditor(name, code) %} {% macro LiveCodeEditor(name, code, is_exercise, last_submitted) %}
<div class="livecode-editor canvas-editor" id="editor-{{name}}" <div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
data-section="{{name}}">
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-lg-8 col-md-6">
<div> <div class="controls">
<textarea class="code">{{code}}</textarea> <button class="run">Run</button>
<div class="livecode-controls"> <button class="reset">Reset</button>
<button type="button" class="run">Run</button> {% if is_exercise %}
<a href="javascript:;" class="reset">Reset</a> <button class="submit pull-right btn-primary">Submit</button>
<a href="javascript:;" class="clear">Clear</a> {% if last_submitted %}
<span class="pull-right" style="padding-right: 10px;">Submitted <span class="human-time" data-timestamp="{{last_submitted}}">on {{last_submitted}}</span></span>
{% endif %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="code-editor">
<div class="canvas-wrapper"> <div class="row">
<canvas class="canvas" width="150" height="150"></canvas> <div class="col-lg-8 col-md-6">
<pre class="output"></pre> <div class="code-wrapper">
<textarea class="code">{{code}}</textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<canvas width="300" height="300"></canvas>
<pre class="output"></pre>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro LiveCodeEditorJS(name, code) %} {% macro LiveCodeEditorJS(name, code) %}
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script> <script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript" src="/assets/community/js/livecode-canvas.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var livecodeEditors = []; var livecodeEditors = [];
$(function() { $(function() {
$(".livecode-editor").each((i, e) => { $(".livecode-editor").each((i, e) => {
var editor = new LiveCodeEditor(e, { var editor = new LiveCodeEditor(e, {
runtime: "python-canvas",
base_url: "{{ livecode_url }}", base_url: "{{ livecode_url }}",
codemirror: true ...getLiveCodeOptions()
}) })
livecodeEditors.push(editor); livecodeEditors.push(editor);
}) })

View File

@@ -55,7 +55,6 @@
.dashboard__details { .dashboard__details {
padding-top: 2rem; padding-top: 2rem;
width: 80%;
} }
.dashboard__course { .dashboard__course {
@@ -83,12 +82,18 @@
.dashboard__description { .dashboard__description {
height: 100px; height: 100px;
} }
@media (max-width: 900px) {
.dashboard__parent {
flex-direction: column;
}
}
</style> </style>
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}
<section>
<div class="dashboard__parent"> <div class="dashboard__parent">
<div> <div class="dashboard__photo">
{{ Profile(member.photo, member.full_name, member.abbr, "large")}} {{ Profile(member.photo, member.full_name, member.abbr, "large")}}
</div> </div>
<div class="dashboard__details"> <div class="dashboard__details">
@@ -106,40 +111,23 @@
<div> <div>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home"> <div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
<div class='container'> <div class="row">
<div class="row row-cols-1 row-cols-xl-5 row-cols-lg-4 row-cols-md-3 row-cols-sm-2"> {% if sketches %}
{% if sketches %} {% for sketch in sketches %}
{% for sketch in sketches %} <div class="col-md-4 col-sm-6">
<div class="col m-4"> {{ widgets.SketchTeaser(sketch=sketch) }}
<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> </div>
{% if not sketches %} {% endfor %}
<p class="text-center">{{member.full_name}} has not created any skecth yet.</p> {% endif %}
{% endif %}
</div> </div>
{% if not sketches %}
<p class="text-center">{{member.full_name}} has not created any skecth yet.</p>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section>
{% endblock %} {% endblock %}
<!-- this is a sample default web page template --> <!-- this is a sample default web page template -->

View File

@@ -13,7 +13,7 @@
<div class='container pb-5'> <div class='container pb-5'>
<h1>Recent Sketches</h1> <h1>Recent Sketches</h1>
<a href="/sketches/new">Create a New Sketch</a> <a href="/sketches/new" class="btn btn-primary">Create a New Sketch</a>
</div> </div>
<div class='container'> <div class='container'>
<div class="row"> <div class="row">

View File

@@ -30,14 +30,18 @@
<div class="sketch-header"> <div class="sketch-header">
{% if editable %} {% if editable %}
<input type="button" class="pull-right" id="sketch-save" value="Save"/> <div class="form-row">
<h1 class="sketch-title"> <div class="col-lg-8 col-md-6">
<input type="text" name="title" id="sketch-title" value="{{ sketch.title }}" /> <input type="text" id="sketch-title" name="title" class="form-control" value="{{ sketch.title }}">
</h1> </div>
<div class="col-lg-4 col-md-6">
<button type="submit" id="sketch-save" class="btn-save btn btn-primary">Save</button>
</div>
</div>
{% else %} {% else %}
<h1 class="sketch-title">{{sketch.title}}</h1> <h1 class="sketch-title">{{sketch.title}}</h1>
<div class="sketch-owner-wrapper">By <span class="sketch-owner">{{sketch.get_owner_name()}}</span></div>
{% endif %} {% endif %}
<div class="sketch-owner-wrapper">By <span class="sketch-owner">{{sketch.get_owner_name()}}</span></div>
</div> </div>
{% if sketch.is_new() and not editable %} {% if sketch.is_new() and not editable %}
@@ -49,7 +53,6 @@
<div class="sketch-editor"> <div class="sketch-editor">
{{LiveCodeEditorLarge(sketch.name, sketch.code) }} {{LiveCodeEditorLarge(sketch.name, sketch.code) }}
</div> </div>
</section>
{% endblock %} {% endblock %}
{%- block script %} {%- block script %}