Chapter 19: User Accounts

Add forms for server management, user authentication, and data ownership.

Adding Servers via Form

ModelForm

# inventory/forms.py
from django import forms
from .models import Server, Event

class ServerForm(forms.ModelForm):
    class Meta:
        model = Server
        fields = ['hostname', 'ip_address', 'os', 'status']
        labels = {
            'hostname': 'Hostname',
            'ip_address': 'IP Address',
            'os': 'Operating System',
            'status': 'Status',
        }


class EventForm(forms.ModelForm):
    class Meta:
        model = Event
        fields = ['event_type', 'message']
        labels = {'event_type': 'Type', 'message': ''}
        widgets = {
            'event_type': forms.Select(choices=[
                ('deploy', 'Deploy'),
                ('restart', 'Restart'),
                ('alert', 'Alert'),
                ('maintenance', 'Maintenance'),
            ]),
            'message': forms.Textarea(attrs={'cols': 60, 'rows': 4}),
        }

New Server URL

# inventory/urls.py
urlpatterns = [
    # ...existing patterns...
    path('new_server/', views.new_server, name='new_server'),
]

New Server View

# inventory/views.py
from django.shortcuts import render, redirect
from .forms import ServerForm

def new_server(request):
    """Register a new server."""
    if request.method != 'POST':
        form = ServerForm()
    else:
        form = ServerForm(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('inventory:servers')

    context = {'form': form}
    return render(request, 'inventory/new_server.html', context)

New Server Template

<!-- inventory/templates/inventory/new_server.html -->
{% extends 'inventory/base.html' %}

{% block content %}
<h1>Register New Server</h1>

<form action="{% url 'inventory:new_server' %}" method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button name="submit">Add Server</button>
</form>
{% endblock content %}

Logging Events

Event URL

path('servers/<int:server_id>/log_event/', views.log_event, name='log_event'),

Event View

from .forms import ServerForm, EventForm

def log_event(request, server_id):
    """Log an event for a server."""
    server = Server.objects.get(id=server_id)

    if request.method != 'POST':
        form = EventForm()
    else:
        form = EventForm(data=request.POST)
        if form.is_valid():
            new_event = form.save(commit=False)
            new_event.server = server
            new_event.save()
            return redirect('inventory:server_detail', server_id=server_id)

    context = {'server': server, 'form': form}
    return render(request, 'inventory/log_event.html', context)

Event Template

<!-- inventory/templates/inventory/log_event.html -->
{% extends 'inventory/base.html' %}

{% block content %}
<h1>{{ server.hostname }}</h1>
<h2>Log Event</h2>

<form action="{% url 'inventory:log_event' server.id %}" method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button name="submit">Log Event</button>
</form>
{% endblock content %}

Editing Servers

# URL
path('servers/<int:server_id>/edit/', views.edit_server, name='edit_server'),

# View
def edit_server(request, server_id):
    """Edit server details."""
    server = Server.objects.get(id=server_id)

    if request.method != 'POST':
        form = ServerForm(instance=server)
    else:
        form = ServerForm(instance=server, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('inventory:server_detail', server_id=server.id)

    context = {'server': server, 'form': form}
    return render(request, 'inventory/edit_server.html', context)

User Authentication

Create accounts app:

python manage.py startapp accounts

Register App and URLs

# inv_project/settings.py
INSTALLED_APPS = [
    'inventory',
    'accounts',
    # ...
]

# inv_project/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', include('inventory.urls')),
]

Login Page

# accounts/urls.py
from django.urls import path, include

app_name = 'accounts'
urlpatterns = [
    path('', include('django.contrib.auth.urls')),
]

Create template: accounts/templates/registration/login.html

{% extends 'inventory/base.html' %}

{% block content %}

{% if form.errors %}
<p class="error">Invalid credentials. Try again.</p>
{% endif %}

<h1>Log In</h1>

<form action="{% url 'accounts:login' %}" method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <button name="submit">Log in</button>
</form>

{% endblock content %}

Login Redirect

# inv_project/settings.py
LOGIN_REDIRECT_URL = 'inventory:index'
LOGOUT_REDIRECT_URL = 'inventory:index'

Update Base Template

<!-- base.html nav section -->
<nav>
    <a href="{% url 'inventory:index' %}">Dashboard</a>
    <a href="{% url 'inventory:servers' %}">Servers</a>

    {% if user.is_authenticated %}
        | {{ user.username }}
        <a href="{% url 'accounts:logout' %}">Log out</a>
    {% else %}
        <a href="{% url 'accounts:login' %}">Log in</a>
    {% endif %}
</nav>

User Registration

# accounts/urls.py
from django.urls import path, include
from . import views

app_name = 'accounts'
urlpatterns = [
    path('', include('django.contrib.auth.urls')),
    path('register/', views.register, name='register'),
]
# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm

def register(request):
    """Register a new user."""
    if request.method != 'POST':
        form = UserCreationForm()
    else:
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            login(request, new_user)
            return redirect('inventory:index')

    context = {'form': form}
    return render(request, 'registration/register.html', context)

Restricting Access

@login_required Decorator

# inventory/views.py
from django.contrib.auth.decorators import login_required

@login_required
def servers(request):
    """List servers - requires authentication."""
    servers = Server.objects.order_by('hostname')
    context = {'servers': servers}
    return render(request, 'inventory/servers.html', context)

@login_required
def new_server(request):
    # ... form handling ...

Apply to all views that modify data.

Login URL Setting

# inv_project/settings.py
LOGIN_URL = 'accounts:login'

Data Ownership

Add Owner to Server

# inventory/models.py
from django.contrib.auth.models import User

class Server(models.Model):
    hostname = models.CharField(max_length=100)
    ip_address = models.GenericIPAddressField()
    os = models.CharField(max_length=50, default='linux')
    status = models.CharField(max_length=20, default='active')
    date_added = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.hostname} ({self.ip_address})"

