Compare commits
7 Commits
minor-issu
...
upcoming-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9b4fe468e | ||
|
|
6cbca8d1bb | ||
|
|
d5067a4bcd | ||
|
|
04d44510de | ||
|
|
844fcc9bca | ||
|
|
145b5efab0 | ||
|
|
63d70fc037 |
@@ -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,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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
54
community/overrides/test_user.py
Normal file
54
community/overrides/test_user.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# 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_hyphen_at_end(self):
|
||||
new_user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "test_with_hyphen_at_end@example.com",
|
||||
"first_name": "Username---"
|
||||
}).insert()
|
||||
length = len(new_user.username)
|
||||
self.assertNotEqual(new_user.username[length-1], "-")
|
||||
|
||||
@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_hyphen_at_end@example.com"
|
||||
]
|
||||
frappe.db.delete("User", {"name": ["in", users]})
|
||||
@@ -2,9 +2,50 @@ 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 self.is_new():
|
||||
|
||||
if self.username.find(" "):
|
||||
self.username.replace(" ", "")
|
||||
|
||||
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
||||
self.username = self.remove_illegal_characters()
|
||||
|
||||
if not self.username:
|
||||
self.username = self.get_username_from_first_name()
|
||||
|
||||
if self.username_exists():
|
||||
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
|
||||
|
||||
else:
|
||||
if not re.match("^[A-Za-z0-9_-]*$", self.username):
|
||||
frappe.throw(_("Username can only contain alphabets, numbers, hyphen and underscore."))
|
||||
|
||||
if self.username[0] == "-" or self.username[len(self.username) - 1] == "-":
|
||||
frappe.throw(_("First and Last character of username cannot be Hyphen(-)."))
|
||||
|
||||
def get_username_from_first_name(self):
|
||||
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
|
||||
|
||||
def remove_illegal_characters(self):
|
||||
username = ''.join([c for c in self.username if c.isalnum() or c in ['-', '_']])
|
||||
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:
|
||||
"""Returns the number of courses authored by this user.
|
||||
"""
|
||||
|
||||
@@ -635,6 +635,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 +665,11 @@ input[type=checkbox] {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.is-default {
|
||||
background: #98A1A9;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.video-preview {
|
||||
margin-top: 16px;
|
||||
@@ -676,12 +686,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;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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 = () => {
|
||||
@@ -209,3 +213,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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user