Merge pull request #487 from pateljannat/live-class

feat: live class
This commit is contained in:
Jannat Patel
2023-03-16 12:30:05 +05:30
committed by GitHub
18 changed files with 957 additions and 66 deletions

View File

@@ -4,7 +4,10 @@
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
from frappe.utils import cint, format_date, format_datetime
import requests
import base64
import json
class LMSClass(Document):
@@ -85,3 +88,72 @@ def update_course(class_name, course, value):
else:
frappe.db.delete("Class Course", {"parent": class_name, "course": course})
return True
@frappe.whitelist()
def create_live_class(
class_name, title, duration, date, time, timezone, auto_recording, description=None
):
date = format_date(date, "yyyy-mm-dd", True)
payload = {
"topic": title,
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
"duration": duration,
"agenda": description,
"private_meeting": True,
"auto_recording": "none"
if auto_recording == "No Recording"
else auto_recording.lower(),
"timezone": timezone,
}
headers = {
"Authorization": "Bearer " + authenticate(),
"content-type": "application/json",
}
response = requests.post(
"https://api.zoom.us/v2/users/me/meetings", headers=headers, data=json.dumps(payload)
)
if response.status_code == 201:
data = json.loads(response.text)
payload.update(
{
"doctype": "LMS Live Class",
"start_url": data.get("start_url"),
"join_url": data.get("join_url"),
"title": title,
"host": frappe.session.user,
"date": date,
"time": time,
"class_name": class_name,
"password": data.get("password"),
"description": description,
"auto_recording": auto_recording,
}
)
class_details = frappe.get_doc(payload)
class_details.save()
return class_details
def authenticate():
zoom = frappe.get_single("Zoom Settings")
if not zoom.enable:
frappe.throw(_("Please enable Zoom Settings to use this feature."))
authenticate_url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={zoom.account_id}"
headers = {
"Authorization": "Basic "
+ base64.b64encode(
bytes(
zoom.client_id
+ ":"
+ zoom.get_password(fieldname="client_secret", raise_exception=False),
encoding="utf8",
)
).decode()
}
response = requests.request("POST", authenticate_url, headers=headers)
return response.json()["access_token"]

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Live Class", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,164 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-03-02 10:59:01.741349",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"host",
"class_name",
"password",
"auto_recording",
"column_break_astv",
"description",
"section_break_glxh",
"date",
"timezone",
"column_break_spvt",
"time",
"duration",
"section_break_yrpq",
"start_url",
"column_break_yokr",
"join_url"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "duration",
"fieldtype": "Int",
"label": "Duration",
"reqd": 1
},
{
"fieldname": "timezone",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Timezone",
"reqd": 1
},
{
"fieldname": "host",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Host",
"options": "User",
"reqd": 1
},
{
"fieldname": "column_break_astv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_glxh",
"fieldtype": "Section Break",
"label": "Date and Time"
},
{
"fieldname": "column_break_spvt",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_yrpq",
"fieldtype": "Section Break"
},
{
"fieldname": "start_url",
"fieldtype": "Small Text",
"label": "Start URL",
"read_only": 1
},
{
"fieldname": "column_break_yokr",
"fieldtype": "Column Break"
},
{
"fieldname": "join_url",
"fieldtype": "Small Text",
"label": "Join URL",
"read_only": 1
},
{
"fieldname": "password",
"fieldtype": "Password",
"label": "Password"
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"reqd": 1
},
{
"fieldname": "class_name",
"fieldtype": "Link",
"label": "Class",
"options": "LMS Class"
},
{
"default": "No Recording",
"fieldname": "auto_recording",
"fieldtype": "Select",
"label": "Auto Recording",
"options": "No Recording\nLocal\nCloud"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-03-14 18:44:48.813102",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Live Class",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,63 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from datetime import timedelta
from frappe.utils import cint, get_datetime
class LMSLiveClass(Document):
def after_insert(self):
calendar = frappe.db.get_value(
"Google Calendar", {"user": frappe.session.user, "enable": 1}, "name"
)
if calendar:
event = self.create_event()
self.add_event_participants(event, calendar)
def create_event(self):
start = f"{self.date} {self.time}"
event = frappe.get_doc(
{
"doctype": "Event",
"subject": f"Live Class on {self.title}",
"starts_on": start,
"ends_on": get_datetime(start) + timedelta(minutes=cint(self.duration)),
}
)
event.save()
return event
def add_event_participants(self, event, calendar):
participants = frappe.get_all(
"Class Student", {"parent": self.class_name}, pluck="student"
)
participants.append(frappe.session.user)
for participant in participants:
frappe.get_doc(
{
"doctype": "Event Participants",
"reference_doctype": "User",
"reference_docname": participant,
"email": participant,
"parent": event.name,
"parenttype": "Event",
"parentfield": "event_participants",
}
).save()
event.reload()
event.update(
{
"sync_with_google_calendar": 1,
"google_calendar": calendar,
"description": f"A Live Class has been scheduled on {frappe.utils.format_date(self.date, 'medium')} at { frappe.utils.format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
}
)
event.save()

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLMSLiveClass(FrappeTestCase):
pass

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestZoomSettings(FrappeTestCase):
pass

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Zoom Settings", {
// refresh(frm) {
// },
// });

View File

@@ -0,0 +1,71 @@
{
"actions": [],
"creation": "2023-02-27 14:30:28.696814",
"default_view": "List",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable",
"sb_00",
"account_id",
"client_id",
"client_secret"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "sb_00",
"fieldtype": "Section Break",
"label": "OAuth Client ID"
},
{
"description": "The Client ID obtained from the Google Cloud Console under <a href=\"https://console.cloud.google.com/apis/credentials\">\n\"APIs &amp; Services\" &gt; \"Credentials\"\n</a>",
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
"mandatory_depends_on": "google_drive_picker_enabled"
},
{
"fieldname": "client_secret",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Client Secret"
},
{
"fieldname": "account_id",
"fieldtype": "Data",
"label": "Account ID"
}
],
"issingle": 1,
"links": [],
"modified": "2023-03-01 17:15:59.722497",
"modified_by": "Administrator",
"module": "LMS",
"name": "Zoom Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ZoomSettings(Document):
pass

