Skip to content

Commit 95efdf7

Browse files
committed
[fix] Fixes by @coderabbitai
1 parent 0adc1da commit 95efdf7

15 files changed

Lines changed: 148 additions & 56 deletions

File tree

openwisp_radius/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django import forms
44
from django.conf import settings
55
from django.contrib import admin, messages
6-
from django.contrib.admin import ModelAdmin, StackedInline, TabularInline
6+
from django.contrib.admin import ModelAdmin, StackedInline
77
from django.contrib.admin.utils import model_ngettext
88
from django.contrib.auth import get_user_model
99
from django.core.exceptions import PermissionDenied
@@ -534,7 +534,7 @@ def has_change_permission(self, request, obj=None):
534534
return False
535535

536536

537-
class RegisteredUserInline(TabularInline):
537+
class RegisteredUserInline(StackedInline):
538538
model = RegisteredUser
539539
form = AlwaysHasChangedForm
540540
extra = 0

openwisp_radius/api/serializers.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -793,17 +793,26 @@ class Meta:
793793
]
794794

795795
def _get_registered_user(self, obj):
796-
view = self.context.get("view")
797-
organization = getattr(view, "organization", None)
798-
org_reg_user = None
799-
global_reg_user = None
800-
for ru in obj.registered_users.all():
801-
if organization and ru.organization_id == organization.pk:
802-
org_reg_user = ru
803-
break
804-
elif ru.organization_id is None:
805-
global_reg_user = ru
806-
return org_reg_user or global_reg_user
796+
if not hasattr(self, "_registered_user_cache"):
797+
self._registered_user_cache = {}
798+
if obj.pk not in self._registered_user_cache:
799+
view = self.context.get("view")
800+
organization = getattr(view, "organization", None)
801+
org_reg_user = None
802+
global_reg_user = None
803+
# We iterate over .all() instead of using .filter() because callers
804+
# of this serializer (e.g. validate_auth_token) prefetch
805+
# "registered_users" via prefetch_related. Using .all() hits the
806+
# in-memory prefetch cache (0 DB queries), whereas .filter() would
807+
# bypass the cache and issue a new query every time.
808+
for ru in obj.registered_users.all():
809+
if organization and ru.organization_id == organization.pk:
810+
org_reg_user = ru
811+
break
812+
elif ru.organization_id is None:
813+
global_reg_user = ru
814+
self._registered_user_cache[obj.pk] = org_reg_user or global_reg_user
815+
return self._registered_user_cache[obj.pk]
807816

808817
def get_is_verified(self, obj):
809818
reg_user = self._get_registered_user(obj)

openwisp_radius/api/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
Organization = load_model("openwisp_users", "Organization")
1111
OrganizationRadiusSettings = load_model("openwisp_radius", "OrganizationRadiusSettings")
12+
RegisteredUser = load_model("openwisp_radius", "RegisteredUser")
1213

1314

1415
class ErrorDictMixin(object):
@@ -33,6 +34,8 @@ def _needs_identity_verification(self, organization_filter_kwargs={}, org=None):
3334
def is_identity_verified_strong(self, user, organization=None):
3435
reg_user = None
3536
global_reg_user = None
37+
# We use all() to utilize the prefetch cache, otherwise
38+
# it would cause an additional query to fetch the registered user
3639
for ru in user.registered_users.all():
3740
if organization and ru.organization_id == organization.pk:
3841
reg_user = ru

openwisp_radius/api/views.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ def create(self, *args, **kwargs):
645645
try:
646646
phone_token.full_clean()
647647
if kwargs.get("enforce_unverified", True):
648-
phone_token._validate_already_verified()
648+
phone_token._validate_already_verified(organization=self.organization)
649649
except ValidationError as e:
650650
error_dict = self._get_error_dict(e)
651651
raise serializers.ValidationError(error_dict)
@@ -754,7 +754,9 @@ def post(self, request, *args, **kwargs):
754754
_("No verification code found in the system for this user.")
755755
)
756756
try:
757-
is_valid = phone_token.is_valid(serializer.data["code"])
757+
is_valid = phone_token.is_valid(
758+
serializer.data["code"], organization=self.organization
759+
)
758760
except PhoneTokenException as e:
759761
return self._error_response(str(e))
760762
if not is_valid:
@@ -763,11 +765,13 @@ def post(self, request, *args, **kwargs):
763765
reg_user, __ = RegisteredUser.get_or_create_for_user_and_org(
764766
user=user,
765767
organization=self.organization,
766-
defaults={"is_verified": False, "method": ""},
768+
defaults={
769+
"is_verified": True,
770+
"method": "mobile_phone",
771+
"is_active": True,
772+
},
767773
)
768774
reg_user.is_verified = True
769-
reg_user.method = "mobile_phone"
770-
user.is_active = True
771775
# Update username if phone_number is used as username
772776
if user.username == user.phone_number:
773777
user.username = phone_token.phone_number

