add initial zoom integration
This commit is contained in:
parent
8df69bf1c0
commit
1e04b5736d
Binary file not shown.
@ -184,3 +184,8 @@ UNFOLD = {
|
|||||||
lambda request: static("unfold/js/app.js"),
|
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 django import forms
|
||||||
from . import models
|
from .models import ZoomMeeting, Candidate
|
||||||
|
|
||||||
class CandidateForm(forms.ModelForm):
|
class CandidateForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Candidate
|
model = Candidate
|
||||||
fields = ['name', 'email', 'resume']
|
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.
@ -37,3 +37,40 @@ class TrainingMaterial(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
def __str__(self):
|
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 django.urls import path
|
||||||
from . import views_frontend
|
from . import views_frontend
|
||||||
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('careers/', views_frontend.job_list, name='job_list'),
|
path('careers/', views_frontend.job_list, name='job_list'),
|
||||||
@ -7,5 +8,12 @@ urlpatterns = [
|
|||||||
path('training/', views_frontend.training_list, name='training_list'),
|
path('training/', views_frontend.training_list, name='training_list'),
|
||||||
path('candidate/<int:candidate_id>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
path('candidate/<int:candidate_id>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
||||||
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
|
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 rest_framework import viewsets
|
||||||
from . import models
|
from django.contrib import messages
|
||||||
from . import serializers
|
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):
|
class JobViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.Job.objects.all()
|
queryset = Job.objects.all()
|
||||||
serializer_class = serializers.JobSerializer
|
serializer_class = JobSerializer
|
||||||
|
|
||||||
class CandidateViewSet(viewsets.ModelViewSet):
|
class CandidateViewSet(viewsets.ModelViewSet):
|
||||||
queryset = models.Candidate.objects.all()
|
queryset = Candidate.objects.all()
|
||||||
serializer_class = serializers.CandidateSerializer
|
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