View File

@@ -1776,7 +1776,7 @@ li {
}
.modal-content {
font-size: var(--text-base) !important;
font-size: var(--text-sm) !important;
}
.modal-header, .modal-body {
@@ -1790,11 +1790,14 @@ li {
.modal-footer {
padding: 0.75rem 1.5rem !important;
border-top: none !important;
background-color: var(--gray-200) !important;
background-color: var(--gray-50) !important;
justify-content: flex-end !important;
}
.modal-header .modal-title {
color: var(--gray-900);
line-height: 1.5rem;
margin-bottom: 0.5rem;
}
.frappe-chart .title {
@@ -1977,3 +1980,50 @@ select {
.common-page-style .tooltip-content {
display: none;
}
.resize-none {
resize: none;
}
.lms-page-style {
background-color: var(--fg-color);
font-size: var(--text-base);
}
.lms-card {
display: flex;
flex-direction: column;
border-radius: 0.75rem;
/* border: 1px solid var(--gray-200); */
box-shadow: var(--shadow-sm);
padding: 0.5rem;
height: 100%;
position: relative;
}
.live-class-panel {
margin-top: auto;
}
.lms-card .live-class-panel .btn {
visibility: hidden;
}
.lms-card:hover .live-class-panel .btn {
visibility: visible;
}
.add-students ul li:nth-last-child(-n+2) {
display: none;
}
.lms-card-title {
color: var(--gray-900);
font-weight: 500;
}
.lms-card-parent {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1.5rem;
}

View File

@@ -70,4 +70,8 @@
<svg id="icon-green-check-circled" width="24" height="24" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM16.8734 10.1402C17.264 9.74969 17.264 9.11652 16.8734 8.726C16.4829 8.33547 15.8498 8.33547 15.4592 8.726L14.6259 9.55933L12.9592 11.226L10.333 13.8522L9.37345 12.8927L8.54011 12.0593C8.14959 11.6688 7.51643 11.6688 7.1259 12.0593C6.73538 12.4499 6.73538 13.083 7.1259 13.4735L7.95923 14.3069L9.6259 15.9735C9.81344 16.1611 10.0678 16.2664 10.333 16.2664C10.5982 16.2664 10.8526 16.1611 11.0401 15.9735L14.3734 12.6402L16.0401 10.9735L16.8734 10.1402Z" fill="#68D391"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="icon-clock" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1F272E" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -5,10 +5,10 @@
{% block page_content %}
<div class="common-page-style">
<div class="common-page-style lms-page-style">
<div class="container">
{{ BreadCrumb(class_info) }}
<div class="common-card-style column-card">
<div class="">
{{ ClassDetails(class_info) }}
{{ ClassSections(class_info, class_courses, class_students, published_courses) }}
</div>
@@ -30,23 +30,23 @@
<!-- Class Details -->
{% macro ClassDetails(class_info) %}
<div class="class-details" data-class="{{ class_info.name }}">
<div class="medium pull-right">
{% if class_info.start_date %}
<span>
{{ frappe.utils.format_date(class_info.start_date, "medium") }} -
</span>
{% endif %}
{% if class_info.end_date %}
<span>
{{ frappe.utils.format_date(class_info.end_date, "medium") }}
</span>
{% endif %}
</div>
<div class="course-home-headings">
{{ class_info.title }}
</div>
<div class="mt-2">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(class_info.start_date, "long") }} -
</span>
<span>
{{ frappe.utils.format_date(class_info.end_date, "long") }}
</span>
</div>
{% if class_info.description %}
<div class="medium">
<div class="">
{{ class_info.description }}
</div>
{% endif %}
@@ -74,6 +74,14 @@
</a>
</li>
{% if class_students | length and (is_moderator or is_student) %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#live-class">
{{ _("Live Class") }}
</a>
</li>
{% endif %}
</ul>
<div class="border-bottom mb-4"></div>
@@ -87,6 +95,12 @@
{{ StudentsSection(class_info, class_students) }}
</div>
{% if class_students | length and (is_moderator or is_student) %}
<div class="tab-pane" id="live-class" role="tabpanel" aria-labelledby="live-class">
{{ LiveClassSection(class_info, live_classes) }}
</div>
{% endif %}
</div>
</div>
{% endmacro %}
@@ -109,7 +123,7 @@
{% macro StudentsSection(class_info, class_students) %}
<div class="medium">
<div class="">
{% if is_moderator %}
{{ AddStudents() }}
{% endif %}
@@ -151,7 +165,7 @@
</table>
{% else %}
<p class="text-muted mt-3"> {{ _("No Students are added to this class.") }} </p>
<p class="text-muted mt-3 ml-5"> {{ _("No Students are added to this class.") }} </p>
{% endif %}
</div>
@@ -160,19 +174,127 @@
{% macro AddStudents() %}
<div class="mb-10">
<div class="mb-2">
{{ _("Add Student") }}
</div>
<form>
<div class="control-input-wrapper mb-2 w-50">
<div class="control-input">
<input type="text" autocomplete="off" class="input-with-feedback form-control" id="student-email"
spellcheck="false">
</div>
</div>
<button class="btn btn-primary btn-sm" id="submit-student">
{{ _("Add") }}
</button>
</form>
<div class="add-students"></div>
<button class="btn btn-primary btn-sm ml-5" id="submit-student">
{{ _("Add") }}
</button>
</div>
{% endmacro %}
{% macro LiveClassSection(class_info, live_classes) %}
<div>
{{ CreateLiveClass(class_info) }}
{{ LiveClassList(class_info, live_classes) }}
</div>
{% endmacro %}
{% macro CreateLiveClass(class_info) %}
{% if is_moderator %}
<button class="btn btn-secondary btn-sm" id="open-class-modal">
{{ _("Create a Live Class") }}
</button>
<div class="modal fade live-class-modal" id="live-class-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">{{ _("Live Class Details") }}</div>
</div>
<div class="modal-body">
<form class="live-class-form" id="live-class-form"></form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
{{ _("Discard") }}
</button>
<button class="btn btn-primary btn-sm" id="create-live-class">
{{ _("Submit") }}
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endmacro %}
{% macro LiveClassList(class_info, live_classes) %}
<div class="lms-card-parent mt-8">
{% for class in live_classes %}
<div class="lms-card">
<div class="mb-0">
<div class="dropdown pull-right">
<svg class="icon icon-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<use href="#icon-dot-horizontal"></use>
</svg>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{% if class.owner == frappe.session.user %}
<li>
<a class="dropdown-item small" href="{{ class.start_url }}"> {{ _("Start") }} </a>
</li>
{% endif %}
{% if is_student %}
<li>
<a class="dropdown-item small" href="{{ class.join_url }}"> {{ _("Join") }} </a>
</li>
{% endif %}
</ul>
</div>
<div class="lms-card-title mb-4">
{{ class.title }}
</div>
</div>
<div>
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</svg>
{{ frappe.utils.format_date(class.date, "full") }}
</div>
<div>
<svg class="icon icon-sm">
<use href="#icon-clock"></use>
</svg>
{{ frappe.utils.format_time(class.time, "hh:mm a") }}
</div>
<div class="mt-4">
{{ class.description }}
</div>
</div>
{% endfor %}
</div>
{% endmacro %}
{%- block script %}
{{ super() }}
{% if is_moderator %}
<script>
frappe.boot.user = {
"can_create": [],
"can_select": ["User"],
"can_read": ["User"]
};
frappe.router = {
slug (name) {
return name.toLowerCase().replace(/ /g, "-");
}
}
</script>
{% endif %}
{{ include_script('controls.bundle.js') }}
{% endblock %}

View File

@@ -10,27 +10,46 @@ frappe.ready(() => {
$(".class-course").click((e) => {
update_course(e);
});
if ($("#live-class-form").length) {
make_live_class_form();
}
if ($(".add-students").length) {
make_add_students_section();
}
$("#open-class-modal").click((e) => {
e.preventDefault();
$("#live-class-modal").modal("show");
});
$("#create-live-class").click((e) => {
create_live_class(e);
});
});
const submit_student = (e) => {
e.preventDefault();
frappe.call({
method: "lms.lms.doctype.lms_class.lms_class.add_student",
args: {
email: $("#student-email").val(),
class_name: $(".class-details").data("class"),
},
callback: (data) => {
frappe.show_alert(
{
message: __("Student added successfully"),
indicator: "green",
},
3
);
window.location.reload();
},
});
if ($('input[data-fieldname="student_input"]').val()) {
frappe.call({
method: "lms.lms.doctype.lms_class.lms_class.add_student",
args: {
email: $('input[data-fieldname="student_input"]').val(),
class_name: $(".class-details").data("class"),
},
callback: (data) => {
frappe.show_alert(
{
message: __("Student added successfully"),
indicator: "green",
},
3
);
window.location.reload();
},
});
}
};
const remove_student = (e) => {
@@ -68,3 +87,269 @@ const update_course = (e) => {
},
});
};
const create_live_class = (e) => {
let class_name = $(".class-details").data("class");
frappe.call({
method: "lms.lms.doctype.lms_class.lms_class.create_live_class",
args: {
class_name: class_name,
title: $("input[data-fieldname='meeting_title']").val(),
duration: $("input[data-fieldname='meeting_duration']").val(),
date: $("input[data-fieldname='meeting_date']").val(),
time: $("input[data-fieldname='meeting_time']").val(),
timezone: $('select[data-fieldname="meeting_timezone"]').val(),
auto_recording: $(
'select[data-fieldname="meeting_recording"]'
).val(),
description: $(
"textarea[data-fieldname='meeting_description']"
).val(),
},
callback: (data) => {
$("#live-class-modal").modal("hide");
frappe.show_alert(
{
message: __("Live Class added successfully"),
indicator: "green",
},
3
);
setTimeout(function () {
window.location.reload();
}, 1000);
},
});
};
const make_live_class_form = (e) => {
this.field_group = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "meeting_title",
fieldtype: "Data",
options: "",
label: "Title",
reqd: 1,
},
{
fieldname: "meeting_time",
fieldtype: "Time",
options: "",
label: "Time",
reqd: 1,
},
{
fieldname: "meeting_timezone",
label: __("Time Zone"),
fieldtype: "Select",
options: get_timezones().join("\n"),
reqd: 1,
},
{
fieldname: "meeting_col",
fieldtype: "Column Break",
options: "",
},
{
fieldname: "meeting_date",
fieldtype: "Date",
options: "",
label: "Date",
reqd: 1,
},
{
fieldname: "meeting_duration",
fieldtype: "Int",
options: "",
label: "Duration (in Minutes)",
reqd: 1,
},
{
fieldname: "meeting_recording",
fieldtype: "Select",
options: "No Recording\nLocal\nCloud",
label: "Auto Recording",
default: "No Recording",
},
{
fieldname: "meeting_sec",
fieldtype: "Section Break",
options: "",
},
{
fieldname: "meeting_description",
fieldtype: "Small Text",
options: "",
max_height: 100,
min_lines: 5,
label: "Description",
},
],
body: $("#live-class-form").get(0),
});
this.field_group.make();
$("#live-class-form .form-section:last").removeClass("empty-section");
$("#live-class-form .frappe-control").removeClass("hide-control");
};
const get_timezones = () => {
return [
"Pacific/Midway",
"Pacific/Pago_Pago",
"Pacific/Honolulu",
"America/Anchorage",
"America/Vancouver",
"America/Los_Angeles",
"America/Tijuana",
"America/Edmonton",
"America/Denver",
"America/Phoenix",
"America/Mazatlan",
"America/Winnipeg",
"America/Regina",
"America/Chicago",
"America/Mexico_City",
"America/Guatemala",
"America/El_Salvador",
"America/Managua",
"America/Costa_Rica",
"America/Montreal",
"America/New_York",
"America/Indianapolis",
"America/Panama",
"America/Bogota",
"America/Lima",
"America/Halifax",
"America/Puerto_Rico",
"America/Caracas",
"America/Santiago",
"America/St_Johns",
"America/Montevideo",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Godthab",
"America/Sao_Paulo",
"Atlantic/Azores",
"Canada/Atlantic",
"Atlantic/Cape_Verde",
"UTC",
"Etc/Greenwich",
"Europe/Belgrade",
"CET",
"Atlantic/Reykjavik",
"Europe/Dublin",
"Europe/London",
"Europe/Lisbon",
"Africa/Casablanca",
"Africa/Nouakchott",
"Europe/Oslo",
"Europe/Copenhagen",
"Europe/Brussels",
"Europe/Berlin",
"Europe/Helsinki",
"Europe/Amsterdam",
"Europe/Rome",
"Europe/Stockholm",
"Europe/Vienna",
"Europe/Luxembourg",
"Europe/Paris",
"Europe/Zurich",
"Europe/Madrid",
"Africa/Bangui",
"Africa/Algiers",
"Africa/Tunis",
"Africa/Harare",
"Africa/Nairobi",
"Europe/Warsaw",
"Europe/Prague",
"Europe/Budapest",
"Europe/Sofia",
"Europe/Istanbul",
"Europe/Athens",
"Europe/Bucharest",
"Asia/Nicosia",
"Asia/Beirut",
"Asia/Damascus",
"Asia/Jerusalem",
"Asia/Amman",
"Africa/Tripoli",
"Africa/Cairo",
"Africa/Johannesburg",
"Europe/Moscow",
"Asia/Baghdad",
"Asia/Kuwait",
"Asia/Riyadh",
"Asia/Bahrain",
"Asia/Qatar",
"Asia/Aden",
"Asia/Tehran",
"Africa/Khartoum",
"Africa/Djibouti",
"Africa/Mogadishu",
"Asia/Dubai",
"Asia/Muscat",
"Asia/Baku",
"Asia/Kabul",
"Asia/Yekaterinburg",
"Asia/Tashkent",
"Asia/Calcutta",
"Asia/Kathmandu",
"Asia/Novosibirsk",
"Asia/Almaty",
"Asia/Dacca",
"Asia/Krasnoyarsk",
"Asia/Dhaka",
"Asia/Bangkok",
"Asia/Saigon",
"Asia/Jakarta",
"Asia/Irkutsk",
"Asia/Shanghai",
"Asia/Hong_Kong",
"Asia/Taipei",
"Asia/Kuala_Lumpur",
"Asia/Singapore",
"Australia/Perth",
"Asia/Yakutsk",
"Asia/Seoul",
"Asia/Tokyo",
"Australia/Darwin",
"Australia/Adelaide",
"Asia/Vladivostok",
"Pacific/Port_Moresby",
"Australia/Brisbane",
"Australia/Sydney",
"Australia/Hobart",
"Asia/Magadan",
"SST",
"Pacific/Noumea",
"Asia/Kamchatka",
"Pacific/Fiji",
"Pacific/Auckland",
"Asia/Kolkata",
"Europe/Kiev",
"America/Tegucigalpa",
"Pacific/Apia",
];
};
const make_add_students_section = () => {
this.field_group = new frappe.ui.FieldGroup({
fields: [
{
fieldname: "student_input",
fieldtype: "Link",
options: "User",
label: "Add Student",
filters: {
ignore_user_type: 1,
},
},
],
body: $(".add-students").get(0),
});
this.field_group.make();
$(".add-students .form-section:last").removeClass("empty-section");
$(".add-students .frappe-control").removeClass("hide-control");
};

