Compare commits

..

26 Commits

Author SHA1 Message Date
Anand Chitipothu
a0b77f5d08 feat: added get_profile_url function to get the profile url in templates
This takes care of generating the correct profile URL depending on the
`profile_url_prefix` setting.

Issue #192
2021-09-07 17:58:09 +05:30
Anand Chitipothu
77c4b53b71 feat: added redirect rule to redirect /profile_/foo to the profile url
Hack to allow redirecting to profile url from JS as it doesn't know
the `profile_url_prefix`.

Issue #192
2021-09-07 17:58:09 +05:30
Anand Chitipothu
035a674cff feat: added support for making profile urls to be top-level
Made the profile_url_prefix customizable by adding it in the hooks.

Issue #192
2021-09-07 17:58:04 +05:30
Jannat Patel
f52e5067b6 Merge pull request #201 from pateljannat/discussion-second-cut
fix: Discussions Redesign
2021-09-06 19:29:40 +05:30
pateljannat
d657525359 fix: discussions redesign 2021-09-06 18:59:59 +05:30
pateljannat
916e64d607 feat: discussions sidebar 2021-09-02 10:47:35 +05:30
pateljannat
9e1daf5062 fix: reply card ui 2021-08-31 19:17:00 +05:30
pateljannat
941a34784c Merge branch 'main' of https://github.com/fossunited/community into discussion-second-cut 2021-08-31 17:38:13 +05:30
Jannat Patel
cd4ffa2eff Merge pull request #199 from pateljannat/certificate-ui
fix: certificate ui
2021-08-31 16:31:14 +05:30
pateljannat
17a7af74f2 fix: certificate ui 2021-08-31 16:19:41 +05:30
Jannat Patel
ff22eaa606 Merge pull request #198 from pateljannat/issue-fixes
fix: ui and removed mockup
2021-08-31 12:51:32 +05:30
pateljannat
417436d7b6 fix: course filter in review 2021-08-31 12:43:51 +05:30
pateljannat
f228489173 fix: ui and removed mockup 2021-08-31 12:30:52 +05:30
pateljannat
a49563e23f fix: discussions template 2021-08-30 18:39:00 +05:30
pateljannat
b3403b78ee fix: removed global discussions page 2021-08-30 12:47:15 +05:30
pateljannat
7a9039090d fix: discussions structure 2021-08-30 12:46:08 +05:30
Jannat Patel
289195e6c9 fix: readme url 2021-08-30 11:14:25 +05:30
Jannat Patel
e6502784ea Merge pull request #197 from pateljannat/ui-issues
fix: ui issues
2021-08-27 09:58:22 +05:30
pateljannat
54f301e8eb fix: edit-profile-link 2021-08-27 09:53:30 +05:30
pateljannat
ed91801769 fix: ui issues 2021-08-26 18:32:14 +05:30
Jannat Patel
6965148e4e Merge pull request #196 from pateljannat/course-cards-web-template
feat: course cards web template
2021-08-26 10:59:28 +05:30
pateljannat
b5481e1dd5 fix: margin 2021-08-26 10:54:38 +05:30
pateljannat
4ec9b56366 feat: course cards web template 2021-08-26 10:44:17 +05:30
Jannat Patel
530fcf9a39 Merge pull request #194 from fossunited/certification-fixes
fix: certificate, profile, quiz, and video markdown
2021-08-25 21:11:28 +05:30
pateljannat
ff1363b437 fix: certificate, profile, quiz, and video markdown 2021-08-25 21:01:13 +05:30
Jannat Patel
952e3a9906 Merge pull request #190 from fossunited/web-form-changes
fix: web form issues
2021-08-24 21:23:23 +05:30
80 changed files with 1024 additions and 1256 deletions

View File

@@ -63,7 +63,7 @@ To setup the repository locally follow the steps mentioned below:
1. Run bench get-app https://github.com/fossunited/community.
1. Run bench --site community.test install-app community.
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.
1. Now open the URL http://community.test:8000/ in your browser, you should see the app running.
### Contribution Guidelines (for The Hard Way)

View File

@@ -1,22 +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 community.widgets import Widget, Widgets
class DiscussionMessage(Document):
def after_insert(self):
data = {
"message": self,
"widgets": Widgets()
}
template = frappe.render_template("community/templates/message_card.html", data)
thread_info = frappe.db.get_value("Discussion Thread", self.thread, ["reference_doctype", "reference_docname"], as_dict=True)
frappe.publish_realtime(event="publish_message",
message = {
"thread": self.thread,
"template": template,
"thread_info": thread_info
},
after_commit=True)

View File

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

View File

