Skip to content

Commit

Permalink
feat: add command to bulk update course verticals
Browse files Browse the repository at this point in the history
  • Loading branch information
zawan-ila committed Jan 21, 2025
1 parent 8ac5a04 commit 8c5680c
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 1 deletion.
10 changes: 9 additions & 1 deletion course_discovery/apps/tagging/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.core.exceptions import PermissionDenied
from simple_history.admin import SimpleHistoryAdmin

from course_discovery.apps.tagging.models import CourseVertical, SubVertical, Vertical
from course_discovery.apps.tagging.models import CourseVertical, SubVertical, UpdateCourseVerticalsConfig, Vertical


class SubVerticalInline(admin.TabularInline):
Expand Down Expand Up @@ -91,3 +91,11 @@ def save_model(self, request, obj, form, change):
if not (request.user.is_superuser or request.user.groups.filter(name__in=allowed_groups).exists()):
raise PermissionDenied("You are not authorized to perform this action.")
super().save_model(request, obj, form, change)


@admin.register(UpdateCourseVerticalsConfig)
class UpdateCourseVerticalsConfigurationAdmin(admin.ModelAdmin):
"""
Admin for ArchiveCoursesConfig model.
"""
list_display = ('id', 'enabled', 'changed_by', 'change_date')
27 changes: 27 additions & 0 deletions course_discovery/apps/tagging/emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.core.mail import EmailMessage
from django.template.loader import get_template

Check warning on line 2 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L1-L2

Added lines #L1 - L2 were not covered by tests

def send_email_for_course_verticals_update(report, to_users):

Check warning on line 4 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L4

Added line #L4 was not covered by tests
"""
Send an overall report of an update_course_verticals mgmt command run
"""
success_count = len(report['successes'])
failure_count = len(report['failures'])
context = {

Check warning on line 10 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L8-L10

Added lines #L8 - L10 were not covered by tests
'total_count': success_count + failure_count,
'failure_count': failure_count,
'success_count': success_count,
'failures': report['failures']
}
html_template = 'course_metadata/email/update_course_verticals.html'
template = get_template(html_template)
html_content = template.render(context)

Check warning on line 18 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L16-L18

Added lines #L16 - L18 were not covered by tests

email = EmailMessage(

Check warning on line 20 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L20

Added line #L20 was not covered by tests
"Update Course Verticals Command Summary",
html_content,
settings.PUBLISHER_FROM_EMAIL,
to_users,
)
email.content_subtype = "html"
email.send()

Check warning on line 27 in course_discovery/apps/tagging/emails.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/emails.py#L26-L27

