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

MAU 2 - Add Monthly Active Enrollment Model #446

Merged
merged 3 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 12 additions & 0 deletions figures/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ class LearnerCourseGradeMetricsAdmin(UserRelatedMixin, admin.ModelAdmin):
read_only_fields = ('user', 'user_link')


@admin.register(figures.models.MonthlyActiveEnrollment)
class MonthlyActiveEnrollmentAdmin(admin.ModelAdmin):
"""Defines the admin interface for the MonthlyActiveEnrollment model
"""
list_display = ('id', 'site', 'course_id', 'user', 'month_for')
list_filter = (
('site', RelatedOnlyDropdownFilter),
('course_id', AllValuesDropdownFilter),
('user', RelatedOnlyFieldListFilter),
'month_for')


@admin.register(figures.models.PipelineError)
class PipelineErrorAdmin(admin.ModelAdmin):
"""Defines the admin interface for the PipelineError model
Expand Down
47 changes: 47 additions & 0 deletions figures/migrations/0017_add_monthly_active_enrollment_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django import VERSION as DJANGO_VERSION

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):
if DJANGO_VERSION[0:2] == (1,8):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sites', '0001_initial'),
('figures', '0016_add_collect_elapsed_to_ed_and_lcgm'),
]
else: # Assuming 1.11+
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sites', '0002_alter_domain_unique'),
('figures', '0016_add_collect_elapsed_to_ed_and_lcgm'),
]

operations = [
migrations.CreateModel(
name='MonthlyActiveEnrollment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('course_id', models.CharField(max_length=255, db_index=True)),
('month_for', models.DateField(db_index=True)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-month_for', 'site', 'course_id'],
},
),
migrations.AlterUniqueTogether(
name='monthlyactiveenrollment',
unique_together=set([('site', 'course_id', 'user', 'month_for')]),
),
]
65 changes: 64 additions & 1 deletion figures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
"""

from __future__ import absolute_import
from datetime import date
from datetime import datetime, date
from time import time
import six
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.validators import MaxValueValidator, MinValueValidator
Expand Down Expand Up @@ -570,6 +571,68 @@ def completed(self):
self.sections_worked == self.sections_possible)


class MonthlyActiveEnrollmentManager(models.Manager):
"""Model manager for MonthlyActiveEnrollment

TODO:
* Add query convenience methods to get aggregate metrics,
* mau_for_site_and_month(self, site, month_for or year and month)
* mau_for_course_and_month(self, course, month_for or year and month)
* current month site MAU
* current month course MAU
"""

def add_mae(self, site_id, course_id, user_id, date_for=None, overwrite=False):
"""
We use 'date_for' instead of 'month_for' to enforce the day of month for
the 'month_for' field
"""
if date_for:
month_for = date(year=date_for.year, month=date_for.month, day=1)
else:
today = datetime.utcnow()
month_for = date(year=today.year, month=today.month, day=1)
if not overwrite:
try:
obj = self.get(
site_id=site_id,
course_id=six.text_type(course_id), # noqa: F821
user_id=user_id,
month_for=month_for)
return (obj, False)
except MonthlyActiveEnrollment.DoesNotExist:
pass

return self.update_or_create(
site_id=site_id,
course_id=six.text_type(course_id), # noqa: F821
user_id=user_id,
month_for=month_for)


