Compare commits

9 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
9 changed files with 110 additions and 52 deletions
+15 -33
View File
@@ -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 = [
@@ -24,19 +20,11 @@ INSTALLED_APPS = [
'django.contrib.humanize', 'django.contrib.humanize',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'cloudinary',
'cloudinary_storage',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'whitenoise.runserver_nostatic', 'whitenoise.runserver_nostatic',
'core.apps.CoreConfig', 'core.apps.CoreConfig',
] ]
CLOUDINARY_STORAGE = {
'CLOUD_NAME': os.getenv('CLOUD_NAME'),
'API_KEY': os.getenv('API_KEY'),
'API_SECRET': os.getenv('API_SECRET'),
}
AUTH_USER_MODEL = 'core.User' AUTH_USER_MODEL = 'core.User'
MIDDLEWARE = [ MIDDLEWARE = [
@@ -69,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
@@ -86,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'
@@ -96,18 +88,8 @@ USE_TZ = True
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/"
MEDIA_ROOT = BASE_DIR / "media"
cloudinary.config(
cloud_name=os.getenv("CLOUD_NAME"),
api_key=os.getenv("API_KEY"),
api_secret=os.getenv("API_SECRET"),
secure=True
)
# Default storage (Cloudinary)
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'
# Auth redirects # Auth redirects
LOGIN_REDIRECT_URL = 'citizen_dashboard' LOGIN_REDIRECT_URL = 'citizen_dashboard'
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'),
),
]
+43 -15
View File
@@ -1,4 +1,3 @@
from cloudinary.models import CloudinaryField
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
@@ -6,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)
@@ -33,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
@@ -45,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)
@@ -78,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 +127,11 @@ class Issue(models.Model):
latitude = models.FloatField(null=True, blank=True) latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True) longitude = models.FloatField(null=True, blank=True)
photo = CloudinaryField( photo = models.ImageField(
'images', upload_to="issues/",
blank=True, blank=True,
null=True, null=True,
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)
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
@@ -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">
+1 -1
View File
@@ -3,7 +3,7 @@
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-5">
<div class="card shadow-lg"> <div class="card shadow-lg">
<div class="card-header bg-warning text-black d-flex justify-content-between align-items-center"> <div class="card-header bg-danger text-black d-flex justify-content-between align-items-center">
<h3>Manage Issues</h3> <h3>Manage Issues</h3>
<a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a> <a href="{% url 'superadmin_dashboard' %}" class="btn btn-light btn-sm">Back to Dashboard</a>
</div> </div>