Compare commits

...

6 Commits

15 changed files with 296 additions and 21 deletions

View File

@ -106,6 +106,7 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
AUTH_USER_MODEL = 'member.UserAccount'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/ # https://docs.djangoproject.com/en/5.2/topics/i18n/

View File

@ -1,7 +1,7 @@
# forms.py # forms.py
from django import forms from django import forms
from .models import Person from .models import Person, UserAccount
class PersonForm(forms.ModelForm): class PersonForm(forms.ModelForm):
class Meta: class Meta:
@ -10,3 +10,21 @@ class PersonForm(forms.ModelForm):
widgets = { widgets = {
'geburtsdatum': forms.DateInput(attrs={'type': 'date'}), 'geburtsdatum': forms.DateInput(attrs={'type': 'date'}),
} }
class AccountForm(forms.ModelForm):
password=forms.CharField(widget=forms.PasswordInput())
confirm_password=forms.CharField(widget=forms.PasswordInput())
class Meta:
model = UserAccount
fields = ['username', 'password']
def clean(self):
cleaned_data = super(AccountForm, self).clean()
passwordStr = cleaned_data.get("password")
confirm_passwordStr = cleaned_data.get("confirm_password")
if passwordStr != confirm_passwordStr:
raise forms.ValidateError("password and confirm_password does not match")

View File

@ -26,24 +26,24 @@ class Command(BaseCommand):
self.stdout.write("🔐 Erstelle Benutzerkonten...") self.stdout.write("🔐 Erstelle Benutzerkonten...")
UserAccount.objects.create( UserAccount.objects.create(
person=p0, person=p0,
userName="admin", username="admin",
passwort_hash=make_password("adminpass1234"), password=make_password("adminpass1234"),
role="superadmin", role="superadmin",
isActive=True, is_active=True,
) )
UserAccount.objects.create( UserAccount.objects.create(
person=p1, person=p1,
userName="maxadmin", username="maxadmin",
passwort_hash=make_password("adminpass123"), password=make_password("adminpass123"),
role="admin", role="admin",
isActive=True, is_active=True,
) )
UserAccount.objects.create( UserAccount.objects.create(
person=p2, person=p2,
userName="erikam", username="erikam",
passwort_hash=make_password("mitglied456"), password=make_password("mitglied456"),
role="mitglied", role="mitglied",
isActive=True, is_active=True,
) )
self.stdout.write(self.style.SUCCESS("✅ Testdaten erfolgreich eingespielt.")) self.stdout.write(self.style.SUCCESS("✅ Testdaten erfolgreich eingespielt."))

View File

@ -0,0 +1,105 @@
# Generated by Django 5.2.1 on 2025-06-15 15:41
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('member', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='useraccount',
options={'verbose_name': 'user', 'verbose_name_plural': 'users'},
),
migrations.AlterModelManagers(
name='useraccount',
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.RemoveField(
model_name='useraccount',
name='isActive',
),
migrations.RemoveField(
model_name='useraccount',
name='lastLogin',
),
migrations.RemoveField(
model_name='useraccount',
name='passwort_hash',
),
migrations.RemoveField(
model_name='useraccount',
name='userName',
),
migrations.AddField(
model_name='useraccount',
name='date_joined',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'),
),
migrations.AddField(
model_name='useraccount',
name='email',
field=models.EmailField(blank=True, max_length=254, verbose_name='email address'),
),
migrations.AddField(
model_name='useraccount',
name='first_name',
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
),
migrations.AddField(
model_name='useraccount',
name='groups',
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'),
),
migrations.AddField(
model_name='useraccount',
name='is_active',
field=models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active'),
),
migrations.AddField(
model_name='useraccount',
name='is_staff',
field=models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status'),
),
migrations.AddField(
model_name='useraccount',
name='is_superuser',
field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'),
),
migrations.AddField(
model_name='useraccount',
name='last_login',
field=models.DateTimeField(blank=True, null=True, verbose_name='last login'),
),
migrations.AddField(
model_name='useraccount',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
migrations.AddField(
model_name='useraccount',
name='password',
field=models.CharField(default='changeme123', max_length=128, verbose_name='password'),
preserve_default=False,
),
migrations.AddField(
model_name='useraccount',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
),
migrations.AddField(
model_name='useraccount',
name='username',
field=models.CharField(default='username', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
preserve_default=False,
),
]

View File

@ -1,20 +1,17 @@
from django.db import models from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone from django.utils import timezone
from .Person import Person from .Person import Person
class UserAccount(models.Model): class UserAccount(AbstractUser):
person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='benutzerkonten') person = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='benutzerkonten')
userName = models.CharField(max_length=150, unique=True)
passwort_hash = models.CharField(max_length=128)
role = models.CharField(max_length=50, choices=[ role = models.CharField(max_length=50, choices=[
('mitglied', 'Mitglied'), ('mitglied', 'Mitglied'),
('geraetewart', 'Gerätewart'), ('geraetewart', 'Gerätewart'),
('kommandant', 'Kommandant'), ('kommandant', 'Kommandant'),
('admin', 'Administrator'), ('admin', 'Administrator'),
]) ])
isActive = models.BooleanField(default=True)
lastLogin = models.DateTimeField(null=True, blank=True)
def __str__(self): def __str__(self):
return f"{self.benutzername} ({self.rolle})" return f"{self.benutzername} ({self.rolle})"

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M7.5 5l5 5-5 5V5z"/>
</svg>

