Compare commits

..

68 Commits

Author SHA1 Message Date
Anand Chitipothu
d55941d4bb Merge branch 'main' into community-member-to-user-refactor 2021-05-24 11:39:20 +05:30
Jannat Patel
6074ee3688 Merge pull request #104 from fossunited/resume-course
Added "Resume Course" button to the course teaser
2021-05-24 11:36:06 +05:30
pateljannat
b3f87ba5b6 fix: removed community course member and references to profile macro 2021-05-24 10:39:04 +05:30
Anand Chitipothu
8d7963fc60 feat: added "Resume Course" button to course teaser
Closes #102
2021-05-24 09:55:35 +05:30
Anand Chitipothu
38938ac14b feat: added ability to find the batch of a student
Added the course field and member_email fields to LMS Batch Membership
to allow the possibility of querying if a user is a student of a course.

Closes #101
2021-05-24 09:55:13 +05:30
Anand Chitipothu
14f9d4875a chore: added issue template for Feature Request 2021-05-24 08:36:09 +05:30
pateljannat
419a7e666f refactor: tests, mentor request, messages 2021-05-22 20:49:47 +05:30
pateljannat
713dcf178a refactor: patches to fix data, profile dashboard, lms mentor mapping page fixes 2021-05-21 21:40:31 +05:30
pateljannat
637c795321 refactor: moved community member class functions to user override 2021-05-21 16:22:59 +05:30
pateljannat
63d00a46c4 Merge branch 'main' of https://github.com/frappe/community into community-member-to-user-refactor 2021-05-21 13:27:22 +05:30
pateljannat
e991dc5c73 refactor: removed community member doctype 2021-05-21 13:27:15 +05:30
Anand Chitipothu
4a2ecff15d Merge pull request #98 from fossunited/remove-primary-color-from-app
fix: removed primary color from app
2021-05-21 13:26:38 +05:30
Jannat Patel
f8d6b5b949 Merge pull request #99 from fossunited/mentor-dashboard
Refactored batch pages and added batch progress page
2021-05-21 13:25:56 +05:30
Anand Chitipothu
c77835b81f feat: added page to see progress of a batch 2021-05-21 13:13:34 +05:30
Anand Chitipothu
e04bbb633d refactor: moved courses/*/index pages to batch/* 2021-05-21 13:12:52 +05:30
Anand Chitipothu
a2b856aaf8 feat: added image to exercise submission 2021-05-21 13:10:54 +05:30
pateljannat
7a650b46ac fix: removed primary color from app 2021-05-21 11:06:37 +05:30
Jannat Patel
b61ca1d7a2 Merge pull request #97 from fossunited/gitignore-build-file
fix: gitignore dist folder
2021-05-21 10:31:40 +05:30
pateljannat
573019bbcc fix: added build dist folder to gitignore 2021-05-21 10:23:44 +05:30
Anand Chitipothu
632693c9f8 fix: show submitted solution in the exercise 2021-05-20 21:03:24 +05:30
Anand Chitipothu
463aec01f8 fix: permission issue when a student submits an exercise 2021-05-20 21:02:54 +05:30
Anand Chitipothu
e7d116f31c chore: removed obsolete doctype LMS Topic
It has been replaced by Chapter and Lesson.
Moved the section_parser from lms_topic directory to the lms.
2021-05-20 16:52:51 +05:30
Anand Chitipothu
f1b3ee19b6 Merge pull request #93 from fossunited/exercises
Added Exercise and Exercise Submission doctypes
2021-05-20 16:43:29 +05:30
Anand Chitipothu
9cb9fad05c fix: fixed failing tests 2021-05-20 16:24:41 +05:30
Anand Chitipothu
0859afdf34 style: fixed the styles of the sidebar 2021-05-20 15:08:12 +05:30
Anand Chitipothu
6407b24324 feat: added course, batch and lesson to exercise submission
Useful to find all the submissions for a batch/lesson.
2021-05-20 15:07:14 +05:30
Anand Chitipothu
34e993cf86 refactor: added lesson to exercise
usualy to know which lesson an exercise is part of by looking at the
exercise.
2021-05-20 13:27:30 +05:30
Anand Chitipothu
8c889ffb92 chore: switched to new build system for css.
- added community.bundle.less file to create css bundle
- removed the obsolete build.json in favor of new build system
- updated the paths in the books
- removed the imported urls from css files (the new build system doesn't support that)
- removed obsolete lms.css
2021-05-20 13:15:08 +05:30
Anand Chitipothu
a67ad67be1 feat: show image for the exercise
generate the image from the answer and display it along with
description. The image is geneated when the exercise is saved.
2021-05-20 12:09:12 +05:30
Anand Chitipothu
646a7b723f fix: fix broken pagination links 2021-05-19 20:12:35 +05:30
Anand Chitipothu
6f7011ca58 feat: integrated exercises into lessons
- an exercise can now be added to a lesson
- it is rendered using livecode editor with submit button
- remembers the submitted code and shows the submission time

Issue #90
2021-05-19 20:06:20 +05:30
Anand Chitipothu
d61acb552a feat: added Exercise and Exercise Submission doctypes
Also:
- added methods to submit an exercise and get the submission for a user
- added test cases

Issue #90
2021-05-19 20:01:17 +05:30
Jannat Patel
265c78e76e Merge pull request #88 from fossunited/minor-fixes
fix: removed slug field ref from courses page
2021-05-19 13:59:43 +05:30
pateljannat
7d180e141c fix: removed print statememt 2021-05-19 13:19:34 +05:30
pateljannat
29f9141ad8 fix: removed slug field ref from courses page 2021-05-19 13:16:46 +05:30
Anand Chitipothu
e6f58f56e0 Merge pull request #87 from fossunited/remove-branding
refactor: removed the branding and customization for mon school
2021-05-19 10:41:45 +05:30
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
118 changed files with 1793 additions and 1879 deletions

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

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

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@
*.egg-info
*.swp
tags
community/docs/current
community/docs/current
community/public/dist

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2021, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on('Community Course Member', {
// refresh: function(frm) {
// }
});

View File

@@ -1,96 +0,0 @@
{
"actions": [],
"autoname": "field:user_name",
"creation": "2021-03-02 11:24:49.612530",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"full_name",
"user_name",
"email",
"short_intro",
"bio",
"photo",
"route"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Full Name",
"reqd": 1
},
{
"fieldname": "user_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "User Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email",
"options": "Email",
"reqd": 1
},
{
"fieldname": "short_intro",
"fieldtype": "Data",
"label": "Short Intro"
},
{
"fieldname": "bio",
"fieldtype": "Markdown Editor",
"label": "Bio"
},
{
"fieldname": "photo",
"fieldtype": "Attach Image",
"label": "Photo"
},
{
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
}
],
"has_web_view": 1,
"index_web_pages_for_search": 1,
"is_published_field": "enabled",
"links": [],
"modified": "2021-04-06 11:50:41.551665",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Course Member",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"route": "community-course-member",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import re
from frappe.website.website_generator import WebsiteGenerator
from frappe import _
class CommunityCourseMember(WebsiteGenerator):
def get_context(self, context):
context.abbr = ("").join([ s[0] for s in self.full_name.split() ])
return context
def validate(self):
self.validate_user_name()
if not self.route:
self.route = self.user_name
def validate_user_name(self):
if len(self.user_name) < 4:
frappe.throw(_("Username must be atleast 4 characters long."))
if not re.match("^[A-Za-z0-9_]*$", self.user_name):
frappe.throw(_("Username can only contain alphabets, numbers, and underscore."))
self.user_name = self.user_name.lower()
def after_insert(self):
if frappe.db.exists("User", self.email):
user = frappe.get_doc("User", self.email)
else:
user, update_password_link = self.create_user()
self.send_email(update_password_link)
def send_email(self, update_password_link):
args = {
'update_password_link': update_password_link,
'full_name': self.full_name,
}
frappe.sendmail(
recipients=self.email,
sender="Administrator",
subject=_("Set your Password"),
template="community_course_membership",
reference_doctype=self.doctype,
reference_name=self.name,
send_priority=0,
queue_separately=True,
args=args)

View File

@@ -1,25 +0,0 @@
{% extends "templates/web.html" %}
{% block page_content %}
<div class="py-20 row">
{% if photo %}
<div class="col-sm-2 border border-dark">
<img src="{{ photo }}" alt="{{ full_name }}">
</div>
{% else %}
<div class="col-sm-2">
<div class="standard-image" style="font-size: 30px;">{{ abbr }}</div>
</div>
{% endif %}
<div class="col">
<h1>{{ full_name }}</h1>
{% if short_intro %}
<p class="lead"> {{ short_intro }} </p>
{% endif %}
{% if bio %}
<p class="markdown-style"> {{ frappe.utils.md_to_html(bio) }} </p>
{% endif %}
</div>
</div>
{% endblock %}
<!-- this is a sample default web page template -->

View File

@@ -1,4 +0,0 @@
<div>
<a href="{{ doc.route }}">{{ doc.full_name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestCommunityCourseMember(unittest.TestCase):
pass

View File

@@ -13,13 +13,13 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Member",
"options": "Community Member"
"options": "User"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 12:03:31.153575",
"modified": "2021-05-21 12:15:51.286478",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Event Volunteer",

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2021, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on('Community Member', {
// refresh: function(frm) {
// }
});

View File

@@ -1,155 +0,0 @@
{
"actions": [],
"allow_guest_to_view": 1,
"allow_rename": 1,
"creation": "2021-02-12 15:47:23.591567",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"full_name",
"email",
"enabled",
"column_break_4",
"username",
"email_preference",
"section_break_7",
"bio",
"section_break_9",
"role",
"photo",
"column_break_12",
"short_intro",
"route",
"abbr"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Full Name",
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "role",
"fieldtype": "Select",
"label": "Role",
"options": "\nBoard\nDirector\nVolunteer\nSpeaker"
},
{
"allow_in_quick_entry": 1,
"fieldname": "photo",
"fieldtype": "Attach Image",
"label": "Photo"
},
{
"fieldname": "short_intro",
"fieldtype": "Data",
"label": "Short Intro"
},
{
"allow_in_quick_entry": 1,
"fieldname": "bio",
"fieldtype": "Markdown Editor",
"label": "Bio"
},
{
"fieldname": "route",
"fieldtype": "Data",
"label": "Route"
},
{
"fieldname": "email",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Email",
"options": "Email",
"reqd": 1,
"unique": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "username",
"fieldtype": "Data",
"in_list_view": 1,
"label": "User Name",
"unique": 1
},
{
"fieldname": "email_preference",
"fieldtype": "Select",
"label": "Email preference",
"options": "Email on every Message\nOne Digest Mail per day\nNever"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "abbr",
"fieldtype": "Data",
"label": "Abbr",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-28 11:22:35.402217",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Member",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"search_fields": "full_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}

View File

@@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import re
from frappe import _
from frappe.model.document import Document
import random
from frappe.utils import cint
import hashlib
class CommunityMember(Document):
def validate(self):
self.validate_username()
self.abbr = ("").join([ s[0] for s in self.full_name.split() ])
if self.route != self.username:
self.route = self.username
def validate_username(self):
if not self.username:
self.username = create_username_from_email(self.email)
if self.username:
if len(self.username) < 4:
frappe.throw(_("Username must be atleast 4 characters long."))
if not re.match("^[A-Za-z0-9_]*$", self.username):
frappe.throw(_("Username can only contain alphabets, numbers and underscore."))
self.username = self.username.lower()
def get_course_count(self) -> int:
"""Returns the number of courses authored by this user.
"""
return frappe.db.count(
'LMS Course', {
'owner': self.email
})
def get_batch_count(self) -> int:
"""Returns the number of batches authored by this user.
"""
return frappe.db.count(
'LMS Batch Membership', {
'member': self.name,
'member_type': 'Mentor'
})
def get_palette(self):
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
['--blue-avatar-bg', '--blue-avatar-color'],
['--green-avatar-bg', '--green-avatar-color'],
['--dark-green-avatar-bg', '--dark-green-avatar-color'],
['--red-avatar-bg', '--red-avatar-color'],
['--yellow-avatar-bg', '--yellow-avatar-color'],
['--purple-avatar-bg', '--purple-avatar-color'],
['--gray-avatar-bg', '--gray-avatar-color0']
]
encoded_name = str(self.full_name).encode("utf-8")
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
def __repr__(self):
return f"<CommunityMember: {self.email}>"
def create_member_from_user(doc, method):
username = doc.username
if ( doc.username and username_exists(doc.username)) or not doc.username:
username = create_username_from_email(doc.email)
elif len(doc.username) < 4:
username = adjust_username(doc.username)
if username_exists(username):
username = username + str(random.randint(0,9))
member = frappe.get_doc({
"doctype": "Community Member",
"full_name": doc.full_name,
"username": username,
"email": doc.email,
"route": doc.username,
"owner": doc.email
})
member.save(ignore_permissions=True)
def username_exists(username):
return frappe.db.exists("Community Member", dict(username=username))
def create_username_from_email(email):
string = email.split("@")[0]
return ''.join(e for e in string if e.isalnum())
def adjust_username(username):
return username.ljust(4, str(random.randint(0,9)))

View File

@@ -1,25 +0,0 @@
{% extends "templates/web.html" %}
{% block page_content %}
<div class="py-20 row">
{% if photo %}
<div class="col-sm-2 border border-dark">
<img src="{{ photo }}" alt="{{ full_name }}">
</div>
{% else %}
<div class="col-sm-2">
<div class="standard-image" style="font-size: 30px;">{{ abbr }}</div>
</div>
{% endif %}
<div class="col">
<h1>{{ full_name }}</h1>
{% if short_intro %}
<p class="lead"> {{ short_intro }} </p>
{% endif %}
{% if bio %}
<p class="markdown-style"> {{ frappe.utils.md_to_html(bio) }} </p>
{% endif %}
</div>
</div>
{% endblock %}
<!-- this is a sample default web page template -->

View File

@@ -1,4 +0,0 @@
<div>
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
</div>
<!-- this is a sample default list template -->

View File

@@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
from __future__ import unicode_literals
from community.lms.doctype.lms_course.test_lms_course import new_user
import frappe
import unittest
class TestCommunityMember(unittest.TestCase):
@classmethod
def setUpClass(self):
users = ["test_user@example.com","test_user1@example.com"]
for user in users:
if not frappe.db.exists("User", user):
new_user("Test User", user)
def test_member_created_from_user(self):
user = frappe.db.get_value("User","test_user@example.com", ["full_name", "email", "username"], as_dict=True)
self.assertTrue(frappe.db.exists("Community Member", {"username":user.username}))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
self.assertEqual(user.full_name, member.full_name)
self.assertEqual(member.owner, user.email)
self.assertEqual(user.username, member.username)
self.assertEqual(member.username, member.route)
def test_members_with_same_name(self):
user1 = frappe.db.get_value("User","test_user@example.com", ["email"], as_dict=True)
user2 = frappe.get_doc("User","test_user1@example.com", ["email"], as_dict=True)
self.assertTrue(frappe.db.exists("Community Member", {"email": user1.email} ))
self.assertTrue(frappe.db.exists("Community Member", {"email": user2.email }))
member1 = frappe.db.get_value("Community Member",
filters={"email": user1.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
member2 = frappe.db.get_value("Community Member",
filters={"email": user2.email},
fieldname=["full_name", "email", "owner", "username", "route"],
as_dict=True
)
self.assertEqual(member1.full_name, member2.full_name)
self.assertEqual(member1.email, user1.email)
self.assertEqual(member2.email, user2.email)
self.assertNotEqual(member1.username, member2.username)
def test_username_validations(self):
user = new_user("Tst", "tst@example.com")
self.assertTrue(frappe.db.exists("Community Member", {"email":user.email} ))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["username"],
as_dict=True
)
self.assertEqual(len(member.username), 4)
frappe.delete_doc("User", user.email)
def test_user_without_username(self):
user = new_user("Test User", "test_user2@example.com")
self.assertTrue(frappe.db.exists("Community Member", {"email":user.email} ))
member = frappe.db.get_value("Community Member",
filters={"email": user.email},
fieldname=["username"],
as_dict=True
)
self.assertTrue(member.username)
frappe.delete_doc("User", user.email)
@classmethod
def tearDownClass(self):
users = ["test_user@example.com","test_user1@example.com"]
for user in users:
if frappe.db.exists("User", user):
frappe.delete_doc("User", user)
if frappe.db.exists("Community Member", {"email": user}):
frappe.delete_doc("Community Member", {"email": user})

View File

@@ -1,16 +1 @@
import frappe
def create_members_from_users():
users = frappe.get_all("User", {"enabled": 1}, ["email"])
for user in users:
if not frappe.db.get_value("Community Member", {"email": user.email}, "name"):
doc = frappe.get_doc("User", {"email": user.email})
username = doc.username if doc.username and len(doc.username) > 3 else ("").join([ s for s in doc.full_name.split() ])
if not frappe.db.exists("Community Member", username):
member = frappe.new_doc("Community Member")
member.full_name = doc.full_name
member.username = username
member.email = doc.email
member.route = username
member.owner = doc.email
member.insert(ignore_permissions=True)

View File

@@ -1,5 +0,0 @@
frappe.ready(function() {
frappe.web_form.after_save = () => {
window.location.href = frappe.web_form.get_value("username")
}
})

View File

@@ -1,100 +0,0 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 1,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"breadcrumbs": "",
"button_label": "Save",
"creation": "2021-03-09 17:34:03.394301",
"doc_type": "Community Member",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-03-22 12:04:22.571655",
"modified_by": "Administrator",
"module": "Community",
"name": "update-profile",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "edit-profile",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_message": "Profile updated successfully.",
"success_url": "/",
"title": "Update Profile",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"label": "Full Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "username",
"fieldtype": "Data",
"hidden": 0,
"label": "User Name",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 1,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "short_intro",
"fieldtype": "Data",
"hidden": 0,
"label": "Short Intro",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "bio",
"fieldtype": "Data",
"hidden": 0,
"label": "Bio",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "photo",
"fieldtype": "Attach Image",
"hidden": 0,
"label": "Photo",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

@@ -1,7 +0,0 @@
from __future__ import unicode_literals
import frappe
def get_context(context):
# do your magic here
pass

View File

@@ -1,12 +1,14 @@
{% set color = member.get_palette() %}
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.photo %}
<img class="avatar-frame standard-image" src="{{ member.photo }}" title="{{ member.full_name }}">
</img>
{% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}"
style="background-color: var({{color[0]}}); color: var({{color[1]}});">
{{ member.abbr }}
<a href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">
</img>
{% else %}
<span class="avatar-frame standard-image" title="{{ member.full_name }}"
style="background-color: var({{color[0]}}); color: var({{color[1]}});">
{{ frappe.utils.get_abbr(member.full_name) }}
</span>
{% endif %}
</span>
{% endif %}
</span>
</a>

View File

@@ -23,7 +23,7 @@
"fieldname": "organizer",
"fieldtype": "Link",
"label": "Organizer",
"options": "Community Member"
"options": "User"
},
{
"fieldname": "year",
@@ -34,7 +34,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-14 11:43:23.515972",
"modified": "2021-05-21 12:22:26.619776",
"modified_by": "Administrator",
"module": "Hackathon",
"name": "Community Hackathon",

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from . import __version__ as app_version
from .install import APP_LOGO_URL
app_name = "community"
app_title = "Community"
@@ -12,17 +11,15 @@ app_color = "grey"
app_email = "jannat@erpnext.com"
app_license = "AGPL"
app_logo_url = APP_LOGO_URL
# Includes in <head>
# ------------------
# include js, css files in header of desk.html
app_include_css = "/assets/community/css/community.css"
app_include_js = "/assets/community/js/community.js"
# app_include_css = "/assets/community/css/community.css"
# app_include_js = "/assets/community/js/community.js"
# include js, css files in header of web template
web_include_css = "/assets/css/community.css"
web_include_css = "community.bundle.css"
# web_include_css = "/assets/community/css/community.css"
# web_include_js = "/assets/community/js/community.js"
@@ -63,7 +60,7 @@ web_include_css = "/assets/css/community.css"
# ------------
# before_install = "community.install.before_install"
after_install = "community.install.after_install"
# after_install = "community.install.after_install"
# Desk Notifications
# ------------------
@@ -87,22 +84,17 @@ after_install = "community.install.after_install"
# ---------------
# Override standard doctype classes
# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
override_doctype_class = {
"User": "community.overrides.user.CustomUser"
}
# Document Events
# ---------------
# Hook on document methods and events
doc_events = {
"User": {
"after_insert": "community.community.doctype.community_member.community_member.create_member_from_user"
},
"LMS Message": {
"after_insert": "community.lms.doctype.lms_message.lms_message.publish_message"
}
}
}
# Scheduled Tasks
# ---------------
@@ -144,11 +136,13 @@ primary_rules = [
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/dashboard", "to_route": ""},
{"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>/schedule", "to_route": "courses/schedule"},
{"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>/about", "to_route": "courses/about"}
{"from_route": "/courses/<course>/<batch>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/<batch>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/<batch>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/<batch>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/<batch>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/<batch>/progress", "to_route": "batch/progress"}
]
# Any frappe default URL is blocked by profile-rules, add it here to unblock it

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

@@ -21,3 +21,16 @@ def get_section(name):
"""
doc = frappe.get_doc("LMS Section", name)
return doc and doc.as_dict()
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('LMS Topic', {
frappe.ui.form.on('Exercise', {
// refresh: function(frm) {
// }

View File

@@ -0,0 +1,106 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-05-19 17:43:39.923430",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"description",
"code",
"answer",
"column_break_4",
"course",
"hints",
"tests",
"image",
"lesson"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course"
},
{
"columns": 4,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"columns": 4,
"fieldname": "answer",
"fieldtype": "Code",
"label": "Answer"
},
{
"fieldname": "tests",
"fieldtype": "Code",
"label": "Tests"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"columns": 4,
"fieldname": "hints",
"fieldtype": "Small Text",
"label": "Hints"
},
{
"columns": 4,
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-20 13:23:12.340928",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "title",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -0,0 +1,59 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from ..lms_sketch.livecode import livecode_to_svg
class Exercise(Document):
def before_save(self):
self.image = livecode_to_svg(None, self.answer)
def get_user_submission(self):
"""Returns the latest submission for this user.
"""
user = frappe.session.user
if not user or user == "Guest":
return
result = frappe.get_all('Exercise Submission',
fields="*",
filters={
"owner": user,
"exercise": self.name
},
order_by="creation desc",
page_length=1)
print("get_user_submission", result)
if result:
return result[0]
def submit(self, code):
"""Submits the given code as solution to exercise.
"""
user = frappe.session.user
if not user or user == "Guest":
return
old_submission = self.get_user_submission()
if old_submission and old_submission.solution == code:
return old_submission
course = frappe.get_doc("LMS Course", self.course)
batch = course.get_student_batch(user)
image = livecode_to_svg(None, code)
doc = frappe.get_doc(
doctype="Exercise Submission",
exercise=self.name,
exercise_title=self.title,
course=self.course,
lesson=self.lesson,
batch=batch and batch.name,
image=image,
solution=code)
doc.insert(ignore_permissions=True)
return doc

View File

@@ -0,0 +1,47 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
import frappe
import unittest
class TestExercise(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabExercise Submission`')
frappe.db.sql('delete from `tabExercise`')
frappe.db.sql('delete from `tabLMS Course`')
def new_exercise(self):
course = frappe.get_doc({
"doctype": "LMS Course",
"name": "test-course",
"title": "Test Course"
})
course.insert()
e = frappe.get_doc({
"doctype": "Exercise",
"name": "test-problem",
"course": course.name,
"title": "Test Problem",
"description": "draw a circle",
"code": "# draw a single cicle",
"answer": (
"# draw a single circle\n" +
"circle(100, 100, 50)")
})
e.insert()
return e
def test_exercise(self):
e = self.new_exercise()
assert e.get_user_submission() is None
def test_exercise_submission(self):
e = self.new_exercise()
submission = e.submit("circle(100, 100, 50)")
assert submission is not None
assert submission.exercise == e.name
assert submission.course == e.course
user_submission = e.get_user_submission()
assert user_submission is not None
assert user_submission.name == submission.name

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on('Exercise Submission', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,90 @@
{
"actions": [],
"creation": "2021-05-19 11:41:18.108316",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"exercise",
"solution",
"exercise_title",
"course",
"batch",
"lesson",
"image"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "Exercise"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "exercise.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "batch",
"fieldtype": "Link",
"label": "Batch",
"options": "LMS Batch"
},
{
"fetch_from": "exercise.lesson",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-21 11:28:45.833018",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",
"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
}

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ExerciseSubmission(Document):
pass

View File

@@ -1,10 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestLMSTopic(unittest.TestCase):
class TestExerciseSubmission(unittest.TestCase):
pass

View File

@@ -47,6 +47,9 @@ class InviteRequest(Document):
@frappe.whitelist(allow_guest=True)
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):
return "user"

View File

@@ -52,15 +52,8 @@ class TestInviteRequest(unittest.TestCase):
self.assertEqual(user.send_welcome_email, 0)
self.assertEqual(user.user_type, "Website User")
member = frappe.db.get_value("Community Member", {"email": "test_invite@example.com"})
self.assertTrue(member)
@classmethod
def tearDownClass(self):
if frappe.db.exists("Community Member", {"email": "test_invite@example.com"}):
frappe.delete_doc("Community Member", {"email": "test_invite@example.com"})
if frappe.db.exists("User", "test_invite@example.com"):
frappe.delete_doc("User", "test_invite@example.com")

View File

@@ -8,7 +8,10 @@
"field_order": [
"chapter",
"lesson_type",
"title"
"title",
"index_",
"body",
"sections"
],
"fields": [
{
@@ -31,11 +34,28 @@
"fieldtype": "Data",
"in_list_view": 1,
"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,
"links": [],
"modified": "2021-05-03 06:51:43.588969",
"modified": "2021-05-13 20:03:51.510605",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -3,8 +3,43 @@
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
import frappe
from frappe.model.document import Document
from ...section_parser import SectionParser
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)]
for s in self.sections:
if s.type == "exercise":
e = s.get_exercise()
e.lesson = self.name
e.save()
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.id = section.id
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

@@ -5,53 +5,76 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
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
class LMSBatch(Document):
def validate(self):
self.validate_if_mentor()
if not self.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):
short_code = frappe.db.get_value("LMS Course", self.course, "short_code")
course_batches = frappe.get_all("LMS Batch",{"course":self.course})
self.code = short_code + str(len(course_batches) + 1)
def get_mentors(self):
mentors = []
memberships = frappe.get_all(
"LMS Batch Membership",
{"batch": self.name, "member_type": "Mentor"},
["member"])
member_names = [m['member'] for m in memberships]
return find_all("Community Member", name=["IN", member_names])
return find_all("User", name=["IN", member_names])
def is_member(self, email):
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
"""
member = find("Community Member", email=email)
return member and frappe.db.exists(
"LMS Batch Membership",
{"batch": self.name, "member": member.name})
@frappe.whitelist()
def get_messages(batch):
messages = frappe.get_all("LMS Message", {"batch": batch}, ["*"], order_by="creation")
for message in messages:
message.message = frappe.utils.md_to_html(message.message)
member_email = frappe.db.get_value("Community Member", message.author, ["email"])
if member_email == frappe.session.user:
message.author_name = "You"
message.is_author = True
return messages
If member_type is specified, checks if the person is a Student/Mentor.
"""
filters = {
"batch": self.name,
"member": email
}
if member_type:
filters['member_type'] = member_type
return frappe.db.exists("LMS Batch Membership", filters)
def get_students(self):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""
memberships = frappe.get_all(
"LMS Batch Membership",
{"batch": self.name, "member_type": "Student"},
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_messages(self):
messages = frappe.get_all("LMS Message", {"batch": self.name}, ["*"], order_by="creation")
for message in messages:
message.message = frappe.utils.md_to_html(message.message)
if message.author == frappe.session.user:
message.author_name = "You"
message.is_author = True
return messages
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc({
"doctype": "LMS Message",
"batch": batch,
"author": get_member_with_email(),
"author": frappe.session.user,
"message": message
})
doc.save(ignore_permissions=True)

View File

@@ -6,11 +6,13 @@
"engine": "InnoDB",
"field_order": [
"batch",
"role",
"column_break_3",
"member",
"member_name",
"member_type"
"member_email",
"column_break_3",
"course",
"member_type",
"role"
],
"fields": [
{
@@ -27,9 +29,10 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "Community Member"
"options": "User"
},
{
"default": "Student",
"fieldname": "member_type",
"fieldtype": "Select",
"in_list_view": 1,
@@ -37,6 +40,7 @@
"options": "\nStudent\nMentor\nStaff"
},
{
"default": "Member",
"fieldname": "role",
"fieldtype": "Select",
"in_standard_filter": 1,
@@ -54,11 +58,28 @@
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "member.email",
"fieldname": "member_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Member Email",
"read_only": 1,
"read_only_depends_on": "member.email"
},
{
"fetch_from": "batch.course",
"fieldname": "course",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Course",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-26 12:52:59.826509",
"modified": "2021-05-24 09:32:04.128620",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -14,32 +14,40 @@ class LMSBatchMembership(Document):
self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self):
previous_membership = frappe.db.get_value("LMS Batch Membership", {"member": self.member, "batch": self.batch, "name": ["!=", self.name]}, ["member_type","member"], as_dict=1)
previous_membership = frappe.db.get_value("LMS Batch Membership",
filters={
"member": self.member,
"batch": self.batch, "name": ["!=", self.name]
},
fieldname=["member_type","member"],
as_dict=1)
if previous_membership:
member_name = frappe.db.get_value("Community Member", self.member, "full_name")
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch))
def validate_membership_in_different_batch_same_course(self):
course = frappe.db.get_value("LMS Batch", self.batch, "course")
previous_membership = frappe.get_all("LMS Batch Membership", {"member": self.member}, ["batch", "member_type"])
previous_membership = frappe.get_all("LMS Batch Membership",
filters={
"member": self.member
},
fields=["batch", "member_type"]
)
for membership in previous_membership:
batch_course = frappe.db.get_value("LMS Batch", membership.batch, "course")
if batch_course == course and (membership.member_type == "Student" or self.member_type == "Student"):
member_name = frappe.db.get_value("Community Member", self.member, "full_name")
member_name = frappe.db.get_value("User", 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.whitelist()
def create_membership(batch, course=None, member=None, member_type="Student", role="Member"):
if not member:
member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name")
def create_membership(batch, member=None, member_type="Student", role="Member"):
frappe.get_doc({
"doctype": "LMS Batch Membership",
"batch": batch,
"role": role,
"member_type": member_type,
"member": member
"member": member or frappe.session.user
}).save(ignore_permissions=True)
if course:
course_slug = frappe.db.get_value("LMS Course", {"title": course}, ["name"])
return course_slug
return "OK"

View File

@@ -8,7 +8,6 @@
"engine": "InnoDB",
"field_order": [
"title",
"slug",
"is_published",
"column_break_3",
"short_code",
@@ -42,14 +41,6 @@
"fieldtype": "Data",
"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",
"fieldtype": "Column Break"
@@ -93,7 +84,7 @@
"link_fieldname": "course"
}
],
"modified": "2021-05-06 11:15:45.728976",
"modified": "2021-05-06 13:37:03.318829",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
@@ -112,7 +103,7 @@
"write": 1
}
],
"search_fields": "slug",
"search_fields": "title",
"sort_field": "creation",
"sort_order": "DESC",
"title_field": "title",

View File

@@ -28,45 +28,22 @@ class LMSCourse(Document):
def generate_slug(self, title):
result = frappe.get_all(
'LMS Course',
fields=['slug'])
slugs = set([row['slug'] for row in result])
fields=['name'])
slugs = set([row['name'] for row in result])
return slugify(title, used_slugs=slugs)
def __repr__(self):
return f"<Course#{self.name}>"
def get_topic(self, slug):
"""Returns the topic with given slug in this course as a Document.
"""
result = frappe.get_all(
"LMS Topic",
filters={"course": self.name, "slug": slug})
if result:
row = result[0]
return frappe.get_doc('LMS Topic', row['name'])
def has_mentor(self, email):
"""Checks if this course has a mentor with given email.
"""
if not email or email == "Guest":
return False
member = self.get_community_member(email)
if not member:
return False
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": member})
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
return mapping != []
def get_community_member(self, email):
"""Returns the name of Community Member document for a give user.
"""
try:
return frappe.db.get_value("Community Member", {"email": email}, "name")
except frappe.DoesNotExistError:
return None
def add_mentor(self, email):
"""Adds a new mentor to the course.
"""
@@ -79,14 +56,10 @@ class LMSCourse(Document):
if self.has_mentor(email):
return
member = self.get_community_member(email)
if not member:
return False
doc = frappe.get_doc({
"doctype": "LMS Course Mentor Mapping",
"course": self.name,
"mentor": member
"mentor": email
})
doc.insert()
@@ -96,7 +69,7 @@ class LMSCourse(Document):
course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors:
member = frappe.get_doc("Community Member", mentor.mentor)
member = frappe.get_doc("User", mentor.mentor)
# TODO: change this to count query
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
course_mentors.append(member)
@@ -107,16 +80,32 @@ class LMSCourse(Document):
"""
if not email:
return False
member = self.get_community_member(email)
return frappe.db.exists({
"doctype": "LMS Course Mentor Mapping",
"course": self.name,
"mentor": member
"mentor": email
})
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
batch_name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"course": self.name,
"member_type": "Student",
"member": email
},
fieldname="batch")
return batch_name and frappe.get_doc("LMS Batch", batch_name)
def get_instructor(self):
member_name = self.get_community_member(self.owner)
return frappe.get_doc("Community Member", member_name)
return frappe.get_doc("User", self.owner)
def get_chapters(self):
"""Returns all chapters of this course.
@@ -131,10 +120,9 @@ class LMSCourse(Document):
batches = find_all("LMS Batch", course=self.name)
if mentor:
# TODO: optimize this
member = self.get_community_member(email=mentor)
memberships = frappe.db.get_all(
"LMS Batch Membership",
{"member": member},
{"member": mentor},
["batch"])
batch_names = {m.batch for m in memberships}
return [b for b in batches if b.name in batch_names]
@@ -148,3 +136,59 @@ class LMSCourse(Document):
visibility="Public")
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

@@ -11,7 +11,6 @@ class TestLMSCourse(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabLMS Course Mentor Mapping`')
frappe.db.sql('delete from `tabLMS Course`')
frappe.db.sql('delete from `tabCommunity Member`')
def new_course(self, title):
doc = frappe.get_doc({

View File

@@ -23,7 +23,7 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Mentor",
"options": "Community Member"
"options": "User"
},
{
"fetch_from": "mentor.full_name",
@@ -36,7 +36,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-20 12:46:48.460934",
"modified": "2021-05-21 11:48:43.340315",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Mentor Mapping",

View File

@@ -19,7 +19,7 @@
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Member",
"options": "Community Member"
"options": "User"
},
{
"fieldname": "course",
@@ -52,7 +52,7 @@
"fieldname": "reviewed_by",
"fieldtype": "Link",
"label": "Reviewed By",
"options": "Community Member"
"options": "User"
},
{
"fieldname": "comments",
@@ -62,7 +62,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-19 09:27:03.814016",
"modified": "2021-05-21 11:49:12.543502",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Mentor Request",

View File

@@ -25,7 +25,7 @@ class LMSMentorRequest(Document):
})
mapping.save()
def send_creation_email(self, member):
def send_creation_email(self):
email_template = self.get_email_template('mentor_request_creation')
if not email_template:
return
@@ -33,7 +33,7 @@ class LMSMentorRequest(Document):
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "slug", "title"], as_dict=True)
message = frappe.render_template(email_template.response,
{
'member_name': member.full_name,
'member_name': frappe.db.get_value("User", frappe.session.user, "full_name"),
'course_url': '/courses/' + course_details.slug,
'course': course_details.title
})
@@ -59,12 +59,10 @@ class LMSMentorRequest(Document):
'course': course_details.title
})
member_email = frappe.db.get_value("Community Member", self.member, "email")
if self.status == 'Approved' or self.status == 'Rejected':
reviewed_by = frappe.db.get_value('Community Member', self.reviewed_by, 'email')
email_args = {
"recipients": member_email,
"cc": [course_details.owner, reviewed_by],
"recipients": self.member,
"cc": [course_details.owner, self.reviewed_by],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
@@ -73,7 +71,7 @@ class LMSMentorRequest(Document):
elif self.status == 'Withdrawn':
email_args = {
"recipients": [member_email, course_details.owner],
"recipients": [self.member, course_details.owner],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
@@ -89,7 +87,7 @@ class LMSMentorRequest(Document):
def has_requested(course):
return frappe.db.count('LMS Mentor Request',
filters = {
'member': get_member().name,
'member': frappe.session.user,
'course': course,
'status': ['in', ('Pending', 'Approved')]
}
@@ -98,15 +96,14 @@ def has_requested(course):
@frappe.whitelist()
def create_request(course):
if not has_requested(course):
member = get_member()
request = frappe.get_doc({
'doctype': 'LMS Mentor Request',
'member': member.name,
'member': frappe.session.user,
'course': course,
'status': 'Pending'
})
request.save(ignore_permissions=True)
request.send_creation_email(member)
request.send_creation_email()
return 'OK'
else:
@@ -114,13 +111,12 @@ def create_request(course):
@frappe.whitelist()
def cancel_request(course):
request = frappe.get_doc('LMS Mentor Request', {'member': get_member().name, 'course': course, 'status': ['in', ('Pending', 'Approved')]})
request = frappe.get_doc('LMS Mentor Request',{
'member': frappe.session.user,
'course': course,
'status': ['in', ('Pending', 'Approved')]
}
)
request.status = 'Withdrawn'
request.save(ignore_permissions=True)
return 'OK'
def get_member():
try:
return frappe.get_doc('Community Member', {'email': frappe.session.user})
except frappe.DoesNotExistError:
return

View File

@@ -26,7 +26,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Author",
"options": "Community Member"
"options": "User"
},
{
"fieldname": "message",
@@ -58,7 +58,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-28 10:17:57.618127",
"modified": "2021-05-21 11:49:34.911479",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Message",
@@ -82,4 +82,4 @@
"sort_order": "DESC",
"title_field": "author",
"track_changes": 1
}
}

View File

@@ -7,98 +7,103 @@ import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import add_days, nowdate
from community.www.courses.utils import get_batch_members
class LMSMessage(Document):
""" def after_insert(self):
self.send_email() """
def after_insert(self):
self.publish_message()
#Todo: Adding email preference field for users
#self.send_email()
def publish_message(self):
template = self.get_message_template()
message = frappe._dict({
"author_name": self.author_name,
"message_time": frappe.utils.pretty_date(self.creation),
"message": frappe.utils.md_to_html(self.message)
})
js = """
$(".msger-input").val("");
var template = `{0}`;
var message = {1};
var session_user = ("{2}" == frappe.session.user) ? true : false;
message.author_name = session_user ? "You" : message.author_name
message.is_author = session_user;
template = frappe.render_template(template, {{
"message": message
}})
$(".message-section").append(template);
""".format(template, message, self.owner)
frappe.publish_realtime(event="eval_js", message=js, after_commit=True)
def get_message_template(self):
return """
<div class="discussion {% if message.is_author %} is-author {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<div class="text-muted">
{{ message.message_time }}
</div>
</div>
<div class="mt-5">
{{ message.message }}
</div>
</div>
"""
def send_email(self):
membership = frappe.get_all("LMS Batch Membership", {"batch": self.batch}, ["member"])
for entry in membership:
member = frappe.get_doc("User", entry.member)
if member.name != self.author:
#Todo: wrap sendmail in frappe.enqueue, else messages takes long to display.
frappe.sendmail(
recipients = member.email,
subject = _("New Message on ") + self.batch,
header = _("New Message on ") + self.batch,
template = "lms_message",
args = {
"author": self.author,
"message": frappe.utils.md_to_html(self.message),
"creation": frappe.utils.format_datetime(self.creation, "medium"),
"course": frappe.db.get_value("LMS Batch", self.batch, ["course"])
}
)
def send_email(self):
membership = frappe.get_all("LMS Batch Membership", {"batch": self.batch}, ["member"])
for entry in membership:
member = frappe.get_doc("Community Member", entry.member)
if member.name != self.author and member.email_preference == "Email on every Message":
frappe.sendmail(
recipients = member.email,
subject = _("New Message on ") + self.batch,
header = _("New Message on ") + self.batch,
template = "lms_message",
args = {
"author": self.author,
"message": frappe.utils.md_to_html(self.message),
"creation": frappe.utils.format_datetime(self.creation, "medium"),
"course": frappe.db.get_value("LMS Batch", self.batch, ["course"])
}
)
def send_daily_digest():
emails = frappe._dict()
messages = frappe.get_all("LMS Message", {"creation": [">=", add_days(nowdate(), -1)]}, ["message", "batch", "author", "creation"])
for message in messages:
membership = frappe.get_all("LMS Batch Membership", {"batch": message.batch}, ["member"])
for entry in membership:
member = frappe.db.get_value("Community Member", entry.member, ["name", "email_preference", "email"], as_dict=1)
if member.name != message.author and member.email_preference == "One Digest Mail per day":
if member.name in emails.keys():
emails[member.name]["messages"].append(message)
else:
emails[member.name] = frappe._dict({
"email": member.email,
"messages": [message]
})
for email in emails:
group_by_batch = frappe._dict()
for message in emails[email]["messages"]:
if message.batch in group_by_batch.keys():
group_by_batch[message.batch].append(message)
else:
group_by_batch[message.batch] = [message]
frappe.sendmail(
recipients = frappe.db.get_value("Community Member", email, "email"),
subject = _("Message Digest"),
header = _("Message Digest"),
template = "lms_daily_digest",
args = {
"batches": group_by_batch
},
delayed = False
)
#Todo: Optimize this
emails = frappe._dict()
messages = frappe.get_all("LMS Message", {"creation": [">=", add_days(nowdate(), -1)]}, ["message", "batch", "author", "creation"])
for message in messages:
membership = frappe.get_all("LMS Batch Membership", {"batch": message.batch}, ["member"])
for entry in membership:
member = frappe.db.get_value("User", entry.member, ["name", "email"], as_dict=1)
if member.name != message.author:
if member.name in emails.keys():
emails[member.name]["messages"].append(message)
else:
emails[member.name] = frappe._dict({
"email": member.email,
"messages": [message]
})
for email in emails:
group_by_batch = frappe._dict()
for message in emails[email]["messages"]:
if message.batch in group_by_batch.keys():
group_by_batch[message.batch].append(message)
else:
group_by_batch[message.batch] = [message]
frappe.sendmail(
recipients = frappe.db.get_value("User", email, "email"),
subject = _("Message Digest"),
header = _("Message Digest"),
template = "lms_daily_digest",
args = {
"batches": group_by_batch
},
delayed = False
)
def publish_message(doc, method):
email = frappe.db.get_value("Community Member", doc.author, "email")
template = get_message_template()
message = frappe._dict()
message.author_name = doc.author_name
message.message_time = frappe.utils.pretty_date(doc.creation)
message.message = frappe.utils.md_to_html(doc.message)
js = """
$(".msger-input").val("");
var template = `{0}`;
var message = {1};
var session_user = ("{2}" == frappe.session.user) ? true : false;
message.author_name = session_user ? "You" : message.author_name
message.is_author = session_user;
template = frappe.render_template(template, {{
"message": message
}})
$(".message-section").append(template);
""".format(template, message, email)
frappe.publish_realtime(event="eval_js", message=js, after_commit=True)
def get_message_template():
return """
<div class="discussion {% if message.is_author %} is-author {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<div class="text-muted">
{{ message.message_time }}
</div>
</div>
<div class="mt-5">
{{ message.message }}
</div>
</div>
"""

View File

@@ -9,7 +9,8 @@
"contents",
"code",
"attrs",
"index"
"index",
"id"
],
"fields": [
{
@@ -43,12 +44,17 @@
"fieldname": "index",
"fieldtype": "Int",
"label": "Index"
},
{
"fieldname": "id",
"fieldtype": "Data",
"label": "id"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-12 17:56:23.118854",
"modified": "2021-05-19 18:55:26.019625",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Section",

View File

@@ -10,6 +10,10 @@ class LMSSection(Document):
def __repr__(self):
return f"<LMSSection {self.label!r}>"
def get_exercise(self):
if self.type == "exercise":
return frappe.get_doc("Exercise", self.id)
def get_latest_code_for_user(self):
"""Returns the latest code for the logged in user.
"""

View File

@@ -3,26 +3,102 @@
import websocket
import json
from .svg import SVG
import frappe
from urllib.parse import urlparse
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 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.
"""
print("livecode_to_svg")
ws = websocket.WebSocket()
ws.settimeout(timeout)
ws.connect(livecode_ws_url)
if livecode_ws_url is None:
livecode_ws_url = get_livecode_ws_url()
msg = {
"msgtype": "exec",
"runtime": "python-canvas",
"code": code
}
ws.send(json.dumps(msg))
try:
ws = websocket.WebSocket()
ws.settimeout(timeout)
ws.connect(livecode_ws_url)
messages = _read_messages(ws)
commands = [m['cmd'] for m in messages if m['msgtype'] == 'draw']
img = draw_image(commands)
return img.tostring()
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 = []
@@ -32,17 +108,19 @@ def _read_messages(ws):
if not msg:
break
messages.append(json.loads(msg))
except websocket.WebSocketTimeoutException:
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=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':
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':
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

View File

@@ -9,6 +9,11 @@ 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):
@@ -48,8 +53,9 @@ class LMSSketch(Document):
else:
ws_url = self.get_livecode_ws_url()
value = livecode.livecode_to_svg(ws_url, self.code)
cache.set(key, value)
return value
if value:
cache.set(key, value)
return value or DEFAULT_IMAGE
@staticmethod
def get_recent_sketches(limit=100, owner=None):
@@ -77,7 +83,7 @@ def save_sketch(name, title, code):
doc.title = title
doc.code = code
doc.runtime = 'python-canvas'
doc.insert()
doc.insert(ignore_permissions=True)
status = "created"
else:
doc = frappe.get_doc("LMS Sketch", name)

View File

@@ -1,89 +0,0 @@
{
"actions": [],
"allow_guest_to_view": 1,
"creation": "2021-03-02 07:20:41.686573",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"title",
"slug",
"preview",
"description",
"order",
"sections"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "order",
"fieldtype": "Int",
"label": "Order"
},
{
"fieldname": "preview",
"fieldtype": "Markdown Editor",
"label": "Preview"
},
{
"fieldname": "sections",
"fieldtype": "Table",
"label": "Sections",
"options": "LMS Section"
},
{
"description": "The slug of the topic. Autogenerated from the title if not specified.",
"fieldname": "slug",
"fieldtype": "Data",
"label": "Slug"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-06 14:12:48.514062",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Topic",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "title",
"sort_field": "creation",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1,
"track_seen": 1,
"track_views": 1
}

View File

@@ -1,43 +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 frappe
from frappe.model.document import Document
from .section_parser import SectionParser
from ...utils import slugify
class LMSTopic(Document):
def before_save(self):
course = self.get_course()
if not self.slug:
self.slug = self.generate_slug(title=self.title)
sections = SectionParser().parse(self.description or "")
self.sections = [self.make_lms_section(i, s) for i, s in enumerate(sections)]
def get_course(self):
return frappe.get_doc("LMS Course", self.course)
def generate_slug(self, title):
result = frappe.get_all(
'LMS Topic',
filters={'course': self.course},
fields=['slug'])
slugs = set([row['slug'] for row in result])
return slugify(title, used_slugs=slugs)
def get_sections(self):
return sorted(self.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

View File

@@ -1,4 +1,9 @@
"""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 dataclasses import dataclass

View File

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

View File

@@ -6,8 +6,14 @@
</div>
</div>
<div class="course-footer">
{% set batch = course.get_student_batch(frappe.session.user) %}
{% if batch %}
<a class="btn btn-secondary pull-right" href="/courses/{{course.name}}/{{batch.name}}/learn">Resume Course</a>
{% endif %}
<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>

View File

@@ -0,0 +1,18 @@
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
<div class="exercise">
<h2>{{ exercise.title }}</h2>
<div class="exercise-description">{{frappe.utils.md_to_html(exercise.description)}}</div>
{% if exercise.image %}
<div class="exercise-image">{{exercise.image}}</div>
{% endif %}
{% set submission = exercise.get_user_submission() %}
{{ LiveCodeEditor(exercise.name,
code=submission.solution if submission else exercise.code,
reset_code=exercise.code,
is_exercise=True,
last_submitted=submission and submission.creation) }}
</div>

View File

@@ -1,6 +1,13 @@
<form id="invite-request-form">
<input class="form-control field-width mr-5" id="invite_email" type="email" placeholder="Email Address">
<a type="submit" id="submit-invite-request" class="btn btn-primary btn-lg" href="#" role="button">Request Invite</a>
<div class="row">
<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>
<script>
frappe.ready(() => {
@@ -12,27 +19,44 @@
invite_email: invite_email
},
callback: (data) => {
$("#invite-request-form").hide();
if (data.message == "OK") {
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>
if (data.message == "invalid email") {
$(".email-validation") && $(".email-validation").remove();
if (invite_email) {
var message = `<div>
<small class="email-validation" style="color: red;">${invite_email} is not a valid email address.</small>
</div>`;
}
else if (data.message == "invite") {
var message = `<div>
<p class="lead">Email ${invite_email} has already been used to request an invitation.</p>
}
else {
var message = `<div>
<small class="email-validation" style="color: red;">Please enter an email address.</small>
</div>`;
}
}
else if (data.message == "user") {
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>
$("#invite-request-form").append(message);
}
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>`;
}
}
$(".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>
</div>
<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>

View File

@@ -0,0 +1,42 @@
import frappe
from frappe.core.doctype.user.user import User
from frappe.utils import cint
import hashlib
class CustomUser(User):
def get_course_count(self) -> int:
"""Returns the number of courses authored by this user.
"""
return frappe.db.count(
'LMS Course', {
'owner': self.email
})
def get_palette(self):
palette = [
['--orange-avatar-bg', '--orange-avatar-color'],
['--pink-avatar-bg', '--pink-avatar-color'],
['--blue-avatar-bg', '--blue-avatar-color'],
['--green-avatar-bg', '--green-avatar-color'],
['--dark-green-avatar-bg', '--dark-green-avatar-color'],
['--red-avatar-bg', '--red-avatar-color'],
['--yellow-avatar-bg', '--yellow-avatar-color'],
['--purple-avatar-bg', '--purple-avatar-color'],
['--gray-avatar-bg', '--gray-avatar-color0']
]
encoded_name = str(self.full_name).encode("utf-8")
hash_name = hashlib.md5(encoded_name).hexdigest()
idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
return palette[idx % 8]
def get_batch_count(self) -> int:
"""Returns the number of batches authored by this user.
"""
return frappe.db.count(
'LMS Batch Membership', {
'member': self.name,
'member_type': 'Mentor'
})

View File

@@ -2,3 +2,7 @@ community.patches.set_email_preferences
community.patches.change_name_for_community_members
community.patches.save_abbr_for_community_members
community.patches.create_mentor_request_email_templates
community.patches.replace_member_with_user_in_batch_membership
community.patches.replace_member_with_user_in_course_mentor_mapping
community.patches.replace_member_with_user_in_lms_message
community.patches.replace_member_with_user_in_mentor_request

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_batch_membership")
memberships = frappe.get_all("LMS Batch Membership", ["member", "name"])
for membership in memberships:
email = frappe.db.get_value("Community Member", membership.member, "email")
frappe.db.set_value("LMS Batch Membership", membership.name, "member", email)

View File

@@ -0,0 +1,9 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_course_mentor_mapping")
mappings = frappe.get_all("LMS Course Mentor Mapping", ["mentor", "name"])
for mapping in mappings:
email = frappe.db.get_value("Community Member", mapping.mentor, "email")
frappe.db.set_value("LMS Course Mentor Mapping", mapping.name, "mentor", email)

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_message")
messages = frappe.get_all("LMS Message", ["author", "name"])
for message in messages:
user = frappe.db.get_value("Community Member", message.author, ["email", "full_name"], as_dict=True)
frappe.db.set_value("LMS Message", message.name, "author", user.email)
frappe.db.set_value("LMS Message", message.name, "author_name", user.full_name)

View File

@@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("lms", "doctype", "lms_mentor_request")
requests = frappe.get_all("LMS Mentor Request", ["member", "name"])
for request in requests:
user = frappe.db.get_value("Community Member", request.member, ["email", "full_name"], as_dict=True)
frappe.db.set_value("LMS Mentor Request", request.name, "member", user.email)
frappe.db.set_value("LMS Mentor Request", request.name, "member_name", user.full_name)

View File

@@ -1,10 +0,0 @@
{
"css/lms.css": [
"public/css/lms.css"
],
"css/community.css": [
"public/css/style.css",
"public/css/vars.css",
"public/css/style.less"
]
}

View File

@@ -0,0 +1,4 @@
@import "./style.css";
@import "./vars.css";
@import "./style.less";

View File

@@ -1,136 +0,0 @@
.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 {
background: #eee;
padding: 10px;
clear: both;
color: #212529;
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 {
font-size: 1.5em;
margin-bottom: 0px;
}
.sketch-header {
margin-bottom: 1em;
}
.sketch-card .sketch-title a {
font-weight: bold;
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 {
display: none;
}
.livecode-controls {
margin-left: 2em;
}
.livecode-controls a {
margin-left: 10px;
color: #666;
}
.canvas-editor {
margin: 10px 0px;
}

View File

@@ -1,5 +1,3 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css");
@import url("https://use.fontawesome.com/releases/v5.13.0/css/all.css");
:root {
--c1: #fefae0;
@@ -25,8 +23,6 @@
--cta-color: var(--c4);
--send-message: var(--c7);
--received-message: var(--c8);
--primary-color: #08B74F;
}
body {
@@ -77,7 +73,7 @@ body {
.lessons {
padding-left: 20px;
}
.lesson {
.lessons .lesson {
margin: 5px 0px;
font-weight: bold;
}
@@ -229,3 +225,8 @@ img.profile-photo {
font-weight: 600;
line-height: 51px;
}
.anchor_style {
text-decoration: none;
color: inherit;
}

View File

@@ -1,10 +1,3 @@
@primary-color: #08B74F;
@import url('https://rsms.me/inter/inter.css');
body {
font-family: "Inter", sans-serif;
}
h2 {
margin: 20px 0px;
color: black;
@@ -232,3 +225,113 @@ section.lightgray {
color: inherit;
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;
}
.lesson-pagination {
clear: both;
}
.exercise-image svg {
width: 200px;
height: 200px;
border: 1px solid #ddd;
margin-bottom: 20px;
}
.svg-200 svg {
width: 200px;
height: 200px;
}
.livecode-editor-small .livecode-editor {
.CodeMirror-scroll {
max-height: 160px;
min-height: 160px;
}
canvas {
width: 150px;
height: 150px;
}
}
.mentor-dashboard {
margin-top: 20px;
.submission {
margin: 40px 0px 0px 20px;
}
}

View File

@@ -1,6 +1,5 @@
/* Define all your css variables here. */
:root {
--primary-color: #08B74F;
--tag-color: #737373;
--sidebar-bg: #F6F6F6;
--sidebar-border: #C4C4C4;

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

@@ -0,0 +1,13 @@
<div class="discussion {% if message.is_author %} is-author {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<div class="text-muted">
{{ message.message_time }}
</div>
</div>
<div class="mt-5">
{{ message.message }}
</div>
</div>

View File

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

View File

@@ -6,10 +6,13 @@
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
{{ Sidebar(course.name, batch.name) }}
{{ Sidebar(course, batch) }}
<div class="container">
{{ CourseBasicDetail(course)}}
{{ InstructorsSection(course.get_instructor()) }}
@@ -46,9 +49,7 @@
{% for m in batch.get_mentors() %}
<div>
{% if m.photo_url %}
<img class="profile-photo" src="{{m.photo_url}}">
{% endif %}
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}

