Compare commits

..

10 Commits

Author SHA1 Message Date
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
pateljannat
44ca940c6b fix: quiz enhancements and tests 2021-08-03 18:30:52 +05:30
pateljannat
c0b688c720 Merge branch 'main' of https://github.com/frappe/community into quiz-cleanup 2021-07-30 18:35:23 +05:30
Jannat Patel
861d5f231d Merge pull request #164 from fossunited/issues
fix: default image, meta, reviews, lesson headers
2021-07-30 17:07:19 +05:30
pateljannat
d14b4f55a6 fix: default image, meta, reviews, lesson headers 2021-07-30 16:24:56 +05:30
Jannat Patel
db9a6c3eda Merge pull request #163 from fossunited/chapter-lesson-patch-fix
fix: chapter lesson patch
2021-07-29 13:48:23 +05:30
pateljannat
5431fcb450 fix: quiz ui change 2021-07-23 17:55:41 +05:30
pateljannat
282c4c5351 feat: explanation field in quiz 2021-07-20 09:36:22 +05:30
25 changed files with 427 additions and 246 deletions

View File

@@ -1,5 +1,5 @@
{% set color = member.get_palette() %} {% 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 }}"> <span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
{% if member.user_image %} {% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}"> <img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}" title="{{ member.full_name }}">

View File

