Skip to content

register task to assign picture password#14565

Draft
ozer550 wants to merge 6 commits intolearningequality:developfrom
ozer550:add-api-support-for-picture-login
Draft

register task to assign picture password#14565
ozer550 wants to merge 6 commits intolearningequality:developfrom
ozer550:add-api-support-for-picture-login

Conversation

@ozer550
Copy link
Copy Markdown
Member

@ozer550 ozer550 commented Apr 9, 2026

Summary

WIP

References

Reviewer guidance

AI usage

@github-actions github-actions bot added DEV: backend Python, databases, networking, filesystem... APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) DEV: frontend SIZE: medium labels Apr 9, 2026
),
)
.annotate(
picture_passwords_exhausted=Case(
Copy link
Copy Markdown
Member Author

@ozer550 ozer550 Apr 13, 2026

Choose a reason for hiding this comment

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

How do we incorporate this in the viewset action as the annotated field will only be available through api? Do we want to add a separate field in the model itself?

@ozer550 ozer550 requested a review from rtibblesbot April 13, 2026 11:48
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

CI passing. No screenshots in the PR body and the dev server wasn't running locally, so UI was not visually verified — please add screenshots before requesting final review. PR description (summary, references, reviewer guidance, AI usage) is still template placeholder text; AI usage is marked REQUIRED in the template.

  • suggestion × 4: see inline comments
  • praise × 1: see inline comment

@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Reviewed the pull request diff checking for:

  • Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
  • Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
  • Architecture: duplicated concerns, minimal interfaces, composition over inheritance
  • Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
  • Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
  • Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
  • Checked CI status and linked issue acceptance criteria
  • For UI changes: inspected screenshots for layout, visual completeness, and consistency

method: 'POST',
data,
});
if (response.data.id && response.data.status) {
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.

suggestion: Distinguishing a task response from a dataset response by checking response.data.id && response.data.status works today because FacilityDatasetSerializer has no status field, but it's implicitly relying on that absence. The endpoint returns HTTP 202 for the task case and 200 otherwise, so response.status === 202 is a more explicit and stable check.

createSnackbar(saveSuccess$());
}
} catch (error) {
createSnackbar(saveFailure$());
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.

suggestion: saveFacilityConfig and saveFacilityLoginSettings each call copySettings() internally. When run in Promise.all, if saveFacilityLoginSettings completes first (calling copySettings() with the pending settings as the new baseline), and saveFacilityConfig then fails, the undoSettingsChange() call here restores settings to the just-copied pending state — effectively a no-op. The user sees the failure snackbar but the UI keeps the unsaved values.

One fix: remove the copySettings() calls from inside both functions and call it once in saveConfig() only after Promise.all resolves successfully.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This might be motivation for moving this task wrangling into the composable, so there's a single save function, but I don't think this is high priority

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.

Understood — a composable-level single save function is the cleaner fix. Makes sense to defer it.

@@ -1,4 +1,4 @@
setuptools
setuptools<=81.0.0
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.

suggestion: Pinning setuptools<=81.0.0 is unrelated to picture login. Please submit this in a separate PR with a note explaining what breakage it prevents.

if job:
job.update_progress(0, total)

for i, learner in enumerate(learners.iterator(), start=1):
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.

praise: Using .iterator() here is the right call for large facilities — avoids loading all learner rows into memory at once. The per-user progress updates and the metadata annotation before re-raising NoAvailableSequences make failure diagnosis straightforward.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feel free to generalize the learner count utility so you can access the queryset for use here too.

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.

Noted — exposing the queryset from the learner count utility would let this code use it directly rather than doing a separate lookup.

@ozer550 ozer550 force-pushed the add-api-support-for-picture-login branch from bc776c9 to b2f369f Compare April 13, 2026 17:44
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

Prior findings

Resolved:

  • requirements/dev.txt:1 — unrelated setuptools pinning (suggestion) — file removed from PR diff

Unaddressed (re-raised below):

  • api.py:295permission_classes=[IsAuthenticated] override on save_facility_login_settings (suggestion)
  • useFacilityEditor.js:229response.data.id && response.data.status as task discriminant (suggestion)
  • index.vue:492copySettings() race when both Promise.all legs call it (suggestion)
  • PR description and screenshots still placeholder/absent (blocking)

1/5 prior findings resolved. 4 re-raised below.


The rebase commit cleanly delegates exhaustion-checking to are_picture_passwords_exhausted and drops the picture_passwords_exhausted FacilityViewSet annotation — both positive simplifications (inline praise on api.py:318). Still no screenshots and the PR description remains template placeholder text; the prior review's blocking request stands.

blocking:

  • PR description still contains template placeholder text ("WIP…"). Summary, references, reviewer guidance, and AI usage sections are all required before final review.
  • No screenshots added for the Vue/UI changes — visual inspection is not possible without them.

suggestions (re-raised): see inline comments on api.py:295, useFacilityEditor.js:229, index.vue:492

new: see inline comments on api.py:318 (praise), index.vue:159 (suggestion)


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria

await saveFacilityConfig();
createSnackbar(saveSuccess$());
pictureLoginTaskLoading.value = true;
await Promise.all([saveFacilityConfig(), saveFacilityLoginSettings()]);
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.

suggestion (re-raised): Both saveFacilityConfig() (line 192 of useFacilityEditor.js) and saveFacilityLoginSettings() (line 232) call copySettings() internally. When run in Promise.all, whichever resolves first calls copySettings() — updating the baseline to the current pending state. If the other then fails, the undoSettingsChange() in the catch block restores to that just-copied pending state, making the undo a no-op. One fix: remove the copySettings() calls from inside both functions and call it once in saveConfig() only after Promise.all resolves successfully.

class="nested-settings picture-password-assignment-status"
data-testid="picture_password_assignment_status"
>
<KCircularLoader :size="24" />
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.

suggestion: The KCircularLoader is shown without any visible or accessible label while picture passwords are being assigned. Screen-reader users won't get feedback about why the save button is disabled. Consider adding a visually-hidden string (e.g., via $computedClass with a screen-reader-only utility) or an aria-live region announcing the progress state.

Copy link
Copy Markdown
Member

@bjester bjester left a comment

Choose a reason for hiding this comment

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

I'd like to see a test on the permissions handling, and if the permissions handling could be cleaned up.

Out of scope: the existing code that was used as an example for some of this

@patch("kolibri.core.auth.api.get_assigned_sequences", return_value=set())
@patch("kolibri.core.auth.api.PICTURE_PASSWORD_SEQUENCE_COUNT", new=1)
def test_enable_rejected_when_exhausted(self, mock_assigned, mock_task):
mock_assigned.return_value = {"1.2.3"}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick: is this overriding the default return_value=set() in the decorator above?

Comment on lines +298 to +306
try:
dataset = FacilityDataset.objects.get(pk=pk)
except FacilityDataset.DoesNotExist:
raise Http404("Facility not found")

if not request.user.can_update(dataset):
raise PermissionDenied("You do not have permission to update this facility")

facility = Facility.objects.get(dataset_id=pk)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I realize the other existing actions on this viewset do this, but the reason for the permission checks might be a side effect of this.

If this code uses self.get_object, that might allow the permissions check to function properly and would likely clean this up, and the other actions, a lot. I think you could focus on trying it on your new code for now, and if it works, we can write a followup issue to address it on the other actions. A unit test to verify the permissions would provide certainty!

You might have to use a query param of facility_id instead of the PK, for it to correctly find the dataset. They should always be 1:1

if job:
job.update_progress(0, total)

for i, learner in enumerate(learners.iterator(), start=1):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feel free to generalize the learner count utility so you can access the queryset for use here too.

createSnackbar(saveSuccess$());
}
} catch (error) {
createSnackbar(saveFailure$());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This might be motivation for moving this task wrangling into the composable, so there's a single save function, but I don't think this is high priority

Copy link
Copy Markdown
Member

@LianaHarris360 LianaHarris360 left a comment

Choose a reason for hiding this comment

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

The task works as expected, including when the settings are updated. There's a few areas on the frontend that could be improved.

class="nested-settings picture-password-assignment-status"
data-testid="picture_password_assignment_status"
>
<KCircularLoader :size="24" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The KCircularLoader should be moved next to the save button instead of being in a separate div inside the KRadioButtonGroup. This way, it's clearer to the user that the spinner is related to saving the settings.

Keep in mind that since there are two save buttons based on isAppContext, there should be two loading spinners as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Facility Re: Facility App (user/class management, facility settings, csv import/export, etc.) DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants