diff --git a/lms/lms/doctype/lms_class/lms_class.py b/lms/lms/doctype/lms_class/lms_class.py index 4a91416d..b2186d5d 100644 --- a/lms/lms/doctype/lms_class/lms_class.py +++ b/lms/lms/doctype/lms_class/lms_class.py @@ -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"] diff --git a/lms/lms/doctype/lms_live_class/__init__.py b/lms/lms/doctype/lms_live_class/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.js b/lms/lms/doctype/lms_live_class/lms_live_class.js new file mode 100644 index 00000000..ebba6c7d --- /dev/null +++ b/lms/lms/doctype/lms_live_class/lms_live_class.js @@ -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) { + +// }, +// }); diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.json b/lms/lms/doctype/lms_live_class/lms_live_class.json new file mode 100644 index 00000000..814bc662 --- /dev/null +++ b/lms/lms/doctype/lms_live_class/lms_live_class.json @@ -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 +} \ No newline at end of file diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.py b/lms/lms/doctype/lms_live_class/lms_live_class.py new file mode 100644 index 00000000..24d239db --- /dev/null +++ b/lms/lms/doctype/lms_live_class/lms_live_class.py @@ -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() diff --git a/lms/lms/doctype/lms_live_class/test_lms_live_class.py b/lms/lms/doctype/lms_live_class/test_lms_live_class.py new file mode 100644 index 00000000..b169369a --- /dev/null +++ b/lms/lms/doctype/lms_live_class/test_lms_live_class.py @@ -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 diff --git a/lms/lms/doctype/zoom_settings/__init__.py b/lms/lms/doctype/zoom_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lms/lms/doctype/zoom_settings/test_zoom_settings.py b/lms/lms/doctype/zoom_settings/test_zoom_settings.py new file mode 100644 index 00000000..3162d3dc --- /dev/null +++ b/lms/lms/doctype/zoom_settings/test_zoom_settings.py @@ -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 diff --git a/lms/lms/doctype/zoom_settings/zoom_settings.js b/lms/lms/doctype/zoom_settings/zoom_settings.js new file mode 100644 index 00000000..70710042 --- /dev/null +++ b/lms/lms/doctype/zoom_settings/zoom_settings.js @@ -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) { + +// }, +// }); diff --git a/lms/lms/doctype/zoom_settings/zoom_settings.json b/lms/lms/doctype/zoom_settings/zoom_settings.json new file mode 100644 index 00000000..a20a6b5e --- /dev/null +++ b/lms/lms/doctype/zoom_settings/zoom_settings.json @@ -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 \n\"APIs & Services\" > \"Credentials\"\n", + "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 +} \ No newline at end of file diff --git a/lms/lms/doctype/zoom_settings/zoom_settings.py b/lms/lms/doctype/zoom_settings/zoom_settings.py new file mode 100644 index 00000000..d7ed9efb --- /dev/null +++ b/lms/lms/doctype/zoom_settings/zoom_settings.py @@ -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 diff --git a/lms/public/css/style.css b/lms/public/css/style.css index 94a244cb..d8e6b3aa 100644 --- a/lms/public/css/style.css +++ b/lms/public/css/style.css @@ -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; +} diff --git a/lms/public/icons/symbol-defs.svg b/lms/public/icons/symbol-defs.svg index 167397e1..37a0ec86 100644 --- a/lms/public/icons/symbol-defs.svg +++ b/lms/public/icons/symbol-defs.svg @@ -70,4 +70,8 @@ + diff --git a/lms/www/classes/class.html b/lms/www/classes/class.html index 87a73f2d..50270274 100644 --- a/lms/www/classes/class.html +++ b/lms/www/classes/class.html @@ -5,10 +5,10 @@ {% block page_content %} -
{{ _("No Students are added to this class.") }}
+{{ _("No Students are added to this class.") }}
{% endif %}