Skip to content
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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
python-version: [3.8]
toxenv: [py38-django32, quality, docs]
os: [ubuntu-latest]
python-version: [3.11, 3.12]
toxenv: [django42, django52, quality, docs]

steps:
- name: checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v5

- name: setup python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

Expand All @@ -47,7 +47,7 @@ jobs:
run: tox

- name: Run coverage
if: matrix.python-version == '3.8' && matrix.toxenv == 'py38-django32'
if: matrix.python-version == '3.11' && matrix.toxenv == 'django42'
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ on:

jobs:
push:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v5

- name: setup python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: 3.8
python-version: 3.11

- name: Install Dependencies
run: pip install -r requirements/pip.txt
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ Change Log
Unreleased
~~~~~~~~~~

[4.3.0] - 2026-01-28
~~~~~~~~~~~~~~~~~~~~

* Drop support for Python 2, Python 3.8, and Django 3.2.
* Add support for Python 3.11 + 3.12 and Django 4.2 + 5.2.
* Fix the ``stats`` API endpoint.
* Fix the ``ENROLLMENT_TRACK_UPDATED`` signal.
* Remove unused compat imports.

[4.2.0] - 2024-06-21
~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion completion_aggregator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

from __future__ import absolute_import, unicode_literals

__version__ = '4.2.0'
__version__ = '4.3.0'
10 changes: 5 additions & 5 deletions completion_aggregator/api/v0/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@

from __future__ import absolute_import, division, print_function, unicode_literals

from django.urls import re_path
from django.urls import path

from . import views

app_name = 'completion_aggregator'

