Skip to content

Comments

Add action to resolve Community Library Submissions#5178

Merged
AlexVelezLl merged 7 commits intolearningequality:community-channelsfrom
Jakoma02:resolve_submission_action
Jul 28, 2025
Merged

Add action to resolve Community Library Submissions#5178
AlexVelezLl merged 7 commits intolearningequality:community-channelsfrom
Jakoma02:resolve_submission_action

Conversation

@Jakoma02
Copy link
Contributor

@Jakoma02 Jakoma02 commented Jul 15, 2025

Summary

This PR extends the CommunityLibrarySubmission model with new internal fields, adds a new CommunityLibraryAdminViewSet that exposes these fields and introduces a resolve action for administrators.

Detailed changes:

  • Added date_resolved, resolved_by, resolved_reason, feedback_notes and internal_notes to the CommunityLibrarySubmission model
  • Exposed resolved_reason, feedback_notes and date_resolved in the user CommunityLibrarySubmissionViewset
  • Added a new Superseded submission state
  • Added resolution_reason_choices constants
  • Added CommunityLibrarySubmissionAdminViewSet extending CommunityLibrarySubmissionViewSet, also exposing resolved_by_id, resolved_by_first_name, resolved_by_last_name and internal_notes
  • Added resolve action with semantics as in the linked issue
  • Added CommunityLibrarySubmissionResolveSerializer for deserializing resolution requests, extending CommunityLibrarySubmissionSerializer
  • Added AdminViewSetTestCase testing the functionality

No manual testing besides designing the tests and making sure that they pass was performed.

References

Solves #5170.

This PR depends on unmerged changes from #5167, so it should only be merged after that PR is merged and should be rebased to incorporate any changes to that PR introduced during the review process.

Reviewer guidance

See the note above about the merging order. Otherwise, not any.

@Jakoma02 Jakoma02 force-pushed the resolve_submission_action branch from 02827cf to c6d2207 Compare July 17, 2025 14:11
Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Thanks @Jakoma02! This is looking great so far! I have left a couple of things I noted, mostly nitpicks, but the most important thing is to ensure the consistency in the returned value of the resolve action. Apart from that eveything looks good!

("contentcuration", "0156_communitylibrarysubmission_admin_fields"),
]

