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
18 changes: 17 additions & 1 deletion openedx/core/djangoapps/appsembler/api/sites.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@

from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from organizations.models import Organization, OrganizationCourse
from organizations.models import (
Organization,
OrganizationCourse,
UserOrganizationMapping,
)

from openedx.core.djangoapps.content.course_overviews.models import CourseOverview

Expand Down Expand Up @@ -67,3 +72,14 @@ def course_belongs_to_site(site, course_id):
def get_enrollments_for_site(site):
course_keys = get_course_keys_for_site(site)
return CourseEnrollment.objects.filter(course_id__in=course_keys)


def get_user_ids_for_site(site):
orgs = Organization.objects.filter(sites__in=[site])
mappings = UserOrganizationMapping.objects.filter(organization__in=orgs)
return mappings.values_list('user_id', flat=True)


def get_users_for_site(site):
user_ids = get_user_ids_for_site(site)
return get_user_model().objects.filter(id__in=user_ids)
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
from xmodule.modulestore.tests.factories import CourseFactory

from organizations.models import UserOrganizationMapping
from openedx.core.djangoapps.appsembler.api.sites import get_enrollments_for_site
from openedx.core.djangoapps.appsembler.api.sites import (
get_enrollments_for_site,
)

from openedx.core.djangoapps.appsembler.api.tests.factories import (
CourseOverviewFactory,
Expand Down Expand Up @@ -120,6 +122,44 @@ def test_get_enrollments_for_course(self):
def test_get_single_enrollment(self):
pass

@ddt.data(('user_id', 'id'), ('username', 'username'))
@ddt.unpack
def test_get_enrollments_for_user(self, query_param, attr_name):
# Set up additional test data
user = UserFactory()
UserOrganizationMappingFactory(user=self.caller,
organization=self.my_site_org,
is_amc_admin=True)
courses = [CourseFactory.create() for i in range(0, 3)]
course_overviews = []
course_enrollments = []
for course in courses:
OrganizationCourseFactory(organization=self.my_site_org,
course_id=str(course.id))
course_overview = CourseOverviewFactory(id=course.id)
course_enrollment = CourseEnrollmentFactory(course=course_overview,
user=user)
course_overviews.append(course_overview)
course_enrollments.append(course_enrollment)

# Set up our request
url = reverse('tahoe-api:v1:enrollments-list')
# Need to resolve without the query parameters
view = resolve(url).func
url += '?{}={}'.format(query_param, getattr(user, attr_name))
request = APIRequestFactory().get(url)
request.META['HTTP_HOST'] = self.my_site.domain
force_authenticate(request, user=self.caller)
response = view(request)
response.render()
results = response.data['results']

expected_course_ids = [str(co.id) for co in course_overviews]
found_course_ids = [obj['course_details']['course_id'] for obj in results]
assert set(found_course_ids) == set(expected_course_ids)
for result in results:
assert result['user'] == user.username

def test_invalid_enroll_data_no_learners(self):
"""
This does a partial test
Expand Down
21 changes: 21 additions & 0 deletions openedx/core/djangoapps/appsembler/api/tests/test_sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
SiteFactory,
)

from student.tests.factories import UserFactory

from openedx.core.djangoapps.appsembler.api import sites as aapi_sites
from openedx.core.djangoapps.appsembler.api.helpers import as_course_key

Expand All @@ -35,6 +37,11 @@
)


def create_org_users(org, new_user_count):
return [UserOrganizationMappingFactory(
organization=org).user for i in xrange(new_user_count)]


class SitesModuleTests(TestCase):
def setUp(self):
"""
Expand Down Expand Up @@ -102,3 +109,17 @@ def test_course_not_belongs_to_invalid_site(self):
with self.assertRaises(ValueError):
aapi_sites.course_belongs_to_site(site=site,
course_id=self.my_course_overviews[0])

def test_get_users_ids_for_site(self):
my_users = create_org_users(org=self.my_site_org, new_user_count=3)
other_users = create_org_users(org=self.other_site_org, new_user_count=2)
retrieved_user_ids = aapi_sites.get_user_ids_for_site(self.my_site)
assert set(retrieved_user_ids) == set([obj.id for obj in my_users])
assert set(retrieved_user_ids).isdisjoint(set([obj.id for obj in other_users]))

def test_get_users_for_site(self):
my_users = create_org_users(org=self.my_site_org, new_user_count=3)
other_users = create_org_users(org=self.other_site_org, new_user_count=2)
retrieved_users = aapi_sites.get_users_for_site(self.my_site)
assert set(retrieved_users) == set(my_users)
assert set(retrieved_users).isdisjoint(set(other_users))
110 changes: 110 additions & 0 deletions openedx/core/djangoapps/appsembler/api/tests/test_user_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

from unittest import skip

from django.contrib.sites.models import Site
from django.core.urlresolvers import resolve, reverse
from django.test import TestCase
from rest_framework.test import APIRequestFactory, force_authenticate

from student.tests.factories import CourseEnrollmentFactory, UserFactory

import ddt
import mock

from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory

from openedx.core.djangoapps.appsembler.api.sites import (
get_users_for_site,
)

from openedx.core.djangoapps.appsembler.api.v1.serializers import UserIndexSerializer

from openedx.core.djangoapps.appsembler.api.tests.factories import (
CourseOverviewFactory,
OrganizationFactory,
UserOrganizationMappingFactory,
)

APPSEMBLER_API_VIEWS_MODULE = 'openedx.core.djangoapps.appsembler.api.v1.views'


@ddt.ddt
@mock.patch(APPSEMBLER_API_VIEWS_MODULE + '.UserIndexViewSet.throttle_classes', [])
class UserIndexViewSetTest(TestCase):

def setUp(self):
"""
Set up test data for site isolation
- two sites, our site and the other site
- two orgs, one per site
- set of learners in our site
- one learner in the other site
- one learner in both sites

- caller user in our site with admin rights

"""
super(UserIndexViewSetTest, self).setUp()
self.my_site = Site.objects.get(domain=u'example.com')
self.other_site = SiteFactory(domain='other-site.test')
self.other_site_org = OrganizationFactory(sites=[self.other_site])
self.my_site_org = OrganizationFactory(sites=[self.my_site])

# Set up users and enrollments for 'my site'
self.my_site_users = [UserFactory() for i in range(3)]
for user in self.my_site_users:
UserOrganizationMappingFactory(user=user,
organization=self.my_site_org)

self.other_site_users = [UserFactory()]
for user in self.other_site_users:
UserOrganizationMappingFactory(user=user,
organization=self.other_site_org)

self.caller = UserFactory()
UserOrganizationMappingFactory(user=self.caller,
organization=self.my_site_org,
is_amc_admin=True)

def test_serializer(self):
user = self.my_site_users[0]
data = UserIndexSerializer(instance=user).data
assert data['username'] == user.username
assert data['fullname'] == user.profile.name
assert data['email'] == user.email

def test_get_all_users_for_site(self):
url = reverse('tahoe-api:v1:users-list')
request = APIRequestFactory().get(url)
request.META['HTTP_HOST'] = self.my_site.domain
force_authenticate(request, user=self.caller)

view = resolve(url).func
response = view(request)
response.render()
results = response.data['results']

response_count = len(results)
expected_users = get_users_for_site(self.my_site)

user_ids = [rec['id'] for rec in results]
assert set(user_ids) == set([obj.id for obj in expected_users])

@skip("Need to implement user filter")
def test_get_all_enrolled_learners_for_site(self):

# Set up enrollment data

url = reverse('tahoe-api:v1:users-list')
request = APIRequestFactory().get(url)
request.META['HTTP_HOST'] = self.my_site.domain
force_authenticate(request, user=self.caller)

view = resolve(url).func
response = view(request)
response.render()
results = response.data['results']

response_count = len(results)
user_ids = [rec['id'] for rec in results]
assert set(user_ids) == set([obj.id for obj in self.my_site_users])
4 changes: 3 additions & 1 deletion openedx/core/djangoapps/appsembler/api/v1/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class CourseEnrollmentFilter(django_filters.FilterSet):
'''
course_id = django_filters.CharFilter(method='filter_course_id')
is_active = django_filters.BooleanFilter(name='is_active',)
user_id = django_filters.CharFilter(name='user__id')
username = django_filters.CharFilter(name='user__username')

def filter_course_id(self, queryset, name, value):
'''
Expand All @@ -65,4 +67,4 @@ def filter_course_id(self, queryset, name, value):

class Meta:
model = CourseEnrollment
fields = ['course_id', 'user_id', 'is_active', ]
fields = ['course_id', 'user_id', 'username', 'is_active', ]
17 changes: 17 additions & 0 deletions openedx/core/djangoapps/appsembler/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

from django.contrib.auth import get_user_model

from rest_framework import serializers

from opaque_keys import InvalidKeyError
Expand Down Expand Up @@ -57,3 +59,18 @@ def validate_identifiers(self, value):
'identifiers must be a list, not a {}'.format(type(value)))
# TODO: Do we want to enforce identifier type (like email, username)
return value