View File

@@ -0,0 +1,7 @@
import frappe
from . import utils
def get_context(context):
utils.get_common_context(context)
print("context", context)

View File

@@ -6,18 +6,23 @@
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
{{ Sidebar(course_slug, batch_code) }}
{{ Sidebar(course, batch) }}
<div class="">
<div class="batch-header">
{{ BatchHearder(course.name, member_count) }}
{{ BatchHearder(course.title, member_count) }}
</div>
<div class="message-section">
{{ Messages(messages) }}
<div class="messages">
<div class="message-section">
{{ Messages(messages) }}
</div>
{{ TextArea() }}
</div>
{{ TextArea() }}
</div>
{% endblock %}
@@ -44,4 +49,4 @@
<input type="text" class="msger-input" placeholder="Write your message...">
<button type="submit" class="msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button>
</form>
{% endmacro %}
{% endmacro %}

View File

@@ -30,4 +30,4 @@ frappe.ready(() => {
$(".msger-input").val("");
}
})
})
})

View File

@@ -0,0 +1,6 @@
import frappe
from . import utils
def get_context(context):
utils.get_common_context(context)
context.messages = context.batch.get_messages()

View File

@@ -1,12 +1,15 @@
{% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditor with context %}
{% block title %}{{topic.title}} ({{course.title}}){% endblock %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ lesson.title }}{% endblock %}
{% block head_include %}
<meta name="description" content="Topic {{topic.title}} of the course {{course.title}}" />
<meta name="keywords" content="course {{course.title}} {{topic.title}}" />
<meta name="description" content="{{lesson.title}} - {{course.title}}" />
<meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
<style>
</style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/assets/css/lms.css">
@@ -18,33 +21,40 @@
<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="/courses">Courses</a></li>
<li class="breadcrumb-item" aria-current="page"><a href="/courses/{{course.slug}}">{{course.title}}</a></li>
</ol>
</nav>
{{ Sidebar(course, batch) }}
<h1>{{ topic.title }}</h1>
<div class="container">
<div class="lesson-page">
{{ pagination(prev_url, next_url) }}
{% for s in topic.get_sections() %}
<h2>{{ lesson.title }}</h2>
{% for s in lesson.get_sections() %}
<div class="section section-{{ s.type }}">
{{ render_section(s) }}
</div>
{% endfor %}
</div>
</section>
{{ pagination(prev_url, next_url) }}
</div>
</div>
{% endblock %}
{% macro render_section(s) %}
{% if s.type == "text" %}
{{ render_section_text(s) }}
{% elif s.type == "example" or s.type == "code" %}
{{ LiveCodeEditor(s.name, s.get_latest_code_for_user()) }}
{{ LiveCodeEditor(s.name,
code=s.get_latest_code_for_user(),
reset_code=s.contents,
is_exercise=False)
}}
{% elif s.type == "exercise" %}
{{ widgets.Exercise(exercise=s.get_exercise())}}
{% else %}
<div>Unknown section type: {{s.type}}</div>
{% endif %}
@@ -58,10 +68,24 @@
</div>
{% endmacro %}
{% macro pagination(prev_url, next_url) %}
<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 style="clear: both;"></div>
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript">
{{ LiveCodeEditorJS() }}
<!-- <script type="text/javascript">
$(function() {
var editorLookup = {};
@@ -104,5 +128,5 @@
})
})
})
</script>
</script> -->
{%- endblock %}

