From 1e04b5736d3207de71d0aa3388c232420eb7845b Mon Sep 17 00:00:00 2001 From: ismail Date: Mon, 29 Sep 2025 12:27:37 +0300 Subject: [PATCH] add initial zoom integration --- .../__pycache__/settings.cpython-313.pyc | Bin 4490 -> 4702 bytes NorahUniversity/settings.py | 7 +- db.sqlite3 | Bin 315392 -> 323584 bytes recruitment/__pycache__/forms.cpython-313.pyc | Bin 791 -> 1361 bytes .../__pycache__/models.cpython-313.pyc | Bin 3111 -> 4770 bytes recruitment/__pycache__/urls.cpython-313.pyc | Bin 731 -> 1480 bytes recruitment/__pycache__/utils.cpython-313.pyc | Bin 2482 -> 9489 bytes recruitment/__pycache__/views.cpython-313.pyc | Bin 1212 -> 7031 bytes recruitment/forms.py | 14 +- recruitment/migrations/0005_zoommeeting.py | 36 +++ .../0005_zoommeeting.cpython-313.pyc | Bin 0 -> 2338 bytes recruitment/models.py | 39 ++- recruitment/urls.py | 8 + recruitment/utils.py | 267 ++++++++++++++++++ recruitment/views.py | 112 +++++++- templates/meetings/create_meeting.html | 191 +++++++++++++ templates/meetings/list_meetings.html | 249 ++++++++++++++++ templates/meetings/meeting_details.html | 254 +++++++++++++++++ templates/meetings/update_meeting.html | 175 ++++++++++++ 19 files changed, 1339 insertions(+), 13 deletions(-) create mode 100644 recruitment/migrations/0005_zoommeeting.py create mode 100644 recruitment/migrations/__pycache__/0005_zoommeeting.cpython-313.pyc create mode 100644 templates/meetings/create_meeting.html create mode 100644 templates/meetings/list_meetings.html create mode 100644 templates/meetings/meeting_details.html create mode 100644 templates/meetings/update_meeting.html diff --git a/NorahUniversity/__pycache__/settings.cpython-313.pyc b/NorahUniversity/__pycache__/settings.cpython-313.pyc index 68f0621734c180216c9d1a1757c29bb16c267038..7b23a963f46e97daf736778c2c0990325a06cd40 100644 GIT binary patch delta 305 zcmeBDzNfT4ss2- z#RH+^L;Ss6{Z=v*X##y&qz)u*aoFVMr$2Vod*lHkTd?9m#1US@y!t;d|fr zy*F>(Ol|9UZSVNkC*Rs-7$!si|Gvj-r``v<-(7nu!4RKt{c?uo6Ll>A!F`wr@k}r> zWs|I3Hk<-69aU9h`Yp>U6bNw)Gapr>v4|RvMB_+}C$!Uv=-7MF55~vFLqp5%f-(2~C;wOrpwEdrS48P}h#`1uF?JRjj{;T5hrLQxPIla~#$(pv5`Hc|fSt1TONZ&`>1*v^Ob`hKT4gp1z6mC<5C#Yg1CgWW0pg)D$sP*s9 z&Im9q+;;sJDgohV;hFGIxLw!K-*4!>9EgC9O{M!epn*fmpWF|0mYM3|zz8^4%|!k8 zE1@z>i+5fBi%LNB#5M80c$cRBr>mcb0Lba)I@w%P&2{3==I+ksF38`2vRL)}7uX?I ztkyO4vN$*(b4)OJ_~G4qGSBWjnTOZ4VLAm}%vDC}6EAmtd+b-?rJ!-Sz)kqTx9#g@ zuJS**SdOPwwHyAvWD?x3ICx=+w79l5^ayy)d$|q=j zbR(yrWo*-y*`cWFJ)Z_Agpz}8M|X?@@%A=gZ$4MHv13@Kf(+9kd19loSu*k_$)UN- zr)ci8*;zDw?(FnOD5SjRrY4XYK82Kq&n?R&uk*yz@c%+e_|h^=y=*Ue_%R%Cvw?28 zNuaJYxM-Ce-Mh93hCFVS?XI>do55uxM=S*`Sk_XjYmx53$Qng#I{FcmX5s~M-Li?^ zuKz~1>L6it5e@N06mW#vVtqRO-j_ML>u9dn@(HX zLNk{#bNc5~v**u*l*2_~l=%cH&6{(G~#BgoC6Qxng zY{NHqGW!LPh%8_8+^P?Fv*Vc44CPtF7`U00 z82H8c!g&{SU*h_}Wyt)Uqk>t9Z5x{qW8mgQ0YgTSX1{!PaYskSHuaLkq@2{e)bjZF z>59{sMOhKd>F=j8^CR)M3r}ZGW!&CBnc0Mi1?a;))5WGSFOXnhV5$>0H)m|q1{+nB Wnp{+xS(2ODE;NmKyU;Y2ca{KXVrXmt diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 09ab644ce2a583d60e427d53c71d96b2d4374e1d..e8b74e72e9e6c69e6982817e7a0ccee24407fdb8 100644 GIT binary patch literal 1361 zcmbVMOKTKC5bmDWK9ZQXCL4sH2##j6IV7N`W|3Yq_{T0!R(jW?+atrK=SF2_=!CeD_4f9R)bk*0@{Z&nCU?9(M#AUVqfiw1l zCMUz}7%hp$9&?z(m)If)KFu7>(U-JE9rQ&5jA=H(E-}X_FvncwPZGuxlJvcxr*E(l zPYXRW|M1MzvUcu>!fdY0wGbN75oXA2P4ztw!$w&M=?68HyXys2zv@X*N%u(6=uBEy z(b!|F47kHUb2#Wp3L2)MFKDS37>)sE&9p5wa7y<8w>%=6F5XTAYfb;fyNVnoyqd=x zEJue-&9F^1(#1Dh7ScPSi?wxS1YScZQ#3rkt}GBy+z(ZL*s=Vtq!fIS6 zyWe$R#a_L$GU&Q%K18za2O8POD}Ah%Y&8bdGYsG|`Y1zgHpQEBRQO97-d*`zD1Isw-#v5|j*2aSv1B1J-PdMNw+MJM_=}?xYt!;}+WC4<7b(ik^;F7_Q;X zS$lQ7x#QhfQLb_;$>nP*Xs{WM%aR=3zZlR(zKyjB8G5gl&YGtKa&1M zci111;64IcKJ7^VwLYT3(qV_crduts0{M)K3?Bj!7vL@+|78lS-W^O^LF*fs#{2#N DRSrxF diff --git a/recruitment/__pycache__/models.cpython-313.pyc b/recruitment/__pycache__/models.cpython-313.pyc index 19def1d66a7e95135ee8ba1414ffadad61c428ea..9db8450a8f0f1a6a748f87d19ca419ffcb50da26 100644 GIT binary patch delta 1399 zcmah{J#5=X6eeZ;l9DLumr|tI7Az-JgcLBGqC(N3aBL`cBR8SiAcav@m98=ii&T$P z0%wtc0v)=v28RyWx>TdOWhv01C_0+-K!cNqF5ZGBo!fVm12qlWaQJxd`@Vbc-Mjli z_`VW<7KN8JGP?)B3j&G8DCUUIjM}6NmturvX}`50iUygF{vMVu;SyAP^Tk4zH8?_@>8* zJ~#BRt&lu!=-zBWINfMt19jbyG+O`{Kx!A5z$W>flk8ZUXLUoJeNE{g9Y=}60&GDO zWG-GQ93v|ZphHt;BUs-G7}`MT;0#bY4X}iB#3BH=sehdBx-9DkmgOCy76@D-Py}d- zOXhJfKf=p|*#mHy{%k6;(#7P}xQurc*--k(O~0?{aMlOtmTI6L!dnVD=(^$V5Vs9Q zQ{6CeJpHor1yS^OGQ7cM)(HCwngy=e*cV}Ir}S8{SyJ6E0FuyR18PqRfQ zE81CcTCuWkjdxE|c_+1Mr&cG!yp_5!e($VUc8WE-Sevdt=~~5&V}Jh4{42D?`{_Vo z_K3)cnqtH}WE*tT{E00w7#Q*z^&RtfwqAHnMd-qFp-dOPUHz_hRQr>HrMLHQHa=GL delta 74 zcmZ3ax?F@Q@=^_78>QL0E9C;$K)01~YL diff --git a/recruitment/__pycache__/urls.cpython-313.pyc b/recruitment/__pycache__/urls.cpython-313.pyc index f41735d00e775e5f79cfb3dfa39d1bda89562f91..a9e091324fb736bc856b957c2536a76badc741e2 100644 GIT binary patch literal 1480 zcmb7D%TE(Q7@wtE=!0!}wdGNTRNI6Ex`tqgKmZW~mWPo|6%Dajx?{W0?l!YqjXhwz zapYp+k)z%?a`51v&`A#)H}PORal>%)VmVK89`R#fmx}?(z3bX$RhxzC zcd1kAy60jF4^ebqXX#4vItm|A1llP4j-s0=dfF%iM-d>3zBY=Wqv+Y?w+C7k+mTl6 zZUd5f9d{pb54I}*>F#&j1H?W2pYD+34ik6uKiv_>JxJU)TiwzSRfs;+lN%l%Ij4eg z&gM(9nLj5-fGz^BRrtopyh6HWLQNrvn;{iXRz?saL$p2n`d+4>8YcB>auHTSO>SP* z%vnmS%7SS7Xr@3@RtpuciDXq%wVY@R&6E|X#ImxcsETYtk=dOkD?%SG+C6{4Mm?V* z8~Ht5Mv7>Ao!kbtKs(D6AvDRuj7l(zASpfB07N@b2bzk?P=lGQ7dGlmlnzM$!3Ncs z!Ke(DOSDg?80(iw19ir!I;g+`Y_F^{P36{jukBa%Wi6*C%cx)=@{s~0(BP(_lZGxD zyfo0y4*5y21J2MHx<_{Z3tca6*2mr;Tkw>EU9wSULs!@bY)2R$z5=hX7iBQ~syxkR zZh=?WfDHDN#>p8nA<~S`;F5F)>=`ky7on&cMe_E=64Li!)-*(!{10TgoX^kLXnmC=s z{K7ZhQ}vFWB(2ySj?I0Y!2DwZ9gQ4oR(J}Br_M$(|A>huKOI`)78bXv=^WlZ{p#8d z=ASe2_6f8S%Q&%YB~mz%swFn6l7e~2$Xh3AE53x|OICah$Jc7{RP~9B`Mv98b@dtM QcP=8`!H)NSbc2ll2lr)y=Kufz delta 344 zcmX@XeVbL|GcPX}0}z2CCpgQ(+9`2lK#{@B)E$)=8#Ju!;z0#tb;v#0C+FJ}otU#)W4Mc!E zRm2G*K#sq~my%eVk(8fUloDSyIguq=LKr9t3?PQ$LLl*hnURt4E`tmZO}@?&&+~zs Nji0HJyGRfy2LOiQGaUc` diff --git a/recruitment/__pycache__/utils.cpython-313.pyc b/recruitment/__pycache__/utils.cpython-313.pyc index 97616fe9934c892e1e80dec7cd38a6a517671dde..88518c792b06e82a8dc3b9a2393b127a7c9bc1fd 100644 GIT binary patch literal 9489 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 diff --git a/recruitment/__pycache__/views.cpython-313.pyc b/recruitment/__pycache__/views.cpython-313.pyc index f11b853791cf65688af49fdb1fc0ac7982acd730..ac8c69b1cc0bdcc40dcb8a4ec15ec185fe44d336 100644 GIT binary patch literal 7031 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 IYiBw4Z|}I~AOHXW literal 1212 zcmd5*y-(Xf6hFsKaK3s_pr$N*31|c*bO1V3UAk03Wr(FtkxnK#h{3Vp-MJK|PE|XV z4*U`QbF2(TC!r2i2ewL~d++Qd6ftz|Nq+vlue*1@_pVni7m!?A-@EOv8bUvmGFaI@ z=zWsl0@=vcwoy$3ZC2sTcBZC-F0pPK+eR%5*#u`L&OvS#O`&;Y=cbW8($MP<2Akgg zH}n*PnM9g0GXIar{C=TQyjE!yP2_<3EeaBu?_7GqC}-F53W=)|w5Z3VtH>z0j_00H zV9oTrRA%~0k4tcY8VEESLB`fVZy5E2l)u9fs+aESN~`1hwvE0C%tpRGao-O`I|HSC zy<&($E!cf4OSdV%9+8pX&`zZ+OhSC8LkSTDLgYn4PvH_F$D!l(J(!R^7Z~?kpZWnI zkdx^`m-~s4VE2GFITO0$dBQvnDV$1fKph~;GQu7*B1Ui7Ax+O=OiU8OxWnBhaX1He zH{_Jb?Ta$GCqE>fSKC2{R$bO{T(5crL2_8t;jDV-IAL4nu`19egf8z;pI4LKtR0;~ zUZy8PRl=5}_=qm?4j7*`_c()iGQ+bfteLLIZ2kl^_Z&bX~O;N?QtS#kA8prY93Wf?LNEMWsdL?M+Au{AxS9mB)lA1 zOdfXdWhf;ig_m0v#Us}uQbdYMEmCYUJSIqSz@?AGC8X9#9uUc65p59Z)T98I(}3wX zD$00Z1dIrnt`jh+V{@g0YT5zOb3&AkW1~c*G}PQV8G?Gc0M&a`B&j>d)pJ~&A}5`B z$V`cFg*oQSvph`wVgjYv6<%|XB%$tLt} z$lJ{a|AV;55XH2VpJX#CN(GWCU&?<-EHA=fgB9+m?Y!1cN)k zszj~GWD`BZn>21>qRN{364T^5CM$-CkuFy-jaWuiE$#iy>f+&C$}f|FONA&g#rBUgjWPYDq2s+#297;7R2|twqp_-Nni~dD z@*x^o(PaIZA1RRtS)o)lQ#R28DBCABK+ z7V=i8=9hHHF4S5rYH34X1=wAjh z5H^D}5wL`03)=1zryCncRur1Bt84%ie=X*b3Tv>zut!;RRmGZO{=_Zh(=>)W9ucAm zgy2nTYZ&AtLeFhkV{p(;Bebd#)6!HO>jqm^aFz8+FR9oh>xnyzRbNitHVh5R zy067&rB4?EkWs}4AB0Zi&AwWK23SO0fnJ*kk!w)YKKwblP+T)AxTu;eaIs2^r?_O9 zMH&9jWZ7N=KeKq~gcbk&+7#iejU&cSFx|t|O~MYPcVmM2n&-H0xn`7$&+^TX&=LK5 zu9@KC?e$on8|!PN`hH1uzFMz$7u@c`PWWxkd4e3B)VfVav>K_Nt)Y5v!R;-)Db;$f zJK{%;RAy_vo-MlB;?C%^!(}&f zc_&@V+;GH?J005TBQ755#BN6|>$`d#W6jxFs%39G;#706z2nsTNLM`34E3a= z-^`MG2b?Xyi!ZJ`mG!bQtgLv$?9H#wDjOOfdmkhkyWt;^o8$qgKs0Bvk5@Cq^ZcJt Yj?e!d?c<}b5_K_G6LWuXpm^T@0K%kXDF6Tf literal 0 HcmV?d00001 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