@@ -5,49 +5,31 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"thread",
"column_break_2",
"parent_message",
"section_break_4",
"message"
"topic",
"reply"
],
"fields": [
{
"fieldname": "message",
"fieldname": "reply",
"fieldtype": "Long Text",
"in_list_view": 1,
"label": "Message"
"label": "Reply"
},
{
"fieldname": "thread",
"fieldname": "topic",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Thread",
"options": "Discussion Thread"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "parent_message",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Parent Message",
"options": "Discussion Message"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
"label": "Topic",
"options": "Discussion Topic"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-08-12 15:59:04.811286",
"modified": "2021-08-27 15:06:51.362714",
"modified_by": "Administrator",
"module": "Community",
"name": "Discussion Message",
"name": "Discussion Reply",
"owner": "Administrator",
"permissions": [
{

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from community.widgets import Widgets
class DiscussionReply(Document):
def after_insert(self):
data = {
"reply": self,
"topic": {
"name": self.topic
},
"widgets": Widgets()
}
template = frappe.render_template("community/templates/discussions/reply_card.html", data)
topic_info = frappe.get_all("Discussion Topic", {"name": self.topic}, ["reference_doctype", "reference_docname", "name", "title", "owner", "creation"])
sidebar = frappe.render_template("community/templates/discussions/sidebar.html", { "topic": topic_info[0], "widgets": Widgets() })
new_topic_template = frappe.render_template("community/templates/discussions/reply_section.html", { "topics": topic_info, "widgets": Widgets() })
frappe.publish_realtime(event="publish_message",
message = {
"template": template,
"topic_info": topic_info[0],
"sidebar": sidebar,
"new_topic_template": new_topic_template
},
after_commit=True)

View File

@@ -4,5 +4,5 @@
# import frappe
import unittest
class TestDiscussionThread(unittest.TestCase):
class TestDiscussionReply(unittest.TestCase):
pass

View File

@@ -1,48 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class DiscussionThread(Document):
pass
@frappe.whitelist()
def submit_discussion(doctype, docname, message, title=None, thread_name=None):
thread = []
filters = {}
if doctype and docname:
filters = {
"reference_doctype": doctype,
"reference_docname": docname
}
elif thread_name:
filters = {
"name": thread_name
}
if filters:
thread = frappe.get_all("Discussion Thread",filters)
if len(thread):
thread = thread[0]
save_message(message, thread)
else:
thread = frappe.get_doc({
"doctype": "Discussion Thread",
"title": title,
"reference_doctype": doctype,
"reference_docname": docname
})
thread.save(ignore_permissions=True)
save_message(message, thread)
return thread.name
def save_message(message, thread):
frappe.get_doc({
"doctype": "Discussion Message",
"message": message,
"thread": thread.name
}).save(ignore_permissions=True)

View File

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

View File

@@ -33,7 +33,7 @@
"modified": "2021-08-11 12:29:43.564123",
"modified_by": "Administrator",
"module": "Community",
"name": "Discussion Thread",
"name": "Discussion Topic",
"owner": "Administrator",
"permissions": [
{
@@ -54,4 +54,4 @@
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -0,0 +1,35 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class DiscussionTopic(Document):
pass
@frappe.whitelist()
def submit_discussion(doctype, docname, reply, title, topic_name=None):
if topic_name:
save_message(reply, topic_name)
return topic_name
topic = frappe.get_doc({
"doctype": "Discussion Topic",
"title": title,
"reference_doctype": doctype,
"reference_docname": docname
})
topic.save(ignore_permissions=True)
save_message(reply, topic.name)
return topic.name
def save_message(reply, topic):
frappe.get_doc({
"doctype": "Discussion Reply",
"reply": reply,
"topic": topic
}).save(ignore_permissions=True)
@frappe.whitelist()
def get_docname(route):
return frappe.db.get_value("Web Page", {"route": route}, ["name"])

View File

@@ -4,5 +4,5 @@
# import frappe
import unittest
class TestDiscussionMessage(unittest.TestCase):
class TestDiscussionTopic(unittest.TestCase):
pass

View File

@@ -0,0 +1,2 @@
{% set docname = frappe.db.get_value("Web Page", {"route": ""}, ["name"])%}
{{ widgets.DiscussionMessage(doctype="Web Page", docname=docname) }}

View File

@@ -0,0 +1,17 @@
{
"__islocal": true,
"__unsaved": 1,
"creation": "2021-08-30 12:42:31.550200",
"docstatus": 0,
"doctype": "Web Template",
"fields": [],
"idx": 0,
"modified": "2021-08-30 12:42:31.550200",
"modified_by": "Administrator",
"module": "Community",
"name": "Discussions",
"owner": "Administrator",
"standard": 1,
"template": "",
"type": "Section"
}

View File

@@ -1,5 +1,5 @@
{% set color = member.get_palette() %}
<a class="button-links" href="/user/{{member.username}}">
<a class="button-links" href="{{ get_profile_url(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 }}">

View File

@@ -0,0 +1,28 @@
<form class="discussion-form">
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<input type="text" autocomplete="off" class="input-with-feedback form-control topic-title" data-fieldtype="Data"
data-fieldname="feedback_comments" placeholder="Title" spellcheck="false"></input>
</div>
</div>
</div>
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="Enter a comment..."
spellcheck="false"></textarea>
</div>
</div>
</div>
<div class="comment-footer">
<div class="button is-default pull-right mb-5 submit-discussion" data-doctype="{{ doctype | urlencode }}"
data-docname="{{ docname | urlencode }}">
Post</div>
</div>
</form>

View File

@@ -1,70 +0,0 @@
<div class="discussions">
<form class="discussion-form {% if doctype or thread %} discussion-on-page {% endif %}" id="discussion-form">
<div class="form-group" {% if title or thread %} style="display: none;" {% endif %}>
<div class="control-input-wrapper">
<div class="control-input">
<input type="text" autocomplete="off" class="input-with-feedback form-control thread-title"
data-fieldtype="Data" data-fieldname="feedback_comments" placeholder="Title" spellcheck="false" {% if title
%} value="{{ title }}" {% endif %}></input>
</div>
</div>
</div>
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control comment-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="Enter a comment..."
spellcheck="false"></textarea>
</div>
</div>
</div>
<div class="comment-footer">
<div class="button is-secondary pull-right" id="submit-discussion"
{% if doctype %} data-doctype="{{ doctype | urlencode}}" {% endif %}
{% if docname %} data-docname="{{ docname | urlencode}}" {% endif %}
{% if thread %} data-thread="{{ thread }}" {% endif %}>
Post</div>
</div>
</form>
</div>
<script>
frappe.ready(() => {
$("#submit-discussion").click((e) => {
submit_discussion(e);
})
})
var submit_discussion = (e) => {
var message = $(".comment-field").val().trim();
if (message) {
var doctype = $(e.currentTarget).attr("data-doctype");
doctype = doctype ? decodeURIComponent(doctype) : doctype;
var docname = $(e.currentTarget).attr("data-docname");
docname = docname ? decodeURIComponent(docname) : docname;
frappe.call({
method: "community.community.doctype.discussion_thread.discussion_thread.submit_discussion",
args: {
"doctype": doctype ? doctype : "",
"docname": docname ? docname : "",
"message": $(".comment-field").val(),
"title": $(".thread-title").val(),
"thread_name": $(e.currentTarget).attr("data-thread")
},
callback: (data) => {
if (! $(".discussion-on-page").length) {
$("#discussion-modal").modal("hide");
window.location.href = `/discussions/${data.message}`;
}
}
})
}
}
</script>

View File

@@ -1,80 +1,54 @@
{% if doctype and docname and not thread %}
{% set topics = frappe.get_all("Discussion Topic",
{"reference_doctype": doctype, "reference_docname": docname}, ["name", "title", "owner", "creation"]) %}
{% set thread_info = frappe.get_all("Discussion Thread", {"reference_doctype": doctype, "reference_docname": docname},
["name"]) %}
{% include "community/templates/discussions/topic_modal.html" %}
{% if thread_info | length %}
{% set thread = thread_info[0].name %}
<div class="discussions-parent">
<div class="discussions-header">
<span class="course-home-headings">{{_('Discussions')}}</span>
{% if topics %}
{% include "community/templates/discussions/button.html" %}
{% endif %}
</div>
{% if topics %}
<div class="common-card-style thread-card course-content-parent" data-doctype="{{ doctype }}"
data-docname="{{ docname }}">
<div class="discussions-sidebar">
{% include "community/templates/discussions/search.html" %}
{% for topic in topics %}
{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name})%}
{% include "community/templates/discussions/sidebar.html" %}
{% endfor %}
</div>
<div class="mr-2" id="discussion-group">
{% include "community/templates/discussions/reply_section.html" %}
</div>
</div>
{% else %}
<div id="no-discussions" class="common-card-style thread-card">
<div class="no-discussions">
<div class="font-weight-bold">No Discussions</div>
<div class="muted-text mt-3 mb-3">There are no discussions for this {{ doctype | lower }}, why don't you start
one! </div>
{% if frappe.session.user == "Guest" %}
<div class="button is-primary mt-3" id="login-from-discussion">Log In</div>
{% elif not condition %}
<div class="button is-primary mt-3" id="login-from-discussion" data-redirect="{{ redirect_to }}">
{{ button_name }}
</div>
{% else %}
{% include "community/templates/discussions/button.html" %}
{% endif %}
</div>
</div>
</div>
{% endif %}
{% endif %}
{% if thread %}
{% set messages = frappe.get_all("Discussion Message", {"thread": thread}, ["name", "message", "owner", "creation"],
order_by="creation") %}
{% endif %}
{% if doctype %}
<div class="course-home-headings mt-5"> Discussions </div>
{% endif %}
<div class="messages mt-5">
{% for message in messages %}
{% include "community/templates/message_card.html" %}
{% endfor %}
</div>
{% if frappe.session.user == "Guest" or (condition is defined and not condition) %}
<div class="d-flex flex-column align-items-center font-weight-bold">
Want to join the discussion?
{% if frappe.session.user == "Guest" %}
<div class="button is-primary mt-5" id="login-from-discussion">Log In</div>
{% elif not condition %}
<div class="button is-primary mt-5" id="login-from-discussion" data-redirect="{{ redirect_to }}">{{ button_name }}</div>
{% endif %}
</div>
{% else %}
{{ widgets.DiscussionComment(doctype=doctype, docname=docname, title=title, thread=thread ) }}
{% endif %}
<script>
frappe.ready(() => {
setup_socket_io();
$("#login-from-discussion").click((e) => {
login_from_discussion(e);
})
})
var setup_socket_io = () => {
const assets = [
"/assets/frappe/js/lib/socket.io.min.js",
"/assets/frappe/js/frappe/socketio_client.js",
]
frappe.require(assets, () => {
if (window.dev_server) {
frappe.boot.socketio_port = "9000";
}
frappe.socketio.init(9000);
var target = $("#submit-discussion");
frappe.socketio.socket.on("publish_message", (data) => {
if (target.attr("data-thread") == data.thread
|| (decodeURIComponent(target.attr("data-doctype")) == data.thread_info.reference_doctype
&& decodeURIComponent(target.attr("data-docname")) == data.thread_info.reference_docname)) {
$(".comment-field").val("");
$(".messages").append(data.template);
}
})
})
}
var login_from_discussion = (e) => {
var redirect = $(e.currentTarget).attr("data-redirect") || window.location.href;
window.location.href = `/login?redirect-to=${redirect}`;
}
</script>
{% block script %}
<script>{% include "community/templates/discussions/discussions.js" %}</script>
{% endblock %}

View File

@@ -134,29 +134,28 @@ fixtures = ["Custom Field"]
website_route_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": "/courses/<course>/<certificate>", "to_route": "courses/certificate"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"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": "/discussions/<discussion>", "to_route": "discussions/discussion"},
{"from_route": "/user/<string(minlength=4):username>", "to_route": "profiles/profile"},
]
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
]
update_website_context = 'community.widgets.update_website_context'
update_website_context = [
'community.widgets.update_website_context',
]
jinja = {
"methods": [
"community.page_renderers.get_profile_url"
],
"filters": []
}
## 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 = []
@@ -175,4 +174,14 @@ community_markdown_macro_renderers = {
"Exercise": "community.plugins.exercise_renderer",
"Quiz": "community.plugins.quiz_renderer",
"YouTubeVideo": "community.plugins.youtube_video_renderer",
"Video": "community.plugins.video_renderer"
}
# page_renderer to manage profile pages
page_renderer = [
"community.page_renderers.ProfileRedirectPage",
"community.page_renderers.ProfilePage"
]
# set this to "/" to have profiles on the top-level
profile_url_prefix = "/users/"

View File

@@ -18,7 +18,8 @@
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
@@ -30,7 +31,8 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "lessons",
@@ -55,7 +57,7 @@
"link_fieldname": "chapter"
}
],
"modified": "2021-08-19 13:43:51.025072",
"modified": "2021-08-31 10:43:45.866864",
"modified_by": "Administrator",
"module": "LMS",
"name": "Chapter",

View File

@@ -17,6 +17,15 @@ frappe.ui.form.on('Lesson', {
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
Video
</div>
<div class="col-sm-4">
{{ Video("url_of_source") }}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4">
YouTube Video

View File

@@ -22,18 +22,21 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Chapter",
"options": "Chapter"
"options": "Chapter",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
"label": "Title",
"reqd": 1
},
{
"fieldname": "body",
"fieldtype": "Markdown Editor",
"label": "Body"
"label": "Body",
"reqd": 1
},
{
"fieldname": "index_label",
@@ -67,7 +70,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-27 16:28:29.203624",
"modified": "2021-08-31 10:44:14.168257",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lesson",

View File

@@ -8,7 +8,7 @@ from frappe.model.document import Document
from ...md import markdown_to_html, find_macros
class Lesson(Document):
def before_save(self):
def on_update(self):
dynamic_documents = ["Exercise", "Quiz"]
for section in dynamic_documents:
self.update_lesson_name_in_document(section)

View File

@@ -13,13 +13,14 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Lesson"
"options": "Lesson",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 16:53:52.732191",
"modified": "2021-08-31 10:44:42.048232",
"modified_by": "Administrator",
"module": "LMS",
"name": "Lessons",

View File

@@ -29,13 +29,17 @@ def create_certificate(course):
return certificate
else:
expires_after_yrs = course_details.expiry
expires_after_yrs = int(course_details.expiry)
expiry_date = None
if expires_after_yrs:
expiry_date = add_years(nowdate(), expires_after_yrs)
certificate = frappe.get_doc({
"doctype": "LMS Certification",
"student": frappe.session.user,
"course": course,
"issue_date": nowdate(),
"expiry_date": add_years(nowdate(), int(expires_after_yrs))
"expiry_date": expiry_date
})
certificate.save(ignore_permissions=True)
return certificate.name

View File

@@ -29,7 +29,10 @@
"section_break_5",
"short_introduction",
"description",
"chapters"
"chapters",
"certification_section",
"enable_certification",
"expiry"
],
"fields": [
{
@@ -119,6 +122,25 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "certification_section",
"fieldtype": "Section Break",
"label": "Certification"
},
{
"default": "0",
"fieldname": "enable_certification",
"fieldtype": "Check",
"label": "Enable Certification"
},
{
"default": "0",
"depends_on": "enable_certification",
"fieldname": "expiry",
"fieldtype": "Select",
"label": "Certification Expires After Years",
"options": "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10"
}
],
"index_web_pages_for_search": 1,
@@ -140,7 +162,7 @@
"link_fieldname": "course"
}
],
"modified": "2021-08-20 11:01:15.795219",
"modified": "2021-08-25 11:04:57.211898",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course",

