Skip to content

Commit 4725c35

Browse files
committed
feat: implement robust referral system and fix User relations
Models: - Refactored InviteUser: changed to OneToOneField and enforced unique=True on . - Implemented auto-generation of unique 6-character invite codes in InviteUser.save(). - Added post_save signal to auto-create InviteUser profiles for new users. - Fixed in User model: changed on_delete to SET_NULL, removed invalid default values, and fixed lazy loading references. - Removed incompatible override in UserManager. Serializers: - Updated UserRegistrationSerializer and GoogleRegister to handle invite codes via SlugRelatedField. - Wrapped user creation methods in @transaction.atomic for data integrity. - Implemented race-condition-safe increment for using F() expressions upon successful registration.
1 parent 5a6ea7f commit 4725c35

4 files changed

Lines changed: 92 additions & 28 deletions

File tree

backend/NextVibeAPI/user/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.contrib import admin
2-
from .models import User, HistorySearch, Notification, UserOnlineSession
2+
from .models import User, HistorySearch, Notification, UserOnlineSession, InviteUser
33
from django.contrib import admin
44

55
@admin.register(User)
@@ -16,3 +16,4 @@ def get_queryset(self, request):
1616
admin.site.register(HistorySearch)
1717
admin.site.register(Notification)
1818
admin.site.register(UserOnlineSession)
19+
admin.site.register(InviteUser)

backend/NextVibeAPI/user/models.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from django.db import models
33
from django.utils import timezone
44
from datetime import timedelta
5+
from django.utils.crypto import get_random_string
6+
from django.db.models.signals import post_save
7+
from django.dispatch import receiver
58

69
class UserQuerySet(models.QuerySet):
710
def visible(self):
@@ -26,9 +29,6 @@ def create_superuser(self, email, username, password=None, **extra_fields):
2629

2730
return self.create_user(email, username, password, **extra_fields)
2831

29-
def get_by_natural_key(self, email):
30-
return self.get(email=email)
31-
3232

3333
class User(AbstractBaseUser):
3434
user_id = models.AutoField(primary_key=True, unique=True)
@@ -58,6 +58,7 @@ class User(AbstractBaseUser):
5858
last_activity = models.DateTimeField(default=timezone.now)
5959
wallet_address = models.CharField(max_length=50, blank=True, null=True)
6060
expo_push_token = models.CharField(max_length=100, blank=True, null=True)
61+
from_invite_code = models.ForeignKey("InviteUser", on_delete=models.SET_NULL, null=True, blank=True, related_name="invited_users")
6162

6263
USERNAME_FIELD = 'email'
6364
REQUIRED_FIELDS = ['username']
@@ -73,6 +74,26 @@ def has_perm(self, perm, obj=None):
7374
def has_module_perms(self, app_label):
7475
return self.is_superuser
7576

77+
class InviteUser(models.Model):
78+
owner = models.OneToOneField(User, on_delete=models.CASCADE)
79+
invite_code = models.CharField(max_length=6, unique=True)
80+
invited_count = models.IntegerField(default=0, null=True, blank=True)
81+
82+
def save(self, *args, **kwargs):
83+
# If code not created
84+
if not self.invite_code:
85+
# Generate str with 6 length
86+
new_code = get_random_string(length=6)
87+
88+
while InviteUser.objects.filter(invite_code=new_code).exists():
89+
new_code = get_random_string(length=6)
90+
91+
self.invite_code = new_code
92+
93+
super().save(*args, **kwargs)
94+
95+
def __str__(self):
96+
return f"Invite profile of {self.owner} ({self.invite_code}) have {self.invited_count} refs"
7697

7798
class HistorySearch(models.Model):
7899
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_history")
@@ -139,8 +160,10 @@ def session_duration(self) -> timedelta:
139160
return end_time - self.connected_at
140161

141162

142-
143-
144-
145-
146-
163+
@receiver(post_save, sender=User)
164+
def create_user_invite_profile(sender, instance, created, **kwargs):
165+
"""
166+
Thats signal works every time, when save User model
167+
"""
168+
if created:
169+
InviteUser.objects.get_or_create(owner=instance)

backend/NextVibeAPI/user/serializers_pac/google_registration.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,26 @@
44
import requests
55
from django.core.files.base import ContentFile
66
import uuid
7+
from django.db import transaction
8+
from django.db.models import F
9+
from users.models import InviteUser
710

