-
Notifications
You must be signed in to change notification settings - Fork 3
add to list dialog updates #1463
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
Conversation
a416f6d to
55a069f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new dialog is UI is looking good. I did notice a few functionality issues.
- An issue with Featured courses ont he homepage
- A permissioning issue... Currently, if one user updates the resource membership, that seems to be affecting all other users.
A suggestion for the first issue is in the review. Overall suggestion for the API below.
This PR adds a variety of new APIs:
GET, POST /api/v1/learning_resources/{learning_resource_id}/relationships/
GET, POST, PATCH, DELETE /api/v1/learning_resources/{learning_resource_id}/relationships/{id}
PATCH /api/v1/learning_resources/{learning_resource_id}/relationships/set_learning_path_relationships/
PATCH /api/v1/learning_resources/{learning_resource_id}/relationships/set_user_list_relationships/
Did you intend to add these all?
Request/Suggestion: IMO, we should only add two new API endpoints:
# more similar to what you have now
PATCH /api/v1/learning_resource/{resource_id}/userlists/
PATCH /api/v1/learning_resource/{resource_id}/userlists/
OR
# needs resource_id in body
POST /api/v1/userlists/modify_items/
POST /api/v1/learningpaths/modify_items/
We'll have to check ownership for all userlists, and users should only be able to edit their own userlist. We definitely want tests for the permissioning / user specificity, too.
| onSuccess: (response, _vars) => { | ||
| queryClient.setQueriesData<PaginatedLearningResourceList>( | ||
| learningResources.featured({}).queryKey, | ||
| (old) => updateListParentsOnAdd(response.data, old), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: On the homepage featured carousel list (applies to learning paths, too), try
- added a resource w/o any userlists to a userlist
- Expected: Bookmark becomes filled
- Actual: no change
- removing all userlists from a resource w/ userlist
- Expected: Bookmark becomes unfilled
- Actual: No change
I think the issue is that in the old way, response.data was a single UserListRelationship. Now it's Array< UserListRelationship>.
Probably easiest to tweak by changing changing updateListParentsOnAdd/Remove. Actually, maybe a new function updateListParentsOnChange... since your API returns ALL the list parents, I think it could be much simpler.
| }, | ||
| onSettled: (_response, _err, vars) => { | ||
| invalidateResourceQueries(queryClient, vars.learning_resource_id, { | ||
| skipFeatured: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be false, and you can remove the onSucccess hook.
See
- Filled vs Unfilled Bookmarks #1180 (comment)
(particularly the last bullet)
learning_resources/views.py
Outdated
| @action(detail=False, methods=["patch"], name="Set User List Relationships") | ||
| def set_user_list_relationships(self, request, *args, **kwargs): # noqa: ARG002 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you look at the generated OpenAPI spec, you'll see
Aside: This bug is why Typescript didn't realize the usage of updateListParentsOnAdd mentioned above was an error.
See tfranzel/drf-spectacular#190 (comment) for more on this, but the tldr is:
detail=Falseonly affects the URL route./set_user_list_relationships/NOTset_user_list_relationships/:id- in particular, it has no affect on the logic of what the route returns, and DRF-spectacular can't predict that, so we need annotate it.
|
|
||
|
|
||
| @extend_schema( | ||
| parameters=[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are 3 parameters here... 2 are relevant for one endpoint, 2 are relevant for the other.
I would try moving the annotation to the methods themselves.
I also think you can skip the annotation for the path parameter. I think DRF-spectacular will pick that up automatically.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right in that you can usually omit the id path param, but in this case drf-spectacular mislabels it so I'm just specifying it to fix that:
"A unique integer value identifying this learning resource relationship." vs "id of the learning resource"
| permission_classes = (AnonymousAccessReadonlyPermission,) | ||
| filter_backends = [MultipleOptionsFilterBackend] | ||
| queryset = LearningResourceRelationship.objects.select_related("parent", "child") | ||
| http_method_names = ["patch"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comment: PUT still seems like a mildly better fit to me since this replaces the existing relationshisps (for that resource). When I briefly tried, I had an issue making PUT work with detail=false, but wince you're using detail=true, I think it would work.
| queryset = LearningResourceRelationship.objects.select_related("parent", "child") | ||
| http_method_names = ["patch"] | ||
|
|
||
| def get_serializer_class(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know if get_serializer_class matters here? I would expect not since both of your methods call a specific serializer + have DRF-spectacular annotations. (More of a django question for my own education.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without it you get this error during the OpenAPI spec check:
/home/runner/work/mit-open/mit-open/learning_resources/views.py:381: Error [LearningResourceListRelationshipViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'LearningResourceListRelationshipViewSet' should either include a `serializer_class` attribute, or override the `get_serializer_class()` method.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that makes sense. DRF-spectacular uses get_serializer_class for introspection, which it can't do with an explicitly coded serializer in the action method.
learning_resources/views.py
Outdated
| if not is_learning_path_editor(request): | ||
| return Response( | ||
| {"error": "You do not have permission to edit learning paths"}, | ||
| status=403, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fairly certain you can pass permission_classes to an action decorator, so you could pass permission_classes=[HasLearningPathPermissions] to the decorator, but this is good too.
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behavior and implementation all look good 👍 👍 I made one request regarding a test that I think should be added.
learning_resources/views.py
Outdated
| child_id=learning_resource_id, | ||
| position=last_index + 1, | ||
| ) | ||
| serializer = UserListRelationshipSerializer(current_relationships, many=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor I guess you could do Serializer = self.get_serializer_class() in the action method rather than using it explicitly. Right now there could be a conflict between the two.
| Set User List relationships for a given Learning Resource | ||
| """ | ||
| learning_resource_id = kwargs.get("pk") | ||
| user_list_ids = request.query_params.getlist("userlist_id") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Throw a 403 here if one of the userlists does not belong to user
I think something like
if (UserList.objects.filter(pk__in=user_list_ids).exclude(author=request.user).exists()):
msg = f"You do not have permission to edit all of {user_list_ids}"
raise PermissionDenied(msg)and maybe add a test for the 403 error.
I think this is worthwhile and doesn't seem too complicated
|
|
||
|
|
||
| def test_set_userlist_relationships(client, user): | ||
| """Test the userlists endpoint for setting multiple userlist relationships""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request: I think it's definitely worth testing "User X changes relationship does not affect other users' relationships" (earlier behavior in this PR.)
Suggestion: To this test, add something like
userlist_other_author = factories.UserListFactory.create()
assert userlist_other_author.author != userlists[0].author # I like this for sanity 🤷
factories.UserListRelationshipFactory.create(
parent=userlist_other_author, child=course.learning_resource
)
# Then at the end, assert child still belongs to userlist_other_author|
@ChristopherChudzicki all set |
…rebuild position index
… that don't belong to you
735c4f3 to
4d70856
Compare
| assert ( | ||
| UserListRelationship.objects.filter(child=course.learning_resource).count() == 3 | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 👍 This fails if the parent__author=request.user filter is missing. That's what I was hoping for. Ty!
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.

What are the relevant tickets?
Closes https://github.com/mitodl/hq/issues/5275
Description (What does it do?)
This PR alters the Add to List dialog used to add Learning Resources to both Learning Paths and User Lists. Instead of adding the resource to lists immediately upon checking the box, the user's changes are saved upon clicking the new "Save" button. New API endpoints were added to accomplish this, sending all the lists that the resource should be a part of as one request. The styling of the modal was also updated to reflect the current designs in Figma.
Screenshots (if appropriate):
How can this be tested?
mit-learnon this branchis_stafforis_superuserset to true