Compare commits

..

1 Commits

Author SHA1 Message Date
Anand Chitipothu
5feeb4ca0c feat: widgets interface
Widgets are reusable jinja templates which can be used in other
themplates. Widgets are written in widgets/ directory in every frappe
module and can be accessed as `{{ widgets.WidgetName(...) }}` from any
template.
2021-04-29 10:37:36 +05:30
179 changed files with 2835 additions and 4548 deletions

View File

@@ -1,20 +0,0 @@
---
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,27 +36,12 @@ jobs:
- name: setup node
uses: actions/setup-node@v2
with:
node-version: '14'
node-version: '12'
check-latest: true
- name: setup cache for bench
uses: actions/cache@v2
with:
path: ~/bench-cache
key: ${{ runner.os }}
- name: install bench
run: |
pip3 install frappe-bench
which bench
run: pip3 install frappe-bench
- name: bench init
run: |
if [ -d ~/bench-cache/bench.tgz ]
then
(cd && tar xzf ~/bench-cache/bench.tgz)
else
bench init ~/frappe-bench --skip-redis-config-generation
mkdir -p ~/bench-cache
(cd && tar czf ~/bench-cache/bench.tgz frappe-bench)
fi
run: bench init ~/frappe-bench --skip-redis-config-generation
- name: add community app to bench
working-directory: /home/runner/frappe-bench
run: bench get-app community $GITHUB_WORKSPACE
@@ -65,13 +50,10 @@ jobs:
run: bench new-site --mariadb-root-password root --admin-password admin frappe.local
- name: install community app
working-directory: /home/runner/frappe-bench
run: bench --verbose --site frappe.local install-app community
run: bench --site frappe.local install-app community
- 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,5 +3,4 @@
*.egg-info
*.swp
tags
community/docs/current
community/public/dist
community/docs/current

View File

@@ -9,51 +9,7 @@ The App has following components:
Community is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
## Development Setup
**Step 1:** Clone the repo
```
$ git clone https://github.com/fossunited/community.git
$ cd community
```
**Step 2:** Run docker-compose
```
$ docker-compose up
```
**Step 3:** Visit the website at http://localhost:8000/
You'll have to go through the setup wizard to setup the website for the first time you access it. Login using the following credentiasl to complete the setup wizard.
```
Username: Administrator
password: admin
```
TODO: Explain how to load sample data
## Stopping the server
Press `ctrl+c` in the terminal to stop the server. You can also run `docker-compose down` in another terminal to stop it.
To completely reset the instance, do the following:
```
$ docker-compose down --volumes
$ docker-compose up
```
## Making Code Changes
The dev setup is configured to reload whenever any code is changed. Just edit the code and reload the webpage.
Commit the changes in a branch and send a pull request.
## Local Setup - The Hard Way
### Local Setup
To setup the repository locally follow the steps mentioned below:
@@ -65,7 +21,7 @@ To setup the repository locally follow the steps mentioned below:
1. Map your site to localhost with the command ```bench --site community.test add-to-hosts```
1. Now open the URL http://community.test:8000/docs in your browser, you should see the app running.
### Contribution Guidelines (for The Hard Way)
### Contribution Guidelines
1. Go to the apps/community directory of your installation and execute git pull --unshallow to ensure that you have the full git repository. Also fork the fossunited/community repository on GitHub.
1. Check out a working branch in git (e.g. git checkout -b my-new-branch).
@@ -76,4 +32,4 @@ To setup the repository locally follow the steps mentioned below:
#### License
AGPL
AGPL

View File

@@ -0,0 +1,8 @@
// 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