View File

@@ -0,0 +1,31 @@
import frappe
from . import utils
def get_context(context):
utils.get_common_context(context)
chapter_index = frappe.form_dict.get("chapter")
lesson_index = frappe.form_dict.get("lesson")
lesson_number = f"{chapter_index}.{lesson_index}"
course_name = context.course.name
batch_name = context.batch.name
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.lesson = context.course.get_lesson(chapter_index, lesson_index)
context.lesson_index = lesson_index
context.chapter_index = chapter_index
print(context.lesson)
outline = context.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}"

View File

@@ -0,0 +1,39 @@
{% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/common_macro.html" import BatchHearder %}
{% block title %}Members{% endblock %}
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
{{ Sidebar(course, batch) }}
<div class="container">
{{ BatchHearder(course.title, member_count)}}
{{ MembersList(members)}}
</div>
{% endblock %}
{% macro MembersList(members) %}
<div class="mt-5">
{% for member in members %}
<div class="d-flex align-items-center">
<div>
{{ widgets.Avatar(member=member, avatar_class="avatar-medium") }}
</div>
<div class="ml-5 mr-5">
<a href="/{{member.username}}">{{ member.full_name }}</a>
</div>
{% if course.is_mentor(member.name) %}
<div class="badge badge-success">Mentor</div>
{% endif %}
</div>
<hr>
{% endfor %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,5 @@
import frappe
from . import utils
def get_context(context):
utils.get_common_context(context)

View File

@@ -0,0 +1,52 @@
{% extends "templates/base.html" %}
{% from "www/macros/sidebar.html" import Sidebar %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ course.title }} - Batch Dashboard{% endblock %}
{% block head_include %}
<meta name="description" content="{{course.title}} - Batch Dashboard" />
<meta name="keywords" content="{{course.title}} - Batch Dashboard" />
<style>
</style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<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 %}
{{ Sidebar(course, batch) }}
<div class="container">
<div class="mentor-dashboard">
<h1>Batch Progress</h1>
{% for exercise in report.exercises %}
<div class="exercise-submissions">
<h2>{{exercise.title}}</h2>
{% for s in report.get_submissions_of_exercise(exercise.name) %}
<div class="submission">
<h4><a href="/{{s.owner.username}}">{{s.owner.full_name}}</a></h4>
<div class="livecode-editor-small">
{{ LiveCodeEditor(name=s.name, code=s.solution, reset_code=s.solution) }}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{%- block script %}
{{ super() }}
{{ LiveCodeEditorJS() }}
{% endblock %}

