Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
virtualenvs-create: true
virtualenvs-in-project: true

- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5
with:
python-version: "3.12.5"
cache: "poetry"
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ repos:
- ".*/generated/"
additional_dependencies: ["gibberish-detector"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.6.2"
rev: "v0.6.3"
hooks:
- id: ruff-format
- id: ruff
Expand Down
15 changes: 15 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
Release Notes
=============

Version 0.18.2
--------------

- Use new OLL column for secondary topic; fix semester capitalization mismatch (#1519)
- Shanbady/fix course panel start date (#1521)
- Update dependency cryptography to v43 [SECURITY] (#1513)
- [pre-commit.ci] pre-commit autoupdate (#1504)
- Update dependency @ckeditor/ckeditor5-dev-utils to v42 (#1503)
- Update dependency @testing-library/react to v16.0.1 (#1496)
- Update dependency @ckeditor/ckeditor5-dev-translations to v42 (#1502)
- Update actions/setup-python digest to f677139 (#1493)
- Cache unit page counts (#1507)
- dfs_query_then_fetch (#1518)
- Add completeness discount to search (#1512)

Version 0.18.1 (Released September 05, 2024)
--------------

Expand Down
49 changes: 48 additions & 1 deletion channels/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.contrib.auth import get_user_model
from django.db import transaction
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.utils import extend_schema_field, inline_serializer
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

Expand Down Expand Up @@ -160,6 +160,53 @@ class Meta:
exclude = ["published"]


class ChannelCountsSerializer(serializers.ModelSerializer):
"""
Serializer for resource counts associated with Channel
"""

counts = serializers.SerializerMethodField(read_only=True)
channel_url = serializers.SerializerMethodField(read_only=True)

def get_channel_url(self, instance) -> str:
"""Get the URL for the channel"""
return instance.channel_url

@extend_schema_field(
inline_serializer(
name="Counts",
fields={
"courses": serializers.IntegerField(),
"programs": serializers.IntegerField(),
},
)
)
def get_counts(self, instance):
if instance.channel_type == "unit":
resources = instance.unit_detail.unit.learningresource_set.all()
elif instance.channel_type == "department":
resources = instance.department_detail.department.learningresource_set.all()
elif instance.channel_type == "topic":
resources = instance.topic_detail.topic.learningresource_set.all()
course_count = resources.filter(course__isnull=False, published=True).count()
program_count = resources.filter(program__isnull=False, published=True).count()
return {"courses": course_count, "programs": program_count}

class Meta:
model = Channel
exclude = [
"avatar",
"published",
"banner",
"about",
"configuration",
"public_description",
"ga_tracking_id",
"featured_list",
"widget_list",
]


class ChannelTopicDetailSerializer(serializers.ModelSerializer):
"""Serializer for the ChannelTopicDetail model"""

Expand Down
6 changes: 6 additions & 0 deletions channels/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from channels.views import (
ChannelByTypeNameDetailView,
ChannelCountsView,
ChannelModeratorDetailView,
ChannelModeratorListView,
ChannelViewSet,
Expand All @@ -19,6 +20,11 @@
ChannelByTypeNameDetailView.as_view({"get": "retrieve"}),
name="channel_by_type_name_api-detail",
),
re_path(
r"^channels/counts/(?P<channel_type>[A-Za-z0-9_\-]+)/$",
ChannelCountsView.as_view({"get": "list"}),
name="channel_counts_api-list",
),
re_path(
r"^channels/(?P<id>\d+)/moderators/$",
ChannelModeratorListView.as_view(),
Expand Down
33 changes: 33 additions & 0 deletions channels/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import logging

from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Prefetch
from django.utils.decorators import method_decorator
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import mixins, viewsets
Expand All @@ -17,6 +19,7 @@
from channels.models import Channel, ChannelGroupRole, ChannelList
from channels.permissions import ChannelModeratorPermissions, HasChannelPermission
from channels.serializers import (
ChannelCountsSerializer,
ChannelCreateSerializer,
ChannelModeratorSerializer,
ChannelSerializer,
Expand All @@ -25,6 +28,7 @@
from learning_resources.views import DefaultPagination
from main.constants import VALID_HTTP_METHODS
from main.permissions import AnonymousAccessReadonlyPermission
from main.utils import cache_page_for_all_users

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -191,3 +195,32 @@ def delete(self, request, *args, **kwargs): # noqa: ARG002
Channel.objects.get(id=self.kwargs["id"]), CHANNEL_ROLE_MODERATORS, user
)
return Response(status=HTTP_204_NO_CONTENT)


@extend_schema_view(
list=extend_schema(summary="Channel Detail Lookup by channel type and name"),
)
class ChannelCountsView(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
View for retrieving an individual channel by type and name
"""

serializer_class = ChannelCountsSerializer
permission_classes = (AnonymousAccessReadonlyPermission,)

def get_queryset(self):
"""
Return the channel by type and name
"""
return Channel.objects.filter(
channel_type=self.kwargs["channel_type"],
published=True,
)

@method_decorator(
cache_page_for_all_users(
settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search"
)
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
72 changes: 72 additions & 0 deletions channels/views_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for channels.views"""

import os
import random
from math import ceil

import pytest
Expand Down Expand Up @@ -443,3 +444,74 @@ def test_channel_configuration_is_not_editable(client, channel):
assert response.status_code == 200
channel.refresh_from_db()
assert channel.configuration == initial_config


def test_channel_counts_view(client):
"""Test the channel counts view returns counts for resources"""
url = reverse(
"channels:v0:channel_counts_api-list",
kwargs={"channel_type": "unit"},
)
total_count = 0
channels = ChannelFactory.create_batch(5, channel_type="unit")
for channel in channels:
resource_count = random.randint(1, 10) # noqa: S311
total_count += resource_count
channel_unit = channel.unit_detail.unit
resources = LearningResourceFactory.create_batch(
resource_count,
published=True,
resource_type="course",
create_course=True,
create_program=False,
)
for resource in resources:
channel_unit.learningresource_set.add(resource)
channel_unit.save()
random_channel = random.choice(list(channels)) # noqa: S311

response = client.get(url)
count_response = response.json()
assert response.status_code == 200
response_count_sum = 0
for item in count_response:
if item["name"] == random_channel.name:
response_count_sum += sum([item["counts"][key] for key in item["counts"]])
assert (
response_count_sum
== random_channel.unit_detail.unit.learningresource_set.count()
)


def test_channel_counts_view_is_cached_for_anonymous_users(client):
"""Test the channel counts view is cached for anonymous users"""
channel_count = 5
channels = ChannelFactory.create_batch(channel_count, channel_type="unit")
url = reverse(
"channels:v0:channel_counts_api-list",
kwargs={"channel_type": "unit"},
)
response = client.get(url).json()
assert len(response) == channel_count
for channel in channels:
channel.delete()
response = client.get(url).json()
assert len(response) == channel_count


def test_channel_counts_view_is_cached_for_authenticated_users(client):
"""Test the channel counts view is cached for authenticated users"""
channel_count = 5
channel_user = UserFactory.create()
client.force_login(channel_user)
channels = ChannelFactory.create_batch(channel_count, channel_type="unit")
url = reverse(
"channels:v0:channel_counts_api-list",
kwargs={"channel_type": "unit"},
)
response = client.get(url).json()
assert len(response) == channel_count
for channel in channels:
channel.delete()
response = client.get(url).json()
assert len(response) == channel_count
Loading