From 1275cdfde6a81c23c5c6fcb65330f32aa71f079f Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:43:03 +0500 Subject: [PATCH 01/15] feat: [POC] for Vertical Tagging Feature --- course_discovery/apps/tagging/__init__.py | 0 course_discovery/apps/tagging/admin.py | 153 ++++++++++++++++++ course_discovery/apps/tagging/apps.py | 6 + course_discovery/apps/tagging/forms.py | 4 + .../apps/tagging/migrations/0001_initial.py | 86 ++++++++++ .../apps/tagging/migrations/__init__.py | 0 course_discovery/apps/tagging/models.py | 61 +++++++ .../courseverticalfilters/change_list.html | 13 ++ .../upload_csv_form.html | 11 ++ .../tagging/verticalfilter/change_list.html | 13 ++ .../verticalfilter/upload_csv_form.html | 11 ++ .../templates/admin/upload_csv_form.html | 10 ++ course_discovery/apps/tagging/tests.py | 3 + course_discovery/apps/tagging/views.py | 3 + course_discovery/settings/base.py | 1 + 15 files changed, 375 insertions(+) create mode 100644 course_discovery/apps/tagging/__init__.py create mode 100644 course_discovery/apps/tagging/admin.py create mode 100644 course_discovery/apps/tagging/apps.py create mode 100644 course_discovery/apps/tagging/forms.py create mode 100644 course_discovery/apps/tagging/migrations/0001_initial.py create mode 100644 course_discovery/apps/tagging/migrations/__init__.py create mode 100644 course_discovery/apps/tagging/models.py create mode 100644 course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/change_list.html create mode 100644 course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html create mode 100644 course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html create mode 100644 course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html create mode 100644 course_discovery/apps/tagging/templates/admin/upload_csv_form.html create mode 100644 course_discovery/apps/tagging/tests.py create mode 100644 course_discovery/apps/tagging/views.py diff --git a/course_discovery/apps/tagging/__init__.py b/course_discovery/apps/tagging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py new file mode 100644 index 0000000000..ed1f2632c8 --- /dev/null +++ b/course_discovery/apps/tagging/admin.py @@ -0,0 +1,153 @@ +import csv +from django.contrib import admin +from django.contrib import messages +from django.urls import reverse +from django.shortcuts import redirect, render +from django.template.response import TemplateResponse +from io import TextIOWrapper +from course_discovery.apps.tagging.forms import CSVUploadForm +from course_discovery.apps.course_metadata.models import Course +from course_discovery.apps.tagging.models import VerticalFilter, SubVericalFilter, CourseVerticalFilters, ProgramVerticalFilters + + +@admin.register(VerticalFilter) +class VerticalFilterAdmin(admin.ModelAdmin): + list_display = ('name', 'is_active', 'description', 'slug',) + search_fields = ('name',) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + extra_context['upload_csv_url'] = reverse('admin:verticalfilter_upload_csv') + return super().changelist_view(request, extra_context=extra_context) + + def get_urls(self): + from django.urls import path + urls = super().get_urls() + custom_urls = [ + path( + 'upload-csv/', + self.admin_site.admin_view(self.upload_csv), + name='verticalfilter_upload_csv', + ), + ] + return custom_urls + urls + + def upload_csv(self, request): + from django.shortcuts import redirect + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + file_data = TextIOWrapper(csv_file, encoding='utf-8') + csv_reader = csv.reader(file_data) + + # Skipping the header row + next(csv_reader) + + created_count = 0 + for row in csv_reader: + # Assuming the CSV structure is [name, description, is_active] + if row: # Skip empty rows + name, description, is_active = row + try: + # Create a VerticalFilter instance for each row + VerticalFilter.objects.create( + name=name, + description=description, + is_active=(is_active.lower() == 'true'), # Assuming 'true' or 'false' in CSV + ) + created_count += 1 + except Exception as e: + # Log the error or handle invalid rows + messages.error(request, f"Error processing row: {row}. Error: {e}") + + messages.success(request, f"{created_count} VerticalFilter instances created successfully!") + return redirect('admin:tagging_verticalfilter_changelist') + + return TemplateResponse( + request, + "admin/tagging/verticalfilter/upload_csv_form.html", + context={'opts': self.model._meta}, + ) + + +@admin.register(SubVericalFilter) +class SubVericalFilterAdmin(admin.ModelAdmin): + list_display = ('name', 'is_active', 'slug', 'description', 'vertical_filters') + list_filter = ('vertical_filters', ) + search_fields = ('name',) + ordering = ('name',) + + +@admin.register(CourseVerticalFilters) +class CourseVerticalFiltersAdmin(admin.ModelAdmin): + list_display = ('course', 'vertical', 'sub_vertical') + list_filter = ('vertical', 'sub_vertical') + search_fields = ('course__title', 'vertical__name', 'sub_vertical__name') + ordering = ('course__title',) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == 'course': + kwargs['queryset'] = Course.objects.filter(draft=False) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + extra_context['upload_csv_url'] = reverse('admin:courseverticalfilters_upload_csv') + return super().changelist_view(request, extra_context=extra_context) + + + def get_urls(self): + from django.urls import path + urls = super().get_urls() + custom_urls = [ + path('upload-csv/', self.upload_csv, name='courseverticalfilters_upload_csv'), + ] + return custom_urls + urls + + # Handle the CSV upload + def upload_csv(self, request): + if request.method == 'POST' and request.FILES.get('csv_file'): + form = CSVUploadForm(request.POST, request.FILES) + if form.is_valid(): + # Process the CSV file + csv_file = form.cleaned_data['csv_file'] + try: + decoded_file = csv_file.read().decode('utf-8').splitlines() + reader = csv.reader(decoded_file) + next(reader) # Skip the header row + for row in reader: + course_uuid = row[0] # Assuming the first column is the course title + vertical_name = row[1] # Assuming the second column is the vertical name + sub_vertical_name = row[2] # Assuming the third column is the sub-vertical name + + # Get the corresponding objects from the database + import pdb; pdb.set_trace(); + course = Course.objects.get(uuid=course_uuid) + vertical, _ = VerticalFilter.objects.get_or_create(name=vertical_name) + sub_vertical, _ = SubVericalFilter.objects.get_or_create(name=sub_vertical_name, vertical_filters=vertical) + + # Create or update the CourseVerticalFilters instance + CourseVerticalFilters.objects.update_or_create( + course=course, + defaults={'vertical': vertical, 'sub_vertical': sub_vertical} + ) + + messages.success(request, "CSV uploaded and processed successfully.") + except Exception as e: + messages.error(request, f"Error processing CSV: {str(e)}") + return redirect('admin:tagging_courseverticalfilters_changelist') + else: + form = CSVUploadForm() + + return render( + request, + 'admin/upload_csv_form.html', # You can create a custom template for the upload form + context={'form': form} + ) + + +@admin.register(ProgramVerticalFilters) +class ProgramVerticalFiltersAdmin(admin.ModelAdmin): + list_display = ('program', 'vertical', 'sub_vertical') + list_filter = ('vertical', 'sub_vertical') + search_fields = ('program__title', 'vertical__name', 'sub_vertical__name') + ordering = ('program__title',) diff --git a/course_discovery/apps/tagging/apps.py b/course_discovery/apps/tagging/apps.py new file mode 100644 index 0000000000..802f074c09 --- /dev/null +++ b/course_discovery/apps/tagging/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TaggingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'course_discovery.apps.tagging' diff --git a/course_discovery/apps/tagging/forms.py b/course_discovery/apps/tagging/forms.py new file mode 100644 index 0000000000..695bd76a03 --- /dev/null +++ b/course_discovery/apps/tagging/forms.py @@ -0,0 +1,4 @@ +from django import forms + +class CSVUploadForm(forms.Form): + csv_file = forms.FileField(label="Upload CSV File") diff --git a/course_discovery/apps/tagging/migrations/0001_initial.py b/course_discovery/apps/tagging/migrations/0001_initial.py new file mode 100644 index 0000000000..07e7d0c668 --- /dev/null +++ b/course_discovery/apps/tagging/migrations/0001_initial.py @@ -0,0 +1,86 @@ +# Generated by Django 4.2.14 on 2024-12-23 13:47 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields +import model_utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_metadata', '0345_courserun_translation_languages_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='VerticalFilter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=255, unique=True)), + ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name', unique=True)), + ('description', models.TextField(blank=True)), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name_plural': 'Vertical Filters', + 'ordering': ['name'], + 'unique_together': {('name',)}, + }, + ), + migrations.CreateModel( + name='SubVericalFilter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=255, unique=True)), + ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name', unique=True)), + ('description', models.TextField(blank=True)), + ('is_active', models.BooleanField(default=True)), + ('vertical_filters', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name_plural': 'Sub Vertical Filters', + 'ordering': ['name'], + 'unique_together': {('name',)}, + }, + ), + migrations.CreateModel( + name='ProgramVerticalFilters', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('program', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='course_metadata.program')), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name_plural': 'Program Vertical Filters', + 'ordering': ['program__title'], + 'unique_together': {('program',)}, + }, + ), + migrations.CreateModel( + name='CourseVerticalFilters', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('course', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='course_metadata.course')), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name_plural': 'Course Vertical Filters', + 'ordering': ['course__title'], + 'unique_together': {('course',)}, + }, + ), + ] diff --git a/course_discovery/apps/tagging/migrations/__init__.py b/course_discovery/apps/tagging/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py new file mode 100644 index 0000000000..d2d3cbe007 --- /dev/null +++ b/course_discovery/apps/tagging/models.py @@ -0,0 +1,61 @@ +from django.db import models +from model_utils.models import TimeStampedModel +from django_extensions.db.fields import AutoSlugField +from course_discovery.apps.course_metadata.models import Course, Program + + +# Create your models here. +class VerticalFilter(TimeStampedModel): + name = models.CharField(max_length=255, unique=True) + slug = AutoSlugField(populate_from='name', max_length=255, unique=True) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = 'Vertical Filters' + ordering = ['name'] + unique_together = ['name'] + +class SubVericalFilter(TimeStampedModel): + name = models.CharField(max_length=255, unique=True) + slug = AutoSlugField(populate_from='name', max_length=255, unique=True) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True) + vertical_filters = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='sub_vertical_filters') + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = 'Sub Vertical Filters' + ordering = ['name'] + unique_together = ['name'] + +class CourseVerticalFilters(TimeStampedModel): + course = models.OneToOneField(Course, on_delete=models.CASCADE, related_name='vertical_filters') + vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='course_vertical_filters') + sub_vertical = models.ForeignKey(SubVericalFilter, on_delete=models.CASCADE, related_name='course_sub_vertical_filters') + + def __str__(self): + return self.course.title + + class Meta: + verbose_name_plural = 'Course Vertical Filters' + ordering = ['course__title'] + unique_together = ['course'] + +class ProgramVerticalFilters(TimeStampedModel): + program = models.OneToOneField(Program, on_delete=models.CASCADE, related_name='vertical_filters') + vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='program_vertical_filters') + sub_vertical = models.ForeignKey(SubVericalFilter, on_delete=models.CASCADE, related_name='program_sub_vertical_filters') + + def __str__(self): + return self.program.title + + class Meta: + verbose_name_plural = 'Program Vertical Filters' + ordering = ['program__title'] + unique_together = ['program'] diff --git a/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/change_list.html b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/change_list.html new file mode 100644 index 0000000000..ef14d1a932 --- /dev/null +++ b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/change_list.html @@ -0,0 +1,13 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools %} + +{{ block.super }} +
+




