Skip to content

Commit 7317469

Browse files
committed
feat: Implement user roles, permissions, and profile management
1 parent 993051d commit 7317469

File tree

16 files changed

+285
-30
lines changed

16 files changed

+285
-30
lines changed

inventory/decorators.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# In inventory/decorators.py
2+
3+
from functools import wraps
4+
from django.contrib import messages
5+
from django.shortcuts import redirect
6+
7+
def admin_required(view_func):
8+
@wraps(view_func)
9+
def _wrapped_view(request, *args, **kwargs):
10+
if not request.user.is_authenticated:
11+
return redirect('login')
12+
13+
if not hasattr(request.user, 'profile') or request.user.profile.role != 'ADMIN':
14+
messages.error(request, "You do not have permission to access this page.")
15+
return redirect('inventory:dashboard')
16+
17+
return view_func(request, *args, **kwargs)
18+
return _wrapped_view

inventory/forms.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from .models import Space
66
from .models import Item
77
from .models import Student
8+
from .models import UserProfile
9+
from django.contrib.auth.models import User
810

911
class SectionForm(forms.ModelForm):
1012
class Meta:
@@ -71,4 +73,16 @@ class StockAdjustmentForm(forms.Form):
7173
widget=forms.Textarea(attrs={'rows': 3}),
7274
required=True,
7375
help_text="Please provide a reason for this stock change (e.g., 'New order received', 'Dropped and damaged')."
74-
)
76+
)
77+
78+
class UserUpdateForm(forms.ModelForm):
79+
"""Form for users to update their own basic information."""
80+
class Meta:
81+
model = User
82+
fields = ['first_name', 'last_name', 'email']
83+
84+
class UserRoleForm(forms.ModelForm):
85+
"""Form for Admins to update a user's role."""
86+
class Meta:
87+
model = UserProfile
88+
fields = ['role']
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.2.7 on 2025-10-25 00:23
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('inventory', '0012_checkoutlog_notes'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='UserProfile',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('role', models.CharField(choices=[('ADMIN', 'Admin'), ('MEMBER', 'Member')], default='MEMBER', max_length=10)),
21+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
22+
],
23+
),
24+
]

inventory/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.urls import reverse
99
from django.utils import timezone
1010
from django.db.models import Sum
11+
from django.db.models.signals import post_save
1112

1213
import barcode
1314
from barcode.writer import SVGWriter
@@ -216,6 +217,24 @@ def save(self, *args, **kwargs):
216217

217218
super().save(*args, **kwargs)
218219

220+
class UserProfile(models.Model):
221+
"""Extends the default Django User model to include a role."""
222+
class Role(models.TextChoices):
223+
ADMIN = 'ADMIN', 'Admin'
224+
MEMBER = 'MEMBER', 'Member'
225+
226+
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile')
227+
role = models.CharField(max_length=10, choices=Role.choices, default=Role.MEMBER)
228+
229+
def __str__(self):
230+
return f"{self.user.username} - {self.get_role_display()}"
231+
232+
def create_user_profile(sender, instance, created, **kwargs):
233+
if created:
234+
UserProfile.objects.create(user=instance)
235+
236+
post_save.connect(create_user_profile, sender=settings.AUTH_USER_MODEL)
237+
219238
class CheckoutLog(models.Model):
220239
"""A record of an item being checked out by a student."""
221240
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name="checkout_logs")

inventory/templates/inventory/base.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@
7777
<div class="navigation-object navigation-category">
7878
<span class="navigation-category-title">Account</span>
7979
</div>
80+
<div class="navigation-object">
81+
<a class="navigation-link" href="{% url 'inventory:my_profile' %}">
82+
<i class="fa-solid fa-user"></i> My Profile ({{ request.user.username }})
83+
</a>
84+
</div>
85+
{% if request.user.profile.role == 'ADMIN' %}
86+
<div class="navigation-object">
87+
<a class="navigation-link" href="{% url 'inventory:team_management' %}">
88+
<i class="fa-solid fa-users"></i> Team Management
89+
</a>
90+
</div>
91+
{% endif %}
8092
<div class="navigation-object">
8193
<form action="{% url 'logout' %}" method="post">
8294
{% csrf_token %}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% extends "inventory/base.html" %}
2+
{% block content %}
3+
<p><a href="{% url 'inventory:team_management' %}">< Back to Team Management</a></p>
4+
<h1>Create a New User</h1>
5+
<div class="detail-container">
6+
<form method="post">
7+
{% csrf_token %}
8+
{{ form.as_p }}
9+
<button type="submit">Create User</button>
10+
</form>
11+
</div>
12+
{% endblock %}

