minor update

This commit is contained in:
2025-08-26 12:05:43 +05:30
parent 0d78cce3dd
commit adbfec2b10
27 changed files with 120 additions and 262 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -23,7 +23,7 @@ class CitizenRegistrationForm(UserCreationForm):
class IssueForm(forms.ModelForm): class IssueForm(forms.ModelForm):
class Meta: class Meta:
model = Issue model = Issue
fields = ['title', 'description', 'category', 'location', 'latitude', 'longitude', 'photo'] fields = ['title', 'description', 'location', 'latitude', 'longitude', 'photo']
widgets = { widgets = {
'latitude': forms.HiddenInput(), 'latitude': forms.HiddenInput(),
'longitude': forms.HiddenInput(), 'longitude': forms.HiddenInput(),
+48 -14
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.2.5 on 2025-08-20 06:30 # Generated by Django 5.2.5 on 2025-08-26 06:15
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
@@ -18,15 +18,6 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='IssueCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
('icon', models.CharField(default='fas fa-exclamation-circle', max_length=50)),
],
),
migrations.CreateModel( migrations.CreateModel(
name='User', name='User',
fields=[ fields=[
@@ -57,21 +48,64 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()), ('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( migrations.CreateModel(
name='Issue', name='Issue',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)), ('title', models.CharField(max_length=200)),
('description', models.TextField()), ('description', models.TextField()),
('location', models.CharField(max_length=200)), ('location', models.CharField(blank=True, max_length=200)),
('latitude', models.FloatField()), ('latitude', models.FloatField(blank=True, null=True)),
('longitude', models.FloatField()), ('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'])])), ('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)), ('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)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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)), ('reporter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reported_issues', to=settings.AUTH_USER_MODEL)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.issuecategory')),
], ],
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')},
},
), ),
] ]
-27
View File
@@ -1,27 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-21 22:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
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')},
},
),
]
-29
View File
@@ -1,29 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-22 00:55
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_vote'),
]
operations = [
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)),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='core.issue')),
('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, related_name='comments', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['created_at'],
},
),
]
@@ -1,32 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 06:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_comment'),
]
operations = [
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)),
],
options={
'ordering': ['name'],
},
),
migrations.AlterField(
model_name='comment',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 06:43
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_department_alter_comment_user'),
]
operations = [
migrations.AddField(
model_name='department',
name='users',
field=models.ManyToManyField(blank=True, related_name='departments', to=settings.AUTH_USER_MODEL),
),
]
@@ -1,24 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 07:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_department_users'),
]
operations = [
migrations.RemoveField(
model_name='department',
name='users',
),
migrations.AddField(
model_name='department',
name='admin',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin_of_department', to=settings.AUTH_USER_MODEL),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 07:24
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_remove_department_users_department_admin'),
]
operations = [
migrations.AddField(
model_name='department',
name='users',
field=models.ManyToManyField(blank=True, related_name='departments', to=settings.AUTH_USER_MODEL),
),
]
@@ -1,38 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 08:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_department_users'),
]
operations = [
migrations.AlterModelOptions(
name='issue',
options={'ordering': ['-created_at']},
),
migrations.AlterField(
model_name='issue',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues', to='core.issuecategory'),
),
migrations.AlterField(
model_name='issue',
name='latitude',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='issue',
name='location',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='issue',
name='longitude',
field=models.FloatField(blank=True, null=True),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 5.2.5 on 2025-08-25 08:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_alter_issue_options_alter_issue_category_and_more'),
]
operations = [
migrations.AddField(
model_name='issue',
name='department',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='issues', to='core.department'),
),
]
-16
View File
@@ -53,14 +53,6 @@ class Department(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class IssueCategory(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
icon = models.CharField(max_length=50, default='fas fa-exclamation-circle')
def __str__(self):
return self.name
class Issue(models.Model): class Issue(models.Model):
STATUS_REPORTED = 'reported' STATUS_REPORTED = 'reported'
STATUS_ACKNOWLEDGED = 'acknowledged' STATUS_ACKNOWLEDGED = 'acknowledged'
@@ -77,14 +69,6 @@ class Issue(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
description = models.TextField() description = models.TextField()
category = models.ForeignKey(
"IssueCategory",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="issues"
)
reporter = models.ForeignKey( reporter = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
+2 -1
View File
@@ -89,7 +89,8 @@
{% if user.is_resolver %} {% if user.is_resolver %}
<li> <li>
<a class="dropdown-item" href="{% url 'department_dashboard' %}">Department Dashboard</a> <a class="dropdown-item" href="{% url 'department_dashboard' %}">
<i class="fas fa-building me-2 text-success"></i>Department Dashboard</a>
</li> </li>
{% endif %} {% endif %}
+12 -2
View File
@@ -141,8 +141,18 @@
<img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top" alt="No image"> <img src="https://via.placeholder.com/300x200/6c757d/ffffff?text=No+Image" class="card-img-top" alt="No image">
{% endif %} {% endif %}
<div class="card-body"> <div class="card-body">
<span class="badge bg-{% if issue.category.name == 'Roads' %}warning text-dark{% elif issue.category.name == 'Sanitation' %}success{% else %}info{% endif %} mb-2"> <span class="badge
{{ issue.category.name|default:"General" }} {% 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> </span>
<h5 class="card-title">{{ issue.title|truncatewords:5 }}</h5> <h5 class="card-title">{{ issue.title|truncatewords:5 }}</h5>
<p class="card-text text-muted">{{ issue.description|truncatewords:15 }}</p> <p class="card-text text-muted">{{ issue.description|truncatewords:15 }}</p>
@@ -5,8 +5,13 @@
<div class="card shadow-lg"> <div class="card shadow-lg">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h3>Department Dashboard</h3> <h3>Department Dashboard</h3>
<p>Welcome, {{ request.user.username }}. Your Departments: <p>
{% for dept in departments %} {{ dept.name }}{% if not forloop.last %}, {% endif %}{% endfor %} Welcome, {{ request.user.username }}. Your Departments:
{% 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> </p>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -20,6 +25,7 @@
<th>Reported By</th> <th>Reported By</th>
<th>Status</th> <th>Status</th>
<th>Created At</th> <th>Created At</th>
<th>Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -43,6 +49,23 @@
{% 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>
{% 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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
+1
View File
@@ -20,4 +20,5 @@ urlpatterns = [
path("issues/<int:pk>/comment/<int:parent_id>/", 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('vote/<int:issue_id>/', views.vote_issue, name='vote_issue'),
path("department/", views.department_dashboard, name="department_dashboard"), path("department/", views.department_dashboard, name="department_dashboard"),
path("update-issue-status/<int:issue_id>/", views.update_issue_status, name="update_issue_status"),
] ]
+25 -18
View File
@@ -8,7 +8,7 @@ from django.db.models import Exists, OuterRef
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from .models import Issue, IssueCategory, User, Vote, Comment, Department from .models import Issue, User, Vote, Comment, Department
from .forms import CitizenRegistrationForm, IssueForm, CommentForm, IssueAssignForm from .forms import CitizenRegistrationForm, IssueForm, CommentForm, IssueAssignForm
def home(request): def home(request):
@@ -16,15 +16,12 @@ def home(request):
resolved_issues = Issue.objects.filter(status='resolved').count() resolved_issues = Issue.objects.filter(status='resolved').count()
active_users = User.objects.filter(is_active=True).count() active_users = User.objects.filter(is_active=True).count()
# Get recently reported issues (last 3 issues)
recent_issues = Issue.objects.all().order_by('-created_at')[:3] recent_issues = Issue.objects.all().order_by('-created_at')[:3]
for issue in recent_issues: for issue in recent_issues:
issue.user_has_voted = issue.has_user_voted(request.user) if request.user.is_authenticated else False issue.user_has_voted = issue.has_user_voted(request.user) if request.user.is_authenticated else False
# Get municipal departments count (assuming you have a Department model) municipal_departments = 5
# If you don't have one yet, you can use a placeholder or create the model
municipal_departments = 5 # Placeholder - replace with actual count when you have the model
context = { context = {
'total_issues': total_issues, 'total_issues': total_issues,
@@ -46,12 +43,10 @@ def citizen_dashboard(request):
all_user_issues = Issue.objects.filter(reporter=request.user) all_user_issues = Issue.objects.filter(reporter=request.user)
user_issues_display = all_user_issues.order_by('-created_at')[:5] user_issues_display = all_user_issues.order_by('-created_at')[:5]
resolved_count = all_user_issues.filter(status='resolved').count() resolved_count = all_user_issues.filter(status='resolved').count()
categories = IssueCategory.objects.all()
context = { context = {
'user_issues': user_issues_display, 'user_issues': user_issues_display,
'resolved_count': resolved_count, 'resolved_count': resolved_count,
'categories': categories,
'issue_form': IssueForm(), 'issue_form': IssueForm(),
} }
return render(request, 'dashboard/citizen_dashboard.html', context) return render(request, 'dashboard/citizen_dashboard.html', context)
@@ -67,6 +62,7 @@ def report_issue(request):
if form.is_valid(): if form.is_valid():
issue = form.save(commit=False) issue = form.save(commit=False)
issue.reporter = request.user issue.reporter = request.user
issue.status = "reported" # default status
issue.save() issue.save()
messages.success(request, 'Issue reported successfully!') messages.success(request, 'Issue reported successfully!')
return redirect('citizen_dashboard') return redirect('citizen_dashboard')
@@ -87,26 +83,19 @@ def view_all_issues(request):
user_vote_subq = Vote.objects.filter(user=request.user, issue_id=OuterRef('pk')) user_vote_subq = Vote.objects.filter(user=request.user, issue_id=OuterRef('pk'))
issues = ( issues = (
Issue.objects Issue.objects
.select_related('category', 'reporter') .select_related('reporter')
.annotate(user_has_voted=Exists(user_vote_subq)) .annotate(user_has_voted=Exists(user_vote_subq))
.order_by('-created_at') .order_by('-created_at')
) )
categories = IssueCategory.objects.all() # Optional filter (status only now)
# Optional filters (status/category) if you wired the dropdowns
status = request.GET.get('status') or '' status = request.GET.get('status') or ''
category_id = request.GET.get('category') or ''
if status: if status:
issues = issues.filter(status=status) issues = issues.filter(status=status)
if category_id:
issues = issues.filter(category_id=category_id)
return render(request, 'issues/view_all_issues.html', { return render(request, 'issues/view_all_issues.html', {
'issues': issues, 'issues': issues,
'categories': categories,
'selected_status': status, 'selected_status': status,
'selected_category': category_id,
}) })
@@ -307,6 +296,24 @@ def resolver_check(user):
return user.is_resolver return user.is_resolver
@login_required @login_required
@user_passes_test(resolver_check) @user_passes_test(lambda u: u.is_resolver)
def department_dashboard(request): def department_dashboard(request):
return render(request, "dashboard/department_dashboard.html") 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")
+5
View File
@@ -0,0 +1,5 @@
from waitress import serve
from civicfix.wsgi import application
if __name__ == "__main__":
serve(application, host="10.1.192.152", port=8000)