Skip to content

Commit 857c80c

Browse files
Added Token based auth views.
1 parent 9a35d39 commit 857c80c

30 files changed

+1156
-133
lines changed

account/admin.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,90 @@
11
from django.contrib import admin
2+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
23

3-
from account.models import User, Profile
4+
from .models import Profile, UserAccount
45

56

6-
@admin.register(User)
7-
class UserAdmin(admin.ModelAdmin):
8-
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active')
9-
list_filter = ('is_staff', 'is_active', 'date_joined')
10-
search_fields = ('username', 'email', 'first_name', 'last_name')
11-
ordering = ('date_joined',)
7+
# Extend the base UserAdmin to customize for UserAccount
8+
@admin.register(UserAccount)
9+
class UserAccountAdmin(BaseUserAdmin):
10+
"""
11+
Admin view for the UserAccount model with enhanced visibility and control over user details.
12+
"""
13+
# Fields to display in the user list
14+
list_display = ('id', 'email', 'username', 'is_active', 'is_staff', 'date_joined')
15+
list_filter = ('is_active', 'is_staff', 'is_manager', 'is_admin')
1216

17+
# Fields to enable search by email and username
18+
search_fields = ('email', 'username')
1319

20+
# Fields layout in the detail view
21+
fieldsets = (
22+
(None, {'fields': ('email', 'username', 'password')}),
23+
('Personal Info', {'fields': ('first_name', 'last_name', 'phone_number', 'bio', 'profile_image')}),
24+
('Permissions', {'fields': ('is_active', 'is_staff', 'is_manager', 'is_admin')}),
25+
('Important dates', {'fields': ('last_login', 'date_joined')}),
26+
('Advanced options', {
27+
'classes': ('collapse',),
28+
'fields': ('groups', 'user_permissions'),
29+
}),
30+
)
31+
32+
# Fields layout when creating a new user
33+
add_fieldsets = (
34+
(None, {
35+
'classes': ('wide',),
36+
'fields': (
37+
'email', 'username', 'first_name', 'last_name', 'phone_number', 'password1', 'password2', 'is_active',
38+
'is_staff', 'is_superuser'),
39+
}),
40+
)
41+
42+
readonly_fields = ('date_joined', 'last_login')
43+
44+
# Ensure email and username are used for logging in and user identification
45+
ordering = ('email',)
46+
47+
def has_add_permission(self, request):
48+
"""
49+
Control add permissions if needed.
50+
"""
51+
return True
52+
53+
def has_change_permission(self, request, obj=None):
54+
"""
55+
Allow edits only for staff members.
56+
"""
57+
if request.user.is_staff:
58+
return True
59+
return False
60+
61+
62+
# Register the ProfileAdmin as well
1463
@admin.register(Profile)
1564
class ProfileAdmin(admin.ModelAdmin):
16-
list_display = ('user', 'location', 'birth_date')
17-
search_fields = ('user__username', 'location')
18-
list_filter = ('birth_date',)
65+
"""
66+
Admin view for the Profile model with limited edit permissions.
67+
"""
68+
list_display = ('user', 'balance', 'date_created')
69+
search_fields = ('user__username', 'user__email') # Use 'username' and 'email' instead of 'name'
70+
71+
readonly_fields = ('balance', 'date_created') # Make balance and date_created read-only
72+
73+
def get_form(self, request, obj=None, **kwargs):
74+
"""Customize form to limit user choices and enforce creation-only permissions."""
75+
form = super().get_form(request, obj, **kwargs)
76+
if obj is None:
77+
# Restrict user choices to those without a profile only during creation
78+
form.base_fields['user'].queryset = UserAccount.objects.filter(profile__isnull=True)
79+
else:
80+
# Make the user field read-only when viewing an existing profile
81+
self.readonly_fields = ('user', 'balance', 'date_created')
82+
return form
83+
84+
def has_change_permission(self, request, obj=None):
85+
"""
86+
Allow profile creation but restrict editing after creation.
87+
"""
88+
if obj is not None:
89+
return False # Disable editing of existing profiles
90+
return True

account/emails.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from djoser.email import ActivationEmail
2+
3+
4+
class CustomActivationEmail(ActivationEmail):
5+
template_name = 'djoser/email/activation.html'
6+
7+
def get_context_data(self):
8+
# Call the base implementation first to get a context dictionary
9+
context = super().get_context_data()
10+
11+
# Ensure correct values are set for protocol and domain
12+
request = self.context.get('request')
13+
if request:
14+
protocol = 'https' if request.is_secure() else 'http'
15+
domain = request.get_host() # This will provide the correct domain, without protocol
16+
else:
17+
protocol = 'http' # Default fallback
18+
domain = '127.0.0.1:8000' # Replace with your actual domain for production
19+
20+
# Construct the activation URL properly
21+
uid = context.get('uid')
22+
token = context.get('token')
23+
activation_url = f"{protocol}://{domain}/activate/{uid}/{token}/"
24+
25+
context['activation_url'] = activation_url
26+
27+
return context

account/models.py

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,119 @@
1-
from django.contrib.auth.models import AbstractUser
1+
from django.conf import settings
2+
from django.contrib.auth.base_user import BaseUserManager
3+
from django.contrib.auth.models import PermissionsMixin, AbstractUser
4+
from django.core.validators import RegexValidator
25
from django.db import models
6+
from django.utils import timezone
7+
from django.utils.translation import gettext_lazy as _
8+
from simple_history.models import HistoricalRecords
39

410
from .utils import profile_upload_to_unique
511

612

