Skip to content

[BD-24] [BB-2726] [TNL-7330] Added Course Membership API function - #25843

Merged
nedbat merged 18 commits into
openedx:masterfrom
open-craft:shimulch/bb-2726-course-membership-api
Jun 3, 2021
Merged

[BD-24] [BB-2726] [TNL-7330] Added Course Membership API function#25843
nedbat merged 18 commits into
openedx:masterfrom
open-craft:shimulch/bb-2726-course-membership-api

Conversation

@shimulch

@shimulch shimulch commented Dec 10, 2020

Copy link
Copy Markdown

openedx/xblock-lti-consumer#124 depends on this PR.

This PR adds a new function get_course_members which can be used to get all users related to a Course. This function can -

  • Returns a paginated dictionary containing all students and access roles such as - staffs, instructors
{
    'count': total number of user instances matching specified filter,
    'num_pages': total number of pages,
    'current_page': current page number,
    'result': [
    	{
        	'id': user id,
            'email': user's email,
            'username': user's username,
            'profile': {
            	'name': user's name,
                'profile_image': user's profile image,
                .... other profile information
            },
            'enrollments': [
            	{
                	'mode': enrollment mode
                }
            ],
            'course_access_roles': [
            	{
                	'role': user's role in course
                }
            ]
        }
    ]
}
  • Filter only students or list of access roles

JIRA tickets:

Discussions: N/A

Dependencies: None

Screenshots: N/A

Sandbox URL: N/A

Merge deadline: ASAP - as it's blocking openedx/xblock-lti-consumer#124

Testing instructions:

  1. Pull this PR on your local devstack.
  2. Open LMS shell python manage.py lms shell.
  3. Import the function from lms.djangoapps.course_api.api import get_course_members.
  4. Get a CourseKey. Ex -
from opaque_keys.edx.keys import CourseKey
course_key = CourseKey.from_string(<YOUR_COURSE_KEY>)
  1. Test by calling get_course_members with available parameters.

Author notes and concerns:

  1. Is lms.djangoapps.course_api.api the correct place for this function?
  2. Generated SQL queries - (log from test)
SELECT COUNT(*) FROM (SELECT DISTINCT "auth_user"."id" AS Col1, "auth_user"."password" AS Col2, "auth_user"."last_login" AS Col3, "auth_user"."is_superuser" AS Col4, "auth_user"."username" AS Col5, "auth_user"."first_name" AS Col6, "auth_user"."last_name" AS Col7, "auth_user"."email" AS Col8, "auth_user"."is_staff" AS Col9, "auth_user"."is_active" AS Col10, "auth_user"."date_joined" AS Col11 FROM "auth_user" LEFT OUTER JOIN "student_courseaccessrole" ON ("auth_user"."id" = "student_courseaccessrole"."user_id") LEFT OUTER JOIN "student_courseenrollment" ON ("auth_user"."id" = "student_courseenrollment"."user_id") WHERE ("auth_user"."is_active" = 1 AND (("student_courseaccessrole"."course_id" = 'edX/toy/2012_Fall' AND "student_courseaccessrole"."role" IN ('staff', 'instructor', 'finance_admin', 'sales_admin', 'beta_testers', 'library_user', 'data_researcher', 'course_creator_group', 'support')) OR ("student_courseenrollment"."course_id" = 'edX/toy/2012_Fall' AND "student_courseenrollment"."is_active" = 1)))) subquery

SELECT DISTINCT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" LEFT OUTER JOIN "student_courseaccessrole" ON ("auth_user"."id" = "student_courseaccessrole"."user_id") LEFT OUTER JOIN "student_courseenrollment" ON ("auth_user"."id" = "student_courseenrollment"."user_id") WHERE ("auth_user"."is_active" = 1 AND (("student_courseaccessrole"."course_id" = 'edX/toy/2012_Fall' AND "student_courseaccessrole"."role" IN ('staff', 'instructor', 'finance_admin', 'sales_admin', 'beta_testers', 'library_user', 'data_researcher', 'course_creator_group', 'support')) OR ("student_courseenrollment"."course_id" = 'edX/toy/2012_Fall' AND "student_courseenrollment"."is_active" = 1))) ORDER BY "auth_user"."id" ASC  LIMIT 3

