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

Only upsert LMSUser on launches #6682

Merged
merged 1 commit into from
Sep 13, 2024
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
2 changes: 1 addition & 1 deletion lms/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def get_lti_user(request) -> LTIUser | None:
if lti_user:
# Make a record of the user for analytics so we can map from the
# LTI users and the corresponding user in H
request.find_service(UserService).upsert_user(lti_user, request.lti_params)
request.find_service(UserService).upsert_user(lti_user)
Copy link
Member Author

Choose a reason for hiding this comment

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

Keep this method as it originally was


# Attach useful information to sentry in case we get an exception further down the line
sentry_sdk.set_tag("application_instance_id", lti_user.application_instance_id)
Expand Down
6 changes: 2 additions & 4 deletions lms/services/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, db, h_authority: str):
self._db = db
self._h_authority = h_authority

def upsert_user(self, lti_user: LTIUser, lti_params: LTIParams) -> User:
def upsert_user(self, lti_user: LTIUser) -> User:
"""Store a record of having seen a particular user."""

# Note! - Storing a user in our DB currently has an implication for
Expand Down Expand Up @@ -65,13 +65,11 @@ def upsert_user(self, lti_user: LTIUser, lti_params: LTIParams) -> User:
# We are only storing emails for teachers now.
user.email = lti_user.email

self._upsert_lms_user(user, lti_params)
return user

def _upsert_lms_user(self, user: User, lti_params: LTIParams) -> LMSUser:
def upsert_lms_user(self, user: User, lti_params: LTIParams) -> LMSUser:
Copy link
Member Author

Choose a reason for hiding this comment

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

Make this public.

"""Upsert LMSUser based on a User object."""
self._db.flush() # Make sure User has hit the DB on the current transaction

