Compare commits

...

19 Commits

Author SHA1 Message Date
Gokuldevx cd1c3d0ff7 comment model fixed 2026-02-02 17:34:09 +05:30
Gokuldevx 219c6d359a bug resolved 2026-02-02 12:42:22 +05:30
Gokuldevx c11cc49463 bug resolved 2026-02-02 12:41:38 +05:30
Gokuldevx 7df8d5187d button fixed 2026-02-02 12:40:25 +05:30
Gokuldevx f95ebbf375 minor issue resolved 2026-02-02 12:39:40 +05:30
Gokuldevx 700260d882 minor issue resolved 2026-02-02 12:39:17 +05:30
Gokuldevx fdc776c351 minor issue resolved 2026-02-02 12:39:05 +05:30
Gokuldevx aa84422d10 db updation 2026-02-02 12:37:39 +05:30
Gokuldevx ceff3cc1a0 db updation 2026-02-02 11:24:34 +05:30
Gokuldevx ab7373fc34 initial commit 2026-02-02 10:52:20 +05:30
Gokuldevx 2767fdf674 update 2025-09-08 17:56:45 +05:30
Gokuldevx be2dc882b3 initial commit 2025-08-28 16:00:16 +05:30
Gokuldevx 345887d56f initial commit 2025-08-28 15:26:04 +05:30
Gokuldevx 0c838571a2 initial commit 2025-08-28 15:22:46 +05:30
Gokuldevx 2d0bbe73bd initial commit 2025-08-28 15:11:40 +05:30
Gokuldevx 70ae15064c initial commit 2025-08-28 14:55:03 +05:30
Gokuldevx 143dd17e6b initial commit 2025-08-28 14:52:13 +05:30
Gokuldevx c67538d474 initial commit 2025-08-28 12:40:12 +05:30
Gokuldevx 716461af49 initial commit 2025-08-28 11:46:58 +05:30
79 changed files with 206 additions and 156 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
civicenv
civicProject.code-workspace
.env
issue_photos/
View File
-10
View File
@@ -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"
-2
View File
@@ -1,2 +0,0 @@
.env
issue_photos/
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 %}
@@ -1,10 +1,6 @@
import os
from pathlib import Path
import dj_database_url
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_dotenv()
@@ -12,9 +8,9 @@ load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
# Security
SECRET_KEY = os.getenv("SECRET_KEY", "unsafe-secret-key")
DEBUG = os.getenv("DEBUG", "False") == "True"
ALLOWED_HOSTS = ["*", "civicfix.onrender.com"]
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
# Installed apps
INSTALLED_APPS = [
@@ -25,13 +21,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party
'cloudinary',
'cloudinary_storage',
'whitenoise.runserver_nostatic',
# Local apps
'core.apps.CoreConfig',
]
@@ -67,13 +57,16 @@ TEMPLATES = [
WSGI_APPLICATION = 'civicfix.wsgi.application'
# Database (Render Postgres, fallback to SQLite locally)
# Database (PostgreSQL for Local)
DATABASES = {
"default": dj_database_url.config(
default=os.environ.get("DATABASE_URL"),
conn_max_age=600,
ssl_require=True,
)
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv("DB_NAME"),
'USER': os.getenv("DB_USER"),
'PASSWORD': os.getenv("DB_PASSWORD"),
'HOST': os.getenv("DB_HOST"),
'PORT': os.getenv("DB_PORT"),
}
}
# Password validators
@@ -84,6 +77,7 @@ AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
@@ -94,15 +88,8 @@ USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
cloudinary.config(
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'
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# Auth redirects
LOGIN_REDIRECT_URL = 'citizen_dashboard'
-16
View File
@@ -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.
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'),
),
]
Binary file not shown.
+42 -14
View File
@@ -5,6 +5,7 @@ from django.conf import settings
from django.db import models
from django.utils import timezone
#User Model
class User(AbstractUser):
is_citizen = models.BooleanField(default=False)
is_moderator = models.BooleanField(default=False)
@@ -32,6 +33,9 @@ class User(AbstractUser):
is_banned = models.BooleanField(default=False)
banned_until = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.username
def ban(self, days=7):
"""Ban user for given number of days (default = 7)."""
self.is_banned = True
@@ -44,16 +48,16 @@ class User(AbstractUser):
self.banned_until = None
self.save()
def is_currently_banned(self):
"""Check if user is still banned (auto-unban if expired)."""
@property
def currently_banned(self):
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
#Department Model
class Department(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True)
@@ -77,10 +81,17 @@ class Department(models.Model):
class Meta:
ordering = ["name"]
constraints = [
models.UniqueConstraint(
fields=["admin"],
name="unique_department_admin"
)
]
def __str__(self):
return self.name
#Issue Model
class Issue(models.Model):
STATUS_REPORTED = 'reported'
STATUS_ACKNOWLEDGED = 'acknowledged'
@@ -117,10 +128,10 @@ class Issue(models.Model):
longitude = models.FloatField(null=True, blank=True)
photo = models.ImageField(
upload_to="issue_photos/",
upload_to="issues/",
blank=True,
null=True,
validators=[FileExtensionValidator(['jpg', 'jpeg', 'png', 'gif'])]
validators=[FileExtensionValidator(["jpg", "jpeg", "png", "webp"])]
)
status = models.CharField(
@@ -133,43 +144,60 @@ class Issue(models.Model):
updated_at = models.DateTimeField(auto_now=True)
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):
return f"{self.title} ({self.get_status_display()})"
# 🔹 Helpers
def vote_count(self):
return self.votes.count() if hasattr(self, "votes") else 0
return self.votes.count()
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
return False
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.status = self.STATUS_ACKNOWLEDGED
self.save()
#Vote Model
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
constraints = [
models.UniqueConstraint(
fields=["user", "issue"],
name="unique_vote_per_user_issue"
)
]
def __str__(self):
return f"{self.user.username} voted on {self.issue.title}"
#Comment Model
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"
content = models.TextField()
parent = models.ForeignKey("self", null=True, blank=True, related_name="replies", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
@@ -44,7 +44,7 @@
<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>
Don't have an account? <a href="{% url 'register' %}" class="text-decoration-none">Register</a>
</p>
</div>
</div>
@@ -79,7 +79,7 @@
<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>
Already have an account? <a href="{% url 'login' %}" class="text-decoration-none">Login</a>
</p>
</div>
</div>
@@ -5,6 +5,7 @@
<div class="card shadow-lg">
<div class="card-header bg-warning text-dark">
<h3>Analytics & Reports</h3>
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
</div>
<div class="card-body">
@@ -22,6 +22,7 @@
<th>#</th>
<th>Title</th>
<th>Reported By</th>
<th>Location</th>
<th>Status</th>
<th>Created At</th>
<th>Action</th>
@@ -33,6 +34,7 @@
<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>
@@ -46,6 +48,7 @@
<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" %}
+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-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 %}
+1
View File
@@ -26,4 +26,5 @@ urlpatterns = [
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'),
]
+10 -2
View File
@@ -76,8 +76,8 @@ def report_issue(request):
@login_required
def view_all_issues(request):
if not request.user.is_citizen:
messages.error(request, 'Access denied. Citizen role required.')
if not request.user.is_active:
messages.error(request, 'Access denied.')
return redirect('home')
# Efficiently annotate whether the current user has voted
@@ -384,6 +384,14 @@ def superadmin_reports(request):
}
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
View File