SELECT "auth_userprofile"."id", "auth_userprofile"."user_id", "auth_userprofile"."name", "auth_userprofile"."meta", "auth_userprofile"."courseware", "auth_userprofile"."language", "auth_userprofile"."location", "auth_userprofile"."year_of_birth", "auth_userprofile"."gender", "auth_userprofile"."level_of_education", "auth_userprofile"."mailing_address", "auth_userprofile"."city", "auth_userprofile"."country", "auth_userprofile"."state", "auth_userprofile"."goals", "auth_userprofile"."allow_certificate", "auth_userprofile"."bio", "auth_userprofile"."profile_image_uploaded_at", "auth_userprofile"."phone_number" FROM "auth_userprofile" WHERE "auth_userprofile"."user_id" IN (1, 2, 3)

SELECT "student_courseaccessrole"."id", "student_courseaccessrole"."user_id", "student_courseaccessrole"."org", "student_courseaccessrole"."course_id", "student_courseaccessrole"."role" FROM "student_courseaccessrole" WHERE ("student_courseaccessrole"."course_id" = 'edX/toy/2012_Fall' AND "student_courseaccessrole"."role" IN ('staff', 'instructor', 'finance_admin', 'sales_admin', 'beta_testers', 'library_user', 'data_researcher', 'course_creator_group', 'support') AND "student_courseaccessrole"."user_id" IN (1, 2, 3))

SELECT "student_courseenrollment"."id", "student_courseenrollment"."user_id", "student_courseenrollment"."course_id", "student_courseenrollment"."created", "student_courseenrollment"."is_active", "student_courseenrollment"."mode" FROM "student_courseenrollment" WHERE ("student_courseenrollment"."course_id" = 'edX/toy/2012_Fall' AND "student_courseenrollment"."user_id" IN (1, 2, 3)) ORDER BY "student_courseenrollment"."user_id" ASC, "student_courseenrollment"."course_id" ASC

Reviewers

@openedx-webhooks

openedx-webhooks commented Dec 10, 2020

Copy link
Copy Markdown

Thanks for the pull request, @shimulch! I've created BLENDED-793 to keep track of it in Jira. More details are on the BD-24 project page.

When this pull request is ready, tag your edX technical lead.

@giovannicimolin giovannicimolin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few comments.

I'm not sure this API is in the best place though. It retrieves users for a course, so it makes sense to have it in the enrollment API no?

CC @nedbat

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shimulch Is there a reason we'd avoid retrieving students enrolled? And if so, this parameters overlaps with access_roles, can we use a single parameter for this with (something like access_role=['student'] to filter students)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In my implementation, I've treated access_roles as valid values for CourseAccessRole. student is not a valid access_role. Students only have the CourseEnrollment instance but no CourseAccessRole instance. As it's an internal API, I wanted to be explicit about that.

Comment thread lms/djangoapps/course_api/api.py Outdated
Comment on lines 315 to 340

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can remove the prefetching here and do it in the viewset, inside the get_queryset method.
This needs to be a simple API to retrieve users enrolled into a course depending on their role.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, we can. But in that case, we would get all the CourseEnrollment of the user, not only those related to the Course. In order to do that, we would need to import CourseEnrollment in lti_consumer.compat module. Should we do that and remove prefetching here?

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Improve the function name to match it's functionality: get_course_member_queryset.

Suggested change
def get_course_members(course_key, include_students=True, access_roles=None, prefetch_accessroles=False, prefetch_enrollments=False):
def get_course_member_queryset(course_key, include_students=True, access_roles=None, prefetch_accessroles=False, prefetch_enrollments=False):

@shimulch

Copy link
Copy Markdown
Author

I'm not sure this API is in the best place though. It retrieves users for a course, so it makes sense to have it in the enrollment API no?

I've thought that too. But in this API, we need to check for both CourseEnrollment and CourseAccessRole. In my understanding enrollment API only deals with students whereas course API seems to be more generic (Students, Staffs, Instructors etc).

@natabene

Copy link
Copy Markdown
Contributor

