Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ERD.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file added __init__.py
Empty file.
Empty file added admin.py
Empty file.
5 changes: 5 additions & 0 deletions apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig

class TicketsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tickets'
37 changes: 37 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
ROLE_CHOICES = [
('customer', 'Customer'),
('agent', 'Agent'),
('admin', 'Admin'),
]
role = models.CharField(max_length=20, choices=ROLE_CHOICES)

class Ticket(models.Model):
STATUS_CHOICES = [
('open', 'Open'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
]
PRIORITY_CHOICES = [
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
]

customer = models.ForeignKey(User, related_name='tickets_created', on_delete=models.CASCADE)
agent = models.ForeignKey(User, related_name='tickets_assigned', on_delete=models.SET_NULL, null=True, blank=True)
subject = models.CharField(max_length=255)
description = models.TextField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class TicketStatusHistory(models.Model):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='status_history')
status = models.CharField(max_length=20, choices=Ticket.STATUS_CHOICES)
changed_by = models.ForeignKey(User, on_delete=models.CASCADE)
changed_at = models.DateTimeField(auto_now_add=True)
42 changes: 42 additions & 0 deletions readmee.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 🎫 Ticketing System Backend – Database Schema

This project defines the PostgreSQL schema and design rationale for a secure and scalable ticketing system backend using Django and Railway-hosted PostgreSQL.

---

## 📐 ERD Diagram

![ERD](./erd.png)
> *(Attach and rename your ERD image as `erd.png` in the schema folder)*

---

## 🧱 Database Design Overview

### 📌 Entities
- **User**: Inherits from Django’s `AbstractUser`. Includes a `role` field (enum: `customer`, `agent`, `admin`).
- **Ticket**: Represents customer-submitted support tickets.
- **TicketStatusHistory**: Audit trail of status changes for each ticket.

### 🧩 Relationships
- One user (customer) can create many tickets.
- One ticket may be assigned to one agent.
- Each ticket can have multiple status changes over time.

---

## 🔐 Row-Level Security (RLS)

To ensure **multi-tenant security**:

- RLS is enabled on the `tickets_ticket` table.
- A PostgreSQL policy restricts customers to only view **their own tickets**.
- Agents can only access **assigned or unassigned (open) tickets**.

Example RLS policy for customers:

```sql
CREATE POLICY customer_tickets_policy
ON tickets_ticket
FOR SELECT
USING (customer_id = current_setting('myapp.current_user_id')::int);
65 changes: 65 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = 'django-insecure-replace-this-in-production'
DEBUG = True
ALLOWED_HOSTS = []

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'tickets',
]

AUTH_USER_MODEL = 'tickets.User'

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'config.wsgi.application'

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}

AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
6 changes: 6 additions & 0 deletions urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib import admin
from django.urls import path

urlpatterns = [
path('admin/', admin.site.urls),
]
Empty file added views.py
Empty file.
4 changes: 4 additions & 0 deletions wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()