urlpatterns = [
re_path(
r'^course/(?P<course_key>.+)/blocks/(?P<block_key>.+)/$',
path(
'course/<path:course_key>/blocks/<path:block_key>/',
views.CompletionBlockUpdateView.as_view(),
name='blockcompletion-update'
),
re_path(r'^course/$', views.CompletionListView.as_view(), name='aggregator-list'),
re_path(r'^course/(?P<course_key>.+)/$', views.CompletionDetailView.as_view(), name='aggregator-detail'),
path('course/', views.CompletionListView.as_view(), name='aggregator-list'),
path('course/<path:course_key>/', views.CompletionDetailView.as_view(), name='aggregator-detail'),
]
8 changes: 4 additions & 4 deletions completion_aggregator/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

from __future__ import absolute_import, division, print_function, unicode_literals

from django.urls import re_path
from django.urls import path

from . import views

app_name = 'completion_aggregator'

urlpatterns = [
re_path(r'^course/$', views.CompletionListView.as_view()),
re_path(r'^course/(?P<course_key>.+)/$', views.CompletionDetailView.as_view()),
re_path(r'^stats/(?P<course_key>.+)/$', views.CourseLevelCompletionStatsView.as_view()),
path('course/', views.CompletionListView.as_view()),
path('course/<path:course_key>/', views.CompletionDetailView.as_view()),
path('stats/<path:course_key>/', views.CourseLevelCompletionStatsView.as_view()),
]
15 changes: 10 additions & 5 deletions completion_aggregator/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,11 +563,16 @@ def get(self, request, course_key):
course_key=course_key,
aggregation_name='course',
user_id__in=[enrollment.user_id for enrollment in enrollments])
completion_stats = aggregator_qs.aggregate(
possible=Avg('possible'),
earned=Sum('earned') / len(enrollments),
percent=Sum('earned') / (Avg('possible') * len(enrollments)))
completion_stats['course_key'] = course_key
num_enrollments = len(enrollments)
aggregates = aggregator_qs.aggregate(avg_possible=Avg("possible"), total_earned=Sum("earned"))
avg_possible = aggregates["avg_possible"] or 0
total_earned = aggregates["total_earned"] or 0
completion_stats = {
"course_key": course_key,
"possible": avg_possible,
"earned": total_earned / num_enrollments if num_enrollments else 0,
"percent": total_earned / (avg_possible * num_enrollments) if avg_possible and num_enrollments else 0,
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for the review: this one has been broken for a while, but we didn't catch it because we were using Django 3.2 in the CI.


serializer = self.get_serializer_class()(
instance=completion_stats,
Expand Down
8 changes: 3 additions & 5 deletions completion_aggregator/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
import logging
import time

import six

from django.conf import settings
from django.core.cache import cache

Expand Down Expand Up @@ -79,7 +77,7 @@ def perform_aggregation(batch_size=10000, delay=0.0, limit=None, routing_key=Non
stale_blocks = collections.defaultdict(set)
forced_updates = set()
enqueued = 0
for idx in six.moves.range(max_id, min([min_id + batch_size, max_id]) - 1, -1 * batch_size):
for idx in range(max_id, min([min_id + batch_size, max_id]) - 1, -1 * batch_size):
if enqueued >= limit:
break
evaluated = stale_queryset.filter(id__gt=idx - batch_size, id__lte=idx)
Expand Down Expand Up @@ -108,11 +106,11 @@ def perform_aggregation(batch_size=10000, delay=0.0, limit=None, routing_key=Non
elif len(stale_blocks[enrollment]) > MAX_KEYS_PER_TASK:
blocks = []
else:
blocks = [six.text_type(block_key) for block_key in stale_blocks[enrollment]]
blocks = [str(block_key) for block_key in stale_blocks[enrollment]]
aggregation_tasks.update_aggregators.apply_async(
kwargs={
'username': enrollment.username,
'course_key': six.text_type(enrollment.course_key),
'course_key': str(enrollment.course_key),
'block_keys': blocks,
'force': enrollment in forced_updates,
},
Expand Down
35 changes: 0 additions & 35 deletions completion_aggregator/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,38 +147,3 @@ def get_mobile_only_courses(enrollments):
course_overview_list = CourseOverview.objects.filter(id__in=course_keys, mobile_available=True)
filtered_course_overview = [overview.id for overview in course_overview_list]
return enrollments.filter(course_id__in=filtered_course_overview)


def get_course(course_key):
"""
Get course for given key.
"""
from courseware.courses import _get_course # pylint: disable=import-error
return _get_course(course_key)


def get_cohorts_for_course(course_key):
"""
Get cohorts for given course key.
"""
from openedx.core.djangoapps.course_groups import cohorts # pylint: disable=import-error
if cohorts.is_course_cohorted(course_key):
return cohorts.get_course_cohort_id(course_key)
return None


def course_access_role_model():
"""
Return the student.models.CourseAccessRole model.
"""
# pragma: no-cover
from common.djangoapps.student.models import CourseAccessRole # pylint: disable=import-error
return CourseAccessRole


def cohort_membership_model():
"""
Return the course_groups.models.CohortMembership model.
"""
from course_groups.models import CohortMembership # pylint: disable=import-error
return CohortMembership
3 changes: 1 addition & 2 deletions completion_aggregator/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from datetime import datetime

import pytz
import six
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
from xblock.plugin import PluginMissingError
Expand Down Expand Up @@ -63,7 +62,7 @@ def set(self, value):
Sets the group to `str(self.course_key)`.
"""
group = six.text_type(self.course_key)
group = str(self.course_key)
CacheGroup().set(group, self.cache_key, value, timeout=UPDATER_CACHE_TIMEOUT)

def touch(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ def _configure_logging(self, options):

if options.get('verbosity') == 0:
log_level = logging.WARNING
elif options.get('verbosity') >= 1:
else:
log_level = logging.INFO
log.setLevel(log_level)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import logging

import six
from opaque_keys.edx.keys import CourseKey

from django.core.management.base import BaseCommand
Expand Down Expand Up @@ -59,7 +58,7 @@ def handle(self, *args, **options):
options['course_keys'] = BlockCompletion.objects.values_list('context_key').distinct()
CourseEnrollment = compat.course_enrollment_model() # pylint: disable=invalid-name
for course in options['course_keys']:
if isinstance(course, six.string_types):
if isinstance(course, str):
course = CourseKey.from_string(course)
all_enrollments = CourseEnrollment.objects.filter(course=course).select_related('user')
StaleCompletion.objects.bulk_create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import django.utils.timezone
import model_utils.fields
import opaque_keys.edx.django.models
import six
import time


Expand Down Expand Up @@ -38,7 +37,7 @@ def copy_data(apps, schema_editor):
}
print()

for start_id in six.moves.range(0, initial_max_id, BATCH_SIZE):
for start_id in range(0, initial_max_id, BATCH_SIZE):
end_id = min(start_id + BATCH_SIZE, initial_max_id)
cursor.execute(copy_sql, [start_id, end_id])
time.sleep(.1)
Expand Down Expand Up @@ -70,7 +69,7 @@ def copy_data(apps, schema_editor):
'source': 'completion_aggregator_stalecompletionold',
'target': 'completion_aggregator_stalecompletion',
}
for start_id in six.moves.range(initial_max_id, final_max_id, BATCH_SIZE):
for start_id in range(initial_max_id, final_max_id, BATCH_SIZE):
end_id = start_id + BATCH_SIZE
cursor.execute(copy_sql, [start_id, end_id])
if end_id - initial_max_id % 100000 == 0:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Rename indexes from index_together to Meta.indexes with explicit names.

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('completion_aggregator', '0005_cachegroupinvalidation'),
]

operations = [
migrations.RenameIndex(
model_name='aggregator',
new_name='aggr_name_user_course_idx',
old_fields=('user', 'aggregation_name', 'course_key'),
),
migrations.RenameIndex(
model_name='aggregator',
new_name='aggr_name_course_block_per_idx',
old_fields=('course_key', 'aggregation_name', 'block_key', 'percent'),
),
migrations.RenameIndex(
model_name='stalecompletion',
new_name='stale_user_course_resolved_idx',
old_fields=('username', 'course_key', 'created', 'resolved'),
),
]
19 changes: 14 additions & 5 deletions completion_aggregator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,15 @@ class Meta:
Metadata describing the Aggregator model.
"""

index_together = [
('user', 'aggregation_name', 'course_key'),
('course_key', 'aggregation_name', 'block_key', 'percent'),
indexes = [
models.Index(
fields=["user", "aggregation_name", "course_key"],
name="aggr_name_user_course_idx",
),
models.Index(
fields=["course_key", "aggregation_name", "block_key", "percent"],
name="aggr_name_course_block_per_idx",
),
]

unique_together = [
Expand Down Expand Up @@ -319,8 +325,11 @@ class Meta:
Metadata describing the StaleCompletion model.
"""

index_together = [
('username', 'course_key', 'created', 'resolved'),
indexes = [
models.Index(
fields=["username", "course_key", "created", "resolved"],
name="stale_user_course_resolved_idx",
),
]

def __str__(self):
Expand Down
8 changes: 1 addition & 7 deletions completion_aggregator/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import logging
from collections import defaultdict

import six
from rest_framework import serializers
from xblock.completable import XBlockCompletionMode
from xblock.core import XBlock
Expand Down Expand Up @@ -375,12 +374,7 @@ def native_identifier(string):
This is required for the first argument to three-argument-`type()`. This
function expects all identifiers comprise only ascii characters.
"""
if six.PY2: # pragma: no cover

if isinstance(string, six.text_type):
# Python 2 identifiers are required to be ascii
string = string.encode('ascii')
elif isinstance(string, bytes): # pragma: no cover
if isinstance(string, bytes): # pragma: no cover
# Python 3 identifiers can technically be non-ascii, but don't do that.
string = string.decode('ascii')
return string
Loading