diff --git a/lms/hooks.py b/lms/hooks.py index 3e1e92d2..188a9e45 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -192,7 +192,9 @@ jinja = { "lms.lms.utils.get_popular_courses", "lms.lms.utils.format_amount", "lms.lms.utils.first_lesson_exists", - "lms.lms.utils.has_course_instructor_role" + "lms.lms.utils.get_courses_under_review", + "lms.lms.utils.has_course_instructor_role", + "lms.lms.utils.has_course_moderator_role" ], "filters": [] } diff --git a/lms/lms/doctype/lms_course/lms_course.py b/lms/lms/doctype/lms_course/lms_course.py index 0f497eb3..f6008f3d 100644 --- a/lms/lms/doctype/lms_course/lms_course.py +++ b/lms/lms/doctype/lms_course/lms_course.py @@ -194,7 +194,7 @@ def submit_for_review(course): @frappe.whitelist() -def save_course(tags, title, short_introduction, video_link, description, course, image=None): +def save_course(tags, title, short_introduction, video_link, description, course, published, upcoming, image=None): if course: doc = frappe.get_doc("LMS Course", course) else: @@ -208,7 +208,9 @@ def save_course(tags, title, short_introduction, video_link, description, course "video_link": video_link, "image": image, "description": description, - "tags": tags + "tags": tags, + "published": published, + "upcoming": upcoming }) doc.save(ignore_permissions=True) return doc.name diff --git a/lms/lms/utils.py b/lms/lms/utils.py index 5fd64ca3..22f78197 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -435,8 +435,23 @@ def redirect_to_courses_list(): raise frappe.Redirect -def has_course_instructor_role(): +def has_course_instructor_role(member=None): return frappe.db.get_value("Has Role", { - "parent": frappe.session.user, + "parent": member or frappe.session.user, "role": "Course Instructor" }, "name") + + +def has_course_moderator_role(member=None): + return frappe.db.get_value("Has Role", { + "parent": member or frappe.session.user, + "role": "Course Moderator" + }, "name") + + +def get_courses_under_review(): + return frappe.get_all("LMS Course", { + "status": "Under Review" + }, ["name", "upcoming", "title", "image", "enable_certification", "status", "published"] +) + diff --git a/lms/lms/widgets/CourseOutline.html b/lms/lms/widgets/CourseOutline.html index b146c3fd..2aec5324 100644 --- a/lms/lms/widgets/CourseOutline.html +++ b/lms/lms/widgets/CourseOutline.html @@ -55,7 +55,7 @@ {% set active = membership.current_lesson == lesson.name %}
- {% if membership or lesson.include_in_preview or is_instructor %} + {% if membership or lesson.include_in_preview or is_instructor or has_course_moderator_role() %} { }; + diff --git a/lms/overrides/user.py b/lms/overrides/user.py index 52db13c7..820a4be5 100644 --- a/lms/overrides/user.py +++ b/lms/overrides/user.py @@ -330,3 +330,22 @@ def get_users(or_filters, start, page_length, text): """.format(or_filters = or_filters, start=start, page_length=page_length), as_dict=1) return users + + +@frappe.whitelist() +def save_role(user, role, value): + 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/patches.txt b/lms/patches.txt index f8a1d28a..8ea2108a 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -31,3 +31,4 @@ lms.patches.v0_0.quiz_submission_member lms.patches.v0_0.delete_old_module_docs #08-07-2022 lms.patches.v0_0.delete_course_web_forms #21-08-2022 lms.patches.v0_0.create_course_instructor_role #29-08-2022 +lms.patches.v0_0.create_course_moderator_role diff --git a/lms/patches/v0_0/create_course_moderator_role.py b/lms/patches/v0_0/create_course_moderator_role.py new file mode 100644 index 00000000..f7127c81 --- /dev/null +++ b/lms/patches/v0_0/create_course_moderator_role.py @@ -0,0 +1,11 @@ +import frappe + +def execute(): + if not frappe.db.exists("Role", "Course Moderator"): + role = frappe.get_doc({ + "doctype": "Role", + "role_name": "Course Moderator", + "home_page": "/dashboard", + "desk_access": 0 + }) + role.save(ignore_permissions=True) diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 2fe71a33..d24d8ff8 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -74,7 +74,6 @@ input[type=checkbox] { .common-page-style { padding: 2rem 0 5rem; - min-height: 60vh; padding-top: 3rem; background-color: var(--bg-color); } @@ -1687,3 +1686,8 @@ li { .review-link { float: right } + +.role { + margin-bottom: 0; + cursor: pointer; +} diff --git a/lms/templates/courses_under_review.html b/lms/templates/courses_under_review.html new file mode 100644 index 00000000..394ec66e --- /dev/null +++ b/lms/templates/courses_under_review.html @@ -0,0 +1,19 @@ +{% set courses = get_courses_under_review() %} + +{% if courses | length %} +
+ {% for course in courses %} + {{ widgets.CourseCard(course=course) }} + {% endfor %} +
+ +{% else %} +
+
+ +
+
+
{{ _("No courses under review") }}
+
+
+{% endif %} diff --git a/lms/www/batch/learn.html b/lms/www/batch/learn.html index 78896567..629e4ca0 100644 --- a/lms/www/batch/learn.html +++ b/lms/www/batch/learn.html @@ -57,6 +57,7 @@ {% macro LessonContent(lesson) %} {% set instructors = get_instructors(course.name) %} {% set is_instructor = is_instructor(course.name) %} +
@@ -70,7 +71,7 @@ {{ _("COMPLETED") }} - {% if is_instructor and not lesson.edit_mode %} + {% if (is_instructor or has_course_moderator_role()) and not lesson.edit_mode %} {% endif %}
@@ -105,7 +106,7 @@
- {% if membership or lesson.include_in_preview or is_instructor %} + {% if show_lesson %} {% if is_instructor and not lesson.include_in_preview and not lesson.edit_mode %}
@@ -190,9 +191,9 @@
{% if lesson.quiz_id %}{{ lesson.quiz_id }}{% endif %}
-
diff --git a/lms/www/courses/course.html b/lms/www/courses/course.html index c8f04de8..4fdaf48e 100644 --- a/lms/www/courses/course.html +++ b/lms/www/courses/course.html @@ -18,6 +18,7 @@
{{ CourseHeaderOverlay(course) }} + {{ CourseSettings(course) }} {{ Description(course) }} {{ Save(course) }} {{ widgets.CourseOutline(course=course, membership=membership, is_user_interested=is_user_interested) }} @@ -210,6 +211,25 @@ {% endmacro %} + +{% macro CourseSettings(course) %} + + {% if course.edit_mode and has_course_moderator_role() %} +
+ + +
+ {% endif %} + +{% endmacro %} + + {% macro Save(course) %} {% if course.edit_mode %} @@ -270,7 +290,7 @@ membership.current_lesson else "1.1" if first_lesson_exists(course.name) else None %} {% if show_start_learing_cta %} -
+
{{ _("Start Learning") }}
@@ -286,7 +306,7 @@ {{ _("Checkout Course") }}
- {% elif course.upcoming and not is_user_interested %} + {% elif course.upcoming and not is_user_interested and not is_instructor %}
{{ _("Notify me when available") }}
@@ -323,7 +343,7 @@ {% endif %} {% endif %} - {% if is_instructor(course.name) %} + {% if is_instructor(course.name) or has_course_moderator_role() %} {{ _("Edit Course") }} {% endif %} {% endmacro %} @@ -341,9 +361,9 @@ frappe.utils.format_time(certificate_request.start_time, "short")) }}

{% endif %} - {% if course.status == "Under Review" %} + {% if course.status == "Under Review" and is_instructor(course.name) %}
- {{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }} + {{ _("This course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
{% endif %} diff --git a/lms/www/courses/course.js b/lms/www/courses/course.js index 9e9f086b..0669f119 100644 --- a/lms/www/courses/course.js +++ b/lms/www/courses/course.js @@ -206,7 +206,7 @@ const submit_for_review = (e) => { }, 3); setTimeout(() => { window.location.reload(); - }, 3000); + }, 1000); } } }); @@ -339,7 +339,9 @@ const save_course = (e) => { "video_link": $("#video-link").text(), "image": $("#image").attr("href"), "description": $("#description").text(), - "course": $("#title").data("course") ? $("#title").data("course") : "" + "course": $("#title").data("course") ? $("#title").data("course") : "", + "published": $("#published").prop("checked") ? 1 : 0, + "upcoming": $("#upcoming").prop("checked") ? 1 : 0 }, callback: (data) => { frappe.show_alert({ diff --git a/lms/www/courses/course.py b/lms/www/courses/course.py index fb83fd9b..f56f1ad2 100644 --- a/lms/www/courses/course.py +++ b/lms/www/courses/course.py @@ -1,6 +1,6 @@ import frappe from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction -from lms.lms.utils import get_membership, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list +from lms.lms.utils import get_membership, has_course_moderator_role, is_instructor, is_certified, get_evaluation_details, redirect_to_courses_list def get_context(context): context.no_cache = 1 @@ -28,7 +28,7 @@ def set_course_context(context, course_name): as_dict=True) if frappe.form_dict.get("edit"): - if not is_instructor(course.name): + if not is_instructor(course.name) and not has_course_moderator_role(): redirect_to_courses_list() course.edit_mode = True diff --git a/lms/www/dashboard/index.html b/lms/www/dashboard/index.html index fb93273d..c7d76e57 100644 --- a/lms/www/dashboard/index.html +++ b/lms/www/dashboard/index.html @@ -16,12 +16,23 @@
{% endif %} -
+ {% if show_review_section %} +
+ {% include "lms/templates/courses_under_review.html" %} +
+ {% endif %} + +
diff --git a/lms/www/dashboard/index.py b/lms/www/dashboard/index.py index 487ebdd1..68951425 100644 --- a/lms/www/dashboard/index.py +++ b/lms/www/dashboard/index.py @@ -1,8 +1,9 @@ import frappe -from lms.lms.utils import has_course_instructor_role +from lms.lms.utils import has_course_instructor_role, has_course_moderator_role def get_context(context): context.no_cache = 1 portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") context.show_creators_section = portal_course_creation == "Anyone" or has_course_instructor_role() + context.show_review_section = has_course_moderator_role() diff --git a/lms/www/profiles/profile.html b/lms/www/profiles/profile.html index 3a4a6560..ad3ff2bc 100644 --- a/lms/www/profiles/profile.html +++ b/lms/www/profiles/profile.html @@ -11,6 +11,7 @@
{% set read_only = member.name != frappe.session.user %} + {{ RoleSettings(member) }} {{ About(member) }} {{ EducationDetails(member) }} {{ WorkDetails(member) }} @@ -45,7 +46,7 @@
-
{{ member.full_name }}
+
{{ member.full_name }}
{% if get_authored_courses(member.name) | length %}
{{ _("Creator") }}
@@ -155,10 +156,34 @@ {% endmacro %} + +{% macro RoleSettings(member) %} + {% if has_course_moderator_role() %} +
+
+
{{ _("Role Settings") }}
+
+ + +
+
+
+ {% endif %} +{% endmacro %} + + {% macro About(member) %} {% if member.bio %} -
+
{{ _("About") }}
{{ member.bio }}
@@ -386,21 +411,3 @@
{% endif %} {% endmacro %} - - -{% block script %} - -{% endblock %} diff --git a/lms/www/profiles/profile.js b/lms/www/profiles/profile.js new file mode 100644 index 00000000..da344e2a --- /dev/null +++ b/lms/www/profiles/profile.js @@ -0,0 +1,42 @@ +frappe.ready(() => { + + make_profile_active_in_navbar(); + + $(".role").change((e) => { + save_role(e); + }); + +}); + + +const make_profile_active_in_navbar = () => { + let member_name = $(".profile-name").data("name"); + if (member_name == frappe.session.user) { + setTimeout(() => { + let link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile"); + link_array.length && $(link_array[0]).addClass("active"); + }, 0) + } +} + + +const save_role = (e) => { + let member_name = $(".profile-name").data("name"); + let role = $(e.currentTarget).children("input"); + frappe.call({ + method: "lms.overrides.user.save_role", + args: { + "user": member_name, + "role": role.data("role"), + "value": role.prop("checked") ? 1 : 0 + }, + callback: (data) => { + if (data.message) { + frappe.show_alert({ + message: __("Saved"), + indicator: "green", + }); + } + } + }) +} diff --git a/lms/www/profiles/profile.py b/lms/www/profiles/profile.py index 93ac9136..ddbdf481 100644 --- a/lms/www/profiles/profile.py +++ b/lms/www/profiles/profile.py @@ -1,6 +1,6 @@ import frappe from lms.page_renderers import get_profile_url_prefix -from urllib.parse import urlencode + def get_context(context): context.no_cache = 1 @@ -12,13 +12,16 @@ def get_context(context): if username: frappe.local.flags.redirect_location = get_profile_url_prefix() + username raise frappe.Redirect + try: context.member = frappe.get_doc("User", {"username": username}) except: context.template = "www/404.html" return + context.profile_tabs = get_profile_tabs(context.member) + def get_profile_tabs(user): """Returns the enabled ProfileTab objects.