Migrate with Default

python manage.py makemigrations inventory

Django asks for default. Choose option 1, enter 1 (admin user ID).

python manage.py migrate

Restrict Servers to Owner

@login_required
def servers(request):
    """Show servers owned by current user."""
    servers = Server.objects.filter(owner=request.user).order_by('hostname')
    context = {'servers': servers}
    return render(request, 'inventory/servers.html', context)

Associate New Servers with User

@login_required
def new_server(request):
    if request.method != 'POST':
        form = ServerForm()
    else:
        form = ServerForm(data=request.POST)
        if form.is_valid():
            new_server = form.save(commit=False)
            new_server.owner = request.user
            new_server.save()
            return redirect('inventory:servers')

    context = {'form': form}
    return render(request, 'inventory/new_server.html', context)

Protecting Server Pages

from django.http import Http404

@login_required
def server_detail(request, server_id):
    """Show server details - only if owned by user."""
    server = Server.objects.get(id=server_id)

    if server.owner != request.user:
        raise Http404

    events = server.event_set.all()
    context = {'server': server, 'events': events}
    return render(request, 'inventory/server_detail.html', context)

Quick Reference

Forms Code

Create ModelForm

class MyForm(forms.ModelForm):

Render in template

{{ form.as_div }}

Process POST

form = Form(data=request.POST)

Validate

form.is_valid()

Save

form.save()

Save without commit

form.save(commit=False)

Pre-fill form

Form(instance=obj)

Auth Code

Require login

@login_required

Check in template

{% if user.is_authenticated %}

Get current user

request.user

Log in user

login(request, user)

User creation form

UserCreationForm()

Restrict queryset

.filter(owner=request.user)

Exercises

19-1. Delete Server

Add ability to delete a server (with confirmation).

19-2. Bulk Import

Create a form to import multiple servers from CSV.

19-3. Event Types

Use Django’s choices parameter for event_type field.

19-4. Team Access

Add a Team model. Users belong to teams. Team members see team’s servers.

Summary

  • ModelForms generate forms from models automatically

  • GET displays form, POST processes form data

  • commit=False creates object without saving

  • {% csrf_token %} protects against CSRF attacks

  • Django’s auth system handles login/logout/registration

  • @login_required restricts views to authenticated users

  • ForeignKey to User enables data ownership

  • Filter querysets by owner=request.user

  • Return Http404 for unauthorized access

Next: Styling and deployment.