User Management & Dashboard Analytics
This commit is contained in:
parent
a200f58095
commit
10bb02df57
Binary file not shown.
@ -1,15 +1,3 @@
|
||||
"""
|
||||
Django settings for config project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
@ -55,6 +43,8 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'crispy_forms',
|
||||
'crispy_bootstrap5',
|
||||
'core',
|
||||
]
|
||||
|
||||
@ -180,6 +170,11 @@ CONTACT_EMAIL_TO = [
|
||||
# When both TLS and SSL flags are enabled, prefer SSL explicitly
|
||||
if EMAIL_USE_SSL:
|
||||
EMAIL_USE_TLS = False
|
||||
|
||||
# Crispy Forms settings
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile
|
||||
from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile, Invitation
|
||||
|
||||
@admin.register(Company)
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
@ -32,4 +32,10 @@ class JobFolderCompletionAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(JobFile)
|
||||
class JobFileAdmin(admin.ModelAdmin):
|
||||
list_display = ('job', 'folder', 'file', 'uploaded_at')
|
||||
list_display = ('job', 'folder', 'file', 'uploaded_at')
|
||||
|
||||
@admin.register(Invitation)
|
||||
class InvitationAdmin(admin.ModelAdmin):
|
||||
list_display = ('email', 'company', 'invited_by', 'created_at', 'expires_at', 'is_accepted')
|
||||
list_filter = ('company', 'is_accepted')
|
||||
search_fields = ('email',)
|
||||
|
||||
@ -75,4 +75,8 @@ class JobFileForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
class ImportJobsForm(forms.Form):
|
||||
file = forms.FileField(label="Excel/CSV File", widget=forms.ClearableFileInput(attrs={'class': 'form-control'}))
|
||||
file = forms.FileField(label="Excel/CSV File", widget=forms.ClearableFileInput(attrs={'class': 'form-control'}))
|
||||
|
||||
|
||||
class InviteUserForm(forms.Form):
|
||||
email = forms.EmailField(label="User Email", widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'email@example.com'}))
|
||||
30
core/migrations/0002_invitation.py
Normal file
30
core/migrations/0002_invitation.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-21 23:17
|
||||
|
||||
import datetime
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Invitation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('token', models.CharField(default='0b006409b9414604b1ccb84b32e8f319', max_length=32, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField(default=datetime.datetime(2026, 1, 28, 23, 17, 24, 824391, tzinfo=datetime.timezone.utc))),
|
||||
('is_accepted', models.BooleanField(default=False)),
|
||||
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='core.company')),
|
||||
('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0002_invitation.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_invitation.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,6 +1,10 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinLengthValidator
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
@ -59,6 +63,11 @@ class Job(models.Model):
|
||||
|
||||
class Meta:
|
||||
unique_together = [['company', 'job_ref'], ['company', 'uprn']]
|
||||
# Add a custom constraint to allow null=True and blank=True for uprn,
|
||||
# but still enforce unique_together when it's not null.
|
||||
# This is handled by a custom validator or by overriding save method if needed.
|
||||
# For now, Django's unique_together with null=True will allow multiple nulls.
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.job_ref} - {self.address_line_1}"
|
||||
@ -79,4 +88,16 @@ class JobFile(models.Model):
|
||||
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.file.name} in {self.folder.name}"
|
||||
return f"{self.file.name} in {self.folder.name}"
|
||||
|
||||
class Invitation(models.Model):
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='invitations')
|
||||
invited_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
email = models.EmailField(unique=True)
|
||||
token = models.CharField(max_length=32, unique=True, default=uuid.uuid4().hex)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField(default=timezone.now() + timedelta(days=7))
|
||||
is_accepted = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Invitation for {self.email} to {self.company.name}"
|
||||
44
core/templates/core/accept_invite.html
Normal file
44
core/templates/core/accept_invite.html
Normal file
@ -0,0 +1,44 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Accept Invitation{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow-lg border-0 rounded-lg">
|
||||
<div class="card-header bg-gradient-primary text-white text-center py-4">
|
||||
<h3 class="font-weight-light my-2">Join {{ invitation.company.name }}</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if user.is_authenticated and user.email != invitation.email %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
You are logged in as {{ user.email }}. This invitation is for {{ invitation.email }}. Please log out or use the correct account.
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="mb-4">You have been invited to join <strong>{{ invitation.company.name }}</strong>. Please create an account or log in to accept the invitation.</p>
|
||||
<form method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if not user.is_authenticated %}
|
||||
<div class="mb-3">
|
||||
{{ form.username|as_crispy_field }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.password|as_crispy_field }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.password2|as_crispy_field }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
<button type="submit" class="btn btn-primary">Accept Invitation</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -67,7 +67,7 @@
|
||||
<a href="{% url 'job_list' %}" class="nav-link-custom">
|
||||
<i class="bi bi-tools"></i> Jobs
|
||||
</a>
|
||||
<a href="#" class="nav-link-custom">
|
||||
<a href="{% url 'user_list' %}" class="nav-link-custom">
|
||||
<i class="bi bi-people"></i> Users
|
||||
</a>
|
||||
</div>
|
||||
@ -106,20 +106,23 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">Pending survey</div>
|
||||
<div class="stat-value text-warning">{{ jobs_with_incomplete_folders }}</div>
|
||||
<div class="stat-label">Jobs with Incomplete Folders</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">Critical Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-label">Completed</div>
|
||||
<h6 class="mb-3">Jobs by Status</h6>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for status in jobs_by_status %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center px-0 py-1">
|
||||
{{ status.status__name }}
|
||||
<span class="badge bg-secondary rounded-pill">{{ status.count }}</span>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item px-0 py-1">No jobs found.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -163,4 +166,4 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
30
core/templates/core/invite_user.html
Normal file
30
core/templates/core/invite_user.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Invite User{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card shadow-lg border-0 rounded-lg">
|
||||
<div class="card-header bg-gradient-primary text-white text-center py-4">
|
||||
<h3 class="font-weight-light my-2">Invite User to {{ company.name }}</h3>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
{{ form.email|as_crispy_field }}
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-0">
|
||||
<a class="small text-primary" href="{% url 'settings' %}">Back to Settings</a>
|
||||
<button type="submit" class="btn btn-primary">Send Invitation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -60,6 +60,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mt-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0">User Management</h5>
|
||||
<a href="{% url 'user_list' %}" class="btn btn-sm btn-primary">Manage Users</a>
|
||||
</div>
|
||||
{% if invitations %}
|
||||
<h6 class="mb-3">Pending Invitations:</h6>
|
||||
<ul class="list-group mb-3">
|
||||
{% for invite in invitations %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
{{ invite.email }}
|
||||
<span class="badge bg-info rounded-pill">Expires {{ invite.expires_at|date:"M d, Y" }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted">No pending invitations.</p>
|
||||
{% endif %}
|
||||
<a href="{% url 'invite_user' %}" class="btn btn-sm btn-outline-primary">Invite New User</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
65
core/templates/core/user_list.html
Normal file
65
core/templates/core/user_list.html
Normal file
@ -0,0 +1,65 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block title %}Users in {{ company.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="mb-0">Users in {{ company.name }}</h2>
|
||||
<a href="{% url 'invite_user' %}" class="btn btn-primary">Invite New User</a>
|
||||
</div>
|
||||
|
||||
{% if users_in_company %}
|
||||
<div class="card shadow-lg border-0 rounded-lg">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user_obj in users_in_company %}
|
||||
<tr>
|
||||
<td>{{ user_obj.username }}</td>
|
||||
<td>{{ user_obj.email }}</td>
|
||||
<td>
|
||||
<form action="{% url 'user_update_role' pk=user_obj.pk %}" method="post" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<select name="role" class="form-select form-select-sm d-inline w-auto" onchange="this.form.submit()">
|
||||
<option value="ADMIN" {% if user_obj.profile.role == 'ADMIN' %}selected{% endif %}>Admin</option>
|
||||
<option value="STANDARD" {% if user_obj.profile.role == 'STANDARD' %}selected{% endif %}>Standard User</option>
|
||||
</select>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{% if user_obj != request.user %}
|
||||
<form action="{% url 'user_delete' pk=user_obj.pk %}" method="post" class="d-inline ms-2">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete this user?');">Delete</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted"> (You)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">No users found in your company yet. Invite some!</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'settings' %}" class="btn btn-secondary">Back to Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -29,4 +29,11 @@ urlpatterns = [
|
||||
path('settings/status/<int:pk>/delete/', views.status_delete, name='status_delete'),
|
||||
path('settings/folder/create/', views.folder_create, name='folder_create'),
|
||||
path('settings/folder/<int:pk>/delete/', views.folder_delete, name='folder_delete'),
|
||||
|
||||
# User Management
|
||||
path('settings/invite-user/', views.invite_user, name='invite_user'),
|
||||
path('settings/users/', views.user_list, name='user_list'),
|
||||
path('settings/users/<int:pk>/update-role/', views.user_update_role, name='user_update_role'),
|
||||
path('settings/users/<int:pk>/delete/', views.user_delete, name='user_delete'),
|
||||
path('accept-invite/<str:token>/', views.accept_invite, name='accept_invite'),
|
||||
]
|
||||
|
||||
201
core/views.py
201
core/views.py
@ -1,15 +1,24 @@
|
||||
import os
|
||||
import io
|
||||
import pandas as pd
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth import login, logout, authenticate
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponse
|
||||
from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile
|
||||
from .forms import CompanyForm, JobStatusForm, RequiredFolderForm, JobForm, JobFileForm, ImportJobsForm
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile, Invitation
|
||||
from .forms import CompanyForm, JobStatusForm, RequiredFolderForm, JobForm, JobFileForm, ImportJobsForm, InviteUserForm
|
||||
|
||||
def home(request):
|
||||
if request.user.is_authenticated:
|
||||
@ -104,9 +113,18 @@ def dashboard(request):
|
||||
company = profile.company
|
||||
jobs = Job.objects.filter(company=company)
|
||||
|
||||
total_jobs = jobs.count()
|
||||
|
||||
jobs_by_status = jobs.values('status__name').annotate(count=Count('id')).order_by('status__name')
|
||||
|
||||
# Jobs with incomplete folders
|
||||
jobs_with_incomplete_folders = jobs.filter(folder_completions__is_completed=False).distinct().count()
|
||||
|
||||
context = {
|
||||
'company': company,
|
||||
'total_jobs': jobs.count(),
|
||||
'total_jobs': total_jobs,
|
||||
'jobs_by_status': jobs_by_status,
|
||||
'jobs_with_incomplete_folders': jobs_with_incomplete_folders,
|
||||
'jobs': jobs.order_by('-created_at')[:5],
|
||||
}
|
||||
return render(request, 'core/dashboard.html', context)
|
||||
@ -388,12 +406,16 @@ def settings_view(request):
|
||||
company = profile.company
|
||||
statuses = company.statuses.all()
|
||||
folders = company.required_folders.all()
|
||||
|
||||
return render(request, 'core/settings.html', {
|
||||
invitations = company.invitations.filter(is_accepted=False, expires_at__gt=timezone.now())
|
||||
|
||||
context = {
|
||||
'company': company,
|
||||
'statuses': statuses,
|
||||
'folders': folders
|
||||
})
|
||||
'folders': folders,
|
||||
'invitations': invitations
|
||||
}
|
||||
|
||||
return render(request, 'core/settings.html', context)
|
||||
|
||||
@login_required
|
||||
def status_create(request):
|
||||
@ -483,3 +505,168 @@ def folder_delete(request, pk):
|
||||
messages.success(request, "Folder removed from company settings.")
|
||||
return redirect('settings')
|
||||
return render(request, 'core/folder_confirm_delete.html', {'folder': folder})
|
||||
|
||||
@login_required
|
||||
def invite_user(request):
|
||||
profile = request.user.profile
|
||||
if not profile.company or profile.role != 'ADMIN':
|
||||
messages.error(request, "You must be an admin to invite users.")
|
||||
return redirect('dashboard')
|
||||
|
||||
company = profile.company
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InviteUserForm(request.POST)
|
||||
if form.is_valid():
|
||||
email = form.cleaned_data['email']
|
||||
|
||||
if Invitation.objects.filter(email=email, company=company, is_accepted=False, expires_at__gt=timezone.now()).exists():
|
||||
messages.warning(request, f"An active invitation for {email} already exists.")
|
||||
return redirect('invite_user')
|
||||
|
||||
# Check if user already exists in this company
|
||||
if User.objects.filter(email=email, profile__company=company).exists():
|
||||
messages.warning(request, f"A user with {email} already exists in your company.")
|
||||
return redirect('invite_user')
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
invitation = Invitation.objects.create(
|
||||
company=company,
|
||||
invited_by=request.user,
|
||||
email=email,
|
||||
expires_at=timezone.now() + timedelta(days=7) # 7 days expiry
|
||||
)
|
||||
|
||||
invite_link = request.build_absolute_uri(
|
||||
reverse('accept_invite', args=[invitation.token])
|
||||
)
|
||||
|
||||
subject = f"Invitation to join {company.name} on RepairsHub"
|
||||
message = f"You have been invited to join {company.name} on RepairsHub. Click the link to accept: {invite_link}"
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
recipient_list = [email]
|
||||
|
||||
send_mail(subject, message, from_email, recipient_list)
|
||||
|
||||
messages.success(request, f"Invitation sent to {email}.")
|
||||
return redirect('settings') # Redirect to settings where user list will be
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error sending invitation: {e}")
|
||||
else:
|
||||
messages.error(request, "Please correct the error below.")
|
||||
else:
|
||||
form = InviteUserForm()
|
||||
|
||||
return render(request, 'core/invite_user.html', {'form': form, 'company': company})
|
||||
|
||||
def accept_invite(request, token):
|
||||
invitation = get_object_or_404(Invitation, token=token, is_accepted=False, expires_at__gt=timezone.now())
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# If user is logged in, and tries to accept an invite for another email, prevent it.
|
||||
if request.user.email != invitation.email:
|
||||
messages.error(request, "You are logged in with a different email address. Please log out or use the correct account.")
|
||||
return redirect('home') # Or a more appropriate page
|
||||
|
||||
# If the user is already authenticated with the correct email, just associate them.
|
||||
with transaction.atomic():
|
||||
profile, created = Profile.objects.get_or_create(user=request.user, defaults={'company': invitation.company, 'role': 'STANDARD'})
|
||||
if not created and profile.company != invitation.company:
|
||||
messages.error(request, "You are already part of another company. Please contact support to join a new company.")
|
||||
return redirect('dashboard')
|
||||
elif not created and profile.company == invitation.company: # Already in this company
|
||||
messages.info(request, "You are already a member of this company.")
|
||||
else: # New association
|
||||
profile.company = invitation.company
|
||||
profile.role = 'STANDARD'
|
||||
profile.save()
|
||||
|
||||
invitation.is_accepted = True
|
||||
invitation.save()
|
||||
messages.success(request, f"Welcome to {invitation.company.name}!")
|
||||
return redirect('dashboard')
|
||||
|
||||
# If user is not authenticated, they need to register/login
|
||||
if request.method == 'POST':
|
||||
form = UserCreationForm(request.POST)
|
||||
if form.is_valid():
|
||||
with transaction.atomic():
|
||||
user = form.save()
|
||||
user.email = invitation.email # Ensure the user's email matches the invitation
|
||||
user.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.company = invitation.company
|
||||
profile.role = 'STANDARD'
|
||||
profile.save()
|
||||
|
||||
invitation.is_accepted = True
|
||||
invitation.save()
|
||||
|
||||
login(request, user)
|
||||
messages.success(request, f"Welcome to {invitation.company.name}!")
|
||||
return redirect('dashboard')
|
||||
else:
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
else:
|
||||
form = UserCreationForm(initial={'email': invitation.email})
|
||||
|
||||
return render(request, 'core/accept_invite.html', {'form': form, 'invitation': invitation})
|
||||
|
||||
@login_required
|
||||
def user_list(request):
|
||||
profile = request.user.profile
|
||||
if not profile.company or profile.role != 'ADMIN':
|
||||
messages.error(request, "You must be an admin to manage users.")
|
||||
return redirect('dashboard')
|
||||
|
||||
company = profile.company
|
||||
users_in_company = User.objects.filter(profile__company=company).select_related('profile')
|
||||
|
||||
context = {
|
||||
'company': company,
|
||||
'users_in_company': users_in_company,
|
||||
}
|
||||
return render(request, 'core/user_list.html', context)
|
||||
|
||||
@login_required
|
||||
def user_update_role(request, pk):
|
||||
profile = request.user.profile
|
||||
if not profile.company or profile.role != 'ADMIN':
|
||||
messages.error(request, "You must be an admin to manage user roles.")
|
||||
return redirect('dashboard')
|
||||
|
||||
user_to_update = get_object_or_404(User, pk=pk, profile__company=profile.company)
|
||||
|
||||
if request.method == 'POST':
|
||||
new_role = request.POST.get('role')
|
||||
if new_role in ['ADMIN', 'STANDARD']:
|
||||
user_to_update.profile.role = new_role
|
||||
user_to_update.profile.save()
|
||||
messages.success(request, f"Role for {user_to_update.username} updated to {new_role}.")
|
||||
else:
|
||||
messages.error(request, "Invalid role selected.")
|
||||
|
||||
return redirect('user_list')
|
||||
|
||||
@login_required
|
||||
def user_delete(request, pk):
|
||||
profile = request.user.profile
|
||||
if not profile.company or profile.role != 'ADMIN':
|
||||
messages.error(request, "You must be an admin to delete users.")
|
||||
return redirect('dashboard')
|
||||
|
||||
user_to_delete = get_object_or_404(User, pk=pk, profile__company=profile.company)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Prevent an admin from deleting themselves
|
||||
if user_to_delete == request.user:
|
||||
messages.error(request, "You cannot delete your own account.")
|
||||
return redirect('user_list')
|
||||
|
||||
username = user_to_delete.username
|
||||
user_to_delete.delete()
|
||||
messages.success(request, f"User {username} deleted.")
|
||||
|
||||
return redirect('user_list')
|
||||
@ -1,3 +1,5 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
django-crispy-forms
|
||||
crispy-bootstrap5
|
||||
Loading…
x
Reference in New Issue
Block a user