After

Width:  |  Height:  |  Size: 119 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
<path fill-rule="evenodd" d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1,26 @@
{% extends "master.html" %}
{% block content %}
<div class="members-table">
<h3>{{ action }} Person</h3>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="{{form.username.id_for_label}}">Username</label>
{{form.username}}
</div>
<div class="form-group">
<label for="{{form.password.id_for_label}}">Password</label>
{{form.password}}
</div>
<div class="form-group">
<label for="{{form.password.id_for_label}}">Password (confirmation)</label>
{{form.confirm_password}}
</div>
<button class="button" type="submit">Speichern</button>
<a href="{% url 'details' id %}" class="button" style="background:#444;">Abbrechen</a>
</form>
</div>
{% endblock %}
{% block title %}
{{ action }} Account
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "master.html" %}
{% block content %}
<div class="members-table">
<h3>Account löschen</h3>
<p>Möchtest du den Useraccount <strong>{{account.username}}</strong> der {{ account.person.vorname }} {{account.person.nachname}} zugeordnet ist, wirklich löschen?</p>
<form method="post">
{% csrf_token %}
<button class="button" type="submit">Ja, löschen</button>
<a href="{% url 'details' id %}" class="button" style="background:#444;">Abbrechen</a>
</form>
</div>
{% endblock %}
{% block title %}
Account löschen
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "master.html" %}
{% block content %}
<div class="members-table">
<h3>Mitglied löschen</h3>
<p>Möchtest du <strong>{{ person.nachname }} {{person.vorname}}</strong> wirklich löschen?</p>
<form method="post">
{% csrf_token %}
<button class="button" type="submit">Ja, löschen</button>
<a href="{% url 'members' %}" class="button" style="background:#444;">Abbrechen</a>
</form>
</div>
{% endblock %}
{% block title %}
Mitglied löschen
{% endblock %}

View File

@ -1,4 +1,7 @@
{% extends "master.html" %} {% extends "master.html" %}
{% load static %}
{% load svg %}
{% block title %} {% block title %}
Details zu {{ mymember.firstname }} {{ mymember.lastname }} Details zu {{ mymember.firstname }} {{ mymember.lastname }}
@ -10,6 +13,33 @@
<p>Geburtsdatum: {{ mymember.geburtsdatum }}</p> <p>Geburtsdatum: {{ mymember.geburtsdatum }}</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Rolle</th>
<th>Aktiv</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for x in accounts %}
<tr>
<td>{{ x.username }}</td>
<td>{{ x.rolle }}</td>
<td>{% if x.aktiv %}{% inline_svg 'icons/uxwing/check.svg' 'icon' %}{% endif %}</td>
<td><a href="{% url 'edit_account' x.id %}" title="Bearbeiten"><img src="{% static 'icons/heroicons/pencil.svg'%}" class="icon"></a> <a href="{% url 'delete_account' x.id %}" title="Löschen"><img src="{% static 'icons/heroicons/trash.svg'%}" class="icon"></a></td>
</tr>
{% endfor %}
<tr>
<td colspan="4">
<a href="{% url 'create_account' mymember.id %}" class="button"><img src="{% static 'icons/heroicons/plus.svg'%}" class="icon"> Konto hinzufügen</a>
</td>
</tr>
</tbody>
</table>
<p>Back to <a href="/members">Members</a></p> <p>Back to <a href="/members">Members</a></p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -14,6 +14,7 @@
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Accounts</th>
<th>Aktiv</th> <th>Aktiv</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
@ -22,8 +23,9 @@
{% for x in mymembers %} {% for x in mymembers %}
<tr> <tr>
<td><a href="{% url 'details' x.id %}"><img src="{% static 'icons/arrow-right.svg' %}" class="icon">{{ x.nachname }} {{ x.vorname}}</a> </td> <td><a href="{% url 'details' x.id %}"><img src="{% static 'icons/arrow-right.svg' %}" class="icon">{{ x.nachname }} {{ x.vorname}}</a> </td>
<td>{{x.accounts}}</td>
<td>{% if x.aktiv %}{% inline_svg 'icons/uxwing/check.svg' 'icon' %}{% endif %}</td> <td>{% if x.aktiv %}{% inline_svg 'icons/uxwing/check.svg' 'icon' %}{% endif %}</td>
<td><a href="{% url 'edit' x.id %}" title="Bearbeiten"><img src="{% static 'icons/heroicons/pencil.svg'%}" class="icon"></a></td> <td><a href="{% url 'edit' x.id %}" title="Bearbeiten"><img src="{% static 'icons/heroicons/pencil.svg'%}" class="icon"></a> <a href="{% url 'delete' x.id %}" title="Löschen"><img src="{% static 'icons/heroicons/trash.svg'%}" class="icon"></a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -25,3 +25,6 @@
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
{% block title %}
{{ action }} Person
{% endblock %}

