Skip to content

Commit

Permalink
feat: add django templates for tagging app UI (#4547)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ali-D-Akbar authored Jan 29, 2025
1 parent 63a1ecd commit 245fa13
Show file tree
Hide file tree
Showing 23 changed files with 929 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ def test_success(self, has_existing_verticals):

self.course1.refresh_from_db()
self.course2.refresh_from_db()
assert self.course1.vertical.vertical == self.ai_vertical
assert self.course1.vertical.sub_vertical == self.python_subvertical
assert self.course2.vertical.vertical == self.literature_vertical
assert self.course2.vertical.sub_vertical == self.kafka_subvertical
assert self.course1.product_vertical.vertical == self.ai_vertical
assert self.course1.product_vertical.sub_vertical == self.python_subvertical
assert self.course2.product_vertical.vertical == self.literature_vertical
assert self.course2.product_vertical.sub_vertical == self.kafka_subvertical
assert CourseVertical.objects.count() == 2
assert Vertical.objects.count() == 2
assert SubVertical.objects.count() == 2
Expand All @@ -103,8 +103,8 @@ def test_empty_subvertical(self):
UpdateCourseVerticalsConfigFactory(enabled=True, csv_file=csv)
call_command('update_course_verticals')

assert self.course1.vertical.vertical == self.ai_vertical
assert self.course1.vertical.sub_vertical is None
assert self.course1.product_vertical.vertical == self.ai_vertical
assert self.course1.product_vertical.sub_vertical is None
assert not hasattr(self.course2, 'vertical')
assert CourseVertical.objects.count() == 1
self.assert_email_content(success_count=1, failure_count=0)
Expand All @@ -116,8 +116,8 @@ def test_nonexistent_vertical(self):
call_command('update_course_verticals')

assert not hasattr(self.course1, 'vertical')
assert self.course2.vertical.vertical == self.literature_vertical
assert self.course2.vertical.sub_vertical == self.kafka_subvertical
assert self.course2.product_vertical.vertical == self.literature_vertical
assert self.course2.product_vertical.sub_vertical == self.kafka_subvertical
assert CourseVertical.objects.count() == 1
self.assert_email_content(
success_count=1, failure_count=1, failure_reasons={f"{self.course1.key}": "ValueError"}
Expand All @@ -131,8 +131,8 @@ def test_inactive_vertical(self):
UpdateCourseVerticalsConfigFactory(enabled=True, csv_file=csv)
call_command('update_course_verticals')

assert self.course1.vertical.vertical == self.ai_vertical
assert self.course1.vertical.sub_vertical == self.python_subvertical
assert self.course1.product_vertical.vertical == self.ai_vertical
assert self.course1.product_vertical.sub_vertical == self.python_subvertical
assert not hasattr(self.course2, 'vertical')
assert CourseVertical.objects.count() == 1
self.assert_email_content(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.11 on 2025-01-28 09:29

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


class Migration(migrations.Migration):

dependencies = [
('tagging', '0002_updatecourseverticalsconfig'),
]

operations = [
migrations.AlterField(
model_name='coursevertical',
name='course',
field=models.OneToOneField(limit_choices_to={'draft': False}, on_delete=django.db.models.deletion.CASCADE, related_name='product_vertical', to='course_metadata.course'),
),
]
23 changes: 23 additions & 0 deletions course_discovery/apps/tagging/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied


class VerticalTaggingAdministratorPermissionRequiredMixin(LoginRequiredMixin):
"""
A mixin to enforce permission on VERTICALS_MANAGEMENT_GROUPS for class-based views.
"""

def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if response.status_code == 403:
return response

in_vertical_management_group = request.user.groups.filter(
name__in=settings.VERTICALS_MANAGEMENT_GROUPS
).exists()

if not request.user.is_superuser and not in_vertical_management_group:
raise PermissionDenied("You do not have permission to access this page.")

return response
2 changes: 1 addition & 1 deletion course_discovery/apps/tagging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class CourseVertical(ProductVertical):
Model for assigning vertical and sub verticals to courses
"""
course = models.OneToOneField(
Course, on_delete=models.CASCADE, related_name="vertical", limit_choices_to={'draft': False}
Course, on_delete=models.CASCADE, related_name="product_vertical", limit_choices_to={'draft': False}
)

def clean(self):
Expand Down
109 changes: 109 additions & 0 deletions course_discovery/apps/tagging/templates/partials/course_table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>#</th>
<th>
<a href="?sort=key&direction={% if current_sort == 'key' and current_direction == 'asc' %}desc{% else %}asc{% endif %}">
Course Key
{% if current_sort == 'key' %}
<span>{% if current_direction == 'asc' %}▲{% else %}▼{% endif %}</span>
{% endif %}
</a>
</th>
<th>
<a href="?sort=title&direction={% if current_sort == 'title' and current_direction == 'asc' %}desc{% else %}asc{% endif %}">
Course Title
{% if current_sort == 'title' %}
<span>{% if current_direction == 'asc' %}▲{% else %}▼{% endif %}</span>
{% endif %}
</a>
</th>
<th>
<a href="?sort=vertical&direction={% if current_sort == 'vertical' and current_direction == 'asc' %}desc{% else %}asc{% endif %}">
Vertical
{% if current_sort == 'vertical' %}
<span>{% if current_direction == 'asc' %}▲{% else %}▼{% endif %}</span>
{% endif %}
</a>
</th>
<th>
<a href="?sort=sub_vertical&direction={% if current_sort == 'sub_vertical' and current_direction == 'asc' %}desc{% else %}asc{% endif %}">
Sub-Vertical
{% if current_sort == 'sub_vertical' %}
<span>{% if current_direction == 'asc' %}▲{% else %}▼{% endif %}</span>
{% endif %}
</a>
</th>
</tr>
</thead>
<tbody>
{% for course in courses %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ course.key }}</td>
<td>
<a href="{% url 'tagging:course_tagging_detail' uuid=course.uuid %}">
{{ course.title }}
</a>
</td>
<td>
{% if course.product_vertical and course.product_vertical.vertical %}
<a href="{% url 'tagging:vertical_detail' slug=course.product_vertical.vertical.slug %}">
{{ course.product_vertical.vertical.name }}
</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
<td>
{% if course.product_vertical and course.product_vertical.sub_vertical %}
<a href="{% url 'tagging:sub_vertical_detail' slug=course.product_vertical.sub_vertical.slug %}">
{{ course.product_vertical.sub_vertical.name }}
</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center text-muted">No courses found.</td>
</tr>
{% endfor %}
</tbody>
</table>

{% if is_paginated %}
<nav>
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-get="?page={{ page_obj.previous_page_number }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-target="#course-table">
&laquo;
</a>
</li>
{% endif %}
{% for page_num in paginator.page_range %}
<li class="page-item {% if page_obj.number == page_num %}active{% endif %}">
<a class="page-link" href="?page={{ page_num }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-get="?page={{ page_num }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-target="#course-table">
{{ page_num }}
</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-get="?page={{ page_obj.next_page_number }}&search={{ request.GET.search|default:'' }}&sort={{ current_sort }}&direction={{ current_direction }}"
hx-target="#course-table">
&raquo;
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

6 changes: 6 additions & 0 deletions course_discovery/apps/tagging/templates/partials/message.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<option value="">-- Select Sub-Vertical --</option>
{% for sub_vertical in sub_verticals %}
<option value={{ sub_vertical.slug }} >{{ sub_vertical.name }}</option>
{% endfor %}
41 changes: 41 additions & 0 deletions course_discovery/apps/tagging/templates/tagging/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Course Tagging{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<script src="https://unpkg.com/htmx.org"></script>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'tagging:course_list' %}">Course Tagging</a>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'tagging:vertical_list' %}">Verticals</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'tagging:sub_vertical_list' %}">Sub-Verticals</a>
</li>
</ul>
<ul class="navbar-nav mr-10">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-light" href="#">{{ user.username }}</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>

<main class="py-4">
{% block content %}
{% endblock %}
</main>
</body>
</html>
18 changes: 18 additions & 0 deletions course_discovery/apps/tagging/templates/tagging/course_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% extends "tagging/base.html" %}

{% block content %}
<div class="container mt-5">
<h1 class="mb-4">Courses</h1>

<form method="get" class="mb-4" hx-get="{% url 'tagging:course_list' %}" hx-target="#course-table" hx-trigger="keyup changed delay:500ms from:search">
<div class="input-group">
<input type="text" name="search" id="search" class="form-control" placeholder="Search courses..." value="{{ request.GET.search|default:'' }}">
<button class="btn btn-primary" type="submit">Search</button>
</div>
</form>

<div id="course-table">
{% include "partials/course_table.html" %}
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{% extends "tagging/base.html" %}

{% block content %}
<div class="container mt-5">
<h1 class="mb-4">Course: {{ course.title }}</h1>
<h2>Key: {{ course.key }}</h2>

<h3>Assign or Edit Vertical and Sub-Vertical</h3>
<form method="post" action=""
hx-post=""
hx-target="#message-container"
hx-swap="innerHTML">
{% csrf_token %}

<div class="form-group">
<label for="vertical">Vertical</label>
<select name="vertical" id="vertical" class="form-control"
hx-trigger="change"
hx-on="change: filterSubVerticals(event)">
<option value="">Select Vertical</option>
{% for vertical in verticals %}
<option value="{{ vertical.slug }}"
{% if course.product_vertical and course.product_vertical.vertical.slug == vertical.slug %}selected{% endif %}>
{{ vertical.name }}
</option>
{% endfor %}
</select>
</div>

<div class="form-group">
<label for="sub_vertical">Sub-Vertical</label>
<select name="sub_vertical" id="sub_vertical" class="form-control">
<option value="">Select Sub-Vertical</option>
{% for sub_vertical in all_sub_verticals %}
<option value="{{ sub_vertical.slug }}"
data-vertical="{{ sub_vertical.vertical.slug }}"
{% if course.product_vertical and course.product_vertical.sub_vertical and course.product_vertical.sub_vertical.slug == sub_vertical.slug %}
selected
{% elif course.product_vertical and not course.product_vertical.sub_vertical and sub_vertical.vertical.slug != course.product_vertical.vertical.slug %}
style="display: none;"
{% elif not course.product_vertical or sub_vertical.vertical.slug != course.product_vertical.vertical.slug %}
style="display: none;"
{% endif %}>
{{ sub_vertical.name }}
</option>
{% endfor %}
</select>
</div>

<button type="submit" class="btn btn-primary">Save</button>
</form>

<!-- Message container for success/error -->
<div id="message-container" class="mt-3"></div>
</div>

<script>
function filterSubVerticals(event) {
const selectedVertical = event.target.value;
const subVerticalSelect = document.getElementById('sub_vertical');
const options = subVerticalSelect.querySelectorAll('option[data-vertical]');

// Clear sub-vertical selection only when vertical is changed to no selection
if (!selectedVertical) {
subVerticalSelect.value = ""; // Reset sub-vertical selection
}

// Hide or show sub-vertical options based on selected vertical
options.forEach(option => {
if (selectedVertical && option.getAttribute('data-vertical') === selectedVertical) {
option.style.display = ""; // Show relevant sub-vertical
} else {
option.style.display = "none"; // Hide irrelevant sub-vertical
}
});

// Automatically clear sub-vertical selection if no matching options are visible
const selectedSubVertical = subVerticalSelect.value;
const matchingOption = Array.from(options).find(option => option.value === selectedSubVertical && option.style.display !== "none");
if (!matchingOption) {
subVerticalSelect.value = ""; // Clear selection if no valid options remain
}
}

document.addEventListener("DOMContentLoaded", function () {
const selectedVertical = document.getElementById('vertical');
filterSubVerticals({ target: selectedVertical });
});
</script>
{% endblock %}
Loading

0 comments on commit 245fa13

Please sign in to comment.