diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index e5d9171b..da26c807 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -58,7 +58,7 @@ jobs:
echo "127.0.0.1 lms.test" | sudo tee -a /etc/hosts
- name: Cache pip
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index ed5c7968..9ed07f0b 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -37,6 +37,7 @@ Cypress.Commands.add("login", (email, password) => {
url: "/api/method/login",
method: "POST",
body: { usr: email, pwd: password },
+ timeout: 60000,
});
});
diff --git a/frontend/src/pages/ProfileRoles.vue b/frontend/src/pages/ProfileRoles.vue
index 20d1b345..0f3799a9 100644
--- a/frontend/src/pages/ProfileRoles.vue
+++ b/frontend/src/pages/ProfileRoles.vue
@@ -72,7 +72,7 @@ const roles = createResource({
})
const updateRole = createResource({
- url: 'lms.overrides.user.save_role',
+ url: 'lms.lms.api.save_role',
makeParams(values) {
return {
user: props.profile.data?.name,
diff --git a/lms/hooks.py b/lms/hooks.py
index 3bb5929e..3fde3094 100644
--- a/lms/hooks.py
+++ b/lms/hooks.py
@@ -88,7 +88,6 @@ setup_wizard_requires = "assets/lms/js/setup_wizard.js"
# Override standard doctype classes
override_doctype_class = {
- "User": "lms.overrides.user.CustomUser",
"Web Template": "lms.overrides.web_template.CustomWebTemplate",
}
@@ -104,6 +103,10 @@ doc_events = {
},
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
+ "User": {
+ "validate": "lms.lms.user.validate_username_duplicates",
+ "after_insert": "lms.lms.user.after_insert",
+ },
}
# Scheduled Tasks
@@ -191,8 +194,8 @@ jinja = {
"lms.lms.utils.get_lesson_index",
"lms.lms.utils.get_lesson_url",
"lms.page_renderers.get_profile_url",
- "lms.overrides.user.get_palette",
"lms.lms.utils.is_instructor",
+ "lms.lms.utils.get_palette",
],
"filters": [],
}
@@ -239,8 +242,6 @@ profile_url_prefix = "/users/"
signup_form_template = "lms.plugins.show_custom_signup"
-on_session_creation = "lms.overrides.user.on_session_creation"
-
add_to_apps_screen = [
{
"name": "lms",
diff --git a/lms/lms/api.py b/lms/lms/api.py
index c83b52c2..f1cf7fef 100644
--- a/lms/lms/api.py
+++ b/lms/lms/api.py
@@ -12,7 +12,6 @@ from frappe.translate import get_all_translations
from frappe import _
from frappe.utils import (
get_datetime,
- getdate,
cint,
flt,
now,
@@ -1303,3 +1302,22 @@ def get_certification_details(course):
paid_certificate = frappe.db.get_value("LMS Course", course, "paid_certificate")
return {"membership": membership, "paid_certificate": paid_certificate}
+
+
+@frappe.whitelist()
+def save_role(user, role, value):
+ frappe.only_for("Moderator")
+ if cint(value):
+ doc = frappe.get_doc(
+ {
+ "doctype": "Has Role",
+ "parent": user,
+ "role": role,
+ "parenttype": "User",
+ "parentfield": "roles",
+ }
+ )
+ doc.save(ignore_permissions=True)
+ else:
+ frappe.db.delete("Has Role", {"parent": user, "role": role})
+ return True
diff --git a/lms/lms/user.py b/lms/lms/user.py
new file mode 100644
index 00000000..ade66c82
--- /dev/null
+++ b/lms/lms/user.py
@@ -0,0 +1,85 @@
+import frappe
+from frappe import _
+from frappe.model.naming import append_number_if_name_exists
+from frappe.website.utils import cleanup_page_name
+from frappe.website.utils import is_signup_disabled
+from frappe.utils import random_string, escape_html
+from lms.lms.utils import get_country_code
+
+
+def validate_username_duplicates(doc, method):
+ while not doc.username or doc.username_exists():
+ doc.username = append_number_if_name_exists(
+ doc.doctype, cleanup_page_name(doc.full_name), fieldname="username"
+ )
+ if " " in doc.username:
+ doc.username = doc.username.replace(" ", "")
+
+ if len(doc.username) < 4:
+ doc.username = doc.email.replace("@", "").replace(".", "")
+
+
+def after_insert(doc, method):
+ doc.add_roles("LMS Student")
+
+
+@frappe.whitelist(allow_guest=True)
+def sign_up(email, full_name, verify_terms, user_category):
+ if is_signup_disabled():
+ frappe.throw(_("Sign Up is disabled"), _("Not Allowed"))
+
+ user = frappe.db.get("User", {"email": email})
+ if user:
+ if user.enabled:
+ return 0, _("Already Registered")
+ else:
+ return 0, _("Registered but disabled")
+ else:
+ if frappe.db.get_creation_count("User", 60) > 300:
+ frappe.respond_as_web_page(
+ _("Temporarily Disabled"),
+ _(
+ "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
+ ),
+ http_status_code=429,
+ )
+
+ user = frappe.get_doc(
+ {
+ "doctype": "User",
+ "email": email,
+ "first_name": escape_html(full_name),
+ "verify_terms": verify_terms,
+ "user_category": user_category,
+ "country": "",
+ "enabled": 1,
+ "new_password": random_string(10),
+ "user_type": "Website User",
+ }
+ )
+ user.flags.ignore_permissions = True
+ user.flags.ignore_password_policy = True
+ user.insert()
+
+ # set default signup role as per Portal Settings
+ default_role = frappe.db.get_single_value("Portal Settings", "default_role")
+ if default_role:
+ user.add_roles(default_role)
+
+ user.add_roles("LMS Student")
+ set_country_from_ip(None, user.name)
+
+ if user.flags.email_sent:
+ return 1, _("Please check your email for verification")
+ else:
+ return 2, _("Please ask your administrator to verify your sign-up")
+
+
+def set_country_from_ip(login_manager=None, user=None):
+ if not user and login_manager:
+ user = login_manager.user
+ user_country = frappe.db.get_value("User", user, "country")
+ # if user_country:
+ # return
+ frappe.db.set_value("User", user, "country", get_country_code())
+ return
diff --git a/lms/lms/utils.py b/lms/lms/utils.py
index b00c2f6b..a031ee72 100644
--- a/lms/lms/utils.py
+++ b/lms/lms/utils.py
@@ -1,6 +1,7 @@
import re
import string
import frappe
+import hashlib
import json
import razorpay
import requests
@@ -2009,3 +2010,25 @@ def get_batch_card_details(batches):
batch.price = fmt_money(batch.amount, 0, batch.currency)
return batches
+
+
+def get_palette(full_name):
+ """
+ Returns a color unique to each member for Avatar"""
+
+ palette = [
+ ["--orange-avatar-bg", "--orange-avatar-color"],
+ ["--pink-avatar-bg", "--pink-avatar-color"],
+ ["--blue-avatar-bg", "--blue-avatar-color"],
+ ["--green-avatar-bg", "--green-avatar-color"],
+ ["--dark-green-avatar-bg", "--dark-green-avatar-color"],
+ ["--red-avatar-bg", "--red-avatar-color"],
+ ["--yellow-avatar-bg", "--yellow-avatar-color"],
+ ["--purple-avatar-bg", "--purple-avatar-color"],
+ ["--gray-avatar-bg", "--gray-avatar-color0"],
+ ]
+
+ encoded_name = str(full_name).encode("utf-8")
+ hash_name = hashlib.md5(encoded_name).hexdigest()
+ idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
+ return palette[idx % 8]
diff --git a/lms/lms/web_template/courses_mentored/courses_mentored.html b/lms/lms/web_template/courses_mentored/courses_mentored.html
deleted file mode 100644
index c40697a8..00000000
--- a/lms/lms/web_template/courses_mentored/courses_mentored.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% set member = frappe.get_doc("User", frappe.session.user) %}
-
- {% if member.get_mentored_courses() | length %}
-
{{ _("Courses Mentored") }}
-
- {% for course in member.get_mentored_courses() %}
- {{ widgets.CourseCard(course=course) }}
- {% endfor %}
-
- {% endif %}
-
diff --git a/lms/lms/web_template/courses_mentored/courses_mentored.json b/lms/lms/web_template/courses_mentored/courses_mentored.json
deleted file mode 100644
index 9903e1fc..00000000
--- a/lms/lms/web_template/courses_mentored/courses_mentored.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "__unsaved": 1,
- "creation": "2021-10-21 11:32:57.411626",
- "docstatus": 0,
- "doctype": "Web Template",
- "fields": [],
- "idx": 0,
- "modified": "2021-10-21 12:01:56.270656",
- "modified_by": "Administrator",
- "module": "LMS",
- "name": "Courses Mentored",
- "owner": "Administrator",
- "standard": 1,
- "template": "",
- "type": "Section"
-}
\ No newline at end of file
diff --git a/lms/lms/widgets/MemberCard.html b/lms/lms/widgets/MemberCard.html
deleted file mode 100644
index 5d8fdd36..00000000
--- a/lms/lms/widgets/MemberCard.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% set color = get_palette(member.full_name) %}
-
-
- {{ widgets.Avatar(member=member, avatar_class=avatar_class) }}
-
-
-
- {{ member.full_name }}
-
-
- {% if member.headline %}
-
{{ member.headline }}
- {% endif %}
-
- {% if member.looking_for_job %}
-
{{ _("Open Network") }}
- {% endif %}
-
- {% set course_count = get_authored_courses(member.name, True) | length %}
- {% set suffix = "Courses" if course_count > 1 else "Course" %}
-
- {% if show_course_count and course_count > 0 %}
-
- Created {{ course_count }} {{ suffix }}
-
- {% endif %}
-
-
-
-
-
diff --git a/lms/overrides/test_user.py b/lms/overrides/test_user.py
deleted file mode 100644
index 2035f89d..00000000
--- a/lms/overrides/test_user.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Copyright (c) 2021, FOSS United and Contributors
-# See license.txt
-
-import unittest
-
-import frappe
-
-from lms.lms.doctype.lms_course.test_lms_course import new_user
-
-
-class TestCustomUser(unittest.TestCase):
- def test_with_basic_username(self):
- user = new_user("Username", "test_with_basic_username@example.com")
- self.assertEqual(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 lms app should override this and save a username."""
- user = new_user("Username", "test-without-username@example.com")
- self.assertTrue(user.username)
-
- def test_with_short_first_name(self):
- user = new_user("USN", "test_with_short_first_name@example.com")
- self.assertGreaterEqual(len(user.username), 4)
-
- @classmethod
- def tearDownClass(cls) -> None:
- users = [
- "test_with_basic_username@example.com",
- "test-without-username@example.com",
- "test_with_short_first_name@example.com",
- ]
- frappe.db.delete("User", {"name": ["in", users]})
diff --git a/lms/overrides/user.py b/lms/overrides/user.py
deleted file mode 100644
index 6117c364..00000000
--- a/lms/overrides/user.py
+++ /dev/null
@@ -1,363 +0,0 @@
-import hashlib
-import frappe
-import requests
-from frappe import _
-from frappe.core.doctype.user.user import User
-from frappe.utils import cint, escape_html, random_string
-from frappe.website.utils import is_signup_disabled
-from lms.lms.utils import get_average_rating, get_country_code
-from frappe.website.utils import cleanup_page_name
-from frappe.model.naming import append_number_if_name_exists
-from lms.widgets import Widgets
-
-
-class CustomUser(User):
- def validate(self):
- super().validate()
- self.validate_username_duplicates()
-
- def after_insert(self):
- super().after_insert()
- self.add_roles("LMS Student")
-
- def validate_username_duplicates(self):
- while not self.username or self.username_exists():
- self.username = append_number_if_name_exists(
- self.doctype, cleanup_page_name(self.full_name), fieldname="username"
- )
- if " " in self.username:
- self.username = self.username.replace(" ", "")
-
- if len(self.username) < 4:
- self.username = self.email.replace("@", "").replace(".", "")
-
- def validate_skills(self):
- unique_skills = []
- for skill in self.skill:
- if not skill.skill_name:
- return
- if not skill.skill_name in unique_skills:
- unique_skills.append(skill.skill_name)
- else:
- frappe.throw(_("Skills must be unique"))
-
- def get_batch_count(self) -> int:
- """Returns the number of batches authored by this user."""
- return frappe.db.count(
- "LMS Enrollment", {"member": self.name, "member_type": "Mentor"}
- )
-
- def get_user_reviews(self):
- """Returns the reviews created by user"""
- return frappe.get_all("LMS Course Review", {"owner": self.name})
-
- def get_mentored_courses(self):
- """Returns all courses mentored by this user"""
- mentored_courses = []
- mapping = frappe.get_all(
- "LMS Course Mentor Mapping",
- {
- "mentor": self.name,
- },
- ["name", "course"],
- )
-
- for map in mapping:
- if frappe.db.get_value("LMS Course", map.course, "published"):
- course = frappe.db.get_value(
- "LMS Course",
- map.course,
- ["name", "upcoming", "title", "image", "enable_certification"],
- as_dict=True,
- )
- mentored_courses.append(course)
-
- return mentored_courses
-
-
-def get_enrolled_courses():
- in_progress = []
- completed = []
- memberships = get_course_membership(None, member_type="Student")
-
- for membership in memberships:
- course = frappe.db.get_value(
- "LMS Course",
- membership.course,
- [
- "name",
- "upcoming",
- "title",
- "short_introduction",
- "image",
- "enable_certification",
- "paid_course",
- "course_price",
- "currency",
- "published",
- "creation",
- ],
- as_dict=True,
- )
- if not course.published:
- continue
- course.enrollment_count = frappe.db.count(
- "LMS Enrollment", {"course": course.name, "member_type": "Student"}
- )
- course.avg_rating = get_average_rating(course.name) or 0
- progress = cint(membership.progress)
- if progress < 100:
- in_progress.append(course)
- else:
- completed.append(course)
-
- in_progress.sort(key=lambda x: x.enrollment_count, reverse=True)
- completed.sort(key=lambda x: x.enrollment_count, reverse=True)
-
- return {"in_progress": in_progress, "completed": completed}
-
-
-def get_course_membership(member=None, member_type=None):
- """Returns all memberships of the user."""
-
- filters = {"member": member or frappe.session.user}
- if member_type:
- filters["member_type"] = member_type
-
- return frappe.get_all("LMS Enrollment", filters, ["name", "course", "progress"])
-
-
-def get_authored_courses(member=None, only_published=True):
- """Returns the number of courses authored by this user."""
- course_details = []
- courses = frappe.get_all(
- "Course Instructor", {"instructor": member or frappe.session.user}, ["parent"]
- )
-
- for course in courses:
- detail = frappe.db.get_value(
- "LMS Course",
- course.parent,
- [
- "name",
- "upcoming",
- "title",
- "short_introduction",
- "image",
- "paid_course",
- "course_price",
- "currency",
- "status",
- "published",
- "creation",
- ],
- as_dict=True,
- )
-
- if only_published and detail and not detail.published:
- continue
- detail.enrollment_count = frappe.db.count(
- "LMS Enrollment", {"course": detail.name, "member_type": "Student"}
- )
- detail.avg_rating = get_average_rating(detail.name) or 0
- course_details.append(detail)
-
- course_details.sort(key=lambda x: x.enrollment_count, reverse=True)
- return course_details
-
-
-def get_palette(full_name):
- """
- Returns a color unique to each member for Avatar"""
-
- palette = [
- ["--orange-avatar-bg", "--orange-avatar-color"],
- ["--pink-avatar-bg", "--pink-avatar-color"],
- ["--blue-avatar-bg", "--blue-avatar-color"],
- ["--green-avatar-bg", "--green-avatar-color"],
- ["--dark-green-avatar-bg", "--dark-green-avatar-color"],
- ["--red-avatar-bg", "--red-avatar-color"],
- ["--yellow-avatar-bg", "--yellow-avatar-color"],
- ["--purple-avatar-bg", "--purple-avatar-color"],
- ["--gray-avatar-bg", "--gray-avatar-color0"],
- ]
-
- encoded_name = str(full_name).encode("utf-8")
- hash_name = hashlib.md5(encoded_name).hexdigest()
- idx = cint((int(hash_name[4:6], 16) + 1) / 5.33)
- return palette[idx % 8]
-
-
-@frappe.whitelist(allow_guest=True)
-def sign_up(email, full_name, verify_terms, user_category):
- if is_signup_disabled():
- frappe.throw(_("Sign Up is disabled"), _("Not Allowed"))
-
- user = frappe.db.get("User", {"email": email})
- if user:
- if user.enabled:
- return 0, _("Already Registered")
- else:
- return 0, _("Registered but disabled")
- else:
- if frappe.db.get_creation_count("User", 60) > 300:
- frappe.respond_as_web_page(
- _("Temporarily Disabled"),
- _(
- "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
- ),
- http_status_code=429,
- )
-
- user = frappe.get_doc(
- {
- "doctype": "User",
- "email": email,
- "first_name": escape_html(full_name),
- "verify_terms": verify_terms,
- "user_category": user_category,
- "country": "",
- "enabled": 1,
- "new_password": random_string(10),
- "user_type": "Website User",
- }
- )
- user.flags.ignore_permissions = True
- user.flags.ignore_password_policy = True
- user.insert()
-
- # set default signup role as per Portal Settings
- default_role = frappe.db.get_value("Portal Settings", None, "default_role")
- if default_role:
- user.add_roles(default_role)
-
- user.add_roles("LMS Student")
- set_country_from_ip(None, user.name)
-
- if user.flags.email_sent:
- return 1, _("Please check your email for verification")
- else:
- return 2, _("Please ask your administrator to verify your sign-up")
-
-
-def set_country_from_ip(login_manager=None, user=None):
- if not user and login_manager:
- user = login_manager.user
- user_country = frappe.db.get_value("User", user, "country")
- # if user_country:
- # return
- frappe.db.set_value("User", user, "country", get_country_code())
- return
-
-
-def on_session_creation(login_manager):
- if frappe.db.get_single_value(
- "System Settings", "setup_complete"
- ) and frappe.db.get_single_value("LMS Settings", "default_home"):
- frappe.local.response["home_page"] = "/lms"
-
-
-@frappe.whitelist()
-def search_users(start: int = 0, text: str = ""):
- start = cint(start)
- search_text = frappe.db.escape(f"%{text}%")
-
- or_filters = get_or_filters(search_text)
- count = len(get_users(or_filters, 0, 900000000))
- users = get_users(or_filters, start, 24)
- user_details = get_user_details(users)
-
- return {"user_details": user_details, "start": start + 24, "count": count}
-
-
-def get_or_filters(text):
- user_fields = [
- "first_name",
- "last_name",
- "full_name",
- "email",
- "preferred_location",
- "dream_companies",
- ]
- education_fields = ["institution_name", "location", "degree_type", "major"]
- work_fields = ["title", "company"]
- certification_fields = ["certification_name", "organization"]
-
- or_filters = []
- if text:
- for field in user_fields:
- or_filters.append(f"u.{field} like {text}")
- for field in education_fields:
- or_filters.append(f"ed.{field} like {text}")
- for field in work_fields:
- or_filters.append(f"we.{field} like {text}")
- for field in certification_fields:
- or_filters.append(f"c.{field} like {text}")
-
- or_filters.append(f"s.skill_name like {text}")
- or_filters.append(f"pf.function like {text}")
- or_filters.append(f"pi.industry like {text}")
-
- return "AND ({})".format(" OR ".join(or_filters)) if or_filters else ""
-
-
-def get_user_details(users):
- user_details = []
- for user in users:
- details = frappe.db.get_value(
- "User",
- user,
- ["name", "username", "full_name", "user_image", "headline", "looking_for_job"],
- as_dict=True,
- )
- user_details.append(Widgets().MemberCard(member=details, avatar_class="avatar-large"))
-
- return user_details
-
-
-def get_users(or_filters, start, page_length):
- users = frappe.db.sql(
- """
- SELECT DISTINCT u.name
- FROM `tabUser` u
- LEFT JOIN `tabEducation Detail` ed
- ON u.name = ed.parent
- LEFT JOIN `tabWork Experience` we
- ON u.name = we.parent
- LEFT JOIN `tabCertification` c
- ON u.name = c.parent
- LEFT JOIN `tabSkills` s
- ON u.name = s.parent
- LEFT JOIN `tabPreferred Function` pf
- ON u.name = pf.parent
- LEFT JOIN `tabPreferred Industry` pi
- ON u.name = pi.parent
- WHERE u.enabled = True {or_filters}
- ORDER BY u.creation desc
- LIMIT {start}, {page_length}
- """.format(
- or_filters=or_filters, start=start, page_length=page_length
- ),
- as_dict=1,
- )
-
- return users
-
-
-@frappe.whitelist()
-def save_role(user, role, value):
- frappe.only_for("Moderator")
- if cint(value):
- doc = frappe.get_doc(
- {
- "doctype": "Has Role",
- "parent": user,
- "role": role,
- "parenttype": "User",
- "parentfield": "roles",
- }
- )
- doc.save(ignore_permissions=True)
- else:
- frappe.db.delete("Has Role", {"parent": user, "role": role})
- return True
diff --git a/lms/templates/signup-form.html b/lms/templates/signup-form.html
index 6f803dbe..296884d2 100644
--- a/lms/templates/signup-form.html
+++ b/lms/templates/signup-form.html
@@ -85,7 +85,7 @@
}
frappe.call({
- method: "lms.overrides.user.sign_up",
+ method: "lms.lms.user.sign_up",
args: {
"email": email,
"full_name": full_name,