Compare commits
15 Commits
minor-issu
...
discussion
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3672e9d91 | ||
|
|
6c9d49bf8c | ||
|
|
2de058246b | ||
|
|
798ea30382 | ||
|
|
83a2f42df9 | ||
|
|
66aace247c | ||
|
|
bc3db06960 | ||
|
|
ddaa063587 | ||
|
|
f9b4fe468e | ||
|
|
6cbca8d1bb | ||
|
|
d5067a4bcd | ||
|
|
04d44510de | ||
|
|
844fcc9bca | ||
|
|
145b5efab0 | ||
|
|
63d70fc037 |
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Discussion Message', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -1,67 +1,53 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-03-19 12:19:32.355307",
|
||||
"creation": "2021-08-11 10:59:38.597046",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"author",
|
||||
"batch",
|
||||
"column_break_3",
|
||||
"author_name",
|
||||
"pin",
|
||||
"section_break_6",
|
||||
"thread",
|
||||
"column_break_2",
|
||||
"parent_message",
|
||||
"section_break_4",
|
||||
"message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "batch",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Batch",
|
||||
"options": "LMS Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "author",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Author",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"fieldtype": "Long Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "pin",
|
||||
"fieldtype": "Check",
|
||||
"label": "Pin"
|
||||
"fieldname": "thread",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Thread",
|
||||
"options": "Discussion Thread"
|
||||
},
|
||||
{
|
||||
"fetch_from": "author.full_name",
|
||||
"fieldname": "author_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Author Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldname": "parent_message",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Parent Message",
|
||||
"options": "Discussion Message"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-21 11:49:34.911479",
|
||||
"modified": "2021-08-12 15:59:04.811286",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Message",
|
||||
"module": "Community",
|
||||
"name": "Discussion Message",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -77,9 +63,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "author",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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)
|
||||
@@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLMSMessage(unittest.TestCase):
|
||||
class TestDiscussionMessage(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('LMS Message', {
|
||||
frappe.ui.form.on('Discussion Thread', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-08-11 10:55:29.341674",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"reference_doctype",
|
||||
"reference_docname"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Doctype",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_docname",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Docname",
|
||||
"options": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-11 12:29:43.564123",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Community",
|
||||
"name": "Discussion Thread",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "title",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestDiscussionThread(unittest.TestCase):
|
||||
pass
|
||||
70
community/community/widgets/DiscussionComment.html
Normal file
70
community/community/widgets/DiscussionComment.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
80
community/community/widgets/DiscussionMessage.html
Normal file
80
community/community/widgets/DiscussionMessage.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% if doctype and docname and not thread %}
|
||||
|
||||
{% set thread_info = frappe.get_all("Discussion Thread", {"reference_doctype": doctype, "reference_docname": docname},
|
||||
["name"]) %}
|
||||
|
||||
{% if thread_info | length %}
|
||||
{% set thread = thread_info[0].name %}
|
||||
{% 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" id="login-from-discussion">Log In</div>
|
||||
{% elif not condition %}
|
||||
<div class="button is-primary" 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>
|
||||
@@ -145,7 +145,8 @@ primary_rules = [
|
||||
{"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>/join", "to_route": "batch/join"},
|
||||
{"from_route": "/discussions/<discussion>", "to_route": "discussions/discussion"}
|
||||
]
|
||||
|
||||
# Any frappe default URL is blocked by profile-rules, add it here to unblock it
|
||||
@@ -167,7 +168,8 @@ whitelist = [
|
||||
"/new-sign-up",
|
||||
"/message",
|
||||
"/about",
|
||||
"/edit-profile"
|
||||
"/edit-profile",
|
||||
"/discussions"
|
||||
]
|
||||
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]
|
||||
|
||||
|
||||
@@ -13,6 +13,38 @@ from ...utils import slugify
|
||||
|
||||
class LMSCourse(Document):
|
||||
|
||||
def on_update(self):
|
||||
if not self.upcoming and self.has_value_changed("upcoming"):
|
||||
self.send_email_to_interested_users()
|
||||
|
||||
def send_email_to_interested_users(self):
|
||||
interested_users = frappe.get_all("LMS Course Interest",
|
||||
{
|
||||
"course": self.name
|
||||
},
|
||||
["name", "user"])
|
||||
subject = self.title + " is available!"
|
||||
args = {
|
||||
"title": self.title,
|
||||
"course_link": "/courses/{0}".format(self.name),
|
||||
"app_name": frappe.db.get_single_value("System Settings", "app_name"),
|
||||
"site_url": frappe.utils.get_url()
|
||||
}
|
||||
|
||||
for user in interested_users:
|
||||
args["first_name"] = frappe.db.get_value("User", user.user, "first_name")
|
||||
email_args = frappe._dict(
|
||||
recipients = user.user,
|
||||
sender = frappe.db.get_single_value("LMS Settings", "email_sender"),
|
||||
subject = subject,
|
||||
header = [subject, "green"],
|
||||
template = "lms_course_interest",
|
||||
args = args,
|
||||
now = True
|
||||
)
|
||||
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
|
||||
frappe.db.set_value("LMS Course Interest", user.name, "email_sent", True)
|
||||
|
||||
@staticmethod
|
||||
def find(name):
|
||||
"""Returns the course with specified name.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('LMS Course Interest', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-08-06 17:37:20.184849",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"user",
|
||||
"email_sent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "User",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "email_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Email Sent",
|
||||
"options": "email_sent"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-06 18:06:21.370741",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Course Interest",
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LMSCourseInterest(Document):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def capture_interest(course):
|
||||
frappe.get_doc({
|
||||
"doctype": "LMS Course Interest",
|
||||
"course": course,
|
||||
"user": frappe.session.user
|
||||
}).save(ignore_permissions=True)
|
||||
return "OK"
|
||||
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLMSCourseInterest(unittest.TestCase):
|
||||
pass
|
||||
@@ -1,110 +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 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 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
|
||||
)
|
||||
@@ -1,7 +1,14 @@
|
||||
frappe.ready(function () {
|
||||
|
||||
frappe.web_form.after_load = () => {
|
||||
if (!frappe.utils.get_url_arg("name")) {
|
||||
window.location.href = `/edit-profile?name=${frappe.session.user}`;
|
||||
}
|
||||
}
|
||||
|
||||
frappe.web_form.after_save = () => {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/${frappe.web_form.get_value(["username"])}`;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "",
|
||||
"button_label": "Save",
|
||||
"client_script": "",
|
||||
"creation": "2021-06-30 13:48:13.682851",
|
||||
"custom_css": "[data-doctype=\"Web Form\"] {\n max-width: 720px;\n margin: 6rem auto;\n}",
|
||||
"doc_type": "User",
|
||||
@@ -20,7 +21,7 @@
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2021-07-14 17:15:15.424855",
|
||||
"modified": "2021-08-06 14:40:39.013776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "profile",
|
||||
@@ -72,6 +73,18 @@
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "username",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Username",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "Get your globally recognized avatar from Gravatar.com",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">All Courses</a>
|
||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||
|
||||
|
||||
{% if course %}
|
||||
<a class="dark-links" href="/courses">All Courses</a>
|
||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||
{% if lesson %}
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||
@@ -12,8 +13,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if member_name %}
|
||||
<span class="muted-text">{{ member_name }}</span>
|
||||
{% if thread %}
|
||||
<a class="dark-links" href="/discussions">Discussions</a>
|
||||
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
|
||||
<span class="muted-text">{{ thread.title }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div>
|
||||
|
||||
<div class="small-title chapter-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
|
||||
<div class="chapter-title small-title" data-target="#{{ course.get_slugified_chapter_title(chapter.title) }}"
|
||||
data-toggle="collapse" aria-expanded="false">
|
||||
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
|
||||
{{ index }}. {{ chapter.title }}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
||||
|
||||
{% if course.upcoming %}
|
||||
<div class="view-course-link is-default">
|
||||
<div class="view-course-link is-secondary border">
|
||||
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
||||
</div>
|
||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||
|
||||
63
community/overrides/test_user.py
Normal file
63
community/overrides/test_user.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
class TestCustomUser(unittest.TestCase):
|
||||
|
||||
def test_with_basic_username(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_basic_username@example.com",
|
||||
"first_name": "Username"
|
||||
}).insert()
|
||||
self.assertEqual(new_user.username, "username")
|
||||
|
||||
def test_without_username(self):
|
||||
""" The user in this test has the same first name as the user of the test test_with_basic_username.
|
||||
In such cases frappe makes the username of the second user empty.
|
||||
The condition in community app should override this and save a username. """
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test-without-username@example.com",
|
||||
"first_name": "Username"
|
||||
}).insert()
|
||||
self.assertTrue(new_user.username)
|
||||
|
||||
def test_with_illegal_characters(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_illegal_characters@example.com",
|
||||
"first_name": "Username$$"
|
||||
}).insert()
|
||||
self.assertEqual(new_user.username[:8], "username")
|
||||
|
||||
def test_with_underscore_at_end(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_underscore_at_end@example.com",
|
||||
"first_name": "Username___"
|
||||
}).insert()
|
||||
self.assertNotEqual(new_user.username[-1], "_")
|
||||
|
||||
|
||||
def test_with_short_first_name(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_short_first_name@example.com",
|
||||
"first_name": "USN"
|
||||
}).insert()
|
||||
self.assertGreaterEqual(len(new_user.username), 4)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
users = [
|
||||
"test_with_basic_username@example.com",
|
||||
"test-without-username@example.com",
|
||||
"test_with_illegal_characters@example.com",
|
||||
"test_with_underscore_at_end@example.com",
|
||||
"test_with_short_first_name@example.com"
|
||||
]
|
||||
frappe.db.delete("User", {"name": ["in", users]})
|
||||
@@ -2,9 +2,57 @@ import frappe
|
||||
from frappe.core.doctype.user.user import User
|
||||
from frappe.utils import cint
|
||||
import hashlib
|
||||
import random
|
||||
import re
|
||||
from frappe import _
|
||||
|
||||
class CustomUser(User):
|
||||
|
||||
def validate(self):
|
||||
super(CustomUser, self).validate()
|
||||
self.validate_username_characters()
|
||||
|
||||
def validate_username_characters(self):
|
||||
if len(self.username):
|
||||
underscore_condition = self.username[0] == "_" or self.username[-1] == "_"
|
||||
else:
|
||||
underscore_condition = ''
|
||||
|
||||
if self.is_new():
|
||||
if not self.username:
|
||||
self.username = self.get_username_from_first_name()
|
||||
|
||||
if self.username.find(" "):
|
||||
self.username.replace(" ", "")
|
||||
|
||||
if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
|
||||
self.username = self.remove_illegal_characters()
|
||||
|
||||
if len(self.username) < 4:
|
||||
self.username = self.email.replace("@", "").replace(".", "")
|
||||
|
||||
while self.username_exists():
|
||||
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
|
||||
|
||||
else:
|
||||
if not self.username:
|
||||
frappe.throw(_("Username already exists."))
|
||||
|
||||
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
||||
frappe.throw(_("Username can only contain alphabets, numbers and unedrscore."))
|
||||
|
||||
if underscore_condition:
|
||||
frappe.throw(_("First and Last character of username cannot be Underscore(_)."))
|
||||
|
||||
if len(self.username) < 4:
|
||||
frappe.throw(_("Username cannot be less than 4 characters"))
|
||||
|
||||
def get_username_from_first_name(self):
|
||||
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
|
||||
|
||||
def remove_illegal_characters(self):
|
||||
return re.sub("[^\w]+", "", self.username).strip("_")
|
||||
|
||||
def get_authored_courses(self) -> int:
|
||||
"""Returns the number of courses authored by this user.
|
||||
"""
|
||||
|
||||
@@ -7,3 +7,4 @@ community.patches.replace_member_with_user_in_course_mentor_mapping
|
||||
community.patches.replace_member_with_user_in_lms_message
|
||||
community.patches.replace_member_with_user_in_mentor_request
|
||||
community.patches.v0_0.chapter_lesson_index_table
|
||||
execute:frappe.delete_doc("DocType", "LMS Message")
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
--control-bg: var(--gray-100);
|
||||
--muted-text: #4C5A67;
|
||||
--button-background: #EEF0F2;
|
||||
--text-xs: 11px;
|
||||
--text-sm: 12px;
|
||||
--text-md: 13px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 16px;
|
||||
--text-xl: 18px;
|
||||
--text-2xl: 20px;
|
||||
--text-3xl: 22px;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -76,82 +84,10 @@ img.profile-photo {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
border: 1px dashed var(--text-color);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.msger-inputarea {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
border-top: 2px solid #ddd;
|
||||
background: #eee;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.msger-inputarea * {
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.msger-input {
|
||||
flex: 1;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.message-section {
|
||||
margin-left: 3%;
|
||||
display: inline-block;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
color: #2D005A;
|
||||
font-weight: 600;
|
||||
line-height: 51px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0 5rem 0;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
margin: 0 auto;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.messages {
|
||||
height: 450px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.messages li {
|
||||
background: #F7F5F5;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin: 2px 8px 2px 0;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.messages li.ours {
|
||||
align-self: flex-end;
|
||||
margin: 2px 0 2px 8px;
|
||||
background: var(--primary-color);
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.message-para {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.batch-header {
|
||||
background: #eee;
|
||||
border: 2px solid #ddd;
|
||||
@@ -191,33 +127,6 @@ input[type=checkbox] {
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
.partiallycomplete {
|
||||
background: #FEF4E2;
|
||||
color: #976417;
|
||||
}
|
||||
|
||||
.partiallycomplete img {
|
||||
background: #976417;
|
||||
}
|
||||
|
||||
.complete {
|
||||
background: #EAF5EE;
|
||||
color: #38A160;
|
||||
}
|
||||
|
||||
.complete img {
|
||||
background: #38A160;
|
||||
}
|
||||
|
||||
.incomplete {
|
||||
background: #FEECEC;
|
||||
color: #E24C4C;
|
||||
}
|
||||
|
||||
.incomplete img {
|
||||
background: #E24C4C;
|
||||
}
|
||||
|
||||
.progress-image {
|
||||
margin-right: 3px;
|
||||
border-radius: 50px;
|
||||
@@ -276,6 +185,7 @@ input[type=checkbox] {
|
||||
.common-page-style {
|
||||
background: #F4F5F6;
|
||||
padding-bottom: 2rem;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.common-card-style {
|
||||
@@ -635,6 +545,11 @@ input[type=checkbox] {
|
||||
font-size: 12px;
|
||||
line-height: 135%;
|
||||
letter-spacing: -0.011em;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wide-button {
|
||||
@@ -660,6 +575,11 @@ input[type=checkbox] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.is-default {
|
||||
background: #98A1A9;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.video-preview {
|
||||
margin-top: 16px;
|
||||
@@ -676,12 +596,6 @@ input[type=checkbox] {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.is-default {
|
||||
background-color: white;
|
||||
border: 1px solid #C8CFD5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.course-home-outline {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
@@ -692,16 +606,18 @@ input[type=checkbox] {
|
||||
|
||||
.chapter-title {
|
||||
font-weight: bold;
|
||||
margin: 0 .25rem .5rem;
|
||||
margin: 0 .25rem 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chapter-description {
|
||||
height: fit-content;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.chapter-icon {
|
||||
@@ -813,7 +729,7 @@ input[type=checkbox] {
|
||||
}
|
||||
|
||||
.lessons {
|
||||
margin: 12px 0px 16px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.course-buttons {
|
||||
@@ -1346,3 +1262,36 @@ pre {
|
||||
.markdown-source h4 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.thread-card {
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.discussion-on-page textarea.form-control {
|
||||
background-color: #FFFFFF;
|
||||
color: inherit;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.comment-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.comment-field {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.message-author {
|
||||
color: #192734;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
5
community/public/icons/message.svg
Normal file
5
community/public/icons/message.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 763 B |
4
community/public/icons/small-add.svg
Normal file
4
community/public/icons/small-add.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4V12" stroke="white" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 8H12" stroke="white" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
19
community/templates/emails/lms_course_interest.html
Normal file
19
community/templates/emails/lms_course_interest.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div>
|
||||
{% set site_link = "<a href='" + site_url + "'>" + app_name + "</a>" %}
|
||||
<p>{{ _("Hi {0},").format(first_name) }}</p>
|
||||
<br>
|
||||
<p>{{ _("The course {0} is now available on {1}.").format(frappe.bold(title), app_name) }}</p>
|
||||
<br>
|
||||
<p>Click on the link below to start learning.</p>
|
||||
<p style="margin: 15px 0px;">
|
||||
<a href="{{ course_link }}" rel="nofollow" class="btn btn-primary">{{ _("Start Learning") }}</a>
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("You can also copy-paste following link in your browser") }}<br>
|
||||
<a href="{{ course_link }}">{{ site_url }}{{ course_link }}</a>
|
||||
</p>
|
||||
<br>
|
||||
<p>Thanks and Regards,</p>
|
||||
<p>{{ app_name }}</p>
|
||||
</div>
|
||||
12
community/templates/message_card.html
Normal file
12
community/templates/message_card.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<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="/{{ 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>
|
||||
@@ -1,47 +0,0 @@
|
||||
{% extends "templates/base.html" %}
|
||||
|
||||
{% block title %}Discuss{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Courses" />
|
||||
<meta name="keywords" content="" />
|
||||
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="messages-container mt-5">
|
||||
{{ widgets.BatchHeader(batch_name=batch.title, member_count=member_count)}}
|
||||
<ol class="messages">
|
||||
{{ Messages(messages) }}
|
||||
</ol>
|
||||
{{ TextArea() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro Messages(messages) %}
|
||||
{% for message in messages %}
|
||||
<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="">
|
||||
{{ frappe.utils.format_datetime(message.creation, "dd-mm-yyyy HH:mm") }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="message-para">
|
||||
{{ message.message }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TextArea() %}
|
||||
<form class="msger-inputarea mb-1">
|
||||
<input type="text" class="msger-input" placeholder="Write your message...">
|
||||
<button type="submit" class="btn btn-primary msger-send-btn" data-batch="{{batch.name | urlencode }}">Send</button>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
@@ -1,35 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
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" //use socketio port shown when bench starts
|
||||
}
|
||||
frappe.socketio.init(9000);
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
var message_element = document.getElementsByClassName("messages")[0]
|
||||
message_element.scrollTo(0, message_element.scrollHeight);
|
||||
document.getElementsByClassName("messages-container")[0].scrollIntoView({block: "center"})
|
||||
}, 300);
|
||||
|
||||
$(".msger-send-btn").click((e) => {
|
||||
e.preventDefault();
|
||||
var message = $(".msger-input").val().trim();
|
||||
if (message) {
|
||||
frappe.call({
|
||||
"method": "community.lms.doctype.lms_batch.lms_batch.save_message",
|
||||
"args": {
|
||||
"batch": decodeURIComponent($(e.target).attr("data-batch")),
|
||||
"message": message
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
$(".msger-input").val("");
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
utils.get_common_context(context)
|
||||
context.messages = context.batch.get_messages()
|
||||
if not context.membership:
|
||||
utils.redirect_to_lesson(context.course)
|
||||
@@ -27,6 +27,11 @@
|
||||
{% if membership %}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% endif %}
|
||||
|
||||
{% set title = lesson.title + " - " + course.title %}
|
||||
{{ widgets.DiscussionMessage(doctype="Lesson", docname=lesson.name,
|
||||
title=title, condition=membership, button_name="Start Learning",
|
||||
redirect_to="/courses/" + course.name) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +50,8 @@
|
||||
<div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
|
||||
{% else %}
|
||||
<div class="common-card-style lesson-content-card">
|
||||
<span>This lesson is not available for Preview. Please join the course to access this lesson. <a href="/courses/{{ course.name }}">Checkout Course Details.</a></span>
|
||||
<span>This lesson is not available for Preview. Please join the course to access this lesson. <a
|
||||
href="/courses/{{ course.name }}">Checkout Course Details.</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@
|
||||
Continue Learning <img class="ml-2" src="/assets/community/icons/white-arrow.svg" />
|
||||
</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 %} >
|
||||
Notify me when available
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if course.video_link %}
|
||||
<div class="button wide-button is-secondary video-preview">
|
||||
Watch Video Preview
|
||||
|
||||
@@ -41,6 +41,10 @@ frappe.ready(() => {
|
||||
submit_review(e);
|
||||
})
|
||||
|
||||
$("#notify-me").click((e) => {
|
||||
notify_user(e);
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
var check_mentor_request = () => {
|
||||
@@ -165,6 +169,7 @@ var show_review_dialog = (e) => {
|
||||
}
|
||||
|
||||
var rotate_chapter_icon = (e) => {
|
||||
e.preventDefault();
|
||||
var icon = $(e.currentTarget).children(".chapter-icon");
|
||||
if (icon.css("transform") == "none") {
|
||||
icon.css("transform", "rotate(90deg)");
|
||||
@@ -209,3 +214,23 @@ var submit_review = (e) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var notify_user = (e) => {
|
||||
e.preventDefault();
|
||||
var course = decodeURIComponent($(e.currentTarget).attr("data-course"));
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "community.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
frappe.msgprint(__("Your interest has been noted. We'll notify you via email when this course becomes available."));
|
||||
$("#notify-me").attr("disabled", true).attr("title", "Your interest has already been noted");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,9 +18,18 @@ def get_context(context):
|
||||
membership = course.get_membership(frappe.session.user)
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.membership = membership
|
||||
if context.course.upcoming:
|
||||
context.is_user_interested = get_user_interest(context.course.name)
|
||||
context.metatags = {
|
||||
"title": course.title,
|
||||
"image": course.image,
|
||||
"description": course.short_introduction,
|
||||
"keywords": course.title
|
||||
}
|
||||
|
||||
def get_user_interest(course):
|
||||
return frappe.db.count("LMS Course Interest",
|
||||
{
|
||||
"course": course,
|
||||
"user": frappe.session.user
|
||||
})
|
||||
|
||||
16
community/www/discussions/discussion.html
Normal file
16
community/www/discussions/discussion.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% 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 %}
|
||||
18
community/www/discussions/discussion.py
Normal file
18
community/www/discussions/discussion.py
Normal file
@@ -0,0 +1,18 @@
|
||||
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
|
||||
65
community/www/discussions/index.html
Normal file
65
community/www/discussions/index.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% 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">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ widgets.DiscussionComment() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
14
community/www/discussions/index.js
Normal file
14
community/www/discussions/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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");
|
||||
}
|
||||
17
community/www/discussions/index.py
Normal file
17
community/www/discussions/index.py
Normal file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
Reference in New Issue
Block a user