initial commit

This commit is contained in:
2026-02-02 10:52:20 +05:30
parent 2767fdf674
commit ab7373fc34
73 changed files with 2 additions and 14 deletions
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
# Unregister default User model if it's registered
# admin.site.unregister(User) # Only needed if User was previously registered
@admin.register(User)
class CustomUserAdmin(UserAdmin):
list_display = ('username', 'email', 'phone', 'is_citizen', 'is_staff')
list_filter = ('is_citizen', 'is_moderator', 'is_resolver', 'is_staff')
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'email', 'phone')}),
('Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser', 'is_citizen', 'is_moderator', 'is_resolver', 'groups', 'user_permissions'),
}),
('Important dates', {'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'email', 'phone', 'password1', 'password2'),
}),
)
+9
View File
@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
def ready(self):
# Import signals or other startup code here if needed
pass
+47
View File
@@ -0,0 +1,47 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User, Issue, Comment
class CitizenRegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
phone = forms.CharField(max_length=15, required=False)
class Meta:
model = User
fields = ['username', 'email', 'phone', 'password1', 'password2']
def save(self, commit=True):
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.phone = self.cleaned_data['phone']
user.is_citizen = True
if commit:
user.save()
return user
class IssueForm(forms.ModelForm):
class Meta:
model = Issue
fields = ['title', 'description', 'location', 'latitude', 'longitude', 'photo']
widgets = {
'latitude': forms.HiddenInput(),
'longitude': forms.HiddenInput(),
'description': forms.Textarea(attrs={'rows': 4, 'placeholder': 'Describe the issue in detail...'}),
}
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ["content"]
widgets = {
"content": forms.Textarea(attrs={"rows": 2, "placeholder": "Add a comment..."})
}
class IssueAssignForm(forms.ModelForm):
class Meta:
model = Issue
fields = ['department']
widgets = {
'department': forms.Select(attrs={'class': 'form-select form-select-sm'}),
}
+22
View File
@@ -0,0 +1,22 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
import os
class Command(BaseCommand):
help = "Create a superuser if none exists (using env vars)."
def handle(self, *args, **options):
User = get_user_model()
username = os.environ.get("SUPERUSER_USERNAME")
email = os.environ.get("SUPERUSER_EMAIL")
password = os.environ.get("SUPERUSER_PASSWORD")
if not username or not password:
self.stdout.write(self.style.WARNING("SUPERUSER_* env vars not set. Skipping."))
return
if not User.objects.filter(username=username).exists():
User.objects.create_superuser(username=username, email=email, password=password)
self.stdout.write(self.style.SUCCESS(f"Superuser '{username}' created."))
else:
self.stdout.write(self.style.NOTICE(f"Superuser '{username}' already exists."))
+111
View File
@@ -0,0 +1,111 @@
# Generated by Django 5.2.5 on 2025-08-26 06:15
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(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')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_citizen', models.BooleanField(default=False)),
('is_moderator', models.BooleanField(default=False)),
('is_resolver', models.BooleanField(default=False)),
('phone', models.CharField(blank=True, max_length=15, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='core_user_groups', related_query_name='core_user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='core_user_permissions', related_query_name='core_user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Department',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('description', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('admin', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin_of_department', to=settings.AUTH_USER_MODEL)),
('users', models.ManyToManyField(blank=True, related_name='departments', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Issue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.TextField()),
('location', models.CharField(blank=True, max_length=200)),
('latitude', models.FloatField(blank=True, null=True)),
('longitude', models.FloatField(blank=True, null=True)),
('photo', models.ImageField(blank=True, null=True, upload_to='issue_photos/', validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg', 'png', 'gif'])])),
('status', models.CharField(choices=[('reported', 'Reported'), ('acknowledged', 'Acknowledged'), ('in_progress', 'In Progress'), ('resolved', 'Resolved')], default='reported', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues', to='core.department')),
('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reported_issues', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='core.comment')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.issue')),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='Vote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='core.issue')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'issue')},
},
),
]
@@ -0,0 +1,23 @@
# Generated by Django 5.2.5 on 2025-08-26 08:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='banned_until',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='user',
name='is_banned',
field=models.BooleanField(default=False),
),
]
+1
View File
@@ -0,0 +1 @@
Binary file not shown.
+184
View File
@@ -0,0 +1,184 @@
from cloudinary.models import CloudinaryField
from datetime import timedelta
from django.contrib.auth.models import AbstractUser
from django.core.validators import FileExtensionValidator
from django.conf import settings
from django.db import models
from django.utils import timezone
class User(AbstractUser):
is_citizen = models.BooleanField(default=False)
is_moderator = models.BooleanField(default=False)
is_resolver = models.BooleanField(default=False)
phone = models.CharField(max_length=15, blank=True, null=True)
groups = models.ManyToManyField(
'auth.Group',
verbose_name='groups',
blank=True,
help_text='The groups this user belongs to.',
related_name='core_user_groups',
related_query_name='core_user',
)
user_permissions = models.ManyToManyField(
'auth.Permission',
verbose_name='user permissions',
blank=True,
help_text='Specific permissions for this user.',
related_name='core_user_permissions',
related_query_name='core_user',
)
# Ban-related fields
is_banned = models.BooleanField(default=False)
banned_until = models.DateTimeField(null=True, blank=True)
def ban(self, days=7):
"""Ban user for given number of days (default = 7)."""
self.is_banned = True
self.banned_until = timezone.now() + timedelta(days=days)
self.save()
def unban(self):
"""Unban user immediately."""
self.is_banned = False
self.banned_until = None
self.save()
def is_currently_banned(self):
"""Check if user is still banned (auto-unban if expired)."""
if self.is_banned and self.banned_until:
if timezone.now() >= self.banned_until:
# Auto unban if ban expired
self.unban()
return False
return True
return False
class Department(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
# Each department can have many users
users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="departments",
blank=True
)
# One admin per department
admin = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
related_name="admin_of_department",
null=True,
blank=True
)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
class Issue(models.Model):
STATUS_REPORTED = 'reported'
STATUS_ACKNOWLEDGED = 'acknowledged'
STATUS_IN_PROGRESS = 'in_progress'
STATUS_RESOLVED = 'resolved'
STATUS_CHOICES = [
(STATUS_REPORTED, 'Reported'),
(STATUS_ACKNOWLEDGED, 'Acknowledged'),
(STATUS_IN_PROGRESS, 'In Progress'),
(STATUS_RESOLVED, 'Resolved'),
]
title = models.CharField(max_length=200)
description = models.TextField()
reporter = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="reported_issues"
)
# 🔹 Add relation to department
department = models.ForeignKey(
Department,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="issues"
)
location = models.CharField(max_length=200, blank=True)
latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True)
photo = CloudinaryField(
'images',
blank=True,
null=True,
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default=STATUS_REPORTED
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-created_at"] # 🔹 latest issues first by default
def __str__(self):
return f"{self.title} ({self.get_status_display()})"
# 🔹 Helpers
def vote_count(self):
return self.votes.count() if hasattr(self, "votes") else 0
def has_user_voted(self, user):
if user.is_authenticated and hasattr(self, "votes"):
return self.votes.filter(user=user).exists()
return
def assign_to_department(self, department):
"""Assign issue to a department and auto-update status to acknowledged"""
self.department = department
self.status = self.STATUS_ACKNOWLEDGED
self.save()
class Vote(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='votes')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'issue') # Prevent duplicate votes per user
def __str__(self):
return f"{self.user.username} voted on {self.issue.title}"
class Comment(models.Model):
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
user = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField() # <--- field name is "content", not "text"
parent = models.ForeignKey("self", null=True, blank=True, related_name="replies", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]
def __str__(self):
return f"Comment by {self.user} on {self.issue}"
@property
def is_reply(self):
return self.parent is not None
+180
View File
@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
Civixfix - {% block title %}Community Issue Reporting Platform{% endblock %}
</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="preload" as="image" href="https://a.tile.openstreetmap.org/13/4521/2632.png">
<link rel="preload" as="image" href="https://b.tile.openstreetmap.org/13/4521/2632.png">
<link rel="preload" as="font"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/webfonts/fa-solid-900.woff2" type="font/woff2"
crossorigin>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<!-- Custom CSS -->
<style>
.hero-section {
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)),
url("https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?ixlib=rb-4.0.3");
background-size: cover;
background-position: center;
color: white;
padding: 100px 0;
margin-bottom: 30px;
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #0d6efd;
}
.issue-card {
transition: transform 0.3s;
}
.issue-card:hover {
transform: translateY(-5px);
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div class="container">
<a class="navbar-brand" href="{% url 'home' %}">
<i class="fas fa-bullhorn me-2"></i>Civixfix
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link active" href="{% url 'home' %}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#features">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#how-it-works">How It Works</a>
</li>
{% if user.is_authenticated and user.is_citizen %}
<li class="nav-item">
<a class="nav-link" href="{% url 'citizen_dashboard' %}">Dashboard</a>
</li>
{% endif %}
</ul>
<div class="d-flex">
{% if user.is_authenticated %}
<div class="dropdown">
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-user me-1"></i> {{ user.username }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if user.is_citizen %}
<li>
<a class="dropdown-item" href="{% url 'citizen_dashboard' %}">
<i class="fas fa-users me-2 text-primary"></i>Citizen Dashboard</a>
</li>
{% endif %}
{% if user.is_resolver %}
<li>
<a class="dropdown-item" href="{% url 'department_dashboard' %}">
<i class="fas fa-building me-2 text-success"></i>Department Dashboard</a>
</li>
{% endif %}
{% if user.is_superuser %}
<li>
<a class="dropdown-item text-danger fw-bold" href="{% url 'superadmin_dashboard' %}">
<i class="fas fa-crown me-1 text-warning fw-bold"></i> Super Admin
</a>
</li>
{% endif %}
<li>
<hr class="dropdown-divider" />
</li>
<li>
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="dropdown-item"><i class="fas fa-right-from-bracket me-2"></i>Logout</button>
</form>
</li>
</ul>
</div>
{% else %}
<!-- Login / Register buttons for anonymous users -->
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
<a href="{% url 'register' %}" class="btn btn-primary">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main>{% block content %}{% endblock %}</main>
<!-- Footer -->
<footer class="text-black py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-4 mb-3">
<h5><i class="fas fa-bullhorn me-2"></i>Civixfix</h5>
<p class="text-muted">
Empowering communities through transparent issue reporting and
resolution.
</p>
</div>
<div class="col-md-4 mb-3">
<h5>Quick Links</h5>
<ul class="list-unstyled">
<li>
<a href="#features" class="text-decoration-none text-muted">Features</a>
</li>
<li>
<a href="#how-it-works" class="text-decoration-none text-muted">How It Works</a>
</li>
<li>
<a href="#" class="text-decoration-none text-muted">Privacy Policy</a>
</li>
</ul>
</div>
<div class="col-md-4 mb-3">
<h5>Contact</h5>
<ul class="list-unstyled text-muted">
<li>
<i class="fas fa-envelope me-2"></i> gokuldevse2001@gmail.com
</li>
<li><i class="fas fa-phone me-2"></i> +91 8129329073</li>
</ul>
</div>
</div>
<hr class="my-4 bg-secondary" />
<div class="text-center text-muted">
<small>&copy; {% now "Y" %} Civixfix. All rights reserved.</small>
</div>
</div>
</footer>
<!-- Bootstrap 5 JS Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+297
View File
@@ -0,0 +1,297 @@
{% extends "core/base.html" %}
{% block content %}
<!-- Hero Section -->
<section class="hero-section text-center">
<div class="container">
<h1 class="display-4 fw-bold mb-4">Report. Resolve. Rejoice.</h1>
<p class="lead mb-5">Your voice matters in making our community better. Report local issues and track their
resolution in real-time.</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
{% if user.is_authenticated %}
<a href="{% url 'citizen_dashboard' %}" class="btn btn-primary btn-lg px-4 gap-3">Report an Issue</a>
{% else %}
<a href="{% url 'login' %}" class="btn btn-primary btn-lg px-4 gap-3">Report an Issue</a>
{% endif %}
<a href="#how-it-works" class="btn btn-outline-light btn-lg px-4">Learn More</a>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="container mb-5">
<div class="row text-center">
<div class="col-md-3">
<h3 class="fw-bold text-primary">{{ total_issues|default:"0" }}+</h3>
<p class="text-muted">Issues Reported</p>
</div>
<div class="col-md-3">
<h3 class="fw-bold text-primary">{{ resolved_issues|default:"0" }}+</h3>
<p class="text-muted">Issues Resolved</p>
</div>
<div class="col-md-3">
<h3 class="fw-bold text-primary">{{ active_users|default:"0" }}+</h3>
<p class="text-muted">Active Users</p>
</div>
<div class="col-md-3">
<h3 class="fw-bold text-primary">{{ total_departments|default:"0" }}</h3>
<p class="text-muted">Municipal Departments</p>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="container mb-5">
<div class="text-center mb-5">
<h2 class="fw-bold">Why Choose Civixfix?</h2>
<p class="lead text-muted">Transparent, efficient, and community-driven problem solving</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<div class="feature-icon">
<i class="fas fa-map-marked-alt"></i>
</div>
<h5 class="card-title">Location-Based Reporting</h5>
<p class="card-text text-muted">Pinpoint issues on an interactive map for accurate location tracking
and faster resolution.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<div class="feature-icon">
<i class="fas fa-tasks"></i>
</div>
<h5 class="card-title">Real-Time Tracking</h5>
<p class="card-text text-muted">Follow your reported issues through every stage from reporting to
resolution.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<div class="feature-icon">
<i class="fas fa-users"></i>
</div>
<h5 class="card-title">Community Engagement</h5>
<p class="card-text text-muted">Vote and comment on issues to help prioritize what matters most to
your neighborhood.</p>
</div>
</div>
</div>
</div>
</section>
<!-- How It Works Section -->
<section id="how-it-works" class="bg-light py-5 mb-5">
<div class="container">
<div class="text-center mb-5">
<h2 class="fw-bold">How Civixfix Works</h2>
<p class="lead text-muted">Simple steps to make your community better</p>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<span class="badge bg-primary rounded-circle mb-3"
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">1</span>
<h5 class="card-title">Report an Issue</h5>
<p class="card-text text-muted">Take a photo, add details, and drop a pin on the map to report
problems in your area.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<span class="badge bg-primary rounded-circle mb-3"
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">2</span>
<h5 class="card-title">Community Support</h5>
<p class="card-text text-muted">Others can vote and comment to show support and add details to
your report.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center p-4">
<span class="badge bg-primary rounded-circle mb-3"
style="width: 50px; height: 50px; line-height: 50px; font-size: 1.5rem;">3</span>
<h5 class="card-title">Official Response</h5>
<p class="card-text text-muted">Municipal authorities receive, prioritize, and work on resolving
the issues.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Recent Issues Section -->
<section class="container mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold">Recently Reported Issues</h2>
{% if user.is_authenticated %}
<a href="{% url 'view_all_issues' %}" class="btn btn-primary">View All Issues</a>
{% else %}
<a href="{% url 'login' %}" class="btn btn-outline-primary">View All Issues</a>
{% endif %}
</div>
<div class="row g-4">
{% for issue in recent_issues %}
<div class="col-md-4">
<div class="card issue-card h-100">
{% if issue.photo %}
<img src="{{ issue.photo.url }}" class="card-img-top" alt="{{ issue.title }}"
style="height: 200px; object-fit: cover;">
{% else %}
<img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top"
alt="No image">
{% endif %}
<div class="card-body">
<span class="badge
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
{% elif issue.department.name == 'Public Safety' %} bg-danger
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
{% else %} bg-light text-dark
{% endif %} mb-2">
{{ issue.department.name|default:"General" }}
</span>
<h5 class="card-title">{{ issue.title|truncatewords:5 }}</h5>
<p class="card-text text-muted">{{ issue.description|truncatewords:15 }}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted"><i class="fas fa-map-marker-alt me-1"></i> {{ issue.location|truncatewords:2 }}</small>
<!-- Vote Button -->
<div class="vote-section">
{% if user.is_authenticated %}
<button
class="btn btn-sm btn-outline-primary vote-btn {% if issue.user_has_voted %}active{% endif %}"
data-issue-id="{{ issue.id }}">
<i class="fas fa-thumbs-up"></i>
<span class="vote-count">{{ issue.vote_count }}</span>
</button>
{% else %}
<a href="{% url 'login' %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-thumbs-up"></i>
<span class="vote-count">{{ issue.vote_count }}</span>
</a>
{% endif %}
</div>
</div>
</div>
<div class="card-footer bg-transparent">
<small
class="text-{% if issue.status == 'resolved' %}success{% elif issue.status == 'in_progress' %}warning{% else %}info{% endif %}">
<i
class="fas fa-{% if issue.status == 'resolved' %}check-circle{% elif issue.status == 'in_progress' %}tasks{% else %}clock{% endif %} me-1"></i>
{{ issue.get_status_display }}
</small>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h4>No issues reported yet</h4>
<p class="text-muted">Be the first to report an issue in your community!</p>
<a href="{% url 'login' %}" class="btn btn-primary">Get Started</a>
</div>
{% endfor %}
</div>
</section>
<!-- Call to Action -->
<section class="bg-primary text-white py-5">
<div class="container text-center">
<h2 class="fw-bold mb-4">Ready to make a difference in your community?</h2>
<p class="lead mb-4">Join {{ active_users|default:"thousands of" }} citizens who are actively improving their
neighborhoods.</p>
{% if user.is_authenticated %}
<a href="{% url 'citizen_dashboard' %}" class="btn btn-light btn-lg px-4">Report an Issue</a>
{% else %}
<a href="{% url 'login' %}" class="btn btn-light btn-lg px-4">Get Started Now</a>
{% endif %}
</div>
</section>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function () {
// Vote functionality
$('.vote-btn').click(function () {
const issueId = $(this).data('issue-id');
const button = $(this);
// Show loading state
const originalHtml = button.html();
button.prop('disabled', true);
button.html('<i class="fas fa-spinner fa-spin"></i>');
// Send vote request
$.ajax({
url: "{% url 'vote_issue' 0 %}".replace('0', issueId),
type: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}'
},
success: function (response) {
if (response.success) {
// Update button state
if (response.voted) {
button.addClass('active');
} else {
button.removeClass('active');
}
// Update vote count
button.find('.vote-count').text(response.vote_count);
// Update button text
button.html('<i class="fas fa-thumbs-up"></i> <span class="vote-count">' + response.vote_count + '</span>');
} else {
alert('Error: ' + response.error);
button.html(originalHtml);
}
},
error: function () {
alert('Error voting. Please try again.');
button.html(originalHtml);
},
complete: function () {
button.prop('disabled', false);
}
});
});
});
</script>
<style>
.vote-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
.vote-btn:hover:not(:disabled) {
transform: scale(1.05);
transition: transform 0.2s;
}
.vote-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>
{% endblock %}
+54
View File
@@ -0,0 +1,54 @@
{% extends "core/base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body p-5">
<h2 class="card-title text-center mb-4">Login to Your Account</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="id_username" class="form-label">Username</label>
<input type="text" name="username" class="form-control" id="id_username" required>
</div>
<div class="mb-4">
<label for="id_password" class="form-label">Password</label>
<input type="password" name="password" class="form-control" id="id_password" required>
</div>
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary btn-lg">Login</button>
</div>
<div class="text-center">
<a href="#" class="text-decoration-none">Forgot password?</a>
</div>
</form>
<hr class="my-4">
<p class="text-center text-muted">
Don't have an account? <a href="{% url 'register' %}" class="text-decoration-none">Register here</a>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h3>Manage Citizens</h3>
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
</div>
<div class="card-body">
{% if citizens %}
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>No.</th>
<th>Username</th>
<th>Email</th>
<th>Phone No.</th>
<th>Date Joined</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for citizen in citizens %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ citizen.username }}</td>
<td>{{ citizen.email }}</td>
<td>{{ citizen.phone }}</td>
<td>{{ citizen.date_joined|date:"M d, Y" }}</td>
<td>
{% if citizen.is_currently_banned %}
<span class="badge bg-danger">Banned until {{ citizen.banned_until|date:"M d, Y" }}</span>
{% else %}
<span class="badge bg-success">Active</span>
{% endif %}
</td>
<td>
{% if citizen.is_currently_banned %}
<a href="{% url 'unban_user' citizen.id %}" class="btn btn-sm btn-success">
<i class="fas fa-unlock"></i> Unban
</a>
{% else %}
<a href="{% url 'ban_user' citizen.id %}" class="btn btn-sm btn-danger">
<i class="fas fa-ban"></i> Ban
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No citizen users found.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+103
View File
@@ -0,0 +1,103 @@
{% extends "core/base.html" %}
{% block title %}Register as Citizen{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body p-5">
<h2 class="card-title text-center mb-4">Create Citizen Account</h2>
<form method="post">
{% csrf_token %}
{# Messages block - FIXED SYNTAX #}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{# Form errors block - FIXED SYNTAX #}
{% if form.errors %}
<div class="alert alert-danger">
<strong>Error!</strong> Please correct the following:
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
<div class="mb-3">
<label for="id_username" class="form-label">Username</label>
{{ form.username }}
<small class="text-muted">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</small>
</div>
<div class="mb-3">
<label for="id_email" class="form-label">Email</label>
{{ form.email }}
</div>
<div class="mb-3">
<label for="id_phone" class="form-label">Phone (Optional)</label>
{{ form.phone }}
</div>
<div class="mb-3">
<label for="id_password1" class="form-label">Password</label>
{{ form.password1 }}
<small class="text-muted">
<ul>
<li>Your password can't be too similar to your other personal information.</li>
<li>Your password must contain at least 8 characters.</li>
<li>Your password can't be a commonly used password.</li>
<li>Your password can't be entirely numeric.</li>
</ul>
</small>
</div>
<div class="mb-4">
<label for="id_password2" class="form-label">Password Confirmation</label>
{{ form.password2 }}
<small class="text-muted">Enter the same password as before, for verification.</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Register</button>
</div>
</form>
<hr class="my-4">
<p class="text-center text-muted">
Already have an account? <a href="{% url 'login' %}" class="text-decoration-none">Login here</a>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function () {
// Add Bootstrap classes to all input fields
$('input:not([type="checkbox"]):not([type="radio"])').addClass('form-control');
$('input[type="checkbox"]').removeClass('form-control').addClass('form-check-input');
$('input[type="radio"]').removeClass('form-control').addClass('form-check-input');
// Add Bootstrap classes to select fields
$('select').addClass('form-select');
});
</script>
{% endblock %}
@@ -0,0 +1,91 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-warning text-dark">
<h3>Analytics & Reports</h3>
</div>
<div class="card-body">
<h5>Total Issues Reported:
<span class="badge bg-dark">{{ total_issues }}</span>
</h5>
<canvas id="statusChart" height="120" class="mt-4"></canvas>
<canvas id="deptChart" height="120" class="mt-4"></canvas>
<canvas id="citizenChart" height="120" class="mt-4"></canvas>
<canvas id="trendChart" height="120" class="mt-4"></canvas>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{# Safely pass JSON to JS #}
{{ status_counts|json_script:"status-data" }}
{{ top_departments|json_script:"dept-data" }}
{{ top_citizens|json_script:"citizen-data" }}
{{ issues_last_30_days|json_script:"trend-data" }}
<script>
const statusCounts = JSON.parse(document.getElementById("status-data").textContent);
const deptCounts = JSON.parse(document.getElementById("dept-data").textContent);
const citizenCounts = JSON.parse(document.getElementById("citizen-data").textContent);
const trendCounts = JSON.parse(document.getElementById("trend-data").textContent);
// Status chart
new Chart(document.getElementById('statusChart'), {
type: 'pie',
data: {
labels: statusCounts.map(item => item.status),
datasets: [{
data: statusCounts.map(item => item.count),
backgroundColor: ['#dc3545', '#0dcaf0', '#ffc107', '#198754']
}]
}
});
// Department chart
new Chart(document.getElementById('deptChart'), {
type: 'bar',
data: {
labels: deptCounts.map(item => item.department__name),
datasets: [{
label: 'Issues by Department',
data: deptCounts.map(item => item.count),
backgroundColor: '#0d6efd'
}]
}
});
// Citizens chart
new Chart(document.getElementById('citizenChart'), {
type: 'bar',
data: {
labels: citizenCounts.map(item => item.reporter__username),
datasets: [{
label: 'Top Citizens',
data: citizenCounts.map(item => item.count),
backgroundColor: '#6610f2'
}]
}
});
// Issues over time
new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: trendCounts.map(item => item.day),
datasets: [{
label: 'Issues Last 30 Days',
data: trendCounts.map(item => item.count),
borderColor: '#fd7e14',
fill: false,
tension: 0.2
}]
}
});
</script>
{% endblock %}
@@ -0,0 +1,289 @@
{% extends "core/base.html" %}
{% block title %}Citizen Dashboard - CivixFix{% endblock %}
{% block extra_css %}
<style>
.dashboard-card {
transition: transform 0.2s;
}
.dashboard-card:hover {
transform: translateY(-2px);
}
.map-container {
height: 300px;
border-radius: 8px;
overflow: hidden;
border: 2px dashed #dee2e6;
}
.issue-status {
font-size: 0.8rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<!-- Sidebar - Quick Actions -->
<div class="col-md-3">
<div class="card shadow-sm dashboard-card">
<div class="card-body text-center">
<h5 class="card-title">Welcome, {{ user.username }}</h5>
<p class="text-muted">Citizen Dashboard</p>
<div class="d-grid gap-2 mb-3">
<button class="btn btn-primary btn-lg" data-bs-toggle="modal" data-bs-target="#reportIssueModal">
<i class="fas fa-plus me-2"></i>Report New Issue
</button>
</div>
<hr>
<h6>Quick Stats</h6>
<div class="row text-center">
<div class="col-6">
<div class="bg-light p-2 rounded">
<h4 class="mb-0 text-primary">{{ user_issues.count }}</h4>
<small>My Reports</small>
</div>
</div>
<div class="col-6">
<div class="bg-light p-2 rounded">
<h4 class="mb-0 text-success">
{{ resolved_count }}
</h4>
<small>Resolved</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content - Recent Issues -->
<div class="col-md-9">
<!-- Welcome Message -->
<div class="card shadow-sm dashboard-card mb-4">
<div class="card-body">
<h4>Welcome to CivixFix!</h4>
<p class="text-muted mb-0">
Report community issues, track their progress, and help make your neighborhood better.
Start by reporting an issue using the button on the left.
</p>
</div>
</div>
<!-- My Recent Issues -->
<div class="card shadow-sm dashboard-card">
<div class="card-header">
<h5 class="mb-0">My Recent Reports</h5>
</div>
<div class="card-body">
{% for issue in user_issues %}
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="card-title mb-1">{{ issue.title }}</h6>
<p class="card-text text-muted mb-2 small">
{{ issue.description|truncatewords:15 }}
</p>
<div class="d-flex gap-2 align-items-center">
<span class="badge
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
{% elif issue.department.name == 'Public Safety' %} bg-danger
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
{% else %} bg-light text-dark
{% endif %}">
{{ issue.department.name|default:"No Department" }}
</span>
<!-- 🔹 Status badge -->
<span class="badge bg-{% if issue.status == 'resolved' %}success
{% elif issue.status == 'in_progress' %}warning
{% else %}primary{% endif %} issue-status">
{{ issue.get_status_display }}
</span>
<small class="text-muted">{{ issue.created_at|date:"M d, Y" }}</small>
</div>
</div>
{% if issue.photo %}
<div class="ms-3">
<img src="{{ issue.photo.url }}" alt="Issue photo" class="img-fluid rounded"
style="max-height: 60px; max-width: 80px;">
</div>
{% endif %}
</div>
</div>
</div>
{% empty %}
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5>No issues reported yet</h5>
<p class="text-muted">Click "Report New Issue" to get started!</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Report Issue Modal -->
<div class="modal fade" id="reportIssueModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Report New Issue</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="issueForm" method="post" enctype="multipart/form-data" action="{% url 'report_issue' %}">
{% csrf_token %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mb-3">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Issue Title*</label>
{{ issue_form.title }}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description*</label>
{{ issue_form.description }}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Location*</label>
{{ issue_form.location }}
<small class="text-muted">Click on the map to select location</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Photo</label>
{{ issue_form.photo }}
</div>
</div>
</div>
<!-- Hidden latitude/longitude fields -->
{{ issue_form.latitude }}
{{ issue_form.longitude }}
<div class="map-container mb-3">
<div id="map" style="height: 100%;"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="issueForm" class="btn btn-primary">
<i class="fas fa-paper-plane me-2"></i>Report Issue
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener("DOMContentLoaded", () => {
let map = null, marker = null;
// Lazy load Leaflet only when modal opens
const ensureLeafletLoaded = () => {
return new Promise((resolve) => {
if (window.L) return resolve();
const css = document.createElement("link");
css.rel = "stylesheet";
css.href = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.css";
document.head.appendChild(css);
const js = document.createElement("script");
js.src = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.js";
js.onload = resolve;
document.body.appendChild(js);
});
};
const initMap = () => {
if (map) return;
map = L.map("map", {
center: [12.9716, 77.5946],
zoom: 13
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "&copy; OpenStreetMap contributors",
maxZoom: 19
}).addTo(map);
map.on("click", (e) => {
if (!marker) {
marker = L.marker(e.latlng).addTo(map);
} else {
marker.setLatLng(e.latlng);
}
marker.bindPopup("Selected location").openPopup();
// update hidden fields immediately
document.getElementById("id_latitude").value = e.latlng.lat.toFixed(6);
document.getElementById("id_longitude").value = e.latlng.lng.toFixed(6);
// defer reverse geocode
requestIdleCallback(() => {
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${e.latlng.lat}&lon=${e.latlng.lng}`)
.then(r => r.json())
.then(data => {
document.getElementById("id_location").value =
data?.display_name ||
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
})
.catch(() => {
document.getElementById("id_location").value =
`Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
});
});
});
};
const modal = document.getElementById("reportIssueModal");
modal.addEventListener("shown.bs.modal", async () => {
await ensureLeafletLoaded();
initMap();
// fix resize/centering after animation
setTimeout(() => {
map.invalidateSize();
if (marker) map.setView(marker.getLatLng(), 15);
}, 200);
});
});
</script>
{% endblock %}
@@ -0,0 +1,80 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-primary text-white">
<h3>Department Dashboard</h3>
<p>
Welcome, {{ request.user.username }}. Department:
{% for dept in departments %}
<span class="badge bg-light text-dark">{{ dept.name }}</span>
{% empty %}
<span class="text-muted">No department assigned.</span>
{% endfor %}
</p>
</div>
<div class="card-body">
{% if issues %}
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>#</th>
<th>Title</th>
<th>Reported By</th>
<th>Location</th>
<th>Status</th>
<th>Created At</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for issue in issues %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ issue.title }}</td>
<td>{{ issue.reporter.username }}</td>
<td>{{ issue.location }}</td>
<td>
{% if issue.status == "reported" %}
<span class="badge bg-danger">Reported</span>
{% elif issue.status == "acknowledged" %}
<span class="badge bg-info text-dark">Acknowledged</span>
{% elif issue.status == "in_progress" %}
<span class="badge bg-warning text-dark">In Progress</span>
{% elif issue.status == "resolved" %}
<span class="badge bg-success">Resolved</span>
{% else %}
<span class="badge bg-secondary">Unknown</span>
{% endif %}
</td>
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
<td>
{% if issue.status in "reported,acknowledged" %}
<form method="post" action="{% url 'update_issue_status' issue.id %}">
{% csrf_token %}
<input type="hidden" name="status" value="in_progress">
<button type="submit" class="btn btn-sm btn-warning">In Progress</button>
</form>
{% elif issue.status == "in_progress" %}
<form method="post" action="{% url 'update_issue_status' issue.id %}">
{% csrf_token %}
<input type="hidden" name="status" value="resolved">
<button type="submit" class="btn btn-sm btn-success">Resolved</button>
</form>
{% else %}
<span class="text-muted">No actions</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No issues assigned to your departments yet.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,34 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-dark text-white">
<h3>Super Admin Dashboard</h3>
</div>
<div class="card-body">
<p>Welcome, {{ request.user.username }} <i class="fas fa-crown text-warning fw-bold"></i></p>
<ul class="list-group">
<li class="list-group-item">
<i class="fas fa-users me-2 text-primary"></i>
<a href="{% url 'manage_users' %}">Manage Users</a>
</li>
<li class="list-group-item">
<i class="fas fa-building me-2 text-success"></i>
<a href="{% url 'manage_departments' %}">Manage Departments</a>
</li>
<li class="list-group-item">
<i class="fas fa-exclamation-circle me-2 text-danger"></i>
<a href="{% url 'manage_issues' %}">Manage Issues</a>
</li>
<li class="list-group-item">
<i class="fas fa-chart-line me-2 text-warning"></i>
<a href="{% url 'superadmin_reports' %}">View Analytics & Reports</a>
</li>
</ul>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,77 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h3>{{ department.name }} Department</h3>
<a href="{% url 'manage_departments' %}" class="btn btn-light btn-sm">Back</a>
</div>
<div class="card-body">
<!-- Department Users -->
<h5>Department Users</h5>
{% if users %}
<ul class="list-group mb-3">
{% for user in users %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<strong>{{ user.username }}</strong> — {{ user.email }}
{% if department.admin and department.admin.id == user.id %}
<span class="badge bg-success ms-2">Admin</span>
{% endif %}
</span>
<!-- Assign as Admin button -->
{% if not department.admin or department.admin.id != user.id %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="assign_admin" value="1">
<input type="hidden" name="admin_user_id" value="{{ user.id }}">
<button type="submit" class="btn btn-sm btn-outline-primary">
Make Admin
</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">No users registered in this department yet.</p>
{% endif %}
<!-- Remove Admin -->
{% if department.admin %}
<form method="post" class="mt-2">
{% csrf_token %}
<input type="hidden" name="remove_admin" value="1">
<button type="submit" class="btn btn-sm btn-outline-danger">
Remove Admin
</button>
</form>
{% endif %}
<hr>
<!-- Register New User -->
<h5>Register New User for {{ department.name }}</h5>
<form method="post" class="row g-2">
{% csrf_token %}
<input type="hidden" name="create_user" value="1">
<div class="col-md-3">
<input type="text" name="username" class="form-control" placeholder="Username" required>
</div>
<div class="col-md-3">
<input type="email" name="email" class="form-control" placeholder="Email">
</div>
<div class="col-md-3">
<input type="password" name="password" class="form-control" placeholder="Password" required>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100">Create User</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,48 @@
<!-- core/templates/core/manage_departments.html -->
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h3>Manage Departments</h3>
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
</div>
<div class="card-body">
<!-- List departments -->
{% if departments %}
<ul class="list-group mb-3">
{% for dept in departments %}
<a href="{% url 'department_detail' dept.pk %}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>
<strong>{{ dept.name }}</strong><br>
<small class="text-muted">{{ dept.description }}</small>
</span>
<span class="badge bg-primary rounded-pill">Manage</span>
</a>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">No departments added yet.</p>
{% endif %}
<!-- Add new department form -->
<form method="post" class="mt-3">
{% csrf_token %}
<div class="row g-2">
<div class="col-md-4">
<input type="text" name="name" class="form-control" placeholder="Department name" required>
</div>
<div class="col-md-6">
<input type="text" name="description" class="form-control" placeholder="Description (optional)">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100"><i class="fas fa-plus me-2"></i>Add</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
+69
View File
@@ -0,0 +1,69 @@
{% extends "core/base.html" %}
{% load humanize %}
{% block content %}
<div class="container my-5">
<!-- Issue details -->
<div class="card shadow-sm mb-4">
{% if issue.photo %}
<div class="text-center my-3">
<img src="{{ issue.photo.url }}" class="img-fluid rounded shadow-sm" style="max-height: 400px; object-fit: contain;"
alt="{{ issue.title }}">
</div>
{% endif %}
<div class="card-body">
<h3>{{ issue.title }}</h3>
<p class="text-muted">{{ issue.description }}</p>
<p><i class="fas fa-map-marker-alt"></i> {{ issue.location }}</p>
<span class="badge bg-info">{{ issue.get_status_display }}</span>
</div>
</div>
<!-- Comments Section -->
<div class="card shadow-sm">
<div class="card-header">
<h5 class="mb-0">Comments</h5>
</div>
<div class="card-body">
{% for comment in comments %}
<div class="mb-3">
<strong>{{ comment.user.username }}</strong>
<small class="text-muted">{{ comment.created_at|naturaltime }}</small>
<p>{{ comment.content }}</p>
<!-- Replies -->
<div class="ms-4">
{% for reply in comment.replies.all %}
<div class="mb-2">
<strong>{{ reply.user.username }}</strong>
<small class="text-muted">{{ reply.created_at|naturaltime }}</small>
<p>{{ reply.content }}</p>
</div>
{% endfor %}
<!-- Reply form -->
{% if user.is_authenticated %}
<form method="post" action="{% url 'add_comment' issue.id comment.id %}">
{% csrf_token %}
<input type="content" name="content" class="form-control form-control-sm" placeholder="Reply...">
</form>
{% endif %}
</div>
</div>
{% empty %}
<p class="text-muted">No comments yet. Be the first!</p>
{% endfor %}
<!-- New comment form -->
{% if user.is_authenticated %}
<form method="post" action="{% url 'add_comment' issue.id %}" class="mt-3">
{% csrf_token %}
<textarea name="content" class="form-control" placeholder="Write a comment..."></textarea>
<button type="submit" class="btn btn-primary btn-sm mt-2">Post</button>
</form>
{% else %}
<p><a href="{% url 'login' %}">Login</a> to comment.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+81
View File
@@ -0,0 +1,81 @@
{% extends "core/base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-lg">
<div class="card-header bg-warning text-black d-flex justify-content-between align-items-center">
<h3>Manage Issues</h3>
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
</div>
<div class="card-body">
{% if issues %}
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>No.</th>
<th>Title</th>
<th>Reported By</th>
<th>Status</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for issue in issues %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ issue.title }}</td>
<td>{{ issue.reporter.username }}</td>
<td>
{% if issue.status == "reported" %}
<span class="badge bg-danger">Reported</span>
{% elif issue.status == "acknowledged" %}
<span class="badge bg-info text-dark">Acknowledged</span>
{% elif issue.status == "in_progress" %}
<span class="badge bg-warning text-dark">In Progress</span>
{% elif issue.status == "resolved" %}
<span class="badge bg-success">Resolved</span>
{% else %}
<span class="badge bg-secondary">Unknown</span>
{% endif %}
</td>
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
<td>
{% if issue.department %}
<span class="fw-bold text-primary">{{ issue.department.name }}</span>
{% else %}
<!-- Assign Department Form -->
<form method="post" action="" class="d-inline">
{% csrf_token %}
<input type="hidden" name="issue_id" value="{{ issue.id }}">
<select name="department" class="form-select form-select-sm d-inline w-auto">
<option value="">— Select Department —</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-sm btn-primary">Assign</button>
</form>
<!-- Report Fake Button -->
<a href="{% url 'delete_fake_issue' issue.id %}" class="btn btn-sm btn-danger ms-1"
onclick="return confirm('Are you sure you want to delete this issue as FAKE?');">
<i class="fas fa-ban"></i> Report Fake
</a>
<a href="{% url 'delete_issue' issue.id %}" class="btn btn-sm btn-outline-danger ms-1"
onclick="return confirm('Are you sure you want to permanently delete this issue? This cannot be undone.');">
<i class="fas fa-trash"></i> Delete
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">No issues reported yet.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+251
View File
@@ -0,0 +1,251 @@
{% extends "core/base.html" %}
{% load humanize %}
{% block content %}
<div class="container my-5">
<!-- Header + Filter -->
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
<h2 class="fw-bold mb-0">All Reported Issues</h2>
<form method="get" class="d-flex flex-wrap gap-2">
<select name="status" class="form-select">
<option value="">All Status</option>
<option value="reported" {% if request.GET.status|default:'' == "reported" %}selected{% endif %}>Reported
</option>
<option value="acknowledged" {% if request.GET.status|default:'' == "acknowledged" %}selected{% endif %}>
Acknowledged
</option>
<option value="in_progress" {% if request.GET.status|default:'' == "in_progress" %}selected{% endif %}>In
Progress
</option>
<option value="resolved" {% if request.GET.status|default:'' == "resolved" %}selected{% endif %}>Resolved
</option>
</select>
<select name="department" class="form-select">
<option value="">All Departments</option>
{% for dept in departments %}
{% with dept.id|stringformat:"s" as dept_id %}
<option value="{{ dept_id }}" {% if request.GET.department|default:'' == dept_id %}selected{% endif %}>
{{ dept.name }}
</option>
{% endwith %}
{% endfor %}
</select>
<button type="submit" class="btn btn-primary">
<i class="fas fa-filter me-1"></i> Filter
</button>
</form>
</div>
<!-- Issue Cards -->
<div class="row g-4">
{% for issue in issues %}
<div class="col-md-4">
<div class="card issue-card h-100 shadow-sm border-0">
{% if issue.photo %}
<img src="{{ issue.photo.url }}" class="card-img-top issue-image" alt="{{ issue.title }}"
style="height: 200px; object-fit: cover; cursor: pointer" data-bs-toggle="modal"
data-bs-target="#imageModal" data-image="{{ issue.photo.url }}" />
{% else %}
<img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top"
alt="No image" />
{% endif %}
<div class="card-body d-flex flex-column">
<!-- Department Badge -->
<span class="badge
{% if issue.department.name == 'Roads & Transportation' %} bg-primary
{% elif issue.department.name == 'Sanitation & Waste Management' %} bg-success
{% elif issue.department.name == 'Public Safety' %} bg-danger
{% elif issue.department.name == 'Water & Sewage' %} bg-info text-dark
{% elif issue.department.name == 'Parks & Recreation' %} bg-warning text-dark
{% elif issue.department.name == 'Electricity & Utilities' %} bg-dark
{% elif issue.department.name == 'Environmental Services' %} bg-secondary
{% elif issue.department.name == 'Public Works' %} bg-teal text-white
{% else %} bg-light text-dark
{% endif %} mb-2">
{{ issue.department.name|default:"General" }}
</span>
<!-- Title + Description -->
<h5 class="card-title">{{ issue.title|truncatewords:6 }}</h5>
<p class="card-text text-muted flex-grow-1">
{{ issue.description|truncatewords:18 }}
</p>
<!-- Location + Vote -->
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">
<i class="fas fa-map-marker-alt me-1"></i> {{ issue.location|truncatewords:2 }}
</small>
<div class="vote-section">
{% if user.is_authenticated %}
<button
class="btn btn-sm btn-outline-primary vote-btn {% if issue.user_has_voted %}active{% endif %}"
data-issue-id="{{ issue.id }}">
<i class="fas fa-thumbs-up"></i>
<span class="vote-count">{{ issue.vote_count }}</span>
</button>
{% else %}
<a href="{% url 'login' %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-thumbs-up"></i>
<span class="vote-count">{{ issue.vote_count }}</span>
</a>
{% endif %}
</div>
</div>
</div>
<div class="card-footer bg-transparent d-flex justify-content-between align-items-center">
<small
class="text-{% if issue.status == 'resolved' %}success{% elif issue.status == 'in_progress' %}warning{% else %}info{% endif %}">
<i
class="fas fa-{% if issue.status == 'resolved' %}check-circle{% elif issue.status == 'in_progress' %}tasks{% else %}clock{% endif %} me-1"></i>
{{ issue.get_status_display }}
</small>
<a href="{% url 'issue_detail' issue.id %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-comments"></i> Comments
</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h4>No issues reported yet</h4>
<p class="text-muted">
Be the first to report an issue in your community!
</p>
<a href="{% url 'citizen_dashboard' %}" class="btn btn-primary">Report an Issue</a>
</div>
{% endfor %}
</div>
</div>
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-light">
<div class="modal-body text-center p-0">
<img id="modalImage" src="" class="img-fluid zoomable" alt="Zoomed image" />
</div>
</div>
</div>
</div>
<!-- Styling -->
<style>
.issue-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.issue-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
}
.vote-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
.vote-btn:hover:not(:disabled) {
transform: scale(1.05);
transition: transform 0.2s;
}
.vote-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#modalImage {
max-height: 85vh;
width: 100%;
object-fit: contain;
transition: transform 0.3s ease-in-out;
cursor: zoom-in;
}
#modalImage.zoomed {
transform: scale(1.5);
cursor: zoom-out;
}
</style>
{% endblock %} {% block extra_js %}
<script>
// CSRF Helper
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
document.cookie.split(";").forEach((cookie) => {
cookie = cookie.trim();
if (cookie.startsWith(name + "=")) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
}
});
}
return cookieValue;
}
const csrftoken = getCookie("csrftoken");
// Handle vote clicks
document.addEventListener("click", function (e) {
const btn = e.target.closest(".vote-btn");
if (!btn) return;
e.preventDefault();
const issueId = btn.dataset.issueId;
if (!issueId) return;
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
fetch("{% url 'vote_issue' 0 %}".replace("0", issueId), {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"X-Requested-With": "XMLHttpRequest",
},
})
.then((r) => r.json())
.then((data) => {
if (!data.success) throw new Error(data.error || "Vote failed");
btn.classList.toggle("active", data.voted);
btn.innerHTML =
'<i class="fas fa-thumbs-up"></i> <span class="vote-count">' +
data.vote_count +
"</span>";
})
.catch((err) => {
console.error(err);
alert("Error: " + err.message);
btn.innerHTML = originalHTML;
})
.finally(() => {
btn.disabled = false;
});
});
// 🔥 Handle image modal
const imageModal = document.getElementById("imageModal");
const modalImage = document.getElementById("modalImage");
document.querySelectorAll(".issue-image").forEach((img) => {
img.addEventListener("click", function () {
const src = this.getAttribute("data-image");
modalImage.src = src;
modalImage.classList.remove("zoomed");
});
});
// Toggle zoom on click inside modal
modalImage.addEventListener("click", function () {
this.classList.toggle("zoomed");
});
</script>
{% endblock %}
View File
+11
View File
@@ -0,0 +1,11 @@
from django import template
register = template.Library()
@register.filter
def pluck(queryset_list, key):
"""
Extracts values from a list of dicts by key.
Example: [{'status': 'open', 'count': 5}]|pluck:"status" -> ['open']
"""
return [d.get(key) for d in queryset_list]
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+30
View File
@@ -0,0 +1,30 @@
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
urlpatterns = [
path('', views.home, name='home'),
path("superadmin/", views.superadmin_dashboard, name="superadmin_dashboard"),
path("superadmin/departments/", views.manage_departments, name="manage_departments"),
path("superadmin/departments/<int:pk>/", views.department_detail, name="department_detail"),
path("superadmin/manage/", views.manage_issues, name="manage_issues"),
path("superadmin/assign-department/<int:issue_id>/", views.assign_department, name="assign_department"),
path('register/', views.register, name='register'),
path('login/', views.custom_login, name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('dashboard/', views.citizen_dashboard, name='citizen_dashboard'),
path('report-issue/', views.report_issue, name='report_issue'),
path('issues/', views.view_all_issues, name='view_all_issues'),
path("issues/<int:pk>/", views.issue_detail, name="issue_detail"),
path("issues/<int:pk>/comment/", views.add_comment, name="add_comment"),
path("issues/<int:pk>/comment/<int:parent_id>/", views.add_comment, name="add_comment"),
path('vote/<int:issue_id>/', views.vote_issue, name='vote_issue'),
path("department/", views.department_dashboard, name="department_dashboard"),
path("update-issue-status/<int:issue_id>/", views.update_issue_status, name="update_issue_status"),
path('manage-users/', views.manage_users, name='manage_users'),
path('ban-user/<int:user_id>/', views.ban_user, name='ban_user'),
path('unban-user/<int:user_id>/', views.unban_user, name='unban_user'),
path('issues/<int:issue_id>/delete_fake/', views.delete_fake_issue, name='delete_fake_issue'),
path("reports/", views.superadmin_reports, name="superadmin_reports"),
path('delete-issue/<int:issue_id>/', views.delete_issue, name='delete_issue'),
]
+419
View File
@@ -0,0 +1,419 @@
from django.contrib import messages
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.forms import AuthenticationForm
from django.db import IntegrityError
from django.db.models import Exists, OuterRef, Count
from django.http import JsonResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.utils.timezone import now, timedelta
from django.views.decorators.http import require_POST
from .models import Issue, User, Vote, Comment, Department
from .forms import CitizenRegistrationForm, IssueForm, CommentForm
def home(request):
total_issues = Issue.objects.count()
resolved_issues = Issue.objects.filter(status=Issue.STATUS_RESOLVED).count()
active_users = User.objects.filter(is_active=True).count()
total_departments = Department.objects.count()
recent_issues = Issue.objects.all().order_by('-created_at')[:3]
for issue in recent_issues:
issue.user_has_voted = issue.has_user_voted(request.user) if request.user.is_authenticated else False
context = {
'total_issues': total_issues,
'resolved_issues': resolved_issues,
'active_users': active_users,
'total_departments': total_departments,
'recent_issues': recent_issues,
}
return render(request, 'core/index.html', context)
@login_required
def citizen_dashboard(request):
if not request.user.is_citizen:
messages.error(request, 'Access denied. Citizen role required.')
return redirect('home')
# Get only basic data for now
all_user_issues = Issue.objects.filter(reporter=request.user)
user_issues_display = all_user_issues.order_by('-created_at')[:5]
resolved_count = all_user_issues.filter(status='resolved').count()
context = {
'user_issues': user_issues_display,
'resolved_count': resolved_count,
'issue_form': IssueForm(),
}
return render(request, 'dashboard/citizen_dashboard.html', context)
@login_required
def report_issue(request):
if not request.user.is_citizen:
messages.error(request, 'Access denied. Citizen role required.')
return redirect('home')
if request.method == 'POST':
form = IssueForm(request.POST, request.FILES)
if form.is_valid():
issue = form.save(commit=False)
issue.reporter = request.user
issue.status = "reported" # default status
issue.save()
messages.success(request, 'Issue reported successfully!')
return redirect('citizen_dashboard')
else:
messages.error(request, 'Please correct the errors below.')
else:
form = IssueForm()
return render(request, 'core/report_issue.html', {'form': form})
@login_required
def view_all_issues(request):
if not request.user.is_active:
messages.error(request, 'Access denied.')
return redirect('home')
# Efficiently annotate whether the current user has voted
user_vote_subq = Vote.objects.filter(user=request.user, issue_id=OuterRef('pk'))
issues = (
Issue.objects
.select_related('reporter')
.annotate(user_has_voted=Exists(user_vote_subq))
.order_by('-created_at')
)
# Optional filter (status only now)
status = request.GET.get('status') or ''
if status:
issues = issues.filter(status=status)
return render(request, 'issues/view_all_issues.html', {
'issues': issues,
'selected_status': status,
})
def register(request):
if request.method == 'POST':
form = CitizenRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
messages.success(request, 'Registration successful! Please login.')
return redirect('login')
else:
form = CitizenRegistrationForm()
return render(request, 'core/register.html', {'form': form})
def custom_login(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
# 🔹 Auto unban check
if hasattr(user, "is_currently_banned") and user.is_currently_banned():
days_left = (user.banned_until - timezone.now()).days
messages.error(
request,
f"🚫 Your account is banned for {days_left} more days for reporting a fake issue."
)
return redirect('login')
# Normal login
login(request, user)
messages.success(request, f'Welcome back, {username}!')
return redirect('home')
else:
messages.error(request, 'Invalid username or password.')
else:
messages.error(request, 'Invalid username or password.')
else:
form = AuthenticationForm()
return render(request, 'core/login.html', {'form': form})
@login_required
@require_POST
def vote_issue(request, issue_id):
try:
issue = Issue.objects.get(id=issue_id)
vote, created = Vote.objects.get_or_create(user=request.user, issue=issue)
if not created:
# User already voted, so remove the vote (toggle)
vote.delete()
voted = False
else:
voted = True
return JsonResponse({
'success': True,
'voted': voted,
'vote_count': issue.vote_count()
})
except Issue.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Issue not found'})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def add_comment(request, pk, parent_id=None):
issue = get_object_or_404(Issue, pk=pk)
parent = get_object_or_404(Comment, pk=parent_id) if parent_id else None
if request.method == "POST":
form = CommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.issue = issue
comment.user = request.user
comment.parent = parent
comment.save()
return redirect("issue_detail", pk=pk)
def issue_detail(request, pk):
issue = get_object_or_404(Issue, pk=pk)
comments = issue.comments.filter(parent__isnull=True) # top-level comments
return render(request, "issues/issue_detail.html", {"issue": issue, "comments": comments})
def add_comment(request, pk, parent_id=None):
if request.method == "POST" and request.user.is_authenticated:
issue = get_object_or_404(Issue, pk=pk)
text = request.POST.get("text")
content = request.POST.get("content")
if content:
parent = Comment.objects.get(pk=parent_id) if parent_id else None
Comment.objects.create(issue=issue, user=request.user, content=content, parent=parent)
return redirect("issue_detail", pk=pk)
def superadmin_check(user):
return user.is_superuser
@login_required
@user_passes_test(superadmin_check)
def superadmin_dashboard(request):
return render(request, "dashboard/superadmin_dashboard.html")
@login_required
@user_passes_test(superadmin_check)
def manage_departments(request):
departments = Department.objects.all().order_by("name")
if request.method == "POST":
name = request.POST.get("name")
description = request.POST.get("description")
if name:
Department.objects.create(name=name, description=description)
return redirect("manage_departments")
return render(request, "department/manage_departments.html", {
"departments": departments
})
@login_required
@user_passes_test(superadmin_check)
def department_detail(request, pk):
department = get_object_or_404(Department, pk=pk)
users = department.users.all()
if request.method == "POST":
# ---- Create user ----
if "create_user" in request.POST:
username = request.POST.get("username", "").strip()
email = request.POST.get("email", "").strip()
password = request.POST.get("password", "").strip()
if username and password:
try:
user = User.objects.create_user(
username=username,
email=email,
password=password
)
user.is_resolver = True
user.save()
department.users.add(user)
messages.success(request, f"User '{username}' created and added to department.")
except IntegrityError:
messages.error(request, "Username already exists.")
# ---- Assign admin ----
elif "assign_admin" in request.POST:
user_id = request.POST.get("admin_user_id")
if user_id:
user = get_object_or_404(User, id=user_id)
department.admin = user
department.save()
messages.success(request, f"{user.username} is now the admin of {department.name}.")
# ---- Remove admin ----
elif "remove_admin" in request.POST:
department.admin = None
department.save()
messages.info(request, "Department admin removed.")
return redirect("department_detail", pk=department.id)
return render(request, "department/department_detail.html", {
"department": department,
"users": users,
})
@login_required
@user_passes_test(superadmin_check)
def manage_issues(request):
issues = Issue.objects.all().order_by('-created_at')
if request.method == "POST":
issue_id = request.POST.get("issue_id")
dept_id = request.POST.get("department")
issue = get_object_or_404(Issue, id=issue_id)
if dept_id: # Only assign if a department is selected
department = get_object_or_404(Department, id=dept_id)
issue.assign_to_department(department) # 🔹 uses helper
return redirect("manage_issues") # refresh page after save
return render(request, "issues/manage_issues.html", {
"issues": issues,
"departments": Department.objects.all()
})
@user_passes_test(superadmin_check)
def assign_department(request, issue_id):
if request.method == "POST":
issue = get_object_or_404(Issue, id=issue_id)
dept_id = request.POST.get("department_id")
if dept_id:
department = get_object_or_404(Department, id=dept_id)
issue.assign_to_department(department)
messages.success(request, f"Issue '{issue.title}' assigned to {department.name}.")
else:
messages.error(request, "Please select a department.")
return redirect("manage_issues")
@login_required
@user_passes_test(superadmin_check)
def manage_users(request):
citizens = User.objects.filter(is_citizen=True).order_by('-date_joined')
return render(request, 'core/manage_users.html', {'citizens': citizens})
@login_required
@user_passes_test(superadmin_check)
def ban_user(request, user_id):
citizen = get_object_or_404(User, id=user_id, is_citizen=True)
citizen.ban(days=7) # default ban 7 days
messages.warning(request, f"{citizen.username} has been banned for 7 days.")
return redirect('manage_users')
@login_required
@user_passes_test(superadmin_check)
def unban_user(request, user_id):
citizen = get_object_or_404(User, id=user_id, is_citizen=True)
citizen.unban()
messages.success(request, f"{citizen.username} has been unbanned.")
return redirect('manage_users')
@login_required
@user_passes_test(superadmin_check)
def delete_fake_issue(request, issue_id):
issue = get_object_or_404(Issue, id=issue_id)
reporter = issue.reporter
reporter.ban(7)
issue.delete()
messages.success(request, f"✅ Issue deleted and user {reporter.username} has been banned for 7 days.")
return redirect("manage_issues")
@login_required
@user_passes_test(superadmin_check)
def superadmin_reports(request):
# 1. Total issues reported
total_issues = Issue.objects.count()
# 2. Issues per status
status_counts = (
Issue.objects.values('status')
.annotate(count=Count('id'))
.order_by()
)
# 3. Top 5 departments with most assigned issues
top_departments = (
Issue.objects.values('department__name')
.annotate(count=Count('id'))
.order_by('-count')[:5]
)
# 4. Top citizens by number of reports
top_citizens = (
Issue.objects.values('reporter__username')
.annotate(count=Count('id'))
.order_by('-count')[:5]
)
# 5. Issues over time (last 30 days)
last_30_days = now() - timedelta(days=30)
issues_last_30_days = (
Issue.objects.filter(created_at__gte=last_30_days)
.extra(select={'day': "date(created_at)"})
.values('day')
.annotate(count=Count('id'))
.order_by('day')
)
context = {
"total_issues": total_issues,
"status_counts": list(status_counts),
"top_departments": list(top_departments),
"top_citizens": list(top_citizens),
"issues_last_30_days": list(issues_last_30_days),
}
return render(request, "core/superadmin_reports.html", context)
@login_required
@user_passes_test(superadmin_check)
def delete_issue(request, issue_id):
issue = get_object_or_404(Issue, id=issue_id)
issue.delete()
messages.success(request, "Issue deleted successfully.")
return redirect('manage_issues')
def resolver_check(user):
return user.is_resolver
@login_required
@user_passes_test(lambda u: u.is_resolver)
def department_dashboard(request):
departments = request.user.departments.all()
issues = Issue.objects.filter(department__in=departments).order_by('-created_at')
return render(request, "dashboard/department_dashboard.html", {"issues": issues, "departments": departments})
@login_required
@user_passes_test(lambda u: u.is_resolver)
def update_issue_status(request, issue_id):
issue = get_object_or_404(Issue, id=issue_id)
# Make sure the user belongs to the department assigned to the issue
if issue.department not in request.user.departments.all():
return redirect("department_dashboard")
if request.method == "POST":
new_status = request.POST.get("status")
if new_status in [Issue.STATUS_IN_PROGRESS, Issue.STATUS_RESOLVED]:
issue.status = new_status
issue.save()
return redirect("department_dashboard")