Add Employee vs Client user type and filter time/reports by employee (#14) (#15)
Unit Tests / test (push) Successful in 16s
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
This commit was merged in pull request #15.
This commit is contained in:
@@ -243,6 +243,29 @@ class AddressModel(models.Model):
|
||||
state = models.CharField(max_length=2)
|
||||
zip_code = models.CharField(max_length=5)
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
class UserType(models.TextChoices):
|
||||
EMPLOYEE = "employee", "Employee"
|
||||
CLIENT = "client", "Client"
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||
user_type = models.CharField(
|
||||
max_length=10,
|
||||
choices=UserType.choices,
|
||||
default=UserType.CLIENT,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} ({self.get_user_type_display()})"
|
||||
|
||||
def is_employee(self):
|
||||
return self.user_type == self.UserType.EMPLOYEE
|
||||
|
||||
def is_client(self):
|
||||
return self.user_type == self.UserType.CLIENT
|
||||
|
||||
|
||||
class Employee(IdMixin, TimeMixin):
|
||||
manager = models.ForeignKey("self", on_delete=models.CASCADE, related_name="manager_employee", null=True, blank=True)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
@@ -279,6 +302,30 @@ class TimeCardCell(IdMixin, TimeMixin):
|
||||
charge_number = models.ForeignKey(ChargeNumber, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
|
||||
def set_user_type(user, user_type):
|
||||
"""Set user type and sync the Employee record (mutually exclusive types)."""
|
||||
user.__dict__.pop("profile", None)
|
||||
profile, _ = UserProfile.objects.get_or_create(
|
||||
user=user,
|
||||
defaults={"user_type": user_type},
|
||||
)
|
||||
if profile.user_type == user_type:
|
||||
if user_type == UserProfile.UserType.EMPLOYEE:
|
||||
Employee.objects.get_or_create(user=user)
|
||||
return profile
|
||||
|
||||
if user_type == UserProfile.UserType.CLIENT:
|
||||
employee = Employee.objects.filter(user=user).first()
|
||||
if employee and TimeCardCell.objects.filter(timeCard__employee=employee).exists():
|
||||
raise ValueError("Cannot set Client: user has time log entries.")
|
||||
|
||||
profile.user_type = user_type
|
||||
profile.save()
|
||||
user.__dict__.pop("profile", None)
|
||||
|
||||
if user_type == UserProfile.UserType.EMPLOYEE:
|
||||
Employee.objects.get_or_create(user=user)
|
||||
else:
|
||||
Employee.objects.filter(user=user).delete()
|
||||
|
||||
return profile
|
||||
|
||||
Reference in New Issue
Block a user