View File

@ -6,4 +6,10 @@ urlpatterns = [
path('members/details/<int:id>', views.details, name="details"), path('members/details/<int:id>', views.details, name="details"),
path('members/create/', views.create, name="create"), path('members/create/', views.create, name="create"),
path('members/edit/<int:id>', views.edit, name="edit"), path('members/edit/<int:id>', views.edit, name="edit"),
path('members/delete/<int:id>', views.delete, name="delete"),
path('members/account/details/<int:id>', views.details_account, name="details_account"),
path('members/account/edit/<int:id>', views.edit_account, name="edit_account"),
path('members/account/create/<int:id>', views.create_account, name="create_account"),
path('members/account/edit/<int:id>', views.edit_account, name="edit_account"),
path('members/account/delete/<int:id>', views.delete_account, name="delete_account"),
] ]

View File

@ -1,11 +1,12 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.template import loader from django.template import loader
from .models import Person from django.db.models import Count
from .forms import PersonForm from .models import Person, UserAccount
from .forms import PersonForm, AccountForm
def members(request): def members(request):
mymembers=Person.objects.all().values() mymembers=Person.objects.annotate(accounts=Count('benutzerkonten'))
template=loader.get_template("memberlist.html") template=loader.get_template("memberlist.html")
context = { context = {
'mymembers': mymembers 'mymembers': mymembers
@ -14,9 +15,11 @@ def members(request):
def details(request, id): def details(request, id):
mymember = Person.objects.get(id=id) mymember = Person.objects.get(id=id)
accounts = UserAccount.objects.filter(person_id=id)
template = loader.get_template("details.html") template = loader.get_template("details.html")
context = { context = {
'mymember': mymember 'mymember': mymember,
'accounts': accounts
} }
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
@ -40,3 +43,51 @@ def edit(request, id):
else: else:
form = PersonForm(instance=person) form = PersonForm(instance=person)
return render(request, 'person_form.html', {'form': form, 'action': 'Bearbeiten'}) return render(request, 'person_form.html', {'form': form, 'action': 'Bearbeiten'})
def delete(request, id):
person = get_object_or_404(Person, id=id)
if request.method == 'POST':
person.delete()
return redirect('members')
return render(request, 'confirm_delete.html', { 'person': person})
def details_account(request, id):
account = get_object_or_404(Account, id=id)
template = loader.get_template("details_account.html")
context = {
'account': account,
}
return HttpResponse(template.render(context, request))
def create_account(request, id):
person = get_object_or_404(Person, id=id)
if request.method == "POST":
form = AccountForm(request.POST)
if form.is_valid():
account=form.save(commit=False)
account.person = person
account.save()
return redirect("details", person.id)
else:
form = AccountForm()
return render(request, "account_form.html", {'form': form, 'action': 'Erstellen', 'id': id})
def edit_account(request, id):
account = get_object_or_404(UserAccount, id=id)
if request.method == 'POST':
form = AccountForm(request.POST, instance=account)
if form.is_valid():
form.save()
return redirect('details', account.person.id)
else:
form = AccountForm(instance=account)
return render(request, 'account_form.html', {'form': form, 'action': "Bearbeiten" , 'id': account.person.id})
def delete_account(request, id):
account=get_object_or_404(UserAccount, id=id)
person_id=account.person.id
if (request.method=='POST'):
account.delete()
return redirect("details", person_id)
return render(request, "confirm_account_delete.html", {'account': account, 'id': account.person.id})