+ + Upload CSV + + +
+{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html new file mode 100644 index 0000000000..78aa4879f0 --- /dev/null +++ b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html @@ -0,0 +1,11 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +

Upload CSV for Course Vertical Filters

+
+ {% csrf_token %} + + + +
+{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html new file mode 100644 index 0000000000..e418f5ba05 --- /dev/null +++ b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html @@ -0,0 +1,13 @@ +{% extends "admin/change_list.html" %} + +{% block object-tools %} + +{{ block.super }} +
+




+ + Upload CSV + + +
+{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html new file mode 100644 index 0000000000..71f3223b09 --- /dev/null +++ b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html @@ -0,0 +1,11 @@ +{% extends "admin/base_site.html" %} + +{% block content %} +

Upload CSV for Vertical Filters

+
+ {% csrf_token %} + + + +
+{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/upload_csv_form.html new file mode 100644 index 0000000000..bbd140e8a4 --- /dev/null +++ b/course_discovery/apps/tagging/templates/admin/upload_csv_form.html @@ -0,0 +1,10 @@ +{% extends "admin/base_site.html" %} +{% block content %} +

Upload CSV for Course Vertical Filters

+
+ {% csrf_token %} + {{ form.as_p }} + +
+

Back to Course Vertical Filters

+{% endblock %} diff --git a/course_discovery/apps/tagging/tests.py b/course_discovery/apps/tagging/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/course_discovery/apps/tagging/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/course_discovery/apps/tagging/views.py b/course_discovery/apps/tagging/views.py new file mode 100644 index 0000000000..91ea44a218 --- /dev/null +++ b/course_discovery/apps/tagging/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/course_discovery/settings/base.py b/course_discovery/settings/base.py index 2d578ac255..d1ba83e24e 100644 --- a/course_discovery/settings/base.py +++ b/course_discovery/settings/base.py @@ -87,6 +87,7 @@ 'course_discovery.apps.publisher_comments', 'course_discovery.apps.learner_pathway', 'course_discovery.apps.taxonomy_support', + 'course_discovery.apps.tagging', ] ES_APPS = [ From 25bd30049f7d615faf5e7a50aaf53948b997cccd Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:44:13 +0500 Subject: [PATCH 02/15] fix: imports sort issue --- course_discovery/apps/tagging/admin.py | 15 +++++++++------ course_discovery/apps/tagging/forms.py | 1 + course_discovery/apps/tagging/models.py | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index ed1f2632c8..07680d4f49 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -1,13 +1,16 @@ import csv -from django.contrib import admin -from django.contrib import messages -from django.urls import reverse +from io import TextIOWrapper + +from django.contrib import admin, messages from django.shortcuts import redirect, render from django.template.response import TemplateResponse -from io import TextIOWrapper -from course_discovery.apps.tagging.forms import CSVUploadForm +from django.urls import reverse + from course_discovery.apps.course_metadata.models import Course -from course_discovery.apps.tagging.models import VerticalFilter, SubVericalFilter, CourseVerticalFilters, ProgramVerticalFilters +from course_discovery.apps.tagging.forms import CSVUploadForm +from course_discovery.apps.tagging.models import ( + CourseVerticalFilters, ProgramVerticalFilters, SubVericalFilter, VerticalFilter +) @admin.register(VerticalFilter) diff --git a/course_discovery/apps/tagging/forms.py b/course_discovery/apps/tagging/forms.py index 695bd76a03..50836dbfc3 100644 --- a/course_discovery/apps/tagging/forms.py +++ b/course_discovery/apps/tagging/forms.py @@ -1,4 +1,5 @@ from django import forms + class CSVUploadForm(forms.Form): csv_file = forms.FileField(label="Upload CSV File") diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index d2d3cbe007..f6a5877738 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -1,6 +1,7 @@ from django.db import models -from model_utils.models import TimeStampedModel from django_extensions.db.fields import AutoSlugField +from model_utils.models import TimeStampedModel + from course_discovery.apps.course_metadata.models import Course, Program From ed502a7673d7db2ef6978efd07c8f4202124059e Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:50:03 +0500 Subject: [PATCH 03/15] chore: remove extra file and fix path --- course_discovery/apps/tagging/admin.py | 2 +- .../courseverticalfilters/upload_csv_form.html | 13 ++++++------- .../tagging/templates/admin/upload_csv_form.html | 10 ---------- 3 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 course_discovery/apps/tagging/templates/admin/upload_csv_form.html diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index 07680d4f49..c236250e15 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -143,7 +143,7 @@ def upload_csv(self, request): return render( request, - 'admin/upload_csv_form.html', # You can create a custom template for the upload form + 'admin/tagging/courseverticalfilters/upload_csv_form.html', # You can create a custom template for the upload form context={'form': form} ) diff --git a/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html index 78aa4879f0..bbd140e8a4 100644 --- a/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html +++ b/course_discovery/apps/tagging/templates/admin/tagging/courseverticalfilters/upload_csv_form.html @@ -1,11 +1,10 @@ {% extends "admin/base_site.html" %} - {% block content %} -

Upload CSV for Course Vertical Filters

-
+

Upload CSV for Course Vertical Filters

+ {% csrf_token %} - - - -
+ {{ form.as_p }} + + +

Back to Course Vertical Filters

{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/upload_csv_form.html deleted file mode 100644 index bbd140e8a4..0000000000 --- a/course_discovery/apps/tagging/templates/admin/upload_csv_form.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block content %} -

Upload CSV for Course Vertical Filters

-
- {% csrf_token %} - {{ form.as_p }} - -
-

Back to Course Vertical Filters

-{% endblock %} From 2c7fa2dc8331114a4b0bcb0980e904bb0def514a Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:52:51 +0500 Subject: [PATCH 04/15] chore: remove some admin code --- course_discovery/apps/tagging/admin.py | 56 ------------------------- course_discovery/apps/tagging/models.py | 1 - 2 files changed, 57 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index c236250e15..8520cf2ae2 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -1,9 +1,7 @@ import csv -from io import TextIOWrapper from django.contrib import admin, messages from django.shortcuts import redirect, render -from django.template.response import TemplateResponse from django.urls import reverse from course_discovery.apps.course_metadata.models import Course @@ -18,60 +16,6 @@ class VerticalFilterAdmin(admin.ModelAdmin): list_display = ('name', 'is_active', 'description', 'slug',) search_fields = ('name',) - def changelist_view(self, request, extra_context=None): - extra_context = extra_context or {} - extra_context['upload_csv_url'] = reverse('admin:verticalfilter_upload_csv') - return super().changelist_view(request, extra_context=extra_context) - - def get_urls(self): - from django.urls import path - urls = super().get_urls() - custom_urls = [ - path( - 'upload-csv/', - self.admin_site.admin_view(self.upload_csv), - name='verticalfilter_upload_csv', - ), - ] - return custom_urls + urls - - def upload_csv(self, request): - from django.shortcuts import redirect - if request.method == 'POST' and request.FILES.get('csv_file'): - csv_file = request.FILES['csv_file'] - file_data = TextIOWrapper(csv_file, encoding='utf-8') - csv_reader = csv.reader(file_data) - - # Skipping the header row - next(csv_reader) - - created_count = 0 - for row in csv_reader: - # Assuming the CSV structure is [name, description, is_active] - if row: # Skip empty rows - name, description, is_active = row - try: - # Create a VerticalFilter instance for each row - VerticalFilter.objects.create( - name=name, - description=description, - is_active=(is_active.lower() == 'true'), # Assuming 'true' or 'false' in CSV - ) - created_count += 1 - except Exception as e: - # Log the error or handle invalid rows - messages.error(request, f"Error processing row: {row}. Error: {e}") - - messages.success(request, f"{created_count} VerticalFilter instances created successfully!") - return redirect('admin:tagging_verticalfilter_changelist') - - return TemplateResponse( - request, - "admin/tagging/verticalfilter/upload_csv_form.html", - context={'opts': self.model._meta}, - ) - - @admin.register(SubVericalFilter) class SubVericalFilterAdmin(admin.ModelAdmin): list_display = ('name', 'is_active', 'slug', 'description', 'vertical_filters') diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index f6a5877738..23c2b218da 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -5,7 +5,6 @@ from course_discovery.apps.course_metadata.models import Course, Program -# Create your models here. class VerticalFilter(TimeStampedModel): name = models.CharField(max_length=255, unique=True) slug = AutoSlugField(populate_from='name', max_length=255, unique=True) From 14ed4c1f5a2fb567d2e7c7dfe604f549cba93e30 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:55:44 +0500 Subject: [PATCH 05/15] chore: clean bit of admin code --- course_discovery/apps/tagging/admin.py | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index 8520cf2ae2..f632a72fab 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -13,11 +13,17 @@ @admin.register(VerticalFilter) class VerticalFilterAdmin(admin.ModelAdmin): + """ + Admin class for VerticalFilter model admin. + """ list_display = ('name', 'is_active', 'description', 'slug',) search_fields = ('name',) @admin.register(SubVericalFilter) class SubVericalFilterAdmin(admin.ModelAdmin): + """ + Admin class for SubVerticalFilter model admin. + """ list_display = ('name', 'is_active', 'slug', 'description', 'vertical_filters') list_filter = ('vertical_filters', ) search_fields = ('name',) @@ -30,18 +36,17 @@ class CourseVerticalFiltersAdmin(admin.ModelAdmin): list_filter = ('vertical', 'sub_vertical') search_fields = ('course__title', 'vertical__name', 'sub_vertical__name') ordering = ('course__title',) - + def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == 'course': kwargs['queryset'] = Course.objects.filter(draft=False) return super().formfield_for_foreignkey(db_field, request, **kwargs) - + def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} extra_context['upload_csv_url'] = reverse('admin:courseverticalfilters_upload_csv') return super().changelist_view(request, extra_context=extra_context) - def get_urls(self): from django.urls import path urls = super().get_urls() @@ -50,29 +55,26 @@ def get_urls(self): ] return custom_urls + urls - # Handle the CSV upload def upload_csv(self, request): if request.method == 'POST' and request.FILES.get('csv_file'): form = CSVUploadForm(request.POST, request.FILES) if form.is_valid(): - # Process the CSV file csv_file = form.cleaned_data['csv_file'] try: decoded_file = csv_file.read().decode('utf-8').splitlines() reader = csv.reader(decoded_file) next(reader) # Skip the header row for row in reader: - course_uuid = row[0] # Assuming the first column is the course title - vertical_name = row[1] # Assuming the second column is the vertical name - sub_vertical_name = row[2] # Assuming the third column is the sub-vertical name + course_uuid = row[0] + vertical_name = row[1] + sub_vertical_name = row[2] - # Get the corresponding objects from the database - import pdb; pdb.set_trace(); course = Course.objects.get(uuid=course_uuid) vertical, _ = VerticalFilter.objects.get_or_create(name=vertical_name) - sub_vertical, _ = SubVericalFilter.objects.get_or_create(name=sub_vertical_name, vertical_filters=vertical) + sub_vertical, _ = SubVericalFilter.objects.get_or_create( + name=sub_vertical_name, vertical_filters=vertical + ) - # Create or update the CourseVerticalFilters instance CourseVerticalFilters.objects.update_or_create( course=course, defaults={'vertical': vertical, 'sub_vertical': sub_vertical} @@ -87,7 +89,7 @@ def upload_csv(self, request): return render( request, - 'admin/tagging/courseverticalfilters/upload_csv_form.html', # You can create a custom template for the upload form + 'admin/tagging/courseverticalfilters/upload_csv_form.html', context={'form': form} ) From c09235fb6c7a7e4eb9add07ef03b175218196852 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 00:56:50 +0500 Subject: [PATCH 06/15] chore: add docstrings to model --- course_discovery/apps/tagging/admin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index f632a72fab..a22c81e406 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -14,7 +14,7 @@ @admin.register(VerticalFilter) class VerticalFilterAdmin(admin.ModelAdmin): """ - Admin class for VerticalFilter model admin. + Admin class for VerticalFilter model. """ list_display = ('name', 'is_active', 'description', 'slug',) search_fields = ('name',) @@ -22,7 +22,7 @@ class VerticalFilterAdmin(admin.ModelAdmin): @admin.register(SubVericalFilter) class SubVericalFilterAdmin(admin.ModelAdmin): """ - Admin class for SubVerticalFilter model admin. + Admin class for SubVerticalFilter model. """ list_display = ('name', 'is_active', 'slug', 'description', 'vertical_filters') list_filter = ('vertical_filters', ) @@ -32,6 +32,9 @@ class SubVericalFilterAdmin(admin.ModelAdmin): @admin.register(CourseVerticalFilters) class CourseVerticalFiltersAdmin(admin.ModelAdmin): + """ + Admin class for CourseVerticalFilters model. + """ list_display = ('course', 'vertical', 'sub_vertical') list_filter = ('vertical', 'sub_vertical') search_fields = ('course__title', 'vertical__name', 'sub_vertical__name') From e4425d1bcf884d66bdcc9a73d4697ce968ccb7d6 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 01:00:57 +0500 Subject: [PATCH 07/15] chore: added docstrings and remove program verticals model --- course_discovery/apps/tagging/admin.py | 12 +----------- course_discovery/apps/tagging/forms.py | 3 +++ .../apps/tagging/migrations/0001_initial.py | 18 +----------------- course_discovery/apps/tagging/models.py | 19 +++++-------------- 4 files changed, 10 insertions(+), 42 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index a22c81e406..6506ae753b 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -6,9 +6,7 @@ from course_discovery.apps.course_metadata.models import Course from course_discovery.apps.tagging.forms import CSVUploadForm -from course_discovery.apps.tagging.models import ( - CourseVerticalFilters, ProgramVerticalFilters, SubVericalFilter, VerticalFilter -) +from course_discovery.apps.tagging.models import CourseVerticalFilters, SubVericalFilter, VerticalFilter @admin.register(VerticalFilter) @@ -95,11 +93,3 @@ def upload_csv(self, request): 'admin/tagging/courseverticalfilters/upload_csv_form.html', context={'form': form} ) - - -@admin.register(ProgramVerticalFilters) -class ProgramVerticalFiltersAdmin(admin.ModelAdmin): - list_display = ('program', 'vertical', 'sub_vertical') - list_filter = ('vertical', 'sub_vertical') - search_fields = ('program__title', 'vertical__name', 'sub_vertical__name') - ordering = ('program__title',) diff --git a/course_discovery/apps/tagging/forms.py b/course_discovery/apps/tagging/forms.py index 50836dbfc3..9cae41fa75 100644 --- a/course_discovery/apps/tagging/forms.py +++ b/course_discovery/apps/tagging/forms.py @@ -2,4 +2,7 @@ class CSVUploadForm(forms.Form): + """ + Form for uploading CSV file to support bulk upload feature + """ csv_file = forms.FileField(label="Upload CSV File") diff --git a/course_discovery/apps/tagging/migrations/0001_initial.py b/course_discovery/apps/tagging/migrations/0001_initial.py index 07e7d0c668..45bf1a9bfa 100644 --- a/course_discovery/apps/tagging/migrations/0001_initial.py +++ b/course_discovery/apps/tagging/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.14 on 2024-12-23 13:47 +# Generated by Django 4.2.14 on 2024-12-23 19:59 from django.db import migrations, models import django.db.models.deletion @@ -51,22 +51,6 @@ class Migration(migrations.Migration): 'unique_together': {('name',)}, }, ), - migrations.CreateModel( - name='ProgramVerticalFilters', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('program', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='course_metadata.program')), - ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_sub_vertical_filters', to='tagging.subvericalfilter')), - ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_vertical_filters', to='tagging.verticalfilter')), - ], - options={ - 'verbose_name_plural': 'Program Vertical Filters', - 'ordering': ['program__title'], - 'unique_together': {('program',)}, - }, - ), migrations.CreateModel( name='CourseVerticalFilters', fields=[ diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index 23c2b218da..14f46bd0f5 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -37,7 +37,11 @@ class Meta: class CourseVerticalFilters(TimeStampedModel): course = models.OneToOneField(Course, on_delete=models.CASCADE, related_name='vertical_filters') vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='course_vertical_filters') - sub_vertical = models.ForeignKey(SubVericalFilter, on_delete=models.CASCADE, related_name='course_sub_vertical_filters') + sub_vertical = models.ForeignKey( + SubVericalFilter, + on_delete=models.CASCADE, + related_name="course_sub_vertical_filters", + ) def __str__(self): return self.course.title @@ -46,16 +50,3 @@ class Meta: verbose_name_plural = 'Course Vertical Filters' ordering = ['course__title'] unique_together = ['course'] - -class ProgramVerticalFilters(TimeStampedModel): - program = models.OneToOneField(Program, on_delete=models.CASCADE, related_name='vertical_filters') - vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='program_vertical_filters') - sub_vertical = models.ForeignKey(SubVericalFilter, on_delete=models.CASCADE, related_name='program_sub_vertical_filters') - - def __str__(self): - return self.program.title - - class Meta: - verbose_name_plural = 'Program Vertical Filters' - ordering = ['program__title'] - unique_together = ['program'] From 5341d396af2e79267c0107423ed6268ea6bd5c3d Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 01:03:16 +0500 Subject: [PATCH 08/15] chore: added docstrings to admin method --- course_discovery/apps/tagging/admin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index 6506ae753b..ee5ac81e12 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -39,16 +39,25 @@ class CourseVerticalFiltersAdmin(admin.ModelAdmin): ordering = ('course__title',) def formfield_for_foreignkey(self, db_field, request, **kwargs): + """ + Override the formfield_for_foreignkey method to filter the course field based on draft status. + """ if db_field.name == 'course': kwargs['queryset'] = Course.objects.filter(draft=False) return super().formfield_for_foreignkey(db_field, request, **kwargs) def changelist_view(self, request, extra_context=None): + """ + Override the changelist_view method to add a custom upload CSV button. + """ extra_context = extra_context or {} extra_context['upload_csv_url'] = reverse('admin:courseverticalfilters_upload_csv') return super().changelist_view(request, extra_context=extra_context) def get_urls(self): + """ + Override the get_urls method to add a custom upload CSV URL. + """ from django.urls import path urls = super().get_urls() custom_urls = [ @@ -57,6 +66,9 @@ def get_urls(self): return custom_urls + urls def upload_csv(self, request): + """ + Custom view to upload a CSV file and process it to update the course vertical filters to support bulk updates. + """ if request.method == 'POST' and request.FILES.get('csv_file'): form = CSVUploadForm(request.POST, request.FILES) if form.is_valid(): From 72dc98a70eca7b2ccff89208b1683e57e2350fbe Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 01:06:22 +0500 Subject: [PATCH 09/15] chore: added docstring to models --- course_discovery/apps/tagging/models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index 14f46bd0f5..f7249f2faf 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -6,6 +6,9 @@ class VerticalFilter(TimeStampedModel): + """ + This model is used to store the vertical mapping for the courses. + """ name = models.CharField(max_length=255, unique=True) slug = AutoSlugField(populate_from='name', max_length=255, unique=True) description = models.TextField(blank=True) @@ -20,6 +23,9 @@ class Meta: unique_together = ['name'] class SubVericalFilter(TimeStampedModel): + """ + This model is used to store the sub vertical mapping for the courses. + """ name = models.CharField(max_length=255, unique=True) slug = AutoSlugField(populate_from='name', max_length=255, unique=True) description = models.TextField(blank=True) @@ -35,6 +41,9 @@ class Meta: unique_together = ['name'] class CourseVerticalFilters(TimeStampedModel): + """ + Model used to assign vertical and sub vertical filters to course. + """ course = models.OneToOneField(Course, on_delete=models.CASCADE, related_name='vertical_filters') vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='course_vertical_filters') sub_vertical = models.ForeignKey( From c2526f063a1ea9830ee38c439f6e39b92fb2e030 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 01:08:13 +0500 Subject: [PATCH 10/15] chore: replace uuid wit key in csv --- course_discovery/apps/tagging/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index ee5ac81e12..6c9957052b 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -78,11 +78,11 @@ def upload_csv(self, request): reader = csv.reader(decoded_file) next(reader) # Skip the header row for row in reader: - course_uuid = row[0] + course_key = row[0] vertical_name = row[1] sub_vertical_name = row[2] - course = Course.objects.get(uuid=course_uuid) + course = Course.objects.get(key=course_key) vertical, _ = VerticalFilter.objects.get_or_create(name=vertical_name) sub_vertical, _ = SubVericalFilter.objects.get_or_create( name=sub_vertical_name, vertical_filters=vertical From 5eb9e5458c7cbd4e4b2c1beef545c72cd3c7a2fb Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 01:18:08 +0500 Subject: [PATCH 11/15] fix: templates and pylint quality --- course_discovery/apps/tagging/admin.py | 1 + course_discovery/apps/tagging/models.py | 2 ++ .../admin/tagging/verticalfilter/change_list.html | 13 ------------- .../tagging/verticalfilter/upload_csv_form.html | 11 ----------- 4 files changed, 3 insertions(+), 24 deletions(-) delete mode 100644 course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html delete mode 100644 course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index 6c9957052b..a6a535709b 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -17,6 +17,7 @@ class VerticalFilterAdmin(admin.ModelAdmin): list_display = ('name', 'is_active', 'description', 'slug',) search_fields = ('name',) + @admin.register(SubVericalFilter) class SubVericalFilterAdmin(admin.ModelAdmin): """ diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index f7249f2faf..12cbe40f7e 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -22,6 +22,7 @@ class Meta: ordering = ['name'] unique_together = ['name'] + class SubVericalFilter(TimeStampedModel): """ This model is used to store the sub vertical mapping for the courses. @@ -40,6 +41,7 @@ class Meta: ordering = ['name'] unique_together = ['name'] + class CourseVerticalFilters(TimeStampedModel): """ Model used to assign vertical and sub vertical filters to course. diff --git a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html deleted file mode 100644 index e418f5ba05..0000000000 --- a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/change_list.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "admin/change_list.html" %} - -{% block object-tools %} - -{{ block.super }} - -{% endblock %} diff --git a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html b/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html deleted file mode 100644 index 71f3223b09..0000000000 --- a/course_discovery/apps/tagging/templates/admin/tagging/verticalfilter/upload_csv_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "admin/base_site.html" %} - -{% block content %} -

Upload CSV for Vertical Filters

-
- {% csrf_token %} - - - -
-{% endblock %} From dee12ab2bc16f9e320038971171a45f7afcb244e Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 13:11:13 +0500 Subject: [PATCH 12/15] chore: added model for Program Vertical filter --- course_discovery/apps/tagging/models.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index 12cbe40f7e..ab326c914d 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -55,9 +55,29 @@ class CourseVerticalFilters(TimeStampedModel): ) def __str__(self): - return self.course.title + return f'{self.course.title} - {self.vertical.name} - {self.sub_vertical.name}' class Meta: verbose_name_plural = 'Course Vertical Filters' ordering = ['course__title'] unique_together = ['course'] + +class ProgramVericalFilters(TimeStampedModel): + """ + Model used to assign verticals to program. + """ + program = models.ForeignKey(Program, on_delete=models.CASCADE, related_name='program_verticals') + vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='program_vertical_filters') + sub_vertical = models.ForeignKey( + SubVericalFilter, + on_delete=models.CASCADE, + related_name='program_sub_vertical_filters', + ) + + def __str__(self): + return f'{self.program.title} - {self.vertical.name} - {self.sub_vertical.name}' + + class Meta: + verbose_name_plural = 'Program Verticals' + ordering = ['program__title'] + unique_together = ['program', 'vertical'] From 42191abccf59aaf7cf8cccc74e71b25d2bcead5e Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 14:07:15 +0500 Subject: [PATCH 13/15] fix: migration --- .../apps/tagging/migrations/0001_initial.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/course_discovery/apps/tagging/migrations/0001_initial.py b/course_discovery/apps/tagging/migrations/0001_initial.py index 45bf1a9bfa..5393f44368 100644 --- a/course_discovery/apps/tagging/migrations/0001_initial.py +++ b/course_discovery/apps/tagging/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.14 on 2024-12-23 19:59 +# Generated by Django 4.2.14 on 2024-12-24 09:06 from django.db import migrations, models import django.db.models.deletion @@ -51,6 +51,22 @@ class Migration(migrations.Migration): 'unique_together': {('name',)}, }, ), + migrations.CreateModel( + name='ProgramVericalFilters', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_verticals', to='course_metadata.program')), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name_plural': 'Program Verticals', + 'ordering': ['program__title'], + 'unique_together': {('program', 'vertical')}, + }, + ), migrations.CreateModel( name='CourseVerticalFilters', fields=[ From 34e2c3f4b45387420559e0951b7fffa9f9bfa081 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 17:12:43 +0500 Subject: [PATCH 14/15] feat: add new model to combine the purpose of Course and Programs Vertical filters --- course_discovery/apps/tagging/admin.py | 21 ++++++++- .../migrations/0002_verticalfiltertags.py | 32 ++++++++++++++ course_discovery/apps/tagging/models.py | 44 ++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index a6a535709b..025baaa528 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -6,7 +6,7 @@ from course_discovery.apps.course_metadata.models import Course from course_discovery.apps.tagging.forms import CSVUploadForm -from course_discovery.apps.tagging.models import CourseVerticalFilters, SubVericalFilter, VerticalFilter +from course_discovery.apps.tagging.models import CourseVerticalFilters, SubVericalFilter, VerticalFilter, ProgramVericalFilters, VerticalFilterTags @admin.register(VerticalFilter) @@ -106,3 +106,22 @@ def upload_csv(self, request): 'admin/tagging/courseverticalfilters/upload_csv_form.html', context={'form': form} ) + +@admin.register(ProgramVericalFilters) +class ProgramVericalFiltersAdmin(admin.ModelAdmin): + """ + Admin class for Program Vertical Filters model. + """ + list_display = ('program', 'vertical', 'sub_vertical') + list_filter = ('vertical', 'sub_vertical') + search_fields = ('program__title', 'vertical__name', 'sub_vertical__name') + ordering = ('program__title',) + +@admin.register(VerticalFilterTags) +class VerticalFilterTagsAdmin(admin.ModelAdmin): + """ + Admin class for VerticalFilterTags model. + """ + list_display = ('content_type', 'object_id', 'vertical', 'sub_vertical') + list_filter = ('vertical', 'sub_vertical', 'content_type') + search_fields = ('object_id', ) diff --git a/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py b/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py new file mode 100644 index 0000000000..7a376ef603 --- /dev/null +++ b/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.14 on 2024-12-24 11:36 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('tagging', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='VerticalFilterTags', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('content_type', models.CharField(choices=[('course', 'Course'), ('program', 'Program'), ('degree', 'Degree')], max_length=10)), + ('object_id', models.UUIDField()), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'ordering': ['content_type', 'object_id'], + 'unique_together': {('content_type', 'object_id', 'vertical')}, + }, + ), + ] diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index ab326c914d..1241a67282 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -2,7 +2,7 @@ from django_extensions.db.fields import AutoSlugField from model_utils.models import TimeStampedModel -from course_discovery.apps.course_metadata.models import Course, Program +from course_discovery.apps.course_metadata.models import Course, Program, Degree class VerticalFilter(TimeStampedModel): @@ -62,6 +62,7 @@ class Meta: ordering = ['course__title'] unique_together = ['course'] + class ProgramVericalFilters(TimeStampedModel): """ Model used to assign verticals to program. @@ -81,3 +82,44 @@ class Meta: verbose_name_plural = 'Program Verticals' ordering = ['program__title'] unique_together = ['program', 'vertical'] + +class VerticalFilterTags(TimeStampedModel): + """ + Model used to assign vertical and sub-vertical filters to Course, Program, or Degree. + """ + CONTENT_TYPE_CHOICES = [ + ('course', 'Course'), + ('program', 'Program'), + ('degree', 'Degree'), + ] + + content_type = models.CharField(max_length=10, choices=CONTENT_TYPE_CHOICES) + object_id = models.UUIDField() + + vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='vertical_filters') + sub_vertical = models.ForeignKey( + SubVericalFilter, + on_delete=models.CASCADE, + related_name='sub_vertical_filters', + ) + + def __str__(self): + return f'{self.get_object_title()} - {self.vertical.name} - {self.sub_vertical.name}' + + def get_object_title(self): + """ + Retrieve the title of the related object based on its type. + """ + if self.content_type == 'course': + return Course.objects.filter(uuid=self.object_id).first().title + elif self.content_type == 'program': + return Program.objects.filter(uuid=self.object_id).first().title + elif self.content_type == 'degree': + return Degree.objects.filter(uuid=self.object_id).first().title + else: + return None + + class Meta: + verbose_name_plural = 'Vertical Filter Tags' + ordering = ['content_type', 'object_id'] + unique_together = ['content_type', 'object_id', 'vertical'] From 2d1a38045874dcaa0a2902bd6e90c539116bf763 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib Date: Tue, 24 Dec 2024 17:23:20 +0500 Subject: [PATCH 15/15] feat: update implementation --- course_discovery/apps/tagging/admin.py | 4 +- .../apps/tagging/migrations/0001_initial.py | 41 ++++++++---- .../migrations/0002_verticalfiltertags.py | 32 --------- course_discovery/apps/tagging/models.py | 66 ++++++++++--------- 4 files changed, 68 insertions(+), 75 deletions(-) delete mode 100644 course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index 025baaa528..90b111bede 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -6,7 +6,7 @@ from course_discovery.apps.course_metadata.models import Course from course_discovery.apps.tagging.forms import CSVUploadForm -from course_discovery.apps.tagging.models import CourseVerticalFilters, SubVericalFilter, VerticalFilter, ProgramVericalFilters, VerticalFilterTags +from course_discovery.apps.tagging.models import CourseVerticalFilters, SubVericalFilter, VerticalFilter, ProgramVerticalFilters, VerticalFilterTags @admin.register(VerticalFilter) @@ -107,7 +107,7 @@ def upload_csv(self, request): context={'form': form} ) -@admin.register(ProgramVericalFilters) +@admin.register(ProgramVerticalFilters) class ProgramVericalFiltersAdmin(admin.ModelAdmin): """ Admin class for Program Vertical Filters model. diff --git a/course_discovery/apps/tagging/migrations/0001_initial.py b/course_discovery/apps/tagging/migrations/0001_initial.py index 5393f44368..617a7d21fe 100644 --- a/course_discovery/apps/tagging/migrations/0001_initial.py +++ b/course_discovery/apps/tagging/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.14 on 2024-12-24 09:06 +# Generated by Django 4.2.14 on 2024-12-24 12:22 from django.db import migrations, models import django.db.models.deletion @@ -52,19 +52,37 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='ProgramVericalFilters', + name='VerticalFilterTags', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_verticals', to='course_metadata.program')), - ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_sub_vertical_filters', to='tagging.subvericalfilter')), - ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_vertical_filters', to='tagging.verticalfilter')), + ('content_type', models.CharField(choices=[('course', 'Course'), ('program', 'Program'), ('degree', 'Degree')], max_length=10)), + ('object_id', models.UUIDField()), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='tagging.verticalfilter')), ], options={ - 'verbose_name_plural': 'Program Verticals', - 'ordering': ['program__title'], - 'unique_together': {('program', 'vertical')}, + 'verbose_name_plural': 'Vertical Filter Tags', + 'ordering': ['content_type', 'object_id'], + 'unique_together': {('content_type', 'object_id', 'vertical')}, + }, + ), + migrations.CreateModel( + name='ProgramVerticalFilters', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_vertical_filters', to='course_metadata.program')), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_vertical_filters', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name_plural': 'Program Vertical Filters', + 'ordering': ['vertical', 'sub_vertical'], + 'abstract': False, + 'unique_together': {('program',)}, }, ), migrations.CreateModel( @@ -74,12 +92,13 @@ class Migration(migrations.Migration): ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('course', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='course_metadata.course')), - ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_sub_vertical_filters', to='tagging.subvericalfilter')), - ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_vertical_filters', to='tagging.verticalfilter')), + ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_sub_vertical_filters', to='tagging.subvericalfilter')), + ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_vertical_filters', to='tagging.verticalfilter')), ], options={ 'verbose_name_plural': 'Course Vertical Filters', - 'ordering': ['course__title'], + 'ordering': ['vertical', 'sub_vertical'], + 'abstract': False, 'unique_together': {('course',)}, }, ), diff --git a/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py b/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py deleted file mode 100644 index 7a376ef603..0000000000 --- a/course_discovery/apps/tagging/migrations/0002_verticalfiltertags.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 4.2.14 on 2024-12-24 11:36 - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('tagging', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='VerticalFilterTags', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('content_type', models.CharField(choices=[('course', 'Course'), ('program', 'Program'), ('degree', 'Degree')], max_length=10)), - ('object_id', models.UUIDField()), - ('sub_vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_vertical_filters', to='tagging.subvericalfilter')), - ('vertical', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='vertical_filters', to='tagging.verticalfilter')), - ], - options={ - 'ordering': ['content_type', 'object_id'], - 'unique_together': {('content_type', 'object_id', 'vertical')}, - }, - ), - ] diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index 1241a67282..35dc89207f 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -4,7 +4,6 @@ from course_discovery.apps.course_metadata.models import Course, Program, Degree - class VerticalFilter(TimeStampedModel): """ This model is used to store the vertical mapping for the courses. @@ -41,47 +40,54 @@ class Meta: ordering = ['name'] unique_together = ['name'] - -class CourseVerticalFilters(TimeStampedModel): +class BaseVerticalFilter(TimeStampedModel): """ - Model used to assign vertical and sub vertical filters to course. + Abstract base model for vertical and sub-vertical filters with timestamps. """ - course = models.OneToOneField(Course, on_delete=models.CASCADE, related_name='vertical_filters') - vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='course_vertical_filters') + vertical = models.ForeignKey( + VerticalFilter, on_delete=models.CASCADE, related_name="%(class)s_vertical_filters" + ) sub_vertical = models.ForeignKey( - SubVericalFilter, - on_delete=models.CASCADE, - related_name="course_sub_vertical_filters", + SubVericalFilter, on_delete=models.CASCADE, related_name="%(class)s_sub_vertical_filters" ) + class Meta: + abstract = True + ordering = ["vertical", "sub_vertical"] + def __str__(self): - return f'{self.course.title} - {self.vertical.name} - {self.sub_vertical.name}' + return f'{self.get_object_title()} - {self.vertical.name} - {self.sub_vertical.name}' - class Meta: - verbose_name_plural = 'Course Vertical Filters' - ordering = ['course__title'] - unique_together = ['course'] + def get_object_title(self): + """ + Should be implemented in the derived models to return the object's title. + """ + raise NotImplementedError("Subclasses must implement `get_object_title`.") +class CourseVerticalFilters(BaseVerticalFilter): + course = models.OneToOneField( + Course, on_delete=models.CASCADE, related_name="vertical_filters" + ) -class ProgramVericalFilters(TimeStampedModel): - """ - Model used to assign verticals to program. - """ - program = models.ForeignKey(Program, on_delete=models.CASCADE, related_name='program_verticals') - vertical = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='program_vertical_filters') - sub_vertical = models.ForeignKey( - SubVericalFilter, - on_delete=models.CASCADE, - related_name='program_sub_vertical_filters', + class Meta(BaseVerticalFilter.Meta): + verbose_name_plural = "Course Vertical Filters" + unique_together = ["course"] + + def get_object_title(self): + return self.course.title + + +class ProgramVerticalFilters(BaseVerticalFilter): + program = models.ForeignKey( + Program, on_delete=models.CASCADE, related_name="program_vertical_filters" ) - def __str__(self): - return f'{self.program.title} - {self.vertical.name} - {self.sub_vertical.name}' + class Meta(BaseVerticalFilter.Meta): + verbose_name_plural = "Program Vertical Filters" + unique_together = ["program"] - class Meta: - verbose_name_plural = 'Program Verticals' - ordering = ['program__title'] - unique_together = ['program', 'vertical'] + def get_object_title(self): + return self.program.title class VerticalFilterTags(TimeStampedModel): """