operations = [
Copy link
Member

Choose a reason for hiding this comment

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

I think we can collapse this migration with the previous one, as I see the only value that is being altered is the `blank=True)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are, of course, right. I incorrectly thought the previous migration was created in an earlier PR, so it was necessary to alter it. It definitely does not make sense to create and alter the field in the same PR. I fixed this in 9cddb63.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

Seems like we still need to remove this file, as its not needed anymore 😅

)
router.register(
r"communitylibrary_submission_admin",
CommunityLibrarySubmissionAdminViewSet,
Copy link
Member

Choose a reason for hiding this comment

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

Just for consistency with other admin viewsets, I think it may be a better idea to name this viewset AdminCommunityLibrarySubmissionViewset and add admin as prefix of the route.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, I changed this in 9cddb63. I am not sure if underscores or dashes should be preferred because I see both in the codebase, I leave underscores for now because that is what was approved for the user-facing community library viewset.

Copy link
Member

Choose a reason for hiding this comment

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

I dont think there is a preferred way, I think thats okay :)

return super().update(instance, validated_data)


def timezoned_datetime_now():
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, did you tried using django's timezone.now method instead? And then mock "django.utils.timezone.now" in the tests? Just gave it a quick try and seemed to be working okay

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was not aware of django.utils.timezone. Directly mocking that function works well, thanks! Changed in 9cddb63.

)

if (
"status" not in validated_data
Copy link
Member

Choose a reason for hiding this comment

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

(nitpick) If we reach to this point, we have already ensured that status is in validated_data thanks to the previous check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was aware that status will always be in the validated data after the first if statement, but I intentionally left it because it seemed easier for the reader to understand that this is indeed not accessing a potentially undefined value, and the performance impact is practically non-existent. But I have no strong feelings about this. We can leave the explicit check out if that seems better to you.

Copy link
Member

Choose a reason for hiding this comment

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

No strong feelings neither, if it was intentional, then its fine!

or validated_data["status"]
== community_library_submission_constants.STATUS_REJECTED
):
if "resolution_reason" not in validated_data:
Copy link
Member

Choose a reason for hiding this comment

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

Could we also check if "resolution_reason" and "feedback_notes" are not empty strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, I missed that possibility. Added in 9cddb63.

field_map = CommunityLibrarySubmissionViewSet.field_map.copy()
field_map.update(
{
"resolved_by_first_name": "resolved_by__first_name",
Copy link
Member

Choose a reason for hiding this comment

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

Now that I think more about this, I think it would be better to just map both first and last name to a resolved_by_name field, as this is what we will always show in the frontend. We can pass functions to field_map fields, just like this example. If you can also update the author field in the CommunityLibrarySubmissionViewSet that'd be nice :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed in 9cddb63.

if submission.status == community_library_submission_constants.STATUS_APPROVED:
self._mark_previous_pending_submissions_as_superseded(submission)

return Response(serializer.data)
Copy link
Member

Choose a reason for hiding this comment

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

Since we use serializers just for write-only changes, we don't set read-only values to the serializer, therefore, here we are not returning the fields date_resolved, resolved_by_id, etc. So here we can instead return the self.serialize_object() value to get the standard response taking into account the values viewset property

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not think of doing it this way, thanks for suggesting this! Changed in 9cddb63.

}
)

def create(self, request, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Here if we want to block all these requests, then it will be cleaner to create a Mixin that defines the common properties and methods we want to share between these two viewsets, and then have the CommunityLibrarySubmissionViewSet inherit from that mixin + the other REST operations. And have the CommunityLibrarySubmissionViewSet just inherit from the mixin and the ReadOnlyValuesViewset. That is a cleaner way to not override these methods if we don't want to allow them in the first place.

In Kolibri we have an example of this: https://github.com/learningequality/kolibri/blob/538fb63ddc7218d849e5fc7df1bf515d301eac4a/kolibri/core/auth/api.py#L542

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have thought about separating the common functionality and then reusing it in both viewsets, but it seemed to be a bit too complicated and confusing and it seemed simpler to just override the methods. But I can see your point that extracting the common pieces into a mixin is somewhat cleaner, I refactored it that way in 9cddb63.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks!

self.patcher.stop()
super().tearDown()

def _manually_resolve_submission(self):
Copy link
Member

Choose a reason for hiding this comment

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

We can name this helper to _manually_reject_submission, so that is clearer why we then check the status == REJECTED in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 9cddb63.

Comment on lines 90 to 95
r"communitylibrary-submission",
CommunityLibrarySubmissionViewSet,
basename="community-library-submission",
)
router.register(
r"admin_communitylibrary_submission",
Copy link
Member

Choose a reason for hiding this comment

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

Either if we use snake case or kebab case for these new routes, I think we should use the same case for both routes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, this was not intentional. Fixed in ff53d60.

if submission.status == community_library_submission_constants.STATUS_APPROVED:
self._mark_previous_pending_submissions_as_superseded(submission)

return Response(self.serialize_object(pk=submission.id))
Copy link
Member

Choose a reason for hiding this comment

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

(AFAIK 😅) In theory we shouldn't need to pass the pk argument to this method, it should behave the same as the get_object methods for detail=True actions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, I removed the explicit pk argument in 73ad4cb.

ReadOnlyValuesViewset,
):
filter_backends = [DjangoFilterBackend]
filterset_fields = ["channel"]
Copy link
Member

@AlexVelezLl AlexVelezLl Jul 21, 2025

Choose a reason for hiding this comment

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

The AdminCommunityLibrarySubmissionViewSet should also allow filtering by channels, and provide paginated responses, so I think we can make these fields part of the CommunityLibrarySubmissionViewSetMixin too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right that it makes sense to support the same filtering and pagination for the admin viewset as well, changed in 72bbef6.

Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Thanks @Jakoma02! We are almost there! Just one little cleanup! After this we can proceed with the merge :)

("contentcuration", "0156_communitylibrarysubmission_admin_fields"),
]

operations = [
Copy link
Member

Choose a reason for hiding this comment

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

Seems like we still need to remove this file, as its not needed anymore 😅

@Jakoma02
Copy link
Contributor Author

Seems like we still need to remove this file, as its not needed anymore 😅

You are of course right, done in 7a3c46e.

@Jakoma02 Jakoma02 requested a review from AlexVelezLl July 24, 2025 20:41
Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Thanks @Jakoma02! Great work! This looks good to me! Merging!

@AlexVelezLl AlexVelezLl merged commit 4863c93 into learningequality:community-channels Jul 28, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants