-
-
Notifications
You must be signed in to change notification settings - Fork 177
New internal YouTube video's player #624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
07b21ce
4ee287d
5e81a3d
36666f1
b6fba7e
8cb37f5
38a549e
db7cfdc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Generated by Django 5.1.6 on 2025-05-15 11:24 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("web", "0061_alter_educationalvideo_description_videorequest"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="educationalvideo", | ||
| name="video_id", | ||
| field=models.CharField( | ||
| default="", | ||
| editable=False, | ||
| help_text="Auto-generated unique video identifier.", | ||
| max_length=12, | ||
| unique=True, | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -677,6 +677,24 @@ class EducationalVideo(models.Model): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| help_text="User who uploaded the video. If null, the submission is considered anonymous.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploaded_at = models.DateTimeField(auto_now_add=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| video_id = models.CharField( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_length=12, unique=True, editable=False, default="", help_text="Auto-generated unique video identifier." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+680
to
+683
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAvoid unique default collisions for video_id; add trailing comma Using default="" on a unique field risks migration-time collisions and invalid intermediate states. Since the field is editable=False and populated in save(), drop the default and add the trailing comma per lint. - video_id = models.CharField(
- max_length=12, unique=True, editable=False, default="", help_text="Auto-generated unique video identifier."
- )
+ video_id = models.CharField(
+ max_length=12,
+ unique=True,
+ editable=False,
+ help_text="Auto-generated unique video identifier.",
+ )Verification (ensure migration 0062 backfills unique IDs before adding the unique constraint): 🏁 Script executed: #!/bin/bash
# Sanity-check for a data migration that assigns video_id before applying UNIQUE
rg -nP 'migrations/.+0062_.*educationalvideo_video_id' -C3
rg -nP '(RunPython|SeparateDatabaseAndState)' -C2 web/migrationsLength of output: 3493 🏁 Script executed: #!/bin/bash
# Search for any occurrences of 'video_id' in migration files to locate when the field was added or altered
rg -n "video_id" -C3 web/migrationsLength of output: 615 🏁 Script executed: #!/bin/bash
# Inspect migration 0062 for AddField, backfill, and unique constraint steps on video_id
rg -nP 'AddField' -C5 web/migrations/0062_educationalvideo_video_id.py
rg -nP 'RunPython' -C5 web/migrations/0062_educationalvideo_video_id.py
rg -nP 'SeparateDatabaseAndState' -C5 web/migrations/0062_educationalvideo_video_id.py
rg -nP 'AlterField' -C5 web/migrations/0062_educationalvideo_video_id.pyLength of output: 650 🏁 Script executed: #!/bin/bash
# Show full AddField block for video_id in migration 0062
sed -n '1,200p' web/migrations/0062_educationalvideo_video_id.py | sed -n '/AddField/,/)/p'Length of output: 432 🏁 Script executed: #!/bin/bash
# Display lines around the video_id field in models.py to confirm default usage
sed -n '670,710p' web/models.pyLength of output: 1362 Prevent video_id default collisions and update migration safely Dropping the
Locations to update:
Model diff: - video_id = models.CharField(
- max_length=12, unique=True, editable=False, default="", help_text="Auto-generated unique video identifier."
- )
+ video_id = models.CharField(
+ max_length=12,
+ unique=True,
+ editable=False,
+ help_text="Auto-generated unique video identifier.",
+ )Migration sketch for 0062 (replace the single AddField with steps): operations = [
- migrations.AddField(
- model_name="educationalvideo",
- name="video_id",
- field=models.CharField(default="", editable=False, help_text="Auto-generated unique video identifier.", max_length=12, unique=True),
- ),
+ # 1) Create the column with only a DB default to avoid nulls
+ migrations.AddField(
+ model_name="educationalvideo",
+ name="video_id",
+ field=models.CharField(
+ max_length=12,
+ editable=False,
+ default="", # temporary DB default
+ ),
+ ),
+ # 2) Backfill all existing rows with unique values
+ migrations.RunPython(backfill_video_ids, reverse_code=migrations.RunPython.noop),
+ # 3) Enforce uniqueness in state
+ migrations.AlterField(
+ model_name="educationalvideo",
+ name="video_id",
+ field=models.CharField(
+ max_length=12,
+ unique=True,
+ editable=False,
+ help_text="Auto-generated unique video identifier.",
+ ),
+ ),
]This ensures no two rows collide on the same default and that the unique constraint is only applied once every record has a valid, unique 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def youtube_id(self) -> str | None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsed = urlparse(self.video_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host = parsed.netloc.lower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # youtu.be/<id> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if host in ("youtu.be", "www.youtu.be"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parsed.path.lstrip("/") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # youtube.com/watch?v=<id> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if host in ("youtube.com", "www.youtube.com"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parse_qs(parsed.query).get("v", [None])[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+684
to
+697
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) 🛠️ Refactor suggestion Harden YouTube ID extraction; support embed/shorts and mobile hosts; validate 11-char pattern Current logic misses /embed/, /shorts/, m.youtube.com, and youtube-nocookie.com, and doesn’t validate the 11-char ID. Tighten extraction and validate using the canonical 11-character regex. @property
- def youtube_id(self) -> str | None:
- parsed = urlparse(self.video_url)
- host = parsed.netloc.lower()
-
- # youtu.be/<id>
- if host in ("youtu.be", "www.youtu.be"):
- return parsed.path.lstrip("/")
-
- # youtube.com/watch?v=<id>
- if host in ("youtube.com", "www.youtube.com"):
- return parse_qs(parsed.query).get("v", [None])[0]
-
- return None
+ def youtube_id(self) -> str | None:
+ parsed = urlparse(self.video_url)
+ host = parsed.netloc.lower().lstrip("www.")
+ path = parsed.path
+
+ candidate: str | None = None
+
+ # youtu.be/<id>
+ if host == "youtu.be":
+ candidate = path.strip("/")
+ # youtube.* domains
+ elif host in ("youtube.com", "m.youtube.com", "music.youtube.com", "youtube-nocookie.com"):
+ # watch?v=<id>
+ candidate = parse_qs(parsed.query).get("v", [None])[0]
+ if not candidate:
+ # /embed/<id> or /shorts/<id>
+ import re as _re
+ m = _re.match(r"^/(?:embed|shorts)/([A-Za-z0-9_-]{11})", path)
+ if m:
+ candidate = m.group(1)
+
+ # Final validation: exact 11 allowed chars
+ if candidate:
+ import re as _re
+ if _re.fullmatch(r"[A-Za-z0-9_-]{11}", candidate):
+ return candidate
+ return NoneOptional: add targeted tests for youtube_id covering watch, youtu.be, embed, shorts, m.youtube.com, and invalid cases. I can generate them if helpful. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| verbose_name = "Educational Video" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -697,27 +715,15 @@ def thumbnail_url(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f"https://img.youtube.com/vi/{vid}/hqdefault.jpg" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def youtube_id(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Extract the YouTube video ID, whether it's a long or short URL. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns None if not a YouTube link. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsed = urlparse(self.video_url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host = parsed.netloc.lower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # youtu.be/<id> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if host in ("youtu.be", "www.youtu.be"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parsed.path.lstrip("/") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # youtube.com/watch?v=<id> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if host in ("youtube.com", "www.youtube.com"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parse_qs(parsed.query).get("v", [None])[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def save(self, *args, **kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not self.video_id: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Keep generating until a unique ID is found | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while True: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_id = uuid.uuid4().hex[:12] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not EducationalVideo.objects.filter(video_id=new_id).exists(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.video_id = new_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
abdelrahman390 marked this conversation as resolved.
Comment on lines
+720
to
+725
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Keep generating until a unique ID is found | |
| while True: | |
| new_id = uuid.uuid4().hex[:12] | |
| if not EducationalVideo.objects.filter(video_id=new_id).exists(): | |
| self.video_id = new_id | |
| break | |
| # Try up to 10 times to generate a unique ID | |
| max_attempts = 10 | |
| for attempt in range(max_attempts): | |
| new_id = uuid.uuid4().hex[:12] | |
| if not EducationalVideo.objects.filter(video_id=new_id).exists(): | |
| self.video_id = new_id | |
| break | |
| else: | |
| raise RuntimeError("Could not generate a unique video_id after 10 attempts.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
🛠️ Refactor suggestion
Prevent infinite loop and handle race on unique video_id; add type annotations; satisfy DJ012
- while True without a cap risks hanging.
- exists() check is racy; catch IntegrityError and retry.
- Add type annotations and import Any; consider moving save() above custom properties to satisfy DJ012.
- def save(self, *args, **kwargs):
- if not self.video_id:
- # Keep generating until a unique ID is found
- while True:
- new_id = uuid.uuid4().hex[:12]
- if not EducationalVideo.objects.filter(video_id=new_id).exists():
- self.video_id = new_id
- break
- super().save(*args, **kwargs)
+ def save(self, *args: "Any", **kwargs: "Any") -> None:
+ from django.db import IntegrityError
+
+ # Generate ID if missing; retry a few times on race
+ if not self.video_id:
+ attempts = 0
+ while attempts < 5:
+ self.video_id = uuid.uuid4().hex[:12] # 12 lowercase hex
+ try:
+ return super().save(*args, **kwargs)
+ except IntegrityError:
+ # Collision on UNIQUE(video_id); retry with a new ID
+ self.video_id = ""
+ attempts += 1
+ # If we get here, something is wrong with ID generation or DB state
+ raise IntegrityError("Failed to generate a unique video_id after multiple attempts.")
+ else:
+ return super().save(*args, **kwargs)Add missing imports at file top (or reuse existing typing imports):
from typing import AnyIf you want to clear DJ012, move save() above custom properties (youtube_id, thumbnail_url). I can push a follow-up patch.
🧰 Tools
🪛 Ruff (0.12.2)
718-726: Order of model's inner classes, methods, and fields does not follow the Django Style Guide: save method should come before custom method
(DJ012)
718-718: Missing return type annotation for public function save
Add return type annotation: None
(ANN201)
718-718: Missing type annotation for *args
(ANN002)
718-718: Missing type annotation for **kwargs
(ANN003)
🤖 Prompt for AI Agents
In web/models.py around lines 718 to 726, the save() implementation risks an
infinite loop, is racy against concurrent inserts, and lacks type annotations;
change it to limit the generation attempts (e.g., max_attempts constant), import
typing.Any and add a return type/arg types for save(self, *args: Any, **kwargs:
Any) -> None, and wrap the super().save(...) in a retry loop that catches
django.db.IntegrityError to retry ID generation when a unique constraint
violation occurs; also ensure the new imports (from typing import Any and from
django.db import IntegrityError) are added at the top, and optionally move
save() above the custom properties to satisfy DJ012.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -721,16 +721,26 @@ <h3 class="text-sm font-semibold mb-2 text-left">Quick Add YouTube Video</h3> | |||||||||||||||||||||||||||||||||||||||||
| name="title" | ||||||||||||||||||||||||||||||||||||||||||
| placeholder="Video title " | ||||||||||||||||||||||||||||||||||||||||||
| class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" /> | ||||||||||||||||||||||||||||||||||||||||||
| <!-- Hidden description (filled by JS) --> | ||||||||||||||||||||||||||||||||||||||||||
| <input type="hidden" id="video-description" name="description" value="" /> | ||||||||||||||||||||||||||||||||||||||||||
| <!-- description --> | ||||||||||||||||||||||||||||||||||||||||||
| <label for="video-description" | ||||||||||||||||||||||||||||||||||||||||||
| class="block text-left text-sm text-gray-700 dark:text-gray-300 mb-1"> | ||||||||||||||||||||||||||||||||||||||||||
| Video description | ||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||
| <textarea id="video-description" | ||||||||||||||||||||||||||||||||||||||||||
| name="description" | ||||||||||||||||||||||||||||||||||||||||||
| rows="3" | ||||||||||||||||||||||||||||||||||||||||||
| placeholder="Enter a description for the video" | ||||||||||||||||||||||||||||||||||||||||||
| class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"></textarea> | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+724
to
+733
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Align label/textarea styling with project form guidelines
- <label for="video-description"
- class="block text-left text-sm text-gray-700 dark:text-gray-300 mb-1">
+ <label for="video-description"
+ class="block text-left text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Video description
</label>
<textarea id="video-description"
name="description"
rows="3"
placeholder="Enter a description for the video"
- class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"></textarea>
+ class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"></textarea>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| <!-- Category + submit --> | ||||||||||||||||||||||||||||||||||||||||||
| <div class="flex gap-2"> | ||||||||||||||||||||||||||||||||||||||||||
| <select id="video-category" | ||||||||||||||||||||||||||||||||||||||||||
| name="category" | ||||||||||||||||||||||||||||||||||||||||||
| required | ||||||||||||||||||||||||||||||||||||||||||
| class="flex-1 px-4 py-2 border rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500"> | ||||||||||||||||||||||||||||||||||||||||||
| <option value="">Select a category</option> | ||||||||||||||||||||||||||||||||||||||||||
| {% for subject in subjects %}<option value="{{ subject.id }}">{{ subject.name }}</option>{% endfor %} | ||||||||||||||||||||||||||||||||||||||||||
| {% for subject in subjects %} | ||||||||||||||||||||||||||||||||||||||||||
| <option value="{{ subject.id }}">{{ subject.name }}</option> | ||||||||||||||||||||||||||||||||||||||||||
| {% endfor %} | ||||||||||||||||||||||||||||||||||||||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||||||||
| <button type="submit" | ||||||||||||||||||||||||||||||||||||||||||
| class="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg flex items-center"> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||
| {% extends "base.html" %} | ||||||
|
|
||||||
| {% load static %} | ||||||
| {% load dict_filters %} | ||||||
|
|
||||||
| {% block title %}Educational Videos{% endblock %} | ||||||
| {% block content %} | ||||||
| <main class="flex-1 w-full max-w-7xl mx-auto px-4 md:px-6 py-8"> | ||||||
| <div class="flex flex-col gap-6"> | ||||||
| <div class="bg-white rounded-lg shadow-md p-6"> | ||||||
| <h1 class="text-2xl font-bold mb-2">Title: {{ video.title }}</h1> | ||||||
| <!-- Video Description --> | ||||||
| {% if video.description %} | ||||||
| <div class="mt-4"> | ||||||
| <h2 class="text-lg font-semibold mb-2">Description:</h2> | ||||||
| <p class="text-gray-700 whitespace-pre-line">{{ video.description }}</p> | ||||||
| </div> | ||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||
| {% endif %} | ||||||
| <!-- Additional Info --> | ||||||
| <div class="mt-4 mb-4 text-md text-gray-500"> | ||||||
| <p>Category: {{ video.category.name }}</p> | ||||||
| <p>Uploaded: {{ video.uploaded_at|date:"F j, Y" }}</p> | ||||||
| </div> | ||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||
| <!-- YouTube Video Embed --> | ||||||
| <div class="relative w-full rounded-lg overflow-hidden aspect-w-16 aspect-h-9 mb-6"> | ||||||
| {% if video.video_url %} | ||||||
| <iframe class=" w-full" | ||||||
|
||||||
| <iframe class=" w-full" | |
| <iframe class="w-full" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -316,6 +316,7 @@ | |
| path("challenges/<int:challenge_id>/submit/", views.challenge_submit, name="challenge_submit"), | ||
| path("current-weekly-challenge/", views.current_weekly_challenge, name="current_weekly_challenge"), | ||
| # Educational Videos URLs | ||
| path("videos/play/<str:video_id>/", views.play_youtube_video, name="play_youtube_video"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConstrain route param to internal 12-hex video_id The route uses our internal video_id (uuid.hex[:12]), not the 11-char YouTube ID. Tighten the pattern to lowercase hex for input hygiene and to avoid accidental matches. -from django.urls import include, path
+from django.urls import include, path, re_path
@@
- path("videos/play/<str:video_id>/", views.play_youtube_video, name="play_youtube_video"),
+ re_path(
+ r"^videos/play/(?P<video_id>[0-9a-f]{12})/$",
+ views.play_youtube_video,
+ name="play_youtube_video",
+ ),Optional hardening (view): normalize incoming IDs to lowercase before querying to prevent case-mismatch misses. # web/views.py
def play_youtube_video(request: HttpRequest, video_id: str) -> HttpResponse:
video = EducationalVideo.objects.filter(video_id=video_id.lower()).first()
...🏁 Script executed: #!/bin/bash
# Ensure all template calls pass `video.video_id` (not youtube_id) to the route
rg -nP -C2 "{% url ['\"]play_youtube_video['\"]\s+" --type=html web/templatesLength of output: 1191 Constrain Verification confirms that all template calls already pass
Proceed with the essential refactor to tighten the URL pattern: • In web/urls.py replace the existing -from django.urls import include, path
+from django.urls import include, path, re_path
@@
- path("videos/play/<str:video_id>/", views.play_youtube_video, name="play_youtube_video"),
+ re_path(
+ r"^videos/play/(?P<video_id>[0-9a-f]{12})/$",
+ views.play_youtube_video,
+ name="play_youtube_video",
+ ),• Optional hardening in web/views.py: normalize incoming IDs to lowercase before querying to avoid case-mismatch misses. def play_youtube_video(request: HttpRequest, video_id: str) -> HttpResponse:
video = EducationalVideo.objects.filter(video_id=video_id.lower()).first()
...🤖 Prompt for AI Agents |
||
| path("videos/", views.educational_videos_list, name="educational_videos_list"), | ||
| path("videos/upload/", views.upload_educational_video, name="upload_educational_video"), | ||
| path("fetch-video-title/", views.fetch_video_title, name="fetch_video_title"), | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5994,6 +5994,22 @@ def upload_educational_video(request): | |||||||||||||||||||||||||||||||
| return render(request, "videos/upload.html", {"form": form}) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def play_youtube_video(request: HttpRequest, video_id: str) -> HttpResponse: | ||||||||||||||||||||||||||||||||
| # Display a YouTube video in the internal player page. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+5997
to
+5999
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Constrain to GET and add a concise docstring. This is a read-only page; be explicit about the allowed method and document the intent. +@require_GET
def play_youtube_video(request: HttpRequest, video_id: str) -> HttpResponse:
- # Display a YouTube video in the internal player page.
+ """Display a YouTube video in the internal player page."""📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| video = EducationalVideo.objects.filter(video_id=video_id).first() | ||||||||||||||||||||||||||||||||
|
abdelrahman390 marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if not video: | ||||||||||||||||||||||||||||||||
| messages.error(request, "The requested video could not be found.") | ||||||||||||||||||||||||||||||||
| return redirect("educational_videos_list") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+6000
to
+6005
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Prefer get_object_or_404 and select_related for correctness and efficiency. Use the model’s uniqueness on video_id and fail fast with a 404 while also reducing potential N+1s in the template. Apply this diff: - video = EducationalVideo.objects.filter(video_id=video_id).first()
-
- if not video:
- messages.error(request, "The requested video could not be found.")
- return redirect("educational_videos_list")
+ video = get_object_or_404(
+ EducationalVideo.objects.select_related("uploader", "category"),
+ video_id=video_id,
+ )If you strongly prefer a redirect with a user-facing message instead of 404, keep the current flow but still add 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+6002
to
+6005
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Internationalize the user-facing error message (if you keep the redirect). Other views use gettext; keep consistency for translations. - messages.error(request, "The requested video could not be found.")
+ messages.error(request, _("The requested video could not be found."))📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| context = { | ||||||||||||||||||||||||||||||||
| "video": video, | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return render(request, "videos/play_youtube_video.html", context) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+5997
to
+6011
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Optional: access control hook for future premium/private videos. If visibility restrictions are planned, add a simple guard (e.g., require login for premium or draft videos). Keep it minimal and feature-flagged to avoid scope creep now. I can sketch a small policy function (e.g., can_view_video(request.user, video)) and wire it here if you want. 🤖 Prompt for AI Agents💡 Verification agent 🧩 Analysis chainVerify the template handles non‑YouTube URLs gracefully. Model property Run this to confirm the template guards on 🏁 Script executed: #!/bin/bash
# Locate the play template and inspect usage of youtube_id and fallback branches
set -euo pipefail
fd -a 'play_youtube_video.html'
rg -n -C3 'youtube_id|iframe|embed' $(fd -a 'play_youtube_video.html')Length of output: 1032 Guard on YouTube ID and provide proper fallback in the play template The current template only checks • File: {% if video.video_url %}
<iframe class="w-full"
height="500"
src="https://www.youtube.com/embed/{{ video.youtube_id }}"
title="{{ video.title }}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
{% else %}
<div class="flex items-center justify-center bg-gray-200 w-full h-full">
<p class="text-gray-500">Video not available</p>
</div>
{% endif %}With this guarded, multi-branch logic: {% if video.youtube_id %}
<iframe class="w-full"
height="500"
src="https://www.youtube.com/embed/{{ video.youtube_id }}"
title="{{ video.title }}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen>
</iframe>
{% elif video.video_url %}
<div class="flex items-center justify-center bg-gray-100 w-full h-full">
<a href="{{ video.video_url }}" target="_blank" rel="noopener" class="text-blue-600 underline">
Watch this video externally
</a>
</div>
{% else %}
<div class="flex items-center justify-center bg-gray-200 w-full h-full">
<p class="text-gray-500">Video not available</p>
</div>
{% endif %}This change ensures:
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def certificate_detail(request, certificate_id): | ||||||||||||||||||||||||||||||||
| certificate = get_object_or_404(Certificate, certificate_id=certificate_id) | ||||||||||||||||||||||||||||||||
| if request.user != certificate.user and not request.user.is_staff: | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using an empty string as the default for a unique field can cause integrity constraint violations. Consider using
default=Noneandnull=True, or remove the default entirely since the field is auto-generated in the save method.