openwisp_radius/base/models.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,14 +1058,20 @@ def save_user(self, user):
10581058
OrganizationUser = swapper.load_model("openwisp_users", "OrganizationUser")
10591059
RegisteredUser = swapper.load_model("openwisp_radius", "RegisteredUser")
10601060
user.save()
1061-
registered_user = RegisteredUser(
1061+
registered_user, created = RegisteredUser.get_or_create_for_user_and_org(
10621062
user=user,
1063-
method="manual",
10641063
organization=self.organization,
1064+
defaults={
1065+
"method": "manual",
1066+
"is_verified": self.organization.radius_settings.needs_identity_verification,
1067+
},
10651068
)
1066-
if self.organization.radius_settings.needs_identity_verification:
1069+
if (
1070+
not created
1071+
and self.organization.radius_settings.needs_identity_verification
1072+
):
10671073
registered_user.is_verified = True
1068-
registered_user.save()
1074+
registered_user.save()
10691075
self.users.add(user)
10701076
if OrganizationUser.objects.filter(
10711077
user=user, organization=self.organization
@@ -1563,26 +1569,35 @@ def send_token(self):
15631569
)
15641570
sms_message.send(meta_data=org_radius_settings.sms_meta_data)
15651571

1566-
def is_valid(self, token):
1572+
def is_valid(self, token, organization=None):
15671573
self.attempts += 1
15681574
try:
1569-
self.verified = self.__check(token)
1575+
self.verified = self.__check(token, organization=organization)
15701576
except exceptions.PhoneTokenException as phone_error:
15711577
self.save()
15721578
raise phone_error
15731579
self.save()
15741580
return self.verified
15751581

1576-
def _validate_already_verified(self):
1582+
def _validate_already_verified(self, organization=None):
15771583
RegisteredUser = swapper.load_model("openwisp_radius", "RegisteredUser")
1578-
if RegisteredUser.objects.filter(user=self.user, is_verified=True).exists():
1584+
if organization is not None:
1585+
reg_user = RegisteredUser.get_global_or_org_specific(
1586+
self.user, organization
1587+
)
1588+
is_verified = reg_user is not None and reg_user.is_verified
1589+
else:
1590+
is_verified = RegisteredUser.objects.filter(
1591+
user=self.user, is_verified=True
1592+
).exists()
1593+
if is_verified:
15791594
logger.warning(f"User {self.user.pk} is already verified")
15801595
raise exceptions.UserAlreadyVerified(
15811596
_("This user has been already verified.")
15821597
)
15831598

1584-
def __check(self, token):
1585-
self._validate_already_verified()
1599+
def __check(self, token, organization=None):
1600+
self._validate_already_verified(organization=organization)
15861601
if self.attempts > app_settings.SMS_TOKEN_MAX_ATTEMPTS:
15871602
logger.warning(
15881603
f"User {self.user} has reached the max "
@@ -1613,6 +1628,7 @@ class AbstractRegisteredUser(UUIDModel):
16131628
organization = models.ForeignKey(
16141629
swapper.get_model_name("openwisp_users", "Organization"),
16151630
on_delete=models.CASCADE,
1631+
related_name="registered_users",
16161632
null=True,
16171633
blank=True,
16181634
verbose_name=_("organization"),
@@ -1684,13 +1700,6 @@ def clean(self):
16841700
_("A registration record already exists for this user/organization.")
16851701
)
16861702

1687-
@classmethod
1688-
def get_for_user_and_org(cls, user, organization):
1689-
try:
1690-
return cls.objects.get(user=user, organization=organization)
1691-
except cls.DoesNotExist:
1692-
return None
1693-
16941703
@classmethod
16951704
def get_or_create_for_user_and_org(cls, user, organization, defaults=None):
16961705
defaults = defaults or {}

openwisp_radius/integrations/monitoring/tasks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def post_save_radiusaccounting(
186186
RegisteredUser.objects.only("method")
187187
.filter(user__username=username)
188188
.filter(Q(organization_id=organization_id) | Q(organization__isnull=True))
189+
.order_by("-organization_id")
189190
.first()
190191
)
191192
if registration_method is None:

openwisp_radius/management/commands/base/delete_unverified_users.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.contrib.auth import get_user_model
44
from django.core.management import BaseCommand
5+
from django.db.models import Count, Q
56
from django.utils.timezone import now
67

78
from openwisp_radius.utils import load_model
@@ -33,12 +34,21 @@ def handle(self, *args, **options):
3334
if exclude_methods:
3435
exclude_methods = exclude_methods.split(",")
3536

36-
qs = User.objects.filter(
37-
date_joined__lt=days,
38-
registered_users__isnull=False,
39-
registered_users__is_verified=False,
40-
is_staff=False,
41-
).distinct()
37+
qs = (
38+
User.objects.filter(
39+
date_joined__lt=days,
40+
registered_users__isnull=False,
41+
is_staff=False,
42+
)
43+
.annotate(
44+
num_verified=Count(
45+
"registered_users",
46+
filter=Q(registered_users__is_verified=True),
47+
)
48+
)
49+
.filter(num_verified=0)
50+
.distinct()
51+
)
4252
if exclude_methods:
4353
qs = qs.exclude(registered_users__method__in=exclude_methods)
4454

openwisp_radius/migrations/0043_registereduser_add_uuid.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def copy_registered_users_reverse(apps, schema_editor):
3131
class Migration(migrations.Migration):
3232
dependencies = [
3333
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
34+
swapper.dependency("openwisp_users", "Organization"),
3435
("openwisp_radius", "0042_set_existing_batches_completed"),
3536
]
3637

@@ -63,6 +64,7 @@ class Migration(migrations.Migration):
6364
blank=True,
6465
help_text=REGISTERED_USER_ORGANIZATION_HELP_TEXT,
6566
null=True,
67+
related_name="registered_users",
6668
on_delete=django.db.models.deletion.CASCADE,
6769
to=swapper.get_model_name("openwisp_users", "Organization"),
6870
verbose_name="organization",

openwisp_radius/migrations/__init__.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.conf import settings
66
from django.contrib.auth.management import create_permissions
77
from django.contrib.auth.models import Permission
8+
from django.db.models import Case, IntegerField, Value, When
89

910
from ..utils import create_default_groups
1011

@@ -88,9 +89,18 @@ def copy_registered_users_ctcr_reverse(
8889

8990
restored_objects = []
9091
previous_user_id = None
91-
queryset = RegisteredUserNew.objects.order_by(
92-
"user_id", "-is_verified", "method", "pk"
92+
# Annotate each row with an explicit verification priority so that stronger
93+
# methods (anything that is not '' or 'email') sort before weaker ones.
94+
# Lexical ordering of 'method' would place '' first, picking the weakest.
95+
method_priority = Case(
96+
When(method="", then=Value(0)),
97+
When(method="email", then=Value(1)),
98+
default=Value(2),
99+
output_field=IntegerField(),
93100
)
101+
queryset = RegisteredUserNew.objects.annotate(
102+
method_priority=method_priority
103+
).order_by("user_id", "-is_verified", "-method_priority", "pk")
94104
for registered_user in queryset.iterator(chunk_size=BATCH_SIZE):
95105
if registered_user.user_id == previous_user_id:
96106
continue
@@ -187,10 +197,23 @@ def migrate_registered_users_multitenant_reverse(
187197
organization__isnull=True,
188198
).values_list("user_id", flat=True)
189199
)
190-
org_records = RegisteredUser.objects.filter(
191-
user_id__in=user_id_batch,
192-
organization__isnull=False,
193-
).order_by("user_id", "-is_verified", "method", "pk")
200+
# Annotate each row with an explicit verification priority so that stronger
201+
# methods (anything that is not '' or 'email') sort before weaker ones.
202+
# Lexical ordering of 'method' would place '' first, picking the weakest.
203+
method_priority = Case(
204+
When(method="", then=Value(0)),
205+
When(method="email", then=Value(1)),
206+
default=Value(2),
207+
output_field=IntegerField(),
208+
)
209+
org_records = (
210+
RegisteredUser.objects.filter(
211+
user_id__in=user_id_batch,
212+
organization__isnull=False,
213+
)
214+
.annotate(method_priority=method_priority)
215+
.order_by("user_id", "-is_verified", "-method_priority", "pk")
216+
)
194217

195218
to_create = []
196219
to_delete_pks = []

openwisp_radius/tests/test_api/test_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ def test_radius_user_serializer(self):
342342
data = RadiusUserSerializer(user, context={"view": view}).data
343343

344344
with self.subTest("test full data"):
345+
registered_user = user.registered_users.get(organization=self.default_org)
345346
self.assertEqual(
346347
data,
347348
{
@@ -353,9 +354,9 @@ def test_radius_user_serializer(self):
353354
"birth_date": user.birth_date,
354355
"location": user.location,
355356
"is_active": user.is_active,
356-
"is_verified": user.registered_users.first().is_verified,
357357
"password_expired": user.has_password_expired(),
358-
"method": user.registered_users.first().method,
358+
"is_verified": registered_user.is_verified,
359+
"method": registered_user.method,
359360
"radius_user_token": user.radius_token.key,
360361
},
361362
)

0 commit comments

Comments
 (0)