Unit Tests / test (push) Successful in 16s
## Summary - Adds `UserProfile` model with mutually exclusive **Employee** / **Client** types - Replaces auto-Employee signal with auto-Client profile on user creation - Data migration: users with time log entries → Employee; others → Client (orphan Employee rows removed) - Admin UI at `/financial/manage_users` to set any user's type; profile page shows current type - **Employees** can log time; **Clients** get read-only access to reports and time logs - Time logs, reports, and dashboard filter to employees only - 14 new tests covering signals, type switching, access control, and filtering ## Design decisions (from issue Q&A) 1. Client login = read-only financial access (reports + time logs, no edit/log time) 2. Employee and Client are strictly mutually exclusive 3. Admins (superusers) can change type via Manage Users 4. Bulk migration applied for existing users ## Test plan - [x] `python manage.py test financial.tests` (14 tests pass) - [x] `python manage.py test public.tests` (21 tests pass) - [ ] Run migration on staging: `python manage.py migrate` - [ ] Verify admin can set user types at `/financial/manage_users` - [ ] Verify employee can log time at `/financial/timekeeping` - [ ] Verify client sees reports/time logs read-only, cannot log time - [ ] Verify employee filter dropdown excludes clients Closes #14 Reviewed-on: #15
77 lines
1.8 KiB
Python
77 lines
1.8 KiB
Python
from functools import wraps
|
|
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.core.exceptions import PermissionDenied
|
|
|
|
|
|
def get_user_profile(user):
|
|
if not user.is_authenticated:
|
|
return None
|
|
from .models import UserProfile
|
|
|
|
return UserProfile.objects.filter(user_id=user.pk).first()
|
|
|
|
|
|
def is_financial_admin(user):
|
|
return user.is_active and user.is_superuser
|
|
|
|
|
|
def is_employee_user(user):
|
|
profile = get_user_profile(user)
|
|
return bool(profile and profile.is_employee())
|
|
|
|
|
|
def is_client_user(user):
|
|
profile = get_user_profile(user)
|
|
return bool(profile and profile.is_client())
|
|
|
|
|
|
def has_financial_access(user):
|
|
return is_financial_admin(user) or is_employee_user(user) or is_client_user(user)
|
|
|
|
|
|
def can_write_financials(user):
|
|
return is_financial_admin(user) or is_employee_user(user)
|
|
|
|
|
|
def get_employees():
|
|
from .models import Employee, UserProfile
|
|
|
|
return Employee.objects.filter(
|
|
user__profile__user_type=UserProfile.UserType.EMPLOYEE
|
|
)
|
|
|
|
|
|
def get_user_employee(user):
|
|
from .models import Employee
|
|
|
|
if not is_employee_user(user):
|
|
return None
|
|
return Employee.objects.filter(user=user).first()
|
|
|
|
|
|
def financial_admin_required(view_func):
|
|
return user_passes_test(is_financial_admin)(view_func)
|
|
|
|
|
|
def financial_access_required(view_func):
|
|
@login_required
|
|
@wraps(view_func)
|
|
def _wrapped(request, *args, **kwargs):
|
|
if has_financial_access(request.user):
|
|
return view_func(request, *args, **kwargs)
|
|
raise PermissionDenied
|
|
|
|
return _wrapped
|
|
|
|
|
|
def financial_write_required(view_func):
|
|
@login_required
|
|
@wraps(view_func)
|
|
def _wrapped(request, *args, **kwargs):
|
|
if can_write_financials(request.user):
|
|
return view_func(request, *args, **kwargs)
|
|
raise PermissionDenied
|
|
|
|
return _wrapped
|