Skip to content

[BD-24] [BB-2726] [TNL-7330] Added batch_get_or_create class method for ExternalId - #25844

Merged
nedbat merged 2 commits into
openedx:masterfrom
open-craft:shimulch/bb-2726-batch-external-id
Mar 25, 2021
Merged

[BD-24] [BB-2726] [TNL-7330] Added batch_get_or_create class method for ExternalId#25844
nedbat merged 2 commits into
openedx:masterfrom
open-craft:shimulch/bb-2726-batch-external-id

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 class method batch_get_or_create_user_ids to ExternalId. This enables a faster way to create ExternalId for a list of User.

When batch_get_or_create_user_ids gets called with a list of User instances and a type_name (Name of the type of ExternalId) it will return a dict mapping user id with corresponding external id.

{
   user_id: ExternalId
}

JIRA tickets: This PR completes a part of the functionality required in the following tickets -

Dependencies: None

Merge deadline: None

Testing instructions:

  1. Pull this PR on your local devstack.
  2. Run your local devstack
  3. Open LMS shell by running make lms-shell
  4. Open Django shell for LMS python manage.py lms shell
  5. Create couple of users -
from django.contrib.auth.models import User
users = [User.objects.create_user(username=f'test{i}', email=f'test{i}@beatles.com', password='test') for i in range(5)]
  1. Import ExternalId model
from openedx.core.djangoapps.external_user_ids.models import ExternalId
  1. Create external ids in batch
result = ExternalId.batch_get_or_create_user_ids(users, 'lti')
  1. Check the result is in the above mentioned format. For example -
{10: <ExternalId: ExternalId object (None)>, 11: <ExternalId: ExternalId object (None)>, 12: <ExternalId: ExternalId object (None)>, 13: <ExternalId: ExternalId object (None)>, 14: <ExternalId: ExternalId object (None)>}

Author notes and concerns:
N/A

Reviewers

@openedx-webhooks openedx-webhooks added needs triage open-source-contribution PR author is not from Axim or 2U labels Dec 10, 2020
@openedx-webhooks

openedx-webhooks commented Dec 10, 2020

Copy link
Copy Markdown

Thanks for the pull request, @shimulch! I've created BLENDED-789 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.

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 is returning ExternalId model, when ideally it should return a dict mapping id to external_user_ids and signaling if they where created.

Can you make this function return something like the reponse shown below?

{
    "id_1": ("external_id_1", True),  # (external_id, created)
    "id_2": ("external_id_2", False),
}

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 way we keep the external user ID API purely using data without passing classes around.

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.

@giovannicimolin, Now the method returns a dictionary as per the suggested structure.

@giovannicimolin giovannicimolin Dec 18, 2020

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 think we can get rid of the created argument here. To simplify the implementation, a "user_id": ExternalId map is enough here.

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.

Suggested change
def batch_get_or_create(cls, users, type_name):
def batch_get_or_create_user_ids(cls, users, type_name):

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.

Updated.

Comment on lines 127 to 131

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 this is not the optimal solution, given that a potentially big user dataset might come in.
Forcing a queryset evaluation and then subtracting two user objects sets might be an expensive operation.

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.

@giovannicimolin, I've optimized it to use a query instead of set operation.

@natabene

Copy link
Copy Markdown
Contributor

@shimulch Thank you for your contribution. Please ping me once it is ready for edX review.

@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.

Aside a minor change in the data returned and a comment about a test, this is good to go.

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.

Since this is a batch operation, we should ideally add some query count test here, to make sure more users != more queries.

This can be done with a simple assertion in the test_batch_get_or_create_user_ids (see assertNumQueries, might require a change to TransactionTestCase instead of TestCase).

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.

Tests with the expected number of queries added. Thanks for the tip 👍🏽

@giovannicimolin giovannicimolin Dec 18, 2020

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 think we can get rid of the created argument here. To simplify the implementation, a "user_id": ExternalId map is enough here.

@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 20, 2020

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 please provide an example of when it's useful to create these in a batch, instead of creating them for each user as needed?

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.

@bradenmacdonald, In the case of LTI Consumer, External ID's are created when a student actually launches LTI from LMS. But when providing course member information to a third party tool, not every member has External ID's available. To create one by one would be a performance issue. Thus this method has been created.

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 Oops, I meant to specify that I'd like that added to the docstring. Dosctrings should generally say both what the code does and why it exists, because without the why it's hard to know when/how it can be refactored or removed in the future.

@natabene

Copy link
Copy Markdown
Contributor

