Compare commits

...

12 Commits

Author SHA1 Message Date
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
d5067a4bcd Merge pull request #169 from fossunited/username-validations
fix: Username validations
2021-08-06 15:05:36 +05:30
pateljannat
04d44510de fix: redirect after edit profile save 2021-08-06 14:41:37 +05:30
pateljannat
844fcc9bca fix: username validations 2021-08-06 14:41:11 +05:30
Jannat Patel
145b5efab0 Merge pull request #168 from fossunited/minor-issues
fix: quiz, course outline, and lesson indexing
2021-08-05 18:41:15 +05:30
pateljannat
4079ed97b9 fix: quiz, course outline, and lesson indexing 2021-08-05 18:26:41 +05:30
pateljannat
63d70fc037 fix: username space and empty validations 2021-08-05 15:51:21 +05:30
Jannat Patel
ce86b5deda Merge pull request #167 from fossunited/issues
fix: cleanup
2021-08-04 19:18:45 +05:30
pateljannat
037e946bbe fix: mentors cleanup 2021-08-04 19:07:14 +05:30
pateljannat
a51c8de1eb fix: profile progress and review links 2021-08-04 14:01:52 +05:30
Jannat Patel
53dc517180 Merge pull request #165 from fossunited/quiz-cleanup
refactor: Quiz cleanup
2021-08-03 19:04:37 +05:30
17 changed files with 207 additions and 126 deletions

View File

@@ -1,5 +1,5 @@
{% set color = member.get_palette() %}
<a href="/{{member.username}}">
<a class="button-links" href="/{{member.username}}">
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">

View File