View File

@@ -26,13 +26,14 @@
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-05 14:57:03.841430",
"modified": "2021-08-31 12:37:23.832131",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Course Review",

View File

@@ -13,6 +13,8 @@
{
"fieldname": "quiz",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Quiz",
"options": "LMS Quiz"
},
@@ -25,12 +27,13 @@
{
"fieldname": "score",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Score"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-07 14:19:54.958989",
"modified": "2021-08-26 12:24:15.973829",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz Submission",

View File

@@ -20,6 +20,7 @@ import markdown
from markdown import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
import html as HTML
def markdown_to_html(text):
"""Renders markdown text into html.
@@ -109,4 +110,5 @@ def sanitize_html(html, macro):
classname = ""
if macro == "YouTubeVideo":
classname = "lesson-video"
return "<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"

View File

@@ -8,7 +8,7 @@ frappe.ready(function () {
frappe.web_form.after_save = () => {
setTimeout(() => {
window.location.href = `/user/${frappe.web_form.get_value(["username"])}`;
window.location.href = `/profile_/${frappe.web_form.get_value(["username"])}`;
})
}
})

View File

View File

@@ -0,0 +1,9 @@
<div>
<h2 class="section-title">{{ title }}</h2>
<div class="cards-parent mt-10">
{% for course_row in courses %}
{% set course = frappe.get_doc("LMS Course", course_row.course) %}
{{ widgets.CourseCard(course=course) }}
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,39 @@
{
"__unsaved": 1,
"creation": "2021-08-26 10:28:55.796139",
"docstatus": 0,
"doctype": "Web Template",
"fields": [
{
"__unsaved": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 0
},
{
"__unsaved": 1,
"fieldname": "courses",
"fieldtype": "Table Break",
"label": "Courses",
"reqd": 0
},
{
"__unsaved": 1,
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"reqd": 0
}
],
"idx": 0,
"modified": "2021-08-26 10:35:35.903834",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Cards",
"owner": "Administrator",
"standard": 1,
"template": "",
"type": "Section"
}

View File

@@ -19,7 +19,7 @@
{% for lesson in course.get_lessons(chapter) %}
<div class="lesson-info{% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
<div class="lesson-info {% if membership.current_lesson == lesson.name %} active-lesson {% endif %}">
{% if membership or lesson.include_in_preview or is_instructor %}
<a class="lesson-links" href="{{ course.get_learn_url(lesson.number) }}{{course.query_parameter}}"
@@ -57,17 +57,18 @@
frappe.ready(() => {
expand_the_active_chapter();
$(".chapter-title").click((e) => {
$(".chapter-title").unbind().click((e) => {
rotate_chapter_icon(e);
});
})
var expand_the_first_chapter = () => {
var elements = $(".collapse");
var elements = $(".course-outline .collapse");
elements.each((i, element) => {
if (i <= 1) {
if (i < 1) {
show_section(element);
return false;
}
});
}
@@ -103,7 +104,6 @@
}
var rotate_chapter_icon = (e) => {
e.preventDefault();
var icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
@@ -111,5 +111,4 @@
icon.css("transform", "none");
}
}
</script>

View File

@@ -70,7 +70,7 @@
<a class="stretched-link" href="{{ course.get_learn_url(lesson_index) }}{{ query_parameter }}"></a>
{% else %}
<div class="view-course-link">
<div class="view-course-link is-default">
View Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
</div>
<a class="stretched-link" href="/courses/{{ course.name }}"></a>

View File

@@ -3,7 +3,7 @@
<div class="course-home-headings">
Course Outline
</div>
<div class="coure-outline">
<div class="common-card-style course-outline">
{% for chapter in course.get_chapters() %}
{{ widgets.ChapterTeaser(index=loop.index, chapter=chapter, course=course, batch=batch, membership=membership) }}
{% endfor %}

View File

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

View File

@@ -1,6 +1,6 @@
<div class="instructor">
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
<a class="ml-1 instructor-title" href="/user/{{instructor.username}}">{{ instructor.full_name }}</a>
<a class="ml-1 instructor-title" href="{{get_profile_url(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

@@ -11,5 +11,5 @@
Created {{ course_count }} {{ suffix }}
</div>
{% endif %}
<a class="stretched-link" href="/user/{{ member.username }}"></a>
<a class="stretched-link" href="{{ get_profile_url(member.username) }}"></a>
</div>

View File

@@ -1,3 +1,4 @@
{% if not course.upcoming %}
<div class="reviews-parent">
{% set reviews = course.get_reviews() %}
{% if reviews | length or course.is_eligible_to_review(membership) %}
@@ -18,7 +19,7 @@
<div class="review-card-footer">
<div>
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
<a class="button-links" href="/user/{{review.owner_details.username}}">
<a class="button-links" href="{{get_profile_url(review.owner_details.username) }}">
<span class="course-instructor">
{{ review.owner_details.full_name }}
</span>
@@ -35,6 +36,10 @@
</div>
{% endfor %}
</div>
{% else %}
<div class="common-card-style thread-card">
<span class="text-center"> No Reviews <img src="/assets/community/icons/slash.svg"></span>
</div>
{% endif %}
</div>
@@ -89,3 +94,4 @@
</div>
</div>
</div>
{% endif %}

View File

@@ -49,6 +49,15 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "LMS Course Interest",
"link_to": "LMS Course Interest",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@@ -121,7 +130,7 @@
"type": "Link"
}
],
"modified": "2021-06-29 15:11:07.324651",
"modified": "2021-08-31 10:33:39.838535",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS",

View File

@@ -58,7 +58,7 @@ class CustomUser(User):
"""
return frappe.get_all(
'LMS Course', {
'owner': self.name,
'instructor': self.name,
'is_published': True
})

105
community/page_renderers.py Normal file
View File

@@ -0,0 +1,105 @@
"""Custom page renderers for Community app.
Handles rendering of profile pages.
"""
import re
import frappe
from frappe.website.page_renderers.base_renderer import BaseRenderer
from frappe.website.page_renderers.template_page import TemplatePage
from frappe.website.page_renderers.document_page import DocumentPage
from frappe.website.page_renderers.list_page import ListPage
from frappe.website.page_renderers.not_found_page import NotFoundPage
from frappe.website.page_renderers.print_page import PrintPage
from frappe.website.page_renderers.redirect_page import RedirectPage
from frappe.website.page_renderers.static_page import StaticPage
from frappe.website.page_renderers.template_page import TemplatePage
from frappe.website.page_renderers.web_form import WebFormPage
def get_profile_url(username):
"""Returns the profile URL given username.
The default URL prefix for profiles is /users, but tha can be customized.
This functions looks at the current value from the config and generates
the URL for the profile.
"""
return get_profile_url_prefix() + username
def get_profile_url_prefix():
hooks = frappe.get_hooks("profile_url_prefix") or ["/users/"]
return hooks[-1]
RE_USERNAME = re.compile("[a-zA-Z0-9_]{4,}")
class ProfileRedirectPage(BaseRenderer):
"""Renderer to redirect /profile_/foo to <profile_prefix>/foo.
This is useful to redirect to profile pages from javascript as there is no
easy to find the profile prefix.
"""
def can_render(self):
return self.path.startswith("profile_/")
def render(self):
username = self.path[len("profile_/"):]
frappe.flags.redirect_location = get_profile_url_prefix() + username
return RedirectPage(self.path).render()
class ProfilePage(BaseRenderer):
def __init__(self, path, http_status_code):
super().__init__(path, http_status_code)
self.renderer = None
def can_render(self):
if "." in self.path:
return False
# has prefix and path starts with prefix?
prefix = get_profile_url_prefix().lstrip("/")
if prefix and not self.path.startswith(prefix):
return False
# not a userpage?
username = self.get_username()
if not RE_USERNAME.match(username):
return False
# if there is prefix then we can allow all usernames
if prefix:
return True
# if we are having top-level usernames, then give preference to
# the existing website_route_rules, web pages, web forms etc.
# Don't handle any of the exsiting website_route_rules
routes = [rule['to_route'] for rule in frappe.get_hooks("website_route_rules")]
if self.path in routes:
return False
# if any of the existing renders can render, let them do
renderers = [StaticPage, WebFormPage, DocumentPage, TemplatePage, ListPage, PrintPage]
for renderer in renderers:
renderer_instance = renderer(self.path, 200)
if renderer_instance.can_render():
self.renderer = renderer_instance
return True
return True
def get_username(self):
prefix = get_profile_url_prefix().lstrip("/")
return self.path[len(prefix):]
def render(self):
if self.renderer:
return self.renderer.render()
else:
username = self.get_username()
return render_portal_page("profiles/profile", username=username)
def render_portal_page(path, **kwargs):
frappe.form_dict.update(kwargs)
page = TemplatePage(path)
return page.render()

View File

@@ -9,3 +9,5 @@ community.patches.replace_member_with_user_in_mentor_request
community.patches.v0_0.chapter_lesson_index_table
execute:frappe.delete_doc("DocType", "LMS Message")
community.patches.v0_0.course_instructor_update
execute:frappe.delete_doc("DocType", "Discussion Message")
execute:frappe.delete_doc("DocType", "Discussion Thread")

View File

@@ -106,3 +106,6 @@ def youtube_video_renderer(video_id):
allowfullscreen>
</iframe>
"""
def video_renderer(src):
return "<video controls width='100%'><source src={0} type='video/mp4'></video>".format(src)

View File

@@ -20,7 +20,6 @@
--received-message: var(--c8);
--checkbox-size: 14px;
--control-bg: var(--gray-100);
--muted-text: #4C5A67;
--button-background: #EEF0F2;
--text-xs: 11px;
--text-sm: 12px;
@@ -32,107 +31,10 @@
--text-3xl: 22px;
}
body {
padding: 0px;
margin: 0px;
}
.chapter-plan {
border-radius: 10px;
margin: 20px 0px;
padding: 20px;
border: 1px solid #ddc;
background: white;
}
.chapter-plan h3 {
font-size: 1.1em;
font-weight: bold;
}
.batch {
border-radius: 10px;
margin: 10px 0px;
background: white;
box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);
border: 1px solid #ddc;
}
.batch-details {
padding: 20px;
}
.batch .cta {
margin-top: 10px;
padding: 10px;
min-height: 28px;
text-align: right;
border-top: 1px solid #ddc;
}
.batch .right {
float: right;
}
img.profile-photo {
width: 24px;
height: 24px;
border-radius: 50%;
}
.lesson-type {
padding-right: 5px;
}
section {
padding: 5rem 0 5rem 0;
}
.batch-header {
background: #eee;
border: 2px solid #ddd;
}
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border: 1px solid #d1d8dd;
border-radius: 4px;
background-color: #fff;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid #d1d8dd;
}
.page-card .page-card-head .indicator {
color: #36414C;
font-size: 14px;
}
.page-card .page-card-head .indicator::before {
margin: 0 6px 0.5px 0px;
}
.page-card .btn {
margin-top: 30px;
}
input[type=checkbox] {
appearance: auto;
}
.progress-image {
margin-right: 3px;
border-radius: 50px;
padding: 5px;
}
.course-image {
height: 168px;
width: 100%;
@@ -162,8 +64,8 @@ input[type=checkbox] {
.course-tags {
display: flex;
position: relative;
top: 0.75rem;
left: 0.75rem;
top: 1rem;
left: 1rem;
}
.course-card-pills {
@@ -184,8 +86,9 @@ input[type=checkbox] {
.common-page-style {
background: #F4F5F6;
padding-bottom: 2rem;
padding-bottom: 5rem;
min-height: 60vh;
padding-top: 3rem;
}
.common-card-style {
@@ -194,7 +97,7 @@ input[type=checkbox] {
border-radius: 8px;
position: relative;
border: 1px solid #EEF0F2;
box-shadow: 0 1px 4px 4px rgb(25, 39, 52, 0.02);
box-shadow: 0 0px 4px 2px #19273405;
}
.course-card {
@@ -205,8 +108,6 @@ input[type=checkbox] {
.muted-text {
font-size: 12px;
line-height: 135%;
color: var(--muted-text);
height: 15px;
}
.course-card-meta {
@@ -242,12 +143,12 @@ input[type=checkbox] {
}
.card-divider {
border: 1px solid #F4F5F6;
border: 1px solid #EEF0F2;
margin-bottom: 1rem;
}
.card-divider-dark {
border: 1px solid #E2E6E9;
border: 1px solid #C8CFD5;
margin-bottom: 16px;
}
@@ -265,37 +166,24 @@ input[type=checkbox] {
.course-student-count {
font-size: 12px;
line-height: 135%;
color: var(--muted-text);
float: right;
}
.view-course-link {
height: 32px;
background: var(--button-background);
border-radius: 4px;
font-size: 12px;
padding: 8px 0px 8px;
text-align: center;
line-height: 135%;
color: var(--text-color);
}
.view-talk-link {
background: var(--button-background);
border-radius: 4px;
font-size: 14px;
padding: 8px 22px 8px;
text-align: center;
line-height: 135%;
color: var(--text-color);
}
.cards-parent {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-moz-column-gap: 32px;
column-gap: 32px;
row-gap: 32px;
-moz-column-gap: 40px;
column-gap: 40px;
row-gap: 40px;
align-items: center;
}
@@ -318,7 +206,8 @@ input[type=checkbox] {
}
.courses-header {
padding: 50px 20px 20px;
margin-bottom: 1.5rem;
padding: 0 1rem;
color: var(--text-color);
font-weight: 600;
font-size: 22px;
@@ -333,8 +222,13 @@ input[type=checkbox] {
}
}
.button-links {
color: #4C5A67;
}
.button-links:hover {
text-decoration: none;
color: #4C5A67;
}
.icon-background {
@@ -420,7 +314,7 @@ input[type=checkbox] {
height: 1.5rem;
width: 1.5rem;
border: 1px solid black;
border-radius: 5px;
border-radius: 8px;
}
.custom-checkbox>label>input:checked+.empty-checkbox {
@@ -438,7 +332,7 @@ input[type=checkbox] {
flex-direction: row;
padding: 24px;
background: #E2E6E9;
border-radius: 12px;
border-radius: 8px;
margin-top: 16px;
}
@@ -463,7 +357,7 @@ input[type=checkbox] {
background-position: center;
background-repeat: no-repeat;
margin-right: 32px;
border-radius: 5px;
border-radius: 8px;
flex: 1;
align-self: center;
}
@@ -545,7 +439,7 @@ input[type=checkbox] {
.button {
box-shadow: var(--btn-shadow);
border-radius: 6px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
@@ -586,8 +480,8 @@ input[type=checkbox] {
}
.is-default {
background: #98A1A9;
color: #ffffff;
background: #F4F5F6;
color: #1F272E;
}
@media (max-width: 600px) {
@@ -736,9 +630,8 @@ input[type=checkbox] {
margin-left: .875rem;
}
.coure-outline {
background: #FFFFFF;
border-radius: 12px;
.course-outline {
flex-direction: column;
padding: 16px 12px 16px;
}
@@ -763,7 +656,7 @@ input[type=checkbox] {
padding: 20px 0px 16px;
}
.member-card .talk-title{
.member-card .talk-title {
font-weight: bold;
}
@@ -792,7 +685,6 @@ input[type=checkbox] {
font-weight: bold;
}
.member-card-xl .member-card-title {
font-weight: bold;
}
@@ -837,6 +729,7 @@ input[type=checkbox] {
letter-spacing: -0.0175em;
color: #192734;
margin-bottom: 1rem;
padding: 0 1rem;
}
.course-detail-headings {
@@ -847,7 +740,7 @@ input[type=checkbox] {
margin: 0;
}
.avatar-medium-schedule{
.avatar-medium-schedule {
width: 70px;
height: 70px;
}
@@ -881,6 +774,14 @@ input[type=checkbox] {
flex-direction: column;
}
.description-card p {
line-height: 1.72;
}
.description-card p:last-child {
margin-bottom: 0;
}
.overview-card {
padding: 1.5rem;
width: 256px;
@@ -996,7 +897,7 @@ input[type=checkbox] {
}
.breadcrumb {
padding: 1rem 0 0;
padding: 0 1rem;
display: flex;
align-items: center;
font-size: 12px;
@@ -1052,7 +953,7 @@ input[type=checkbox] {
.active-lesson {
background-color: #EBF5FF;
border-radius: 4px;
border-radius: 0.25rem;
}
.lesson-progress {
@@ -1061,7 +962,7 @@ input[type=checkbox] {
font-size: 10px;
line-height: 120%;
margin: 0px 10px 20px;
border-radius: 4px;
border-radius: 8px;
font-weight: bold;
}
@@ -1071,7 +972,7 @@ input[type=checkbox] {
.profile-banner {
height: 248px;
border-radius: 12px 12px 0px 0px;
border-radius: 8px 8px 0px 0px;
background-size: cover;
background-position: center;
}
@@ -1090,7 +991,7 @@ input[type=checkbox] {
.profile-info {
height: 68px;
background: #ffffff;
border-radius: 0px 0px 12px 12px;
border-radius: 0px 0px 8px 8px;
}
.profile-avatar {
@@ -1333,13 +1234,12 @@ pre {
font-size: 20px;
}
.schedule-info{
.schedule-info {
padding: 20px;
}
.drop-down-icon {
padding : 5px 0 0 5px;
padding: 5px 0 0 5px;
}
.event-btn {
@@ -1354,12 +1254,12 @@ pre {
padding-bottom: 30px;
}
.exhibitor-card .company-name{
.exhibitor-card .company-name {
font-size: 25px;
margin-top: 30px;
}
.exhibitor-card .company-logo{
.exhibitor-card .company-logo {
height: 158px;
width: 252px;
object-fit: contain;
@@ -1376,23 +1276,83 @@ pre {
margin-top: 6px;
}
.info-avatar img{
.info-avatar img {
object-fit: contain;
}
.thread-card {
flex-direction: column;
padding: 1.5rem;
padding: 1rem;
}
textarea.form-control {
.discussions-parent .form-control {
background-color: #FFFFFF;
font-size: inherit;
color: inherit;
padding: 0.75rem 1rem;
}
.discussion-on-page .comment-field {
height: 48px;
box-shadow: inset 0px 0px 4px rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.modal .comment-field {
height: 300px;
}
.discussion-on-page textarea.form-control {
background-color: #FFFFFF;
color: inherit;
height: 160px;
.no-discussions {
width: 250px;
margin: 0 auto;
text-align: center;
}
.no-discussions .button {
margin: auto;
}
.discussions-header {
margin: 2.5rem 0 1.25rem;
}
.discussions-header .button {
float: right;
}
.discussions-parent .search-field {
background-color: #E2E6E9;
background-image: url(/assets/community/icons/search.svg);
background-repeat: no-repeat;
text-indent: 1.5rem;
background-position: 1rem 0.7rem;
height: 36px;
font-size: 12px;
padding: 0.65rem 0.9rem;
}
.discussions-sidebar {
background-color: #F4F5F6;
padding: 0.75rem;
border-radius: 4px;
max-height: 700px;
overflow-y: auto;
}
#discussion-group {
max-height: 700px;
overflow-y: auto;
}
.sidebar-topic {
padding: 0.75rem;
margin: 0.75rem 0;
cursor: pointer;
}
.sidebar-topic[aria-expanded="true"] {
background: #FFFFFF;
border-radius: 4px;
}
.comment-footer {
@@ -1400,52 +1360,87 @@ textarea.form-control {
justify-content: flex-end;
}
.comment-field {
font-size: inherit;
.reply-card {
margin-bottom: 40px;
}
.thread-title {
font-size: inherit;
.discussions-parent .collapsing {
transition: height 0s;
}
.message-author {
color: #192734;
margin-left: 0.5rem;
.discussion-topic-title {
color: var(--text-color);
}
.discussion-on-page .topic-title {
display: none;
}
.discussions-sidebar .sidebar-parent:last-child .card-divider {
display: none;
}
.certificate-page .common-card-style {
flex-direction: column;
font-family: Inter;
color: black;
font-size: 2rem;
text-align: center;
padding: 5rem;
background-image: url(/assets/community/images/certificate-background.png);
font-size: 1.25rem;
}
.certificate-content {
padding: 5rem 9rem;
}
.certificate-footer {
margin-bottom: 4rem;
}
.certificate-ribbon {
background-color: var(--primary-color);
margin-right: 3rem;
width: 5%;
}
.certificate-heading {
font-size: 4rem;
font-size: 3rem;
margin-bottom: 3rem;
font-weight: bold;
}
.certificate-para {
margin-bottom: 3rem;
margin-bottom: 4rem;
}
.certificate-logo {
height: 30px;
}
@media (max-width: 1024px) {
.certificate-content {
padding: 3rem 5rem;
}
}
@media (max-width: 768px) {
.certificate-page .common-card-style {
padding: 2rem;
font-size: 1.5rem;
}
.certificate-heading {
font-size: 3rem;
}
}
@media (max-width: 360px) {
.certificate-heading {
font-size: 2rem;
}
}
@media (max-width: 500px) {
.certificate-page .common-card-style {
font-size: 1rem;
}
.certificate-content {
padding: 3rem;
}
.certificate-ribbon {
background-color: var(--primary-color);
margin-right: 2rem;
width: 20%;
}
}
@media (max-width: 375px) {
.certificate-content {
padding: 2rem;
}
}

View File

@@ -1,5 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00001 3.00001H12L15.5 3C16.6046 3 17.5 3.89543 17.5 5V9.78889V12.2556C17.5 13.3601 16.6046 14.2556 15.5 14.2556H14.5C14.2239 14.2556 14 14.4794 14 14.7556V16.4507C14 16.8715 13.5119 17.1041 13.185 16.839L10.2754 14.4789C10.0972 14.3344 9.87483 14.2556 9.64544 14.2556H4.49997C3.39539 14.2556 2.49995 13.3601 2.49997 12.2555L2.50001 9.78889V5.00001C2.50001 3.89544 3.39544 3.00001 4.50001 3.00001L6.00001 3.00001Z" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="square"/>
<path d="M6 6.5H13" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M6 9H10" stroke="#1F272E" stroke-miterlimit="10" stroke-linecap="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.66659 9.77761C2.66659 10.0919 2.79145 10.3934 3.01372 10.6157C3.23598 10.8379 3.53744 10.9628 3.85177 10.9628H10.9629L13.3333 13.3332V3.85169C13.3333 3.53736 13.2084 3.2359 12.9861 3.01364C12.7639 2.79137 12.4624 2.6665 12.1481 2.6665H3.85177C3.53744 2.6665 3.23598 2.79137 3.01372 3.01364C2.79145 3.2359 2.66659 3.53736 2.66659 3.85169V9.77761Z" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 528 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 7.44462C3.5 5.26607 5.26607 3.5 7.44462 3.5C9.62318 3.5 11.3892 5.26607 11.3892 7.44462C11.3892 8.50829 10.9683 9.47362 10.2838 10.1831C10.265 10.1972 10.247 10.2128 10.2299 10.2299C10.2128 10.247 10.1972 10.265 10.1831 10.2838C9.47362 10.9683 8.50829 11.3892 7.44462 11.3892C5.26607 11.3892 3.5 9.62318 3.5 7.44462ZM10.5696 11.2767C9.71788 11.9722 8.62996 12.3892 7.44462 12.3892C4.71378 12.3892 2.5 10.1755 2.5 7.44462C2.5 4.71378 4.71378 2.5 7.44462 2.5C10.1755 2.5 12.3892 4.71378 12.3892 7.44462C12.3892 8.62996 11.9722 9.71788 11.2767 10.5696L13.3538 12.6467C13.549 12.8419 13.549 13.1585 13.3538 13.3538C13.1585 13.549 12.8419 13.549 12.6467 13.3538L10.5696 11.2767Z" fill="#4C5A67"/>
</svg>

After

Width:  |  Height:  |  Size: 849 B

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5229 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5229 6.47715 22 12 22Z" stroke="#4C5A67" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 4.93005L19.14 19.0701" stroke="#4C5A67" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 428 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5005 4.3335C8.5005 4.05735 8.27665 3.8335 8.0005 3.8335C7.72436 3.8335 7.5005 4.05735 7.5005 4.3335V7.50062H4.33337C4.05723 7.50062 3.83337 7.72447 3.83337 8.00062C3.83337 8.27676 4.05723 8.50062 4.33337 8.50062H7.5005V11.6677C7.5005 11.9439 7.72436 12.1677 8.0005 12.1677C8.27665 12.1677 8.5005 11.9439 8.5005 11.6677V8.50062H11.6676C11.9438 8.50062 12.1676 8.27676 12.1676 8.00062C12.1676 7.72447 11.9438 7.50062 11.6676 7.50062H8.5005V4.3335Z" fill="#4C5A67"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@@ -1,32 +1,37 @@
<div class="common-card-style">
<div class="certificate-heading">
Certificate of Completion
</div>
<div class="certificate-para">
This is to certify that <span class="font-weight-bold">{{ student.full_name }}</span> has successfully completed
<span class="font-weight-bold">{{ course.title }}</span> online course on
<span class="font-weight-bold">{{ frappe.utils.format_date(certificate.issue_date, "medium") }}</span>
</div>
<div style="display: flex; justify-content: space-between;" class="certificate-footer">
<div>
<div class="font-weight-bold">
Instructor:
</div>
<div>
{{ instructor.full_name }}
</div>
<div class="certificate-content">
<div class="certificate-heading">
Certificate of Completion
</div>
<div>
<div class="font-weight-bold">
Expiry Date:
</div>
<div>
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</div>
<div class="certificate-para">
This is to certify that <span class="font-weight-bold">{{ student.full_name }}</span> has successfully completed
<span class="font-weight-bold">{{ course.title }}</span> online course on
<span class="font-weight-bold">{{ frappe.utils.format_date(certificate.issue_date, "medium") }}</span>
</div>
<div class="certificate-footer">
{% if instructor %}
<div>
<span>
Instructor:
</span>
<span class="font-weight-bold">
{{ instructor.full_name }}
</span>
</div>
{% endif %}
{% if certificate.expiry_date %}
<div>
<span>
Expiry Date:
</span>
<span class="font-weight-bold">
{{ frappe.utils.format_date(certificate.expiry_date, "medium") }}
</span>
</div>
{% endif %}
</div>
<img src="{{ logo }}" class="certificate-logo">
</div>
<div>
<img src="{{ logo }}" style="height: 50px;">
</div>
<div class="certificate-ribbon"></div>
</div>
<script src="/assets/community/js/html2canvas.js"></script>

View File

@@ -0,0 +1,7 @@
{% if frappe.session.user != "Guest" and
(condition is not defined or (condition is defined and condition )) %}
<span class="button is-secondary reply">
New Discussion
<img src="/assets/community/icons/small-add-black.svg">
</span>
{% endif %}

View File

@@ -0,0 +1,198 @@
frappe.ready(() => {
setup_socket_io();
set_docname_if_missing();
expand_first_discussion();
$(".search-field").keyup((e) => {
search_topic(e);
});
$(".reply").click((e) => {
show_new_topic_modal(e);
});
$("#login-from-discussion").click((e) => {
login_from_discussion(e);
});
$(".sidebar-topic").click((e) => {
if ($(e.currentTarget).attr("aria-expanded") == "true") {
e.stopPropagation();
}
});
$(document).on("click", ".submit-discussion", (e) => {
submit_discussion(e);
});
})
var show_new_topic_modal = (e) => {
e.preventDefault();
$("#discussion-modal").modal("show");
var topic = $(e.currentTarget).attr("data-topic");
$(".modal-headings").text(topic ? "Reply" : "Start a Discussion");
topic ? $(".topic-title").addClass("hide") : $(".topic-title").removeClass("hide");
$("#submit-discussion").attr("data-topic", topic ? topic : "");
}
var setup_socket_io = () => {
const assets = [
"/assets/frappe/js/lib/socket.io.min.js",
"/assets/frappe/js/frappe/socketio_client.js",
]
frappe.require(assets, () => {
if (window.dev_server) {
frappe.boot.socketio_port = "9000";
}
frappe.socketio.init(9000);
frappe.socketio.socket.on("publish_message", (data) => {
publish_message(data);
})
})
}
var publish_message = (data) => {
post_message_cleanup();
if ($(`.discussion-on-page[data-topic=${data.topic_info.name}]`).length) {
if ($(`.discussion-on-page[data-topic=${data.topic_info.name}] .card-divider-dark`).length) {
$(data.template).insertAfter(`.discussion-on-page[data-topic=${data.topic_info.name}] .card-divider-dark`);
}
else {
$('<div class="card-divider-dark mb-10"></div>' + data.template).insertAfter(`.discussion-on-page[data-topic=${data.topic_info.name}] .discussion-form`);
}
}
else if ((decodeURIComponent($(".discussions-parent .course-content-parent").attr("data-doctype")) == data.topic_info.reference_doctype
&& decodeURIComponent($(".discussions-parent .course-content-parent").attr("data-docname")) == data.topic_info.reference_docname)) {
$(data.sidebar).insertAfter(`.discussions-sidebar .form-group`);
$(`#discussion-group`).prepend(data.new_topic_template);
if (data.topic_info.owner == frappe.session.user) {
$(".discussion-on-page").collapse();
$(".sidebar-topic").first().click();
}
}
else {
window.location.reload();
}
update_reply_count(data.topic_info.name);
}
var post_message_cleanup = () => {
$(".comment-field").val("");
$("#discussion-modal").modal("hide");
$("#no-discussions").addClass("hide");
}
var update_reply_count = (topic) => {
var reply_count = $(`[data-target='#t${topic}']`).find(".reply-count").text();
reply_count = parseInt(reply_count) + 1;
$(`[data-target='#t${topic}']`).find(".reply-count").text(reply_count);
}
var set_docname_if_missing = () => {
if ($("[data-docname='None']").length) {
frappe.call({
method: "community.community.doctype.discussion_topic.discussion_topic.get_docname",
args: {
"route": window.location.href.split("/").slice(-1)[0]
},
callback: (data) => {
$("[data-docname='None']").attr("data-docname", data.message);
}
})
}
}
var expand_first_discussion = () => {
$($(".discussions-parent .collapse")[0]).addClass("show");
$($(".discussions-sidebar [data-toggle='collapse']")[0]).attr("aria-expanded", true);
}
var search_topic = (e) => {
var input = $(e.currentTarget).val();
var topics = $(".discussions-parent .discussion-topic-title");
if (input.length < 3 || input.trim() == "") {
topics.closest(".sidebar-parent").removeClass("hide");
return
}
topics.each((i, elem) => {
var topic_id = $(elem).parent().attr("data-target");
/* Check match in replies */
var match_in_reply = false;
var replies = $(`${topic_id}`);
for (var reply of replies.find(".reply-text")) {
if (has_common_substring($(reply).text(), input)) {
match_in_reply = true;
break;
}
}
/* Match found in title or replies, then show */
if (has_common_substring($(elem).text(), input) || match_in_reply) {
$(elem).closest(".sidebar-parent").removeClass("hide")
}
else {
$(elem).closest(".sidebar-parent").addClass("hide");
}
})
}
var has_common_substring = (str1, str2) => {
var str1_arr = str1.toLowerCase().split(" ");
var str2_arr = str2.toLowerCase().split(" ");
var substring_found = false;
for (var first_word of str1_arr) {
for (var second_word of str2_arr) {
if (first_word.indexOf(second_word) > -1) {
substring_found = true;
break;
}
}
}
return substring_found;
}
var submit_discussion = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
var title = $(".topic-title:visible").length ? $(".topic-title:visible").val().trim() : "";
var reply = $(".comment-field:visible").val().trim();
if (reply) {
var doctype = $(e.currentTarget).attr("data-doctype");
doctype = doctype ? decodeURIComponent(doctype) : doctype;
var docname = $(e.currentTarget).attr("data-docname");
docname = docname ? decodeURIComponent(docname) : docname;
frappe.call({
method: "community.community.doctype.discussion_topic.discussion_topic.submit_discussion",
args: {
"doctype": doctype ? doctype : "",
"docname": docname ? docname : "",
"reply": reply,
"title": title,
"topic_name": $(e.currentTarget).closest(".discussion-on-page").attr("data-topic")
}
})
}
}
var login_from_discussion = (e) => {
var redirect = $(e.currentTarget).attr("data-redirect") || window.location.href;
window.location.href = `/login?redirect-to=${redirect}`;
}

View File

@@ -0,0 +1,13 @@
<div class="reply-card">
{% set member = frappe.get_doc("User", reply.owner) %}
<div class="d-flex align-items-center muted-text">
{% set member = frappe.get_doc("User", reply.owner) %}
{{ widgets.Avatar(member=member, avatar_class="avatar-small")}}
<a class="button-links ml-2" href="{{ get_profile_url(member.username) }}">
{{ member.full_name }}
</a>
<div class="ml-2 frappe-timestamp" data-timestamp="{{ reply.creation }}"> just now </div>
</div>
<div class="card-divider mt-3"></div>
<div class="reply-text">{{ reply.reply }}</div>
</div>

View File

@@ -0,0 +1,41 @@
{% for topic in topics %}
{% set replies = frappe.get_all("Discussion Reply", {"topic": topic.name},
["reply", "owner", "creation"], order_by="creation desc")%}
<!-- Hack to reorganize the replies array. So even though we want reverse cronology, the first reply should appear first. -->
{% if replies.insert(0, replies.pop()) %}{% endif %}
{% if replies %}
<div class="collapse discussion-on-page" id="t{{ topic.name }}" data-topic="{{ topic.name }}"
data-parent="#discussion-group">
<div class="course-home-headings p-0">{{ topic.title }}</div>
{% for reply in replies %}
{% if loop.index == 2 %}
<div class="card-divider-dark mb-10"></div>
{% endif %}
{% include "community/templates/discussions/reply_card.html" %}
{% if loop.index == 1 %}
{% if frappe.session.user == "Guest" or (condition is defined and not condition) %}
<div class="d-flex flex-column align-items-center muted-text">
Want to join the discussion?
{% if frappe.session.user == "Guest" %}
<div class="button is-primary mt-3 mb-3" id="login-from-discussion">Log In</div>
{% elif not condition %}
<div class="button is-primary mt-3 mb-3" id="login-from-discussion" data-redirect="{{ redirect_to }}">{{ button_name }}
</div>
{% endif %}
</div>
{% else %}
{{ widgets.CommentBox(doctype=doctype, docname=docname) }}
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endfor %}

View File

@@ -0,0 +1,9 @@
<div class="form-group">
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control search-field"
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="Search Topics"
spellcheck="false"></textarea>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<div class="sidebar-parent">
<div class="sidebar-topic" data-target="#t{{ topic.name }}" data-toggle="collapse" aria-expanded="false">
<div class="discussion-topic-title">{{ topic.title }}</div>
<div class="mt-2 mb-3">
{% set creator = frappe.get_doc("User", topic.owner) %}
{{ widgets.Avatar(member=creator, avatar_class="avatar-small") }}
<span class="course-instructor">
{{ creator.full_name }}
</span>
<span class="muted-text pull-right">
<span class="mr-2">
<img src="/assets/community/icons/message.svg">
<span class="reply-count">{{ replies | length }}</span>
</span>
<span> {{ frappe.utils.format_date(topic.creation, "dd MMM YYYY") }} </span>
</span>
</div>
</div>
<div class="card-divider"></div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="modal fade discussion-modal" id="discussion-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="course-home-headings modal-headings">Start a Discussion</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ widgets.CommentBox(doctype=doctype, docname=docname) }}
</div>
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
<div class="common-card-style thread-card mb-5">
<div class="mb-4">
{% set member = frappe.get_doc("User", message.owner) %}
{{ widgets.Avatar(member=member, avatar_class="avatar-small")}}
<a class="button-links" href="/user/{{ member.username }}">
<span class="message-author">{{ member.full_name }}</span>
</a>
<span class="muted-text pull-right">{{ frappe.utils.format_datetime(message.creation, "dd MMM hh:mm") }}</span>
</div>
<div class="card-divider"></div>
<div>{{ message.message }}</div>
</div>

View File

@@ -26,10 +26,9 @@
<div class="custom-checkbox">
<label class="quiz-label">
<input class="option" value="{{ option | urlencode }}"
data-correct="{{ question['is_correct_' + loop.index | string] }}"
{% if question.multiple %} type="checkbox"
{% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
<img class="empty-checkbox mr-3"/>
data-correct="{{ question['is_correct_' + loop.index | string] }}" {% if question.multiple %}
type="checkbox" {% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
<img class="empty-checkbox mr-3" />
</label>
<span class="label-area">{{ frappe.utils.md_to_html(option) }}</span>
</div>
@@ -51,6 +50,7 @@
<button class="btn btn-primary pull-right" id="check" disabled>Check</button>
<button class="btn btn-primary hide" id="next">Next</button>
<button class="btn btn-primary hide" id="summary">Summary</button>
<small id="submission-message" class="font-weight-bold hide"> Please join the course to submit the Quiz.</small>
</div>
<div class="button is-secondary pull-right hide" id="try-again">Try Again</div>
<h4 class="success-message"></h4>

View File

@@ -27,27 +27,24 @@
{% if membership %}
{{ pagination(prev_url, next_url) }}
{% endif %}
{% set title = lesson.title + " - " + course.title %}
{% set condition = membership or is_instructor %}
{{ widgets.DiscussionMessage(doctype="Lesson", docname=lesson.name,
title=title, condition=condition, button_name="Start Learning",
redirect_to="/courses/" + course.name) }}
</div>
</div>
{{ Discussions() }}
</div>
</div>
{% endblock %}
{% macro LessonContent(lesson) %}
{% set is_instructor = frappe.session.user == course.instructor %}
<div class="lesson-content">
<div class="course-home-headings title {% if membership %} is-member {% endif %}" data-lesson="{{ lesson.name }}"
<div class="course-home-headings title
{% if membership %} is-member {% endif %}
{% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}"
data-course="{{ course.name }}">
{{ lesson.title }}
<span class="lesson-progress {{hide if course.get_progress(lesson.name) != 'Complete' else ''}}">COMPLETED</span>
</div>
{% set is_instructor = frappe.session.user == course.instructor %}
{% if membership or lesson.include_in_preview or is_instructor %}
<div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
@@ -94,7 +91,7 @@
Next
<img class="ml-2" src="/assets/community/icons/side-arrow-white.svg">
</a>
{% elif course.enable_certification %}
{% elif course.enable_certification %}
<div class="button is-primary {% if course.get_course_progress() != 100 %} hide {% endif %}" id="certification">
Get Certificate
</div>
@@ -104,6 +101,15 @@
</div>
{% endmacro %}
{% macro Discussions() %}
{% set is_instructor = frappe.session.user == course.instructor %}
{% set title = lesson.title + " - " + course.title %}
{% set condition = is_instructor if is_instructor else membership %}
{{ widgets.DiscussionMessage(doctype="Lesson", docname=lesson.name,
condition=condition, button_name="Start Learning",
redirect_to="/courses/" + course.name) }}
{% endmacro %}
{%- block script %}
{{ super() }}

View File

@@ -133,7 +133,18 @@ var check_answer = (e) => {
$(".explanation").removeClass("hide");
$("#check").addClass("hide");
current_index == total_questions ? $("#summary").removeClass("hide") : $("#next").removeClass("hide");
if (current_index == total_questions) {
if ($(".eligible-for-submission").length) {
$("#summary").removeClass("hide")
}
else {
$("#submission-message").removeClass("hide");
}
}
else {
$("#next").removeClass("hide")
}
var [answer, is_correct] = parse_options();
add_to_local_storage(quiz_name, current_index, answer, is_correct)

View File

@@ -1,13 +1,9 @@
frappe.ready(() => {
if ($(document).width() <= 550) {
$(".certificate-footer").css("flex-direction", "column");
$(".certificate-footer").children().addClass("mb-5");
}
$("#export-as-pdf").click((e) => {
export_as_pdf(e);
})
})
var export_as_pdf = (e) => {

View File

@@ -22,8 +22,8 @@
{% macro CourseCardWide(course) %}
<div class="common-card-style course-card-wide">
<div class="course-image-wide {% if not course.image %} default-image {% endif %}"
{% if course.image %}style="background-image: url({{ course.image }});"{% endif %}>
<div class="course-image-wide {% if not course.image %} default-image {% endif %}" {% if course.image
%}style="background-image: url({{ course.image }});" {% endif %}>
<div class="course-tags">
{% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div>
@@ -59,8 +59,8 @@
</a>
{% endif %}
{% if course.upcoming %}
<button class="button wide-button is-default" id="notify-me" data-course="{{course.name | urlencode}}"
{% if is_user_interested %} disabled="disabled" title="Your interest has already been noted." {% endif %} >
<button class="button wide-button is-default" id="notify-me" data-course="{{course.name | urlencode}}" {% if
is_user_interested %} disabled="disabled" title="Your interest has already been noted." {% endif %}>
Notify me when available
</button>
{% endif %}
@@ -70,10 +70,6 @@
<img class="ml-2" src="/assets/community/images/play.png" />
</div>
{% endif %}
{% set certificate = course.is_certified() %}
{% if certificate %}
<a class="button wide-button is-secondary dark-links" href="/courses/{{ course.name }}/{{ certificate }}">Get Certificate</a>
{% endif %}
</div>
</div>
</div>
@@ -101,6 +97,7 @@
<!-- Course Outline and Creator -->
{% macro CourseOutlineAndCreator(course) %}
{% set certificate = course.is_certified() %}
<div class="course-outline-instructor-parent">
<div class="course-home-outline">
{{ widgets.CourseOutline(course=course, membership=membership) }}
@@ -137,9 +134,13 @@
{{ frappe.utils.rounded(progress) }}%
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: {{ progress }}%"
aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar" role="progressbar" style="width: {{ progress }}%" aria-valuenow="{{ progress }}"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
{% if certificate %}
<a class="muted-text dark-links mt-5 text-center" href="/courses/{{ course.name }}/{{ certificate }}">Get
Certificate</a>
{% endif %}
</div>
</div>
{% endif %}
@@ -178,7 +179,7 @@
<div class="course-home-headings">
Course Description
</div>
<div class="common-card-style description-card small-title">
<div class="common-card-style description-card">
{{ frappe.utils.md_to_html(course.description) }}
</div>
</div>

View File

@@ -1,16 +0,0 @@
{% extends "templates/base.html" %}
{% block title %}{{ thread.title }}{% endblock %}
{% block head_include %}
<style>
</style>
{% endblock %}
{% block content %}
<div class="common-page-style">
<div class="container">
{{ widgets.BreadCrumb(thread=thread) }}
<div class="course-home-headings">{{ thread.title }}</div>
{{ widgets.DiscussionMessage(thread=thread.name) }}
</div>
</div>
{% endblock %}

View File

@@ -1,18 +0,0 @@
import frappe
def get_context(context):
context.no_cache = 1
try:
thread_name = frappe.form_dict["discussion"]
except KeyError:
redirect_to_discussions()
context.thread = frappe.db.get_value("Discussion Thread", thread_name, ["name", "title"], as_dict=True)
if not len(context.thread):
redirect_to_discussions
def redirect_to_discussions():
frappe.local.flags.redirect_location = "/discussions"
raise frappe.Redirect

View File

@@ -1,65 +0,0 @@
{% extends "templates/base.html" %}
{% block title %}{{ 'Discussions' }}{% endblock %}
{% block head_include %}
<style>
</style>
{% endblock %}
{% block content %}
<div class="common-page-style">
<div class="container">
<div class="courses-header">
<span>{{_('Discussions')}}</span>
<div id="new-topic" class="button is-primary pull-right">
<img src="/assets/community/icons/small-add.svg">
Start a Discussion</div>
</div>
<div class="card-divider-dark"></div>
{% if threads | length %}
<div class="cards-parent">
{% for thread in threads %}
<div class="common-card-style thread-card">
<div class="course-card-title">{{ thread.title }}</div>
<div class="card-divider"></div>
<div>
<span class="course-student-count">
<span class="mr-4">
<img class="icon-background" src="/assets/community/icons/message.svg" />
{{ thread.message_count }}
</span>
<span class="mr-4">
<img class="icon-background" src="/assets/community/icons/user.svg" />
{{ thread.member_count }}
</span>
</span>
</div>
<a class="stretched-link" href="/discussions/{{ thread.name }}"></a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center font-weight-bold mt-5">
No discussions yet.
</div>
{% endif %}
</div>
</div>
<!-- New Topic Modal -->
<div class="modal fade discussion-modal" id="discussion-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="course-home-headings modal-headings">Start a Discussion</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ widgets.DiscussionComment() }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,14 +0,0 @@
frappe.ready(() => {
$("#new-topic").click((e) => {
show_new_topic_modal(e);
})
})
var show_new_topic_modal = (e) => {
e.preventDefault();
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/discussions/`;
return;
}
$("#discussion-modal").modal("show");
}

View File

@@ -1,17 +0,0 @@
import frappe
def get_context(context):
context.threads = get_threads()
def get_threads():
threads = frappe.get_all("Discussion Thread", fields=["name", "title"])
for thread in threads:
messages = frappe.get_all("Discussion Message",
{
"thread": thread.name
},
["owner"],
as_list=True)
thread.message_count = len(messages)
thread.member_count = len(set(messages))
return threads

View File

@@ -51,7 +51,7 @@
{% endif %}
</span>
{% if frappe.session.user == member.email %}
<a class="dark-links pull-right" href="edit-profile?name={{ member.email }}">Edit Profile</a>
<a class="dark-links pull-right" href="/edit-profile?name={{ member.email }}">Edit Profile</a>
{% endif %}
</div>
</div>
@@ -59,6 +59,9 @@
{% endmacro %}
{% macro AboutOverviewSection(member) %}
{% set enrollment = member.get_course_membership("Student") | length %}
{% set mentorship = member.get_course_membership("Mentor") | length %}
{% set reviews = member.get_user_reviews() | length %}
<div class="profile-parent-section">
{% if member.bio %}
<div class="profile-about-section">
@@ -70,31 +73,35 @@
</div>
</div>
{% endif %}
{% if enrollment or reviews or mentorship %}
<div class="course-overview-section">
<div class="course-home-headings">
Overview
</div>
<div class="common-card-style overview-card small-title">
{% if member.get_course_membership("Student") | length %}
{% if enrollment %}
<div class="overtime-item">
<img class="icon-background mr-1" src="/assets/community/icons/user.svg" />
{{ member.get_course_membership("Student") | length }} Enrolled
{{ enrollment }} Enrolled
</div>
{% endif %}
{% if member.get_user_reviews() | length %}
{% if reviews %}
<div class="overtime-item">
<img class="icon-background mr-1" src="/assets/community/icons/rating.svg" />
{{ member.get_user_reviews() | length }} Created
{{ reviews }} Created
</div>
{% endif %}
{% if member.get_course_membership("Mentor") | length%}
{% if mentorship %}
<div class="overtime-item">
<img class="icon-background mr-1" src="/assets/community/icons/calendar.svg" />
{{ member.get_course_membership("Mentor") | length }} Mentored
{{ mentorship }} Mentored
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endmacro %}

View File

@@ -1,39 +0,0 @@
# Mockups
HTML Mockups using [Mockdown][].
[Mockdown]: https://github.com/anandology/mockdown
## How to use
**Step 1:** Get into `mockups` directory
```
$ cd mockups
```
**Step 2:** Instal `mockdown`
```
$ pip install mockdown
```
**Step 3:** Start mockdown server
```
$ mockdown
...
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
...
```
**Step 4:** See the mockups at <http://localhost:5000/>.
## How does it work?
Mockdown uses [Jinja][] templates for writing HTML.
[Jinja]: https://jinja.palletsprojects.com/
To make is easy to provide test data, Mockdown looks for YAML file with the same name as the template. For example, `home.html` template uses the data from `home.yml`.

View File

@@ -1,43 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" crossorigin="anonymous">
<link href="/static/style.css" rel="stylesheet">
<title>{% block title %}FOSS United{% endblock %}</title>
</head>
<body>
<nav class="navbar navbar-light navbar-expand-lg">
<div class="container">
<a class="navbar-brand" href="/"><span>Home</span></a>
<button aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-target="#navbarSupportedContent" data-toggle="collapse" type="button">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="mr-auto navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/sketches.html">
Sketches
</a>
</li>
</ul>
<ul class="ml-auto navbar-nav">
<!-- post login tools -->
<li class="nav-item">
<a class="nav-link btn-login-area" href="/login.html">Login</a>
</li>
</ul>
</div>
</div>
</nav>
{% block content %}
<h1>Lorem ipsum...</h1>
{% endblock %}
</body>
</html>

View File

@@ -1,114 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="course-header">
<div class="course-type">course</div>
<h1>{{title}}</h1>
</div>
<div class="row">
<div class="col-lg-9 col-md-12">
<div class="course-details">
<h2>Course Description</h2>
<div class="course-description">
{{ description }}
</div>
<div class="preview-video">
<iframe
width="560"
height="315"
src="{{youtube_embed_url}}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
<h2>Upcoming Batches</h2>
<div class="row">
{% for batch in batches %}
<div class="col-lg-4 col-md-6">
<div class="batch">
<div class="batch-details">
<div>Session every {{batch.weekdays}}</div>
<div>{{batch.timeslot}}</div>
<div>Starting from {{batch.start_date}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in batch.mentors %}
<div>
<img class="profile-photo" src="{{m.photo_url}}">
<span class="instructor-title">{{m.name}}</span>
</div>
{% endfor %}
</div>
<div class="cta">
<div class="">
<button type="button">Join this Batch</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<h2>Course Outline</h2>
{% for chapter in chapters %}
<div class="chapter-plan">
<h3><span class="chapter-number">{{loop.index}}</span> {{chapter.title}}</h3>
<div class="chapter-description">
{{chapter.description}}
</div>
<div class="lessons">
{% for lesson in chapter.lessons %}
<div class="lesson">
<span class="lesson-type"><i class="{{lesson.icon}}"></i></span>
<span class="lesson-title">{{lesson.title}}</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="col-lg-3 col-md-12">
<div class="sidebar">
<h3>Instructor</h3>
<div class="instructor">
<div class="instructor-title">{{instructor.name}}</div>
<div class="instructor-subtitle">Created {{instructor.num_courses}} courses</div>
</div>
</div>
<div class="sidebar">
<h3>Mentors</h3>
{% for m in mentors %}
<div class="instructor">
<div class="instructor-title">{{m.name}}</div>
<div class="instructor-subtitle">Mentored {{m.num_courses}} batches</div>
</div>
{% endfor %}
<div class="notice">
Interested to become a mentor?
<div><a href="#">Apply Now!</a></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,89 +0,0 @@
title: The Joy of Programming
description: |
Learn the joy of programming by turning the computer into a canvas.
youtube_embed_url: "https://www.youtube.com/embed/IFWAYnUeHR8?start=149"
stats:
chapters: 4
lessons: 25
videos: 6
completed: 287
instructor:
name: Anand Chitipothu
num_courses: 4
mentors:
- name: Anand Chitipothu
num_courses: 4
- name: Rushabh Mehta
num_courses: 3
- name: Jannat Patel
num_courses: 3
batches:
- id: jp01
status: scheduled
mentors:
- name: Anand Chitipothu
photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg
num_batches: 4
start_date: May 3, 2021
weekdays: Mon, Thu
timeslot: 5:00-6:00 PM
- id: jp02
status: scheduled
mentors:
- name: Anand Chitipothu
photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg
num_batches: 4
start_date: May 4, 2021
weekdays: Tue, Fri
timeslot: 5:00-6:00 PM
- id: jp03
status: scheduled
mentors:
- name: Rusbhabh Mehta
photo_url: https://pbs.twimg.com/profile_images/2599066714/igu5hx4wlg3mxucodinl.jpeg
num_batches: 4
start_date: May 15, 2021
weekdays: Sat
timeslot: 5:00-6:00 PM
chapters:
- title: Getting Started
description: |
Getting started with programming by turning the computer into a canvas.
lessons:
- index: 1
type: video
icon: bi bi-play-circle
title: Introduction to Programming
- index: 2
type: practice
icon: bi bi-code-square
title: Drawing Shapes
- title: Repeating Things
description: |
Isn't it very boring to do the same thing again and again?
Well, that is for humans. Computers love to do the same thing again and again.
Learn how to tell the computer to repeat multiple times the same task, or
with slight change every time.
lessons:
- index: 1
type: video
icon: bi bi-play-circle
title: Rinse and Repeat
- index: 2
type: practice
title: many circles
icon: bi bi-check2-circle
- index: 3
type: practice
icon: bi bi-code-square
title: print, print, print!

View File

@@ -1,33 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="course-header">
<h1>Sketches</h1>
</div>
<div class="row sketches-gallery">
{% for s in sketches %}
<div class="col-md-3">
<div class="sketch-card">
<div class="sketch-image">
<a href="#">
<svg width="300" height="300" viewBox="0 0 300 300" fill="none" stroke="black" xmlns="http://www.w3.org/2000/svg">
<circle cx="150" cy="150" r="150.0"></circle>
</svg>
</a>
</div>
<div class="sketch-footer">
<div class="sketch-title">
<a href="#">{{s.title}}</a>
</div>
<div class="sketch-author">
by {{s.author}}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -1,38 +0,0 @@
sketches:
- id: 20
title: Big Circle
author: Anand Chitipothu
- id: 19
title: Small Circle
author: Anand Chitipothu
- id: 18
title: Circles in Queue
author: Chaitanya
- id: 17
title: Random Bottom Circles
author: Anand Chitipothu
- id: 16
title: Pipes
author: Vishal
- id: 15
title: New Sketch
author: Malleshwari
- id: 20
title: Big Circle
author: Anand Chitipothu
- id: 19
title: Small Circle
author: Anand Chitipothu
- id: 18
title: Circles in Queue
author: Chaitanya
- id: 17
title: Random Bottom Circles
author: Anand Chitipothu
- id: 16
title: Pipes
author: Vishal
- id: 15
title: New Sketch
author: Malleshwari

View File

@@ -1,221 +0,0 @@
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css");
:root {
--c1: #fefae0;
--c2: #264653;
--c3: #e9c46a;
--c4: #2a9d8f;
--c5: #f4a261;
--c6: #e76f51;
--c7: #ccd5ae;
--bg: var(--c1);
--header-bg: var(--c2);
--header-color: var(--c3);
--tag-color: var(--c7);
--sidebar-bg: var(--c7);
--h-color: var(--c2);
--text-color: #333;
--text-color-light: #ccc;
--cta-color: var(--c4);
}
body {
padding: 0px;
margin: 0px;
background: var(--bg);
}
.navbar-light {
border-bottom: 1px solid #E2E6E9;
}
.page-header {
margin-top: 20px;
padding: 20px;
border-radius: 10px;
}
.page-header .page-header{
margin-top: 20px;
padding: 20px;
border-radius: 10px;
}
.course-header {
margin-top: 20px;
padding: 20px;
background: var(--header-bg);
color: var(--header-color);
border-radius: 10px;
}
.course-header h1 {
color: inherit;
}
.course-type {
text-transform: uppercase;
font-size: 1.0em;
color: var(--tag-color);
}
.sidebar {
background: var(--sidebar-bg);
margin: 20px 0px;
border-radius: 10px;
padding: 1px 20px 20px 20px;
color: var(--text-color);
}
.sidebar h3 {
margin-top: 20px;
color: var(--c2);
}
.instructor {
padding: 10px;
}
.instructor-title {
font-weight: bold;
}
.instructor-subtitle {
font-size: 0.8em;
color: var(--text-color);
}
.sidebar .notice {
padding: 10px;
border-radius: 10px;
border: 1px dashed var(--text-color);
}
.sidebar .notice a {
color: inherit;
text-decoration: underline;
}
.course-details {
margin: 20px 0px;
}
.course-details h2 {
color: var(--h-color);
font-size: 1.4em;
font-weight: bold;
margin: 20px 0px 10px 0px;
}
.chapter-plan {
border-radius: 10px;
margin: 20px 0px;
padding: 20px;
border: 1px solid #ddc;
background: white;
}
.chapter-plan h3 {
font-size: 1.1em;
font-weight: bold;
}
.chapter-number {
background: var(--text-color);
color: white;
border-radius: 50%;
height: 24px;
min-width: 24px;
align-items: center;
padding: 2px 8px 2px 8px;
margin-right: 5px;
}
.chapter-description {
margin: 20px 0px;
}
.lessons {
padding-left: 20px;
}
.lesson {
margin: 5px 0px;
font-weight: bold;
}
.batch {
border-radius: 10px;
margin: 10px 0px;
background: white;
border: 1px solid #ddc;
}
.batch-details {
padding: 20px;
}
.batch .cta {
margin-top: 10px;
padding: 10px;
min-height: 28px;
text-align: right;
border-top: 1px solid #ddc;
}
.batch .cta button {
background: var(--cta-color);
color: white;
border: none;
border-radius: 5px;
padding: 5px 10px;
}
.batch .right {
float: right;
}
img.profile-photo {
width: 24px;
height: 24px;
border-radius: 50%;
}
.lesson-type {
padding-right: 5px;
}
.preview-video {
text-align: center;
margin: 20px 0px;
}
.preview-video iframe {
max-width: 100%
}
.sketches-gallery svg {
width: 200px;
height: 200px;
}
.sketch-card {
background: white;
border-radius: 10px;
border: 1px solid #ddd;
margin: 10px;
}
.sketch-card .sketch-image {
padding: 10px;
}
.sketch-footer {
padding: 10px;
background: #eee;
}