@shimulch Just checking if you are still planning on pursuing this PR.

@pomegranited

Copy link
Copy Markdown
Contributor

@natabene CC @bradenmacdonald @shimulch This PR is related to openedx/xblock-lti-consumer#124, which is for a Blended project. Could someone tag it so we know it's not an ordinary OSPR?

@natabene natabene changed the title [BB-2726] [TNL-7330] Added batch_get_or_create class method for ExternalId [BD-24] [BB-2726] [TNL-7330] Added batch_get_or_create class method for ExternalId Mar 11, 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 11, 2021
@giovannicimolin

Copy link
Copy Markdown
Contributor

@natabene As @pomegranited mentioned, this is related to BD-24. We ended up having discussions related to the implementation on this ADR before continuing the work. We're now unblocked and will start moving this forward in the coming week.

@shimulch
shimulch force-pushed the shimulch/bb-2726-batch-external-id branch from b7951e3 to 8654d80 Compare March 11, 2021 14:33
@shimulch
shimulch force-pushed the shimulch/bb-2726-batch-external-id branch from 4556a0d to e0785b0 Compare March 12, 2021 05:46

@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 Code is working perfectly, but I have a few nits and changes:

  1. Python APIs should not return internal classes/objects whenever possible. Please change the ExternalId object on the response with the plain text external id.
  2. Improve this PR test instructions and proper testing instructions for verifying this works on the devstack.
  3. Clean up the unused description fields.
  4. Rebase this PR on the latest master.

Comment on lines 112 to 116

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.

ExternalIds have no relation to LTI. This explanation should be in the lti_consumer code that uses this API.
Here you should put a description of what this API does, its limitations and return format.

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.

Removed, LTI specific doc, and added generic doc which explains what this method does.

@shimulch
shimulch force-pushed the shimulch/bb-2726-batch-external-id branch from 6c6b020 to 8db49de Compare March 18, 2021 06:45
@shimulch

Copy link
Copy Markdown
Author

@giovannicimolin,

Python APIs should not return internal classes/objects whenever possible. Please change the ExternalId object on the response with the plain text external id.

This method is just a batch version of the existing add_new_user_id method which returns the ExternalId object as well.

Improve this PR test instructions and proper testing instructions for verifying this works on the devstack.

Thanks for pointing that out. I've added proper test instruction on how to verify the new method.

Clean up the unused description fields.

If you are talking about the PR description, then yes, I've cleaned up. If that's not the case I am probably not sure what you meant.

Rebase this PR on the latest master.

Rebased.

@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.

👍

  • I tested this:
  1. Followed the testing instructions on the devstack.
  2. Checked that the query count didn't increase when increasing the number of users created (tested with 2, 20, and 200 users in the test script above).
  • I read through the code
  • I checked for accessibility issues NA
  • Includes documentation: In docstrings

@shimulch This is good to go. Great work.
@nedbat This is ready for your review.

type_obj = ExternalIdType.objects.get(name=type_name)
except ExternalIdType.DoesNotExist:
LOGGER.info(
'Batch ID Creation failed, no external id type of {type}'.format(

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.

Suggested change
'Batch ID Creation failed, no external id type of {type}'.format(
'Batch ID Creation failed, no external id type of {type!r}'.format(

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.

Updated.

type=type_name
)
)
return 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.

Should this be {} instead to stick to the signature of the function?

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.

Or would it be better to raise an exception? I don't know about ExternalIdType, and what callers expect.

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.

(or at least update the docstring)

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.

The reasoning behind sending None is to keep it similar to add_new_user_id which also returns None if fails. I've updated the docstring.

with self.assertNumQueries(self.EXPECTED_NUM_OF_QUERIES):
result = ExternalId.batch_get_or_create_user_ids(users, id_type)

assert len(result) == len(result)

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 surely is not what you meant!

Suggested change
assert len(result) == len(result)
assert len(result) == len(users)

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.

Sorry for that and thanks for catching it.

@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.

GitHub says I have to leave a comment explaining the changes, so I will point you to all of the comments I left explaining changes :)

@edx-status-bot

Copy link
Copy Markdown

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

@shimulch

Copy link
Copy Markdown
Author

@nedbat Thanks for the review. I've addressed your comments. :-)

@nedbat
nedbat merged commit eba710c into openedx:master Mar 25, 2021
@giovannicimolin
giovannicimolin deleted the shimulch/bb-2726-batch-external-id branch March 25, 2021 16:53
@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.

@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.

1 similar comment
@edx-pipeline-bot

Copy link
Copy Markdown
Contributor

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

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