add initial zoom integration
This commit is contained in:
parent
8df69bf1c0
commit
1e04b5736d
Binary file not shown.
@ -183,4 +183,9 @@ UNFOLD = {
|
||||
"SCRIPTS": [
|
||||
lambda request: static("unfold/js/app.js"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
||||
SECRET_TOKEN = '6KdTGyF0SSCSL_V4Xa34aw'
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,7 +1,15 @@
|
||||
from django import forms
|
||||
from . import models
|
||||
from .models import ZoomMeeting, Candidate
|
||||
|
||||
class CandidateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.Candidate
|
||||
fields = ['name', 'email', 'resume']
|
||||
model = Candidate
|
||||
fields = ['name', 'email', 'resume']
|
||||
|
||||
class ZoomMeetingForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ZoomMeeting
|
||||
fields = ['topic', 'start_time', 'duration']
|
||||
widgets = {
|
||||
'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
||||
}
|
||||
36
recruitment/migrations/0005_zoommeeting.py
Normal file
36
recruitment/migrations/0005_zoommeeting.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-29 09:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0004_remove_candidate_status_candidate_applied'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ZoomMeeting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('topic', models.CharField(max_length=255)),
|
||||
('meeting_id', models.CharField(max_length=20, unique=True)),
|
||||
('start_time', models.DateTimeField()),
|
||||
('duration', models.PositiveIntegerField()),
|
||||
('timezone', models.CharField(max_length=50)),
|
||||
('join_url', models.URLField()),
|
||||
('password', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('host_email', models.EmailField(max_length=254)),
|
||||
('status', models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('ended', 'Ended')], default='waiting', max_length=10)),
|
||||
('host_video', models.BooleanField(default=True)),
|
||||
('participant_video', models.BooleanField(default=True)),
|
||||
('join_before_host', models.BooleanField(default=False)),
|
||||
('mute_upon_entry', models.BooleanField(default=False)),
|
||||
('waiting_room', models.BooleanField(default=False)),
|
||||
('zoom_gateway_response', models.JSONField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -36,4 +36,41 @@ class TrainingMaterial(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
def __str__(self):
|
||||
return self.title
|
||||
return self.title
|
||||
|
||||
|
||||
class ZoomMeeting(models.Model):
|
||||
# Basic meeting details
|
||||
topic = models.CharField(max_length=255)
|
||||
meeting_id = models.CharField(max_length=20, unique=True) # Unique identifier for the meeting
|
||||
start_time = models.DateTimeField()
|
||||
duration = models.PositiveIntegerField() # Duration in minutes
|
||||
timezone = models.CharField(max_length=50)
|
||||
join_url = models.URLField() # URL for participants to join
|
||||
password = models.CharField(max_length=50, blank=True, null=True)
|
||||
|
||||
# Host information
|
||||
host_email = models.EmailField()
|
||||
|
||||
# Status
|
||||
STATUS_CHOICES = [
|
||||
('waiting', 'Waiting'),
|
||||
('started', 'Started'),
|
||||
('ended', 'Ended'),
|
||||
]
|
||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='waiting')
|
||||
|
||||
# Settings
|
||||
host_video = models.BooleanField(default=True)
|
||||
participant_video = models.BooleanField(default=True)
|
||||
join_before_host = models.BooleanField(default=False)
|
||||
mute_upon_entry = models.BooleanField(default=False)
|
||||
waiting_room = models.BooleanField(default=False)
|
||||
|
||||
zoom_gateway_response = models.JSONField(blank=True, null=True)
|
||||
# Timestamps
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.topic
|
||||
@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from . import views_frontend
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('careers/', views_frontend.job_list, name='job_list'),
|
||||
@ -7,5 +8,12 @@ urlpatterns = [
|
||||
path('training/', views_frontend.training_list, name='training_list'),
|
||||
path('candidate/<int:candidate_id>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
||||
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
|
||||
|
||||
path('', views.ZoomMeetingListView.as_view(), name='list_meetings'),
|
||||
path('create-meeting/', views.ZoomMeetingCreateView.as_view(), name='create_meeting'),
|
||||
path('meeting-details/<int:pk>/', views.ZoomMeetingDetailsView.as_view(), name='meeting_details'),
|
||||
path('update-meeting/<int:pk>/', views.ZoomMeetingUpdateView.as_view(), name='update_meeting'),
|
||||
path('delete-meeting/<int:pk>/', views.ZoomMeetingDeleteView, name='delete_meeting'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -44,3 +44,270 @@ def dashboard_callback(request, context):
|
||||
|
||||
|
||||
|
||||
|
||||
def get_access_token():
|
||||
"""Obtain an access token using server-to-server OAuth."""
|
||||
client_id = settings.ZOOM_CLIENT_ID
|
||||
client_secret = settings.ZOOM_CLIENT_SECRET
|
||||
|
||||
auth_url = "https://zoom.us/oauth/token"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
data = {
|
||||
"grant_type": "account_credentials",
|
||||
"account_id": settings.ZOOM_ACCOUNT_ID,
|
||||
}
|
||||
|
||||
auth = (client_id, client_secret)
|
||||
|
||||
response = requests.post(auth_url, headers=headers, data=data, auth=auth)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json().get("access_token")
|
||||
else:
|
||||
raise Exception(f"Failed to obtain access token: {response.json()}")
|
||||
|
||||
def create_zoom_meeting(topic, start_time, duration):
|
||||
"""
|
||||
Create a Zoom meeting using the Zoom API.
|
||||
|
||||
Args:
|
||||
topic (str): The topic of the meeting.
|
||||
start_time (str): The start time of the meeting in ISO 8601 format (e.g., "2023-10-01T10:00:00Z").
|
||||
duration (int): The duration of the meeting in minutes.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the meeting details if successful, or an error message if failed.
|
||||
"""
|
||||
try:
|
||||
access_token = get_access_token()
|
||||
|
||||
meeting_details = {
|
||||
"topic": topic,
|
||||
"type": 2,
|
||||
"start_time": start_time,
|
||||
"duration": duration,
|
||||
"timezone": "UTC",
|
||||
"settings": {
|
||||
"host_video": True,
|
||||
"participant_video": True,
|
||||
"join_before_host": True,
|
||||
"mute_upon_entry": False,
|
||||
"approval_type": 2,
|
||||
"audio": "both",
|
||||
"auto_recording": "none"
|
||||
}
|
||||
}
|
||||
|
||||
# Make API request to Zoom to create the meeting
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.post(
|
||||
"https://api.zoom.us/v2/users/me/meetings",
|
||||
headers=headers,
|
||||
json=meeting_details
|
||||
)
|
||||
|
||||
# Check response status
|
||||
if response.status_code == 201:
|
||||
meeting_data = response.json()
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Meeting created successfully.",
|
||||
"meeting_details": {
|
||||
"join_url": meeting_data['join_url'],
|
||||
"meeting_id": meeting_data['id'],
|
||||
"password": meeting_data['password'],
|
||||
"host_email": meeting_data['host_email']
|
||||
},
|
||||
"zoom_gateway_response": meeting_data
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to create meeting.",
|
||||
"details": response.json()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
def list_zoom_meetings(next_page_token=None):
|
||||
"""
|
||||
List all meetings for a user using the Zoom API.
|
||||
|
||||
Args:
|
||||
next_page_token (str, optional): The token for paginated results. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the list of meetings or an error message.
|
||||
"""
|
||||
try:
|
||||
access_token = get_access_token()
|
||||
user_id = 'me'
|
||||
|
||||
params = {}
|
||||
if next_page_token:
|
||||
params['next_page_token'] = next_page_token
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.get(
|
||||
f"https://api.zoom.us/v2/users/{user_id}/meetings",
|
||||
headers=headers,
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
meetings_data = response.json()
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Meetings retrieved successfully.",
|
||||
"meetings": meetings_data.get("meetings", []),
|
||||
"next_page_token": meetings_data.get("next_page_token")
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to retrieve meetings.",
|
||||
"details": response.json()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
def get_zoom_meeting_details(meeting_id):
|
||||
"""
|
||||
Retrieve details of a specific meeting using the Zoom API.
|
||||
|
||||
Args:
|
||||
meeting_id (str): The ID of the meeting to retrieve.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the meeting details or an error message.
|
||||
"""
|
||||
try:
|
||||
access_token = get_access_token()
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.get(
|
||||
f"https://api.zoom.us/v2/meetings/{meeting_id}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
meeting_data = response.json()
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Meeting details retrieved successfully.",
|
||||
"meeting_details": meeting_data
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to retrieve meeting details.",
|
||||
"details": response.json()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
def update_zoom_meeting(meeting_id, updated_data):
|
||||
"""
|
||||
Update a Zoom meeting using the Zoom API.
|
||||
|
||||
Args:
|
||||
meeting_id (str): The ID of the meeting to update.
|
||||
updated_data (dict): A dictionary containing the fields to update (e.g., topic, start_time, duration).
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the updated meeting details or an error message.
|
||||
"""
|
||||
try:
|
||||
access_token = get_access_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.patch(
|
||||
f"https://api.zoom.us/v2/meetings/{meeting_id}",
|
||||
headers=headers,
|
||||
json=updated_data
|
||||
)
|
||||
|
||||
if response.status_code == 204:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Meeting updated successfully."
|
||||
}
|
||||
else:
|
||||
print(response.json())
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to update meeting.",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
def delete_zoom_meeting(meeting_id):
|
||||
"""
|
||||
Delete a Zoom meeting using the Zoom API.
|
||||
|
||||
Args:
|
||||
meeting_id (str): The ID of the meeting to delete.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary indicating success or failure.
|
||||
"""
|
||||
try:
|
||||
access_token = get_access_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
}
|
||||
response = requests.delete(
|
||||
f"https://api.zoom.us/v2/meetings/{meeting_id}",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 204:
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Meeting deleted successfully."
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Failed to delete meeting.",
|
||||
"details": response.json()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
@ -1,14 +1,110 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
import requests
|
||||
from django.views import View
|
||||
from datetime import datetime
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from .forms import ZoomMeetingForm
|
||||
from rest_framework import viewsets
|
||||
from . import models
|
||||
from . import serializers
|
||||
from django.contrib import messages
|
||||
from .models import ZoomMeeting, Job, Candidate
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from .serializers import JobSerializer, CandidateSerializer
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
||||
from .utils import create_zoom_meeting, delete_zoom_meeting, list_zoom_meetings, get_zoom_meeting_details, update_zoom_meeting
|
||||
|
||||
class JobViewSet(viewsets.ModelViewSet):
|
||||
queryset = models.Job.objects.all()
|
||||
serializer_class = serializers.JobSerializer
|
||||
queryset = Job.objects.all()
|
||||
serializer_class = JobSerializer
|
||||
|
||||
class CandidateViewSet(viewsets.ModelViewSet):
|
||||
queryset = models.Candidate.objects.all()
|
||||
serializer_class = serializers.CandidateSerializer
|
||||
queryset = Candidate.objects.all()
|
||||
serializer_class = CandidateSerializer
|
||||
|
||||
|
||||
class ZoomMeetingCreateView(CreateView):
|
||||
model = ZoomMeeting
|
||||
template_name = 'meetings/create_meeting.html'
|
||||
form_class = ZoomMeetingForm
|
||||
success_url = '/'
|
||||
|
||||
def form_valid(self, form):
|
||||
instance = form.save(commit=False)
|
||||
try:
|
||||
topic = instance.topic
|
||||
if instance.start_time < timezone.now():
|
||||
messages.error(self.request, "Start time must be in the future.")
|
||||
return redirect('/create-meeting/', status=400)
|
||||
start_time = instance.start_time.isoformat() + "Z"
|
||||
duration = instance.duration
|
||||
|
||||
result = create_zoom_meeting(topic, start_time, duration)
|
||||
|
||||
if result["status"] == "success":
|
||||
instance.meeting_id = result['meeting_details']['meeting_id']
|
||||
instance.join_url = result['meeting_details']['join_url']
|
||||
instance.host_email = result['meeting_details']['host_email']
|
||||
instance.zoom_gateway_response = result['zoom_gateway_response']
|
||||
instance.save()
|
||||
messages.success(self.request, result["message"])
|
||||
|
||||
return redirect('/', status=201)
|
||||
else:
|
||||
messages.error(self.request, result["message"])
|
||||
return redirect('/', status=400)
|
||||
except Exception as e:
|
||||
return redirect('/', status=500)
|
||||
|
||||
class ZoomMeetingListView(ListView):
|
||||
model = ZoomMeeting
|
||||
template_name = 'meetings/list_meetings.html'
|
||||
context_object_name = 'meetings'
|
||||
|
||||
class ZoomMeetingDetailsView(DetailView):
|
||||
model = ZoomMeeting
|
||||
template_name = 'meetings/meeting_details.html'
|
||||
context_object_name = 'meeting'
|
||||
|
||||
class ZoomMeetingUpdateView(UpdateView):
|
||||
model = ZoomMeeting
|
||||
form_class = ZoomMeetingForm
|
||||
context_object_name = 'meeting'
|
||||
template_name = 'meetings/update_meeting.html'
|
||||
success_url = '/'
|
||||
|
||||
def form_valid(self, form):
|
||||
instance = form.save(commit=False)
|
||||
updated_data = {
|
||||
'topic': instance.topic,
|
||||
'start_time': instance.start_time.isoformat() + "Z",
|
||||
'duration': instance.duration
|
||||
}
|
||||
if instance.start_time < timezone.now():
|
||||
messages.error(self.request, "Start time must be in the future.")
|
||||
return redirect(f'/update-meeting/{instance.pk}/', status=400)
|
||||
|
||||
result = update_zoom_meeting(instance.meeting_id, updated_data)
|
||||
if result["status"] == "success":
|
||||
instance.save()
|
||||
messages.success(self.request, result["message"])
|
||||
return redirect(reverse('meeting_details', kwargs={'pk': instance.pk}))
|
||||
else:
|
||||
messages.error(self.request, result["message"])
|
||||
return redirect(reverse('meeting_details', kwargs={'pk': instance.pk}))
|
||||
|
||||
def ZoomMeetingDeleteView(request, pk):
|
||||
meeting = get_object_or_404(ZoomMeeting, pk=pk)
|
||||
meeting_id = meeting.meeting_id
|
||||
try:
|
||||
result = delete_zoom_meeting(meeting_id)
|
||||
if result["status"] == "success":
|
||||
meeting.delete()
|
||||
messages.success(request, result["message"])
|
||||
else:
|
||||
messages.error(request, result["message"])
|
||||
return redirect('/')
|
||||
except Exception as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect('/')
|
||||
|
||||
191
templates/meetings/create_meeting.html
Normal file
191
templates/meetings/create_meeting.html
Normal file
@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Zoom Meeting</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #1b8354;
|
||||
--background-color: #fcfcfc;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
--card-bg: #ffffff;
|
||||
--secondary-text: #666;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--secondary-text);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Zoom Meeting Manager</h1>
|
||||
<p>Create a new Zoom meeting</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Create Meeting Form -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Create New Meeting</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-row">
|
||||
<label for="topic" class="form-label">Topic</label>
|
||||
<input type="text" class="form-input" id="topic" name="topic" placeholder="Enter meeting topic" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="start_time" class="form-label">Start Time (UTC)</label>
|
||||
<input type="datetime-local" class="form-input" id="start_time" name="start_time" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="duration" class="form-label">Duration (minutes)</label>
|
||||
<input type="number" class="form-input" id="duration" name="duration" value="60" min="1">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button type="submit" class="btn btn-primary">Create Meeting</button>
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
249
templates/meetings/list_meetings.html
Normal file
249
templates/meetings/list_meetings.html
Normal file
@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zoom Meetings</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #1b8354;
|
||||
--background-color: #fcfcfc;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
--card-bg: #ffffff;
|
||||
--secondary-text: #666;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meetings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.meeting-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 20px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.meeting-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.meeting-topic {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meeting-detail {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
min-width: 100px;
|
||||
color: var(--secondary-text);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background-color: #fff8e1;
|
||||
color: #ff8f00;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 15px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.85em;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ff4d4d;
|
||||
color: white;
|
||||
border: 1px solid #ff4d4d;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.meetings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Zoom Meetings</h1>
|
||||
<p>Your upcoming and past meetings</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'create_meeting' %}" class="btn btn-primary" style="margin-bottom: 20px;">Create Meeting</a>
|
||||
|
||||
{% if meetings %}
|
||||
<div class="meetings-grid">
|
||||
{% for meeting in meetings %}
|
||||
<div class="meeting-card">
|
||||
<div class="meeting-topic">{{ meeting.topic }}</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">ID:</div>
|
||||
<div class="detail-value">{{ meeting.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">Time:</div>
|
||||
<div class="detail-value">{{ meeting.start_time }}</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">Duration:</div>
|
||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">Status:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge status-waiting">{{ meeting.status|title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="display: flex; align-items: center;">
|
||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-primary">View</a>
|
||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-secondary" style="margin-left: 10px;">Update</a>
|
||||
<form method="post" action="{% url 'delete_meeting' meeting.pk %}" style="display:inline; margin-left: 10px;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if next_page_token %}
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<a href="?next_page_token={{ next_page_token }}" class="btn btn-primary">Load More</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="meeting-card">
|
||||
<p>No meetings found.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
254
templates/meetings/meeting_details.html
Normal file
254
templates/meetings/meeting_details.html
Normal file
@ -0,0 +1,254 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Meeting Details</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #1b8354;
|
||||
--background-color: #fcfcfc;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
--card-bg: #ffffff;
|
||||
--secondary-text: #666;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
min-width: 200px;
|
||||
color: var(--secondary-text);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background-color: #fff8e1;
|
||||
color: #ff8f00;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.detail-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Zoom Meeting Details</h1>
|
||||
<p>All information about your scheduled meeting</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Meeting Information</h2>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Topic:</div>
|
||||
<div class="detail-value">{{ meeting.topic }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Meeting ID:</div>
|
||||
<div class="detail-value">{{ meeting.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Status:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge status-waiting">{{ meeting.status|title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Start Time:</div>
|
||||
<div class="detail-value">{{ meeting.start_time }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Duration:</div>
|
||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Host:</div>
|
||||
<div class="detail-value">{{ meeting.host_email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Join Information</h2>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Password:</div>
|
||||
<div class="detail-value">{{ meeting.password }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">H.323 Password:</div>
|
||||
<div class="detail-value">{{ meeting.h323_password }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">PSTN Password:</div>
|
||||
<div class="detail-value">{{ meeting.pstn_password }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Meeting Settings</h2>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Host Video:</div>
|
||||
<div class="detail-value">{{ meeting.settings.host_video|yesno:"Enabled,Disabled" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Participant Video:</div>
|
||||
<div class="detail-value">{{ meeting.settings.participant_video|yesno:"Enabled,Disabled" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Join Before Host:</div>
|
||||
<div class="detail-value">{{ meeting.settings.join_before_host|yesno:"Allowed,Not Allowed" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Waiting Room:</div>
|
||||
<div class="detail-value">{{ meeting.settings.waiting_room|yesno:"Enabled,Disabled" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Audio:</div>
|
||||
<div class="detail-value">{{ meeting.settings.audio }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="{{ meeting.start_url }}" class="btn btn-primary" target="_blank">Start Meeting</a>
|
||||
<a href="{{ meeting.join_url }}" class="btn btn-secondary" target="_blank">Join Meeting</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
175
templates/meetings/update_meeting.html
Normal file
175
templates/meetings/update_meeting.html
Normal file
@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Update Meeting</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #1b8354;
|
||||
--background-color: #fcfcfc;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
--card-bg: #ffffff;
|
||||
--secondary-text: #666;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
padding: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--secondary-text);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Update Zoom Meeting</h1>
|
||||
<p>Modify the details of your scheduled meeting</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Meeting Information</h2>
|
||||
|
||||
<form method="post" action="{% url 'update_meeting' meeting.pk %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-row">
|
||||
<label for="topic" class="form-label">Topic:</label>
|
||||
<input type="text" id="topic" name="topic" class="form-input" value="{{ meeting.topic }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="start_time" class="form-label">Start Time (ISO 8601):</label>
|
||||
<input type="datetime-local" id="start_time" name="start_time" class="form-input"
|
||||
value="{{ meeting.start_time|slice:'0:16'|date:'Y-m-d\TH:i' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="duration" class="form-label">Duration (minutes):</label>
|
||||
<input type="number" id="duration" name="duration" class="form-input" value="{{ meeting.duration }}" required>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn btn-primary">Update Meeting</button>
|
||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user