class UserIndexSerializer(serializers.ModelSerializer):
"""Provides a limited set of user information for summary display
"""
id = serializers.IntegerField(read_only=True)
username = serializers.CharField(read_only=True)
fullname = serializers.CharField(
source='profile.name', default=None, read_only=True)

class Meta:
model = get_user_model()
fields = (
'id', 'username', 'fullname', 'email',
)
6 changes: 6 additions & 0 deletions openedx/core/djangoapps/appsembler/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
'registrations',
)

router.register(
r'users',
views.UserIndexViewSet,
'users',
)

urlpatterns = [
url(r'', include(router.urls, )),
]
31 changes: 29 additions & 2 deletions openedx/core/djangoapps/appsembler/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import random
import string


from django.contrib.auth import get_user_model
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from django.conf import settings
from django.db import transaction
Expand Down Expand Up @@ -56,7 +56,9 @@
TahoeLimitOffsetPagination
)
from openedx.core.djangoapps.appsembler.api.v1.serializers import (
CourseOverviewSerializer, BulkEnrollmentSerializer
CourseOverviewSerializer,
BulkEnrollmentSerializer,
UserIndexSerializer,
)

# TODO: Just move into v1 directory
Expand All @@ -68,6 +70,7 @@
get_site_for_course,
get_enrollments_for_site,
course_belongs_to_site,
get_users_for_site,
)


Expand Down Expand Up @@ -357,3 +360,27 @@ def create(self, request, *args, **kwargs):
response_code = status.HTTP_400_BAD_REQUEST

return Response(response_data, status=response_code)


class UserIndexViewSet(TahoeAuthMixin, viewsets.ReadOnlyModelViewSet):
"""Provides course information

To provide data for all learners on your site::

GET /tahoe/api/v1/users/

To provide details on a specific learner:

GET /tahoe/api/v1/users/<user id>/

"""
model = get_user_model()
pagination_class = TahoeLimitOffsetPagination
serializer_class = UserIndexSerializer
throttle_classes = (TahoeAPIUserThrottle,)
filter_backends = (DjangoFilterBackend, )

def get_queryset(self):
site = django.contrib.sites.shortcuts.get_current_site(self.request)
queryset = get_users_for_site(site)
return queryset