@shimulch Thank you for your contribution. Please let me know once it is ready for our review.

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add an example of how to filter out all roles here (e.g pass an empty dict to exclude all roles (except students).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added example calls for different use cases.

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we can get to a simplified version of this.

Can you squash prefetch_accessroles and prefetch_enrollments down to a single parameter (prefetch_user_role_data)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Moved into one single flag prefetch_user_course_roles.

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Run this before doing the prefetch?

I'm not sure it'll have any impact since django uses lazy querysets, but the code will look better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When querying with in relation to many to many tables, some matches multiple times (ex: both instructor and enrolled) and I've seen it returns duplicates. That's why this has been placed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shimulch Yes, I got that. I just want you to run distinct before the prefetch operations. Just move this line to above the prefetch if statement.

@openedx-webhooks openedx-webhooks added waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. and removed needs triage labels Dec 28, 2020
@bradenmacdonald

Copy link
Copy Markdown
Contributor

I've thought that too. But in this API, we need to check for both CourseEnrollment and CourseAccessRole. In my understanding enrollment API only deals with students whereas course API seems to be more generic (Students, Staffs, Instructors etc).

There is already code to deal with access roles in the enrollment API:

https://github.com/edx/edx-platform/blob/151bd136667d27bf85256e382ed5b5ea0144e00f/openedx/core/djangoapps/enrollments/views.py#L252-L261

In fact, it seems more generic - it uses a pluggable "enrollment data backend" and uses RoleCache - so even if you keep this, you should probably use that existing method or similar.

My question is: what is this API adding that isn't covered by existing APIs? I assume there is already an API to get all the staff, instructors, and TAs for a course, because that's needed for the instructor dashboard "staff" tool as well as the "course team" page on Studio. And there is of course already an API to list all the students in a course. So is this just providing a combined view of data that can already be retrieved through two existing API calls?

@shimulch

shimulch commented Jan 8, 2021

Copy link
Copy Markdown
Author

In fact, it seems more generic - it uses a pluggable "enrollment data backend" and uses RoleCache - so even if you keep this, you should probably use that existing method or similar.

Using RoleCache might not be a good idea, because the function runs a conditional query on CourseAccessRole, where RoleCache is per object thing. But to support pluggable "enrollment data backend", do you think this whole function should move into openedx/core/djangoapps/enrollments/data.py? So that other backend also can override the behavior?

What is this API adding that isn't covered by existing APIs?

As you have already mentioned, the combined view of all User objects related to a course.

I assume there is already an API to get all the staff, instructors, and TAs for a course, because that's needed for the instructor dashboard "staff" tool as well as the "course team" page on Studio. And there is of course already an API to list all the students in a course. So is this just providing a combined view of data that can already be retrieved through two existing API calls?

Yes, but it returns a User queryset object which can be paginated. I couldn't figure out any other way to combine this other than creating this function. Initially, we had created this function directly on xblock-lti-consumer. But as it uses lots of Platform-specific API's, it seems it would better if we integrate this on the platform in a reusable manner, in case some other xblock also needs similar functionality.

@bradenmacdonald

Copy link
Copy Markdown
Contributor

But to support pluggable "enrollment data backend", do you think this whole function should move into openedx/core/djangoapps/enrollments/data.py? So that other backend also can override the behavior?

Probably. At this point it might be a good idea to check the PRs / git-blame to see who made the enrollment API pluggable and get their input. I didn't even know it had a pluggable backend. But whoever did that or maintains it can probably offer more useful advice here.

Yes, but it returns a User queryset object which can be paginated.

I see, the goal here is adding a python API, even though there are similar REST APIs that provide this information. Your rationale makes total sense, and an api.py file would be the right place to put this, yes. The main question now is how it's implemented, and it seems like we have to figure out how it needs to interact with the pluggable backend and/or RoleCache.

@shimulch

Copy link
Copy Markdown
Author

The pluggable data API seem to introduced in this PR - https://github.com/edx/edx-platform/pull/5695

And from the PR description, it seems the goal was to make it easier for testing with fake data. If that's the only reason, I think we might not have to worry about data API here.

@bradenmacdonald

Copy link
Copy Markdown
Contributor

And from the PR description, it seems the goal was to make it easier for testing with fake data. If that's the only reason, I think we might not have to worry about data API here.

Ah, ok. Sorry for focusing on that then. I can try to review this soon but it might be better if someone with more context on these APIs can weigh in.

@natabene

Copy link
Copy Markdown
Contributor

@shimulch Is this ready for edX review?

@shimulch

Copy link
Copy Markdown
Author

@natabene This is also part of BD-24 as like https://github.com/edx/edx-platform/pull/25844 and was blocked due to some performance concerns. Since the ADR got accepted, we are now working back again on this one.

@natabene natabene changed the title [BB-2726] [TNL-7330] Added Course Membership API function [BD-24] [BB-2726] [TNL-7330] Added Course Membership API function Mar 12, 2021
@openedx-webhooks openedx-webhooks added blended PR is managed through 2U's blended developmnt program needs triage and removed open-source-contribution PR author is not from Axim or 2U waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. labels Mar 12, 2021
@shimulch
shimulch force-pushed the shimulch/bb-2726-course-membership-api branch 2 times, most recently from 2d99a22 to 6779a03 Compare March 14, 2021 04:35

@giovannicimolin giovannicimolin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shimulch I left a few comments. I tested this to be working, but there's a problem with the approach here, and I've suggested a few changes.

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shimulch Yes, I got that. I just want you to run distinct before the prefetch operations. Just move this line to above the prefetch if statement.

Comment thread lms/djangoapps/course_api/api.py Outdated
Comment on lines 327 to 340

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@shimulch I don't think the behavior here is the expected one. We always want the course_id filter to be applied here. I suggested the prefetching is just to avoid making repetitive queries when retrieving data from the related models, but I see that there's some filtering happening.

I think the best approach here is to:

  1. Remove the prefetch_user_course_roles conditional, leaving prefetching and filtering to always happen.
  2. Change the function docstring to be explicitly clear that only course access roles and enrollments related to the queried course will be retrieved.
  3. Update tests to take this into account, and check that accessing the related objects doesn't trigger extra queries when using this API.

@giovannicimolin giovannicimolin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good so far, and it seems to be working as expected. Left a few minor comments since this is just a wip version.

Comment thread lms/djangoapps/course_api/api.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you add a ordering query here? Otherwise the paginator might yield inconsistent results.
I got this warning on my devstack:

2021-03-17 14:53:06,478 WARNING 300 [py.warnings] [user None] [ip None] warnings.py:109 - /edx/app/edxapp/edx-platform/lms/djangoapps/course_api/api.py:348: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'django.contrib.auth.models.User'> QuerySet.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added order by user id.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This mentions "legacy" serializer, isn't there a more up-to-date one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I didn't find any other profile serializer other than AccountLegacyProfileSerializer. And this seems to be used while updating profile here.

@giovannicimolin

giovannicimolin commented May 18, 2021

Copy link
Copy Markdown
Contributor

@ormsbee I've implemented your suggestions and the end result is blazing fast now. 🚀

bool(CourseEnrollment.objects.filter(course_id=course_id)[MAX_SUPPORTED_ENROLLMENTS:1])

This didn't work, querysets evaluate as false if the start was bigger than the end in the index. This worked perfectly though:

    over_limit = CourseEnrollment.get_active_enrollments_in_course(
        course_key
    )[settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT:][:1]
    if over_limit.exists():
        raise Exception(
            f"Can't retrieve course members for courses with more than"
            f"{settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT} active enrollments"
        )

Here are the queries executed:

# 1
SELECT (1) AS "a" FROM "student_courseenrollment" WHERE ("student_courseenrollment"."course_id" = 'edX/toy/2012_Fall' AND "student_courseenrollment"."is_active" = 1)  LIMIT 1 OFFSET 1000

#2
SELECT "student_courseenrollment"."id", "student_courseenrollment"."user_id", "student_courseenrollment"."course_id", "student_courseenrollment"."created", "student_courseenrollment"."is_active", "student_courseenrollment"."mode", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined", "auth_userprofile"."id", "auth_userprofile"."user_id", "auth_userprofile"."name", "auth_userprofile"."meta", "auth_userprofile"."courseware", "auth_userprofile"."language", "auth_userprofile"."location", "auth_userprofile"."year_of_birth", "auth_userprofile"."gender", "auth_userprofile"."level_of_education", "auth_userprofile"."mailing_address", "auth_userprofile"."city", "auth_userprofile"."country", "auth_userprofile"."state", "auth_userprofile"."goals", "auth_userprofile"."bio", "auth_userprofile"."profile_image_uploaded_at", "auth_userprofile"."phone_number" FROM "student_courseenrollment" INNER JOIN "auth_user" ON ("student_courseenrollment"."user_id" = "auth_user"."id") LEFT OUTER JOIN "auth_userprofile" ON ("auth_user"."id" = "auth_userprofile"."user_id") WHERE ("student_courseenrollment"."course_id" = 'edX/toy/2012_Fall' AND "student_courseenrollment"."is_active" = 1) ORDER BY "student_courseenrollment"."user_id" ASC, "student_courseenrollment"."course_id" ASC  LIMIT 1000

#3
SELECT "student_courseaccessrole"."id", "student_courseaccessrole"."user_id", "student_courseaccessrole"."org", "student_courseaccessrole"."course_id", "student_courseaccessrole"."role", "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined", "auth_userprofile"."id", "auth_userprofile"."user_id", "auth_userprofile"."name", "auth_userprofile"."meta", "auth_userprofile"."courseware", "auth_userprofile"."language", "auth_userprofile"."location", "auth_userprofile"."year_of_birth", "auth_userprofile"."gender", "auth_userprofile"."level_of_education", "auth_userprofile"."mailing_address", "auth_userprofile"."city", "auth_userprofile"."country", "auth_userprofile"."state", "auth_userprofile"."goals", "auth_userprofile"."bio", "auth_userprofile"."profile_image_uploaded_at", "auth_userprofile"."phone_number" FROM "student_courseaccessrole" INNER JOIN "auth_user" ON ("student_courseaccessrole"."user_id" = "auth_user"."id") LEFT OUTER JOIN "auth_userprofile" ON ("auth_user"."id" = "auth_userprofile"."user_id") WHERE "student_courseaccessrole"."course_id" = 'edX/toy/2012_Fall'

This is ready for review. CC @nedbat

@ormsbee ormsbee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Queries look good. Just a few requests on documentation, tests, and errors.

Also, when you make the final commit message for this, please be sure to write details about why this was implemented in the way it was. I think that it's a reasonable tradeoff, but it's going to look a little weird without context, and people may not immediately realize the performance issues or the complexity brought on by the fact that we're trying to combine two tables with users that can be in either or both places. Lacking that context could cause folks a lot of pain a few years down the line.

Thank you for working through this!

"staff",
]
}
}

