add initial zoom integration

This commit is contained in:
ismail 2025-09-29 12:27:37 +03:00
parent 8df69bf1c0
commit 1e04b5736d
19 changed files with 1339 additions and 13 deletions

View File

@ -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'

Binary file not shown.

View File

@ -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'}),
}

View 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)),
],
),
]

View File

@ -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

View File

@ -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'),
]

View File

@ -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)
}

View File

@ -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('/')

View 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>

View 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>

View 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>

View 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>