Merge pull request #614 from pateljannat/timetable

feat: Batch Timetable
This commit is contained in:
Jannat Patel
2023-09-20 13:07:09 +05:30
committed by GitHub
23 changed files with 688 additions and 159 deletions

View File

@@ -105,13 +105,10 @@
</a>
</li>
{% if flow | length %}
{% if show_timetable %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#schedule">
{{ _("Schedule") }}
<span class="course-list-count">
{{ flow | length }}
</span>
<a class="nav-link" data-toggle="tab" href="#timetable">
{{ _("Timetable") }}
</a>
</li>
{% endif %}
@@ -169,9 +166,9 @@
{{ CoursesSection(batch_info, batch_courses) }}
</div>
{% if flow | length %}
<div class="tab-pane" id="schedule" role="tabpanel" aria-labelledby="schedule">
{{ ScheduleSection(flow) }}
{% if show_timetable %}
<div class="tab-pane" id="timetable" role="tabpanel" aria-labelledby="timetable">
{{ Timetable() }}
</div>
{% endif %}
@@ -513,79 +510,38 @@
{% endmacro %}
{% macro ScheduleSection(flow) %}
{% macro Timetable() %}
<article>
<header class="edit-header mb-5">
<div class="bold-heading">
{{ _("Schedule") }}
{{ _("Timetable") }}
</div>
</header>
<div>
{% for chapter in flow %}
<div class="chapter-parent">
<div class="chapter-title" data-toggle="collapse" data-target="#{{ get_slugified_chapter_title(chapter.chapter_title) }}">
<img class="chapter-icon" src="/assets/lms/icons/chevron-right.svg">
<div class="chapter-title-main">
{{ chapter.chapter_title }}
</div>
</div>
<div class="chapter-content lessons collapse navbar-collapse" id="{{ get_slugified_chapter_title(chapter.chapter_title) }}">
<div class="schedule-header">
<div class="w-50">
{{ _("Lesson") }}
</div>
<div class="w-25">
{{ _("Date") }}
</div>
<div class="w-25 text-center">
{{ _("Start Time") }}
</div>
<div class="w-25 text-center">
{{ _("End Time") }}
</div>
</div>
{% for lesson in chapter.lessons %}
<div class="lesson-info flex align-center">
<a class="lesson-links w-50" href="{{ lesson.url }}">
<svg class="icon icon-sm mr-2">
<use class="" href="#{{ lesson.icon }}">
</svg>
{{ lesson.title }}
{% if current_student.name and get_membership(lesson.course, current_student.name) %}
{% set lesson_progress = get_progress(lesson.course, lesson.name, current_student.name) %}
<svg class="icon icon-md lesson-progress-tick ml-3 {% if lesson_progress != 'Complete' %} hide {% endif %}">
<use class="" href="#icon-success">
</svg>
{% endif %}
</a>
<div class="w-25">
{{ frappe.utils.format_date(lesson.date, "medium") }}
</div>
<div class="w-25 text-center">
{% if lesson.start_time %}
{{ frappe.utils.format_time(lesson.start_time, "HH:mm a") }}
{% else %}
-
{% endif %}
</div>
<div class="w-25 text-center">
{% if lesson.end_time %}
{{ frappe.utils.format_time(lesson.end_time, "HH:mm a") }}
{% else %}
-
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<div class="calendar-navigation">
<button class="btn icon-btn btn-default" id="prev-week">
<svg class="icon icon-md">
<use href="#icon-left"></use>
</svg>
</button>
<span class="calendar-range"></span>
<button class="btn icon-btn btn-default" id="next-week">
<svg class="icon icon-md">
<use href="#icon-right"></use>
</svg>
</button>
</div>
<div class="calendar-legends">
{% for legend in legends %}
<div class="legend-item">
<div class="legend-color" style="background-color: {{ legend.color }}"></div>
<div class="legend-text">{{ legend.title }}</div>
</div>
{% endfor %}
</div>
<div id="calendar" class="timetable-calendar" style="height: 700px"
data-start="{{ batch_info.start_time }}" data-end="{{ batch_info.end_time }}">
</div>
</article>
{% endmacro %}
@@ -595,4 +551,6 @@
frappe.boot.single_types = []
let courses = {{ course_list | json }};
</script>
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
{% endblock %}

View File

@@ -2,6 +2,24 @@ frappe.ready(() => {
let self = this;
frappe.require("controls.bundle.js");
if ($("#calendar").length) {
setup_timetable();
}
if ($("#calendar").length) {
$(document).on("click", "#prev-week", (e) => {
this.calendar_ && this.calendar_.prev();
set_calendar_range(this.calendar_, this.events);
});
}
if ($("#calendar").length) {
$(document).on("click", "#next-week", (e) => {
this.calendar_ && this.calendar_.next();
set_calendar_range(this.calendar_, this.events);
});
}
if ($("#live-class-form").length) {
setTimeout(() => {
make_live_class_form();
@@ -606,3 +624,144 @@ const submit_evaluation_form = (values) => {
},
});
};
const setup_timetable = () => {
let self = this;
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.get_batch_timetable",
args: {
batch: $(".class-details").data("batch"),
},
callback: (r) => {
if (r.message.length) {
setup_calendar(r.message);
self.events = r.message;
}
},
});
};
const setup_calendar = (events) => {
const element = $("#calendar");
const Calendar = tui.Calendar;
const calendar_id = "calendar1";
const container = element[0];
const options = get_calendar_options(element, calendar_id);
const calendar = new Calendar(container, options);
this.calendar_ = calendar;
console.log(options);
create_events(calendar, events);
add_links_to_events(calendar, events);
scroll_to_date(calendar, events);
set_calendar_range(calendar, events);
};
const get_calendar_options = (element, calendar_id) => {
const start_time = element.data("start");
const end_time = element.data("end");
return {
defaultView: "week",
usageStatistics: false,
week: {
narrowWeekend: true,
hourStart: parseInt(start_time.split(":")[0]) - 1,
/* hourEnd: parseInt(end_time.split(":")[0]) + 1, */
},
month: {
narrowWeekend: true,
},
taskView: false,
isReadOnly: true,
calendars: [
{
id: calendar_id,
name: "Timetable",
backgroundColor: "var(--fg-color)",
},
],
template: {
time: function (event) {
return `<div class="calendar-event-time">
<div> ${frappe.datetime.get_time(event.start.d.d)} -
${frappe.datetime.get_time(event.end.d.d)} </div>
<div class="calendar-event-title"> ${event.title} </div>
</div>`;
},
},
};
};
const create_events = (calendar, events, calendar_id) => {
let calendar_events = [];
events.forEach((event, idx) => {
calendar_events.push({
id: `event${idx}`,
calendarId: calendar_id,
title: event.title,
start: `${event.date}T${event.start_time}`,
end: `${event.date}T${event.end_time}`,
isAllday: event.start_time ? false : true,
borderColor: get_background_color(event.reference_doctype),
backgroundColor: "var(--fg-color)",
customStyle: {
borderRadius: "var(--border-radius-md)",
boxShadow: "var(--shadow-base)",
borderWidth: "8px",
padding: "0.25rem 0.5rem 0.5rem",
},
raw: {
url: event.url,
},
});
});
calendar.createEvents(calendar_events);
};
const add_links_to_events = (calendar, events) => {
calendar.on("clickEvent", ({ event }) => {
const el = document.getElementById("clicked-event");
window.open(event.raw.url, "_blank");
});
};
const scroll_to_date = (calendar, events) => {
if (
new Date() < new Date(events[0].date) ||
new Date() > new Date(events.slice(-1).date)
) {
calendar.setDate(new Date(events[0].date));
}
};
const set_calendar_range = (calendar, events) => {
let week_start = moment(calendar.getDateRangeStart().d.d);
let week_end = moment(calendar.getDateRangeEnd().d.d);
$(".calendar-range").text(
`${moment(week_start).format("DD MMMM YYYY")} - ${moment(
week_end
).format("DD MMMM YYYY")}`
);
if (week_start.diff(moment(events[0].date), "days") <= 0) {
$("#prev-week").hide();
} else {
$("#prev-week").show();
}
if (week_end.diff(moment(events.slice(-1)[0].date), "days") > 0) {
$("#next-week").hide();
} else {
$("#next-week").show();
}
};
const get_background_color = (doctype) => {
if (doctype == "Course Lesson") return "var(--blue-400)";
if (doctype == "LMS Quiz") return "var(--green-400)";
if (doctype == "LMS Assignment") return "var(--orange-400)";
if (doctype == "LMS Live Class") return "var(--purple-400)";
};

View File

@@ -1,6 +1,6 @@
from frappe import _
import frappe
from frappe.utils import getdate, cint
from frappe.utils import getdate, get_datetime
from lms.www.utils import get_assessments, is_student
from lms.lms.utils import (
has_course_moderator_role,
@@ -89,7 +89,13 @@ def get_context(context):
)
context.all_assignments = get_all_assignments(batch_name)
context.all_quizzes = get_all_quizzes(batch_name)
context.flow = get_scheduled_flow(batch_name)
context.show_timetable = frappe.db.count(
"LMS Batch Timetable",
{
"parent": batch_name,
},
)
context.legends = get_legends()
def get_all_quizzes(batch_name):
@@ -210,38 +216,6 @@ def sort_students(batch_students):
return batch_students
def get_scheduled_flow(batch_name):
chapters = []
lessons = frappe.get_all(
"Scheduled Flow",
{"parent": batch_name},
["name", "lesson", "date", "start_time", "end_time"],
order_by="idx",
)
for lesson in lessons:
lesson = get_lesson_details(lesson, batch_name)
chapter_exists = [
chapter for chapter in chapters if chapter.chapter == lesson.chapter
]
if len(chapter_exists) == 0:
chapters.append(
frappe._dict(
{
"chapter": lesson.chapter,
"chapter_title": frappe.db.get_value("Course Chapter", lesson.chapter, "title"),
"lessons": [lesson],
}
)
)
else:
chapter_exists[0]["lessons"].append(lesson)
return chapters
def get_lesson_details(lesson, batch_name):
lesson.update(
frappe.db.get_value(
@@ -277,3 +251,24 @@ def get_course_progress(batch_courses, student_details):
student_details.courses[course.course] = membership.progress
else:
student_details.courses[course.course] = 0
def get_legends():
return [
{
"title": "Lesson",
"color": "var(--blue-400)",
},
{
"title": "Quiz",
"color": "var(--green-400)",
},
{
"title": "Assignment",
"color": "var(--orange-400)",
},
{
"title": "Live Class",
"color": "var(--purple-400)",
},
]

View File

@@ -136,9 +136,9 @@
{% endif %}
<div class="mt-2">
{% if is_moderator or is_evaluator or is_student %}
{% if is_moderator or is_evaluator %}
<a class="btn btn-primary wide-button" href="/batches/{{ batch_info.name }}">
{{ _("Checkout Batch") }}
{{ _("Manage Batch") }}
</a>
{% elif batch_info.paid_batch %}
<a class="btn btn-primary wide-button {% if batch_info.seat_count and not seats_left %} hide {% endif %}"

View File

@@ -31,10 +31,15 @@ def get_context(context):
context.is_moderator = has_course_moderator_role()
context.is_evaluator = has_course_evaluator_role()
context.is_student = is_student(batch_name)
if not context.is_moderator and not context.batch_info.published:
raise frappe.PermissionError(_("You do not have permission to access this page."))
if context.is_student:
frappe.local.flags.redirect_location = f"/batches/{batch_name}"
raise frappe.Redirect
context.courses = frappe.get_all(
"Batch Course",
{"parent": batch_name},
@@ -51,5 +56,3 @@ def get_context(context):
context.student_count = frappe.db.count("Batch Student", {"parent": batch_name})
context.seats_left = context.batch_info.seat_count - context.student_count
context.is_student = is_student(batch_name)

View File

@@ -31,7 +31,6 @@ def get_context(context):
batch.seats_left = (
batch.seat_count - batch.student_count if batch.seat_count else None
)
print(batch.name, batch.published)
if not batch.published:
private_batches.append(batch)
elif getdate(batch.start_date) < getdate():

View File

@@ -24,7 +24,7 @@ def get_context(context):
def validate_access(doctype, docname, module):
if frappe.session.user == "Guest":
raise frappe.PermissionError(_("You are not allowed to access this page."))
raise frappe.PermissionError(_("Please login to continue with payment."))
if module not in ["course", "batch"]:
raise ValueError(_("Module is incorrect."))

View File

@@ -108,6 +108,8 @@ def get_assignment_details(assessment, member):
f"/assignment-submission/{assessment.assessment_name}/{submission_name}"
)
return assessment
def get_quiz_details(assessment, member):
assessment.title = frappe.db.get_value("LMS Quiz", assessment.assessment_name, "title")
@@ -131,6 +133,8 @@ def get_quiz_details(assessment, member):
)
assessment.url = f"/quiz-submission/{assessment.assessment_name}/{submission_name}"
return assessment
def is_student(batch, member=None):
if not member: