A Django web application for curating Bug Bounty educational Videos 
"""
ASGI config for bugbountytube project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bugbountytube.settings')

application = get_asgi_application()

bugbountytube/asgi.py
PatrikFehrenbach/practical-bug-bounty
"""
Django settings for bugbountytube project.

Generated by 'django-admin startproject' using Django 4.1.6.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from pathlib import Path
import os
import dj_database_url
import sentry_sdk

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

sentry_sdk.init(
    dsn=os.environ.get('SENTRY_DSN'),
    # Set traces_sample_rate to 1.0 to capture 100%
    # of transactions for performance monitoring.
    traces_sample_rate=1.0,
    # Set profiles_sample_rate to 1.0 to profile 100%
    # of sampled transactions.
    # We recommend adjusting this value in production.
    profiles_sample_rate=1.0,
)


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-bym030g!9%ywrl^qxzhcfh=6o448g^(_+gq=m(m%8l^k67jh@2'

# SECURITY WARNING: don't run with debug turned on in production!

IS_DEBUG = os.environ.get('IS_DEBUG', 'True')

if IS_DEBUG == 'True':
    DEBUG = True
else:
    DEBUG = False

ALLOWED_HOSTS = ['*']
CSRF_TRUSTED_ORIGINS = ['https://practicalbugbounty.com', 'https://www.practicalbugbounty.com']

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'videos',
    'grappelli',
    'adminsortable2',
    'markdownx',
    'challenges',
    'resources',
    'whitenoise.runserver_nostatic',
    'django_select2'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
]

ROOT_URLCONF = 'bugbountytube.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'], 
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'bugbountytube.wsgi.application'

USE_POSTGRES = os.environ.get('USE_POSTGRES', 'False') == 'True'

if USE_POSTGRES:
    DATABASE_URL = os.environ.get('DATABASE_URL')
    if DATABASE_URL:
        DATABASES = {
            'default': dj_database_url.config(default=DATABASE_URL)
        }
    else:
        raise ValueError("USE_POSTGRES is set, but DATABASE_URL is not provided.")
else:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': BASE_DIR / 'db.sqlite3',
        }
    }

# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATICFILES_DIRS = [
    BASE_DIR / 'static',
]

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'collected_static'
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

bugbountytube/settings.py
PatrikFehrenbach/practical-bug-bounty
"""bugbountytube URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('videos.urls')),
    path('', include('challenges.urls')),
    path('', include('resources.urls')),
    path('grappelli/', include('grappelli.urls')), # grappelli URLS
    path('markdownx/', include('markdownx.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
bugbountytube/urls.py
PatrikFehrenbach/practical-bug-bounty
"""
WSGI config for bugbountytube project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bugbountytube.settings')

application = get_wsgi_application()

bugbountytube/wsgi.py
PatrikFehrenbach/practical-bug-bounty
from django.contrib import admin
from .models import ChallengePlatform

admin.site.register(ChallengePlatform)
challenges/admin.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-10 19:09

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='ChallengePlatform',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255)),
                ('description', models.TextField()),
                ('url', models.URLField()),
                ('difficulty_level', models.CharField(choices=[('Beginner', 'Beginner'), ('Intermediate', 'Intermediate'), ('Advanced', 'Advanced')], max_length=100)),
                ('payment_status', models.CharField(choices=[('Paid', 'Paid'), ('Unpaid', 'Unpaid'), ('Both', 'Both')], max_length=100)),
                ('is_active', models.BooleanField(default=True)),
            ],
        ),
    ]

challenges/migrations/0001_initial.py
PatrikFehrenbach/practical-bug-bounty
from django.db import models