@@ -0,0 +1,96 @@
{
"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

@@ -0,0 +1,67 @@
# -*- 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)
def create_user(self):
user = frappe.get_doc({
"doctype": "User",
"email": self.email,
"first_name": self.full_name.split(" ")[0],
"full_name": self.full_name,
"username": self.user_name,
"send_welcome_email": 0,
"user_type": 'Website User',
"redirect_url": self.name
})
user.save(ignore_permissions=True)
update_password_link = user.reset_password()
return user, update_password_link

View File

@@ -0,0 +1,25 @@
{% 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

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

View File

@@ -0,0 +1,10 @@
# -*- 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": "User"
"options": "Community Member"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-21 12:15:51.286478",
"modified": "2021-02-15 12:03:31.153575",
"modified_by": "Administrator",
"module": "Community",
"name": "Community Event Volunteer",

View File

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

View File

@@ -0,0 +1,154 @@
{
"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",
"role",
"short_intro",
"section_break_7",
"bio",
"section_break_9",
"username",
"photo",
"column_break_12",
"email_preference",
"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
},
{
"fieldname": "username",
"fieldtype": "Data",
"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
}
],
"has_web_view": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-16 10:22:46.837311",
"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

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.website.website_generator import WebsiteGenerator
import re
from frappe import _
from frappe.model.rename_doc import rename_doc
class CommunityMember(WebsiteGenerator):
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 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 __repr__(self):
return f"<CommunityMember: {self.email}>"
def create_member_from_user(doc, method):
member = frappe.get_doc({
"doctype": "Community Member",
"full_name": doc.full_name,
"username": doc.username if len(doc.username) > 3 else ("").join([ s for s in doc.full_name.split() ]),
"email": doc.email,
"route": doc.username,
"owner": doc.email
})
member.save(ignore_permissions=True)

View File

@@ -0,0 +1,25 @@
{% 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

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

View File

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

View File

@@ -1 +1,17 @@
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):
print(doc.email, 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

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

View File

@@ -0,0 +1,100 @@
{
"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

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

View File

@@ -1,14 +0,0 @@
{% set color = member.get_palette() %}
<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>
</a>

View File

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

View File

@@ -3,14 +3,13 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
# import frappe
from frappe.model.document import Document
from frappe import _
class CommunityProjectMember(Document):
def validate(self):
self.validate_if_already_member()
def validate_if_already_member(self):
if frappe.get_all("Community Project Member", {"owner": self.owner}):
frappe.throw(_("You have already applied for the membership of this project."))

View File

@@ -10,16 +10,15 @@ app_icon = "octicon octicon-file-directory"
app_color = "grey"
app_email = "jannat@erpnext.com"
app_license = "AGPL"
# 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 = "community.bundle.css"
web_include_css = "/assets/css/community.css"
# web_include_css = "/assets/community/css/community.css"
# web_include_js = "/assets/community/js/community.js"
@@ -84,17 +83,19 @@ web_include_css = "community.bundle.css"
# ---------------
# Override standard doctype classes
override_doctype_class = {
"User": "community.overrides.user.CustomUser"
}
# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
# Document Events
# ---------------
# Hook on document methods and events
doc_events = {
}
"User": {
"after_insert": "community.community.doctype.community_member.community_member.create_member_from_user"
}
}
# Scheduled Tasks
# ---------------
@@ -132,24 +133,20 @@ primary_rules = [
{"from_route": "/sketches/<sketch>", "to_route": "sketches/sketch"},
{"from_route": "/courses/<course>", "to_route": "courses/course"},
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
{"from_route": "/hackathons", "to_route": "hackathons"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"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>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/learn/<int:chapter>.<int:lesson>", "to_route": "batch/learn"},
{"from_route": "/courses/<course>/schedule", "to_route": "batch/schedule"},
{"from_route": "/courses/<course>/members", "to_route": "batch/members"},
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"},
{"from_route": "/courses/<course>/join", "to_route": "batch/join"}
{"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"}
]
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
whitelist = [
"/home",
"/login",
"/update-password",
"/update-profile",
@@ -162,9 +159,7 @@ whitelist = [
"/hackathons",
"/dashboard",
"/join-request"
"/add-a-new-batch",
"/new-sign-up",
"/message"
"/add-a-new-batch"
]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
@@ -176,15 +171,3 @@ profile_rules = [
website_route_rules = primary_rules + whitelist_rules + profile_rules
update_website_context = 'community.widgets.update_website_context'
## Specify the additional tabs to be included in the user profile page.
## Each entry must be a subclass of community.community.plugins.ProfileTab
# profile_tabs = []
## Specify the extension to be used to control what scripts and stylesheets
## to be included in lesson pages. The specified value must be be a
## subclass of community.community.plugins.PageExtension
# community_lesson_page_extension = None
## Markdown Macros for Lessons
# community_markdown_macro_renderers = {"Exercise": "myapp.mymodule.plugins.render_exercise"}

View File

@@ -16,32 +16,8 @@ def autosave_section(section, code):
return {"name": doc.name}
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
def get_section(name):
"""Saves the code edited in one of the sections.
"""
ex = frappe.get_doc("Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist()
def save_current_lesson(batch_name, lesson_name):
"""Saves the current lesson for a student/mentor.
"""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"batch": batch_name,
"member": frappe.session.user
},
fieldname="name")
if not name:
return
doc = frappe.get_doc("LMS Batch Membership", name)
doc.current_lesson = lesson_name
doc.save(ignore_permissions=True)
return {"current_lesson": doc.current_lesson}
doc = frappe.get_doc("LMS Section", name)
return doc and doc.as_dict()

View File

@@ -1,15 +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
class Chapter(Document):
def get_lessons(self):
rows = frappe.db.get_all("Lesson",
filters={"chapter": self.name},
fields='name',
order_by="index_")
return [frappe.get_doc('Lesson', row['name']) 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('Exercise', {
frappe.ui.form.on('Code Revision', {
// refresh: function(frm) {
// }

View File

@@ -0,0 +1,58 @@
{
"actions": [],
"creation": "2021-04-07 00:26:28.806520",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section",
"code",
"author"
],
"fields": [
{
"fieldname": "section",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Section",
"options": "LMS Section"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "author",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Author",
"options": "User"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-14 11:26:19.628317",
"modified_by": "Administrator",
"module": "LMS",
"name": "Code Revision",
"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",
"title_field": "section",
"track_changes": 1
}

View File

@@ -1,8 +1,10 @@
# -*- 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
class LMSCourseProgress(Document):
class CodeRevision(Document):
pass

View File

@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestLesson(unittest.TestCase):
class TestCodeRevision(unittest.TestCase):
pass

View File

@@ -1,123 +0,0 @@
{
"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",
"index_",
"index_label"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"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",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index",
"read_only": 1
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-01 05:22:15.656013",
"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": "index_label",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -1,59 +0,0 @@
# 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)
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

@@ -1,49 +0,0 @@
# 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",
"short_introduction": "Test Course",
"description": "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

@@ -1,8 +0,0 @@
// 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

@@ -1,90 +0,0 @@
{
"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

@@ -1,13 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from ..lesson.lesson import update_progress
class ExerciseSubmission(Document):
def after_insert(self):
course_details = frappe.get_doc("LMS Course", self.course)
if not (course_details.is_mentor(frappe.session.user) or frappe.flags.in_test):
update_progress(self.lesson)

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseSubmission(unittest.TestCase):
pass

View File

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

View File

@@ -1,88 +0,0 @@
{
"actions": [],
"creation": "2021-04-29 16:29:56.857914",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invite_email",
"signup_email",
"column_break_4",
"status",
"full_name",
"username",
"invite_code"
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "invite_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Invite Email",
"options": "Email",
"unique": 1
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"label": "Full Name"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "signup_email",
"fieldtype": "Data",
"label": "Signup Email",
"options": "Email"
},
{
"fieldname": "username",
"fieldtype": "Data",
"label": "Username"
},
{
"fieldname": "invite_code",
"fieldtype": "Data",
"label": "Invite Code"
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nApproved\nRejected\nRegistered"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-03 09:22:20.954921",
"modified_by": "Administrator",
"module": "LMS",
"name": "Invite Request",
"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,
"search_fields": "invite_email, signup_email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "invite_email",
"track_changes": 1
}

View File

@@ -1,87 +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 import _
from frappe.model.document import Document
import json
from frappe.utils.password import get_decrypted_password
class InviteRequest(Document):
def on_update(self):
if self.has_value_changed("status") and self.status == "Approved":
self.send_email()
def create_user(self, password):
full_name_split = self.full_name.split(" ")
user = frappe.get_doc({
"doctype": "User",
"email": self.signup_email,
"first_name": full_name_split[0],
"last_name": full_name_split[1] if len(full_name_split) > 1 else "",
"username": self.username,
"send_welcome_email": 0,
"user_type": "Website User",
"new_password": password
})
user.save(ignore_permissions=True)
return user
def send_email(self):
subject = _("Your request has been approved.")
args = {
"full_name": self.full_name,
"signup_form_link": "/new-sign-up?invite_code={0}".format(self.name),
"site_url": frappe.utils.get_url()
}
frappe.sendmail(
recipients=self.invite_email,
sender=frappe.db.get_single_value("LMS Settings", "email_sender"),
subject=subject,
header=[subject, "green"],
template = "lms_invite_request_approved",
args=args)
@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"
if frappe.db.exists("Invite Request", {"invite_email": invite_email}):
return "invite"
frappe.get_doc({
"doctype": "Invite Request",
"invite_email": invite_email,
"status": "Approved"
}).save(ignore_permissions=True)
return "OK"
@frappe.whitelist(allow_guest=True)
def update_invite(data):
data = frappe._dict(json.loads(data)) if type(data) == str else frappe._dict(data)
try:
doc = frappe.get_doc("Invite Request", data.invite_code)
except frappe.DoesNotExistError:
frappe.throw(_("Invalid Invite Code."))
doc.signup_email = data.signup_email
doc.username = data.username
doc.full_name = data.full_name
doc.invite_code = data.invite_code
doc.save(ignore_permissions=True)
user = doc.create_user(data.password)
if user:
doc.status = "Registered"
doc.save(ignore_permissions=True)
return "OK"

View File

@@ -1,62 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
from __future__ import unicode_literals
from community.lms.doctype.invite_request.invite_request import create_invite_request, update_invite
import frappe
import unittest
class TestInviteRequest(unittest.TestCase):
@classmethod
def setUpClass(self):
create_invite_request("test_invite@example.com")
def test_create_invite_request(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
invite = frappe.db.get_value("Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=["invite_email", "status", "signup_email"],
as_dict=True)
self.assertEqual(invite.status, "Approved")
self.assertEqual(invite.signup_email, None)
def test_create_invite_request_update(self):
if frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"}):
data = {
"signup_email": "test_invite@example.com",
"username": "test_invite",
"full_name": "Test Invite",
"password": "Test@invite",
"invite_code": frappe.db.get_value("Invite Request", {"invite_email": "test_invite@example.com"}, "name")
}
update_invite(data)
invite = frappe.db.get_value("Invite Request",
filters={"invite_email": "test_invite@example.com"},
fieldname=["invite_email", "status", "signup_email", "full_name", "username", "invite_code", "name"],
as_dict=True)
self.assertEqual(invite.signup_email, "test_invite@example.com")
self.assertEqual(invite.full_name, "Test Invite")
self.assertEqual(invite.username, "test_invite")
self.assertEqual(invite.invite_code, invite.name)
self.assertEqual(invite.status, "Registered")
user = frappe.db.get_value("User", "test_invite@example.com",
fieldname=["first_name", "username", "send_welcome_email", "user_type"],
as_dict=True)
self.assertTrue(user)
self.assertEqual(user.first_name, invite.full_name.split(" ")[0])
self.assertEqual(user.username, invite.username)
self.assertEqual(user.send_welcome_email, 0)
self.assertEqual(user.user_type, "Website User")
@classmethod
def tearDownClass(self):
if frappe.db.exists("User", "test_invite@example.com"):
frappe.delete_doc("User", "test_invite@example.com")
invite_request = frappe.db.exists("Invite Request", {"invite_email": "test_invite@example.com"})
if invite_request:
frappe.delete_doc("Invite Request", invite_request)

View File

@@ -1,98 +0,0 @@
{
"actions": [],
"autoname": "format:{####} {title}",
"creation": "2021-05-03 06:21:12.995987",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"chapter",
"lesson_type",
"include_in_preview",
"column_break_4",
"title",
"index_",
"index_label",
"section_break_6",
"body"
],
"fields": [
{
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter"
},
{
"default": "Video",
"fieldname": "lesson_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Lesson Type",
"options": "Video\nText\nQuiz"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"default": "1",
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index"
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
"label": "Body"
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
},
{
"default": "0",
"fieldname": "include_in_preview",
"fieldtype": "Check",
"label": "Include In Preview"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-11 19:03:23.138165",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",
"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

@@ -1,104 +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 ...md import markdown_to_html, find_macros
class Lesson(Document):
def before_save(self):
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
index = 1
for name in exercises:
e = frappe.get_doc("Exercise", name)
e.lesson = self.name
e.index_ = index
e.save()
index += 1
self.update_orphan_exercises(exercises)
def update_orphan_exercises(self, active_exercises):
"""Updates the exercises that were previously part of this lesson,
but not any more.
"""
linked_exercises = {row['name'] for row in frappe.get_all('Exercise', {"lesson": self.name})}
active_exercises = set(active_exercises)
orphan_exercises = linked_exercises - active_exercises
for name in orphan_exercises:
ex = frappe.get_doc("Exercise", name)
ex.lesson = None
ex.index_ = 0
ex.index_label = ""
ex.save()
def render_html(self):
return markdown_to_html(self.body)
def get_exercises(self):
if not self.body:
return []
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
return [frappe.get_doc("Exercise", name) for name in exercises]
def get_progress(self):
return frappe.db.get_value("LMS Course Progress", {"lesson": self.name, "owner": frappe.session.user}, "status")
def get_slugified_class(self):
if self.get_progress():
return ("").join([ s for s in self.get_progress().lower().split() ])
return
@frappe.whitelist()
def save_progress(lesson, batch):
if not frappe.db.exists("LMS Batch Membership",
{
"member": frappe.session.user,
"batch": batch
}):
return
if frappe.db.exists("LMS Course Progress",
{
"lesson": lesson,
"owner": frappe.session.user
}):
return
lesson_details = frappe.get_doc("Lesson", lesson)
dynamic_content = find_macros(lesson_details.body)
status = "Complete"
if dynamic_content:
status = "Partially Complete"
frappe.get_doc({
"doctype": "LMS Course Progress",
"lesson": lesson_details.name,
"status": status
}).save(ignore_permissions=True)
def update_progress(lesson):
user = frappe.session.user
if not all_dynamic_content_submitted(lesson, user):
return
if frappe.db.exists("LMS Course Progress", {"lesson": lesson, "owner": user}):
course_progress = frappe.get_doc("LMS Course Progress", {"lesson": lesson, "owner": user})
course_progress.status = "Complete"
course_progress.save(ignore_permissions=True)
def all_dynamic_content_submitted(lesson, user):
exercise_names = frappe.get_list("Exercise", {"lesson": lesson}, pluck="name", ignore_permissions=True)
all_exercises_submitted = False
query = {
"exercise": ["in", exercise_names],
"owner": user
}
if frappe.db.count("Exercise Submission", query) == len(exercise_names):
all_exercises_submitted = True
return all_exercises_submitted

View File

@@ -1,15 +1,17 @@
{
"actions": [],
"autoname": "field:title",
"creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"title",
"start_date",
"start_time",
"column_break_3",
"title",
"code",
"sessions_on",
"end_time",
"section_break_5",
@@ -25,11 +27,16 @@
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course"
},
{
"fieldname": "code",
"fieldtype": "Data",
"label": "Code",
"read_only": 1,
"unique": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
@@ -42,12 +49,10 @@
"label": "Description"
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
"options": "\nPublic\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
@@ -56,19 +61,16 @@
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
"options": "\nActive\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
"options": "\nReady\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
@@ -76,8 +78,7 @@
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_9",
@@ -85,19 +86,16 @@
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
"fieldtype": "Section Break"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
@@ -108,7 +106,6 @@
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
}
],
@@ -120,7 +117,7 @@
"link_fieldname": "batch"
}
],
"modified": "2021-05-26 16:43:57.399747",
"modified": "2021-04-21 12:45:21.144972",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch",

View File

@@ -5,87 +5,36 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from community.lms.doctype.lms_batch_membership.lms_batch_membership import create_membership
from community.query import find, find_all
from community.www.courses.utils import get_member_with_email
class LMSBatch(Document):
def validate(self):
self.validate_if_mentor()
def validate(self):
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 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 after_insert(self):
create_membership(batch=self.name, member_type="Mentor")
def get_mentors(self):
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("User", name=["IN", member_names])
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
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
def get_membership(self, email):
"""Returns the membership document of given user.
"""
name = frappe.get_value(
doctype="LMS Batch Membership",
filters={
"batch": self.name,
"member": email
},
fieldname="name")
return frappe.get_doc("LMS Batch Membership", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user.
"""
membership = self.get_membership(user)
return membership and membership.current_lesson
@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
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc({
"doctype": "LMS Message",
"batch": batch,
"author": frappe.session.user,
"author": get_member_with_email(),
"message": message
})
doc.save(ignore_permissions=True)
return doc

View File

@@ -2,13 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('LMS Batch Membership', {
onload: function(frm) {
frm.set_query('member', function(doc) {
return {
filters: {
"ignore_user_type": 1,
}
};
});
},
// refresh: function(frm) {
// }
});

View File

@@ -6,15 +6,11 @@
"engine": "InnoDB",
"field_order": [
"batch",
"role",
"column_break_3",
"member",
"member_name",
"member_username",
"column_break_3",
"course",
"member_type",
"role",
"current_lesson",
"is_current"
"member_type"
],
"fields": [
{
@@ -31,10 +27,9 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member",
"options": "User"
"options": "Community Member"
},
{
"default": "Student",
"fieldname": "member_type",
"fieldtype": "Select",
"in_list_view": 1,
@@ -42,7 +37,6 @@
"options": "\nStudent\nMentor\nStaff"
},
{
"default": "Member",
"fieldname": "role",
"fieldtype": "Select",
"in_standard_filter": 1,
@@ -60,40 +54,11 @@
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "batch.course",
"fieldname": "course",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Course",
"read_only": 1
},
{
"fieldname": "current_lesson",
"fieldtype": "Link",
"label": "Current Lesson",
"options": "Lesson"
},
{
"fetch_from": "member.username",
"fieldname": "member_username",
"fieldtype": "Data",
"label": "Memeber Username",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_current",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Currently Being Used",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-14 10:24:35.425498",
"modified": "2021-04-26 12:52:59.826509",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -8,58 +8,35 @@ from frappe.model.document import Document
from frappe import _
class LMSBatchMembership(Document):
def validate(self):
self.validate_membership_in_same_batch()
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}, ["member_type","member"], as_dict=1)
if previous_membership:
member_name = frappe.db.get_value("Community Member", self.member, "full_name")
frappe.throw(_("{0} is already a {1} of {2}").format(member_name, previous_membership.member_type, self.batch))
def validate(self):
self.validate_membership_in_same_batch()
self.validate_membership_in_different_batch_same_course()
def validate_membership_in_same_batch(self):
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("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",
filters={
"member": self.member,
"name": ["!=", self.name]
},
fields=["batch", "member_type", "name"]
)
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("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))
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"])
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")
frappe.throw(_("{0} is already a {1} of {2} course through {3} batch").format(member_name, membership.member_type, course, membership.batch))
@frappe.whitelist()
def create_membership(batch, member=None, member_type="Student", role="Member"):
frappe.get_doc({
"doctype": "LMS Batch Membership",
"batch": batch,
"role": role,
"member_type": member_type,
"member": member or frappe.session.user
}).save(ignore_permissions=True)
return "OK"
@frappe.whitelist()
def update_current_membership(batch, course, member=frappe.session.user):
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": course})
for membership in all_memberships:
frappe.db.set_value("LMS Batch Membership", membership.name, "is_current", 0)
current_membership = frappe.get_all("LMS Batch Membership", {"batch": batch, "member": member})
if len(current_membership):
frappe.db.set_value("LMS Batch Membership", current_membership[0].name, "is_current", 1)
if not member:
member = frappe.db.get_value("Community Member", {"email": frappe.session.user}, "name")
frappe.get_doc({
"doctype": "LMS Batch Membership",
"batch": batch,
"role": role,
"member_type": member_type,
"member": member
}).save(ignore_permissions=True)
return "OK"

View File

@@ -3,78 +3,8 @@
# See license.txt
from __future__ import unicode_literals
import frappe
# import frappe
import unittest
class TestLMSBatchMembership(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabLMS Batch Membership`")
frappe.db.sql("DELETE FROM `tabLMS Batch`")
frappe.db.sql('delete from `tabLMS Course Mentor Mapping`')
frappe.db.sql("DELETE FROM `tabLMS Course`")
frappe.db.sql("DELETE FROM `tabUser` where email like '%@test.com'")
def new_course_batch(self):
course = frappe.get_doc({
"doctype": "LMS Course",
"name": "test-course",
"title": "Test Course",
"short_code": "XX",
"short_introduction": "Test Course",
"description": "Test Course"
})
course.insert()
self.new_user("mentor@test.com", "Test Mentor")
# without this, the creating batch will fail
course.add_mentor("mentor@test.com")
frappe.session.user = "mentor@test.com"
batch = frappe.get_doc({
"doctype": "LMS Batch",
"name": "test-batch",
"title": "Test Batch",
"course": course.name
})
batch.insert(ignore_permissions=True)
frappe.session.user = "Administrator"
return course, batch
def new_user(self, email="test@test.com", full_name="Test User"):
user = frappe.get_doc({
"doctype": "User",
"name": email,
"email": email,
"first_name": full_name,
})
user.insert()
return user
def add_membership(self, batch_name, member_name, member_type="Student"):
doc = frappe.get_doc({
"doctype": "LMS Batch Membership",
"batch": batch_name,
"member": member_name,
"member_type": member_type
})
doc.insert()
return doc
def test_membership(self):
course, batch = self.new_course_batch()
member = self.new_user("test01@test.com")
membership = self.add_membership(batch.name, member.name)
assert membership.course == course.name
assert membership.member_name == member.full_name
def test_membership_change_role(self):
course, batch = self.new_course_batch()
member = self.new_user("test01@test.com")
membership = self.add_membership(batch.name, member.name)
# it should be possible to change role
membership.role = "Admin"
membership.save()
pass

View File

@@ -1,26 +1,15 @@
{
"actions": [
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_lessons",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Lessons"
},
{
"action": "community.lms.doctype.lms_course.lms_course.reindex_exercises",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Exercises"
}
],
"actions": [],
"allow_guest_to_view": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2021-03-01 16:49:33.622422",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"slug",
"is_published",
"column_break_3",
"short_code",
@@ -41,8 +30,7 @@
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description",
"reqd": 1
"label": "Description"
},
{
"default": "0",
@@ -55,6 +43,14 @@
"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"
@@ -71,16 +67,15 @@
{
"fieldname": "short_introduction",
"fieldtype": "Small Text",
"label": "Short Introduction",
"reqd": 1
"label": "Short Introduction"
}
],
"index_web_pages_for_search": 1,
"is_published_field": "is_published",
"links": [
{
"group": "Chapters",
"link_doctype": "Chapter",
"group": "Topics",
"link_doctype": "LMS Topic",
"link_fieldname": "course"
},
{
@@ -92,14 +87,9 @@
"group": "Mentors",
"link_doctype": "LMS Course Mentor Mapping",
"link_fieldname": "course"
},
{
"group": "Mentors",
"link_doctype": "LMS Mentor Request",
"link_fieldname": "course"
}
],
"modified": "2021-06-01 04:36:45.696776",
"modified": "2021-04-21 14:45:41.658056",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",
@@ -118,7 +108,7 @@
"write": 1
}
],
"search_fields": "title",
"search_fields": "slug",
"sort_field": "creation",
"sort_order": "DESC",
"title_field": "title",

View File

@@ -5,37 +5,33 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from ...utils import slugify
from community.query import find, find_all
from frappe.utils import flt
class LMSCourse(Document):
@staticmethod
def find(name):
"""Returns the course with specified name.
"""
return find("LMS Course", is_published=True, name=name)
def autoname(self):
if not self.name:
self.name = self.generate_slug(title=self.title)
@staticmethod
def find_all():
"""Returns all published courses.
"""
return find_all("LMS Course", is_published=True)
def before_save(self):
if not self.slug:
self.slug = self.generate_slug(title=self.title)
def generate_slug(self, title):
result = frappe.get_all(
'LMS Course',
fields=['name'])
slugs = set([row['name'] for row in result])
fields=['slug'])
slugs = set([row['slug'] for row in result])
return slugify(title, used_slugs=slugs)
def __repr__(self):
return f"<Course#{self.name}>"
return f"<Course#{self.name} {self.slug}>"
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.
@@ -43,9 +39,21 @@ class LMSCourse(Document):
if not email or email == "Guest":
return False
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
member = self.get_community_member(email)
if not member:
return False
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": member})
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.
"""
@@ -58,10 +66,14 @@ 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": email
"mentor": member
})
doc.insert()
@@ -71,199 +83,8 @@ 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("User", mentor.mentor)
member = frappe.get_doc("Community Member", 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)
return course_mentors
def is_mentor(self, email):
"""Checks if given user is a mentor for this course.
"""
if not email:
return False
return frappe.db.exists({
"doctype": "LMS Course Mentor Mapping",
"course": self.name,
"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):
return frappe.get_doc("User", self.owner)
def get_chapters(self):
"""Returns all chapters of this course.
"""
# TODO: chapters should have a way to specify the order
return find_all("Chapter", course=self.name, order_by="index_")
def get_batch(self, batch_name):
return find("LMS Batch", name=batch_name, course=self.name)
def get_batches(self, mentor=None):
batches = find_all("LMS Batch", course=self.name)
if mentor:
# TODO: optimize this
memberships = frappe.db.get_all(
"LMS Batch Membership",
{"member": mentor},
["batch"])
batch_names = {m.batch for m in memberships}
return [b for b in batches if b.name in batch_names]
def get_upcoming_batches(self):
now = frappe.utils.nowdate()
batches = find_all("LMS Batch",
course=self.name,
start_date=[">", now],
status="Active",
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_lesson_index(self, lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
lesson = frappe.get_doc("Lesson", lesson_name)
chapter = frappe.get_doc("Chapter", lesson.chapter)
return f"{chapter.index_}.{lesson.index_}"
def reindex_lessons(self):
for i, c in enumerate(self.get_chapters(), start=1):
c.index_ = i
c.save()
self._reindex_lessons_in_chapter(c)
def _reindex_lessons_in_chapter(self, c):
for i, lesson in enumerate(c.get_lessons(), start=1):
lesson.index = i
lesson.index_label = f"{c.index_}.{i}"
lesson.save()
def reindex_exercises(self):
for i, c in enumerate(self.get_chapters(), start=1):
if c.index_ != i:
c.index_ = i
c.save()
self._reindex_exercises_in_chapter(c)
def _reindex_exercises_in_chapter(self, c):
i = 1
for lesson in c.get_lessons():
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{c.index_}.{i}"
exercise.save()
i += 1
def get_learn_url(self, lesson_number):
if not lesson_number:
return
return f"/courses/{self.name}/learn/{lesson_number}"
def get_current_batch(self, member=frappe.session.user):
current_membership = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name, "is_current": 1}, pluck="batch")
print(current_membership, member, self.name)
if len(current_membership):
return current_membership[0]
print(frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch"))
return frappe.db.get_value("LMS Batch Membership", {"member": member, "course": self.name}, "batch")
def get_all_memberships(self, member=frappe.session.user):
print(member)
all_memberships = frappe.get_all("LMS Batch Membership", {"member": member, "course": self.name}, ["batch", "is_current"])
print(all_memberships)
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships
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):
current = flt(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):
current = flt(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_"],
order_by="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'] = flt("{}.{}".format(chapter_numbers[lesson['chapter']], lesson['index_']))
return lessons
@frappe.whitelist()
def reindex_lessons(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_lessons()
frappe.msgprint("All lessons in this course have been re-indexed.")
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data['name'])
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")

View File

@@ -4,66 +4,45 @@
from __future__ import unicode_literals
import frappe
from .lms_course import LMSCourse
import unittest
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`')
frappe.db.sql('delete from `tabUser` where email like "%@example.com"')
def new_course(self, title):
doc = frappe.get_doc({
"doctype": "LMS Course",
"title": title,
"short_introduction": title,
"description": title
"title": title
})
doc.insert()
return doc
def new_user(self, name, email):
doc = frappe.get_doc(dict(
doctype='User',
email=email,
first_name=name))
doc.insert()
return doc
def test_new_course(self):
course = self.new_course("Test Course")
assert course.title == "Test Course"
assert course.name == "test-course"
assert course.slug == "test-course"
assert course.get_mentors() == []
def test_find_all(self):
courses = LMSCourse.find_all()
assert courses == []
# new couse, but not published
course = self.new_course("Test Course")
assert courses == []
# publish the course
course.is_published = True
course.save()
# now we should find one course
courses = LMSCourse.find_all()
assert [c.name for c in courses] == [course.name]
# disabled this test as it is failing
def _test_add_mentors(self):
course = self.new_course("Test Course")
assert course.get_mentors() == []
user = new_user("Tester", "tester@example.com")
user = self.new_user("Tester", "tester@example.com")
course.add_mentor("tester@example.com")
mentors = course.get_mentors()
mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors]
assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}]
def tearDown(self):
if frappe.db.exists("User", "tester@example.com"):
frappe.delete_doc("User", "tester@example.com")
def new_user(name, email):
doc = frappe.get_doc(dict(
doctype='User',
email=email,
first_name=name))
doc.insert()
return doc

View File

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

View File

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

View File

@@ -1,78 +0,0 @@
{
"actions": [],
"creation": "2021-05-31 17:20:13.388453",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"column_break_3",
"lesson",
"chapter",
"course"
],
"fields": [
{
"fetch_from": "chapter.course",
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fetch_from": "lesson.chapter",
"fieldname": "chapter",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Complete\nPartially Complete\nIncomplete"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-02 13:05:31.114939",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Progress",
"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

@@ -1,8 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSCourseProgress(unittest.TestCase):
pass

View File

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

View File

@@ -8,115 +8,84 @@ from frappe.model.document import Document
from frappe import _
class LMSMentorRequest(Document):
def on_update(self):
if self.has_value_changed('status'):
def on_update(self):
if self.has_value_changed('status'):
template = frappe.db.get_single_value('LMS Settings', 'mentor_request_status_update')
if not template:
return
if self.status == "Approved":
self.create_course_mentor_mapping()
if self.status != "Pending":
self.send_status_change_email()
def create_course_mentor_mapping(self):
mapping = frappe.get_doc({
"doctype": "LMS Course Mentor Mapping",
"mentor": self.member,
"course": self.course
})
mapping.save()
def send_creation_email(self):
email_template = self.get_email_template('mentor_request_creation')
if not email_template:
return
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': frappe.db.get_value("User", frappe.session.user, "full_name"),
'course_url': '/courses/' + course_details.slug,
'course': course_details.title
})
email_args = {
"recipients": [frappe.session.user, course_details.owner],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def send_status_change_email(self):
email_template = self.get_email_template('mentor_request_status_update')
if not email_template:
return
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "title"], as_dict=True)
message = frappe.render_template(email_template.response,
{
'member_name': self.member_name,
'status': self.status,
'course': course_details.title
})
if self.status == 'Approved' or self.status == 'Rejected':
email_args = {
"recipients": self.member,
"cc": [course_details.owner, self.reviewed_by],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
elif self.status == 'Withdrawn':
email_args = {
"recipients": [self.member, course_details.owner],
"subject": email_template.subject,
"header": email_template.subject,
"message": message
}
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def get_email_template(self, template_name):
template = frappe.db.get_single_value('LMS Settings', template_name)
if template:
return frappe.get_doc('Email Template', template)
email_template = frappe.get_doc('Email Template', template)
message = frappe.render_template(email_template.response, {'member_name': self.member_name, 'status': self.status})
subject = _('The status of your application has changed.')
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')
send_email(member_email, [get_course_author(self.course), reviewed_by], subject, message)
elif self.status == 'Withdrawn':
send_email([member_email, get_course_author(self.course)], None, subject, message)
@frappe.whitelist()
def has_requested(course):
return frappe.db.count('LMS Mentor Request',
filters = {
'member': frappe.session.user,
'course': course,
'status': ['in', ('Pending', 'Approved')]
}
return len(frappe.get_all('LMS Mentor Request',
filters = {
'member': get_member().name,
'course': course,
'status': ['in', ('Pending', 'Approved')]
}
)
)
@frappe.whitelist()
def create_request(course):
if not has_requested(course):
request = frappe.get_doc({
'doctype': 'LMS Mentor Request',
'member': frappe.session.user,
'course': course,
'status': 'Pending'
})
request.save(ignore_permissions=True)
request.send_creation_email()
return 'OK'
else:
return 'Already Applied'
if not has_requested(course):
member = get_member()
frappe.get_doc({
'doctype': 'LMS Mentor Request',
'member': member.name,
'course': course,
'status': 'Pending'
}).save(ignore_permissions=True)
send_creation_email(course, member)
return 'OK'
else:
return 'Already Applied'
@frappe.whitelist()
def cancel_request(course):
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'
request = frappe.get_doc('LMS Mentor Request', {'member': get_member().name, '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
def get_course_author(course):
return frappe.db.get_value('LMS Course', course, 'owner')
def send_creation_email(course, member):
template = frappe.db.get_single_value('LMS Settings', 'mentor_request_creation')
if not template:
return
email_template = frappe.get_doc('Email Template', template)
member_name = member.full_name
message = frappe.render_template(email_template.response, {'member_name': member_name})
subject = _('Request for Mentorship')
send_email([frappe.session.user, get_course_author(course)], None, subject, message)
def send_email(recipients, cc, subject, message):
frappe.sendmail(
recipients = recipients,
cc = cc,
sender = frappe.db.get_single_value('LMS Settings', 'email_sender'),
subject = subject,
send_priority = 0,
queue_separately = True,
message = message
)

View File

@@ -26,7 +26,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Author",
"options": "User"
"options": "Community Member"
},
{
"fieldname": "message",
@@ -58,7 +58,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-21 11:49:34.911479",
"modified": "2021-04-26 16:11:20.142164",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Message",
@@ -80,6 +80,5 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "author",
"track_changes": 1
}
}

View File

@@ -7,104 +7,57 @@ import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import add_days, nowdate
class LMSMessage(Document):
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.format_datetime(self.creation, "dd-mm-yyyy HH:mm"),
"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
}})
$(".messages").append(template);
var message_element = document.getElementsByClassName("messages")[0]
message_element.scrollTo(0, message_element.scrollHeight);
""".format(template, message, self.owner)
frappe.publish_realtime(event="eval_js", message=js, after_commit=True)
def get_message_template(self):
return """
<li class="{% if message.is_author %} ours {% endif %}">
<div class="d-flex justify-content-between">
<div class="font-weight-bold">
{{ message.author_name }}
</div>
<small class="">
{{ message.message_time }}
</small>
</div>
<div class="message-para">
{{ message.message }}
</div>
</li>
"""
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 after_insert(self):
frappe.publish_realtime("new_lms_message", {"message":"JJannat"}, user="Administrator")
self.send_email()
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():
#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
)
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
)

View File

@@ -0,0 +1,60 @@
{
"actions": [],
"creation": "2021-03-05 15:10:53.906006",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"label",
"type",
"contents",
"code",
"attrs",
"index"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Type"
},
{
"fieldname": "contents",
"fieldtype": "Markdown Editor",
"label": "Contents"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "attrs",
"fieldtype": "Long Text",
"label": "attrs"
},
{
"fieldname": "index",
"fieldtype": "Int",
"label": "Index"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-12 17:56:23.118854",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Section",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,29 @@
# -*- 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
class LMSSection(Document):
def __repr__(self):
return f"<LMSSection {self.label!r}>"
def get_latest_code_for_user(self):
"""Returns the latest code for the logged in user.
"""
if not frappe.session.user or frappe.session.user == "Guest":
return self.contents
result = frappe.get_all('Code Revision',
fields=["code"],
filters={
"author": frappe.session.user,
"section": self.name
},
order_by="creation desc",
page_length=1)
if result:
return result[0]['code']
else:
return self.contents

View File

@@ -6,11 +6,10 @@
"engine": "InnoDB",
"field_order": [
"livecode_url",
"column_break_2",
"email_sender",
"mentor_request_section",
"mentor_request_creation",
"mentor_request_status_update"
"mentor_request_status_update",
"email_sender"
],
"fields": [
{
@@ -41,16 +40,12 @@
"fieldname": "mentor_request_section",
"fieldtype": "Section Break",
"label": "Mentor Request"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-29 17:14:43.589700",
"modified": "2021-04-21 13:28:35.783395",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Settings",

View File

@@ -3,102 +3,26 @@
import websocket
import json
from .svg import SVG
import frappe
from urllib.parse import urlparse
# Files to pass to livecode server
# The same code is part of livecode-canvas.js
# TODO: generate livecode-canvas.js from this file
START = '''
import sketch
code = open("main.py").read()
env = dict(sketch.__dict__)
exec(code, env)
'''
SKETCH = '''
import json
def sendmsg(msgtype, function, args):
"""Sends a message to the frontend.
The frontend will receive the specified message whenever
this function is called. The frontend can decide to some
action on each of these messages.
"""
msg = dict(msgtype=msgtype, function=function, args=args)
print("--MSG--", json.dumps(msg))
def _draw(func, **kwargs):
sendmsg(msgtype="draw", function=func, args=kwargs)
def circle(x, y, d):
"""Draws a circle of diameter d with center (x, y).
"""
_draw("circle", x=x, y=y, d=d)
def line(x1, y1, x2, y2):
"""Draws a line from point (x1, y1) to point (x2, y2).
"""
_draw("line", x1=x1, y1=y1, x2=x2, y2=y2)
def rect(x, y, w, h):
"""Draws a rectangle on the canvas.
Parameters
----------
x: x coordinate of the top-left corner of the rectangle
y: y coordinate of the top-left corner of the rectangle
w: width of the rectangle
h: height of the rectangle
"""
_draw("rect", x=x, y=y, w=w, h=h)
def clear():
_draw("clear")
# clear the canvas on start
clear()
'''
def get_livecode_url():
doc = frappe.get_cached_doc("LMS Settings")
return doc.livecode_url
def get_livecode_ws_url():
url = urlparse(get_livecode_url())
protocol = "wss" if url.scheme == "https" else "ws"
return protocol + "://" + url.netloc + "/livecode"
def livecode_to_svg(livecode_ws_url, code, *, timeout=3):
def livecode_to_svg(livecode_ws_url, code, *, timeout=1):
"""Renders the code as svg.
"""
if livecode_ws_url is None:
livecode_ws_url = get_livecode_ws_url()
print("livecode_to_svg")
ws = websocket.WebSocket()
ws.settimeout(timeout)
ws.connect(livecode_ws_url)
try:
ws = websocket.WebSocket()
ws.settimeout(timeout)
ws.connect(livecode_ws_url)
msg = {
"msgtype": "exec",
"runtime": "python-canvas",
"code": code
}
ws.send(json.dumps(msg))
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')
messages = _read_messages(ws)
commands = [m['cmd'] for m in messages if m['msgtype'] == 'draw']
img = draw_image(commands)
return img.tostring()
def _read_messages(ws):
messages = []
@@ -108,19 +32,17 @@ def _read_messages(ws):
if not msg:
break
messages.append(json.loads(msg))
except websocket.WebSocketTimeoutException as e:
print("Error:", e)
except websocket.WebSocketTimeoutException:
pass
return messages
def draw_image(commands):
img = SVG(width=300, height=300, viewBox="0 0 300 300", fill='none', stroke='black')
for c in commands:
args = c['args']
if c['function'] == 'circle':
img.circle(cx=args['x'], cy=args['y'], r=args['d']/2)
img.circle(cx=c['x'], cy=c['y'], r=c['d']/2)
elif c['function'] == 'line':
img.line(x1=args['x1'], y1=args['y1'], x2=args['x2'], y2=args['y2'])
img.line(x1=c['x1'], y1=c['y1'], x2=c['x2'], y2=c['y2'])
elif c['function'] == 'rect':
img.rect(x=args['x'], y=args['y'], width=args['w'], height=args['h'])
img.rect(x=c['x'], y=c['y'], width=c['w'], height=c['h'])
return img

View File

@@ -9,12 +9,10 @@ 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):
def get_owner_name(self):
return get_userinfo(self.owner)['full_name']
@property
def sketch_id(self):
"""Returns the numeric part of the name.
@@ -23,14 +21,6 @@ class LMSSketch(Document):
"""
return self.name.replace("SKETCH-", "")
def get_owner(self):
"""Returns the owner of this sketch as a document.
"""
return frappe.get_doc("User", self.owner)
def get_owner_name(self):
return self.get_owner().full_name
def get_livecode_url(self):
doc = frappe.get_cached_doc("LMS Settings")
return doc.livecode_url
@@ -53,25 +43,8 @@ class LMSSketch(Document):
else:
ws_url = self.get_livecode_ws_url()
value = livecode.livecode_to_svg(ws_url, self.code)
if value:
cache.set(key, value)
return value or DEFAULT_IMAGE
@staticmethod
def get_recent_sketches(limit=100, owner=None):
"""Returns the recent sketches.
"""
filters = {}
if owner:
filters = {"owner": owner}
sketches = frappe.get_all(
"LMS Sketch",
filters=filters,
fields='*',
order_by='modified desc',
page_length=limit
)
return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches]
cache.set(key, value)
return value
def __repr__(self):
return f"<LMSSketch {self.name}>"
@@ -83,7 +56,7 @@ def save_sketch(name, title, code):
doc.title = title
doc.code = code
doc.runtime = 'python-canvas'
doc.insert(ignore_permissions=True)
doc.insert()
status = "created"
else:
doc = frappe.get_doc("LMS Sketch", name)
@@ -103,3 +76,36 @@ def save_sketch(name, title, code):
"status": status,
"name": doc.name,
}
def get_recent_sketches():
"""Returns the recent sketches.
The return value will be a list of dicts with each entry containing
the following fields:
- name
- title
- owner
- owner_name
- modified
"""
sketches = frappe.get_all(
"LMS Sketch",
fields='*',
order_by='modified desc',
page_length=100
)
for s in sketches:
s['owner_name'] = get_userinfo(s['owner'])['full_name']
return [frappe.get_doc(doctype='LMS Sketch', **doc) for doc in sketches]
def get_userinfo(email):
"""Returns the username and fullname of a user.
Please note that the email could be "Administrator" or "Guest"
as a special case to denote the system admin and guest user respectively.
"""
user = frappe.get_doc("User", email)
return {
"full_name": user.full_name,
"username": user.username
}

View File

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

View File

@@ -1,61 +1,69 @@
{
"actions": [],
"autoname": "format:{####} {title}",
"creation": "2021-05-03 05:49:08.383057",
"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",
"locked",
"index_"
"order",
"sections"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "0",
"fieldname": "locked",
"fieldtype": "Check",
"label": "Locked"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
"options": "LMS Course",
"reqd": 1
},
{
"default": "1",
"fieldname": "index_",
"fieldname": "order",
"fieldtype": "Int",
"label": "Index"
"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": [
{
"group": "Lessons",
"link_doctype": "Lesson",
"link_fieldname": "chapter"
}
],
"modified": "2021-05-13 21:05:20.531890",
"links": [],
"modified": "2021-04-06 14:12:48.514062",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapter",
"name": "LMS Topic",
"owner": "Administrator",
"permissions": [
{
@@ -72,8 +80,10 @@
}
],
"search_fields": "title",
"sort_field": "modified",
"sort_order": "DESC",
"sort_field": "creation",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
"track_changes": 1,
"track_seen": 1,
"track_views": 1
}

View File

@@ -0,0 +1,43 @@
# -*- 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

@@ -0,0 +1,79 @@
"""Utility to split the text in the topic into multiple sections.
"""
from __future__ import annotations
from dataclasses import dataclass
import re
from typing import List, Tuple, Dict, Iterator
RE_SECTION = re.compile(r"^\{\{\s(\w+)\s*(?:\((.*)\))?\s*\}\}\s*")
class SectionParser:
def parse(self, text: str) -> Iterator[Section]:
"""Parses given text into sections and return an iterator over sections.
"""
lines = text.splitlines()
marked_lines = self.parse_lines(lines)
return self.group_sections(marked_lines)
def parse_lines(self, lines: List[str]) -> List[Tuple[str, str, str]]:
for line in lines:
m = RE_SECTION.match(line)
if m:
yield m.group(1), self.parse_attrs(m.group(2)), None
else:
yield None, None, line
def parse_attrs(self, attrs_str: str) -> Dict[str, str]:
# XXX-Anand: Hack
code = "dict({})".format(attrs_str or "")
return eval(code)
def group_sections(self, marked_lines) -> Iterator[Section]:
index = 0
def make_section(type='text', id=None, label=None, **attrs):
nonlocal index
index += 1
id = id or f"section-{index}"
label = label or id
return Section(
type=type,
id=id,
label=label,
attrs=attrs)
section = make_section("text")
for mark, attrs, line in marked_lines:
if not mark:
section.append(line)
continue
yield section
if mark == 'end':
section = make_section(type='text')
else:
section = make_section(**attrs)
yield section
@dataclass
class Section:
"""One section of the Topic.
"""
type: str
id: str
label: str
contents: str = ""
attrs: dict = None
def append(self, line):
if not line.endswith("\n"):
line = line + "\n"
self.contents += line
def __repr__(self):
attrs = dict(type=self.type, id=self.id, label=self.label, **self.attrs)
attrs_str = ", ".join(f'{k}="{v}"' for k, v in attrs.items())
return f'<Section({attrs_str})>'

View File

@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestChapter(unittest.TestCase):
class TestLMSTopic(unittest.TestCase):
pass

View File

@@ -1,107 +0,0 @@
"""
The md module extends markdown to add macros.
Macros can be added to the markdown text in the following format.
{{ MacroName("macro-argument") }}
These macros will be rendered using a pluggable mechanism.
Apps can provide a hook community_markdown_macro_renderers, a
dictionary mapping the macro name to the function that to render
that macro. The function will get the argument passed to the macro
as argument.
"""
import frappe
import re
from bs4 import BeautifulSoup
import markdown
from markdown import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
def markdown_to_html(text):
"""Renders markdown text into html.
"""
return markdown.markdown(text, extensions=['fenced_code', MacroExtension()])
def find_macros(text):
"""Returns all macros in the given text.
>>> find_macros(text)
[
('YouTubeVideo': 'abcd1234')
('Exercise', 'two-circles'),
('Exercise', 'four-circles')
]
"""
macros = re.findall(MACRO_RE, text)
# remove the quotes around the argument
return [(name, _remove_quotes(arg)) for name, arg in macros]
def _remove_quotes(value):
"""Removes quotes around a value.
Also strips the whitespace.
>>> _remove_quotes('"hello"')
'hello'
>>> _remove_quotes("'hello'")
'hello'
>>> _remove_quotes("hello")
'hello'
"""
return value.strip(" '\"")
def get_macro_registry():
d = frappe.get_hooks("community_markdown_macro_renderers") or {}
return {name: frappe.get_attr(klass[0]) for name, klass in d.items()}
def render_macro(macro_name, macro_argument):
# stripping the quotes on either side of the argument
macro_argument = _remove_quotes(macro_argument)
registry = get_macro_registry()
if macro_name in registry:
return registry[macro_name](macro_argument)
else:
return f"<p>Unknown macro: {macro_name}</p>"
MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}'
class MacroExtension(Extension):
"""MacroExtension is a markdown extension to support macro syntax.
"""
def extendMarkdown(self, md):
self.md = md
pattern = MacroInlineProcessor(MACRO_RE)
pattern.md = md
md.inlinePatterns.register(pattern, 'macro', 75)
class MacroInlineProcessor(InlineProcessor):
"""MacroInlineProcessor is class that is handles the logic
of how to render each macro occurence in the markdown text.
"""
def handleMatch(self, m, data):
"""Handles each macro match and return rendered contents
for that macro as an etree node.
"""
macro = m.group(1)
arg = m.group(2)
html = render_macro(macro, arg)
html = sanitize_html(str(html))
e = etree.fromstring(html)
return e, m.start(0), m.end(0)
def sanitize_html(html):
"""Sanotize the html using BeautifulSoup.
The markdown processor request the correct markup and crashes on
any broken tags. This makes sures that all those things are fixed
before passing to the etree parser.
"""
soup = BeautifulSoup(html, features="lxml")
nodes = soup.body.children
return "<div>" + "\n".join(str(node) for node in nodes) + "</div>"

View File

@@ -1,5 +0,0 @@
"""Handy module to make access to all doctypes from a single place.
"""
from .doctype.lms_course.lms_course import LMSCourse as Course
from .doctype.lms_sketch.lms_sketch import LMSSketch as Sketch
from .doctype.lms_batch_membership.lms_batch_membership import LMSBatchMembership as Membership

View File

@@ -1,39 +1,35 @@
frappe.ready(function () {
frappe.web_form.after_save = () => {
let data = frappe.web_form.get_values();
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.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"
},
"callback": (data) => {
if (data.message == "OK") {
window.location.href = "/courses"
}
}
})
}
frappe.web_form.validate = () => {
let sysdefaults = frappe.boot.sysdefaults;
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)
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.'));
return false;
}
if (data.start_time > data.end_time) {
frappe.msgprint(__('Start Time should be less than End Time.'));
return false;
}
return true;
};
})
frappe.web_form.validate = () => {
let data = frappe.web_form.get_values();
if (!frappe.datetime.validate(data.start_time) || !frappe.datetime.validate(data.end_time)) {
frappe.msgprint(__('Invalid Start or End Time.'));
return false;
}
if (data.start_time > data.end_time) {
frappe.msgprint(__('Start Time should be less than End Time.'));
return false;
}
console.log(data.start_date, date.nowdate())
if (data.start_date < date.nowdate()) {
frappe.msgprint(__('Start date cannot be a past date.'))
return false;
}
return true;
};
})

View File

@@ -11,7 +11,6 @@
"apply_document_permissions": 0,
"button_label": "Save",
"creation": "2021-04-20 11:37:49.135114",
"custom_css": ".datepicker.active {\n background-color: white;\n}\n\n[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
"doc_type": "LMS Batch",
"docstatus": 0,
"doctype": "Web Form",
@@ -19,7 +18,7 @@
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-06-14 15:28:08.206622",
"modified": "2021-04-26 11:08:00.026388",
"modified_by": "Administrator",
"module": "LMS",
"name": "add-a-new-batch",
@@ -39,7 +38,7 @@
"allow_read_on_all_link_options": 0,
"fieldname": "course",
"fieldtype": "Link",
"hidden": 1,
"hidden": 0,
"label": "Course",
"max_length": 0,
"max_value": 0,
@@ -91,7 +90,7 @@
"fieldname": "start_time",
"fieldtype": "Data",
"hidden": 0,
"label": "Start Time",
"label": "Start Time (HH:MM:SS)",
"max_length": 0,
"max_value": 0,
"read_only": 0,
@@ -103,7 +102,7 @@
"fieldname": "end_time",
"fieldtype": "Data",
"hidden": 0,
"label": "End Time",
"label": "End Time (HH:MM:SS)",
"max_length": 0,
"max_value": 0,
"read_only": 0,

View File

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

View File

@@ -0,0 +1,12 @@
frappe.ready(function() {
// bind events here
frappe.web_form.after_load = () => {
frappe.web_form.set_value(["batch"], [frappe.utils.get_url_arg('batch')]);
frappe.web_form.set_value(["author"], [frappe.utils.get_url_arg('author')]);
}
frappe.web_form.success_url = `courses/course?course=${frappe.utils.get_url_arg('course')}`;
$('.breadcrumb-container')
.html(`<a href="${frappe.web_form.success_url}">Back to my course</a>`)
.addClass('py-4');
})

View File

@@ -0,0 +1,77 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"button_label": "Send",
"client_script": "",
"creation": "2021-03-23 13:10:16.814983",
"doc_type": "LMS Message",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-03-23 19:25:54.984968",
"modified_by": "Administrator",
"module": "LMS",
"name": "add-messages",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 0,
"route": "add-messages",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "",
"title": "Add Messages",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldname": "batch",
"fieldtype": "Link",
"hidden": 0,
"label": "Batch",
"max_length": 0,
"max_value": 0,
"options": "LMS Batch",
"read_only": 1,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "message",
"fieldtype": "Data",
"hidden": 0,
"label": "Message",
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "author",
"fieldtype": "Link",
"hidden": 1,
"label": "Author",
"max_length": 0,
"max_value": 0,
"options": "Community Member",
"read_only": 1,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

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

View File

@@ -0,0 +1,3 @@
frappe.ready(function() {
// bind events here
})

View File

@@ -0,0 +1,48 @@
{
"accept_payment": 0,
"allow_comments": 0,
"allow_delete": 0,
"allow_edit": 0,
"allow_incomplete": 0,
"allow_multiple": 0,
"allow_print": 0,
"amount": 0.0,
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"button_label": "Save",
"creation": "2021-04-15 13:32:14.171328",
"doc_type": "LMS Batch Membership",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
"is_standard": 1,
"login_required": 1,
"max_attachment_size": 0,
"modified": "2021-04-15 13:32:14.171328",
"modified_by": "Administrator",
"module": "LMS",
"name": "join-a-batch",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
"route": "join-a-batch",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
"show_sidebar": 0,
"sidebar_items": [],
"success_url": "/join-a-batch",
"title": "Join a Batch",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
"fieldtype": "Attach",
"hidden": 0,
"max_length": 0,
"max_value": 0,
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
}
]
}

View File

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

View File

@@ -1,4 +0,0 @@
<div class="p-5 batch-header">
<h3>{{batch_name}}</h3>
<div class="text-muted">{{member_count}} members</div>
</div>

View File

@@ -1,76 +0,0 @@
<div class="mt-5">
<a class="anchor_style" href="/courses">Courses</a> /{% if course.is_mentor(frappe.session.user) %} <a
class="anchor_style" href="/courses/{{ course.name }}"> {{ course.title }}</a> {% else %} <span class="text-muted">
{{ course.title }}</span> {% endif %}
{% set all_memberships = course.get_all_memberships() %}
{% if all_memberships | length > 1 %}
<a class="nav-link pull-right" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
Switch Batch
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
{% for membership in all_memberships %}
{% if not membership.is_current %}
<a class="dropdown-item switch-batch" href="#" data-batch="{{ membership.batch | urlencode }}" data-course="{{ course.name | urlencode }}">{{ membership.batch_title }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% if not batch %}
{% set display_class = "hide" %}
{% else %}
{% set display_class = "" %}
{% endif %}
<ul class="nav nav-tabs mt-4">
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="home" href="/courses/{{course.name}}/home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" id="learn" href="/courses/{{course.name}}/learn">Lessons</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="schedule" href="/courses/{{course.name}}/schedule">Schedule</a>
</li> -->
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="members" href="/courses/{{course.name}}/members">Members</a>
</li>
<li class="nav-item {{ display_class }}">
<a class="nav-link" id="discussion" href="/courses/{{course.name}}/discuss">Discussion</a>
</li>
<!-- <li class="nav-item">
<a class="nav-link" id="about" href="/courses/{{course.name}}/about">About</a>
</li> -->
{% if batch and batch.is_member(frappe.session.user, member_type="Mentor") %}
<li class="nav-item">
<a class="nav-link" id="progress" href="/courses/{{course.name}}/progress">Progress</a>
</li>
{% endif %}
</ul>
<script>
frappe.ready(() => {
var selector = document.querySelector(`a[href="${decodeURIComponent(window.location.pathname)}"]`)
if (selector) {
selector.classList.add('active');
}
else {
$("#learn").addClass('active')
}
$(".switch-batch").click((e) => {
e.preventDefault();
var batch = decodeURIComponent($(e.currentTarget).attr("data-batch"));
var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
frappe.call({
method: "community.lms.doctype.lms_batch_membership.lms_batch_membership.update_current_membership",
args: {
batch: batch,
course: course
},
callback: (data) => {
window.location.reload();
}
})
})
})
</script>

View File

@@ -1,54 +0,0 @@
<div class="chapter-teaser">
<div class="teaser-body">
<div class="chapter-title mb-5 font-weight-bold"><span class="mr-1">{{index}}.</span> {{ chapter.title }}</div>
<div class="chapter-description">
{{ chapter.description or "" }}
</div>
<div class="chapter-lessons">
{% for lesson in chapter.get_lessons() %}
<div class="lesson-teaser">
<a {% if show_link or lesson.include_in_preview %}
href="{{ course.get_learn_url(course.get_lesson_index(lesson.name)) }}" {% else %} href="" class="no-preview"
{% endif %} data-course="{{ course.name }}">{{ lesson.title }}</a>
{% if show_progress and not course.is_mentor(frappe.session.user) and lesson.get_progress() %}
<a class="ml-5 badge p-1 {{ lesson.get_slugified_class() }}"> <img class="progress-image"
src="/assets/community/images/Vector.png"> {{ lesson.get_progress() }}</a>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
<script>
frappe.ready(() => {
var d;
$(".no-preview").click((e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
var message = __("Please enroll for this course to access the lesson.");
var label = __("Checkout Upcoming Batches");
var action = "checkout_upcoming_batches";
d = frappe.msgprint({
title: __("This lesson is not available for preview!"),
message: message,
primary_action: {
"label": label,
"client_action": action,
}
});
})
window.redirect_to_login = () => {
window.location.href = `/login?redirect-to=/courses/${$(".no-preview").attr("data-course")}`
}
window.checkout_upcoming_batches = () => {
if ($(".upcoming").length > 0) {
$('html,body').animate({ scrollTop: $(".upcoming").offset().top }, 300);
}
frappe.hide_msgprint();
}
})
</script>

View File

@@ -1,7 +0,0 @@
<div class="mt-5">
<h3> Course Outline </h3>
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, show_link=show_link, show_progress=show_progress)}}
{% endfor %}
</div>

View File

@@ -1,19 +0,0 @@
<div class="course-teaser">
<div class="course-body">
<h3 class="course-title"><a href="/courses/{{ course.name }}">{{ course.title }}</a></h3>
<div class="course-intro">
{{ course.short_introduction or "" }}
</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">
{% 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

@@ -1,18 +0,0 @@
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
<div class="exercise">
<h2>Exercise {{exercise.index_label}}: {{ 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 +0,0 @@
<div class="instructor">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<a class="ml-1 instructor-title" href="/{{instructor.username}}">{{ instructor.full_name }}</a>
<div class="instructor-subtitle">Course Creator</div>
<!-- <div class="instructor-subtitle">Created {{instructor.get_course_count()}} courses</div> -->
</div>

View File

@@ -1,30 +0,0 @@
<div class="batch">
<div class="batch-details">
<div class="">Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.get_mentors() %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
{% if can_manage or can_join %}
<div class="cta">
<div class="">
{% if can_manage %}
<a href="" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Manage</a>
{% elif can_join %}
<button class="join-batch btn btn-primary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -1,64 +0,0 @@
<form id="invite-request-form">
<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(() => {
$("#submit-invite-request").click(function () {
var invite_email = $("#invite_email").val()
frappe.call({
method: "community.lms.doctype.invite_request.invite_request.create_invite_request",
args: {
invite_email: invite_email
},
callback: (data) => {
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 {
var message = `<div>
<small class="email-validation" style="color: red;">Please enter an email address.</small>
</div>`;
}
$("#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>`;
}
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);
}
}
})
})
})
</script>

View File

@@ -1,16 +0,0 @@
<div class="sketch-teaser">
<div class="sketch-image">
<a href="/sketches/{{sketch.sketch_id}}">
{{ sketch.to_svg() }}
</a>
</div>
<div class="sketch-footer">
<div class="sketch-title">
<a href="sketches/{{sketch.sketch_id}}">{{sketch.title}}</a>
</div>
<div class="sketch-author">
{% set owner = sketch.get_owner() %}
by <a href="/{{owner.username}}">{{owner.full_name}}</a>
</div>
</div>
</div>

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