@ormsbee ormsbee May 19, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The docstring should clearly indicate the too-large-course error case, and answer the following:

  • Where is the config value set?
  • What exception is raised?

Comment thread lms/djangoapps/course_api/api.py Outdated
course_key
)[settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT:][:1]
if over_limit.exists():
raise Exception(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please make a new error type that subclasses Exception, so callers can check for it explicitly.

Comment thread lms/djangoapps/course_api/api.py Outdated
user_info[access_role.user_id] = get_user_info(enrollment.user)

# Merge user role information with `user_info`
for user in user_info:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: user_id might be clearer here, since you're iterating over those, and not User model objects.

Comment thread lms/djangoapps/course_api/api.py Outdated

# Merge user role information with `user_info`
for user in user_info:
user_info[user]['roles'] = user_roles.get(user, [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: user_roles is a defaultdict(list), so the .get with a default value shouldn't be necessary (i.e. user_roles[user] should work).

raise Exception(
f"Can't retrieve course members for courses with more than"
f"{settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT} active enrollments"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Say I got paged for this because a certain course that was dependent on this API grew beyond a certain size and suddenly started breaking... and then I looked in the logs and saw this message. Since the immediate short-term fix is going to be to bump up the limit, I think it'd be helpful to not just give the number, but also log which setting it's pulled from (so that the word COURSE_MEMBER_API_ENROLLMENT_LIMIT is in the log string and I can go from that log message to searching my config).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

... not that this sort of thing has ever happened to us... 🤧

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we add the course key to this error message? Or will it be obvious from some other source in the logs?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@nedbat Yes, good idea! The log output now look like this:

Can't retrieve course members for COURSE_KEY since it has more than 1000 active enrollments. This limit is stored on `settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT`

self.assertEqual(
members[self.honor.id]['roles'],
['student']
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We want to test that the combining of data works properly, so I think it would be useful to have test cases for people who exist only in enrollments (students), people who exist in both (course staff, beta testing students), and people who exist only in access_roles (data_researcher).

@ormsbee

ormsbee commented May 19, 2021

Copy link
Copy Markdown
Contributor

@nedbat: The performance issues that I was concerned about have been resolved. I left a review here tonight, but please feel free to take over the reviews/merging at your discretion. Thank you for looping me on in this!

@giovannicimolin

giovannicimolin commented May 19, 2021

Copy link
Copy Markdown
Contributor

@ormsbee Thanks for the thorough review, I've addressed all of your review comments.
I'll squash the commits with a detailed commit message once I get final approval on the implementation, to avoid rebasing the PR on each new comment.

@nedbat This is ready for review :)

Comment thread common/djangoapps/student/models.py Outdated
@classmethod
def access_roles_in_course(cls, course_key):
"""
Returns all users that have a course access role in a given course.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It doesn't return all users but all CourseAccessRoles for a given course, with user information prefetched.

Suggested change
Returns all users that have a course access role in a given course.
Returns all CourseAccessRole for a given course and prefetches user information.

@giovannicimolin

Copy link
Copy Markdown
Contributor

jenkins run python

@shimulch shimulch left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@ormsbee Thanks for all the suggestions. 😃
@giovannicimolin Thanks a lot for working on this. LGTM 👍🏽

@giovannicimolin

Copy link
Copy Markdown
Contributor

@nedbat This is ready for review.

Comment thread lms/djangoapps/course_api/api.py Outdated
# Retrieve all active enrollments in course and prefetch user information
enrollments = CourseEnrollment.get_active_enrollments_in_course(
course_key
)[:settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we still limiting here? If the limit accomplishes anything, then we'd never reach this line of code, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yup, makes sense. I copied this from Dave's pseudo-implementation above but it has no effect (or at least it shouldn't).

Comment thread lms/djangoapps/course_api/api.py Outdated
}
}
"""
def get_user_info(user, enrollment_mode=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: this function doesn't feel like a "get_" to me, since it just reformats the data you pass to it. I would name it "make_user_info_dict" or something.

@nedbat nedbat left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I really like this pull request: it took a lot of discussion, but in the end, is good clean code.

@edx-status-bot

Copy link
Copy Markdown

Your PR has finished running tests. There were no failures.

@giovannicimolin

giovannicimolin commented May 26, 2021

Copy link
Copy Markdown
Contributor

@nedbat I addressed your review comments :)

@nedbat
nedbat merged commit 2a8a58a into openedx:master Jun 3, 2021
@giovannicimolin
giovannicimolin deleted the shimulch/bb-2726-course-membership-api branch June 3, 2021 13:40
@edx-pipeline-bot

Copy link
Copy Markdown
Contributor

EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production.

@edx-pipeline-bot

Copy link
Copy Markdown
Contributor

EdX Release Notice: This PR has been deployed to the production environment.

nedbat pushed a commit that referenced this pull request Jun 3, 2021
The get_course_members API returns a dict of users associated with a course.
This is a potentially expensive operation on a large course, so there is a
control in place to limit its cost.  If a course has more than
settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT enrollments, then the function
raises an OverEnrollmentLimitException.

This API was added to help implement the LTI 1.3 Names and Roles Provisioning
service.

Jira references: [BD-24] [BB-2726] [TNL-7330]
Pull request: #25843
Co-authored-by: Giovanni Cimolin da Silva <giovannicimolin@gmail.com>

(cherry picked from commit 2a8a58a)
@nedbat

nedbat commented Jun 3, 2021

Copy link
Copy Markdown
Contributor

I've cherry-picked this onto Lilac.

@edx-pipeline-bot

Copy link
Copy Markdown
Contributor

EdX Release Notice: This PR has been deployed to the production environment.

blarghmatey pushed a commit to mitodl/edx-platform that referenced this pull request Aug 2, 2021
The get_course_members API returns a dict of users associated with a course.
This is a potentially expensive operation on a large course, so there is a
control in place to limit its cost.  If a course has more than
settings.COURSE_MEMBER_API_ENROLLMENT_LIMIT enrollments, then the function
raises an OverEnrollmentLimitException.

This API was added to help implement the LTI 1.3 Names and Roles Provisioning
service.

Jira references: [BD-24] [BB-2726] [TNL-7330] 
Pull request: openedx#25843
Co-authored-by: Giovanni Cimolin da Silva <giovannicimolin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blended PR is managed through 2U's blended developmnt program merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants