@@ -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"]
|
||||
|
||||
0
lms/lms/doctype/lms_live_class/__init__.py
Normal file
0
lms/lms/doctype/lms_live_class/__init__.py
Normal file
8
lms/lms/doctype/lms_live_class/lms_live_class.js
Normal file
8
lms/lms/doctype/lms_live_class/lms_live_class.js
Normal 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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
164
lms/lms/doctype/lms_live_class/lms_live_class.json
Normal file
164
lms/lms/doctype/lms_live_class/lms_live_class.json
Normal 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
|
||||
}
|
||||
63
lms/lms/doctype/lms_live_class/lms_live_class.py
Normal file
63
lms/lms/doctype/lms_live_class/lms_live_class.py
Normal 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()
|
||||
9
lms/lms/doctype/lms_live_class/test_lms_live_class.py
Normal file
9
lms/lms/doctype/lms_live_class/test_lms_live_class.py
Normal 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
|
||||
0
lms/lms/doctype/zoom_settings/__init__.py
Normal file
0
lms/lms/doctype/zoom_settings/__init__.py
Normal file
9
lms/lms/doctype/zoom_settings/test_zoom_settings.py
Normal file
9
lms/lms/doctype/zoom_settings/test_zoom_settings.py
Normal 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
|
||||
8
lms/lms/doctype/zoom_settings/zoom_settings.js
Normal file
8
lms/lms/doctype/zoom_settings/zoom_settings.js
Normal 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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
71
lms/lms/doctype/zoom_settings/zoom_settings.json
Normal file
71
lms/lms/doctype/zoom_settings/zoom_settings.json
Normal 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 & Services\" > \"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
|
||||
}
|
||||
9
lms/lms/doctype/zoom_settings/zoom_settings.py
Normal file
9
lms/lms/doctype/zoom_settings/zoom_settings.py
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 %}
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user