View File

@@ -0,0 +1,64 @@
import frappe
from community.lms.models import Course
from collections import defaultdict
from . import utils
def get_context(context):
utils.get_common_context(context)
exercise_name = frappe.form_dict.get("exercise")
if exercise_name:
exercise = frappe.get_doc("Exercise", exercise_name)
else:
exercise = None
context.exercise = exercise
context.report = BatchReport(context.course, context.batch)
class BatchReport:
def __init__(self, course, batch):
self.submissions = get_submissions(batch)
self.exercises = self.get_exercises(course.name)
self.submissions_by_exercise = defaultdict(list)
for s in self.submissions:
self.submissions_by_exercise[s.exercise].append(s)
def get_exercises(self, course_name):
return frappe.get_all("Exercise", {"course": course_name}, ["name", "title"])
def get_submissions_of_exercise(self, exercise_name):
return self.submissions_by_exercise[exercise_name]
def get_submissions(batch):
students = batch.get_students()
students_map = {s['email']: s for s in students}
names, values = nparams("s", students_map.keys())
sql = """
select owner, exercise, name, solution, creation, image
from (
select owner, exercise, name, solution, creation, image,
row_number() over (partition by owner, exercise order by creation desc) as ix
from `tabExercise Submission`) as t
where t.ix=1 and owner IN {}
""".format(names)
data = frappe.db.sql(sql, values=values, as_dict=True)
for row in data:
row['owner'] = students_map[row['owner']]
return data
def nparams(name, values):
"""Creates n paramters from a list of values for a db query.
>>> nparams("name", ["a", "b])
("(%(name_1)s, %(name_2)s)", {"name_1": "a", "name_2": "b"})
"""
keys = [f"{name}_{i}" for i, _ in enumerate(values, start=1)]
param_names = [f"%({k})s" for k in keys]
param_values = dict(zip(keys, values))
joined_names = "(" + ", ".join(param_names) + ")"
return joined_names, param_values

