Compare commits
19 Commits
4b245d4884
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
| cd1c3d0ff7 | |||
| 219c6d359a | |||
| c11cc49463 | |||
| 7df8d5187d | |||
| f95ebbf375 | |||
| 700260d882 | |||
| fdc776c351 | |||
| aa84422d10 | |||
| ceff3cc1a0 | |||
| ab7373fc34 | |||
| 2767fdf674 | |||
| be2dc882b3 | |||
| 345887d56f | |||
| 0c838571a2 | |||
| 2d0bbe73bd | |||
| 70ae15064c | |||
| 143dd17e6b | |||
| c67538d474 | |||
| 716461af49 |
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
civicenv
|
.env
|
||||||
civicProject.code-workspace
|
issue_photos/
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
SECRET_KEY="django-insecure-wa6p9d+go#+evjql%m(+e5eti$%z7yx2o#cbq8bsh!==icxua3"
|
|
||||||
DEBUG="False"
|
|
||||||
CLOUD_NAME="dkxbfoesf"
|
|
||||||
API_KEY="658671916285379"
|
|
||||||
API_SECRET="_CwNDj4L2dE9yH90Ynj7slPlbo0"
|
|
||||||
CLOUDINARY_URL="cloudinary://658671916285379:_CwNDj4L2dE9yH90Ynj7slPlbo0@dkxbfoesf"
|
|
||||||
DATABASE_URL="postgresql://civicfix_user:YG56PWj9Xj1DvYIKF35TKmIEjrsfis6d@dpg-d2mpapripnbc73f5vaj0-a.oregon-postgres.render.com/civicfix"
|
|
||||||
SUPERUSER_USERNAME="admin"
|
|
||||||
SUPERUSER_PASSWORD="82c96bb18606401630ab9d2836325fbd"
|
|
||||||
SUPERUSER_EMAIL="gokuldevse2001@gmail.com"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.env
|
|
||||||
issue_photos/
|
|
||||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,78 +0,0 @@
|
|||||||
{% 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>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">No issues reported yet.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
Binary file not shown.
@@ -1,10 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import dj_database_url
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import cloudinary
|
|
||||||
import cloudinary.uploader
|
|
||||||
import cloudinary.api
|
|
||||||
|
|
||||||
# Load .env file (for local dev only, Render will use Environment tab)
|
# Load .env file (for local dev only, Render will use Environment tab)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -12,9 +8,9 @@ load_dotenv()
|
|||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY", "unsafe-secret-key")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
DEBUG = os.getenv("DEBUG", "False") == "True"
|
DEBUG = True
|
||||||
ALLOWED_HOSTS = ["*", "civicfix.onrender.com"]
|
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
# Installed apps
|
# Installed apps
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
@@ -25,13 +21,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
# Third-party
|
|
||||||
'cloudinary',
|
|
||||||
'cloudinary_storage',
|
|
||||||
'whitenoise.runserver_nostatic',
|
'whitenoise.runserver_nostatic',
|
||||||
|
|
||||||
# Local apps
|
|
||||||
'core.apps.CoreConfig',
|
'core.apps.CoreConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -67,13 +57,16 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'civicfix.wsgi.application'
|
WSGI_APPLICATION = 'civicfix.wsgi.application'
|
||||||
|
|
||||||
# Database (Render Postgres, fallback to SQLite locally)
|
# Database (PostgreSQL for Local)
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": dj_database_url.config(
|
'default': {
|
||||||
default=os.environ.get("DATABASE_URL"),
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
conn_max_age=600,
|
'NAME': os.getenv("DB_NAME"),
|
||||||
ssl_require=True,
|
'USER': os.getenv("DB_USER"),
|
||||||
)
|
'PASSWORD': os.getenv("DB_PASSWORD"),
|
||||||
|
'HOST': os.getenv("DB_HOST"),
|
||||||
|
'PORT': os.getenv("DB_PORT"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password validators
|
# Password validators
|
||||||
@@ -84,6 +77,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
@@ -93,16 +87,9 @@ USE_TZ = True
|
|||||||
# Static & Media files
|
# Static & Media files
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
cloudinary.config(
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
cloud_name = os.getenv("CLOUD_NAME"),
|
|
||||||
api_key = os.getenv("API_KEY"),
|
|
||||||
api_secret = os.getenv("API_SECRET")
|
|
||||||
)
|
|
||||||
|
|
||||||
MEDIA_URL = f"https://res.cloudinary.com/{os.getenv('CLOUD_NAME')}/"
|
|
||||||
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
|
|
||||||
|
|
||||||
# Auth redirects
|
# Auth redirects
|
||||||
LOGIN_REDIRECT_URL = 'citizen_dashboard'
|
LOGIN_REDIRECT_URL = 'citizen_dashboard'
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!-- base.html -->
|
|
||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Civixfix - {% block title %}{% endblock %}</title>
|
|
||||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2026-02-02 06:27
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_user_banned_until_user_is_banned'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='vote',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='issue',
|
||||||
|
name='photo',
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to='issues/', validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg', 'png', 'webp'])]),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='issue',
|
||||||
|
index=models.Index(fields=['status'], name='core_issue_status_2609e6_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='issue',
|
||||||
|
index=models.Index(fields=['created_at'], name='core_issue_created_50e100_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='issue',
|
||||||
|
index=models.Index(fields=['department'], name='core_issue_departm_5c654b_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='issue',
|
||||||
|
index=models.Index(fields=['reporter'], name='core_issue_reporte_3f70e1_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='department',
|
||||||
|
constraint=models.UniqueConstraint(fields=('admin',), name='unique_department_admin'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='vote',
|
||||||
|
constraint=models.UniqueConstraint(fields=('user', 'issue'), name='unique_vote_per_user_issue'),
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@ from django.conf import settings
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
#User Model
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
is_citizen = models.BooleanField(default=False)
|
is_citizen = models.BooleanField(default=False)
|
||||||
is_moderator = models.BooleanField(default=False)
|
is_moderator = models.BooleanField(default=False)
|
||||||
@@ -32,6 +33,9 @@ class User(AbstractUser):
|
|||||||
is_banned = models.BooleanField(default=False)
|
is_banned = models.BooleanField(default=False)
|
||||||
banned_until = models.DateTimeField(null=True, blank=True)
|
banned_until = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
def ban(self, days=7):
|
def ban(self, days=7):
|
||||||
"""Ban user for given number of days (default = 7)."""
|
"""Ban user for given number of days (default = 7)."""
|
||||||
self.is_banned = True
|
self.is_banned = True
|
||||||
@@ -44,16 +48,16 @@ class User(AbstractUser):
|
|||||||
self.banned_until = None
|
self.banned_until = None
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_currently_banned(self):
|
@property
|
||||||
"""Check if user is still banned (auto-unban if expired)."""
|
def currently_banned(self):
|
||||||
if self.is_banned and self.banned_until:
|
if self.is_banned and self.banned_until:
|
||||||
if timezone.now() >= self.banned_until:
|
if timezone.now() >= self.banned_until:
|
||||||
# Auto unban if ban expired
|
|
||||||
self.unban()
|
self.unban()
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
#Department Model
|
||||||
class Department(models.Model):
|
class Department(models.Model):
|
||||||
name = models.CharField(max_length=100, unique=True)
|
name = models.CharField(max_length=100, unique=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
@@ -77,10 +81,17 @@ class Department(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["admin"],
|
||||||
|
name="unique_department_admin"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
#Issue Model
|
||||||
class Issue(models.Model):
|
class Issue(models.Model):
|
||||||
STATUS_REPORTED = 'reported'
|
STATUS_REPORTED = 'reported'
|
||||||
STATUS_ACKNOWLEDGED = 'acknowledged'
|
STATUS_ACKNOWLEDGED = 'acknowledged'
|
||||||
@@ -117,10 +128,10 @@ class Issue(models.Model):
|
|||||||
longitude = models.FloatField(null=True, blank=True)
|
longitude = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
photo = models.ImageField(
|
photo = models.ImageField(
|
||||||
upload_to="issue_photos/",
|
upload_to="issues/",
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[FileExtensionValidator(['jpg', 'jpeg', 'png', 'gif'])]
|
validators=[FileExtensionValidator(["jpg", "jpeg", "png", "webp"])]
|
||||||
)
|
)
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@@ -133,43 +144,60 @@ class Issue(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created_at"] # 🔹 latest issues first by default
|
ordering = ["-created_at"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["status"]),
|
||||||
|
models.Index(fields=["created_at"]),
|
||||||
|
models.Index(fields=["department"]),
|
||||||
|
models.Index(fields=["reporter"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# 🔹 latest issues first by default
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} ({self.get_status_display()})"
|
return f"{self.title} ({self.get_status_display()})"
|
||||||
|
|
||||||
# 🔹 Helpers
|
# 🔹 Helpers
|
||||||
def vote_count(self):
|
def vote_count(self):
|
||||||
return self.votes.count() if hasattr(self, "votes") else 0
|
return self.votes.count()
|
||||||
|
|
||||||
|
|
||||||
def has_user_voted(self, user):
|
def has_user_voted(self, user):
|
||||||
if user.is_authenticated and hasattr(self, "votes"):
|
if user.is_authenticated:
|
||||||
return self.votes.filter(user=user).exists()
|
return self.votes.filter(user=user).exists()
|
||||||
return
|
return False
|
||||||
|
|
||||||
|
|
||||||
def assign_to_department(self, department):
|
def assign_to_department(self, department):
|
||||||
"""Assign issue to a department and auto-update status to acknowledged"""
|
if self.department_id == department.id:
|
||||||
|
return
|
||||||
self.department = department
|
self.department = department
|
||||||
self.status = self.STATUS_ACKNOWLEDGED
|
self.status = self.STATUS_ACKNOWLEDGED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
#Vote Model
|
||||||
class Vote(models.Model):
|
class Vote(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='votes')
|
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='votes')
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'issue') # Prevent duplicate votes per user
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "issue"],
|
||||||
|
name="unique_vote_per_user_issue"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user.username} voted on {self.issue.title}"
|
return f"{self.user.username} voted on {self.issue.title}"
|
||||||
|
|
||||||
|
#Comment Model
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
|
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="comments")
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
content = models.TextField() # <--- field name is "content", not "text"
|
content = models.TextField()
|
||||||
parent = models.ForeignKey("self", null=True, blank=True, related_name="replies", on_delete=models.CASCADE)
|
parent = models.ForeignKey("self", null=True, blank=True, related_name="replies", on_delete=models.CASCADE)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<p class="text-center text-muted">
|
<p class="text-center text-muted">
|
||||||
Don't have an account? <a href="{% url 'register' %}" class="text-decoration-none">Register here</a>
|
Don't have an account? <a href="{% url 'register' %}" class="text-decoration-none">Register</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
|
|
||||||
<p class="text-center text-muted">
|
<p class="text-center text-muted">
|
||||||
Already have an account? <a href="{% url 'login' %}" class="text-decoration-none">Login here</a>
|
Already have an account? <a href="{% url 'login' %}" class="text-decoration-none">Login</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+1
@@ -5,6 +5,7 @@
|
|||||||
<div class="card shadow-lg">
|
<div class="card shadow-lg">
|
||||||
<div class="card-header bg-warning text-dark">
|
<div class="card-header bg-warning text-dark">
|
||||||
<h3>Analytics & Reports</h3>
|
<h3>Analytics & Reports</h3>
|
||||||
|
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
+3
@@ -22,6 +22,7 @@
|
|||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Reported By</th>
|
<th>Reported By</th>
|
||||||
|
<th>Location</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Created At</th>
|
<th>Created At</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
<td>{{ forloop.counter }}</td>
|
<td>{{ forloop.counter }}</td>
|
||||||
<td>{{ issue.title }}</td>
|
<td>{{ issue.title }}</td>
|
||||||
<td>{{ issue.reporter.username }}</td>
|
<td>{{ issue.reporter.username }}</td>
|
||||||
|
<td>{{ issue.location }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if issue.status == "reported" %}
|
{% if issue.status == "reported" %}
|
||||||
<span class="badge bg-danger">Reported</span>
|
<span class="badge bg-danger">Reported</span>
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
<span class="badge bg-secondary">Unknown</span>
|
<span class="badge bg-secondary">Unknown</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
|
<td>{{ issue.created_at|date:"M d, Y H:i" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if issue.status in "reported,acknowledged" %}
|
{% if issue.status in "reported,acknowledged" %}
|
||||||
@@ -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-danger 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 %}
|
||||||
Binary file not shown.
BIN
Binary file not shown.
@@ -26,4 +26,5 @@ urlpatterns = [
|
|||||||
path('unban-user/<int:user_id>/', views.unban_user, name='unban_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('issues/<int:issue_id>/delete_fake/', views.delete_fake_issue, name='delete_fake_issue'),
|
||||||
path("reports/", views.superadmin_reports, name="superadmin_reports"),
|
path("reports/", views.superadmin_reports, name="superadmin_reports"),
|
||||||
|
path('delete-issue/<int:issue_id>/', views.delete_issue, name='delete_issue'),
|
||||||
]
|
]
|
||||||
@@ -76,8 +76,8 @@ def report_issue(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def view_all_issues(request):
|
def view_all_issues(request):
|
||||||
if not request.user.is_citizen:
|
if not request.user.is_active:
|
||||||
messages.error(request, 'Access denied. Citizen role required.')
|
messages.error(request, 'Access denied.')
|
||||||
return redirect('home')
|
return redirect('home')
|
||||||
|
|
||||||
# Efficiently annotate whether the current user has voted
|
# Efficiently annotate whether the current user has voted
|
||||||
@@ -384,6 +384,14 @@ def superadmin_reports(request):
|
|||||||
}
|
}
|
||||||
return render(request, "core/superadmin_reports.html", context)
|
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):
|
def resolver_check(user):
|
||||||
return user.is_resolver
|
return user.is_resolver
|
||||||
|
|
||||||
Reference in New Issue
Block a user