Compare commits

...

8 Commits

Author SHA1 Message Date
pateljannat
a3672e9d91 feat: discussions 2021-08-16 13:33:08 +05:30
pateljannat
6c9d49bf8c discussion doctypes 2021-08-11 11:14:40 +05:30
pateljannat
2de058246b Merge branch 'main' of https://github.com/frappe/community into main 2021-08-11 10:46:11 +05:30
pateljannat
798ea30382 fix: chapter teaser jerk issue 2021-08-11 10:46:01 +05:30
Jannat Patel
83a2f42df9 Merge pull request #171 from fossunited/username-fixes
fix: username issues
2021-08-10 13:19:29 +05:30
pateljannat
66aace247c fix: conditions and tests 2021-08-10 10:28:59 +05:30
pateljannat
bc3db06960 fix: username issues 2021-08-09 16:54:02 +05:30
Jannat Patel
ddaa063587 Merge pull request #170 from fossunited/upcoming-course-notify
feat: notify me
2021-08-09 15:39:04 +05:30
33 changed files with 575 additions and 376 deletions

View File

@@ -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) {
// }
});

View File

@@ -1,67 +1,53 @@
{ {
"actions": [], "actions": [],
"creation": "2021-03-19 12:19:32.355307", "creation": "2021-08-11 10:59:38.597046",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"author", "thread",
"batch", "column_break_2",
"column_break_3", "parent_message",
"author_name", "section_break_4",
"pin",
"section_break_6",
"message" "message"
], ],
"fields": [ "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", "fieldname": "message",
"fieldtype": "Markdown Editor", "fieldtype": "Long Text",
"in_list_view": 1, "in_list_view": 1,
"label": "Message" "label": "Message"
}, },
{ {
"default": "0", "fieldname": "thread",
"fieldname": "pin", "fieldtype": "Link",
"fieldtype": "Check", "in_list_view": 1,
"label": "Pin" "in_standard_filter": 1,
"label": "Thread",
"options": "Discussion Thread"
}, },
{ {
"fetch_from": "author.full_name", "fieldname": "column_break_2",
"fieldname": "author_name",
"fieldtype": "Data",
"label": "Author Name",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break" "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" "fieldtype": "Section Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-21 11:49:34.911479", "modified": "2021-08-12 15:59:04.811286",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "Community",
"name": "LMS Message", "name": "Discussion Message",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -77,9 +63,7 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "author",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -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)

View File

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

View File

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

View File

@@ -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
}

View File

@@ -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)

View File

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

View 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>

View 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>

View File

@@ -145,7 +145,8 @@ primary_rules = [
{"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"}, {"from_route": "/courses/<course>/discuss", "to_route": "batch/discuss"},
{"from_route": "/courses/<course>/about", "to_route": "batch/about"}, {"from_route": "/courses/<course>/about", "to_route": "batch/about"},
{"from_route": "/courses/<course>/progress", "to_route": "batch/progress"}, {"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 # Any frappe default URL is blocked by profile-rules, add it here to unblock it
@@ -167,7 +168,8 @@ whitelist = [
"/new-sign-up", "/new-sign-up",
"/message", "/message",
"/about", "/about",
"/edit-profile" "/edit-profile",
"/discussions"
] ]
whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist] whitelist_rules = [{"from_route": p, "to_route": p[1:]} for p in whitelist]

View File

@@ -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
)

View File

@@ -1,8 +1,9 @@
<div class="breadcrumb"> <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 %} {% 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 %} {% if lesson %}
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a> <a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
<img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg"> <img class="ml-1 mr-1" src="/assets/community/icons/chevron-right.svg">
@@ -12,8 +13,9 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if member_name %} {% if thread %}
<span class="muted-text">{{ member_name }}</span> <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 %} {% endif %}
</div> </div>

View File

@@ -1,6 +1,6 @@
<div> <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"> data-toggle="collapse" aria-expanded="false">
<img class="chapter-icon" src="/assets/community/icons/chevron-right.svg"> <img class="chapter-icon" src="/assets/community/icons/chevron-right.svg">
{{ index }}. {{ chapter.title }} {{ index }}. {{ chapter.title }}

View File

@@ -34,14 +34,22 @@ class TestCustomUser(unittest.TestCase):
}).insert() }).insert()
self.assertEqual(new_user.username[:8], "username") self.assertEqual(new_user.username[:8], "username")
def test_with_hyphen_at_end(self): def test_with_underscore_at_end(self):
new_user = frappe.get_doc({ new_user = frappe.get_doc({
"doctype": "User", "doctype": "User",
"email": "test_with_hyphen_at_end@example.com", "email": "test_with_underscore_at_end@example.com",
"first_name": "Username---" "first_name": "Username___"
}).insert() }).insert()
length = len(new_user.username) self.assertNotEqual(new_user.username[-1], "_")
self.assertNotEqual(new_user.username[length-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 @classmethod
def tearDownClass(cls) -> None: def tearDownClass(cls) -> None:
@@ -49,6 +57,7 @@ class TestCustomUser(unittest.TestCase):
"test_with_basic_username@example.com", "test_with_basic_username@example.com",
"test-without-username@example.com", "test-without-username@example.com",
"test_with_illegal_characters@example.com", "test_with_illegal_characters@example.com",
"test_with_hyphen_at_end@example.com" "test_with_underscore_at_end@example.com",
"test_with_short_first_name@example.com"
] ]
frappe.db.delete("User", {"name": ["in", users]}) frappe.db.delete("User", {"name": ["in", users]})