View File

@@ -4,10 +4,11 @@
{% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %}
{% block content %}
{{ Sidebar(course, batch_code) }}
{{ Sidebar(course, batch) }}
<div class="container">
</div>
{% endblock %}

View File

@@ -3,7 +3,6 @@ from community.lms.models import Course
def get_context(context):
context.no_cache = 1
course_name = frappe.form_dict["course"]
batch_name = frappe.form_dict["batch"]

View File

@@ -1,7 +1,7 @@
import frappe
from community.lms.models import Course
def get_context(context):
def get_common_context(context):
context.no_cache = 1
course_name = frappe.form_dict["course"]
@@ -19,3 +19,10 @@ def get_context(context):
context.course = course
context.batch = batch
context.members = batch.get_mentors() + batch.get_students()
context.member_count = len(context.members)
context.livecode_url = get_livecode_url()
def get_livecode_url():
return frappe.db.get_single_value("LMS Settings", "livecode_url")

View File

@@ -55,7 +55,7 @@
<h2>Course Description</h2>
<div class="course-description">
{{ course.short_introduction }}
{{ frappe.utils.md_to_html(course.description) }}
</div>
{% endmacro %}
@@ -88,7 +88,8 @@
{% if can_manage %}
<a href="/courses/{{course.name}}/{{batch.name}}/about" class="btn btn-secondary">Manage</a>
{% 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 %}
</div>
</div>
@@ -99,7 +100,7 @@
<h2>Your Batches</h2>
{% 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.
</div>
@@ -111,11 +112,11 @@
{% endfor %}
</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 %}
<div class="mentor_message">
<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>
{% endif %}
{% endmacro %}

View File

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

Some files were not shown because too many files have changed in this diff Show More