@@ -37,6 +37,7 @@
"fieldname": "member_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Type",
"options": "\nStudent\nMentor\nStaff"
},
@@ -44,7 +45,6 @@
"default": "Member",
"fieldname": "role",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Role",
"options": "\nMember\nAdmin"
},
@@ -63,9 +63,10 @@
{
"fetch_from": "batch.course",
"fieldname": "course",
"fieldtype": "Data",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course"
"label": "Course",
"options": "LMS Course"
},
{
"fieldname": "current_lesson",
@@ -83,7 +84,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-06 20:50:46.885325",
"modified": "2021-08-04 17:10:42.708479",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Membership",

View File

@@ -74,8 +74,11 @@ class LMSCourse(Document):
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors:
member = frappe.get_doc("User", mentor.mentor)
# TODO: change this to count query
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"}))
member.batch_count = frappe.db.count("LMS Batch Membership",
{
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member)
return course_mentors
@@ -191,7 +194,13 @@ class LMSCourse(Document):
"""Returns the {chapter_index}.{lesson_index} for the lesson.
"""
lesson = frappe.db.get_value("Lessons", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
if not lesson:
return None
chapter = frappe.db.get_value("Chapters", {"chapter": lesson.parent}, ["idx"], as_dict=True)
if not chapter:
return None
return f"{chapter.idx}.{lesson.idx}"
def reindex_exercises(self):
@@ -238,21 +247,6 @@ class LMSCourse(Document):
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships
def get_mentors(self, batch=None):
filters = {
"course": self.name,
"member_type": "Mentor"
}
if batch:
filters["batch"] = batch
memberships = frappe.get_all(
"LMS Batch Membership",
filters,
["member"])
member_names = [m['member'] for m in memberships]
return find_all("User", name=["IN", member_names])
def get_students(self, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict.
"""

View File

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

View File

@@ -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",

View File

@@ -27,7 +27,7 @@
{{ lesson.title }}
{% if membership %}
<img class="lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
<img class="ml-1 lesson-progress-tick {{ course.get_progress(lesson.name) != 'Complete' and 'hide' }}"
src="/assets/community/icons/check.svg">
{% endif %}

View File

@@ -1,30 +0,0 @@
<div class="batch">
<div class="batch-details">
<div class="">Session every {{batch.sessions_on}}</div>
<div>{{frappe.utils.format_time(batch.start_time, "short")}} -
{{frappe.utils.format_time(batch.end_time, "short")}}
</div>
<div>Starting {{frappe.utils.format_date(batch.start_date, "medium")}}</div>
<div class="course-type" style="color: #888; padding: 10px 0px;">mentors</div>
{% for m in course.get_mentors(batch.name) %}
<div>
{{ widgets.Avatar(member=m, avatar_class="avatar-medium" ) }}
<span class="instructor-title">{{m.full_name}}</span>
</div>
{% endfor %}
</div>
{% if can_manage or can_join %}
<div class="cta">
<div class="">
{% if can_manage %}
<a href="/courses/{{ course.name }}/home?batch={{ batch.name }}" class="btn btn-primary manage-batch" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Manage</a>
{% elif can_join %}
<button class="join-batch btn btn-secondary" data-batch="{{ batch.name | urlencode }}"
data-course="{{ course.name | urlencode }}">Join this Batch</button>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@@ -18,9 +18,11 @@
<div class="review-card-footer">
<div>
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
<span class="course-instructor">
{{ review.owner_details.full_name }}
</span>
<a class="button-links" href="/{{review.owner_details.username}}">
<span class="course-instructor">
{{ review.owner_details.full_name }}
</span>
</a>
</div>
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}

View File

@@ -0,0 +1,63 @@
# 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_underscore_at_end(self):
new_user = frappe.get_doc({
"doctype": "User",
"email": "test_with_underscore_at_end@example.com",
"first_name": "Username___"
}).insert()
self.assertNotEqual(new_user.username[-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
def tearDownClass(cls) -> None:
users = [
"test_with_basic_username@example.com",
"test-without-username@example.com",
"test_with_illegal_characters@example.com",
"test_with_underscore_at_end@example.com",
"test_with_short_first_name@example.com"
]
frappe.db.delete("User", {"name": ["in", users]})

View File

@@ -2,9 +2,57 @@ 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 len(self.username):
underscore_condition = self.username[0] == "_" or self.username[-1] == "_"
else:
underscore_condition = ''
if self.is_new():
if not self.username:
self.username = self.get_username_from_first_name()
if self.username.find(" "):
self.username.replace(" ", "")
if not re.match("^[A-Za-z0-9_]*$", self.username) or underscore_condition:
self.username = self.remove_illegal_characters()
if len(self.username) < 4:
self.username = self.email.replace("@", "").replace(".", "")
while self.username_exists():
self.username = self.remove_illegal_characters() + str(random.randint(0, 99))
else:
if not self.username:
frappe.throw(_("Username already exists."))
if not re.match("^[A-Za-z0-9_]*$", self.username):
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):
return frappe.scrub(self.first_name) + str(random.randint(0, 99))
def remove_illegal_characters(self):
return re.sub("[^\w]+", "", self.username).strip("_")
def get_authored_courses(self) -> int:
"""Returns the number of courses authored by this user.
"""

View File

@@ -780,7 +780,7 @@ input[type=checkbox] {
}
.lesson-links {
display: flex;
display: block;
padding: 0 1rem;
margin-bottom: .25rem;
color: inherit;
@@ -794,6 +794,13 @@ input[type=checkbox] {
border-radius: .25rem;
}
.course-content-parent .lesson-links {
padding: 0 0 0 1rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
line-height: 200%;
}
.chapter-content {
margin: 0;
margin-left: .875rem;
@@ -1214,7 +1221,19 @@ input[type=checkbox] {
left: 174px;
font-size: 12px;
line-height: 165%;
width: fit-content;
width: 85%;
}
@media (max-width: 1200px) {
.profile-profession {
width: 75%;
}
}
@media (max-width: 767px) {
.profile-profession {
width: 60%;
}
}
@media (max-width: 500px) {
@@ -1228,6 +1247,7 @@ input[type=checkbox] {
.profile-profession {
top: 5px;
left: 70px;
width: 75%;
}
}

View File

@@ -7,8 +7,8 @@
<div class="questions">
{% for question in quiz.questions %}
<div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}"
data-question="{{ question.question }}"data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
<p>{{ question.question }}</p>
data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
<p>{{ frappe.utils.md_to_html(question.question) }}</p>
{% if question.multiple %}
<small class="font-weight-bold">Choose all answers that apply:</small>

View File

@@ -58,6 +58,7 @@ var mark_active_question = (e = undefined) => {
$(".current-question").text(`${next_index}`);
$("#check").removeClass("hide").attr("disabled", true);
$("#next").addClass("hide");
$(".explanation").addClass("hide");
}
var mark_progress = (e) => {

View File

@@ -17,10 +17,9 @@ def get_context(context):
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
else:
index_ = "1.1"
frappe.local.flags.redirect_location = context.course.get_learn_url(index_) + context.course.query_parameter
raise frappe.Redirect
utils.redirect_to_lesson(context.course, index_)
context.lesson = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))[0]
context.lesson = get_current_lesson_details(lesson_number, context)
neighbours = context.course.get_neighbours(lesson_number, context.lessons)
context.next_url = get_learn_url(neighbours["next"], context.course)
context.prev_url = get_learn_url(neighbours["prev"], context.course)
@@ -40,6 +39,12 @@ def get_context(context):
"is_member": context.membership is not None
}
def get_current_lesson_details(lesson_number, context):
details_list = list(filter(lambda x: cstr(x.number) == lesson_number, context.lessons))
if not len(details_list):
utils.redirect_to_lesson(context.course)
return details_list[0]
def get_learn_url(lesson_number, course):
return course.get_learn_url(lesson_number) and course.get_learn_url(lesson_number) + course.query_parameter

View File

@@ -24,9 +24,6 @@ def get_common_context(context):
if batch:
context.batch = batch
context.members = course.get_mentors(membership.batch) + course.get_students(membership.batch)
context.member_count = len(context.members)
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
context.livecode_url = get_livecode_url()

View File

@@ -102,24 +102,33 @@
</div>
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }}
</div>
{% if course.get_course_progress() %}
{% set progress = course.get_course_progress() %}
{% if progress %}
<div class="course-progress-section">
<div class="course-home-headings">
Your Progress
</div>
<div class="common-card-style progress-card">
<p class="small-title">
{% if progress != 100 %}
Great work so far!
{% else %}
Excellent Work on completing this course 👏
{% endif %}
</p>
<p class="progress-text">
{% if progress != 100 %}
Challenge yourself to complete the lessons and grow professionally.
{% else %}
You have reached a new level in your journey to success!
{% endif %}
</p>
<div class="progress-percentage">
{{ frappe.utils.rounded(course.get_course_progress()) }}%
{{ frappe.utils.rounded(progress) }}%
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: {{ course.get_course_progress() }}%"
aria-valuenow="{{ course.get_course_progress() }}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar" role="progressbar" style="width: {{ progress }}%"
aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
@@ -187,55 +196,3 @@
{% endif %}
</div>
{% endmacro %}
{% macro BatchSection(course) %}
<div class="row">
<div class="col-lg-8 col-md-12">
{% if course.is_mentor(frappe.session.user) %}
{{ BatchSectionForMentors(course, course.get_batches(mentor=frappe.session.user)) }}
{% else %}
{{ BatchSectionForStudents(course, course.get_upcoming_batches()) }}
{% endif %}
</div>
</div>
{% endmacro %}
{% macro BatchSectionForMentors(course, mentor_batches) %}
<h2>Your Batches</h2>
{% if mentor_batches %}
<div class="row">
{% for batch in mentor_batches %}
<div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_manage=True) }}
</div>
{% endfor %}
</div>
<a class="add-batch margin-bottom" href="/add-a-new-batch?new=1&course={{course.name}}">Add a new batch</a>
{% else %}
<div class="mentor_message">
<p>
You are a mentor for this course.
</p>
<a class="" href="/add-a-new-batch?new=1&course={{course.name}}">Create your first batch</a>
</div>
{% endif %}
{% endmacro %}
{% macro BatchSectionForStudents(course, upcoming_batches) %}
{% if upcoming_batches %}
<div class="mt-5">
<h3 class="upcoming">Upcoming Batches</h3>
<div class="row">
{% for batch in upcoming_batches %}
<div class="col-lg-4 col-md-6">
{{ widgets.RenderBatch(course=course, batch=batch, can_join=True) }}
</div>
{% endfor %}
</div>
{% else %}
<div class="mt-5 upcoming">There are no Upcoming Batches for this course currently.</div>
{% endif %}
</div>
{% endmacro %}

View File

@@ -35,21 +35,24 @@
{% endif %}
<span class="social-icons">
{% if member.linkedin %}
<a class="linkedin" href="{{ member.linkedin }}">
<a class="linkedin button-links" href="{{ member.linkedin }}">
<img src="/assets/community/images/linkedin.png">
</a>
{% endif %}
{% if member.medium %}
<a class="medium" href="{{ member.medium}}">
<a class="medium button-links" href="{{ member.medium}}">
<img src="/assets/community/icons/medium.svg">
</a>
{% endif %}
{% if member.github %}
<a class="github" href="{{ member.github }}">
<a class="github button-links" href="{{ member.github }}">
<img src="/assets/community/icons/github.svg">
</a>
{% endif %}
</span>
{% if frappe.session.user == member.email %}
<a class="dark-links pull-right" href="edit-profile?name={{ member.email }}">Edit Profile</a>
{% endif %}
</div>
</div>
</div>