Compare commits
2 Commits
username-f
...
upcoming-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9b4fe468e | ||
|
|
6cbca8d1bb |
@@ -13,6 +13,38 @@ from ...utils import slugify
|
|||||||
|
|
||||||
class LMSCourse(Document):
|
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
|
@staticmethod
|
||||||
def find(name):
|
def find(name):
|
||||||
"""Returns the course with specified 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
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
{% set query_parameter = "?batch=" + membership.batch if membership and membership.batch else "" %}
|
||||||
|
|
||||||
{% if course.upcoming %}
|
{% 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" />
|
Upcoming Course <img class="ml-3" src="/assets/community/icons/black-arrow.svg" />
|
||||||
</div>
|
</div>
|
||||||
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
<a class="stretched-link" href="/courses/{{ course.name }}"></a>
|
||||||
|
|||||||
@@ -34,22 +34,14 @@ class TestCustomUser(unittest.TestCase):
|
|||||||
}).insert()
|
}).insert()
|
||||||
self.assertEqual(new_user.username[:8], "username")
|
self.assertEqual(new_user.username[:8], "username")
|
||||||
|
|
||||||
def test_with_underscore_at_end(self):
|
def test_with_hyphen_at_end(self):
|
||||||
new_user = frappe.get_doc({
|
new_user = frappe.get_doc({
|
||||||
"doctype": "User",
|
"doctype": "User",
|
||||||
"email": "test_with_underscore_at_end@example.com",
|
"email": "test_with_hyphen_at_end@example.com",
|
||||||
"first_name": "Username___"
|
"first_name": "Username---"
|
||||||
}).insert()
|
}).insert()
|
||||||
self.assertNotEqual(new_user.username[-1], "_")
|
length = len(new_user.username)
|
||||||
|
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:
|
||||||
@@ -57,7 +49,6 @@ 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_underscore_at_end@example.com",
|
"test_with_hyphen_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]})
|
||||||
|
|||||||
@@ -13,45 +13,38 @@ 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) or underscore_condition:
|
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
||||||
self.username = self.remove_illegal_characters()
|
self.username = self.remove_illegal_characters()
|
||||||
|
|
||||||
if len(self.username) < 4:
|
if not self.username:
|
||||||
self.username = self.email.replace("@", "").replace(".", "")
|
self.username = self.get_username_from_first_name()
|
||||||
|
|
||||||
while self.username_exists():
|
if 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 self.username:
|
if not re.match("^[A-Za-z0-9_-]*$", self.username):
|
||||||
frappe.throw(_("Username already exists."))
|
frappe.throw(_("Username can only contain alphabets, numbers, hyphen and underscore."))
|
||||||
|
|
||||||
if not re.match("^[A-Za-z0-9_]*$", self.username):
|
if self.username[0] == "-" or self.username[len(self.username) - 1] == "-":
|
||||||
frappe.throw(_("Username can only contain alphabets, numbers and unedrscore."))
|
frappe.throw(_("First and Last character of username cannot be Hyphen(-)."))
|
||||||
|
|
||||||
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):
|
||||||
return re.sub("[^\w]+", "", self.username).strip("_")
|
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:
|
def get_authored_courses(self) -> int:
|
||||||
"""Returns the number of courses authored by this user.
|
"""Returns the number of courses authored by this user.
|
||||||
|
|||||||
@@ -635,6 +635,11 @@ input[type=checkbox] {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 135%;
|
line-height: 135%;
|
||||||
letter-spacing: -0.011em;
|
letter-spacing: -0.011em;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wide-button {
|
.wide-button {
|
||||||
@@ -660,6 +665,11 @@ input[type=checkbox] {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-default {
|
||||||
|
background: #98A1A9;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.video-preview {
|
.video-preview {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -676,12 +686,6 @@ input[type=checkbox] {
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-default {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #C8CFD5;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.course-home-outline {
|
.course-home-outline {
|
||||||
margin-top: 3rem;
|
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" />
|
Continue Learning <img class="ml-2" src="/assets/community/icons/white-arrow.svg" />
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if course.video_link %}
|
||||||
<div class="button wide-button is-secondary video-preview">
|
<div class="button wide-button is-secondary video-preview">
|
||||||
Watch Video Preview
|
Watch Video Preview
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ frappe.ready(() => {
|
|||||||
submit_review(e);
|
submit_review(e);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$("#notify-me").click((e) => {
|
||||||
|
notify_user(e);
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var check_mentor_request = () => {
|
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)
|
membership = course.get_membership(frappe.session.user)
|
||||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||||
context.membership = membership
|
context.membership = membership
|
||||||
|
if context.course.upcoming:
|
||||||
|
context.is_user_interested = get_user_interest(context.course.name)
|
||||||
context.metatags = {
|
context.metatags = {
|
||||||
"title": course.title,
|
"title": course.title,
|
||||||
"image": course.image,
|
"image": course.image,
|
||||||
"description": course.short_introduction,
|
"description": course.short_introduction,
|
||||||
"keywords": course.title
|
"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