7-
class User(AbstractUser):
8-
email = models.EmailField(unique=True)
13+
class Profile(models.Model):
14+
"""
15+
Model representing a user's profile.
16+
Connects to income, expenses, and allows for personal data storage.
17+
"""
18+
user = models.OneToOneField(
19+
settings.AUTH_USER_MODEL,
20+
on_delete=models.CASCADE,
21+
related_name="profile"
22+
)
23+
balance = models.DecimalField(
24+
max_digits=10,
25+
decimal_places=2,
26+
default=0.0,
27+
help_text="User's current balance."
28+
)
29+
date_created = models.DateTimeField(auto_now_add=True)
30+
profile_pic = models.ImageField(
31+
upload_to=profile_upload_to_unique,
32+
blank=True,
33+
null=True,
34+
help_text="User's profile picture."
35+
)
936

37+
# Add historical records field to track changes
38+
history = HistoricalRecords()
1039

11-
class Profile(models.Model):
12-
user = models.OneToOneField(User, on_delete=models.CASCADE)
13-
bio = models.TextField(blank=True)
14-
location = models.CharField(max_length=100, blank=True)
15-
birth_date = models.DateField(null=True, blank=True)
16-
picture = models.ImageField(upload_to=profile_upload_to_unique, null=True, blank=True)
40+
def __str__(self):
41+
"""String representation of the profile object, displaying the associated user's username."""
42+
return f'Profile of {self.user}'
1743

18-
class Meta:
19-
verbose_name = "Profile"
20-
verbose_name_plural = "Profiles"
21-
indexes = [
22-
models.Index(fields=['user']),
23-
]
24-
ordering = ['user']
44+
45+
# Custom User Manager
46+
class UserManager(BaseUserManager):
47+
def create_user(self, email, password=None, **extra_fields):
48+
if not email:
49+
raise ValueError(_('The Email field must be set'))
50+
email = self.normalize_email(email)
51+
user = self.model(email=email, **extra_fields)
52+
user.set_password(password)
53+
user.save(using=self._db)
54+
return user
55+
56+
def create_superuser(self, email, password=None, **extra_fields):
57+
extra_fields.setdefault('is_staff', True)
58+
extra_fields.setdefault('is_superuser', True)
59+
extra_fields.setdefault('is_active', True)
60+
61+
if extra_fields.get('is_staff') is not True:
62+
raise ValueError(_('Superuser must have is_staff=True.'))
63+
if extra_fields.get('is_superuser') is not True:
64+
raise ValueError(_('Superuser must have is_superuser=True.'))
65+
66+
return self.create_user(email, password, **extra_fields)
67+
68+
69+
# Custom User Model
70+
class UserAccount(AbstractUser, PermissionsMixin):
71+
email = models.EmailField(_('email address'), unique=True)
72+
username = models.CharField(_('username'), max_length=30, unique=True, blank=False,
73+
help_text="User's unique username",
74+
validators=[
75+
RegexValidator(
76+
regex=r'^[\w-]+$',
77+
message=_(
78+
"Username can only contain letters, numbers, underscores, or hyphens.")
79+
)
80+
]
81+
)
82+
phone_number = models.CharField(_('phone number'), max_length=15, unique=True, null=True, blank=True,
83+
validators=[
84+
RegexValidator(
85+
regex=r'^\+?1?\d{9,15}$',
86+
message=_(
87+
"Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed."
88+
),
89+
)
90+
], )
91+
first_name = models.CharField(_('first name'), max_length=30, blank=True)
92+
last_name = models.CharField(_('last name'), max_length=30, blank=True)
93+
profile_image = models.ImageField(_('profile image'), upload_to='profile_images/', null=True, blank=True)
94+
bio = models.TextField(_('bio'), max_length=500, blank=True)
95+
last_login_ip = models.GenericIPAddressField(_('last login IP'), null=True, blank=True)
96+
last_login = models.DateTimeField(_('last login'), auto_now=True)
97+
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
98+
99+
is_active = models.BooleanField(_('active'), default=True)
100+
is_staff = models.BooleanField(_('staff status'), default=False)
101+
is_manager = models.BooleanField(_('manager status'), default=False)
102+
is_admin = models.BooleanField(_('admin status'), default=False)
103+
104+
objects = UserManager()
105+
106+
USERNAME_FIELD = 'email'
107+
REQUIRED_FIELDS = ['username', 'phone_number', 'first_name', 'last_name']
108+
109+
@property
110+
def name(self):
111+
return f'{self.first_name} {self.last_name}'
25112

26113
def __str__(self):
27-
return f'{self.user.username} Profile'
114+
return self.username or self.email
115+
116+
class Meta:
117+
verbose_name = _('user')
118+
verbose_name_plural = _('users')
119+
ordering = ['-pk']

account/permissions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from rest_framework.permissions import BasePermission, SAFE_METHODS
2+
3+
4+
class IsOwnerOrAdmin(BasePermission):
5+
"""
6+
Custom permission that allows access only to the owner of the profile or an admin.
7+
"""
8+
9+
def has_permission(self, request, view):
10+
# Allow access if the user is authenticated
11+
return request.user and request.user.is_authenticated
12+
13+
def has_object_permission(self, request, view, obj):
14+
# Allow read access, or write access only if user is owner or admin
15+
if request.method in SAFE_METHODS:
16+
return True
17+
return obj.user == request.user or request.user.is_staff
18+
19+
20+
class IsNotAuthenticated(BasePermission):
21+
"""
22+
Custom permission that allows access only to non-authenticated users.
23+
"""
24+
25+
def has_permission(self, request, view):
26+
return not request.user or not request.user.is_authenticated

0 commit comments

Comments
 (0)