diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 68f0621..7b23a96 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-313.pyc and b/NorahUniversity/__pycache__/settings.cpython-313.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 9e0f512..74fceb8 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -183,4 +183,9 @@ UNFOLD = { "SCRIPTS": [ lambda request: static("unfold/js/app.js"), ], -} \ No newline at end of file +} + +ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' +ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' +ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L' +SECRET_TOKEN = '6KdTGyF0SSCSL_V4Xa34aw' \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index c972d8b..81757bc 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 09ab644..e8b74e7 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 19def1d..9db8450 100644 Binary files a/recruitment/__pycache__/models.cpython-313.pyc and b/recruitment/__pycache__/models.cpython-313.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index f41735d..a9e0913 100644 Binary files a/recruitment/__pycache__/urls.cpython-313.pyc and b/recruitment/__pycache__/urls.cpython-313.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-313.pyc b/recruitment/__pycache__/utils.cpython-313.pyc index 97616fe..88518c7 100644 Binary files a/recruitment/__pycache__/utils.cpython-313.pyc and b/recruitment/__pycache__/utils.cpython-313.pyc differ diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index f11b853..ac8c69b 100644 Binary files a/recruitment/__pycache__/views.cpython-313.pyc and b/recruitment/__pycache__/views.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 1cf91d8..421bfd0 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -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'] \ No newline at end of file + 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'}), + } \ No newline at end of file diff --git a/recruitment/migrations/0005_zoommeeting.py b/recruitment/migrations/0005_zoommeeting.py new file mode 100644 index 0000000..fea9cc1 --- /dev/null +++ b/recruitment/migrations/0005_zoommeeting.py @@ -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)), + ], + ), + ] diff --git a/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc new file mode 100644 index 0000000..4b39aa5 Binary files /dev/null and b/recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc differ diff --git a/recruitment/models.py b/recruitment/models.py index 96822e2..c9b7a7a 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/recruitment/urls.py b/recruitment/urls.py index f2702e0..c91bee1 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -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//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//', views.ZoomMeetingDetailsView.as_view(), name='meeting_details'), + path('update-meeting//', views.ZoomMeetingUpdateView.as_view(), name='update_meeting'), + path('delete-meeting//', views.ZoomMeetingDeleteView, name='delete_meeting'), ] + diff --git a/recruitment/utils.py b/recruitment/utils.py index d5f1dbc..b49638e 100644 --- a/recruitment/utils.py +++ b/recruitment/utils.py @@ -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) + } \ No newline at end of file diff --git a/recruitment/views.py b/recruitment/views.py index 258d167..3df1dbd 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -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('/') diff --git a/templates/meetings/create_meeting.html b/templates/meetings/create_meeting.html new file mode 100644 index 0000000..b1d0ae5 --- /dev/null +++ b/templates/meetings/create_meeting.html @@ -0,0 +1,191 @@ + + + + + + Create Zoom Meeting + + + +
+
+

Zoom Meeting Manager

+

Create a new Zoom meeting

+
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + +
+
+
Create New Meeting
+
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+
+ + \ No newline at end of file diff --git a/templates/meetings/list_meetings.html b/templates/meetings/list_meetings.html new file mode 100644 index 0000000..37f7304 --- /dev/null +++ b/templates/meetings/list_meetings.html @@ -0,0 +1,249 @@ + + + + + + Zoom Meetings + + + +
+
+

Zoom Meetings

+

Your upcoming and past meetings

+
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + Create Meeting + + {% if meetings %} +
+ {% for meeting in meetings %} +
+
{{ meeting.topic }}
+ +
+
ID:
+
{{ meeting.id }}
+
+ +
+
Time:
+
{{ meeting.start_time }}
+
+ +
+
Duration:
+
{{ meeting.duration }} minutes
+
+ +
+
Status:
+
+ {{ meeting.status|title }} +
+
+ +
+ View + Update +
+ {% csrf_token %} + +
+
+
+ {% endfor %} +
+ + + {% if next_page_token %} +
+ Load More +
+ {% endif %} + {% else %} +
+

No meetings found.

+
+ {% endif %} +
+ + \ No newline at end of file diff --git a/templates/meetings/meeting_details.html b/templates/meetings/meeting_details.html new file mode 100644 index 0000000..48f3634 --- /dev/null +++ b/templates/meetings/meeting_details.html @@ -0,0 +1,254 @@ + + + + + + Meeting Details + + + +
+
+

Zoom Meeting Details

+

All information about your scheduled meeting

+
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + +
+

Meeting Information

+ +
+
Topic:
+
{{ meeting.topic }}
+
+ +
+
Meeting ID:
+
{{ meeting.id }}
+
+ +
+
Status:
+
+ {{ meeting.status|title }} +
+
+ +
+
Start Time:
+
{{ meeting.start_time }}
+
+ +
+
Duration:
+
{{ meeting.duration }} minutes
+
+ +
+
Host:
+
{{ meeting.host_email }}
+
+
+ +
+

Join Information

+ +
+
Password:
+
{{ meeting.password }}
+
+ +
+
H.323 Password:
+
{{ meeting.h323_password }}
+
+ +
+
PSTN Password:
+
{{ meeting.pstn_password }}
+
+
+ +
+

Meeting Settings

+ +
+
Host Video:
+
{{ meeting.settings.host_video|yesno:"Enabled,Disabled" }}
+
+ +
+
Participant Video:
+
{{ meeting.settings.participant_video|yesno:"Enabled,Disabled" }}
+
+ +
+
Join Before Host:
+
{{ meeting.settings.join_before_host|yesno:"Allowed,Not Allowed" }}
+
+ +
+
Waiting Room:
+
{{ meeting.settings.waiting_room|yesno:"Enabled,Disabled" }}
+
+ +
+
Audio:
+
{{ meeting.settings.audio }}
+
+
+ + +
+ + \ No newline at end of file diff --git a/templates/meetings/update_meeting.html b/templates/meetings/update_meeting.html new file mode 100644 index 0000000..62c8cdc --- /dev/null +++ b/templates/meetings/update_meeting.html @@ -0,0 +1,175 @@ + + + + + + Update Meeting + + + +
+
+

Update Zoom Meeting

+

Modify the details of your scheduled meeting

+
+ + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + +
+

Meeting Information

+ +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancel +
+
+
+
+ + \ No newline at end of file