811
User = get_user_model()
912

10-
1113
class GoogleRegister(serializers.ModelSerializer):
1214
token = serializers.SerializerMethodField()
1315
avatar_url = serializers.URLField(write_only=True, required=False)
16+
17+
from_invite_code = serializers.SlugRelatedField(
18+
slug_field='invite_code',
19+
queryset=InviteUser.objects.all(),
20+
required=False,
21+
allow_null=True
22+
)
1423

1524
class Meta:
1625
model = User
17-
fields = ("user_id", "email", "username", "token", "avatar", "avatar_url")
26+
fields = ("user_id", "email", "username", "token", "avatar", "avatar_url", "from_invite_code")
1827

1928
def get_token(self, obj):
2029
refresh = RefreshToken.for_user(obj)
@@ -23,18 +32,32 @@ def get_token(self, obj):
2332
"access": str(refresh.access_token),
2433
}
2534

35+
@transaction.atomic
2636
def create(self, validated_data):
2737
avatar_url = validated_data.pop("avatar_url", None)
28-
user, created = User.objects.get_or_create(email=validated_data["email"], defaults={"username": validated_data["username"]})
29-
30-
if created and avatar_url:
31-
try:
32-
response = requests.get(avatar_url, timeout=5)
33-
response.raise_for_status()
34-
file_name = f"user_{user.user_id}_{uuid.uuid4().hex}.jpg"
35-
user.avatar.save(file_name, ContentFile(response.content), save=True)
36-
except requests.RequestException:
37-
pass
38-
39-
return user
40-
38+
invite_obj = validated_data.pop("from_invite_code", None)
39+
40+
defaults = {"username": validated_data["username"]}
41+
if invite_obj:
42+
defaults["from_invite_code"] = invite_obj
43+
44+
user, created = User.objects.get_or_create(
45+
email=validated_data["email"],
46+
defaults=defaults
47+
)
48+
49+
if created:
50+
if avatar_url:
51+
try:
52+
response = requests.get(avatar_url, timeout=5)
53+
response.raise_for_status()
54+
file_name = f"user_{user.user_id}_{uuid.uuid4().hex}.jpg"
55+
user.avatar.save(file_name, ContentFile(response.content), save=True)
56+
except requests.RequestException:
57+
pass
58+
59+
if invite_obj:
60+
invite_obj.invited_count = F('invited_count') + 1
61+
invite_obj.save(update_fields=['invited_count'])
62+
63+
return user

backend/NextVibeAPI/user/serializers_pac/registration.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
from rest_framework import serializers
22
from django.contrib.auth import get_user_model
33
from rest_framework_simplejwt.tokens import RefreshToken
4-
5-
4+
from user.models import InviteUser
5+
from django.db import transaction
66

77
class UserRegistrationSerializer(serializers.ModelSerializer):
88
avatar_url = serializers.SerializerMethodField()
99
password = serializers.CharField(write_only=True)
1010
token = serializers.SerializerMethodField()
11+
12+
from_invite_code = serializers.SlugRelatedField(
13+
slug_field='invite_code',
14+
queryset=InviteUser.objects.all(),
15+
required=True,
16+
allow_null=False
17+
)
1118
class Meta:
12-
fields = ("user_id", "email", "username", "password", "token", "avatar_url")
19+
fields = ("user_id", "email", "username", "password", "token", "avatar_url", "from_invite_code")
1320
model = get_user_model()
1421
extra_kwargs = {
1522
'password': {'write_only': True},
1623
'email': {'required': True},
1724
'username': {'required': True},
25+
'from_invite_code': {'required': True},
1826
}
19-
27+
28+
@transaction.atomic
2029
def create(self, validated_data):
30+
invite_obj = validated_data.get('from_invite_code')
31+
2132
user = get_user_model()(
2233
email=validated_data['email'],
2334
username=validated_data['username'],
35+
from_invite_code=invite_obj
2436
)
2537
user.set_password(validated_data['password']) # Hash password
2638
user.save()
39+
40+
if invite_obj:
41+
invite_obj.invited_count += 1
42+
invite_obj.save(update_fields=['invited_count'])
43+
2744
return user
2845

2946
def get_token(self, obj):

0 commit comments

Comments
 (0)