@python_2_unicode_compatible
class MonthlyActiveEnrollment(TimeStampedModel):
"""Capture enrollment activity for a given month

An enrollment is a unique user+course pair

"""
site = models.ForeignKey(Site, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
course_id = models.CharField(max_length=255, db_index=True)
month_for = models.DateField(db_index=True)

objects = MonthlyActiveEnrollmentManager()

class Meta:
ordering = ['-month_for', 'site', 'course_id']
unique_together = ['site', 'course_id', 'user', 'month_for']

def __str__(self):
return "id:{}, site:{} course_id:{} user:{} month_for:{},".format(
self.id, self.site.domain, self.course_id, self.user.username, self.month_for)


@python_2_unicode_compatible
class PipelineError(TimeStampedModel):
"""
Expand Down
24 changes: 14 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def sm_test_data(db):


@pytest.mark.django_db
def make_site_data(num_users=3, num_courses=2):
def make_site_data(num_users=3, num_courses=2, create_enrollments=True):

site = SiteFactory()
if organizations_support_sites():
Expand All @@ -74,14 +74,15 @@ def make_site_data(num_users=3, num_courses=2):

users = [UserFactory() for i in range(num_users)]

enrollments = []
for i, user in enumerate(users):
# Create increasing number of enrollments for each user, maximum to one less
# than the number of courses
for j in range(i):
enrollments.append(
CourseEnrollmentFactory(course=courses[j-1], user=user)
)
if create_enrollments:
enrollments = []
for i, user in enumerate(users):
# Create increasing number of enrollments for each user, maximum to one less
# than the number of courses
for j in range(i):
enrollments.append(
CourseEnrollmentFactory(course=courses[j-1], user=user)
)

if organizations_support_sites():
for course in courses:
Expand All @@ -91,13 +92,16 @@ def make_site_data(num_users=3, num_courses=2):
# Set up user mappings
map_users_to_org(org, users)

return dict(
data = dict(
site=site,
org=org,
courses=courses,
users=users,
enrollments=enrollments,
)
if create_enrollments:
data['enrollments'] = enrollments
return data


@pytest.fixture
Expand Down
12 changes: 12 additions & 0 deletions tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
CourseMauMetrics,
EnrollmentData,
LearnerCourseGradeMetrics,
MonthlyActiveEnrollment,
SiteDailyMetrics,
SiteMonthlyMetrics,
SiteMauMetrics,
Expand Down Expand Up @@ -356,6 +357,17 @@ class Meta:
sections_possible = 10


class MonthlyActiveEnrollmentFactory(DjangoModelFactory):
class Meta:
model = MonthlyActiveEnrollment
site = factory.SubFactory(SiteFactory)
month_for = factory.Sequence(lambda n: (
datetime.date(2020, 6, 1) - relativedelta(months=n)))
course_id = factory.Sequence(lambda n:
'course-v1:StarFleetAcademy+SFA{}+2161'.format(n))
user = factory.SubFactory(UserFactory)


class SiteDailyMetricsFactory(DjangoModelFactory):
class Meta:
model = SiteDailyMetrics
Expand Down
122 changes: 122 additions & 0 deletions tests/models/test_monthly_active_enrollment_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Tests MonthlyActiveEnrollment model

Initially just basic testing for coverage
"""
from __future__ import absolute_import
import six
from datetime import date
import pytest

from figures.models import MonthlyActiveEnrollment

from tests.factories import MonthlyActiveEnrollmentFactory
from tests.helpers import organizations_support_sites
from tests.conftest import make_site_data


@pytest.fixture
@pytest.mark.django_db
def mae_test_data(db, settings):
"""
Because Figures `MonthlyActiveEnrollment` is decoupled from course data,
we do not need to create enrollments
"""
if organizations_support_sites():
settings.FEATURES['FIGURES_IS_MULTISITE'] = True

our_site_data = make_site_data(create_enrollments=False)
other_site_data = make_site_data(create_enrollments=False)
return dict(us=our_site_data, them=other_site_data)


@pytest.mark.parametrize('overwrite, expect_created', [
(False, True),
(True, True),
])
def test_add_mae_new_overwrite_option(mae_test_data,
overwrite,
expect_created):
"""
Test 'overwrite' option when adding a monthly active enrollment
"""
# Arrange
us = mae_test_data['us']
course_id = us['courses'][0].id
user = us['users'][0]
assert not MonthlyActiveEnrollment.objects.count()

# Act
obj, created = MonthlyActiveEnrollment.objects.add_mae(
site_id=us['site'].id,
course_id=course_id,
user_id=user.id,
overwrite=overwrite)

# Assert
assert obj
assert created == expect_created
assert MonthlyActiveEnrollment.objects.count() == 1


@pytest.mark.parametrize('overwrite, expect_created', [
(False, False),
(True, False),
])
def test_add_mae_existing_overwrite_option(mae_test_data,
overwrite,
expect_created):
"""
Test 'overwrite' option when adding to an existing monthly active enrollment
"""

# Arrange
us = mae_test_data['us']
course_id = us['courses'][0].id
user = us['users'][0]
date_for = date.today()
month_for = date(year=date_for.year, month=date_for.month, day=1)
mae = MonthlyActiveEnrollmentFactory(
site=us['site'],
course_id=six.text_type(course_id),
user=user,
month_for=month_for)

assert MonthlyActiveEnrollment.objects.count() == 1

# Act
obj, created = MonthlyActiveEnrollment.objects.add_mae(
site_id=us['site'].id,
course_id=course_id,
user_id=user.id,
overwrite=overwrite)

# Assert
assert obj and obj.id and obj.id == mae.id
assert created == expect_created
assert MonthlyActiveEnrollment.objects.count() == 1


def test_add_mae_with_date_for(mae_test_data):
"""
Test using the current date as 'date_for'
"""
# Arrange
us = mae_test_data['us']
course_id = us['courses'][0].id
user = us['users'][0]
assert not MonthlyActiveEnrollment.objects.count()
date_for = date.today()
month_for = date(year=date_for.year, month=date_for.month, day=1)

# Act
obj, created = MonthlyActiveEnrollment.objects.add_mae(
site_id=us['site'].id,
course_id=course_id,
user_id=user.id,
date_for=date_for)

# Assert
assert obj and obj.id
assert created
assert MonthlyActiveEnrollment.objects.count() == 1
assert obj.month_for == month_for
2 changes: 2 additions & 0 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
SiteDailyMetrics,
SiteMonthlyMetrics,
LearnerCourseGradeMetrics,
MonthlyActiveEnrollment,
PipelineError,
CourseMauMetrics,
)
Expand All @@ -39,6 +40,7 @@ def setup(self, db):
(SiteDailyMetrics, figures.admin.SiteDailyMetricsAdmin),
(SiteMonthlyMetrics, figures.admin.SiteMonthlyMetricsAdmin),
(LearnerCourseGradeMetrics, figures.admin.LearnerCourseGradeMetricsAdmin),
(MonthlyActiveEnrollment, figures.admin.MonthlyActiveEnrollmentAdmin),
(PipelineError, figures.admin.PipelineErrorAdmin),
(CourseMauMetrics, figures.admin.CourseMauMetricsAdmin),
])
Expand Down