diff --git a/course_discovery/apps/tagging/admin.py b/course_discovery/apps/tagging/admin.py index e69de29bb2..ca7f42f57b 100644 --- a/course_discovery/apps/tagging/admin.py +++ b/course_discovery/apps/tagging/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin + +from course_discovery.apps.tagging.models import VerticalFilter, SubVerticalFilter, CourseVerticalFilter + + +@admin.register(VerticalFilter) +class VerticalFilterAdmin(admin.ModelAdmin): + """ + Admin class for VerticalFilter model. + """ + list_display = ('name', 'is_active', 'slug',) + search_fields = ('name',) + + +@admin.register(SubVerticalFilter) +class SubVericalFilterAdmin(admin.ModelAdmin): + """ + Admin class for SubVerticalFilter model. + """ + list_display = ('name', 'is_active', 'slug', 'vertical_filters') + list_filter = ('vertical_filters', ) + search_fields = ('name',) + ordering = ('name',) + + +@admin.register(CourseVerticalFilter) +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') + ordering = ('course__title',) 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..b8e9853563 --- /dev/null +++ b/course_discovery/apps/tagging/migrations/0001_initial.py @@ -0,0 +1,119 @@ +# Generated by Django 4.2.14 on 2025-01-15 14:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields +import model_utils.fields +import simple_history.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('course_metadata', '0346_archivecoursesconfig'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + 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)), + ('is_active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name_plural': 'Vertical Filters', + 'ordering': ['name'], + 'unique_together': {('name',)}, + }, + ), + migrations.CreateModel( + name='SubVerticalFilter', + 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)), + ('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='HistoricalVerticalFilter', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, 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(db_index=True, max_length=255)), + ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name')), + ('is_active', models.BooleanField(default=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical vertical filter', + 'verbose_name_plural': 'historical Vertical Filters', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='HistoricalSubVerticalFilter', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, 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(db_index=True, max_length=255)), + ('slug', django_extensions.db.fields.AutoSlugField(blank=True, editable=False, max_length=255, populate_from='name')), + ('is_active', models.BooleanField(default=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('vertical_filters', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='tagging.verticalfilter')), + ], + options={ + 'verbose_name': 'historical sub vertical filter', + 'verbose_name_plural': 'historical Sub Vertical Filters', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='CourseVerticalFilter', + 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='%(class)s_sub_vertical_filters', to='tagging.subverticalfilter')), + ('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': ['vertical', 'sub_vertical'], + 'abstract': False, + 'unique_together': {('course',)}, + }, + ), + ] diff --git a/course_discovery/apps/tagging/models.py b/course_discovery/apps/tagging/models.py index e69de29bb2..937db7031e 100644 --- a/course_discovery/apps/tagging/models.py +++ b/course_discovery/apps/tagging/models.py @@ -0,0 +1,78 @@ +from django.db import models +from django_extensions.db.fields import AutoSlugField +from simple_history.models import HistoricalRecords +from model_utils.models import TimeStampedModel + +from course_discovery.apps.course_metadata.models import Course, ManageHistoryMixin + +class VerticalFilter(ManageHistoryMixin, TimeStampedModel): + """ + Model for defining vertical filters used to categorize product types under specific verticals. + """ + name = models.CharField(max_length=255, unique=True) + slug = AutoSlugField(populate_from='name', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + history = HistoricalRecords() + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = 'Vertical Filters' + ordering = ['name'] + unique_together = ['name'] + +class SubVerticalFilter(ManageHistoryMixin, TimeStampedModel): + """ + Model for defining sub vertical filters used to categorize product types under specific verticals. + """ + name = models.CharField(max_length=255, unique=True) + slug = AutoSlugField(populate_from='name', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + vertical_filters = models.ForeignKey(VerticalFilter, on_delete=models.CASCADE, related_name='sub_vertical_filters') + history = HistoricalRecords() + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = 'Sub Vertical Filters' + ordering = ['name'] + unique_together = ['name'] + +class BaseVerticalFilter(ManageHistoryMixin, TimeStampedModel): + """ + Abstract base model for assigning vertical and sub vertical filters to product types. + """ + vertical = models.ForeignKey( + VerticalFilter, on_delete=models.CASCADE, related_name="%(class)s_vertical_filters" + ) + sub_vertical = models.ForeignKey( + SubVerticalFilter, on_delete=models.CASCADE, related_name="%(class)s_sub_vertical_filters" + ) + history = HistoricalRecords() + + class Meta: + abstract = True + ordering = ["vertical", "sub_vertical"] + + def __str__(self): + return f'{self.get_object_title()} - {self.vertical.name} - {self.sub_vertical.name}' + + def get_object_title(self): + """ + Returns a string representing the title of the object. + """ + raise NotImplementedError("Subclasses must implement `get_object_title`.") + +class CourseVerticalFilter(BaseVerticalFilter): + course = models.OneToOneField( + Course, on_delete=models.CASCADE, related_name="vertical_filters" + ) + + class Meta(BaseVerticalFilter.Meta): + verbose_name_plural = "Course Vertical Filters" + unique_together = ["course"] + + def get_object_title(self): + return self.course.title