lms_user = bulk_upsert(
self._db,
LMSUser,
Expand Down
7 changes: 6 additions & 1 deletion lms/views/lti/basic_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from lms.models import Assignment
from lms.product.plugin.misc import MiscPlugin
from lms.security import Permissions
from lms.services import LTIGradingService, VitalSourceService
from lms.services import LTIGradingService, UserService, VitalSourceService
from lms.services.assignment import AssignmentService
from lms.validation import BasicLTILaunchSchema, ConfigureAssignmentSchema

Expand Down Expand Up @@ -51,6 +51,11 @@ def __init__(self, context, request) -> None:
self.request.find_service(name="application_instance").update_from_lti_params(
self.request.lti_user.application_instance, self.request.lti_params
)
# Keep a record of every LMS user in the DB
# While request.user gets updated on every request we only need/want to update LMSUser on launches
request.find_service(UserService).upsert_lms_user(
request.user, request.lti_params
)
self.course = self._record_course()

@view_config(
Expand Down
6 changes: 5 additions & 1 deletion lms/views/lti/deep_linking.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from lms.events import LTIEvent
from lms.product.plugin.misc import MiscPlugin
from lms.security import Permissions
from lms.services import JWTService
from lms.services import JWTService, UserService
from lms.validation import DeepLinkingLTILaunchSchema
from lms.validation._base import JSONPyramidRequestSchema

Expand All @@ -67,6 +67,10 @@ def deep_linking_launch(context, request):
request.find_service(name="application_instance").update_from_lti_params(
request.lti_user.application_instance, request.lti_params
)
# Keep a record of every LMS user in the DB
# While request.user gets updated on every request we only need/want to update LMSUser on launches
request.find_service(UserService).upsert_lms_user(request.user, request.lti_params)

course = request.find_service(name="course").get_from_launch(
request.product.family, request.lti_params
)
Expand Down
4 changes: 1 addition & 3 deletions tests/unit/lms/security_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,7 @@ def test_it(self, pyramid_request, user_service, lti_user, sentry_sdk):
result = get_lti_user(pyramid_request)

assert result == lti_user
user_service.upsert_user.assert_called_once_with(
result, pyramid_request.lti_params
)
user_service.upsert_user.assert_called_once_with(result)
sentry_sdk.set_tag.assert_called_once_with(
"application_instance_id", lti_user.application_instance_id
)
Expand Down
47 changes: 21 additions & 26 deletions tests/unit/lms/services/user_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

class TestUserService:
@pytest.mark.usefixtures("user_is_instructor")
def test_upsert_user(self, service, lti_user, db_session, pyramid_request):
user = service.upsert_user(lti_user, pyramid_request.lti_params)
def test_upsert_user(self, service, lti_user, db_session):
user = service.upsert_user(lti_user)

saved_user = db_session.query(User).order_by(User.id.desc()).first()
assert saved_user == Any.instance_of(User).with_attrs(
Expand All @@ -31,43 +31,50 @@ def test_upsert_user(self, service, lti_user, db_session, pyramid_request):
}
)
assert saved_user == user
self.assert_lms_user(db_session, user, pyramid_request.lti_params)

@pytest.mark.usefixtures("user_is_learner")
def test_upsert_user_doesnt_save_email_for_students(
self, service, lti_user, db_session, pyramid_request
self, service, lti_user, db_session
):
service.upsert_user(lti_user, pyramid_request.lti_params)
service.upsert_user(lti_user)

saved_user = db_session.query(User).order_by(User.id.desc()).first()
assert saved_user.roles == lti_user.roles
assert not saved_user.email
self.assert_lms_user(db_session, saved_user, pyramid_request.lti_params)

@pytest.mark.usefixtures("user")
def test_upsert_user_with_an_existing_user(
self, service, lti_user, db_session, pyramid_request
):
user = service.upsert_user(lti_user, pyramid_request.lti_params)
def test_upsert_user_with_an_existing_user(self, service, lti_user, db_session):
user = service.upsert_user(lti_user)

saved_user = db_session.get(User, user.id)
assert saved_user.id == user.id
assert saved_user.roles == lti_user.roles
assert user == saved_user
self.assert_lms_user(db_session, saved_user, pyramid_request.lti_params)

@pytest.mark.usefixtures("user")
def test_upsert_user_doesnt_save_email_for_existing_students(
self, service, lti_user, db_session, pyramid_request
self, service, lti_user, db_session
):
lti_user.roles = "Student"

service.upsert_user(lti_user, pyramid_request.lti_params)
service.upsert_user(lti_user)

saved_user = db_session.query(User).order_by(User.id.desc()).first()
assert saved_user.roles == lti_user.roles
assert not saved_user.email
self.assert_lms_user(db_session, saved_user, pyramid_request.lti_params)

def test_upsert_lms_user(self, service, lti_user, pyramid_request, db_session):
user = service.upsert_user(lti_user)
lms_user = service.upsert_lms_user(user, pyramid_request.lti_params)

lms_user = db_session.scalars(
select(LMSUser).where(LMSUser.h_userid == user.h_userid)
).one()

assert lms_user.display_name == user.display_name
assert lms_user.email == user.email
assert lms_user.updated == user.updated
assert lms_user.lti_v13_user_id == pyramid_request.lti_params.v13.get("sub")

def test_get(self, user, service):
db_user = service.get(user.application_instance, user.user_id)
Expand Down Expand Up @@ -176,18 +183,6 @@ def test_get_users_by_instructor_h_userid(

assert db_session.scalars(query).all() == [student_in_assigment]

def assert_lms_user(self, db_session, user, lti_params):
"""Assert the corresponding LMSUser to user exists in the DB with the same attributes."""

lms_user = db_session.scalars(
select(LMSUser).where(LMSUser.h_userid == user.h_userid)
).one()

assert lms_user.display_name == user.display_name
assert lms_user.email == user.email
assert lms_user.updated == user.updated
assert lms_user.lti_v13_user_id == lti_params.v13.get("sub")

@pytest.fixture
def course(self, application_instance, db_session):
course = factories.Course(application_instance=application_instance)
Expand Down
1 change: 1 addition & 0 deletions tests/unit/lms/views/lti/basic_launch_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"lti_role_service",
"grouping_service",
"misc_plugin",
"user_service",
)
class TestBasicLaunchViews:
def test___init___(
Expand Down
5 changes: 5 additions & 0 deletions tests/unit/lms/views/lti/deep_linking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from tests import factories


@pytest.mark.usefixtures("user_service")
class TestDeepLinkingFieldsRequestSchema:
@pytest.mark.parametrize(
"payload",
Expand Down Expand Up @@ -81,6 +82,7 @@ def test_it(
application_instance_service,
course_service,
misc_plugin,
user_service,
):
deep_linking_launch(context, pyramid_request)

Expand All @@ -90,6 +92,9 @@ def test_it(
course_service.get_from_launch.assert_called_once_with(
pyramid_request.product.family, pyramid_request.lti_params
)
user_service.upsert_lms_user.assert_called_once_with(
pyramid_request.user, pyramid_request.lti_params
)
lti_h_service.sync.assert_called_once_with(
[course_service.get_from_launch.return_value], pyramid_request.params
)
Expand Down
Loading