View File

@@ -1,6 +1,7 @@
import frappe
from lms.lms.utils import has_course_moderator_role
from frappe import _
from frappe.utils import getdate
def get_context(context):
@@ -40,3 +41,13 @@ def get_context(context):
context.class_students = class_students
context.is_moderator = has_course_moderator_role()
students = [student.student for student in class_students]
context.is_student = frappe.session.user in students
context.live_classes = frappe.get_all(
"LMS Live Class",
{"class_name": class_name, "date": [">=", getdate()]},
["title", "description", "time", "date", "start_url", "join_url", "owner"],
order_by="date",
)

View File

@@ -4,10 +4,10 @@
{% endblock %}
{% block page_content %}
<div class="common-page-style">
<div class="common-page-style lms-page-style">
<div class="container">
{% if has_course_moderator_role() %}
<a class="btn btn-default btn-sm pull-right" href="/class/new">
<a class="btn btn-secondary btn-sm pull-right" href="/class/new">
{{ _("Create Class") }}
</a>
{% endif %}
@@ -29,36 +29,40 @@
{% macro ClassCards(classes) %}
<div class="cards-parent">
<div class="lms-card-parent">
{% for class in classes %}
{% set course_count = frappe.db.count("Class Course", {"parent": class.name}) %}
{% set student_count = frappe.db.count("Class Student", {"parent": class.name}) %}
<div class="common-card-style column-card">
<div class="text-muted small">
<div class="lms-card">
<div class="lms-card-title">
{{ class.title }}
</div>
<div class="text-muted small mb-4">
{% if course_count %}
<span>
<span class="mr-3">
{{ course_count }} {{ _("Courses") }}
</span>
{% endif %}
{% if student_count %}
<span class="ml-3">
<span>
{{ student_count }} {{ _("Students") }}
</span>
{% endif %}
</div>
<div class="course-card-title mb-4">
{{ class.title }}
</div>
<div class="">
<svg class="icon icon-sm">
<use href="#icon-calendar"></use>
</svg>
<span>
{{ frappe.utils.format_date(class.start_date, "medium") }} -
{{ frappe.utils.format_date(class.start_date, "long") }} -
</span>
<span>
{{ frappe.utils.format_date(class.end_date, "medium") }}
{{ frappe.utils.format_date(class.end_date, "long") }}
</span>
</div>
<a class="stretched-link" href="/classes/{{ class.name }}"></a>

View File

@@ -1,10 +1,12 @@
import frappe
from frappe import _
from frappe.utils import getdate
def get_context(context):
context.no_cache = 1
context.classes = frappe.get_all(
"LMS Class", fields=["name", "title", "start_date", "end_date"]
"LMS Class",
{"end_date": [">=", getdate()]},
["name", "title", "start_date", "end_date"],
)