Added lines #L26 - L27 were not covered by tests
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
Management command for updating course verticals and subverticals
Example usage:
$ ./manage.py update_course_verticals
"""
import logging
import unicodecsv
from django.conf import settings
from django.core.management import BaseCommand, CommandError

Check warning on line 11 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L8-L11

Added lines #L8 - L11 were not covered by tests

from course_discovery.apps.tagging.emails import send_email_for_course_verticals_update
from course_discovery.apps.course_metadata.models import Course
from course_discovery.apps.tagging.models import CourseVertical, SubVertical, UpdateCourseVerticalsConfig, Vertical

Check warning on line 15 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L13-L15

Added lines #L13 - L15 were not covered by tests

logger = logging.getLogger(__name__)

Check warning on line 17 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L17

Added line #L17 was not covered by tests


class Command(BaseCommand):
help = "Update course verticals and subverticals"

Check warning on line 21 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L20-L21

Added lines #L20 - L21 were not covered by tests

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.report = {

Check warning on line 25 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L23-L25

Added lines #L23 - L25 were not covered by tests
'failures': [],
'successes': [],
}

def handle(self, *args, **options):
reader = self.get_csv_reader()

Check warning on line 31 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L30-L31

Added lines #L30 - L31 were not covered by tests

for row in reader:
try:
course_key = row.get('course')
self.process_vertical_information(row)
except Exception as exc:
self.report['failures'].append(

Check warning on line 38 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L34-L38

Added lines #L34 - L38 were not covered by tests
{
'id': course_key,
'reason': repr(exc)
}
)
logger.exception(f"Failed to set vertical/subvertical information for course with key {course_key}")

Check warning on line 44 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L44

Added line #L44 was not covered by tests
else:
self.report['successes'].append(

Check warning on line 46 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L46

Added line #L46 was not covered by tests
{
'id': course_key,
}
)
logger.info(f"Successfully set vertical and subvertical info for course with key {course_key}")

Check warning on line 51 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L51

Added line #L51 was not covered by tests

send_email_for_course_verticals_update(self.report, settings.COURSE_VERTICALS_UPDATE_RECIPIENTS)

Check warning on line 53 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L53

Added line #L53 was not covered by tests

def process_vertical_information(self, row):
course_key, vertical_name, subvertical_name = row['course'], row['vertical'], row['subvertical']
course = Course.objects.get(key=course_key)
vertical = Vertical.objects.filter(name=vertical_name).first()
subvertical = SubVertical.objects.filter(name=subvertical_name).first()

Check warning on line 59 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L55-L59

Added lines #L55 - L59 were not covered by tests
if (not vertical and vertical_name) or (not subvertical and subvertical_name):
raise ValueError("Incorrect vertical or subvertical provided")

Check warning on line 61 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L61

Added line #L61 was not covered by tests

course_vertical = CourseVertical.objects.filter(course=course).first()

Check warning on line 63 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L63

Added line #L63 was not covered by tests
if course_vertical:
logger.info(f"Changing existing vertical association for course with key {course.key}")
course_vertical.vertical = vertical
course_vertical.subvertical = subvertical
course_vertical.save()

Check warning on line 68 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L65-L68

Added lines #L65 - L68 were not covered by tests
else:
CourseVertical.objects.create(course=course, vertical=vertical, subvertical=subvertical)

Check warning on line 70 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L70

Added line #L70 was not covered by tests

def get_csv_reader(self):
config = UpdateCourseVerticalsConfig.current()

Check warning on line 73 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L72-L73

Added lines #L72 - L73 were not covered by tests
if not config.enabled:
raise CommandError('Configuration object is not enabled')

Check warning on line 75 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L75

Added line #L75 was not covered by tests

if not config.csv_file:
raise CommandError('Configuration object does not have any input csv')

Check warning on line 78 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L78

Added line #L78 was not covered by tests

reader = unicodecsv.DictReader(config.csv_file)
return reader

Check warning on line 81 in course_discovery/apps/tagging/management/commands/update_course_verticals.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/management/commands/update_course_verticals.py#L80-L81

Added lines #L80 - L81 were not covered by tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.17 on 2025-01-21 12:05

from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tagging', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='UpdateCourseVerticalsConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('csv_file', models.FileField(help_text='A csv file containing the course keys, verticals and subverticals', upload_to='', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['csv'])])),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
]
14 changes: 14 additions & 0 deletions course_discovery/apps/tagging/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from config_models.models import ConfigurationModel
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django_extensions.db.fields import AutoSlugField
from model_utils.models import TimeStampedModel
Expand Down Expand Up @@ -111,3 +113,15 @@ def save(self, *args, **kwargs):

def get_object_title(self):
return self.course.title


class UpdateCourseVerticalsConfig(ConfigurationModel):
"""
Configuration to store a csv file for the update_course_verticals command
"""
# Timeout set to 0 so that the model does not read from cached config in case the config entry is deleted.
cache_timeout = 0
csv_file = models.FileField(
validators=[FileExtensionValidator(allowed_extensions=['csv'])],
help_text="A csv file containing the course keys, verticals and subverticals"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "course_metadata/email/email_base.html" %}
{% load django_markup %}
{% block body %}

<p>
The course verticals update process has completed. A summary is presented below.
</p>
<div>
<table border="2" width="50%" style="padding: 5px;">
<tr>
<th colspan="2">
Course Verticals Update Summary
</th>
</tr>
<tr>
<th>Total Data Rows</th>
<td>{{ total_count }}</td>
</tr>
<tr>
<th>Successfully Archived</th>
<td>{{ success_count }}</td>
</tr>
<tr>
<th>Failures</th>
<td>{{ failure_count }}</td>
</tr>
</table>
</div>


{% if failure_count > 0 %}
<div>
<h3>Verticals Update Failures</h3>
<ul>
{% for failure in failures %}
<li>[{{failure.id}}]: {{failure.reason}}</li>
{% endfor %}
</ul>
</div>
{% endif %}

{% endblock body %}
2 changes: 2 additions & 0 deletions course_discovery/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,3 +804,5 @@

# The list of user groups that have access to assign verticals and sub-verticals to courses
VERTICALS_MANAGEMENT_GROUPS = []

COURSE_VERTICALS_UPDATE_RECIPIENTS = ['[email protected]']

0 comments on commit 8c5680c

Please sign in to comment.