Skip to content

Commit

Permalink
feat: Add user authentication backend, admin customizations, and test…
Browse files Browse the repository at this point in the history
… authentication endpoint
  • Loading branch information
AhmedNassar7 committed Feb 28, 2025
1 parent 36ce8cb commit e5f3fb2
Show file tree
Hide file tree
Showing 21 changed files with 591 additions and 278 deletions.
4 changes: 2 additions & 2 deletions apps/authentication/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def send_reset_email(user, token):
recipient_list=[user.email],
html_message=html_message
)

logger.info(f"Password reset email sent to {user.email}")
return True

Expand Down Expand Up @@ -99,4 +99,4 @@ def reset_password(token, new_password):

except Exception as e:
logger.error(f"Error resetting password: {str(e)}")
return False
return False
6 changes: 3 additions & 3 deletions apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def post(self, request):
try:
user = User.objects.get(email=serializer.validated_data['email'])
token = PasswordResetService.create_reset_token(user)

if PasswordResetService.send_reset_email(user, token):
return Response(
{"message": "Password reset email sent successfully"},
Expand Down Expand Up @@ -66,7 +66,7 @@ def post(self, request):
if serializer.is_valid():
token = serializer.validated_data['token']
is_valid = PasswordResetService.validate_token(token)

return Response({"is_valid": is_valid}, status=status.HTTP_200_OK)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Expand Down Expand Up @@ -97,4 +97,4 @@ def post(self, request):
status=status.HTTP_400_BAD_REQUEST
)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
12 changes: 6 additions & 6 deletions apps/trains/management/commands/generate_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ def _generate_train_schedules(self, train, stations, start_time, hours):
"""Generate schedules for a train considering its direction."""
try:
current_time = start_time

# Get stations in the correct order based on train direction
ordered_stations = list(stations)
if train.direction == train.line.get_last_station().name:
ordered_stations = list(reversed(ordered_stations))

# Find current station index
current_index = ordered_stations.index(train.current_station)

# Generate schedules starting from current station
for hour in range(hours):
hour_start = start_time + timedelta(hours=hour)
Expand All @@ -85,20 +85,20 @@ def _generate_train_schedules(self, train, stations, start_time, hours):
# Generate schedules for remaining stations in direction
for idx in range(current_index, len(ordered_stations)):
station = ordered_stations[idx]

if idx == current_index:
arrival_time = current_time
else:
prev_station = ordered_stations[idx - 1]

# Calculate travel time
distance = prev_station.distance_to(station)
speed = AVERAGE_SPEEDS['PEAK'] if self._is_peak_hour() else AVERAGE_SPEEDS['NORMAL']
travel_time = (distance / speed) * 3600 # Convert to seconds

# Add dwell time
dwell_time = self._get_dwell_time(prev_station)

# Calculate arrival time
arrival_time = current_time + timedelta(seconds=int(travel_time + dwell_time))

Expand Down
6 changes: 3 additions & 3 deletions apps/trains/management/commands/initialize_trains.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ def _create_line_trains(self, line, stations, ac_trains, total_trains, station_s
try:
station_count = stations.count()
trains_created = 0

# Create trains for each station (both directions)
for station_index in range(station_count):
station = stations[station_index]

# Create trains in both directions at each station
for direction_index in range(2): # 0: forward, 1: backward
is_ac = trains_created < ac_trains

# Determine next station based on direction
if direction_index == 0: # Forward direction
next_station = stations[station_index + 1] if station_index < station_count - 1 else stations[0]
Expand Down
2 changes: 2 additions & 0 deletions apps/users/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# apps/users/__init__.py
default_app_config = 'apps.users.apps.UsersConfig'
13 changes: 13 additions & 0 deletions apps/users/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from .user_admin import UserAdmin

User = get_user_model()

# Register the User model with the custom admin
admin.site.register(User, UserAdmin)

# Admin site customization
admin.site.site_header = "Egypt Metro Administration"
admin.site.site_title = "Egypt Metro Admin Portal"
admin.site.index_title = "Welcome to Egypt Metro Admin Portal"
172 changes: 50 additions & 122 deletions apps/users/admin/user_admin.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
# apps/users/admin/user_admin.py
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib import admin
from django.utils.html import format_html
from django.urls import path
from django.shortcuts import render
from ..models import User
from .filters import SubscriptionTypeFilter, UserActivityFilter
from .actions import make_premium, deactivate_users
from ..constants.choices import SubscriptionType


@admin.register(User)
class UserAdmin(admin.ModelAdmin):
# Display configuration
class UserAdmin(BaseUserAdmin):
"""Enhanced UserAdmin with custom functionality"""

list_display = (
'username',
'email',
'full_name',
'get_full_name',
'subscription_badge',
'balance_display',
'last_login_display',
'status_badge',
'account_actions',
'is_active',
)

# Filtering and searching
list_filter = (
SubscriptionTypeFilter,
UserActivityFilter,
Expand All @@ -42,24 +36,14 @@ class UserAdmin(admin.ModelAdmin):
'national_id',
)

# Ordering and pagination
ordering = ('-date_joined',)
list_per_page = 25
show_full_result_count = True

# Actions
actions = [make_premium, deactivate_users]
actions_on_top = True
actions_on_bottom = True

# Fieldsets for add/edit forms
fieldsets = (
('Account Information', {
(None, {
'fields': ('username', 'password')
}),
('Personal Information', {
'fields': (
'username',
'email',
'password',
('first_name', 'last_name'),
'email',
'phone_number',
'national_id',
)
Expand All @@ -71,7 +55,6 @@ class UserAdmin(admin.ModelAdmin):
'balance',
),
'classes': ('collapse',),
'description': 'Manage user subscription and payment details',
}),
('Permissions', {
'fields': (
Expand All @@ -82,32 +65,44 @@ class UserAdmin(admin.ModelAdmin):
'user_permissions',
),
'classes': ('collapse',),
'description': 'User permission settings',
}),
('Important Dates', {
'fields': (
'last_login',
'date_joined',
'created_at',
'updated_at',
),
'fields': ('last_login', 'date_joined'),
'classes': ('collapse',),
}),
)

# Read-only fields
readonly_fields = (
'last_login',
'date_joined',
'created_at',
'updated_at',
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'username',
'email',
'password1',
'password2',
'first_name',
'last_name',
'subscription_type',
),
}),
)