@@ -136,7 +136,6 @@ primary_rules = [
{"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"}, {"from_route": "/courses/<course>/<topic>", "to_route": "courses/topic"},
{"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"}, {"from_route": "/hackathons/<hackathon>", "to_route": "hackathons/hackathon"},
{"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"}, {"from_route": "/hackathons/<hackathon>/<project>", "to_route": "hackathons/project"},
{"from_route": "/dashboard", "to_route": ""},
{"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"}, {"from_route": "/add-a-new-batch", "to_route": "add-a-new-batch"},
{"from_route": "/courses/<course>/home", "to_route": "batch/home"}, {"from_route": "/courses/<course>/home", "to_route": "batch/home"},
{"from_route": "/courses/<course>/learn", "to_route": "batch/learn"}, {"from_route": "/courses/<course>/learn", "to_route": "batch/learn"},

View File

@@ -37,6 +37,7 @@
"fieldname": "member_type", "fieldname": "member_type",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Type", "label": "Member Type",
"options": "\nStudent\nMentor\nStaff" "options": "\nStudent\nMentor\nStaff"
}, },
@@ -44,7 +45,6 @@
"default": "Member", "default": "Member",
"fieldname": "role", "fieldname": "role",
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1,
"label": "Role", "label": "Role",
"options": "\nMember\nAdmin" "options": "\nMember\nAdmin"
}, },
@@ -63,9 +63,10 @@
{ {
"fetch_from": "batch.course", "fetch_from": "batch.course",
"fieldname": "course", "fieldname": "course",
"fieldtype": "Data", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Course" "label": "Course",
"options": "LMS Course"
}, },
{ {
"fieldname": "current_lesson", "fieldname": "current_lesson",
@@ -83,7 +84,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-06 20:50:46.885325", "modified": "2021-08-04 17:10:42.708479",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch Membership", "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"]) mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name}, ["mentor"])
for mentor in mentors: for mentor in mentors:
member = frappe.get_doc("User", mentor.mentor) member = frappe.get_doc("User", mentor.mentor)
# TODO: change this to count query member.batch_count = frappe.db.count("LMS Batch Membership",
member.batch_count = len(frappe.get_all("LMS Batch Membership", {"member": member.name, "member_type": "Mentor"})) {
"member": member.name,
"member_type": "Mentor"
})
course_mentors.append(member) course_mentors.append(member)
return course_mentors return course_mentors
@@ -238,21 +241,6 @@ class LMSCourse(Document):
membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title") membership.batch_title = frappe.db.get_value("LMS Batch", membership.batch, "title")
return all_memberships 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): def get_students(self, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict. """Returns (email, full_name, username) of all the students of this batch as a list of dict.
""" """

View File

@@ -46,34 +46,30 @@ class LMSQuiz(Document):
return result[0] return result[0]
@frappe.whitelist() @frappe.whitelist()
def submit(quiz, result): def quiz_summary(quiz, results):
score = 0 score = 0
answer_map = { results = json.loads(results)
"is_correct_1": "option_1",
"is_correct_2": "option_2",
"is_correct_3": "option_3",
"is_correct_4": "option_4"
}
result = json.loads(result)
quiz_details = frappe.get_doc("LMS Quiz", quiz)
for response in result: for result in results:
match = list(filter(lambda x: x.question == response.get("question"), quiz_details.questions))[0] correct = result["is_correct"][0]
correct_options = quiz_details.get_correct_options(match) result["question"] = frappe.db.get_value("LMS Quiz Question",
correct_answers = [ match.get(answer_map[option]) for option in correct_options ] {"parent": quiz, "idx": result["question_index"]},
["question"])
if response.get("answer") == correct_answers: for point in result["is_correct"]:
response["result"] = "Right" correct = correct and point
score += 1
else: result["result"] = "Right" if correct else "Wrong"
response["result"] = "Wrong" score += correct
response["answer"] = ("").join([ ans if idx == len(response.get("answer")) -1 else ans + ", " for idx, ans in enumerate(response.get("answer")) ])
del result["is_correct"]
del result["question_index"]
frappe.get_doc({ frappe.get_doc({
"doctype": "LMS Quiz Submission", "doctype": "LMS Quiz Submission",
"quiz": quiz, "quiz": quiz,
"result": result, "result": results,
"score": score "score": score
}).save(ignore_permissions=True) }).save(ignore_permissions=True)
update_progress(quiz_details.lesson)
return score return score

View File

@@ -3,6 +3,39 @@
# import frappe # import frappe
import unittest import unittest
import frappe
class TestLMSQuiz(unittest.TestCase): class TestLMSQuiz(unittest.TestCase):
pass
@classmethod
def setUpClass(cls) -> None:
frappe.get_doc({
"doctype": "LMS Quiz",
"title": "Test Quiz"
}).save()
def test_with_multiple_options(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question multiple",
"option_1": "Option 1",
"is_correct_1": 1,
"option_2": "Option 2",
"is_correct_2": 1
})
quiz.save()
self.assertTrue(quiz.questions[0].multiple)
def test_with_no_correct_option(self):
quiz = frappe.get_doc("LMS Quiz", "Test Quiz")
quiz.append("questions", {
"question": "Question no correct option",
"option_1": "Option 1",
"option_2": "Option 2",
})
self.assertRaises(frappe.ValidationError, quiz.save)
@classmethod
def tearDownClass(cls) -> None:
frappe.db.delete("LMS Quiz", "Test Quiz")
frappe.db.delete("LMS Quiz Question", {"parent": "Test Quiz"})

View File

@@ -9,15 +9,23 @@
"options_section", "options_section",
"option_1", "option_1",
"is_correct_1", "is_correct_1",
"column_break_5",
"explanation_1",
"section_break_5", "section_break_5",
"option_2", "option_2",
"is_correct_2", "is_correct_2",
"column_break_10",
"explanation_2",
"column_break_4", "column_break_4",
"option_3", "option_3",
"is_correct_3", "is_correct_3",
"column_break_15",
"explanation_3",
"section_break_11", "section_break_11",
"option_4", "option_4",
"is_correct_4", "is_correct_4",
"column_break_20",
"explanation_4",
"multiple" "multiple"
], ],
"fields": [ "fields": [
@@ -101,12 +109,52 @@
{ {
"fieldname": "section_break_11", "fieldname": "section_break_11",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"depends_on": "option_1",
"fieldname": "explanation_1",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_2",
"fieldname": "explanation_2",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_3",
"fieldname": "explanation_3",
"fieldtype": "Data",
"label": "Explanation"
},
{
"depends_on": "option_4",
"fieldname": "explanation_4",
"fieldtype": "Data",
"label": "Explanation"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-22 16:54:13.133859", "modified": "2021-07-19 19:35:28.446236",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz Question", "name": "LMS Quiz Question",

View File

@@ -1,10 +1,14 @@
<div class="common-card-style course-card"> <div class="common-card-style course-card">
<div class="course-image" style="background-image: url({{ course.image }});"> <div class="course-image {% if not course.image %}default-image{% endif %}"
{% if course.image %} style="background-image: url( {{ course.image }} );" {% endif %}>
<div class="course-tags"> <div class="course-tags">
{% for tag in course.get_tags() %} {% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div> <div class="course-card-pills">{{ tag }}</div>
{% endfor %} {% endfor %}
</div> </div>
{% if not course.image %}
<div class="default-image-text">{{ course.title[0] }}</div>
{% endif %}
</div> </div>
<div class="course-card-content"> <div class="course-card-content">
<div class="course-card-meta muted-text"> <div class="course-card-meta muted-text">

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

@@ -1,23 +1,28 @@
{% if course.get_reviews() | length %}
<div class="reviews-parent"> <div class="reviews-parent">
<div class="reviews-heading"> {% set reviews = course.get_reviews() %}
<div class="course-home-headings">Student Review</div> {% if reviews | length or course.is_eligible_to_review(membership) %}
<div class="mb-5">
<span class="course-home-headings">Reviews</span>
{% if course.is_eligible_to_review(membership) %} {% if course.is_eligible_to_review(membership) %}
<a class="review-link" href=""> <span class="review-link button is-secondary pull-right">
Provide your Feedback Write a review
</a> </span>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% if reviews | length %}
<div class="reviews-section"> <div class="reviews-section">
{% for review in course.get_reviews() %} {% for review in reviews %}
<div class="review-card"> <div class="review-card">
<div class="common-card-style review-content small-title"> {{ review.review }} </div> <div class="common-card-style review-content small-title"> {{ review.review }} </div>
<div class="review-card-footer"> <div class="review-card-footer">
<div> <div>
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }} {{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
<span class="course-instructor"> <a class="button-links" href="/{{review.owner_details.username}}">
{{ review.owner_details.full_name }} <span class="course-instructor">
</span> {{ review.owner_details.full_name }}
</span>
</a>
</div> </div>
<div class="rating"> <div class="rating">
{% for i in [1, 2, 3, 4, 5] %} {% for i in [1, 2, 3, 4, 5] %}
@@ -30,6 +35,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
</div> </div>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" <div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
@@ -67,8 +73,8 @@
</div> </div>
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input"> <div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field" data-fieldtype="Text" <textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldname="feedback_comments" placeholder="" style="height: 300px;" data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 300px;"
spellcheck="false"></textarea> spellcheck="false"></textarea>
</div> </div>
</div> </div>
@@ -77,9 +83,9 @@
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">Submit</div> <div class="button submit-review is-primary" data-course="{{ course.name | urlencode}}" id="submit-review">
Submit</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endif %}

View File

@@ -234,6 +234,22 @@ input[type=checkbox] {
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.default-image {
background-color: var(--avatar-frame-bg);
color: var(--avatar-frame-color);
display: flex;
flex-direction: column;
}
.default-image-text {
display: flex;
flex: 1;
align-self: center;
justify-content: normal;
font-size: 7rem;
font-weight: bold;
}
.course-tags { .course-tags {
display: flex; display: flex;
position: relative; position: relative;
@@ -317,7 +333,7 @@ input[type=checkbox] {
.card-divider { .card-divider {
border: 1px solid #F4F5F6; border: 1px solid #F4F5F6;
margin-bottom: 16px; margin-bottom: 1rem;
} }
.card-divider-dark { .card-divider-dark {
@@ -471,23 +487,30 @@ input[type=checkbox] {
--star-fill: #74808B; --star-fill: #74808B;
} }
div.custom-checkbox>label>input { .custom-checkbox {
display: flex;
align-items: center;
}
.custom-checkbox>label>input {
visibility: hidden; visibility: hidden;
} }
div.custom-checkbox>label>img { .custom-checkbox>label>.empty-checkbox {
height: 20px; height: 1.5rem;
width: 20px; width: 1.5rem;
border: 1px solid black; border: 1px solid black;
border-radius: 5px; border-radius: 5px;
} }
div.custom-checkbox>label>input:checked+img { .custom-checkbox>label>input:checked+.empty-checkbox {
background: url(/assets/community/images/Vector.png); background: url(/assets/community/icons/tick.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center center; background-position: center center;
background-size: 15px 15px; }
object-fit: contain;
.quiz-label {
margin-bottom: 0;
} }
.course-card-wide { .course-card-wide {
@@ -601,7 +624,7 @@ div.custom-checkbox>label>input:checked+img {
} }
.button { .button {
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.13), 0px 0px 0.5px rgba(0, 0, 0, 0.5); box-shadow: var(--btn-shadow);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -997,8 +1020,24 @@ div.custom-checkbox>label>input:checked+img {
color: red; color: red;
} }
.quiz-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.question { .question {
flex-direction: column; flex-direction: column;
width: 688px;
margin: auto;
}
.question p {
margin-bottom: 0;
}
.active-question .card-divider {
margin-top: 1rem;
} }
.dark-links { .dark-links {
@@ -1041,7 +1080,7 @@ div.custom-checkbox>label>input:checked+img {
} }
.course-content-parent .course-home-headings { .course-content-parent .course-home-headings {
margin: 0px 0px 16px; margin: 0px 0px 1rem;
} }
.lesson-pagination { .lesson-pagination {
@@ -1175,7 +1214,19 @@ div.custom-checkbox>label>input:checked+img {
left: 174px; left: 174px;
font-size: 12px; font-size: 12px;
line-height: 165%; 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) { @media (max-width: 500px) {
@@ -1189,6 +1240,7 @@ div.custom-checkbox>label>input:checked+img {
.profile-profession { .profile-profession {
top: 5px; top: 5px;
left: 70px; left: 70px;
width: 75%;
} }
} }
@@ -1271,3 +1323,19 @@ pre {
width: 100%; width: 100%;
overflow-x: auto; overflow-x: auto;
} }
.markdown-source h1 {
font-size: 1.3rem;
}
.markdown-source h2 {
font-size: 1.2rem;
}
.markdown-source h3 {
font-size: 1.1rem;
}
.markdown-source h4 {
font-size: 1rem;
}

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill="#68D391" stroke="#68D391" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12H16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@@ -1,3 +1 @@
<svg class="icon"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
<use href="#icon-tick"></use>
</svg>

Before

Width:  |  Height:  |  Size: 58 B

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20ZM6.0429 6.04289C6.43342 5.65237 7.06659 5.65237 7.45711 6.04289L10.0011 8.58692L12.5451 6.04294C12.9356 5.65242 13.5688 5.65242 13.9593 6.04294C14.3499 6.43347 14.3499 7.06663 13.9593 7.45716L11.4154 10.0011L13.9593 12.5451C14.3499 12.9356 14.3499 13.5688 13.9593 13.9593C13.5688 14.3499 12.9357 14.3499 12.5451 13.9593L10.0011 11.4154L7.45711 13.9594C7.06659 14.3499 6.43342 14.3499 6.0429 13.9594C5.65237 13.5689 5.65237 12.9357 6.0429 12.5452L8.58693 10.0011L6.0429 7.45711C5.65237 7.06658 5.65237 6.43342 6.0429 6.04289Z" fill="#F56B6B"/>
</svg>

After

Width:  |  Height:  |  Size: 808 B

View File

@@ -1,33 +1,59 @@
{% set last_submission = quiz.get_last_submission_details() %} <div id="quiz-title" class="course-home-headings">{{ quiz.title }}</div>
{% if last_submission %}
<div class="mb-5 pull-right"> <div class="card-divider"></div>
<div class="text-muted">Last Submitted On: {{ frappe.utils.pretty_date(last_submission.creation) }}</div>
<div class="text-muted">Last Submission Score: {{ last_submission.score }}</div> <div class="mt-5">
</div> <form id="quiz-form">
{% endif %} <div class="questions">
<h3 id="title" class="mb-5">{{ quiz.title }}</h3> {% for question in quiz.questions %}
<form id="quiz-form"> <div class="question {% if loop.index == 1 %} active-question {% else %} hide {% endif %}"
{% for question in quiz.questions %} data-question="{{ question.question }}" data-multi="{{ question.multiple}}" data-qt-index="{{ loop.index }}">
<div class="question mb-5" data-question="{{ question.question }}" <p>{{ frappe.utils.md_to_html(question.question) }}</p>
data-multi="{{ question.multiple_correct_answers}}">
<p> {{ loop.index }}. {{ question.question }}</p> {% if question.multiple %}
{% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %} <small class="font-weight-bold">Choose all answers that apply:</small>
{% for option in options %} {% else %}
{% if option %} <small class="font-weight-bold">Choose 1 answer:</small>
<div class="custom-checkbox mb-2"> {% endif %}
<label>
<input {% if question.multiple %} type="checkbox" {% else %} type="radio" <div class="card-divider"></div>
name="{{ question.question | urlencode }}" {% endif %} class="option" value="{{ option | urlencode }}">
<img /> {% set options = [question.option_1, question.option_2, question.option_3, question.option_4] %}
</label>
<span class="label-area">{{ option }}</span> {% for option in options %}
{% if option %}
<div class="custom-checkbox">
<label class="quiz-label">
<input class="option" value="{{ option | urlencode }}"
data-correct="{{ question['is_correct_' + loop.index | string] }}"
{% if question.multiple %} type="checkbox"
{% else %} type="radio" name="{{ question.question | urlencode }}" {% endif %}>
<img class="empty-checkbox mr-3"/>
</label>
<span class="label-area">{{ frappe.utils.md_to_html(option) }}</span>
</div>
{% set explanation = question['explanation_' + loop.index | string] %}
{% if explanation %}
<small class="explanation muted-text hide">{{ explanation }}</small>
{% endif %}
<div class="card-divider"></div>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div> </div>
{% endif %} <div class="quiz-footer">
{% endfor %} <span class="font-weight-bold"> <span class="current-question">1</span> of {{ quiz.questions | length }}</span>
</div> <button class="btn btn-primary pull-right" id="check" disabled>Check</button>
{% endfor %} <button class="btn btn-primary hide" id="next">Next</button>
<button class="btn btn-secondary hide mb-5" id="try-again">Try Again</button> <button class="btn btn-primary hide" id="summary">Summary</button>
<button class="btn btn-primary" id="submit-quiz">Submit</button> </div>
<h4 class="success-message"></h4> <div class="button is-secondary pull-right hide" id="try-again">Try Again</div>
<h5 class="score text-muted"></h5> <h4 class="success-message"></h4>
</form> <h5 class="score text-muted"></h5>
</form>
</div>

View File

@@ -1,13 +1,8 @@
{% extends "templates/base.html" %} {% extends "templates/base.html" %}
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %} {% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
{% block title %}{{ lesson.title }}{% endblock %} {% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{lesson.title}} - {{course.title}}" />
<meta name="keywords" content="{{lesson.title}} - {{course.title}}" />
<style>
</style>
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="/assets/css/lms.css"> <link rel="stylesheet" href="/assets/css/lms.css">
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css"> <link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
@@ -47,7 +42,7 @@
</div> </div>
{% if membership or lesson.include_in_preview %} {% if membership or lesson.include_in_preview %}
<div class="common-card-style lesson-content-card from-markdown">{{ lesson.render_html() }}</div> <div class="common-card-style lesson-content-card markdown-source">{{ lesson.render_html() }}</div>
{% else %} {% else %}
<div class="common-card-style lesson-content-card"> <div class="common-card-style lesson-content-card">
<span>This lesson is not available for Preview. Please join the course to access this lesson. <a href="/courses/{{ course.name }}">Checkout Course Details.</a></span> <span>This lesson is not available for Preview. Please join the course to access this lesson. <a href="/courses/{{ course.name }}">Checkout Course Details.</a></span>

View File

@@ -1,13 +1,27 @@
frappe.ready(() => { frappe.ready(() => {
localStorage.removeItem($("#quiz-title").text());
save_current_lesson(); save_current_lesson();
$(".option").click((e) => {
enable_check(e);
})
$("#progress").click((e) => { $("#progress").click((e) => {
mark_progress(e); mark_progress(e);
}); });
$("#submit-quiz").click((e) => { $("#summary").click((e) => {
submit_quiz(e); quiz_summary(e);
});
$("#check").click((e) => {
check_answer(e);
});
$("#next").click((e) => {
mark_active_question(e);
}); });
$("#try-again").click((e) => { $("#try-again").click((e) => {
@@ -25,6 +39,27 @@ var save_current_lesson = () => {
} }
} }
var enable_check = (e) => {
if ($(".option:checked").length && $("#check").attr("disabled")) {
$("#check").removeAttr("disabled");
}
}
var mark_active_question = (e = undefined) => {
var current_index;
var next_index = 1;
if (e) {
e.preventDefault();
current_index = $(".active-question").attr("data-qt-index");
next_index = parseInt(current_index) + 1;
}
$(".question").addClass("hide").removeClass("active-question");
$(`.question[data-qt-index='${next_index}']`).removeClass("hide").addClass("active-question");
$(".current-question").text(`${next_index}`);
$("#check").removeClass("hide").attr("disabled", true);
$("#next").addClass("hide");
}
var mark_progress = (e) => { var mark_progress = (e) => {
var status = $(e.currentTarget).attr("data-progress"); var status = $(e.currentTarget).attr("data-progress");
frappe.call({ frappe.call({
@@ -56,47 +91,77 @@ var change_progress_indicators = (status, e) => {
$(e.currentTarget).text(label).attr("data-progress", data_progress); $(e.currentTarget).text(label).attr("data-progress", data_progress);
} }
var submit_quiz = (e) => { var quiz_summary = (e) => {
e.preventDefault(); e.preventDefault();
var result = []; var quiz_name = $("#quiz-title").text();
$('.question').each((i, element) => { var total_questions = $(".question").length;
var options = $(element).find(".option");
var answers = [];
options.filter((i, op) => $(op).prop("checked")).each((i, elem) => answers.push(decodeURIComponent(elem.value)));
result.push({
"question": element.dataset.question,
"answer": answers
});
});
frappe.call({ frappe.call({
method: "community.lms.doctype.lms_quiz.lms_quiz.submit", method: "community.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
args: { args: {
quiz: $("#title").text(), "quiz": quiz_name,
result: result "results": localStorage.getItem(quiz_name)
}, },
callback: (data) => { callback: (data) => {
$("#submit-quiz").addClass("hide"); var message = data.message == total_questions ? "Excellent Work" : "You were almost there."
$(".question").addClass("hide");
$(".quiz-footer").addClass("hide");
$("#quiz-form").parent().prepend(
`<div class="text-center summary"><h2>${message} 👏 </h2>
<div class="font-weight-bold">${data.message}/${total_questions} correct.</div></div>`);
$("#try-again").removeClass("hide"); $("#try-again").removeClass("hide");
$(":input[type='checkbox']").prop("disabled", true);
$(":input[type='radio']").prop("disabled", true);
if (data.message == result.length) {
$(".success-message").text("Congratulations, you cleared the quiz!");
}
else {
$(".success-message").text("Some of your answers weren't correct. You can give it another shot.");
}
$(".score").text(`Score: ${data.message}/${result.length}`);
} }
}) })
} }
var try_quiz_again = (e) => { var try_quiz_again = (e) => {
e.preventDefault(); window.location.reload();
$(":input[type='checkbox']").prop("disabled", false); }
$(":input[type='radio']").prop("disabled", false);
$("#quiz-form").trigger("reset"); var check_answer = (e) => {
$(".success-message").text(""); e.preventDefault();
$(".score").text("");
$("#submit-quiz").removeClass("hide"); var quiz_name = $("#quiz-title").text();
$("#try-again").addClass("hide"); var total_questions = $(".question").length;
var current_index = $(".active-question").attr("data-qt-index");
$(".explanation").removeClass("hide");
$("#check").addClass("hide");
current_index == total_questions ? $("#summary").removeClass("hide") : $("#next").removeClass("hide");
var [answer, is_correct] = parse_options();
add_to_local_storage(quiz_name, current_index, answer, is_correct)
}
var parse_options = () => {
var answer = [];
var is_correct = [];
$(".active-question input").each((i, element) => {
var correct = parseInt($(element).attr("data-correct"));
if ($(element).prop("checked")) {
answer.push(decodeURIComponent($(element).val()));
correct && is_correct.push(1);
correct ? add_icon(element, "check") : add_icon(element, "wrong");
}
else {
correct && is_correct.push(0);
correct ? add_icon(element, "minus-circle-green") : add_icon(element, "minus-circle");
}
})
return [answer, is_correct];
}
var add_icon = (element, icon) => {
$(element).parent().empty().html(`<img class="mr-3" src="/assets/community/icons/${icon}.svg">`);
}
var add_to_local_storage = (quiz_name, current_index, answer, is_correct) => {
var quiz_stored = JSON.parse(localStorage.getItem(quiz_name));
var quiz_obj = {
"question_index": current_index,
"answer": answer.join(),
"is_correct": is_correct
}
quiz_stored ? quiz_stored.push(quiz_obj) : quiz_stored = [quiz_obj]
localStorage.setItem(quiz_name, JSON.stringify(quiz_stored))
} }

View File

@@ -25,6 +25,13 @@ def get_context(context):
context.next_url = get_learn_url(neighbours["next"], context.course) context.next_url = get_learn_url(neighbours["next"], context.course)
context.prev_url = get_learn_url(neighbours["prev"], context.course) context.prev_url = get_learn_url(neighbours["prev"], context.course)
meta_info = context.lesson.title + " - " + context.course.title
context.metatags = {
"title": meta_info,
"keywords": meta_info,
"description": meta_info
}
context.page_extensions = get_page_extensions() context.page_extensions = get_page_extensions()
context.page_context = { context.page_context = {
"course": context.course.name, "course": context.course.name,

View File

@@ -24,9 +24,6 @@ def get_common_context(context):
if batch: if batch:
context.batch = 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.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
context.livecode_url = get_livecode_url() context.livecode_url = get_livecode_url()

View File

@@ -3,8 +3,6 @@
{% block title %}{{ course.title }} {% block title %}{{ course.title }}
{% endblock %} {% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="Courses" />
<meta name="keywords" content="Courses {{course.title}}" />
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css"> <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@@ -24,12 +22,16 @@
{% macro CourseCardWide(course) %} {% macro CourseCardWide(course) %}
<div class="common-card-style course-card-wide"> <div class="common-card-style course-card-wide">
<div class="course-image-wide" style="background-image: url({{ course.image }});"> <div class="course-image-wide {% if not course.image %} default-image {% endif %}"
{% if course.image %}style="background-image: url({{ course.image }});"{% endif %}>
<div class="course-tags"> <div class="course-tags">
{% for tag in course.get_tags() %} {% for tag in course.get_tags() %}
<div class="course-card-pills">{{ tag }}</div> <div class="course-card-pills">{{ tag }}</div>
{% endfor %} {% endfor %}
</div> </div>
{% if not course.image %}
<div class="default-image-text">{{ course.title[0] }}</div>
{% endif %}
</div> </div>
<div class="course-card-wide-content"> <div class="course-card-wide-content">
<div class="course-info"> <div class="course-info">
@@ -100,24 +102,33 @@
</div> </div>
{{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }} {{ widgets.MemberCard(member=course.get_instructor(), show_course_count=True, dimension_class="member-card-large") }}
</div> </div>
{% if course.get_course_progress() %} {% set progress = course.get_course_progress() %}
{% if progress %}
<div class="course-progress-section"> <div class="course-progress-section">
<div class="course-home-headings"> <div class="course-home-headings">
Your Progress Your Progress
</div> </div>
<div class="common-card-style progress-card"> <div class="common-card-style progress-card">
<p class="small-title"> <p class="small-title">
{% if progress != 100 %}
Great work so far! Great work so far!
{% else %}
Excellent Work on completing this course 👏
{% endif %}
</p> </p>
<p class="progress-text"> <p class="progress-text">
{% if progress != 100 %}
Challenge yourself to complete the lessons and grow professionally. Challenge yourself to complete the lessons and grow professionally.
{% else %}
You have reached a new level in your journey to success!
{% endif %}
</p> </p>
<div class="progress-percentage"> <div class="progress-percentage">
{{ frappe.utils.rounded(course.get_course_progress()) }}% {{ frappe.utils.rounded(progress) }}%
</div> </div>
<div class="progress"> <div class="progress">
<div class="progress-bar" role="progressbar" style="width: {{ course.get_course_progress() }}%" <div class="progress-bar" role="progressbar" style="width: {{ progress }}%"
aria-valuenow="{{ course.get_course_progress() }}" aria-valuemin="0" aria-valuemax="100"></div> aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -185,55 +196,3 @@
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% 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

@@ -18,3 +18,9 @@ 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
context.metatags = {
"title": course.title,
"image": course.image,
"description": course.short_introduction,
"keywords": course.title
}

View File

@@ -2,8 +2,6 @@
{% from "www/hackathons/macros/card.html" import null_card %} {% from "www/hackathons/macros/card.html" import null_card %}
{% block title %}{{ 'Courses' }}{% endblock %} {% block title %}{{ 'Courses' }}{% endblock %}
{% block head_include %} {% block head_include %}
<meta name="description" content="{{ 'Courses' }}" />
<meta name="keywords" content="Courses" />
<style> <style>
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -3,6 +3,12 @@ import frappe
def get_context(context): def get_context(context):
context.no_cache = 1 context.no_cache = 1
context.courses = get_courses() context.courses = get_courses()
context.metatags = {
"title": "All Courses",
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"description": "This page lists all the courses published on our website",
"keywords": "All Courses, Courses, Learn"
}
def get_courses(): def get_courses():
course_names = frappe.get_all("LMS Course", filters={"is_published": True}, pluck="name") course_names = frappe.get_all("LMS Course", filters={"is_published": True}, pluck="name")

View File

@@ -35,21 +35,24 @@
{% endif %} {% endif %}
<span class="social-icons"> <span class="social-icons">
{% if member.linkedin %} {% if member.linkedin %}
<a class="linkedin" href="{{ member.linkedin }}"> <a class="linkedin button-links" href="{{ member.linkedin }}">
<img src="/assets/community/images/linkedin.png"> <img src="/assets/community/images/linkedin.png">
</a> </a>
{% endif %} {% endif %}
{% if member.medium %} {% if member.medium %}
<a class="medium" href="{{ member.medium}}"> <a class="medium button-links" href="{{ member.medium}}">
<img src="/assets/community/icons/medium.svg"> <img src="/assets/community/icons/medium.svg">
</a> </a>
{% endif %} {% endif %}
{% if member.github %} {% if member.github %}
<a class="github" href="{{ member.github }}"> <a class="github button-links" href="{{ member.github }}">
<img src="/assets/community/icons/github.svg"> <img src="/assets/community/icons/github.svg">
</a> </a>
{% endif %} {% endif %}
</span> </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> </div>
</div> </div>