refactor: renamed app to lms
This commit is contained in:
0
lms/www/__init__.py
Normal file
0
lms/www/__init__.py
Normal file
0
lms/www/__pycache__/__init__.py
Normal file
0
lms/www/__pycache__/__init__.py
Normal file
0
lms/www/batch/__init__.py
Normal file
0
lms/www/batch/__init__.py
Normal file
0
lms/www/batch/__pycache__/__init__.py
Normal file
0
lms/www/batch/__pycache__/__init__.py
Normal file
72
lms/www/batch/join.html
Normal file
72
lms/www/batch/join.html
Normal file
@@ -0,0 +1,72 @@
|
||||
% extends "templates/base.html" %}
|
||||
{% block title %}Join a Course{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Join a Course"/>
|
||||
<meta name="keywords" content="" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if frappe.session.user == "Guest" %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>Login Required</span>
|
||||
</div>
|
||||
<div class=''>Please log in to confirm joining the course {{ batch.course_title }}.</div>
|
||||
<a type="submit" id="login" class="btn btn-primary w-100"
|
||||
href="/login?redirect-to=/courses/{{ batch.course }}/join?batch={{ batch.name }}">{{_("Login")}}</a>
|
||||
</div>
|
||||
|
||||
{% elif already_a_member %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>Already a member</span>
|
||||
</div>
|
||||
<div class=''>You are already a member of the batch {{ batch.title }} for the course {{ batch.course_title }}.
|
||||
</div>
|
||||
<a type="submit" id="batch-home" class="btn btn-primary w-100" href="">{{_("Go to Batch Home")}}</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="page-card">
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator blue password-box'>Confirm your membership</span>
|
||||
</div>
|
||||
<div>Please provide your confirmation to be a part of the batch {{ batch.title }} for the course
|
||||
{{ batch.course_title }}.
|
||||
</div>
|
||||
<a type="submit" id="confirm" class="btn btn-primary w-100">{{_("Confirm")}}</a>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#confirm").click((e) => {
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
|
||||
"args": {
|
||||
"batch": {{ batch.name }},
|
||||
"course": {{ batch.course }}
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint({
|
||||
message: __("You are now a member of this batch!"),
|
||||
clear: true
|
||||
});
|
||||
setTimeout(function () {
|
||||
window.location.href = "/courses/{{ batch.course }}/home";
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
8
lms/www/batch/join.py
Normal file
8
lms/www/batch/join.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
batch_name = frappe.form_dict["batch"]
|
||||
context.batch = frappe.get_doc("LMS Batch", batch_name)
|
||||
context.already_a_member = context.batch.is_member(frappe.session.user)
|
||||
context.batch.course_title = frappe.db.get_value("LMS Course", context.batch.course, "title")
|
||||
177
lms/www/batch/learn.html
Normal file
177
lms/www/batch/learn.html
Normal file
@@ -0,0 +1,177 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/macros/livecode.html" import LiveCodeEditorJS, LiveCodeEditor with context %}
|
||||
{% block title %} {{ lesson.title }} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
|
||||
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_header() }}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container course-details-page">
|
||||
{{ BreadCrumb(course, lesson) }}
|
||||
<div class="course-content-parent">
|
||||
<div class="course-details-outline">
|
||||
{{ widgets.CourseOutline(course=course, membership=membership) }}
|
||||
</div>
|
||||
<div class="lesson-pagination-parent">
|
||||
{{ LessonContent(lesson) }}
|
||||
{% if membership %}
|
||||
{{ pagination(prev_url, next_url) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ Discussions() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro BreadCrumb(course, lesson) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses">{{ course.title }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ lesson.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LessonContent(lesson) %}
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% set is_instructor = is_instructor(course.name) %}
|
||||
<div class="lesson-content">
|
||||
<div class="lesson-title">
|
||||
<div class="course-home-headings title mb-0
|
||||
{% if membership %} is-member {% endif %}
|
||||
{% if membership or is_instructor %} eligible-for-submission {% endif %}" data-lesson="{{ lesson.name }}"
|
||||
data-course="{{ course.name }}">{{ lesson.title }}</div>
|
||||
<span class="lesson-progress {{hide if get_progress(course.name, lesson.name) != 'Complete' else ''}}">COMPLETED</span>
|
||||
|
||||
{% if is_instructor %}
|
||||
<a class="button is-default button-links ml-auto" href="/lesson?name={{ lesson.name }}"> {{ _("Edit") }} </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
{% set ins_len = instructors | length %}
|
||||
{% for instructor in instructors %}
|
||||
{% if ins_len > 1 and loop.index == 1 %}
|
||||
<div class="avatar-group overlap">
|
||||
{% endif %}
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
|
||||
{% if ins_len > 1 and loop.index == ins_len %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<a class="button-links ml-1" href="{{ get_profile_url(instructors[0].username) }}">
|
||||
<span class="course-meta">
|
||||
{% if ins_len == 1 %}
|
||||
{{ instructors[0].full_name }}
|
||||
{% else %}
|
||||
{% set suffix = "other" if ins_len - 1 == 1 else "others" %}
|
||||
{{ instructors[0].full_name.split(" ")[0] }} and {{ ins_len - 1 }} {{ suffix }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-5 course-meta"> {{ frappe.utils.format_date(lesson.creation, "medium") }} </div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="markdown-source lesson-content-card">
|
||||
{% if membership or lesson.include_in_preview or is_instructor %}
|
||||
{% if is_instructor and not lesson.include_in_preview %}
|
||||
<div class="small alert alert-secondary alert-dismissible mt-4 mb-4">
|
||||
This lesson is not available for preview. As you are the Instructor of the course only you can see it.
|
||||
<a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ render_html(lesson.body) }}
|
||||
{% else %}
|
||||
<div class="">
|
||||
<a class="button is-primary pull-right" href="/courses/{{ course.name }}"> Start Learning </a>
|
||||
<div class="">This lesson is not available for preview. Please join the course to access it.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro pagination(prev_url, next_url) %}
|
||||
<div class="lesson-pagination">
|
||||
|
||||
<div>
|
||||
{% if prev_url %}
|
||||
<a class="button is-secondary dark-links prev" href="{{ prev_url }}">
|
||||
<img class="mr-2" src="/assets/lms/icons/left-arrow.svg">
|
||||
Prev
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% if not is_mentor(course.name, frappe.session.user) and membership %}
|
||||
{% set progress = get_progress(course.name, lesson.name) %}
|
||||
<div class="custom-checkbox {% if progress == 'Complete' %} hide {% endif %}">
|
||||
<label class="quiz-label">
|
||||
<input class="mark-progress" type="checkbox" checked>
|
||||
<img class="empty-checkbox" />
|
||||
<span class="small">{{ _("Mark as complete on moving to the next lesson") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="button is-secondary mark-progress {{ progress }} {% if progress == 'Incomplete' or progress == None %} hide {% endif %}"
|
||||
data-progress="Incomplete">
|
||||
Mark as Incomplete
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<a class="button is-primary next {% if membership.progress|int == 100 and not next_url %} hide {% endif %}"
|
||||
{% if next_url %} data-href="{{ next_url }}" {% endif %} href="">
|
||||
{% if next_url %} Next {% else %} Mark as Complete {% endif %}
|
||||
<img class="ml-2" src="/assets/lms/icons/side-arrow-white.svg">
|
||||
</a>
|
||||
{% if course.enable_certification %}
|
||||
<div class="button is-primary {% if membership.progress|int != 100 or next_url %} hide {% endif %}" id="certification">
|
||||
Get Certificate
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Discussions() %}
|
||||
{% set is_instructor = frappe.session.user == course.instructor %}
|
||||
{% set condition = is_instructor if is_instructor else membership %}
|
||||
{% set doctype, docname = "Course Lesson", lesson.name %}
|
||||
{% set title = "Questions" %}
|
||||
{% set cta_title = "Ask a Question" %}
|
||||
{% set button_name = "Start Learning" %}
|
||||
{% set redirect_to = "/courses/" + course.name %}
|
||||
{% set empty_state_title = "Have a doubt?" %}
|
||||
{% set empty_state_subtitle = "Post it here, our mentors will help you out." %}
|
||||
|
||||
{% include "frappe/templates/discussions/discussions_section.html" %}
|
||||
{% endmacro %}
|
||||
|
||||
{%- block script %}
|
||||
{{ super() }}
|
||||
|
||||
<script type="text/javascript">
|
||||
var page_context = {{ page_context | tojson }};
|
||||
</script>
|
||||
|
||||
{% for ext in page_extensions %}
|
||||
{{ ext.render_footer() }}
|
||||
{% endfor %}
|
||||
{%- endblock %}
|
||||
383
lms/www/batch/learn.js
Normal file
383
lms/www/batch/learn.js
Normal file
@@ -0,0 +1,383 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
localStorage.removeItem($("#quiz-title").text());
|
||||
fetch_assignments();
|
||||
|
||||
save_current_lesson();
|
||||
|
||||
$(".option").click((e) => {
|
||||
enable_check(e);
|
||||
})
|
||||
|
||||
$(".mark-progress").click((e) => {
|
||||
mark_progress(e);
|
||||
});
|
||||
|
||||
$(".next").click((e) => {
|
||||
mark_progress(e);
|
||||
});
|
||||
|
||||
$("#summary").click((e) => {
|
||||
quiz_summary(e);
|
||||
});
|
||||
|
||||
$("#check").click((e) => {
|
||||
check_answer(e);
|
||||
});
|
||||
|
||||
$("#next").click((e) => {
|
||||
mark_active_question(e);
|
||||
});
|
||||
|
||||
$("#try-again").click((e) => {
|
||||
try_quiz_again(e);
|
||||
});
|
||||
|
||||
$("#certification").click((e) => {
|
||||
create_certificate(e);
|
||||
});
|
||||
|
||||
$(".submit-work").click((e) => {
|
||||
attach_work(e);
|
||||
});
|
||||
|
||||
$(".clear-work").click((e) => {
|
||||
clear_work(e);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const save_current_lesson = () => {
|
||||
if ($(".title").hasClass("is-member")) {
|
||||
frappe.call("lms.lms.api.save_current_lesson", {
|
||||
course_name: $(".title").attr("data-course"),
|
||||
lesson_name: $(".title").attr("data-lesson")
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const enable_check = (e) => {
|
||||
if ($(".option:checked").length) {
|
||||
$("#check").removeAttr("disabled");
|
||||
$(".custom-checkbox").removeClass("active-option");
|
||||
$(".option:checked").closest(".custom-checkbox").addClass("active-option");
|
||||
}
|
||||
};
|
||||
|
||||
const 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");
|
||||
$(".explanation").addClass("hide");
|
||||
};
|
||||
|
||||
const mark_progress = (e) => {
|
||||
/* Prevent default only for Next button anchor tag and not for progress checkbox */
|
||||
if ($(e.currentTarget).prop("nodeName") != "INPUT")
|
||||
e.preventDefault();
|
||||
else
|
||||
return
|
||||
|
||||
const target = $(e.currentTarget).attr("data-progress") ? $(e.currentTarget) : $("input.mark-progress");
|
||||
const current_status = $(".lesson-progress").hasClass("hide") ? "Incomplete": "Complete";
|
||||
|
||||
let status = "Incomplete";
|
||||
if (target.prop("nodeName") == "INPUT" && target.prop("checked")) {
|
||||
status = "Complete";
|
||||
}
|
||||
|
||||
if (status != current_status) {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.course_lesson.course_lesson.save_progress",
|
||||
args: {
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
course: $(".title").attr("data-course"),
|
||||
status: status
|
||||
},
|
||||
callback: (data) => {
|
||||
change_progress_indicators(status, e);
|
||||
show_certificate_if_course_completed(data);
|
||||
move_to_next_lesson(status, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
move_to_next_lesson(e);
|
||||
};
|
||||
|
||||
const change_progress_indicators = (status, e) => {
|
||||
if (status == "Complete") {
|
||||
$(".lesson-progress").removeClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").removeClass("hide");
|
||||
}
|
||||
else {
|
||||
$(".lesson-progress").addClass("hide");
|
||||
$(".active-lesson .lesson-progress-tick").addClass("hide");
|
||||
}
|
||||
if (status == "Incomplete" && !$(e.currentTarget).hasClass("next")) {
|
||||
$(e.currentTarget).addClass("hide");
|
||||
$("input.mark-progress").prop("checked", false).closest(".custom-checkbox").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
const show_certificate_if_course_completed = (data) => {
|
||||
if (data.message == 100 && !$(".next").attr("data-next") && $("#certification").hasClass("hide")) {
|
||||
$("#certification").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
const move_to_next_lesson = (status, e) => {
|
||||
if ($(e.currentTarget).hasClass("next") && $(e.currentTarget).attr("data-href")) {
|
||||
window.location.href = $(e.currentTarget).attr("data-href");
|
||||
}
|
||||
else if (status == "Complete") {
|
||||
$("input.mark-progress").closest(".custom-checkbox").addClass("hide");
|
||||
$("div.mark-progress").removeClass("hide");
|
||||
$(".next").addClass("hide");
|
||||
}
|
||||
else {
|
||||
$("input.mark-progress").closest(".custom-checkbox").removeClass("hide");
|
||||
$("div.mark-progress").addClass("hide");
|
||||
$(".next").removeClass("hide");
|
||||
}
|
||||
};
|
||||
|
||||
const quiz_summary = (e) => {
|
||||
e.preventDefault();
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_quiz.lms_quiz.quiz_summary",
|
||||
args: {
|
||||
"quiz": quiz_name,
|
||||
"results": localStorage.getItem(quiz_name)
|
||||
},
|
||||
callback: (data) => {
|
||||
var message = data.message == total_questions ? "Excellent Work" : "You were almost there."
|
||||
$(".question").addClass("hide");
|
||||
$("#summary").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");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const try_quiz_again = (e) => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const check_answer = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
var quiz_name = $("#quiz-title").text();
|
||||
var total_questions = $(".question").length;
|
||||
var current_index = $(".active-question").attr("data-qt-index");
|
||||
|
||||
$(".explanation").removeClass("hide");
|
||||
$("#check").addClass("hide");
|
||||
|
||||
if (current_index == total_questions) {
|
||||
if ($(".eligible-for-submission").length) {
|
||||
$("#summary").removeClass("hide")
|
||||
}
|
||||
else {
|
||||
$("#submission-message").removeClass("hide");
|
||||
}
|
||||
}
|
||||
else {
|
||||
$("#next").removeClass("hide")
|
||||
}
|
||||
|
||||
var [answer, is_correct] = parse_options();
|
||||
add_to_local_storage(quiz_name, current_index, answer, is_correct)
|
||||
};
|
||||
|
||||
const 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];
|
||||
};
|
||||
|
||||
const add_icon = (element, icon) => {
|
||||
$(element).closest(".custom-checkbox").removeClass("active-option");
|
||||
var label = $(element).siblings(".option-text").text();
|
||||
$(element).parent().empty().html(`<div class="option-text"><img class="mr-3" src="/assets/lms/icons/${icon}.svg"> ${label}</div>`);
|
||||
};
|
||||
|
||||
const 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))
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(".title").attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certification.lms_certification.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const attach_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
let files = target.siblings(".attach-file").prop("files")
|
||||
if (files && files.length) {
|
||||
files = add_files(files)
|
||||
return_as_dataurl(files)
|
||||
files.map((file) => {
|
||||
upload_file(file, target);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const upload_file = (file, target) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200) {
|
||||
let response = JSON.parse(xhr.responseText)
|
||||
create_lesson_work(response.message, target);
|
||||
} else if (xhr.status === 403) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
frappe.msgprint(`Not permitted. ${response._error_message || ''}`);
|
||||
|
||||
} else if (xhr.status === 413) {
|
||||
frappe.msgprint('Size exceeds the maximum allowed file size.');
|
||||
|
||||
} else {
|
||||
frappe.msgprint(xhr.status === 0 ? 'XMLHttpRequest Error' : `${xhr.status} : ${xhr.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('POST', '/api/method/upload_file', true);
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
xhr.setRequestHeader('X-Frappe-CSRF-Token', frappe.csrf_token);
|
||||
|
||||
let form_data = new FormData();
|
||||
if (file.file_obj) {
|
||||
form_data.append('file', file.file_obj, `${frappe.session.user}-${file.name}`);
|
||||
form_data.append('folder', `${$(".title").attr("data-lesson")} ${$(".title").attr("data-course")}`)
|
||||
}
|
||||
|
||||
xhr.send(form_data);
|
||||
});
|
||||
}
|
||||
|
||||
const create_lesson_work = (file, target) => {
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.upload_assignment",
|
||||
args: {
|
||||
assignment: file.file_url,
|
||||
lesson: $(".title").attr("data-lesson"),
|
||||
identifier: target.siblings(".attach-file").attr("id")
|
||||
},
|
||||
callback: (data) => {
|
||||
target.siblings(".attach-file").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", file.file_url).text(file.file_name)
|
||||
target.addClass("hide");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const return_as_dataurl = (files) => {
|
||||
let promises = files.map(file =>
|
||||
frappe.dom.file_to_base64(file.file_obj)
|
||||
.then(dataurl => {
|
||||
file.dataurl = dataurl;
|
||||
this.on_success && this.on_success(file);
|
||||
})
|
||||
);
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
const add_files = (files) => {
|
||||
files = Array.from(files).map(file => {
|
||||
let is_image = file.type.startsWith('image');
|
||||
return {
|
||||
file_obj: file,
|
||||
cropper_file: file,
|
||||
crop_box_data: null,
|
||||
optimize: this.attach_doc_image ? true : false,
|
||||
name: file.name,
|
||||
doc: null,
|
||||
progress: 0,
|
||||
total: 0,
|
||||
failed: false,
|
||||
request_succeeded: false,
|
||||
error_message: null,
|
||||
uploading: false,
|
||||
private: !is_image
|
||||
}
|
||||
});
|
||||
return files
|
||||
};
|
||||
|
||||
const clear_work = (e) => {
|
||||
const target = $(e.currentTarget);
|
||||
const parent = target.closest(".preview-work");
|
||||
parent.addClass("hide");
|
||||
parent.siblings(".attach-file").removeClass("hide").val(null);
|
||||
parent.siblings(".submit-work").removeClass("hide");
|
||||
};
|
||||
|
||||
const fetch_assignments = () => {
|
||||
if ($(".attach-file").length <= 0)
|
||||
return;
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lesson_assignment.lesson_assignment.get_assignment",
|
||||
args: {
|
||||
"lesson": $(".title").attr("data-lesson")
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message && data.message.length) {
|
||||
const assignments = data.message;
|
||||
for (let i in assignments) {
|
||||
let target = $(`#${assignments[i]["id"]}`);
|
||||
target.addClass("hide");
|
||||
target.siblings(".submit-work").addClass("hide");
|
||||
target.siblings(".preview-work").removeClass("hide");
|
||||
target.siblings(".preview-work").find("a").attr("href", assignments[i]["assignment"]).text(assignments[i]["file_name"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
69
lms/www/batch/learn.py
Normal file
69
lms/www/batch/learn.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from re import I
|
||||
import frappe
|
||||
from lms.www.utils import get_common_context, redirect_to_lesson
|
||||
from lms.lms.utils import get_lesson_url
|
||||
from frappe.utils import cstr, flt
|
||||
from lms.www import batch
|
||||
|
||||
def get_context(context):
|
||||
get_common_context(context)
|
||||
|
||||
chapter_index = frappe.form_dict.get("chapter")
|
||||
lesson_index = frappe.form_dict.get("lesson")
|
||||
lesson_number = f"{chapter_index}.{lesson_index}"
|
||||
if not chapter_index or not lesson_index:
|
||||
if context.batch:
|
||||
index_ = get_lesson_index(context.course, context.batch, frappe.session.user) or "1.1"
|
||||
else:
|
||||
index_ = "1.1"
|
||||
redirect_to_lesson(context.course, index_)
|
||||
|
||||
context.lesson = get_current_lesson_details(lesson_number, context)
|
||||
neighbours = get_neighbours(lesson_number, context.lessons)
|
||||
context.next_url = get_url(neighbours["next"], context.course)
|
||||
context.prev_url = get_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)
|
||||
context.page_context = {
|
||||
"course": context.course.name,
|
||||
"batch": context.get("batch") and context.batch.name,
|
||||
"lesson": context.lesson.name,
|
||||
"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):
|
||||
redirect_to_lesson(context.course)
|
||||
return details_list[0]
|
||||
|
||||
def get_url(lesson_number, course):
|
||||
return get_lesson_url(course.name, lesson_number) and get_lesson_url(course.name, lesson_number) + course.query_parameter
|
||||
|
||||
def get_lesson_index(course, batch, user):
|
||||
lesson = batch.get_current_lesson(user)
|
||||
return lesson and course.get_lesson_index(lesson)
|
||||
|
||||
def get_page_extensions(context):
|
||||
default_value = ["lms.plugins.PageExtension"]
|
||||
classnames = frappe.get_hooks("lms_lesson_page_extensions") or default_value
|
||||
extensions = [frappe.get_attr(name)() for name in classnames]
|
||||
for e in extensions:
|
||||
e.set_context(context)
|
||||
return extensions
|
||||
|
||||
def get_neighbours(current, lessons):
|
||||
current = flt(current)
|
||||
numbers = sorted(lesson.number for lesson in lessons)
|
||||
index = numbers.index(current)
|
||||
return {
|
||||
"prev": numbers[index-1] if index-1 >= 0 else None,
|
||||
"next": numbers[index+1] if index+1 < len(numbers) else None
|
||||
}
|
||||
0
lms/www/cohorts/__init__.py
Normal file
0
lms/www/cohorts/__init__.py
Normal file
34
lms/www/cohorts/base.html
Normal file
34
lms/www/cohorts/base.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "templates/base.html" %}
|
||||
|
||||
{% macro render_nav(nav) %}
|
||||
|
||||
<div class="breadcrumb">
|
||||
{% for link in nav %}
|
||||
<a class="dark-links" href="{{ link.href }}">{{ link.title }}</a>
|
||||
{% if not loop.last %}
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block title %}Cohorts{% endblock %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="Cohorts" />
|
||||
<meta name="keywords" content="Cohorts" />
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class='container'>
|
||||
{{ render_nav(nav | default([])) }}
|
||||
|
||||
{% block page_content %}
|
||||
Hello, world!
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
77
lms/www/cohorts/cohort.html
Normal file
77
lms/www/cohorts/cohort.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
{% block title %}Manage {{ course.title }}{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<h2>{{cohort.title}} <span class="badge badge-secondary">Cohort</span></h2>
|
||||
|
||||
<p>
|
||||
{% set stats = cohort.get_stats() %}
|
||||
|
||||
{{ stats.subgroups }} Subgroups
|
||||
| {{ stats.mentors }} Mentors
|
||||
| {{ stats.students }} students
|
||||
| {{ stats.join_requests }} join requests
|
||||
</p>
|
||||
|
||||
{% if is_mentor %}
|
||||
<div class="alert alert-info">
|
||||
{% set sg = mentor.get_subgroup() %}
|
||||
<p>You are a mentor of <b>{{sg.title}}</b> subgroup.</p>
|
||||
<p><a href="{{sg.get_url()}}" class="btn btn-primary">Visit Your Subgroup →</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{% set num_subgroups = cohort.get_subgroups() | length %}
|
||||
{{ render_navitem("Subgroups", "", page=page, count=num_subgroups) }}
|
||||
{% for p in cohort.get_pages(scope="Cohort") %}
|
||||
{{ render_navitem(p.title, p.slug, page=page) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="my-5">
|
||||
{% if not page %}
|
||||
{{ render_subgroups() }}
|
||||
{% else %}
|
||||
{{ render_page(page) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_subgroups() %}
|
||||
<ul class="list-group">
|
||||
{% for sg in cohort.get_subgroups(include_counts=True) %}
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<a class="subgroup-title"
|
||||
style="font-weight: 700; color: inherit;"
|
||||
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{sg.slug}}"
|
||||
>{{sg.title}}</a>
|
||||
</div>
|
||||
<div style="font-size: 0.8em;">
|
||||
{{sg.num_mentors}} Mentors
|
||||
|
|
||||
{{sg.num_students}} Students
|
||||
|
|
||||
{{sg.num_join_requests}} Join Requests
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_navitem(title, link, page, count=-1) %}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {{ 'active' if link==page }}"
|
||||
href="/courses/{{course.name}}/cohorts/{{cohort.slug}}/{{link}}"
|
||||
>{{title}}
|
||||
{% if count != -1 %}
|
||||
<span
|
||||
class="badge {{'badge-primary' if link==page else 'badge-secondary'}}"
|
||||
>{{count}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
32
lms/www/cohorts/cohort.py
Normal file
32
lms/www/cohorts/cohort.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course = utils.get_course()
|
||||
cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"])
|
||||
if not cohort:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
user = frappe.session.user
|
||||
mentor = cohort.get_mentor(user)
|
||||
is_mentor = mentor is not None
|
||||
is_admin = cohort.is_admin(user) or "System Manager" in frappe.get_roles()
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, "/courses/" + course.name)
|
||||
utils.add_nav(context, "Cohorts", "/courses/" + course.name + "/manage")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.mentor = mentor
|
||||
context.is_mentor = is_mentor
|
||||
context.is_admin = is_admin
|
||||
context.page = frappe.form_dict.get("page") or ""
|
||||
context.page_scope = "Cohort"
|
||||
|
||||
# Function to render to custom page given the slug
|
||||
context.render_page = lambda page: frappe.render_template(
|
||||
cohort.get_page_template(page, scope="Cohort"),
|
||||
context)
|
||||
38
lms/www/cohorts/index.html
Normal file
38
lms/www/cohorts/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
{% block title %}Manage {{ course.title }}{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
{% if cohorts %}
|
||||
<h2>Cohorts</h2>
|
||||
<div class="row">
|
||||
{% for cohort in cohorts %}
|
||||
<div class="col-md-6">
|
||||
{{ render_cohort(course, cohort) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h2>Permission Denied</h2>
|
||||
<p>You don't have permission to manage this course.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_cohort(course, cohort) %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{cohort.title}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{cohort.begin_date}} - {{cohort.end_date}}</h6>
|
||||
<p>
|
||||
{% set stats = cohort.get_stats() %}
|
||||
|
||||
{{ stats.subgroups }} Subgroups
|
||||
| {{ stats.mentors }} Mentors
|
||||
| {{ stats.students }} students
|
||||
| {{ stats.join_requests }} join requests
|
||||
</p>
|
||||
|
||||
<a href="/courses/{{course.name}}/cohorts/{{cohort.slug}}" class="card-link">Manage</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
31
lms/www/cohorts/index.py
Normal file
31
lms/www/cohorts/index.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
from .utils import get_course, add_nav
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.course = get_course()
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.flags.redirect_location = "/login?redirect-to=" + frappe.request.path
|
||||
raise frappe.Redirect()
|
||||
|
||||
if not context.course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
context.cohorts = get_cohorts(context.course)
|
||||
if len(context.cohorts) == 1:
|
||||
frappe.local.flags.redirect_location = context.cohorts[0].get_url()
|
||||
raise frappe.Redirect
|
||||
|
||||
add_nav(context, "All Courses", "/courses")
|
||||
add_nav(context, context.course.title, "/courses/" + context.course.name)
|
||||
|
||||
def get_cohorts(course):
|
||||
if "System Manager" in frappe.get_roles():
|
||||
return course.get_cohorts()
|
||||
|
||||
staff_roles = frappe.get_all("Cohort Staff", filters={"course": course.name}, fields=["cohort"])
|
||||
mentor_roles = frappe.get_all("Cohort Mentor", filters={"course": course.name}, fields=["cohort"])
|
||||
roles = staff_roles + mentor_roles
|
||||
names = {role.cohort for role in roles}
|
||||
return [frappe.get_doc("Cohort", name) for name in names]
|
||||
88
lms/www/cohorts/join.html
Normal file
88
lms/www/cohorts/join.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
|
||||
{% block title %}Join Course{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<h2>Join Course</h2>
|
||||
|
||||
<p>
|
||||
Course: {{course.title}}
|
||||
</p>
|
||||
<p>
|
||||
Cohort: {{cohort.title}}
|
||||
</p>
|
||||
<p>
|
||||
Subgroup: {{subgroup.title}}
|
||||
</p>
|
||||
|
||||
{% if frappe.session.user == "Guest" %}
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Please login to be able to join the course.</p>
|
||||
|
||||
<p>
|
||||
If you don't already have an account, you can <a href="/login#signup">sign up for a new account</a>.
|
||||
</p>
|
||||
<a class="btn btn-primary" href="/login">Login to continue</a>
|
||||
</div>
|
||||
{% elif subgroup.has_student(frappe.session.user) %}
|
||||
<div class="alert alert-info">
|
||||
<p>You are already a student of this course.</p>
|
||||
<a class="btn btn-primary" href="/">Start Learning →</a>
|
||||
</div>
|
||||
{% elif subgroup.has_join_request(frappe.session.user) %}
|
||||
<div class="alert alert-info">
|
||||
<p>We have received your request to join the course. You'll hear back from us soon.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<a class="btn btn-primary" id="join">Join the course</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
console.log("ready!")
|
||||
$("#join").click(function() {
|
||||
var parts = window.location.pathname.split("/")
|
||||
var course = parts[2];
|
||||
var cohort = parts[4];
|
||||
var subgroup = parts[5];
|
||||
var invite_code = parts[6];
|
||||
|
||||
frappe.call('lms.lms.api.join_cohort', {
|
||||
course: course,
|
||||
cohort: cohort,
|
||||
subgroup: subgroup,
|
||||
invite_code: invite_code
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "Notification",
|
||||
primary_action_label: "Proceed",
|
||||
primary_action() {
|
||||
d.hide();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
var message = "We've received your interest to join the course. We'll hear from us soon.";
|
||||
d.show();
|
||||
d.set_message(message);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
24
lms/www/cohorts/join.py
Normal file
24
lms/www/cohorts/join.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
course = utils.get_course(frappe.form_dict["course"])
|
||||
cohort = course and utils.get_cohort(course, frappe.form_dict["cohort"])
|
||||
subgroup = cohort and utils.get_subgroup(cohort, frappe.form_dict["subgroup"])
|
||||
if not subgroup:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
invite_code = frappe.form_dict["invite_code"]
|
||||
if subgroup.invite_code != invite_code:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, "/courses/" + course.name)
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.subgroup = subgroup
|
||||
264
lms/www/cohorts/subgroup.html
Normal file
264
lms/www/cohorts/subgroup.html
Normal file
@@ -0,0 +1,264 @@
|
||||
{% extends "www/cohorts/base.html" %}
|
||||
{% block title %} Subgroup {{subgroup.title}} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<h2 id="page-title"
|
||||
data-subgroup="{{subgroup.name}}"
|
||||
data-title="{{subgroup.title}}"
|
||||
>{{subgroup.title}} <span class="badge badge-secondary">Subgroup</span></h2>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
{{ render_navitem("Mentors", "/mentors", stats.mentors, page=="mentors")}}
|
||||
{{ render_navitem("Students", "/students", stats.students, page=="students")}}
|
||||
{% if is_mentor or is_admin %}
|
||||
{{ render_navitem("Join Requests", "/join-requests", stats.join_requests, page=="join-requests")}}
|
||||
|
||||
{% for p in cohort.get_pages(scope="Subgroup") %}
|
||||
{{ render_navitem(p.title, "/" + p.slug, -1, page==p.slug) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if is_admin %}
|
||||
{{ render_navitem("Admin", "/admin", -1, page=="admin")}}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="my-5">
|
||||
{% if page == "info" %}
|
||||
{{ render_info() }}
|
||||
{% elif page == "mentors" %}
|
||||
{{ render_mentors() }}
|
||||
{% elif page == "students" %}
|
||||
{{ render_students() }}
|
||||
{% elif page == "join-requests" %}
|
||||
{{ render_join_requests() }}
|
||||
{% elif page == "admin" %}
|
||||
{{ render_admin() }}
|
||||
{% else %}
|
||||
{{ render_page(page) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro render_admin() %}
|
||||
<div style="background: white; padding: 20px;">
|
||||
<h5>Add a new mentor</h5>
|
||||
<form id="add-mentor-form">
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control" id="mentor-email" aria-describedby="emailHelp" placeholder="E-mail address">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="add-mentor">Add Mentor</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_mentors() %}
|
||||
<h5>Mentors</h5>
|
||||
{% set mentors = subgroup.get_mentors() %}
|
||||
{% if mentors %}
|
||||
<div class="member-parent">
|
||||
{% for m in mentors %}
|
||||
{{ widgets.MemberCard(member=m, show_course_count=False) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<em>None found.</em>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_students() %}
|
||||
{% set students = subgroup.get_students() %}
|
||||
{% if students %}
|
||||
<div class="member-parent">
|
||||
{% for student in students %}
|
||||
{{ widgets.MemberCard(member=student, show_course_count=False) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<em>None found.</em>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro render_join_requests() %}
|
||||
<h5>Invite Link</h5>
|
||||
{% set link = subgroup.get_invite_link() %}
|
||||
<p><a href="{{ link }}" id="invite-link">{{link}}</a>
|
||||
<br>
|
||||
<a class="btn btn-seconday btn-sm" id="copy-to-clipboard">Copy to Clipboard</a>
|
||||
</p>
|
||||
|
||||
{% set join_requests = subgroup.get_join_requests() %}
|
||||
<h5>Pending Requests</h5>
|
||||
{% if join_requests %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>When</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for r in join_requests %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td class="timestamp">{{r.creation}}</td>
|
||||
<td>{{r.email}}</td>
|
||||
<td class="actions"
|
||||
data-name="{{r.name}}"
|
||||
data-email="{{r.email}}">
|
||||
<a class="action-approve" href="#">Approve</a> | <a class="action-reject" href="#">Reject</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>There are no pending join requests.</em></p>
|
||||
{% endif %}
|
||||
{% set rejected_requests = subgroup.get_join_requests(status="Rejected") %}
|
||||
|
||||
<h5>Rejected Requests</h5>
|
||||
{% if rejected_requests %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>When</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{% for r in rejected_requests %}
|
||||
<tr>
|
||||
<td>{{loop.index}}</td>
|
||||
<td class="timestamp">{{r.creation}}</td>
|
||||
<td>{{r.email}}</td>
|
||||
<td class="actions"
|
||||
data-name="{{r.name}}"
|
||||
data-email="{{r.email}}">
|
||||
<a class="action-undo" href="#">Undo</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>There are no rejected requests.</em></p>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_navitem(title, link, count, active) %}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link {{ 'active' if active }}"
|
||||
href="/courses/{{course.name}}/subgroups/{{cohort.slug}}/{{subgroup.slug}}{{link}}"
|
||||
>{{title}}
|
||||
{% if count != -1 %}
|
||||
<span
|
||||
class="badge {{'badge-primary' if active else 'badge-secondary'}}"
|
||||
>{{count}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#copy-to-clipboard").click(function() {
|
||||
var invite_link = $("#invite-link").text();
|
||||
navigator.clipboard.writeText(invite_link)
|
||||
.then(() => {
|
||||
$("#copy-to-clipboard").text("Copied!");
|
||||
setTimeout(
|
||||
() => $("#copy-to-clipboard").text("Copy to Clipboard"),
|
||||
500);
|
||||
});
|
||||
});
|
||||
|
||||
$(".timestamp"). each(function() {
|
||||
var t = moment($(this).text());
|
||||
var dt = t.from(moment.now());
|
||||
$(this).text(dt);
|
||||
});
|
||||
|
||||
$(".action-approve").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
|
||||
frappe.confirm(
|
||||
`Are you sure to accept ${email} to this subgroup?`,
|
||||
function() {
|
||||
run_action("lms.lms.api.approve_cohort_join_request", name, el, "approved", "Approved");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(".action-reject").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
frappe.confirm(`Are you sure to reject <strong>${email}</strong> from joining this subgroup?`, function() {
|
||||
run_action("lms.lms.api.reject_cohort_join_request", name, el, "rejected", "Rejected!");
|
||||
});
|
||||
});
|
||||
|
||||
$(".action-undo").click(function() {
|
||||
var el = $(this).parent().parent();
|
||||
var name = $(this).parent().data("name");
|
||||
var email = $(this).parent().data("email");
|
||||
frappe.confirm(`Are you sure to undo the rejection of <strong>${email}</strong>?`, function() {
|
||||
run_action("lms.lms.api.undo_reject_cohort_join_request", name, el, "undo-reject", "Reject Undone!");
|
||||
});
|
||||
});
|
||||
|
||||
function run_action(method, join_request, elem, classname, label) {
|
||||
frappe.call(method, {
|
||||
join_request: join_request,
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
$(elem)
|
||||
.addClass(classname)
|
||||
.find("td.actions").html(label);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#add-mentor").click(function() {
|
||||
var subgroup = $("#page-title").data("subgroup");
|
||||
var title = $("#page-title").data("title");
|
||||
var email = $("#mentor-email").val();
|
||||
frappe.call("lms.lms.api.add_mentor_to_subgroup", {
|
||||
subgroup: subgroup,
|
||||
email: email
|
||||
})
|
||||
.then(r => {
|
||||
if (r.message.ok) {
|
||||
frappe.msgprint(`Successfully added ${email} as mentor to ${title}`);
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(r.message.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style type="text/css">
|
||||
tr.approved {
|
||||
background:#c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
tr.rejected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
tr.undo-reject {
|
||||
background:#d6d8d9;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
67
lms/www/cohorts/subgroup.py
Normal file
67
lms/www/cohorts/subgroup.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import frappe
|
||||
from . import utils
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
course = utils.get_course()
|
||||
|
||||
cohort = utils.get_cohort(course, frappe.form_dict['cohort'])
|
||||
subgroup = utils.get_subgroup(cohort, frappe.form_dict['subgroup'])
|
||||
|
||||
if not subgroup:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
|
||||
page = frappe.form_dict.get("page")
|
||||
is_mentor = subgroup.is_mentor(frappe.session.user)
|
||||
is_admin = cohort.is_admin(frappe.session.user) or "System Manager" in frappe.get_roles()
|
||||
|
||||
if is_admin:
|
||||
role = "Admin"
|
||||
elif is_mentor:
|
||||
role = "Mentor"
|
||||
else:
|
||||
role = "Public"
|
||||
|
||||
pages = [
|
||||
("mentors", ["Admin", "Mentor", "Public"]),
|
||||
("students", ["Admin", "Mentor", "Public"]),
|
||||
("join-requests", ["Admin", "Mentor"]),
|
||||
("admin", ["Admin"])
|
||||
]
|
||||
pages += [(p.slug, ["Admin", "Mentor"]) for p in cohort.get_pages(scope="Subgroup")]
|
||||
|
||||
page_names = [p for p, roles in pages if role in roles]
|
||||
|
||||
if page not in page_names:
|
||||
frappe.local.flags.redirect_location = subgroup.get_url() + "/mentors"
|
||||
raise frappe.Redirect
|
||||
|
||||
utils.add_nav(context, "All Courses", "/courses")
|
||||
utils.add_nav(context, course.title, f"/courses/{course.name}")
|
||||
utils.add_nav(context, "Cohorts", f"/courses/{course.name}/manage")
|
||||
utils.add_nav(context, cohort.title, f"/courses/{course.name}/cohorts/{cohort.slug}")
|
||||
|
||||
context.course = course
|
||||
context.cohort = cohort
|
||||
context.subgroup = subgroup
|
||||
context.stats = get_stats(subgroup)
|
||||
context.page = page
|
||||
context.is_admin = is_admin
|
||||
context.is_mentor = is_mentor
|
||||
context.page_scope = "Subgroup"
|
||||
|
||||
# Function to render to custom page given the slug
|
||||
context.render_page = lambda page: frappe.render_template(
|
||||
cohort.get_page_template(page, scope="Subgroup"),
|
||||
context)
|
||||
|
||||
def get_stats(subgroup):
|
||||
return {
|
||||
"join_requests": len(subgroup.get_join_requests()),
|
||||
"students": len(subgroup.get_students()),
|
||||
"mentors": len(subgroup.get_mentors())
|
||||
}
|
||||
|
||||
def has_page(cohort, page):
|
||||
return cohort.get_page(page, scope="Subgroup")
|
||||
25
lms/www/cohorts/utils.py
Normal file
25
lms/www/cohorts/utils.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import frappe
|
||||
|
||||
def get_course(course_name=None):
|
||||
course_name = course_name or frappe.form_dict["course"]
|
||||
return course_name and get_doc("LMS Course", course_name)
|
||||
|
||||
def get_doc(doctype, name):
|
||||
try:
|
||||
return frappe.get_doc(doctype, name)
|
||||
except frappe.exceptions.DoesNotExistError:
|
||||
return
|
||||
|
||||
def get_cohort(course, cohort_slug):
|
||||
name = frappe.get_value("Cohort", {"course": course.name, "slug": cohort_slug})
|
||||
return name and frappe.get_doc("Cohort", name)
|
||||
|
||||
def get_subgroup(cohort, subgroup_slug):
|
||||
name = frappe.get_value("Cohort Subgroup", {"cohort": cohort.name, "slug": subgroup_slug})
|
||||
return name and frappe.get_doc("Cohort Subgroup", name)
|
||||
|
||||
def add_nav(context, title, href):
|
||||
"""Adds a breadcrumb to the navigation.
|
||||
"""
|
||||
nav = context.setdefault("nav", [])
|
||||
nav.append({"title": title, "href": href})
|
||||
0
lms/www/community/__init__.py
Normal file
0
lms/www/community/__init__.py
Normal file
35
lms/www/community/index.html
Normal file
35
lms/www/community/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ _('Community') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
|
||||
<input class="search" id="search-user" placeholder="{{ _('Try a Name, Company, or Industry') }}">
|
||||
|
||||
<div class="course-home-headings">{{ _("Community") }} </div>
|
||||
|
||||
<div class="empty-state alert alert-dismissible hide" id="search-empty-state">
|
||||
<a href="#" class="close-search-empty-state" aria-label="close">×</a>
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/frappe/images/ui-states/search-empty-state.svg">
|
||||
</div>
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No results found") }}</div>
|
||||
<div class="course-meta">{{ _("Try some other keyword or explore our community") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="member-parent">
|
||||
{% for user in users %}
|
||||
{{ widgets.MemberCard(member=user, show_course_count=False, avatar_class="avatar-large") }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if user_count > user_details | length %}
|
||||
<div class="mt-10 d-flex justify-content-center">
|
||||
<div class="button is-secondary" id="load-more" data-start="30" data-count="{{ user_count }}">{{ _("Load More") }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
lms/www/community/index.js
Normal file
58
lms/www/community/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
$("#load-more").click((e) => {
|
||||
search(e);
|
||||
});
|
||||
|
||||
$(".close-search-empty-state").click((e) => {
|
||||
close_search_empty_state(e);
|
||||
});
|
||||
|
||||
$("#search-user").keyup(function() {
|
||||
let timer;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { search.apply(this, arguments); }, 300);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const search = (e) => {
|
||||
$("#search-empty-state").addClass("hide");
|
||||
let start = $(e.currentTarget).data("start")
|
||||
let input = $("#search-user").val();
|
||||
if ($(e.currentTarget).prop("nodeName") == "INPUT")
|
||||
start = 0;
|
||||
|
||||
frappe.call({
|
||||
method: "lms.overrides.user.search_users",
|
||||
args: {
|
||||
"start": start,
|
||||
"text": input
|
||||
},
|
||||
callback: (data) => {
|
||||
if ($(e.currentTarget).prop("nodeName") == "INPUT")
|
||||
$(".member-parent").empty();
|
||||
|
||||
if (data.message.user_details.length)
|
||||
$("#load-more").removeClass("hide");
|
||||
else
|
||||
$("#search-empty-state").removeClass("hide");
|
||||
|
||||
$(".member-parent").append(data.message.user_details);
|
||||
update_load_more_state(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const close_search_empty_state = (e) => {
|
||||
$("#search-empty-state").addClass("hide");
|
||||
$("#search-user").val("").keyup();
|
||||
}
|
||||
|
||||
const update_load_more_state = (data) => {
|
||||
$("#load-more").data("start", data.message.start);
|
||||
$("#load-more").data("count", data.message.count);
|
||||
if ($(".member-card").length == $("#load-more").data("count")) {
|
||||
$("#load-more").addClass("hide");
|
||||
}
|
||||
}
|
||||
10
lms/www/community/index.py
Normal file
10
lms/www/community/index.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.user_count = frappe.db.count("User", {"enabled": True})
|
||||
context.users = frappe.get_all("User",
|
||||
filters={"enabled": True},
|
||||
fields=["name", "username", "full_name", "user_image", "headline"],
|
||||
start=0,
|
||||
page_length=24,
|
||||
order_by="creation desc")
|
||||
0
lms/www/courses/__init__.py
Normal file
0
lms/www/courses/__init__.py
Normal file
0
lms/www/courses/__pycache__/__init__.py
Normal file
0
lms/www/courses/__pycache__/__init__.py
Normal file
25
lms/www/courses/certificate.html
Normal file
25
lms/www/courses/certificate.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% from "www/macros/common_macro.html" import MentorsSection %}
|
||||
|
||||
{% block title %} {{ student.full_name }} - {{ course.title }} {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container certificate-page">
|
||||
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">All Courses</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<a class="dark-links" href="/courses/{{ course.name }}">{{ course.title }}</a>
|
||||
</div>
|
||||
|
||||
{% if certificate.student == frappe.session.user %}
|
||||
<div class="d-flex justify-content-end mb-5">
|
||||
<div class="button is-secondary pull-right" id="export-as-pdf" data-certificate="{{ certificate.name }}"
|
||||
data-certificate-name="{{ student.full_name }} - {{ course.title }}">Export</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include "lms/templates/certificate.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
lms/www/courses/certificate.js
Normal file
25
lms/www/courses/certificate.js
Normal file
@@ -0,0 +1,25 @@
|
||||
frappe.ready(() => {
|
||||
|
||||
$("#export-as-pdf").click((e) => {
|
||||
export_as_pdf(e);
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
var export_as_pdf = (e) => {
|
||||
var button = $(e.currentTarget);
|
||||
button.text(__("Exporting..."));
|
||||
|
||||
html2canvas(document.querySelector('.common-card-style'), {
|
||||
scrollY: -window.scrollY,
|
||||
scrollX: 0
|
||||
}).then(function(canvas) {
|
||||
let dataURL = canvas.toDataURL('image/png');
|
||||
let a = document.createElement('a');
|
||||
a.href = dataURL;
|
||||
a.download = button.attr("data-certificate-name");
|
||||
a.click();
|
||||
}).finally(() => {
|
||||
button.text(__("Export"))
|
||||
});
|
||||
}
|
||||
31
lms/www/courses/certificate.py
Normal file
31
lms/www/courses/certificate.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
certificate_name = frappe.form_dict["certificate"]
|
||||
except KeyError:
|
||||
redirect_to_course_list()
|
||||
|
||||
context.certificate = frappe.db.get_value("LMS Certification", certificate_name,
|
||||
["name", "student", "issue_date", "expiry_date", "course"], as_dict=True)
|
||||
|
||||
if context.certificate.course != course_name:
|
||||
redirect_to_course_list()
|
||||
|
||||
context.course = frappe.db.get_value("LMS Course", course_name,
|
||||
["instructor", "title", "name"], as_dict=True)
|
||||
|
||||
context.instructor = frappe.db.get_value("User", context.course.instructor,
|
||||
["full_name", "username"], as_dict=True)
|
||||
|
||||
context.student = frappe.db.get_value("User", context.certificate.student,
|
||||
["full_name"], as_dict=True)
|
||||
|
||||
context.logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
||||
|
||||
def redirect_to_course_list():
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
295
lms/www/courses/course.html
Normal file
295
lms/www/courses/course.html
Normal file
@@ -0,0 +1,295 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ course.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style pt-0 pb-0">
|
||||
<div class="course-home-top-container">
|
||||
{{ CourseHomeHeader(course) }}
|
||||
<div class="course-home-page">
|
||||
<div class="container">
|
||||
<div class="course-body-container">
|
||||
{{ CourseHeaderOverlay(course) }}
|
||||
{{ Description(course) }}
|
||||
{{ widgets.CourseOutline(course=course, membership=membership) }}
|
||||
{{ widgets.Reviews(course=course, membership=membership) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ RelatedCourses(course) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro CourseHomeHeader(course) %}
|
||||
<div class="course-head-container"
|
||||
style=" {% if course.image %}
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-image: url({{ course.image }});
|
||||
{% else %} background-color: var(--gray-200) {% endif %}">
|
||||
<div class="container pt-5 pb-5">
|
||||
<div class="course-card-wide" style="">
|
||||
{{ BreadCrumb(course) }}
|
||||
{{ CourseCardWide(course) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<!-- BreadCrumb -->
|
||||
|
||||
{% macro BreadCrumb(course) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/courses">{{ _("All Courses") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ course.title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<!-- Course Card -->
|
||||
|
||||
{% macro CourseCardWide(course) %}
|
||||
<div class="">
|
||||
<div class="d-flex align-items-center">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
<div class="course-card-pills">{{ tag }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="course-card-wide-title">
|
||||
{{ course.title }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ course.short_introduction }}
|
||||
</div>
|
||||
|
||||
<div class="course-intructor-rating-section">
|
||||
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-md">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
|
||||
</div>
|
||||
|
||||
{% if get_lessons(course.name) | length %}
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set avg_rating = get_average_rating(course.name) %}
|
||||
{% if avg_rating %}
|
||||
<div class="rating mr-2">
|
||||
{% for i in [1, 2, 3, 4, 5] %}
|
||||
<svg class="icon icon-md {% if i <= avg_rating %} star-click {% endif %}" data-rating="{{ i }}">
|
||||
<use href="#icon-star"></use>
|
||||
</svg>
|
||||
{% endfor %}
|
||||
<span> {{ avg_rating }} {{ _(" Rating ") }} </span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if membership %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
<div class="progress" title="{{ progress }}% Completed">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="{{ progress }}"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:{{ progress }}%">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="bold-heading">Instructors:</div>
|
||||
{% for instructor in get_instructors(course.name) %}
|
||||
<div class="mt-1">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-small") }}
|
||||
<a class="button-links" href="{{ get_profile_url(instructor.username) }}">
|
||||
<span class="course-instructor"> {{ instructor.full_name }} </span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endmacro%}
|
||||
|
||||
{% macro CourseHeaderOverlay(course) %}
|
||||
<div class="course-overlay-card {% if course.video_link %} video-in-overlay {% endif %}">
|
||||
|
||||
{% if course.video_link %}
|
||||
<iframe class="preview-video" src="{{ course.video_link }}"></iframe>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-overlay-content">
|
||||
<div class="course-home-headings"> {{ course.title }} </div>
|
||||
|
||||
<div id="interest-alert" class="{% if not is_user_interested %} hide {% endif %}">
|
||||
{{ _("You have opted to be notified for this course. You will receive an email when the course becomes available.") }}
|
||||
</div>
|
||||
|
||||
{% if course.status == "Under Review" %}
|
||||
<div class="mb-4">
|
||||
{{ _("Your course is currently under review. Once the review is complete, the System Admins will publish it on the website.") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="vertically-center mb-4">
|
||||
{% if is_instructor(course.name) %}
|
||||
<a class="button is-default button-links mr-2" href="/course?name={{ course.name }}"> {{ _("Edit") }} </a>
|
||||
<a class="button is-default button-links mr-2" href="/chapter">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("Add Chapter") }}
|
||||
</a>
|
||||
<a class="button is-default button-links mr-2" href="/lesson">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("Add Lesson") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="vertically-center mb-2">
|
||||
<div class="">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use class="" href="#icon-users">
|
||||
</svg>
|
||||
{{ get_students(course.name) | length }} {{ _("Enrolled") }}
|
||||
</div>
|
||||
|
||||
<span class="seperator"></span>
|
||||
|
||||
{% if get_lessons(course.name) | length %}
|
||||
<div class="">
|
||||
<svg class="icon icon-md mr-1">
|
||||
<use href="#icon-education"></use>
|
||||
</svg>
|
||||
{{ get_lessons(course.name) | length }} {{ _("Lessons") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% set lesson_index = get_lesson_index(membership.current_lesson) if membership and
|
||||
membership.current_lesson
|
||||
else '1.1' %}
|
||||
|
||||
{% if show_start_learing_cta %}
|
||||
<div class="button wide-button is-primary join-batch" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Start Learning") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) and not course.is_published and course.status != "Under Review" %}
|
||||
<div class="button wide-button is-primary" id="submit-for-review" data-course="{{ course.name | urlencode }}">
|
||||
{{ _("Submit for Review") }}
|
||||
<img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</div>
|
||||
|
||||
{% elif is_instructor(course.name) %}
|
||||
<a class="button wide-button is-primary" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Checkout Course") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
|
||||
{% elif membership %}
|
||||
<a class="button wide-button is-primary" id="continue-learning"
|
||||
href="{{ get_lesson_url(course.name, lesson_index) }}{{ course.query_parameter }}">
|
||||
{{ _("Continue Learning") }} <img class="ml-2" src="/assets/lms/icons/white-arrow.svg" />
|
||||
</a>
|
||||
|
||||
{% elif course.upcoming and not is_user_interested %}
|
||||
<div class="button wide-button is-default"
|
||||
id="notify-me" data-course="{{course.name | urlencode}}">
|
||||
{{ _("Notify me when available") }}
|
||||
</div>
|
||||
|
||||
{% elif is_cohort_staff(course.name, frappe.session.user) %}
|
||||
<a class="button wide-button is-secondary"
|
||||
href="/courses/{{course.name}}/manage"
|
||||
style="color: inherit;">
|
||||
{{ _("Manage the course") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% set certificate = is_certified(course.name) %}
|
||||
{% set progress = frappe.utils.cint(membership.progress) %}
|
||||
{% if certificate %}
|
||||
<a class="button wide-button is-secondary mt-2" href="/courses/{{ course.name }}/{{ certificate }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</a>
|
||||
{% elif course.enable_certification and progress == 100 %}
|
||||
<div class="button wide-button is-secondary mt-4" id="certification" data-course="{{ course.name }}">
|
||||
{{ _("Get Certificate") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Description(course) %}
|
||||
<div class="course-description-section">
|
||||
{{ frappe.utils.md_to_html(course.description) }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CourseCreator(course) %}
|
||||
<div class="course-home-headings"> {{ _("Course Creators") }} </div>
|
||||
|
||||
<div class="common-card-style course-creators-card">
|
||||
{% set instructors = get_instructors(course.name) %}
|
||||
{% for instructor in instructors %}
|
||||
<div class="d-flex align-items-center">
|
||||
{{ widgets.Avatar(member=instructor, avatar_class="avatar-medium") }}
|
||||
<div class="ml-4">
|
||||
<div class="course-creator-name"> {{ instructor.full_name }} </div>
|
||||
<div class="course-meta"> {{ get_authored_courses(instructor.name) | length }} {{ _("Courses Created") }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RelatedCourses(course) %}
|
||||
{% if course.related_courses | length %}
|
||||
<div class="related-courses">
|
||||
<div class="container">
|
||||
<div class="course-home-headings"> {{ _("Other Courses") }} </div>
|
||||
<div class="carousel slide" id="carouselExampleControls" data-ride="carousel" data-interval="false">
|
||||
<div class="carousel-inner">
|
||||
{% for crs in course.related_courses %}
|
||||
{% if loop.index % 3 == 1 %}
|
||||
<div class="carousel-item {% if loop.index == 1 %} active {% endif %}"><div class="cards-parent">
|
||||
{% endif %}
|
||||
{{ widgets.CourseCard(course=crs, read_only=False) }}
|
||||
{% if loop.index % 3 == 0 or loop.index == course.related_courses | length %} </div> </div> {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if course.related_courses | length > 3 %}
|
||||
<div class="slider-controls">
|
||||
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif%}
|
||||
{% endmacro %}
|
||||
281
lms/www/courses/course.js
Normal file
281
lms/www/courses/course.js
Normal file
@@ -0,0 +1,281 @@
|
||||
frappe.ready(() => {
|
||||
if (frappe.session.user != "Guest") {
|
||||
check_mentor_request();
|
||||
}
|
||||
|
||||
hide_wrapped_mentor_cards();
|
||||
|
||||
$("#apply-now").click((e) => {
|
||||
create_mentor_request(e);
|
||||
});
|
||||
|
||||
$("#cancel-request").click((e) => {
|
||||
cancel_mentor_request(e);
|
||||
});
|
||||
|
||||
$(".join-batch").click((e) => {
|
||||
join_course(e)
|
||||
});
|
||||
|
||||
$(".view-all-mentors").click((e) => {
|
||||
view_all_mentors(e);
|
||||
});
|
||||
|
||||
$(".review-link").click((e) => {
|
||||
show_review_dialog(e);
|
||||
});
|
||||
|
||||
$(".icon-rating").click((e) => {
|
||||
highlight_rating(e);
|
||||
});
|
||||
|
||||
$("#submit-review").click((e) => {
|
||||
submit_review(e);
|
||||
})
|
||||
|
||||
$("#notify-me").click((e) => {
|
||||
notify_user(e);
|
||||
})
|
||||
|
||||
$("#certification").click((e) => {
|
||||
create_certificate(e);
|
||||
});
|
||||
|
||||
$("#submit-for-review").click((e) => {
|
||||
submit_for_review(e);
|
||||
});
|
||||
|
||||
$(document).scroll(function() {
|
||||
let timer;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => { handle_overlay_display.apply(this, arguments); }, 500);
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
var check_mentor_request = () => {
|
||||
frappe.call({
|
||||
'method': 'lms.lms.doctype.lms_mentor_request.lms_mentor_request.has_requested',
|
||||
'args': {
|
||||
course: decodeURIComponent($("#course-title").attr("data-course")),
|
||||
},
|
||||
'callback': (data) => {
|
||||
if (data.message > 0) {
|
||||
$("#mentor-request").addClass("hide");
|
||||
$("#already-applied").removeClass("hide")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var hide_wrapped_mentor_cards = () => {
|
||||
var offset_top_prev;
|
||||
|
||||
$(".member-parent .member-card").each(function () {
|
||||
var offset_top = $(this).offset().top;
|
||||
if (offset_top > offset_top_prev) {
|
||||
$(this).addClass('wrapped').slideUp("fast");
|
||||
}
|
||||
if (!offset_top_prev) {
|
||||
offset_top_prev = offset_top;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if ($(".wrapped").length < 1) {
|
||||
$(".view-all-mentors").hide();
|
||||
}
|
||||
}
|
||||
|
||||
var create_mentor_request = (e) => {
|
||||
e.preventDefault();
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${$(e.currentTarget).attr("data-course")}`;
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.create_request",
|
||||
"args": {
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
$("#mentor-request").addClass("hide");
|
||||
$("#already-applied").removeClass("hide")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var cancel_mentor_request = (e) => {
|
||||
e.preventDefault()
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_mentor_request.lms_mentor_request.cancel_request",
|
||||
"args": {
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
$("#mentor-request").removeClass("hide");
|
||||
$("#already-applied").addClass("hide")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var join_course = (e) => {
|
||||
e.preventDefault();
|
||||
var course = $(e.currentTarget).attr("data-course")
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
||||
return;
|
||||
}
|
||||
var batch = $(e.currentTarget).attr("data-batch");
|
||||
batch = batch ? decodeURIComponent(batch) : "";
|
||||
frappe.call({
|
||||
"method": "lms.lms.doctype.lms_batch_membership.lms_batch_membership.create_membership",
|
||||
"args": {
|
||||
"batch": batch ? batch : "",
|
||||
"course": course
|
||||
},
|
||||
"callback": (data) => {
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint(__("You are now a student of this course."));
|
||||
setTimeout(function () {
|
||||
window.location.href = `/courses/${course}/learn/1.1`;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var view_all_mentors = (e) => {
|
||||
$(".wrapped").each((i, element) => {
|
||||
$(element).slideToggle("slow");
|
||||
})
|
||||
var text_element = $(".view-all-mentors .course-instructor .all-mentors-text");
|
||||
var text = text_element.text() == "View all mentors" ? "View less" : "View all mentors";
|
||||
text_element.text(text);
|
||||
|
||||
if ($(".mentor-icon").css("transform") == "none") {
|
||||
$(".mentor-icon").css("transform", "rotate(180deg)");
|
||||
} else {
|
||||
$(".mentor-icon").css("transform", "");
|
||||
}
|
||||
}
|
||||
|
||||
var show_review_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
$("#review-modal").modal("show");
|
||||
}
|
||||
|
||||
var highlight_rating = (e) => {
|
||||
var rating = $(e.currentTarget).attr("data-rating");
|
||||
$(".icon-rating").removeClass("star-click");
|
||||
$(".icon-rating").each((i, elem) => {
|
||||
if (i <= rating-1) {
|
||||
$(elem).addClass("star-click");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var submit_review = (e) => {
|
||||
e.preventDefault();
|
||||
var rating = $(".rating-field").children(".star-click").length;
|
||||
var review = $(".review-field").val();
|
||||
if (!review || !rating) {
|
||||
$(".error-field").text("Both Rating and Review are required.");
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course_review.lms_course_review.submit_review",
|
||||
args: {
|
||||
"rating": rating,
|
||||
"review": review,
|
||||
"course": decodeURIComponent($(e.currentTarget).attr("data-course"))
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "OK") {
|
||||
$(".review-modal").modal("hide");
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
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: "lms.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
$("#interest-alert").removeClass("hide");
|
||||
$("#notify-me").addClass("hide");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const create_certificate = (e) => {
|
||||
e.preventDefault();
|
||||
course = $(e.currentTarget).attr("data-course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_certification.lms_certification.create_certificate",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
window.location.href = `/courses/${course}/${data.message.name}`;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
const element_not_in_viewport = (el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return rect.bottom < 0 || rect.right < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight;
|
||||
};
|
||||
|
||||
const handle_overlay_display = () => {
|
||||
const element = $(".related-courses").length && $(".related-courses")[0];
|
||||
if (element && element_not_in_viewport(element)) {
|
||||
$(".course-overlay-card").css({
|
||||
"position": "fixed",
|
||||
"top": "30%",
|
||||
"bottom": "inherit"
|
||||
});
|
||||
}
|
||||
else if (element && !element_not_in_viewport(element)) {
|
||||
$(".course-overlay-card").css({
|
||||
"position": "absolute",
|
||||
"top": "inherit",
|
||||
"bottom": "5%"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submit_for_review = (e) => {
|
||||
let course = $(e.currentTarget).data("course");
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.submit_for_review",
|
||||
args: {
|
||||
"course": course
|
||||
},
|
||||
callback: (data) => {
|
||||
if (data.message == "No Chp") {
|
||||
frappe.msgprint(__(`There are no chapters in this course.
|
||||
Please add chapters and lessons to your course before you submit it for review.`));
|
||||
} else if (data.message == "OK") {
|
||||
frappe.msgprint(__("Your course has been submitted for review."))
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
52
lms/www/courses/course.py
Normal file
52
lms/www/courses/course.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import frappe
|
||||
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
|
||||
from lms.lms.utils import get_membership, is_instructor
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
course_name = frappe.form_dict["course"]
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
|
||||
course = frappe.db.get_value("LMS Course", course_name,
|
||||
["name", "title", "image", "short_introduction", "description", "is_published", "upcoming",
|
||||
"disable_self_learning", "video_link", "enable_certification", "status"],
|
||||
as_dict=True)
|
||||
|
||||
related_courses = frappe.get_all("Related Courses", {"parent": course.name}, ["course"])
|
||||
for csr in related_courses:
|
||||
csr.update(frappe.db.get_value("LMS Course",
|
||||
csr.course, ["name", "upcoming", "title", "image", "enable_certification"], as_dict=True))
|
||||
course.related_courses = related_courses
|
||||
|
||||
if course is None:
|
||||
frappe.local.flags.redirect_location = "/courses"
|
||||
raise frappe.Redirect
|
||||
|
||||
context.course = course
|
||||
membership = get_membership(course.name, frappe.session.user)
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.membership = membership
|
||||
if context.course.upcoming:
|
||||
context.is_user_interested = get_user_interest(context.course.name)
|
||||
context.restriction = check_profile_restriction()
|
||||
context.show_start_learing_cta = show_start_learing_cta(course, membership, context.restriction)
|
||||
context.metatags = {
|
||||
"title": course.title,
|
||||
"image": course.image,
|
||||
"description": course.short_introduction,
|
||||
"keywords": course.title
|
||||
}
|
||||
|
||||
def get_user_interest(course):
|
||||
return frappe.db.count("LMS Course Interest",
|
||||
{
|
||||
"course": course,
|
||||
"user": frappe.session.user
|
||||
})
|
||||
|
||||
def show_start_learing_cta(course, membership, restriction):
|
||||
return not course.disable_self_learning and not membership and not course.upcoming and not restriction.get("restrict") and not is_instructor(course.name)
|
||||
35
lms/www/courses/index.html
Normal file
35
lms/www/courses/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ 'Courses' }}{% endblock %}
|
||||
{% block head_include %}
|
||||
<style>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
{% if restriction.restrict %}
|
||||
{% set profile_link = "<a href='/edit-profile'> profile </a>" %}
|
||||
<div class="empty-state">
|
||||
<div class="course-home-headings text-center mb-0" style="color: inherit;">{{ _("You haven't completed your profile.") }}</div>
|
||||
<p class="small text-center">{{ _("Complete your {0} to access the courses.").format(profile_link) }}</p>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% include "lms/templates/search_course/search_course.html" %}
|
||||
<div class="course-list">
|
||||
{% set courses = live_courses %}
|
||||
{% set title = _("All Live Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "live-courses" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
|
||||
{% set courses = upcoming_courses %}
|
||||
{% set title = _("All Upcoming Courses ({0})").format(courses | length) %}
|
||||
{% set classes = "upcoming-courses mt-10" %}
|
||||
{% include "lms/templates/course_list.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
lms/www/courses/index.py
Normal file
26
lms/www/courses/index.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import frappe
|
||||
from lms.lms.doctype.lms_settings.lms_settings import check_profile_restriction
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.live_courses, context.upcoming_courses = get_courses()
|
||||
context.restriction = check_profile_restriction()
|
||||
context.metatags = {
|
||||
"title": "All Live 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():
|
||||
courses = frappe.get_all("LMS Course",
|
||||
filters={"is_published": True},
|
||||
fields=["name", "upcoming", "title", "image", "enable_certification"])
|
||||
|
||||
live_courses, upcoming_courses = [], []
|
||||
for course in courses:
|
||||
if course.upcoming:
|
||||
upcoming_courses.append(course)
|
||||
else:
|
||||
live_courses.append(course)
|
||||
return live_courses, upcoming_courses
|
||||
0
lms/www/courses/utils.py
Normal file
0
lms/www/courses/utils.py
Normal file
0
lms/www/dashboard/__init__.py
Normal file
0
lms/www/dashboard/__init__.py
Normal file
49
lms/www/dashboard/index.html
Normal file
49
lms/www/dashboard/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ _("Dashboard")}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
|
||||
<div class="common-page-style dashboard">
|
||||
<div class="container">
|
||||
{% if portal_course_creation %}
|
||||
<a class="button is-default button-links pull-right hide" id="create-course-link" href="/course">
|
||||
<svg class="icon icon-sm mr-1"><use href="#icon-add"></use></svg>
|
||||
{{ _("New Course")}}
|
||||
</a>
|
||||
{% endif %}
|
||||
<ul class="nav" id="courses-tab">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#courses-enrolled"> {{ _("Courses Enrolled") }} </a>
|
||||
</li>
|
||||
{% if portal_course_creation %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#courses-created">{{ _("Courses Created") }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="border-bottom mb-4"></div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="courses-enrolled" role="tabpanel" aria-labelledby="courses-enrolled">
|
||||
{% include "lms/lms/web_template/courses_enrolled/courses_enrolled.html" %}
|
||||
</div>
|
||||
{% if portal_course_creation %}
|
||||
<div class="tab-pane fade" id="courses-created" role="tabpanel" aria-labelledby="courses-created">
|
||||
{% include "lms/templates/courses_created.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$('#courses-tab a[data-toggle="tab"]').on('shown.bs.tab', (e) => {
|
||||
let link = $("#create-course-link");
|
||||
$(e.currentTarget).attr("href") == "#courses-created" ? link.removeClass("hide") : link.addClass("hide");
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
0
lms/www/hackathons/__init__.py
Normal file
0
lms/www/hackathons/__init__.py
Normal file
0
lms/www/hackathons/__pycache__/__init__.py
Normal file
0
lms/www/hackathons/__pycache__/__init__.py
Normal file
129
lms/www/hackathons/hackathon.html
Normal file
129
lms/www/hackathons/hackathon.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ hackathon }}{% endblock %}
|
||||
{% from "www/hackathons/macros/hero.html" import hero %}
|
||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
||||
{% from "www/hackathons/macros/navbar.html" import navbar %}
|
||||
{% from "www/hackathons/macros/user.html" import show_user %}
|
||||
|
||||
{% block head_include %}
|
||||
<style>
|
||||
div.card-hero-img {
|
||||
height: 220px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(250, 251, 252);
|
||||
}
|
||||
|
||||
.card-image-wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 220px;
|
||||
background-color: rgb(250, 251, 252);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-body {
|
||||
align-self: center;
|
||||
color: #d1d8dd;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0 5rem 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% macro card(project) %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<a href="/hackathons/{{ hackathon }}/{{ project.name }}" class="no-decoration no-underline">
|
||||
<div class="card h-100">
|
||||
<div class='card-body'>
|
||||
<h5 class='card-title'>{{ project.name }}</h5>
|
||||
<div class="text-muted">{{ project.project_short_intro }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro card_talk(talk) %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<a href="{{talk.video_link}}" class="no-decoration no-underline">
|
||||
<div class="card h-100">
|
||||
<div class='card-body'>
|
||||
<h5 class='card-title'>{{ talk.topic }}</h5>
|
||||
<div class="text-muted">{{ talk.speaker }}</div>
|
||||
<div class="text-muted">{{ frappe.utils.format_datetime(talk.date_and_time, "medium") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro card_update(update) %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<div class="card h-100">
|
||||
<div class='card-body'>
|
||||
<p>{{ frappe.utils.md_to_html(update.project_update) }}</p>
|
||||
<div>
|
||||
<a href="/hackathons/{{hackathon}}/{{update.project}}">{{ update.project}}</a>
|
||||
by {{ show_user(update.owner) }}
|
||||
<div class="text-muted">{{ frappe.utils.format_datetime(update.creation, "medium") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
{{ hero(hackathon, {'name': 'Home', 'url': '/hackathons'}) }}
|
||||
<div class='container'>
|
||||
{{ navbar(hackathon) }}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="home">
|
||||
<div class="row mt-5">
|
||||
{% for project in projects %}
|
||||
{{ card(project) }}
|
||||
{% endfor %}
|
||||
{% if projects %}
|
||||
{% for n in range( (3 - (projects|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade py-4" role="tabpanel" id="talks">
|
||||
<div class="row mt-5">
|
||||
{% for talk in talks %}
|
||||
{{ card_talk(talk) }}
|
||||
{% endfor %}
|
||||
{% if talks %}
|
||||
{% for n in range( (3 - (talks|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade py-4" role="tabpanel" id="updates">
|
||||
<div class="row mt-5">
|
||||
{% for update in updates %}
|
||||
{{ card_update(update) }}
|
||||
{% endfor %}
|
||||
{% if updates %}
|
||||
{% for n in range( (3 - (updates|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
25
lms/www/hackathons/hackathon.py
Normal file
25
lms/www/hackathons/hackathon.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
try:
|
||||
hackathon = frappe.form_dict['hackathon']
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = '/hackathons'
|
||||
raise frappe.Redirect
|
||||
context.projects = get_hackathon_projects(hackathon)
|
||||
context.hackathon = hackathon
|
||||
context.talks = get_hackathon_talks(hackathon)
|
||||
context.updates = get_hackathon_updates(context.projects)
|
||||
|
||||
def get_hackathon_projects(hackathon):
|
||||
return frappe.get_all("Community Project", filters={"hackathon":hackathon}, fields=["name", "project_short_intro"])
|
||||
|
||||
def get_hackathon_talks(hackathon):
|
||||
return frappe.get_all("Community Talk", {"event": hackathon}, ["topic", "speaker", "date_and_time", "video_link"])
|
||||
|
||||
def get_hackathon_updates(projects):
|
||||
project_list = [project.name for project in projects]
|
||||
return frappe.get_all("Community Project Update", {"project": ["in", project_list]}, ["project", "`update` as project_update", "owner", "creation"])
|
||||
63
lms/www/hackathons/index.html
Normal file
63
lms/www/hackathons/index.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ 'Hackathons' }}{% endblock %}
|
||||
{% from "www/hackathons/macros/card.html" import hackathon_card %}
|
||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{ 'Hackathon' }}" />
|
||||
<meta name="keywords" content="An app that supports Communities." />
|
||||
<style>
|
||||
div.card-hero-img {
|
||||
height: 220px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(250, 251, 252);
|
||||
}
|
||||
|
||||
.card-image-wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 220px;
|
||||
background-color: rgb(250, 251, 252);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-body {
|
||||
align-self: center;
|
||||
color: #d1d8dd;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0 5rem 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="top-section" style="padding: 6rem 0rem;">
|
||||
<div class='container pb-5'>
|
||||
<h1>{{ 'Hackathon' }}</h1>
|
||||
<!-- <p class="mt-4">
|
||||
{% if frappe.session.user == 'Guest' %}
|
||||
<a class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||
{% endif %}
|
||||
</p> -->
|
||||
</div>
|
||||
<div class='container'>
|
||||
<div class="row mt-5">
|
||||
{% for hackathon in hackathons %}
|
||||
{{ hackathon_card(hackathon) }}
|
||||
{% endfor %}
|
||||
{% if hackathons %}
|
||||
{% for n in range( (3 - (hackathons|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
lms/www/hackathons/index.py
Normal file
8
lms/www/hackathons/index.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
context.hackathons = get_hackathons()
|
||||
|
||||
def get_hackathons():
|
||||
return frappe.get_all("Community Hackathon")
|
||||
0
lms/www/hackathons/macros/__init__.py
Normal file
0
lms/www/hackathons/macros/__init__.py
Normal file
18
lms/www/hackathons/macros/card.html
Normal file
18
lms/www/hackathons/macros/card.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% macro hackathon_card(hackathon) %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<a href="/hackathons/{{ hackathon.name }}" class="no-decoration no-underline">
|
||||
<div class="card h-100" style="box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);">
|
||||
<div class='card-body'>
|
||||
<h5 class='card-title'>{{ hackathon.name }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro null_card() %}
|
||||
<div class="col-sm-4 mb-4 text-left">
|
||||
<div class="h-100 d-none d-sm-block" style="box-shadow: 0px 5px 10px rgb(0 0 0 / 10%);border-radius: 0.25rem;background-color: rgb(250, 251, 252);">
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
15
lms/www/hackathons/macros/hero.html
Normal file
15
lms/www/hackathons/macros/hero.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% macro hero(title, back) %}
|
||||
<div class='container pb-5'>
|
||||
<div class="mb-3">
|
||||
<a href="{{ back.url }}" class="text-muted">
|
||||
{{_('Back to')}} {{ _(back.name) }}
|
||||
</a>
|
||||
</div>
|
||||
<h1>{{ title }}</h1>
|
||||
<!-- <p class="mt-4">
|
||||
{% if frappe.session.user == 'Guest' %}
|
||||
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||
{% endif %}
|
||||
</p> -->
|
||||
</div>
|
||||
{% endmacro %}
|
||||
16
lms/www/hackathons/macros/navbar.html
Normal file
16
lms/www/hackathons/macros/navbar.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% macro navbar(hackathon) %}
|
||||
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home"
|
||||
aria-selected="true">Projects</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="talks-tab" data-toggle="tab" href="#talks" role="tab" aria-controls="talks"
|
||||
aria-selected="false">Talks</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="updates-tab" data-toggle="tab" href="#updates" role="tab" aria-controls="updates"
|
||||
aria-selected="false">Updates</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
3
lms/www/hackathons/macros/user.html
Normal file
3
lms/www/hackathons/macros/user.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% macro show_user(user) %}
|
||||
{{ frappe.db.get_value("User", user, "full_name") }}
|
||||
{% endmacro %}
|
||||
174
lms/www/hackathons/project.html
Normal file
174
lms/www/hackathons/project.html
Normal file
@@ -0,0 +1,174 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ hackathon }}{% endblock %}
|
||||
{% from "www/hackathons/macros/hero.html" import hero %}
|
||||
{% from "www/hackathons/macros/card.html" import null_card %}
|
||||
{% from "www/hackathons/macros/user.html" import show_user %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{ 'Hackathon' }}" />
|
||||
<meta name="keywords" content="An app that supports Communities" />
|
||||
<style>
|
||||
div.card-hero-img {
|
||||
height: 220px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: rgb(250, 251, 252);
|
||||
}
|
||||
|
||||
.card-image-wrapper {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 220px;
|
||||
background-color: rgb(250, 251, 252);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-body {
|
||||
align-self: center;
|
||||
color: #d1d8dd;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 5rem 0 5rem 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
{{ hero(project, {'name': hackathon, 'url': '/hackathons/' + hackathon}) }}
|
||||
<div class='container'>
|
||||
{% if project %}
|
||||
<h1 class="mb-2">{{project.project_name}}</h1>
|
||||
|
||||
{% if frappe.session.user != "Guest" %}
|
||||
{% if is_owner %}
|
||||
<p>
|
||||
<div class="badge badge-info">Owner</div>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if is_member %}
|
||||
<p>
|
||||
<div class="badge badge-info">Member</div>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<p>{{ project.project_short_intro[:220] }}</p>
|
||||
{% if project.repository_link %}
|
||||
<a href="{{ project.repository_link }}" class="btn btn-default btn-sm" target="_blank">Respository</a>
|
||||
{% endif %}
|
||||
{% if project.video_link %}
|
||||
<a href="{{ project.video_link }}" class="btn btn-default btn-sm" target="_blank">Video ▶️</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-default btn-sm btn-like" data-project={{project.name}}>👍</button>
|
||||
|
||||
<ul class="nav nav-tabs mt-4" id="myTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home"
|
||||
aria-selected="true">Readme</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="members-tab" data-toggle="tab" href="#members" role="tab"
|
||||
aria-controls="members" aria-selected="false">Members ({{ confirmed_members|len + 1}})</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="updates-tab" data-toggle="tab" href="#updates" role="tab"
|
||||
aria-controls="updates" aria-selected="false">Updates ({{ (updates|len) + 1 }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
||||
<!-- readme -->
|
||||
<div class="tab-pane fade show active py-4 markdown-style" id="home" role="tabpanel"
|
||||
aria-labelledby="home-tab">
|
||||
{{ frappe.utils.md_to_html(project.project_description or "No README created yet") }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane fade py-4" id="members" role="tabpanel" aria-labelledby="members-tab">
|
||||
|
||||
<!-- members -->
|
||||
<div class="list-group">
|
||||
<!-- owner -->
|
||||
<div class="list-group-item">{{ show_user(project.owner) }}</div>
|
||||
<!-- all members -->
|
||||
{% for member in members %}
|
||||
{% set is_user = member.owner == frappe.session.user %}
|
||||
{% set is_pending = is_user and member.status=="Pending" %}
|
||||
{% if member.status == "Accepted" %}
|
||||
<div class="list-group-item">
|
||||
{{ show_user(member.owner) }}
|
||||
{% if is_user %}
|
||||
<button data-request-id="{{ member.name }}"
|
||||
class="btn btn-sm btn-default btn-leave ml-4">Leave</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif member.status == "Pending" and is_owner %}
|
||||
<div class="list-group-item">Join request from: <b>{{ show_user(member.owner) }}</b>
|
||||
<p class="alert alert-warning mt-2">{{ member.intro }}</p>
|
||||
<div class="my-3">
|
||||
<button data-request-id="{{ member.name }}"
|
||||
class="btn btn-sm btn-secondary btn-accept">Accept</button>
|
||||
<button data-request-id="{{ member.name }}"
|
||||
class="btn btn-sm btn-default btn-reject">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if frappe.session.user != 'Guest' %}
|
||||
<!-- join / pending -->
|
||||
{% if not (my_project or is_member or is_pending) and project.accepting_members %}
|
||||
<a class="btn btn-sm btn-secondary mt-2"
|
||||
href="/join-request?new=1&project={{ project.name }}&project_name={{ project.project_name }}">Join
|
||||
{{ project.project_name }}</a>
|
||||
{% elif is_pending %}
|
||||
<p class="alert alert-warning mt-2">Your application is pending</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- updates -->
|
||||
<div class="tab-pane fade py-4" id="updates" role="tabpanel" aria-labelledby="updates-tab">
|
||||
|
||||
{% macro add_update(update, date) %}
|
||||
<div class='list-group-item'>
|
||||
{{ frappe.utils.md_to_html(update or '') }}
|
||||
<div class="small text-muted text-right">{{ frappe.utils.format_datetime(date, "medium") }}</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% if frappe.session.user != 'Guest' and (is_owner or is_member) %}
|
||||
<p>
|
||||
<a href="/project-update?new=1&project={{ project.name }}&hackathon={{ hackathon }}" class="btn btn-secondary btn-sm">Add
|
||||
Update</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class='list-group'>
|
||||
|
||||
<!-- updates -->
|
||||
{% for update in updates %}
|
||||
{{ add_update(update.project_update, update.creation) }}
|
||||
{%
|
||||
|
||||
<!-- creation -->
|
||||
{{ add_update("Project created by " + frappe.db.get_value('User', project.owner, 'full_name'),
|
||||
project.creation) }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
48
lms/www/hackathons/project.js
Normal file
48
lms/www/hackathons/project.js
Normal file
@@ -0,0 +1,48 @@
|
||||
$('#req-evals').on('click', () => {
|
||||
frappe.msgprint("The evaluations have been moved to <a href='https://t.me/fossunited'>Telegram</a>")
|
||||
})
|
||||
var set_likes = function (liked, likes) {
|
||||
let $btn = $('.btn-like');
|
||||
likes ? $btn.text(`${likes} 👍`): $btn.text(`👍`);
|
||||
if (liked) {
|
||||
$btn.addClass('btn-dark').removeClass('btn-default');
|
||||
} else {
|
||||
$btn.addClass('btn-default').removeClass('btn-dark');
|
||||
}
|
||||
};
|
||||
|
||||
// set initial likes
|
||||
frappe.ready(() => {
|
||||
frappe.call('lms.www.hackathons.project.like', { project: get_url_arg().get("project"), initial: true }, (data) => {
|
||||
set_likes(data.message.action == "Liked", data.message.likes)
|
||||
})
|
||||
})
|
||||
|
||||
var get_url_arg = () => {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
// like - unlike
|
||||
$('.btn-like').on('click', (e) => {
|
||||
frappe.call('lms.www.hackathons.project.like', { project: get_url_arg().get("project") }, (data) => {
|
||||
set_likes(data.message.action == "Liked", data.message.likes);
|
||||
});
|
||||
});
|
||||
|
||||
// accept / reject
|
||||
$('.btn-accept').on('click', (e) => {
|
||||
frappe.call('lms.www.hackathons.project.join_request', { id: $(e.target).attr('data-request-id'), action: 'Accept' }, (data) => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-reject').on('click', (ev) => {
|
||||
frappe.call('lms.www.hackathons.project.join_request', { id: $(ev.target).attr('data-request-id'), action: 'Reject' }, (data) => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-leave').on('click', (ev) => {
|
||||
frappe.call('lms.www.hackathons.project.join_request', { id: $(ev.target).attr('data-request-id'), action: 'Reject' }, (data) => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
90
lms/www/hackathons/project.py
Normal file
90
lms/www/hackathons/project.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
try:
|
||||
project = frappe.form_dict['project']
|
||||
hackathon = frappe.form_dict['hackathon']
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = '/hackathons'
|
||||
raise frappe.Redirect
|
||||
context.project = get_project(project)
|
||||
context.hackathon = hackathon
|
||||
context.members = get_members(project)
|
||||
context.confirmed_members = get_comfirmed_members(project)
|
||||
context.updates = get_updates(project)
|
||||
if frappe.session.user != "Guest":
|
||||
context.my_project = get_my_projects()
|
||||
context.is_owner = context.project.owner == frappe.session.user
|
||||
context.accepted_members = get_accepted_members(project)
|
||||
context.is_member = check_is_member(project)
|
||||
context.liked = get_liked_project(project)
|
||||
|
||||
def get_project(project_name):
|
||||
try:
|
||||
return frappe.get_doc('Community Project', project_name)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.throw(_("Project {0} does not exist.").format(project_name))
|
||||
|
||||
def get_members(project_name):
|
||||
return frappe.get_all("Community Project Member", {"project": project_name, "status": ("!=", "Rejected") }, ['name', "owner", "status", 'intro'])
|
||||
|
||||
def get_comfirmed_members(project_name):
|
||||
return frappe.get_all("Community Project Member", {"project": project_name, "status": ("=", "Accepted") }, ['name'])
|
||||
|
||||
def get_updates(project_name):
|
||||
return frappe.get_all('Community Project Update', {"project": project_name}, ['owner', 'creation', '`update` as project_update'])
|
||||
|
||||
def get_accepted_members(project_name):
|
||||
return frappe.get_all("Community Project Member", {"project": project_name, "status": "Accepted" })
|
||||
|
||||
def get_my_projects():
|
||||
my_project = frappe.db.get_value('Community Project', {"owner": frappe.session.user})
|
||||
if not my_project:
|
||||
my_project = frappe.db.get_value('Community Project Member', {"owner": frappe.session.user, "status": 'Accepted'}, 'project')
|
||||
return my_project
|
||||
|
||||
def check_is_member(project_name):
|
||||
return frappe.get_all("Community Project Member", {"project": project_name, "status": "Accepted", "owner": frappe.session.user })
|
||||
|
||||
def get_liked_project(project_name):
|
||||
return frappe.db.get_value("Community Project Like", {"owner": frappe.session.user, "project": project_name})
|
||||
|
||||
@frappe.whitelist()
|
||||
def join_request(id, action):
|
||||
if action == 'Accept':
|
||||
project_member = frappe.get_doc('Community Project Member', id)
|
||||
if len(frappe.db.get_all('Community Project Member',
|
||||
dict(project = project_member.project, status = 'Accepted'))) > 2:
|
||||
frappe.throw('A project cannot have more than 4 members')
|
||||
frappe.db.set_value('Community Project Member', id, 'status', 'Accepted')
|
||||
else:
|
||||
frappe.db.set_value('Community Project Member', id, 'status', 'Rejected')
|
||||
|
||||
def has_already_liked(project):
|
||||
likes = frappe.db.get_value('Community Project Like', {"owner": frappe.session.user, "project": project})
|
||||
return likes
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_project_likes(project):
|
||||
return len(frappe.get_all("Community Project Like", {"project": project}))
|
||||
|
||||
@frappe.whitelist()
|
||||
def like(project, initial=False):
|
||||
liked_project = has_already_liked(project)
|
||||
action = "Liked" if (liked_project and initial) else "Unliked"
|
||||
if not initial:
|
||||
if liked_project:
|
||||
action = "Unliked"
|
||||
frappe.get_doc("Community Project Like", liked_project).delete()
|
||||
else:
|
||||
action = "Liked"
|
||||
frappe.get_doc({"doctype": "Community Project Like","project": project}).save()
|
||||
|
||||
frappe.db.set_value("Community Project", project, "likes", get_project_likes(project))
|
||||
return {
|
||||
"action": action,
|
||||
"likes": get_project_likes(project)
|
||||
}
|
||||
0
lms/www/jobs/__init__.py
Normal file
0
lms/www/jobs/__init__.py
Normal file
70
lms/www/jobs/index.html
Normal file
70
lms/www/jobs/index.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ _('Job Openings') }}{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style">
|
||||
|
||||
<div class="container">
|
||||
{% if allow_posting and jobs | length %}
|
||||
<a class="button is-primary pull-right mt-5" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="course-home-headings mb-2">{{ _("{0}").format(title) }}</div>
|
||||
<div class="job-subtitle">{{ _("{0}").format(subtitle) }}</div>
|
||||
|
||||
{% if jobs | length %}
|
||||
<div class="common-card-style job-list-card">
|
||||
{% for job in jobs %}
|
||||
<div class="job-card">
|
||||
<div class="avatar avatar-medium mr-3" title="{{ job.company_name}}">
|
||||
<span class="avatar-frame company-logo" style="background-image: url( {{ job.company_logo | urlencode }} );"></span>
|
||||
</div>
|
||||
<div class="job-card-info">
|
||||
<div class="job-card-heading">{{ _(job.job_title) }}</div>
|
||||
<div class="vertically-center course-meta">
|
||||
<div class="mr-3">{{ job.company_name }}</div>
|
||||
<div class="vertically-center">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-location">
|
||||
</svg>
|
||||
{{ job.location }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-card-logo-section course-meta">
|
||||
<div class="indicator-pill green ml-3"> {{ job.type }} </div>
|
||||
<div class="">{{ frappe.utils.format_date(job.creation, "medium") }}</div>
|
||||
</div>
|
||||
<a class="stretched-link" href="/jobs/{{ job.name }}"></a>
|
||||
</div>
|
||||
{% if loop.index != jobs | length %}
|
||||
<div class="card-divider mt-5"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="empty-state">
|
||||
<div>
|
||||
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
|
||||
</div>
|
||||
<div class="empty-state-text">
|
||||
<div class="empty-state-heading">{{ _("No open jobs") }}</div>
|
||||
<div class="course-meta">{{ _("There are no job openings at present.") }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if allow_posting %}
|
||||
<a class="button is-secondary dark-links m-auto" href="/job-opportunity?new=1">{{ _("Post a Job") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
16
lms/www/jobs/index.py
Normal file
16
lms/www/jobs/index.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
context.jobs = frappe.get_all("Job Opportunity",
|
||||
{
|
||||
"status": "Open",
|
||||
"disabled": False
|
||||
},
|
||||
[
|
||||
"job_title", "location", "type", "company_name",
|
||||
"company_logo", "name", "creation"
|
||||
],
|
||||
order_by="creation desc")
|
||||
context.title = frappe.db.get_single_value("Job Settings", "title")
|
||||
context.subtitle = frappe.db.get_single_value("Job Settings", "subtitle")
|
||||
context.allow_posting = frappe.db.get_single_value("Job Settings", "allow_posting")
|
||||
96
lms/www/jobs/job.html
Normal file
96
lms/www/jobs/job.html
Normal file
@@ -0,0 +1,96 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block title %}{{ _(job.job_title) }}{% endblock %}
|
||||
|
||||
{% block head_include %}
|
||||
{% include "public/icons/symbol-defs.svg" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="common-page-style">
|
||||
<div class="container">
|
||||
{{ BreadCrumb(job) }}
|
||||
<div class="common-card-style job-detail-card">
|
||||
|
||||
<div class="d-flex">
|
||||
<div class="avatar avatar-medium align-self-center" title="{{ job.company_name}}">
|
||||
<div class="avatar-frame company-logo" style="
|
||||
background-image: url( {{ job.company_logo | urlencode }} );"></div>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<div class="vertically-center">
|
||||
<div class="course-home-headings mb-0">{{ _(job.job_title) }}</div>
|
||||
<div class="indicator-pill green ml-5"> {{ job.type }} </div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<a class="dark-links course-meta mr-5" href="{{ job.company_website }}">{{ job.company_name }}</a>
|
||||
<div class="vertically-center course-meta mr-5">
|
||||
<svg class="icon icon-sm">
|
||||
<use class="" href="#icon-location">
|
||||
</svg>
|
||||
<div>{{ job.location }}</div>
|
||||
</div>
|
||||
<div class="course-meta"> {{ frappe.utils.format_date(job.creation, "medium") }} </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set application_link = job.application_link if frappe.session.user != 'Guest' else '/login?redirect-to=/jobs/' + job.name %}
|
||||
<div class="d-flex align-items-start ml-auto">
|
||||
<a class="button is-primary mr-2"
|
||||
href="{{ application_link }}">{{ _("Apply") }}</a>
|
||||
<div class="button is-default mr-2" id="report" data-job="{{ job.name }}">{{ _("Report") }}</div>
|
||||
{% if job.owner == frappe.session.user %}
|
||||
<a class="button is-default button-links" href="/job-opportunity?name={{ job.name }}">Edit</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="course-meta mt-10">{{ _(job.description) }}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade report-modal" id="report-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="font-weight-bold">{{ _("Report this Post") }}</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="review-form" id="review-form">
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Reason for reporting") }}</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input">
|
||||
<textarea type="text" autocomplete="off" class="input-with-feedback form-control report-field"
|
||||
data-fieldtype="Text" data-fieldname="feedback_comments" placeholder="" style="height: 200px;"
|
||||
spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="error-field muted-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="button submit-review is-primary" data-job="{{ job.name }}" id="submit-report">
|
||||
Report</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% macro BreadCrumb(job) %}
|
||||
<div class="breadcrumb">
|
||||
<a class="dark-links" href="/jobs">{{ _("Job Openings") }}</a>
|
||||
<img class="ml-1 mr-1" src="/assets/lms/icons/chevron-right.svg">
|
||||
<span class="breadcrumb-destination">{{ job.job_title }}</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
32
lms/www/jobs/job.js
Normal file
32
lms/www/jobs/job.js
Normal file
@@ -0,0 +1,32 @@
|
||||
frappe.ready(() => {
|
||||
$("#report").click((e) => {
|
||||
open_report_dialog(e);
|
||||
});
|
||||
|
||||
$("#submit-report").click((e) => {
|
||||
report(e);
|
||||
});
|
||||
});
|
||||
|
||||
const open_report_dialog = (e) => {
|
||||
e.preventDefault();
|
||||
if (frappe.session.user == "Guest") {
|
||||
window.location.href = `/login?redirect-to=/jobs/${$(e.currentTarget).data("job")}`;
|
||||
return;
|
||||
}
|
||||
$("#report-modal").modal("show");
|
||||
};
|
||||
|
||||
const report = (e) => {
|
||||
frappe.call({
|
||||
method: "lms.job.doctype.job_opportunity.job_opportunity.report",
|
||||
args: {
|
||||
"job": $(e.currentTarget).data("job"),
|
||||
"reason": $(".report-field").val()
|
||||
},
|
||||
callback: (data) => {
|
||||
$(".report-modal").modal("hide");
|
||||
frappe.msgprint(__("Thanks for informing us about this post. The admin will look into it and take an appropriate action soon."))
|
||||
}
|
||||
})
|
||||
}
|
||||
9
lms/www/jobs/job.py
Normal file
9
lms/www/jobs/job.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import frappe
|
||||
|
||||
def get_context(context):
|
||||
try:
|
||||
job = frappe.form_dict["job"]
|
||||
except KeyError:
|
||||
frappe.local.flags.redirect_location = "/jobs"
|
||||
raise frappe.Redirect
|
||||
context.job = frappe.get_doc("Job Opportunity", job)
|
||||
0
lms/www/macros/__init__.py
Normal file
0
lms/www/macros/__init__.py
Normal file
21
lms/www/macros/common_macro.html
Normal file
21
lms/www/macros/common_macro.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% macro MentorsSection(mentors, is_mentor, course_name) %}
|
||||
<h3>Mentors</h3>
|
||||
{% for m in mentors %}
|
||||
<div class="instructor">
|
||||
<div class="instructor-title">{{m.full_name}}</div>
|
||||
<div class="instructor-subtitle">Mentored {{m.get_batch_count()}} batches</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not is_mentor %}
|
||||
<div id="mentor-request" class="notice">
|
||||
Interested to become a mentor?
|
||||
|
||||
<div><a id="apply-now" data-course="{{course_name | urlencode}}" href="">Apply Now!</a></div>
|
||||
</div>
|
||||
<div id="already-applied" class="notice hide">
|
||||
You've applied to become a mentor for this course. Your request is currently under review.
|
||||
|
||||
If you are not any more interested to mentor this course, you can <a id="cancel-request" data-course="{{course_name | urlencode}}" href="">cancel your application</a>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
127
lms/www/macros/livecode.html
Normal file
127
lms/www/macros/livecode.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{% macro LiveCodeEditorLarge(name, code) %}
|
||||
<div class="livecode-editor livecode-editor-large" id="editor-{{name}}">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="controls">
|
||||
<button class="run">Run</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="code-wrapper">
|
||||
<textarea class="code">{{code}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||
<canvas width="300" height="300"></canvas>
|
||||
<pre class="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro LiveCodeEditor(name, code, reset_code, is_exercise=False, last_submitted=None) %}
|
||||
<div class="livecode-editor livecode-editor-inline" id="editor-{{name}}">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="controls">
|
||||
<button class="run">Run</button>
|
||||
<button class="reset">Reset</button>
|
||||
{% if is_exercise %}
|
||||
<button class="submit pull-right btn-primary">Submit</button>
|
||||
{% if last_submitted %}
|
||||
<span class="pull-right" style="padding-right: 10px;"><span class="human-time" data-timestamp="{{last_submitted}}"></span></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display: none">
|
||||
<pre class="reset-code">{{reset_code}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-editor">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-6">
|
||||
<div class="code-wrapper">
|
||||
<textarea class="code">{{code}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 canvas-wrapper">
|
||||
<canvas width="300" height="300"></canvas>
|
||||
<pre class="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
|
||||
{% macro LiveCodeEditorJS(name, code) %}
|
||||
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// comment_when is failing because of this
|
||||
if (!frappe.sys_defaults) {
|
||||
frappe.sys_defaults = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
|
||||
<script type="text/javascript" src="/assets/lms/js/livecode-canvas.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var livecodeEditors = [];
|
||||
var livecodeEditorsMap = {};
|
||||
|
||||
$(function() {
|
||||
$(".livecode-editor").each((i, e) => {
|
||||
var name = e.id.replace("editor-", "");
|
||||
var editor = new LiveCodeEditor(e, {
|
||||
base_url: "{{ livecode_url }}",
|
||||
...getLiveCodeOptions()
|
||||
})
|
||||
livecodeEditors.push(editor);
|
||||
livecodeEditorsMap[e.id] = editor;
|
||||
|
||||
$(e).find(".reset").on('click', function() {
|
||||
let code = $(e).find(".reset-code").html();
|
||||
editor.codemirror.doc.setValue(code);
|
||||
});
|
||||
|
||||
$(e).find(".submit").on('click', function() {
|
||||
let code = editor.codemirror.doc.getValue();
|
||||
console.log("submit", name, code);
|
||||
frappe.call("lms.lms.api.submit_solution", {
|
||||
"exercise": name,
|
||||
"code": code
|
||||
}).then(r => {
|
||||
if (r.message.name) {
|
||||
frappe.msgprint("Submitted successfully!");
|
||||
|
||||
let d = r.message.creation;
|
||||
$(e).find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function updateSubmitTimes() {
|
||||
$(".human-time").each(function(i, e) {
|
||||
var d = $(e).data().timestamp;
|
||||
$(e).html(__("Submitted {0}", [comment_when(d)]));
|
||||
});
|
||||
}
|
||||
|
||||
updateSubmitTimes();
|
||||
</script>
|
||||
|
||||
{% endmacro %}
|
||||
70
lms/www/new-sign-up.html
Normal file
70
lms/www/new-sign-up.html
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %} {{_("New Sign Up")}} {% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<form id="new-sign-up">
|
||||
<div class="form-group">
|
||||
<label for="full_name">Full Name:</label>
|
||||
<input id="full_name" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="signup_email">Email:</label>
|
||||
<input id="signup_email" type="email" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">Username:</label>
|
||||
<input id="username" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" type="password" class="form-control" required>
|
||||
<span class="password-strength-indicator indicator"></span>
|
||||
|
||||
</div>
|
||||
<p class='password-strength-message text-muted small hidden'></p>
|
||||
<div class="form-group">
|
||||
<label for="invite_code">Invite Code:</label>
|
||||
<input id="invite_code" type="text" class="form-control" readonly required
|
||||
value="{{ frappe.form_dict['invite_code'] }}">
|
||||
</div>
|
||||
<button type="submit" id="submit" class="btn btn-primary">{{_("Submit")}}</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
$("#submit").click(function () {
|
||||
var data = {
|
||||
full_name: $("#full_name").val(),
|
||||
signup_email: $("#signup_email").val(),
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
invite_code: $("#invite_code").val(),
|
||||
};
|
||||
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "lms.lms.doctype.invite_request.invite_request.update_invite",
|
||||
args: {
|
||||
"data": data
|
||||
},
|
||||
callback: (data) => {
|
||||
$("input").val("");
|
||||
if (data.message == "OK") {
|
||||
frappe.msgprint({
|
||||
message: __("Your Account has been successfully created!"),
|
||||
clear: true
|
||||
});
|
||||
setTimeout(function() {
|
||||
window.location.href = "/login";
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
0
lms/www/profiles/__init__.py
Normal file
0
lms/www/profiles/__init__.py
Normal file
0
lms/www/profiles/__pycache__/__init__.py
Normal file
0
lms/www/profiles/__pycache__/__init__.py
Normal file
345
lms/www/profiles/profile.html
Normal file
345
lms/www/profiles/profile.html
Normal file
@@ -0,0 +1,345 @@
|
||||
{% extends "templates/base.html" %}
|
||||
{% block head_include %}
|
||||
<meta name="description" content="{{ member.full_name }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="common-page-style profile-page">
|
||||
{{ ProfileBanner(member) }}
|
||||
<div class="profile-page-body">
|
||||
<div class="container">
|
||||
{% set read_only = member.name != frappe.session.user %}
|
||||
{{ About(member) }}
|
||||
{{ EducationDetails(member) }}
|
||||
{{ WorkDetails(member) }}
|
||||
{{ Certification(member) }}
|
||||
{{ Contact(member) }}
|
||||
{{ Skills(member) }}
|
||||
{{ CareerPreference(member) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{{ CoursesEnrolled(member, read_only) }}
|
||||
{{ CoursesCreated(member, read_only) }}
|
||||
{{ CoursesMentored(member, read_only) }}
|
||||
{{ ProfileTabs(profile_tabs) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% macro ProfileBanner(member) %}
|
||||
{% set cover_image = member.cover_image if member.cover_image else "/assets/lms/images/profile-banner.png" %}
|
||||
{% set enrollment = get_course_membership(frappe.session.user, member_type="Student") | length %}
|
||||
{% set enrollment_suffix = _("Courses") if enrollment > 1 else _("Course") %}
|
||||
<div class="container">
|
||||
<div class="profile-banner" style="background-image: url({{ cover_image }})">
|
||||
<div class="profile-avatar">
|
||||
{{ widgets.Avatar(member=member, avatar_class="avatar-square") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-name-section">
|
||||
<div class="profile-name"> {{ member.full_name }} </div>
|
||||
|
||||
{% if get_authored_courses(member.name) | length %}
|
||||
<div class="creator-badge"> {{ _("Creator") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.looking_for_job %}
|
||||
<div class="creator-badge"> {{ _("Open Network") }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if frappe.session.user == member.email %}
|
||||
<a class="dark-links profile-link" href="/edit-profile?name={{ member.email }}"> {{ _("Edit Profile") }} </a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="profile-meta">
|
||||
{% if member.headline %}
|
||||
<div class="course-meta mr-3"> {{ member.headline }} </div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrollment %}
|
||||
<div class="course-meta">
|
||||
<img src="/assets/lms/icons/book_plain.svg">
|
||||
{{ enrollment }} {{ enrollment_suffix }} {{ _("taken") }} </div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesCreated(member, read_only) %}
|
||||
{% set authored_courses = get_authored_courses(member.name) %}
|
||||
{% if authored_courses | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Created") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in authored_courses %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesMentored(member, read_only) %}
|
||||
{% if member.get_mentored_courses() | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Mentored") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in member.get_mentored_courses() %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CoursesEnrolled(member, read_only) %}
|
||||
{% set enrolled = get_enrolled_courses() %}
|
||||
|
||||
{% if enrolled.completed | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses Completed") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in enrolled.completed %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrolled.in_progress | length %}
|
||||
<div class="profile-courses">
|
||||
<div class="course-home-headings"> {{ _("Courses In Progress") }} </div>
|
||||
<div class="cards-parent">
|
||||
{% for course in enrolled.in_progress %}
|
||||
{{ widgets.CourseCard(course=course, read_only=read_only) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ProfileTabs(profile_tabs) %}
|
||||
<div>
|
||||
{% for tab in profile_tabs %}
|
||||
{% set slug = title.lower().replace(" ", "-") %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade py-4 show active" role="tabpanel" id="slug">
|
||||
{{ tab.render() }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro About(member) %}
|
||||
{% if member.bio %}
|
||||
<div class="">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("About") }} </div>
|
||||
<div class="description">{{ member.bio }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro WorkPreference(member) %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Work Preference") }} </div>
|
||||
<div> {{ member.attire }} </div>
|
||||
<div> {{ member.collaboration }} </div>
|
||||
<div> {{ member.role }} </div>
|
||||
<div> {{ member.location_preference }} </div>
|
||||
<div> {{ member.time }} </div>
|
||||
<div> {{ member.company_type }} </div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro CareerPreference(member) %}
|
||||
{% if member.preferred_functions or member.preferred_industries or member.preferred_location or member.dream_companies %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Career Preference") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.preferred_functions | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Functions:") }}</b>
|
||||
{% for function in member.preferred_functions %}
|
||||
<div class="description">{{ function.function }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_industries | length %}
|
||||
<div>
|
||||
<b>{{ _("Preferred Industries:") }}</b>
|
||||
{% for industry in member.preferred_industries %}
|
||||
<div class="description">{{ industry.industry }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.preferred_location %}
|
||||
<div>
|
||||
<b> {{ _("Preferred Locations:") }} </b>
|
||||
<div class="description"> {{ member.preferred_location }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if member.dream_companies %}
|
||||
<div>
|
||||
<b> {{ _("Dream Companies:") }} </b>
|
||||
<div class="description"> {{ member.dream_companies }} </div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Contact(member) %}
|
||||
{% if member.linkedin or member.medium or member.github %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Contact") }} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% if member.linkedin %}
|
||||
{% set linkedin = member.linkedin[:-1] if member.linkedin[-1] == "/" else member.linkedin %}
|
||||
<a class="button-links description" href="{{ member.linkedin }}">
|
||||
<img src="/assets/lms/icons/linkedin.svg"> {{ linkedin.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.medium %}
|
||||
<a class="button-links description" href="{{ member.medium}}">
|
||||
<img src="/assets/lms/icons/medium.svg"> {{ member.medium.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if member.github %}
|
||||
<a class="button-links description" href="{{ member.github }}">
|
||||
<img src="/assets/lms/icons/github.svg"> {{ member.github.split("/")[-1] }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Skills(member) %}
|
||||
{% if member.skill | length %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Skills")}} </div>
|
||||
<div class="profile-column-grid">
|
||||
{% for skill in member.skill %}
|
||||
<div class="description"> {{ skill.skill_name }} </div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro EducationDetails(member) %}
|
||||
{% if member.education %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Education") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for edu in member.education %}
|
||||
<div class="profile-card-row">
|
||||
<div class="bold-title"> {{ edu.institution_name }} </div>
|
||||
<div class="profile-item"> {{ edu.degree_type }} <span></span> {{ edu.major }}
|
||||
{% if not member.hide_private %}
|
||||
<!-- {% if edu.grade_type %} {{ edu.grade_type }} {% endif %} -->
|
||||
{% if edu.grade %} <span></span> {{ edu.grade }} {% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="description">
|
||||
{% if edu.start_date %}
|
||||
{{ frappe.utils.format_date(edu.start_date, "MMM YYYY") }} -
|
||||
{% endif %}
|
||||
{{ frappe.utils.format_date(edu.end_date, "MMM YYYY") }} </div>
|
||||
<div class="description"> {{ edu.location }} </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro WorkDetails(member) %}
|
||||
{% set work_details = member.work_experience + member.internship %}
|
||||
{% if work_details | length %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Work Experience") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for work in work_details %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ work.title }} </div>
|
||||
<div class="profile-item"> {{ work.company }} </div>
|
||||
<div class="description"> {{ frappe.utils.format_date(work.from_date, "MMM YYYY") }} -
|
||||
{% if work.to_date %} {{ frappe.utils.format_date(work.to_date, "MMM YYYY") }} {% else %} Present {% endif %} </div>
|
||||
<div class="description"> {{ work.location }} </div>
|
||||
{% if work.description %} <div class="profile-item"> {{ work.description }} </div> {% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro Certification(member) %}
|
||||
{% if member.certification %}
|
||||
<div class="education-details">
|
||||
<div class="common-card-style profile-card">
|
||||
<div class="course-home-headings"> {{ _("Certification") }} </div>
|
||||
<div class="profile-grid-card">
|
||||
{% for cert in member.certification %}
|
||||
<div class="">
|
||||
<div class="bold-title"> {{ cert.certification_name }} </div>
|
||||
<div class="profile-item"> {{ cert.organization }} </div>
|
||||
<div class="description"> {{ frappe.utils.format_date(cert.issue_date, "MMM YYYY") }}
|
||||
{% if cert.expiration_date %} - {{ frappe.utils.format_date(cert.expiration_date, "MMM YYYY") }} {% endif %} </div>
|
||||
{% if cert.description %} <div class="profile-item"> {{ cert.description }} </div> {% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
if ("{{ member.name }}" == frappe.session.user) {
|
||||
setTimeout(() => {
|
||||
var link_array = $('.nav-link').filter((i, elem) => $(elem).text().trim() === "My Profile");
|
||||
link_array.length && $(link_array[0]).addClass("active");
|
||||
}, 0)
|
||||
}
|
||||
|
||||
if ($(".profile-column-one").children().length == 0) {
|
||||
$(".profile-column-one").hide();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
29
lms/www/profiles/profile.py
Normal file
29
lms/www/profiles/profile.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
from lms.page_renderers import get_profile_url_prefix
|
||||
from urllib.parse import urlencode
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
username = frappe.form_dict["username"]
|
||||
except KeyError:
|
||||
username = frappe.db.get_value("User", frappe.session.user, ["username"])
|
||||
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.
|
||||
|
||||
Each ProfileTab is rendered as a tab on the profile page and the
|
||||
they are specified as profile_tabs hook.
|
||||
"""
|
||||
tabs = frappe.get_hooks("profile_tabs") or []
|
||||
return [frappe.get_attr(tab)(user) for tab in tabs]
|
||||
34
lms/www/utils.py
Normal file
34
lms/www/utils.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import frappe
|
||||
from lms.lms.utils import slugify, get_membership, get_lessons, get_batch, get_lesson_url
|
||||
def get_common_context(context):
|
||||
context.no_cache = 1
|
||||
|
||||
try:
|
||||
batch_name = frappe.form_dict["batch"]
|
||||
except KeyError:
|
||||
batch_name = None
|
||||
|
||||
course = frappe.db.get_value("LMS Course",
|
||||
frappe.form_dict["course"], ["name", "title", "video_link"], as_dict=True)
|
||||
if not course:
|
||||
context.template = "www/404.html"
|
||||
return
|
||||
context.course = course
|
||||
context.lessons = get_lessons(course.name)
|
||||
membership = get_membership(course.name, frappe.session.user, batch_name)
|
||||
context.membership = membership
|
||||
if membership:
|
||||
batch = get_batch(course.name, membership.batch)
|
||||
|
||||
if batch:
|
||||
context.batch = batch
|
||||
|
||||
context.course.query_parameter = "?batch=" + membership.batch if membership and membership.batch else ""
|
||||
context.livecode_url = get_livecode_url()
|
||||
|
||||
def get_livecode_url():
|
||||
return frappe.db.get_single_value("LMS Settings", "livecode_url")
|
||||
|
||||
def redirect_to_lesson(course, index_="1.1"):
|
||||
frappe.local.flags.redirect_location = get_lesson_url(course.name, index_) + course.query_parameter
|
||||
raise frappe.Redirect
|
||||
Reference in New Issue
Block a user