From c2c80996f70f6d4cfdffea5a7da7263e418b4f01 Mon Sep 17 00:00:00 2001 From: Deepjyot Kapoor <45242325+deepjyotk@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:40:34 -0500 Subject: [PATCH] #104: system admin listing approval (#116) Co-authored-by: aakashshankar --- src/home/repositories.py | 22 ++++++- .../templates/admin_only.html | 49 +++++++++++++++ .../templates/partials/_navbar.html | 59 +++++++++++++------ src/public_service_finder/urls.py | 16 ++++- .../utils/enums/service_status.py | 7 +++ src/public_service_finder/views.py | 53 ++++++++++++++++- src/services/models.py | 14 +++++ src/services/repositories.py | 51 +++++++++++++++- src/services/templates/service_list.html | 3 + src/services/tests.py | 20 +++++++ src/services/views.py | 10 +++- 11 files changed, 277 insertions(+), 27 deletions(-) create mode 100644 src/public_service_finder/templates/admin_only.html create mode 100644 src/public_service_finder/utils/enums/service_status.py diff --git a/src/home/repositories.py b/src/home/repositories.py index 7111fa3..987ee5b 100644 --- a/src/home/repositories.py +++ b/src/home/repositories.py @@ -3,7 +3,7 @@ from decimal import Decimal import boto3 from urllib.parse import quote -from boto3.dynamodb.conditions import Attr, And, Key +from boto3.dynamodb.conditions import Attr, And, Key, Or from django.conf import settings from botocore.exceptions import ClientError from geopy import distance as dist @@ -22,6 +22,7 @@ def fetch_items_with_filter( ): filter_expression = None + # Create filter based on search query and category if search_query and category_filter: filter_expression = And( Attr("Name").contains(search_query), @@ -32,6 +33,23 @@ def fetch_items_with_filter( elif category_filter: filter_expression = Attr("Category").contains(category_filter) + # Add ServiceStatus filter (if exists, it should be "APPROVED") + service_status_filter = Or( + Attr( + "ServiceStatus" + ).not_exists(), # Include items where ServiceStatus does not exist + Attr("ServiceStatus").eq( + "APPROVED" + ), # Include items where ServiceStatus is "APPROVED" + ) + + # Combine the existing filter expression with the new ServiceStatus filter + if filter_expression: + filter_expression = And(filter_expression, service_status_filter) + else: + filter_expression = service_status_filter + + # Scan with the combined filter expression scan_kwargs = {} if filter_expression: scan_kwargs["FilterExpression"] = filter_expression @@ -39,6 +57,7 @@ def fetch_items_with_filter( response = self.services_table.scan(**scan_kwargs) items = response.get("Items", []) + # Filter items based on radius if provided if radius and ulat and ulon: filtered_items = [] for item in items: @@ -122,6 +141,7 @@ def update_service_rating(self, service_id, new_rating): def fetch_reviews_for_service(self, service_id): try: + # Query to get all reviews with matching ServiceId response = self.reviews_table.scan( FilterExpression=Key("ServiceId").eq(service_id) diff --git a/src/public_service_finder/templates/admin_only.html b/src/public_service_finder/templates/admin_only.html new file mode 100644 index 0000000..c90736b --- /dev/null +++ b/src/public_service_finder/templates/admin_only.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} +{% block title %}Admin Only Page{% endblock %} + +{% block content %} +
+
+

Pending Approval Listings

+ + {% if pending_services %} +
+ + + + + + + + + + + {% for service in pending_services %} + + + + + + + {% endfor %} + +
Service NameDescriptionStatusActions
{{ service.name }}{{ service.description }}{{ service.service_status }} +
+ {% csrf_token %} + + +
+
+
+ {% else %} +

No pending listings at the moment.

+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/src/public_service_finder/templates/partials/_navbar.html b/src/public_service_finder/templates/partials/_navbar.html index 857f20c..63980b5 100644 --- a/src/public_service_finder/templates/partials/_navbar.html +++ b/src/public_service_finder/templates/partials/_navbar.html @@ -17,16 +17,30 @@ -
+
+ {% if user.is_authenticated and user.user_type == 'service_provider' %} + + {% else %} + {% endif %} + {% if user.is_superuser %} + + {% endif %} +
- - + + +
@@ -62,10 +76,17 @@
+ {% if user.is_authenticated and user.user_type == 'service_provider' %} + + + Home + + {% else %} Home + {% endif %} Forum diff --git a/src/public_service_finder/urls.py b/src/public_service_finder/urls.py index 9bf4ca2..1906705 100644 --- a/src/public_service_finder/urls.py +++ b/src/public_service_finder/urls.py @@ -2,7 +2,11 @@ from django.contrib import admin from django.urls import path, include -from .views import root_redirect_view # Import the redirect view +from .views import ( + admin_only_view_new_listings, + admin_update_listing, + root_redirect_view, +) # Import the redirect view urlpatterns = [ path("admin/", admin.site.urls), @@ -10,5 +14,15 @@ path("home/", include("home.urls")), path("", root_redirect_view, name="root_redirect"), # Use the custom redirect view path("services/", include("services.urls", namespace="services")), + path( + "admin-only-view-new-listings/", + admin_only_view_new_listings, + name="admin_only_view_new_listings", + ), + path( + "admin-listing/update//", + admin_update_listing, + name="admin_update_listing", + ), # Changed prefix path("forum/", include("forum.urls", namespace="forum")), ] diff --git a/src/public_service_finder/utils/enums/service_status.py b/src/public_service_finder/utils/enums/service_status.py new file mode 100644 index 0000000..fb59885 --- /dev/null +++ b/src/public_service_finder/utils/enums/service_status.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ServiceStatus(Enum): + PENDING_APPROVAL = "PENDING_APPROVAL" + APPROVED = "APPROVED" + REJECTED = "REJECTED" diff --git a/src/public_service_finder/views.py b/src/public_service_finder/views.py index 4bbf1d7..720d629 100644 --- a/src/public_service_finder/views.py +++ b/src/public_service_finder/views.py @@ -1,8 +1,10 @@ # public_service_finder/views.py - from django.shortcuts import redirect - -# from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test +from django.shortcuts import render +from public_service_finder.utils.enums.service_status import ServiceStatus +from services.repositories import ServiceRepository +from django.contrib import messages def root_redirect_view(request): @@ -14,3 +16,48 @@ def root_redirect_view(request): ) else: return redirect("user_login") # Redirect to user login if not logged in + + +@login_required +@user_passes_test(lambda user: user.is_superuser) +def admin_only_view_new_listings(request): + service_repo = ServiceRepository() + pending_services = service_repo.get_pending_approval_services() + return render(request, "admin_only.html", {"pending_services": pending_services}) + + +@login_required +@user_passes_test(lambda user: user.is_superuser) +def admin_update_listing(request, service_id): + if request.method == "POST": + new_status = request.POST.get("status") + + # Only allow "approve" or "reject" as valid status values + if new_status not in ["approve", "reject"]: + messages.error(request, "Invalid status value.") + return redirect("admin_only_view_new_listings") + + service_repo = ServiceRepository() + try: + service_repo.update_service_status( + service_id, + ( + ServiceStatus.APPROVED.value + if new_status == "approve" + else ServiceStatus.REJECTED.value + ), + ) + messages.success( + request, + f"Service ID {service_id} has been successfully updated to '{new_status}'.", + ) + except Exception as e: + messages.error( + request, f"An error occurred while updating the service: {str(e)}" + ) + + # Redirect to the listings page to see updated statuses + return redirect("admin_only_view_new_listings") + + # If the request is not POST, redirect back to the listings page + return redirect("admin_only_view_new_listings") diff --git a/src/services/models.py b/src/services/models.py index d06242e..c24ad38 100644 --- a/src/services/models.py +++ b/src/services/models.py @@ -6,6 +6,8 @@ from typing import Dict, Any import uuid +from public_service_finder.utils.enums.service_status import ServiceStatus + @dataclass class ServiceDTO: @@ -20,10 +22,16 @@ class ServiceDTO: description: Dict[str, Any] category: str provider_id: str + service_status: str + service_created_timestamp: str + service_approved_timestamp: str @classmethod def from_dynamodb_item(cls, item: Dict[str, Any]) -> "ServiceDTO": """Create ServiceDTO from DynamoDB item""" + service_status = item.get("ServiceStatus", "PENDING_APPROVAL") + if service_status.startswith("ServiceStatus."): + service_status = service_status.split(".")[1] return cls( id=item["Id"], name=item["Name"], @@ -34,6 +42,9 @@ def from_dynamodb_item(cls, item: Dict[str, Any]) -> "ServiceDTO": description=item["Description"], category=item["Category"], provider_id=item["ProviderId"], + service_status=ServiceStatus(service_status).value, + service_created_timestamp=item.get("CreatedTimestamp", "NONE"), + service_approved_timestamp=item.get("ApprovedTimestamp", "NONE"), ) def to_dynamodb_item(self) -> Dict[str, Any]: @@ -48,4 +59,7 @@ def to_dynamodb_item(self) -> Dict[str, Any]: "Description": self.description, "Category": self.category, "ProviderId": self.provider_id, + "ServiceStatus": self.service_status, + "CreatedTimestamp": self.service_created_timestamp, + "ApprovedTimestamp": self.service_approved_timestamp, } diff --git a/src/services/repositories.py b/src/services/repositories.py index 1014954..ff35935 100644 --- a/src/services/repositories.py +++ b/src/services/repositories.py @@ -2,9 +2,9 @@ import logging import boto3 +from boto3.dynamodb.conditions import Attr from botocore.exceptions import ClientError from django.conf import settings - from .models import ServiceDTO log = logging.getLogger(__name__) @@ -67,3 +67,52 @@ def delete_service(self, service_id: str) -> bool: except ClientError as e: print(e.response["Error"]["Message"]) return False + + def get_pending_approval_services(self) -> list[ServiceDTO]: + try: + response = self.table.scan( + FilterExpression=Attr("ServiceStatus").eq("PENDING_APPROVAL") + ) + return [ + ServiceDTO.from_dynamodb_item(item) + for item in response.get("Items", []) + ] + except ClientError as e: + log.error( + f"Error fetching pending approval services: {e.response['Error']['Message']}" + ) + return [] + + def update_service_status(self, service_id: str, new_status: str) -> bool: + try: + # Update service status + service_id_str = str(service_id) + response = self.table.update_item( + Key={"Id": service_id_str}, + UpdateExpression="SET ServiceStatus = :new_status", + ExpressionAttributeValues={":new_status": new_status}, + ConditionExpression="attribute_exists(Id)", # Ensure item exists + ReturnValues="UPDATED_NEW", + ) + print("response: " + response) + + # Logging successful update + log.info( + f"Updated ServiceStatus for service ID {service_id} to {new_status}" + ) + return True + except ClientError as e: + if e.response["Error"]["Code"] == "ConditionalCheckFailedException": + log.error(f"Service ID {service_id} does not exist.") + else: + log.error( + f"Error updating service status for ID {service_id}: {e.response['Error']['Message']}" + ) + + return False + + except Exception as e: + log.error( + f"Unexpected error updating service status for ID {service_id}, exception: {e}" + ) + return False diff --git a/src/services/templates/service_list.html b/src/services/templates/service_list.html index 3d38c99..568d46f 100644 --- a/src/services/templates/service_list.html +++ b/src/services/templates/service_list.html @@ -49,6 +49,9 @@

Services Dashboard {{ service.category }} + + {{ service.service_status }} +