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
10 changes: 9 additions & 1 deletion course_access_groups/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from django.apps import AppConfig

from openedx.core.djangoapps.plugins.constants import ProjectType, PluginURLs
from openedx.core.djangoapps.plugins.constants import ProjectType, PluginSignals, PluginURLs


class CourseAccessGroupsConfig(AppConfig):
Expand All @@ -25,4 +25,12 @@ class CourseAccessGroupsConfig(AppConfig):
PluginURLs.REGEX: '^course_access_groups/api/v1/',
},
},
PluginSignals.CONFIG: {
ProjectType.LMS: {
PluginSignals.RECEIVERS: [{
PluginSignals.RECEIVER_FUNC_NAME: 'on_learner_account_activated',
PluginSignals.SIGNAL_PATH: 'openedx.core.djangoapps.signals.signals.USER_ACCOUNT_ACTIVATED',
}],
}
},
}
20 changes: 20 additions & 0 deletions course_access_groups/migrations/0002_membership_automatic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-02-15 16:49
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('course_access_groups', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='membership',
name='automatic',
field=models.BooleanField(default=False, help_text='If created by MembershipRule'),
),
]
43 changes: 42 additions & 1 deletion course_access_groups/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.contrib.auth import get_user_model
from django.db import models
from model_utils import models as utils_models
from organizations.models import Organization
from organizations.models import Organization, UserOrganizationMapping
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview


Expand Down Expand Up @@ -40,6 +40,47 @@ class Membership(utils_models.TimeStampedModel):
get_user_model(),
help_text='Learner. A learner can only be enrolled in a single Course Access Group.'
)
automatic = models.BooleanField(
default=False,
help_text='If created by MembershipRule',
)

@classmethod
def create_from_rules(cls, user):
"""
Automatically enroll a user based on existing MembershipRule.

:param user:
:raise ValueError if the user is not active.
:return: Membership (or None)
"""
if not user.is_active:
# Ensure that only users with verified emails are enrolled the group
# This error should not happen in production.
# If it does, look at the both the `Registration` class and the USER_ACCOUNT_ACTIVATED signal in Open edX.
raise ValueError('Course Access Groups: Unable to create automatic Membership for inactive user.')

_, email_domain = user.email.rsplit('@', 1)

# Ideally an exception should be thrown if there's more than one organization
# but such error is out of the scope of the CAG module.
user_orgs = Organization.objects.filter(
pk__in=UserOrganizationMapping.objects.filter(user=user),
)
rule = MembershipRule.objects.filter(
domain=email_domain,
group__organization=user_orgs,
).first()

if rule:
membership, _created = cls.objects.get_or_create(
user=user,
defaults={
'group': rule.group,
'automatic': True,
},
)
return membership


class MembershipRule(utils_models.TimeStampedModel):
Expand Down
19 changes: 19 additions & 0 deletions course_access_groups/singals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Signals and receivers for Course Access Groups.
"""

from __future__ import absolute_import, unicode_literals

from course_access_groups.models import Membership


def on_learner_account_activated(sender, user, **kwargs): # pylint: disable=unused-argument
"""
Receive the `USER_ACCOUNT_ACTIVATED` signal to apply MembershipRule.

:param sender: The sender class.
:param user: The activated learner.
:param kwargs: Extra keyword args.
"""
Membership.create_from_rules(user)
1 change: 1 addition & 0 deletions mocks/openedx/core/djangoapps/plugins/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@

# Minimal mocks to reduce test maintenance costs.
ProjectType = Mock()
PluginSignals = Mock()
PluginURLs = Mock()
49 changes: 42 additions & 7 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tests for the `course-access-groups` models module.
"""

from __future__ import absolute_import, unicode_literals

from course_access_groups import acl_backends, models, urls
import pytest
from organizations.models import UserOrganizationMapping
from course_access_groups.models import (
Membership,
MembershipRule,
)
from test_utils.factories import (
UserFactory,
CourseAccessGroupFactory,
)
from course_access_groups.singals import on_learner_account_activated


def test_fake():
@pytest.mark.django_db
class TestMembershipRuleApply(object):
"""
Just a fake unit test case.
Test the on_learner_account_activated signal and its MembershipRule.apply_for_user helper.
"""
assert acl_backends
assert models
assert urls

@pytest.mark.parametrize('email, should_enroll', [
['someone@known_site.com', True],
['another.one@other_site.com', False],
])
def test_simple_match(self, email, should_enroll):
"""
Basic test for membership rules.
"""
assert not Membership.objects.count()
user = UserFactory.create(is_active=True, email=email)
group = CourseAccessGroupFactory.create()
UserOrganizationMapping.objects.create(user=user, organization=group.organization)
MembershipRule.objects.create(name='Something', domain='known_site.com', group=group)

on_learner_account_activated(self.__class__, user)
membership = Membership.objects.filter(user=user).first()

assert bool(membership) == should_enroll
assert not membership or (membership.group == group)

def test_inactive_user(self):
"""
Ensure inactive user don't get a rule by mistake.
"""
user = UserFactory.create(is_active=False)
with pytest.raises(ValueError):
on_learner_account_activated(self.__class__, user)