# Custom display methods
def full_name(self, obj):
"""Display user's full name"""
return f"{obj.first_name} {obj.last_name}" if obj.first_name or obj.last_name else "-"
full_name.short_description = 'Full Name'
readonly_fields = ('last_login', 'date_joined')
ordering = ('-date_joined',)
list_per_page = 25
actions = [make_premium, deactivate_users]
filter_horizontal = ('groups', 'user_permissions',)

def get_full_name(self, obj):
"""Get user's full name with proper fallback"""
if obj.first_name and obj.last_name:
return f"{obj.first_name} {obj.last_name}"
elif obj.first_name:
return obj.first_name
elif obj.last_name:
return obj.last_name
return obj.username
get_full_name.short_description = 'Full Name'

def subscription_badge(self, obj):
"""Display subscription type with color-coded badge"""
Expand All @@ -116,94 +111,27 @@ def subscription_badge(self, obj):
SubscriptionType.BASIC: '#007bff',
SubscriptionType.PREMIUM: '#28a745',
}
color = colors.get(obj.subscription_type, '#6c757d')
text = obj.get_subscription_type_display()
return format_html(
'<span style="background-color: {}; color: white; padding: 3px 8px; '
'border-radius: 3px; font-size: 0.875em;">{}</span>',
colors.get(obj.subscription_type, '#6c757d'),
obj.get_subscription_type_display()
'border-radius: 3px;">{}</span>',
color, text
)
subscription_badge.short_description = 'Subscription'

def balance_display(self, obj):
"""Display formatted balance"""
"""Display formatted balance with currency"""
balance = obj.balance if obj.balance is not None else 0.00
return format_html(
'<span style="font-family: monospace;">EGP {:.2f}</span>',
obj.balance
'{:.2f}'.format(balance)
)
balance_display.short_description = 'Balance'

def last_login_display(self, obj):
"""Display formatted last login date"""
if obj.last_login:
return obj.last_login.strftime("%Y-%m-%d %H:%M")
return format_html(
'<span style="color: #dc3545;">Never</span>'
)
last_login_display.short_description = 'Last Login'

def status_badge(self, obj):
"""Display user status with color-coded badge"""
if obj.is_active:
return format_html(
'<span style="background-color: #28a745; color: white; '
'padding: 3px 8px; border-radius: 3px;">Active</span>'
)
return format_html(
'<span style="background-color: #dc3545; color: white; '
'padding: 3px 8px; border-radius: 3px;">Inactive</span>'
)
status_badge.short_description = 'Status'

def account_actions(self, obj):
"""Display action buttons for each user"""
return format_html(
'<a class="button" href="{}">Edit</a>&nbsp;'
'<a class="button" href="{}">History</a>',
f'/admin/users/user/{obj.pk}/change/',
f'/admin/users/user/{obj.pk}/history/'
)
account_actions.short_description = 'Actions'

# Custom views
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path(
'statistics/',
self.admin_site.admin_view(self.statistics_view),
name='user-statistics',
),
]
return custom_urls + urls

def statistics_view(self, request):
"""Custom view for user statistics"""
context = {
'title': 'User Statistics',
'app_label': self.model._meta.app_label,
'opts': self.model._meta,
'has_change_permission': self.has_change_permission(request),
}
return render(request, 'admin/users/statistics.html', context)

# Custom save handling
def save_model(self, request, obj, form, change):
"""Custom save method for user model"""
"""Handle user creation and password updates"""
if not change: # New user
obj.set_password(form.cleaned_data["password"])
obj.set_password(form.cleaned_data["password1"])
elif 'password' in form.changed_data: # Password changed
obj.set_password(form.cleaned_data["password"])
super().save_model(request, obj, form, change)

# Admin site customization
class Media:
css = {
'all': ('admin/css/user_admin.css',)
}
js = ('admin/js/user_admin.js',)


# Admin site customization
admin.site.site_header = "Egypt Metro Administration"
admin.site.site_title = "Egypt Metro Admin Portal"
admin.site.index_title = "Welcome to Egypt Metro Admin Portal"
7 changes: 5 additions & 2 deletions apps/users/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# apps/users/api/urls.py
from django.urls import path
from .views import auth, profile, base

from .views import auth, profile, base, auth_test
from rest_framework_simplejwt.views import TokenRefreshView

app_name = 'users'
Expand All @@ -17,5 +18,7 @@
# Token refresh endpoint
path('token/refresh/', TokenRefreshView.as_view(), name='token-refresh'),

path("superusers/", base.get_superusers, name="get_superusers")
path("superusers/", base.get_superusers, name="get_superusers"),

path('test-auth/', auth_test.test_auth, name='test-auth'),
]
15 changes: 15 additions & 0 deletions apps/users/api/views/auth_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# apps/users/views/auth_test.py

from django.http import JsonResponse
from django.contrib.auth.decorators import login_required


@login_required
def test_auth(request):
"""Test authentication status."""
return JsonResponse({
'authenticated': True,
'user': request.user.email,
'is_superuser': request.user.is_superuser,
'is_staff': request.user.is_staff
})
Loading

0 comments on commit e5f3fb2

Please sign in to comment.