class ChallengePlatform(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    url = models.URLField()
    difficulty_level = models.CharField(
        max_length=100, 
        choices=[
            ('Beginner', 'Beginner'), 
            ('Intermediate', 'Intermediate'), 
            ('Advanced', 'Advanced')
        ]
    )
    payment_status = models.CharField(
        max_length=100, 
        choices=[
            ('Paid', 'Paid'),
            ('Unpaid', 'Unpaid'),
            ('Both', 'Both')
        ]
    )
    is_active = models.BooleanField(default=True)
    
    def __str__(self):
        return self.name

challenges/models.py
PatrikFehrenbach/practical-bug-bounty
from django.urls import path
from . import views


urlpatterns = [

    path('challenges/', views.challenges, name='challenges'),

]
challenges/urls.py
PatrikFehrenbach/practical-bug-bounty
from django.shortcuts import render
from .models import ChallengePlatform
# Create your views here.
def challenges(request):
    all_challenges = ChallengePlatform.objects.filter(is_active=True)
    return render(request, 'challenges.html', {'challenges': all_challenges})

challenges/views.py
PatrikFehrenbach/practical-bug-bounty
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bugbountytube.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

manage.py
PatrikFehrenbach/practical-bug-bounty
from django.contrib import admin
from django.urls import path
from django.shortcuts import render, redirect
from .models import Resource, Tag
from .forms import ImportMediumForm
import requests
import os
from threading import Thread


ACCESS_TOKEN = os.environ.get('GITHUB_ACCESS_TOKEN')

# Admin for the Tag model
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ('name',)
    search_fields = ('name',)

# GitHub import mixin
class GitHubImportMixin:
    def import_github(self, request):
        if request.method == "POST":
            repo_urls = request.POST.get('repo_urls').splitlines()

            # Start the background task in a separate thread
            thread = Thread(target=self.import_github_thread, args=(repo_urls,))
            thread.start()

            return redirect("..")
        return render(request, "admin/import_github.html")

    def import_github_thread(self, repo_urls):
        for repo_url in repo_urls:
            details = fetch_github_repo_details(repo_url.strip())
            if details and details.get('title'):
                existing_resource = Resource.objects.filter(title=details['title']).first()
                if not existing_resource:
                    resource = Resource(
                        resource_type='github_repo',
                        title=details['title'],
                        description=details.get('description') or '',
                        url=details.get('url', ''),
                        author=details.get('author', ''),
                    )
                    resource.save()

                    # Process tags
                    for topic in details['tags']:
                        tag, created = Tag.objects.get_or_create(name=topic)
                        resource.tags.add(tag)
                    resource.save()

    def get_github_urls(self):
        return [
            path('import-github/', self.admin_site.admin_view(self.import_github), name='import-github'),
        ]

# Medium import mixin
class MediumImportMixin:
    def format_medium_title(self, title):
        parts = title.split('-')[:-1]  # split by '-' and discard the last part
        formatted_title = ' '.join([part.capitalize() for part in parts])
        return formatted_title

    def import_medium(self, request):
        if request.method == "POST":
            article_urls = request.POST.get('article_urls').splitlines()
            tag_ids = request.POST.getlist('tags')  # Get all selected tag IDs
            tags = Tag.objects.filter(id__in=tag_ids)  # Fetch all selected tags

            for url in article_urls:
                parts = url.split('/')
                author = parts[-2][1:]  # Remove '@' from the username
                raw_title = parts[-1]
                title = self.format_medium_title(raw_title)  # Note the 'self.' here
                resource = Resource(
                    resource_type='article',
                    title=title,
                    url=url,
                    author=author
                )
                try:
                    resource.save()
                    resource.tags.set(tags)  # Set multiple tags
                except Exception as e:
                    print(e)
                    continue

            return redirect("..")

        form = ImportMediumForm()
        return render(request, "admin/import_medium.html", {'form': form})



    def get_medium_urls(self):
        return [
            path('import-medium/', self.admin_site.admin_view(self.import_medium), name='import-medium'),
        ]

# ResourceAdmin inheriting from the mixins
@admin.register(Resource)
class ResourceAdmin(GitHubImportMixin, MediumImportMixin, admin.ModelAdmin):
    list_display = ('title', 'resource_type', 'added_date', 'author')
    list_filter = ('resource_type', 'added_date', 'tags')
    search_fields = ('title', 'description', 'author')
    ordering = ('-added_date',)
    date_hierarchy = 'added_date'
    filter_horizontal = ('tags',)

    def get_urls(self):
        urls = super().get_urls()
        return self.get_github_urls() + self.get_medium_urls() + urls

# Fetch GitHub repo details function
def fetch_github_repo_details(repo_url):
    parts = repo_url.split('/')
    owner = parts[-2]
    repo_name = parts[-1]

    headers = {
        "Authorization": f"token {ACCESS_TOKEN}",
        "Accept": "application/vnd.github.v3+json"
    }

    # Fetch repo details using GitHub API
    try:
        api_url = f"https://api.github.com/repos/{owner}/{repo_name}"
        response = requests.get(api_url, headers=headers)
        data = response.json()
    except Exception as e:
        return None

    # Fetch topics (tags) associated with the repo
    topics_response = requests.get(api_url + "/topics", headers=headers)
    topics = topics_response.json().get('names', [])

    return {
        'title': data.get('name'),
        'description': data.get('description'),
        'url': data.get('html_url'),
        'author': data.get('owner', {}).get('login'),
        'tags': topics,
    }

resources/admin.py
PatrikFehrenbach/practical-bug-bounty
from django import forms
from django_select2.forms import Select2MultipleWidget
from .models import Tag

class ImportMediumForm(forms.Form):
    article_urls = forms.CharField(widget=forms.Textarea(attrs={'rows': 10}), label="Medium Article URLs (One per line)")
    tags = forms.ModelMultipleChoiceField(
        queryset=Tag.objects.all(),
        widget=Select2MultipleWidget(attrs={'data-placeholder': 'Select tags'}),
        label="Tags for these Articles"
    )

resources/forms.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.2.6 on 2023-10-11 13:30

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Tag',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100, unique=True)),
            ],
        ),
        migrations.CreateModel(
            name='Resource',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('resource_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('github_repo', 'GitHub Repository'), ('article', 'Article'), ('video', 'Video')], default='article', max_length=20)),
                ('title', models.CharField(max_length=255)),
                ('description', models.TextField(blank=True)),
                ('url', models.URLField(max_length=500)),
                ('added_date', models.DateField(auto_now_add=True)),
                ('author', models.CharField(blank=True, max_length=255)),
                ('tags', models.ManyToManyField(blank=True, related_name='resources', to='resources.tag')),
            ],
        ),
    ]

resources/migrations/0001_initial.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.2.6 on 2023-10-12 09:16

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('resources', '0001_initial'),
    ]

    operations = [
        migrations.AlterField(
            model_name='resource',
            name='title',
            field=models.CharField(max_length=255, unique=True),
        ),
    ]

resources/migrations/0002_alter_resource_title.py
PatrikFehrenbach/practical-bug-bounty
from django.db import models

class Tag(models.Model):
    """Model to represent tags for resources."""
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name

class Resource(models.Model):
    """Model to represent a bug bounty resource."""

    RESOURCE_TYPES = (
        ('blog_post', 'Blog Post'),
        ('github_repo', 'GitHub Repository'),
        ('article', 'Article'),
        ('video', 'Video'),
        # ... add more types as needed
    )

    resource_type = models.CharField(max_length=20, choices=RESOURCE_TYPES, default='article')
    title = models.CharField(max_length=255, unique=True)
    description = models.TextField(blank=True)
    url = models.URLField(max_length=500)
    added_date = models.DateField(auto_now_add=True)
    author = models.CharField(max_length=255, blank=True)
    tags = models.ManyToManyField(Tag, related_name="resources", blank=True)

    def __str__(self):
        return self.title

resources/models.py
PatrikFehrenbach/practical-bug-bounty
from django.urls import path
from . import views


urlpatterns = [

    path('resources/', views.resources, name='resources'),

]
resources/urls.py
PatrikFehrenbach/practical-bug-bounty
from django.shortcuts import render
from django.db.models import Q, Count
from .models import Resource, Tag
from django.core.paginator import Paginator

def resources(request):
    # Constants
    ITEMS_PER_PAGE = 10  # Change this to your desired items per page

    # Fetch all resources initially
    resource_list = Resource.objects.all()

    # Filter by resource_type if provided
    resource_type = request.GET.get('resource_type')
    if resource_type:
        resource_list = resource_list.filter(resource_type=resource_type)

    # Filter by tag if provided
    tag_name = request.GET.get('tag')
    if tag_name:
        resource_list = resource_list.filter(tags__name=tag_name)

    # Search by title or description if search term is provided
    search_term = request.GET.get('search')
    if search_term:
        resource_list = resource_list.filter(
            Q(title__icontains=search_term) | Q(description__icontains=search_term)
        )

    # Pagination for resources
    paginator = Paginator(resource_list, ITEMS_PER_PAGE)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    resource_type_counts = {res_type[0]: Resource.objects.filter(resource_type=res_type[0]).count() for res_type in Resource.RESOURCE_TYPES}

    icons = {
        'blog_post': 'bi bi-journal-text',
        'github_repo': 'bi bi-github',
        'article': 'bi bi-file-earmark-text',
        'video': 'bi bi-camera-video',
    }

    # Fetch tags ordered by their usage frequency and with minimum occurrences
    tags_with_count = Tag.objects.annotate(resource_count=Count('resources')).filter(resource_count__gte=3).values_list('name', 'resource_count').order_by('-resource_count')

    context = {
        'resource_type_counts': resource_type_counts,
        'resources': page_obj,  # Update this to pass the paginated resources
        'tags_with_count': tags_with_count,  # Pass tags with their count
        'resource_type': resource_type,  # Pass the selected resource type
        'tag_name': tag_name,  # Pass the selected tag for retaining the filter during pagination
        'RESOURCE_TYPES': Resource.RESOURCE_TYPES,
        'icons': icons
    }

    return render(request, 'resources.html', context)


resources/views.py
PatrikFehrenbach/practical-bug-bounty
from django.contrib import admin
from .models import Module, SubModule, Topic, Video, CourseNote, AdditionalLink
from adminsortable2.admin import SortableAdminMixin
from markdownx.admin import MarkdownxModelAdmin

@admin.register(Module)
class ModuleAdmin(SortableAdminMixin, admin.ModelAdmin):
    list_display = ['name', 'description']
    search_fields = ['name', 'description']

@admin.register(SubModule)
class SubModuleAdmin(SortableAdminMixin, admin.ModelAdmin):
    list_display = ['name', 'description', 'module']
    list_filter = ['module']
    search_fields = ['name', 'description']

@admin.register(Topic)
class TopicAdmin(SortableAdminMixin, admin.ModelAdmin):
    list_display = ['name', 'description', 'submodule']
    list_filter = ['submodule']
    search_fields = ['name', 'description']

@admin.register(Video)
class VideoAdmin(admin.ModelAdmin):
    list_display = ['title', 'url', 'tags', 'topic']
    list_filter = ['topic']
    search_fields = ['title', 'tags']

@admin.register(CourseNote)
class CourseNoteAdmin(MarkdownxModelAdmin):
    list_display = ['topic', 'content']
    list_filter = ['topic']

@admin.register(AdditionalLink)
class AdditionalLinkAdmin(admin.ModelAdmin):
    list_display = ['topic', 'url', 'description']
    list_filter = ['topic']
    search_fields = ['description', 'url']

videos/admin.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-05 21:06

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255, unique=True)),
                ('description', models.TextField(blank=True)),
            ],
        ),
        migrations.CreateModel(
            name='Video',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=255)),
                ('url', models.URLField()),
                ('tags', models.CharField(blank=True, max_length=255)),
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='videos.category')),
            ],
        ),
    ]

videos/migrations/0001_initial.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-05 23:04

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('videos', '0001_initial'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='category',
            options={'verbose_name_plural': 'categories'},
        ),
        migrations.AddField(
            model_name='category',
            name='parent',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subcategories', to='videos.category'),
        ),
        migrations.AlterField(
            model_name='category',
            name='name',
            field=models.CharField(max_length=255),
        ),
        migrations.AlterUniqueTogether(
            name='category',
            unique_together={('name', 'parent')},
        ),
    ]

videos/migrations/0002_alter_category_options_category_parent_and_more.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-06 19:02

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('videos', '0002_alter_category_options_category_parent_and_more'),
    ]

    operations = [
        migrations.AlterModelOptions(
            name='category',
            options={'ordering': ['order'], 'verbose_name_plural': 'categories'},
        ),
        migrations.AddField(
            model_name='category',
            name='order',
            field=models.PositiveIntegerField(db_index=True, default=0, editable=False),
        ),
    ]