View File

@@ -13,38 +13,45 @@ class CustomUser(User):
self.validate_username_characters() self.validate_username_characters()
def validate_username_characters(self): 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 self.is_new():
if not self.username:
self.username = self.get_username_from_first_name()
if self.username.find(" "): if self.username.find(" "):
self.username.replace(" ", "") self.username.replace(" ", "")
if not re.match("^[A-Za-z0-9_]*$", self.username): if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
self.username = self.remove_illegal_characters() self.username = self.remove_illegal_characters()
if not self.username: if len(self.username) < 4:
self.username = self.get_username_from_first_name() self.username = self.email.replace("@", "").replace(".", "")
if self.username_exists(): while self.username_exists():
self.username = self.remove_illegal_characters() + str(random.randint(0, 99)) self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
else: else:
if not re.match("^[A-Za-z0-9_-]*$", self.username): if not self.username:
frappe.throw(_("Username can only contain alphabets, numbers, hyphen and underscore.")) frappe.throw(_("Username already exists."))
if self.username[0] == "-" or self.username[len(self.username) - 1] == "-": if not re.match("^[A-Za-z0-9_]*$", self.username):
frappe.throw(_("First and Last character of username cannot be Hyphen(-).")) 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): def get_username_from_first_name(self):
return frappe.scrub(self.first_name) + str(random.randint(0, 99)) return frappe.scrub(self.first_name) + str(random.randint(0, 99))
def remove_illegal_characters(self): def remove_illegal_characters(self):
username = ''.join([c for c in self.username if c.isalnum() or c in ['-', '_']]) return re.sub("[^\w]+", "", self.username).strip("_")
while username[0] == "-" or username[len(username) - 1] == "-":
if username[0] == "-":
username = username[1:]
if username[len(username) - 1]:
username = username[:1]
return username
def get_authored_courses(self) -> int: def get_authored_courses(self) -> int:
"""Returns the number of courses authored by this user. """Returns the number of courses authored by this user.

View File

@@ -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_lms_message
community.patches.replace_member_with_user_in_mentor_request community.patches.replace_member_with_user_in_mentor_request
community.patches.v0_0.chapter_lesson_index_table community.patches.v0_0.chapter_lesson_index_table
execute:frappe.delete_doc("DocType", "LMS Message")

View File

@@ -22,6 +22,14 @@
--control-bg: var(--gray-100); --control-bg: var(--gray-100);
--muted-text: #4C5A67; --muted-text: #4C5A67;
--button-background: #EEF0F2; --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 { body {
@@ -76,82 +84,10 @@ img.profile-photo {
padding-right: 5px; 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 { section {
padding: 5rem 0 5rem 0; 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 { .batch-header {
background: #eee; background: #eee;
border: 2px solid #ddd; border: 2px solid #ddd;
@@ -191,33 +127,6 @@ input[type=checkbox] {
appearance: auto; 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 { .progress-image {
margin-right: 3px; margin-right: 3px;
border-radius: 50px; border-radius: 50px;
@@ -276,6 +185,7 @@ input[type=checkbox] {
.common-page-style { .common-page-style {
background: #F4F5F6; background: #F4F5F6;
padding-bottom: 2rem; padding-bottom: 2rem;
min-height: 60vh;
} }
.common-card-style { .common-card-style {
@@ -696,16 +606,18 @@ input[type=checkbox] {
.chapter-title { .chapter-title {
font-weight: bold; font-weight: bold;
margin: 0 .25rem .5rem; margin: 0 .25rem 0;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
padding-bottom: 0.5rem;
} }
.chapter-description { .chapter-description {
height: fit-content; height: fit-content;
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
margin-bottom: 0.75rem;
} }
.chapter-icon { .chapter-icon {
@@ -817,7 +729,7 @@ input[type=checkbox] {
} }
.lessons { .lessons {
margin: 12px 0px 16px; margin-bottom: 1rem;
} }
.course-buttons { .course-buttons {
@@ -1350,3 +1262,36 @@ pre {
.markdown-source h4 { .markdown-source h4 {
font-size: 1rem; 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;
}

View 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

View 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

View 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>

View File

@@ -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 %}

View File

@@ -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("");
}
})
})

View File

@@ -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)

View File

@@ -27,6 +27,11 @@
{% if membership %} {% if membership %}
{{ pagination(prev_url, next_url) }} {{ pagination(prev_url, next_url) }}
{% endif %} {% 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> </div>
</div> </div>
@@ -45,7 +50,8 @@
<div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div> <div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
{% else %} {% else %}
<div class="common-card-style lesson-content-card"> <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> </div>
{% endif %} {% endif %}

View File

@@ -169,6 +169,7 @@ var show_review_dialog = (e) => {
} }
var rotate_chapter_icon = (e) => { var rotate_chapter_icon = (e) => {
e.preventDefault();
var icon = $(e.currentTarget).children(".chapter-icon"); var icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") { if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)"); icon.css("transform", "rotate(90deg)");

View 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 %}

View 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

View 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">&times;</span>
</button>
</div>
<div class="modal-body">
{{ widgets.DiscussionComment() }}
</div>
</div>
</div>
</div>
{% endblock %}

View 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");
}

View 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