inventory/templates/inventory/item_detail.html

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ <h3>Item Barcode</h3>
3434
<div class="action-grid">
3535
<!-- First Row -->
3636
<div class="action-row">
37+
{% if request.user.profile.role == 'ADMIN' %}
3738
<div>
3839
<a href="{% url 'inventory:item_update' section_code=item.space.section.section_code space_code=item.space.space_code item_code=item.item_code %}">Edit Details</a>
3940
</div>
41+
{% endif %}
4042
<div>
4143
<a href="{% url 'inventory:adjust_stock' section_code=item.space.section.section_code space_code=item.space.space_code item_code=item.item_code action='RECEIVED' %}" class="link-button">Receive Stock</a>
4244
</div>
@@ -62,12 +64,14 @@ <h3>Item Barcode</h3>
6264

6365
<hr>
6466

65-
<h2 class="destructive" style="margin-top: 2em;">Delete this Item</h2>
66-
<p>This action is irreversible.</p>
67-
<form action="{% url 'inventory:item_delete' section_code=item.space.section.section_code space_code=item.space.space_code item_code=item.item_code %}" method="post">
68-
{% csrf_token %}
69-
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this item?');">I understand the consequences, delete this item</button>
70-
</form>
67+
{% if request.user.profile.role == 'ADMIN' %}
68+
<h2 class="destructive" style="margin-top: 2em;">Delete this Item</h2>
69+
<p>This action is irreversible.</p>
70+
<form action="{% url 'inventory:item_delete' section_code=item.space.section.section_code space_code=item.space.space_code item_code=item.item_code %}" method="post">
71+
{% csrf_token %}
72+
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this item?');">I understand the consequences, delete this item</button>
73+
</form>
74+
{% endif %}
7175
</div>
7276

7377
<!-- === RIGHT COLUMN === -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% extends "inventory/base.html" %}
2+
{% block content %}
3+
<h1>My Profile</h1>
4+
<div class="detail-container">
5+
<form method="post">
6+
{% csrf_token %}
7+
{{ form.as_p }}
8+
<button type="submit">Save Changes</button>
9+
</form>
10+
<hr>
11+
<p><a href="{% url 'password_change' %}">Change Password</a></p>
12+
</div>
13+
{% endblock %}

inventory/templates/inventory/section_detail.html

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ <h1>{{ section.name }}</h1>
3131
<button type="submit">Add to print queue</button>
3232
</form>
3333
</div>
34-
35-
<h2 class="destructive">Delete this section</h2>
36-
<p>If you no longer use this section, you can choose to delete it. All spaces associated with this section will be destroyed. This action is irreversible.</p>
37-
<form action="{% url 'inventory:section_delete' section.section_code %}" method="post">
38-
{% csrf_token %}
39-
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this section? This action is irreversible.');">I understand the consequences, delete this section</button>
40-
</form>
34+
35+
{% if request.user.profile.role == 'ADMIN' %}
36+
<h2 class="destructive">Delete this section</h2>
37+
<p>If you no longer use this section, you can choose to delete it. All spaces associated with this section will be destroyed. This action is irreversible.</p>
38+
<form action="{% url 'inventory:section_delete' section.section_code %}" method="post">
39+
{% csrf_token %}
40+
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this section? This action is irreversible.');">I understand the consequences, delete this section</button>
41+
</form>
42+
{% endif %}
4143
</div>
4244
{% endblock %}

inventory/templates/inventory/space_detail.html

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ <h1>{{ space.name }}</h1>
3232
</form>
3333
</div>
3434

35-
<h2 class="destructive">Delete this space</h2>
36-
<p>If you no longer use this space, you can choose to delete it. All inventory items associated with this space will be destroyed. This action is irreversible.</p>
37-
<form action="{% url 'inventory:space_delete' section.section_code space.space_code %}" method="post">
38-
{% csrf_token %}
39-
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this space? This action is irreversible.');">I understand the consequences, delete this space</button>
40-
</form>
35+
{% if request.user.profile.role == 'ADMIN' %}
36+
<h2 class="destructive">Delete this space</h2>
37+
<p>If you no longer use this space, you can choose to delete it. All inventory items associated with this space will be destroyed. This action is irreversible.</p>
38+
<form action="{% url 'inventory:space_delete' section.section_code space.space_code %}" method="post">
39+
{% csrf_token %}
40+
<button type="submit" class="destructive-background" onclick="return confirm('Are you sure you want to delete this space? This action is irreversible.');">I understand the consequences, delete this space</button>
41+
</form>
42+
{% endif %}
4143
</div>
4244
{% endblock %}

0 commit comments

Comments
 (0)