Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add django templates for tagging app UI #4547

Merged
merged 1 commit into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Check warning on line 14 in course_discovery/apps/tagging/mixins.py

View check run for this annotation

Codecov / codecov/patch

course_discovery/apps/tagging/mixins.py#L14

Added line #L14 was not covered by tests

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>
Ali-D-Akbar marked this conversation as resolved.
Show resolved Hide resolved
</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
Loading