From be2e9ab4222482672d81b402bbe9b8ea7934d0ae Mon Sep 17 00:00:00 2001 From: Vikrant Kumar Date: Wed, 30 Sep 2026 12:34:43 +0530 Subject: [PATCH] Fall back to the basic role when somebody holds no office MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit active_designation returned the first office held, and a basic role is not an office — so anyone holding only student, faculty or staff resolved to no role at all and was refused by every one of the 175 role_required endpoints. An account that also holds an office looked fine, which is why it reached production before it was caught. A role that is no longer held now falls back the same way rather than to nothing, and the two cases that were missing are tested: somebody holding only the basic role, and somebody holding nothing at all. --- FusionIIIT/applications/globals/decorators.py | 6 ++++- FusionIIIT/applications/globals/tests.py | 22 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/FusionIIIT/applications/globals/decorators.py b/FusionIIIT/applications/globals/decorators.py index 97230782b..b3716ff43 100644 --- a/FusionIIIT/applications/globals/decorators.py +++ b/FusionIIIT/applications/globals/decorators.py @@ -32,12 +32,16 @@ def active_designation(user): office outranks the basic role, which is the rule the IAM applies too — so somebody who has never touched the role switcher is not locked out of the post they hold. + + Falling through to the basic role matters: most people hold nothing else, + and treating "no office" as "no role" refuses every student their own + screens. """ held = held_designations(user) chosen = getattr(getattr(user, 'extrainfo', None), 'last_selected_role', None) if chosen and chosen.lower() in held: return chosen.lower() - return next(iter(sorted(held - BASIC_ROLES)), None) + return next(iter(sorted(held - BASIC_ROLES)), None) or next(iter(sorted(held)), None) def role_required(allowed_roles): diff --git a/FusionIIIT/applications/globals/tests.py b/FusionIIIT/applications/globals/tests.py index 6d05c60ca..5b407d606 100644 --- a/FusionIIIT/applications/globals/tests.py +++ b/FusionIIIT/applications/globals/tests.py @@ -98,11 +98,11 @@ def test_never_having_switched_does_not_lock_the_office_out(self): """The column is null for anyone who has not used the switcher.""" self.assertEqual(active_designation(self.user), 'acadadmin') - def test_a_role_that_is_no_longer_held_is_ignored(self): + def test_a_role_that_is_no_longer_held_falls_back_to_one_that_is(self): self._act_as('acadadmin') HoldsDesignation.objects.filter( designation__name='acadadmin').delete() - self.assertIsNone(active_designation(self.user)) + self.assertEqual(active_designation(self.user), 'student') def test_a_stand_in_is_authorised_and_the_absent_holder_is_not(self): """`working` is the occupant; `user` is the substantive holder.""" @@ -151,3 +151,21 @@ def test_an_old_token_is_refused_and_destroyed(self): with self.assertRaises(AuthenticationFailed): self.auth.authenticate_credentials(self.token.key) self.assertFalse(Token.objects.filter(pk=self.token.pk).exists()) + + +class PlainStudentTests(TestCase): + """Most people hold no office at all. They must keep their own screens.""" + + def setUp(self): + self.user = User.objects.create_user(username='student1') + ExtraInfo.objects.create(id='student1', user=self.user, user_type='student') + HoldsDesignation.objects.create( + user=self.user, working=self.user, + designation=Designation.objects.create(name='student')) + + def test_a_student_holding_only_the_basic_role_still_acts_in_it(self): + self.assertEqual(active_designation(self.user), 'student') + + def test_somebody_holding_nothing_acts_in_nothing(self): + bare = User.objects.create_user(username='nobody') + self.assertIsNone(active_designation(bare))