videos/migrations/0003_alter_category_options_category_order.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-06 19:49

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('videos', '0003_alter_category_options_category_order'),
    ]

    operations = [
        migrations.CreateModel(
            name='VideoSuggestion',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=255)),
                ('url', models.URLField()),
                ('tags', models.CharField(blank=True, max_length=255)),
                ('approved', models.BooleanField(default=False)),
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='videos.category')),
            ],
        ),
        migrations.CreateModel(
            name='CategorySuggestion',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255)),
                ('description', models.TextField(blank=True)),
                ('approved', models.BooleanField(default=False)),
                ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='videos.category')),
            ],
        ),
    ]

videos/migrations/0004_videosuggestion_categorysuggestion.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-06 21:21

from django.db import migrations
import markdownx.models


class Migration(migrations.Migration):

    dependencies = [
        ('videos', '0004_videosuggestion_categorysuggestion'),
    ]

    operations = [
        migrations.AlterField(
            model_name='categorysuggestion',
            name='description',
            field=markdownx.models.MarkdownxField(),
        ),
    ]

videos/migrations/0005_alter_categorysuggestion_description.py
PatrikFehrenbach/practical-bug-bounty
# Generated by Django 4.1.6 on 2023-10-08 08:14

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    dependencies = [
        ('videos', '0005_alter_categorysuggestion_description'),
    ]

    operations = [
        migrations.CreateModel(
            name='AdditionalLink',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('url', models.URLField()),
                ('description', models.TextField(blank=True)),
            ],
        ),
        migrations.CreateModel(
            name='CourseNote',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('content', models.TextField()),
                ('order', models.PositiveIntegerField(db_index=True, default=0, editable=False)),
            ],
        ),
        migrations.CreateModel(
            name='Module',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255)),
                ('description', models.TextField(blank=True)),
                ('order', models.PositiveIntegerField(db_index=True, default=0, editable=False)),
            ],
            options={
                'ordering': ['order'],
            },
        ),
        migrations.CreateModel(
            name='SubModule',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255)),
                ('description', models.TextField(blank=True)),
                ('order', models.PositiveIntegerField(db_index=True, default=0, editable=False)),
                ('module', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submodules', to='videos.module')),
            ],
            options={
                'ordering': ['order'],
            },
        ),
        migrations.CreateModel(
            name='Topic',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=255)),
                ('description', models.TextField(blank=True)),
                ('order', models.PositiveIntegerField(db_index=True, default=0, editable=False)),
                ('submodule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='videos.submodule')),
            ],
            options={
                'ordering': ['order'],
            },
        ),
        migrations.RemoveField(
            model_name='categorysuggestion',
            name='parent',
        ),
        migrations.RemoveField(
            model_name='video',
            name='category',
        ),
        migrations.RemoveField(
            model_name='videosuggestion',
            name='category',
        ),
        migrations.DeleteModel(
            name='Category',
        ),
        migrations.AddField(
            model_name='coursenote',
            name='topic',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='videos.topic'),
        ),
        migrations.AddField(
            model_name='additionallink',
            name='topic',
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='additional_links', to='videos.topic'),
        ),
        migrations.AddField(
            model_name='video',
            name='topic',
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='videos', to='videos.topic'),
        ),
    ]

videos/migrations/0006_additionallink_coursenote_module_submodule_topic_and_more.py
PatrikFehrenbach/practical-bug-bounty
from django.db import models
from markdownx.models import MarkdownxField

class Module(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    order = models.PositiveIntegerField(default=0, editable=False, db_index=True)

    class Meta:
        ordering = ['order']

    def __str__(self):
        return self.name

class SubModule(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='submodules')
    order = models.PositiveIntegerField(default=0, editable=False, db_index=True)

    class Meta:
        ordering = ['order']
        
    def __str__(self):
        return f"{self.module.name} - {self.name}"

class Topic(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)
    submodule = models.ForeignKey(SubModule, on_delete=models.CASCADE, related_name='topics')
    order = models.PositiveIntegerField(default=0, editable=False, db_index=True)
    
    class Meta:
        ordering = ['order']

    def __str__(self):
        return f"{self.submodule.name} - {self.name}"

class Video(models.Model):
    title = models.CharField(max_length=255)
    url = models.URLField()
    tags = models.CharField(max_length=255, blank=True)  # Comma-separated tags
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='videos', blank=True, null=True)

    def __str__(self):
        return self.title

class CourseNote(models.Model):
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='notes')
    content = models.TextField()
    order = models.PositiveIntegerField(default=0, editable=False, db_index=True)

    def __str__(self):
        return f"Note for {self.topic.name}"

class AdditionalLink(models.Model):
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE, related_name='additional_links')
    url = models.URLField()
    description = models.TextField(blank=True)

    def __str__(self):
        return self.url


class CategorySuggestion(models.Model):
    name = models.CharField(max_length=255)
    description = MarkdownxField()
    approved = models.BooleanField(default=False)

    def __str__(self):
        return self.name


class VideoSuggestion(models.Model):
    title = models.CharField(max_length=255)
    url = models.URLField()
    tags = models.CharField(max_length=255, blank=True)  
    approved = models.BooleanField(default=False)

    def __str__(self):
        return self.title
videos/models.py
PatrikFehrenbach/practical-bug-bounty
from django import template

register = template.Library()

@register.filter(name='get_icon')
def get_icon(icons_dict, key):
    return icons_dict.get(key, '')

@register.filter(name='get_item')
def get_item(dictionary, key):
    return dictionary.get(key)
videos/templatetags/custom_filters.py
PatrikFehrenbach/practical-bug-bounty
from django import template

register = template.Library()

@register.filter(name='embed_url')
def embed_url(value):
    if "watch?v=" in value:
        return value.replace("watch?v=", "embed/")
    elif "youtu.be/" in value:
        return value.replace("youtu.be/", "youtube.com/embed/")
    return value

videos/templatetags/video_filters.py
PatrikFehrenbach/practical-bug-bounty
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('contribute/', views.contribute, name='contribute'),
    path('philosophy/', views.philosophy, name='philosophy'),
    path('contact/', views.contact, name='contact'),
    path('community/', views.community, name='community'),
    path('search/', views.search, name='search'),
    path('privacy/', views.privacy, name='privacy'),

#    path('category/<int:category_id>/', views.category_videos, name='category_videos'),

]

videos/urls.py
PatrikFehrenbach/practical-bug-bounty
from django.shortcuts import render, redirect,get_object_or_404
from .models import Module, SubModule, Topic, Video
from django.http import JsonResponse
import re
import logging

def index(request):
    feedback_message = None


    modules = Module.objects.all()
    return render(request, 'index.html', {
        'modules': modules,
        'feedback_message': feedback_message
    })

def privacy(request):
        
        return render(request, 'privacy.html')

def privacy(request):
            
            return render(request, 'privacy.html')
        

def contact(request):
    
    return render(request, 'contact.html')

def contribute(request):
    
    return render(request, 'contribute.html')

def community(request):
        
        return render(request, 'community.html')

def philosophy(request):
    
    return render(request, 'philosophy.html')

def about(request):
        
        return render(request, 'about.html')
    
def search(request):
    query = request.GET.get('q')
    results = []

    # Search Videos
    videos = Video.objects.filter(title__icontains=query)
    for video in videos:
        results.append({'type': 'Video', 'title': video.title, 'url': video.url})

    # Extend this for other models like Topic, CourseNote, etc.

    return JsonResponse(results, safe=False)

def extract_video_id_from_url(url):
    # Extract video ID from a YouTube URL using regex
    video_id_match = re.search(r'(?<=v=)[^&#]+', url)
    video_id_match = video_id_match or re.search(r'(?<=be/)[^&#]+', url)
    video_id = (video_id_match.group(0) if video_id_match else None)
    return video_id

def get_video_details(video_id, api_key):
    youtube = build('youtube', 'v3', developerKey=api_key)

    try:
        response = youtube.videos().list(
            part='snippet',
            id=video_id
        ).execute()

        # Extract details from the response
        items = response.get('items', [])
        if not items:
            return None

        snippet = items[0]['snippet']
        title = snippet['title']
        description = snippet['description']
        channel_title = snippet['channelTitle']

        return {
            'title': title,
            'description': description,
            'channel_title': channel_title,
        }

    except HttpError as e:
        print(f"An error occurred: {e}")
        return None

videos/views.py
PatrikFehrenbach/practical-bug-bounty