From fd4aef5a40c8c81917954fe45e3356ba8e4dd898 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Mon, 11 Mar 2024 14:48:48 +0530 Subject: [PATCH 01/31] Added all the functionality of app and admin --- apple.py | 42 ++ module_activity/api/serializers.py | 117 +++- module_activity/api/urls.py | 3 + module_activity/api/views.py | 222 +++++++- module_activity/forms.py | 87 +++ ...ter_principalhealthdata_height_and_more.py | 23 + .../migrations/0008_bowel_stool_name.py | 18 + ...el_created_by_bowel_created_on_and_more.py | 141 +++++ module_activity/models.py | 11 +- module_activity/urls.py | 20 + module_activity/views.py | 431 +++++++++++++-- module_auth/api/serializers.py | 5 + module_auth/api/urls.py | 8 +- module_auth/api/utils.py | 14 +- module_auth/api/views.py | 238 ++++++-- module_auth/forms.py | 56 +- module_auth/urls.py | 6 + module_auth/views.py | 158 ++++-- module_cms/api/serializers.py | 5 - module_cms/api/views.py | 4 +- module_cms/forms.py | 12 - module_cms/urls.py | 2 + module_cms/views.py | 121 ++-- module_iam/fixtures/iam_actions_fixture.json | 46 ++ .../iam_principal_source_fixture.json | 46 ++ .../fixtures/iam_principal_type_fixture.json | 35 ++ .../fixtures/iam_resources_fixture.json | 172 ++++++ module_iam/forms.py | 325 +++++++++++ .../{resource_action.py => iam_constant.py} | 29 +- module_iam/iam_context_processors.py | 60 ++ module_iam/iam_fixture_script.py | 169 ++++++ .../management/commands/load_iam_fixture.py | 122 ++++ module_iam/migrations/0004_appversion.py | 22 + .../migrations/0005_alter_appversion_table.py | 17 + .../0006_alter_appversion_version.py | 19 + module_iam/models.py | 84 ++- module_iam/urls.py | 29 +- module_iam/views.py | 371 ++++++++++++- module_notification/forms.py | 7 + .../migrations/0001_initial.py | 36 ++ module_notification/models.py | 12 + module_notification/urls.py | 16 + module_notification/views.py | 163 +++++- module_project/date_utils.py | 5 + module_project/mixins.py | 34 +- module_project/service.py | 168 +++--- module_project/settings/base.py | 19 +- module_project/urls.py | 5 +- module_project/utils.py | 18 +- module_support/forms.py | 0 module_support/urls.py | 15 +- module_support/views.py | 151 ++++- requirements.txt | 22 + static/img/bowel.png | Bin 0 -> 85125 bytes static/img/default_profile.jpg | Bin 0 -> 4426 bytes static/img/foods.png | Bin 0 -> 87570 bytes static/src/assets/img/left-arrow.svg | 4 + templates/base_structure/elements/header.html | 23 +- .../base_structure/elements/sidebar.html | 52 +- .../base_structure/layout/base_template.html | 2 +- .../base_structure/layout/dashboard.html | 252 +++++++-- .../cdn_through_html/apexchart_cdn_css.html | 3 + .../cdn_through_html/apexchart_cdn_js.html | 3 + templates/module_activity/base_add.html | 45 ++ .../chronic_condition_archive_list.html | 229 ++++++++ .../chronic_conditon_list.html | 11 +- .../intolerance_archive_list.html | 229 ++++++++ .../module_activity/intolerance_list.html | 35 +- .../past_treatment_archive_list.html | 229 ++++++++ .../module_activity/past_treatment_list.html | 15 +- templates/module_activity/report_view.html | 0 .../symptoms_archive_list.html | 229 ++++++++ templates/module_activity/symptoms_list.html | 11 +- templates/module_auth/email_template.html | 33 +- templates/module_auth/user_add.html | 45 ++ templates/module_auth/user_view.html | 153 ++++++ templates/module_auth/users_archive_list.html | 257 +++++++++ templates/module_auth/users_list.html | 183 ++++-- templates/module_cms/faq.html | 483 +++++++++------- templates/module_cms/faq_add.html | 54 ++ templates/module_iam/iam_group.html | 353 ++++++++++++ templates/module_iam/iam_group_add.html | 64 +++ .../module_iam/iam_principal_group_link.html | 431 +++++++++++++++ templates/module_iam/iam_role.html | 362 ++++++++++++ templates/module_iam/iam_role_add.html | 123 +++++ templates/module_iam/profile_details.html | 127 +++++ .../module_iam/profile_details_edit.html | 136 +++++ .../module_notification/add_notification.html | 45 ++ .../module_notification/notification.html | 362 ++++++++++++ templates/module_support/contact_us.html | 520 ++++++++++++++++++ .../contactus_archive_list.html | 375 +++++++++++++ templates/module_support/feedback.html | 208 +++++++ 92 files changed, 8931 insertions(+), 716 deletions(-) create mode 100644 apple.py create mode 100644 module_activity/forms.py create mode 100644 module_activity/migrations/0007_alter_principalhealthdata_height_and_more.py create mode 100644 module_activity/migrations/0008_bowel_stool_name.py create mode 100644 module_activity/migrations/0009_bowel_active_bowel_created_by_bowel_created_on_and_more.py create mode 100644 module_iam/fixtures/iam_actions_fixture.json create mode 100644 module_iam/fixtures/iam_principal_source_fixture.json create mode 100644 module_iam/fixtures/iam_principal_type_fixture.json create mode 100644 module_iam/fixtures/iam_resources_fixture.json create mode 100644 module_iam/forms.py rename module_iam/{resource_action.py => iam_constant.py} (59%) create mode 100644 module_iam/iam_context_processors.py create mode 100644 module_iam/iam_fixture_script.py create mode 100644 module_iam/management/commands/load_iam_fixture.py create mode 100644 module_iam/migrations/0004_appversion.py create mode 100644 module_iam/migrations/0005_alter_appversion_table.py create mode 100644 module_iam/migrations/0006_alter_appversion_version.py create mode 100644 module_notification/forms.py create mode 100644 module_notification/migrations/0001_initial.py create mode 100644 module_notification/urls.py create mode 100644 module_support/forms.py create mode 100644 static/img/bowel.png create mode 100644 static/img/default_profile.jpg create mode 100644 static/img/foods.png create mode 100644 static/src/assets/img/left-arrow.svg create mode 100644 templates/cdn_through_html/apexchart_cdn_css.html create mode 100644 templates/cdn_through_html/apexchart_cdn_js.html create mode 100644 templates/module_activity/base_add.html create mode 100644 templates/module_activity/chronic_condition_archive_list.html create mode 100644 templates/module_activity/intolerance_archive_list.html create mode 100644 templates/module_activity/past_treatment_archive_list.html create mode 100644 templates/module_activity/report_view.html create mode 100644 templates/module_activity/symptoms_archive_list.html create mode 100644 templates/module_auth/user_add.html create mode 100644 templates/module_auth/users_archive_list.html create mode 100644 templates/module_cms/faq_add.html create mode 100644 templates/module_iam/iam_group.html create mode 100644 templates/module_iam/iam_group_add.html create mode 100644 templates/module_iam/iam_principal_group_link.html create mode 100644 templates/module_iam/iam_role.html create mode 100644 templates/module_iam/iam_role_add.html create mode 100644 templates/module_iam/profile_details.html create mode 100644 templates/module_iam/profile_details_edit.html create mode 100644 templates/module_notification/add_notification.html create mode 100644 templates/module_notification/notification.html create mode 100644 templates/module_support/contact_us.html create mode 100644 templates/module_support/contactus_archive_list.html create mode 100644 templates/module_support/feedback.html diff --git a/apple.py b/apple.py new file mode 100644 index 0000000..d03773e --- /dev/null +++ b/apple.py @@ -0,0 +1,42 @@ +import jwt +from jwt.exceptions import ExpiredSignatureError, InvalidTokenError +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from django.contrib.auth import get_user_model +from .utils import generate_token_and_user_data + +User = get_user_model() + +@api_view(['POST']) +def signin_apple(request): + try: + id_token = request.data['id_token'] + email = request.data['email'] + full_name = request.data['full_name'] + + # Verify the JWT token + header = {'alg': 'ES256', 'kid': 'YOUR_APPLE_KEY_ID'} + key = open('path/to/your/Apple-developer-cert.p8', 'rb').read() + decoded_token = jwt.decode(id_token, key, audience='YOUR_APP_BUNDLE_ID', algorithms=['ES256'], options={'verify_aud': False}) + + # Create a new user + user, created = User.objects.get_or_create( + email=email, + defaults={ + 'first_name': full_name.split()[0], + 'last_name': full_name.split()[1], + 'is_active': True, + }, + ) + + if created: + user.save() + + # Generate a JWT token for the new user + token_data = generate_token_and_user_data(user) + + return Response(token_data, status=status.HTTP_200_OK) + + except (KeyError, ExpiredSignatureError, InvalidTokenError) as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/module_activity/api/serializers.py b/module_activity/api/serializers.py index e9a99ab..29e944c 100644 --- a/module_activity/api/serializers.py +++ b/module_activity/api/serializers.py @@ -1,3 +1,6 @@ +import os +import math +from django.conf import settings from rest_framework import serializers from django.utils import timezone from datetime import datetime @@ -20,17 +23,19 @@ from ..models import ( MealRecord, ) + class IAmPrincipalSerializer(serializers.ModelSerializer): class Meta: model = IAmPrincipal fields = [ - # "profile_photo", + "profile_photo", "first_name", "date_of_birth", "gender", "phone_no", ] + class PrincipalHealthDataSerializer(serializers.ModelSerializer): class Meta: model = PrincipalHealthData @@ -44,6 +49,7 @@ class PrincipalHealthDataSerializer(serializers.ModelSerializer): "eat_frequency", ] + class PrincipalAndHealthSerializer(serializers.ModelSerializer): ethenicity = serializers.CharField(read_only=True) weight = serializers.DecimalField(max_digits=5, decimal_places=2, read_only=True) @@ -52,6 +58,7 @@ class PrincipalAndHealthSerializer(serializers.ModelSerializer): exercise_frequency = serializers.CharField(read_only=True) sleep_duration = serializers.CharField(read_only=True) eat_frequency = serializers.CharField(read_only=True) + profile_complete = serializers.IntegerField(read_only=True) class Meta: model = IAmPrincipal @@ -62,8 +69,8 @@ class PrincipalAndHealthSerializer(serializers.ModelSerializer): "date_of_birth", "gender", "phone_no", - "phone_verified", - "email_verified", + # "phone_verified", + # "email_verified", "ethenicity", "weight", "height", @@ -71,27 +78,87 @@ class PrincipalAndHealthSerializer(serializers.ModelSerializer): "exercise_frequency", "sleep_duration", "eat_frequency", + "profile_complete" ] + def calculate_profile_completion(self, user): + """ + Calculates the profile completion percentage for a user based on the required fields. + """ + fields = self.fields + try: + # Retrieve the user profile from the database + profile = IAmPrincipal.objects.get(id=user) + try: + # Retrieve the user's health data from the database + health_data = PrincipalHealthData.objects.get(principal=profile) + except PrincipalHealthData.DoesNotExist: + # If health data doesn't exist, set health_data to None + health_data = None + + # Initialize a counter for completed fields + completed_fields = sum( + 1 + for field in fields + if ( + # If the field is in the user profile and the field value is not None, not an empty string, and not an instance of datetime.date + (field in vars(profile) and vars(profile).get(field, '') and vars(profile).get(field) != datetime.date) or + + # If health data exists, the field is in the user's health data, and the field value is not None, not an empty string, and not an instance of datetime.date + (health_data and field in vars(health_data) and vars(health_data).get(field, '') and vars(health_data).get(field) != datetime.date) + ) + ) + + except IAmPrincipal.DoesNotExist: + # If the user profile doesn't exist, return 0 + return 0 + + # Calculate the total number of fields + total_fields = len(fields) - 1 # Exclude profile_complete field + + # Calculate the profile completion percentage + completion_percentage = math.floor((completed_fields / total_fields) * 100) + + # Return the profile completion percentage + return completion_percentage + def get_image_url(self, obj, field_name, request): image_field = getattr(obj, field_name) if image_field: return request.build_absolute_uri(image_field.url) - return "" + else: + # Return the URL of the default image from the static path + default_image_path = os.path.join( + settings.STATIC_URL, "img/default_profile.jpg" + ) + return request.build_absolute_uri(default_image_path) def to_representation(self, instance): data = super().to_representation(instance) request = self.context.get("request") data["profile_photo"] = self.get_image_url(instance, "profile_photo", request) - health_data = instance.health_data_principal - if health_data: - data['ethenicity'] = health_data.ethenicity - data['weight'] = health_data.weight - data['height'] = health_data.height - data['gastrointestinal_health'] = health_data.gastrointestinal_health - data['exercise_frequency'] = health_data.exercise_frequency - data['sleep_duration'] = health_data.sleep_duration - data['eat_frequency'] = health_data.eat_frequency + data["profile_complete"] = self.calculate_profile_completion(request.user.id) + if ( + hasattr(instance, "health_data_principal") + and instance.health_data_principal + ): + health_data = instance.health_data_principal + data["ethenicity"] = health_data.ethenicity + data["weight"] = health_data.weight + data["height"] = health_data.height + data["gastrointestinal_health"] = health_data.gastrointestinal_health + data["exercise_frequency"] = health_data.exercise_frequency + data["sleep_duration"] = health_data.sleep_duration + data["eat_frequency"] = health_data.eat_frequency + else: + # If health_data_principal doesn't exist or is empty, set empty strings for all attributes + data["ethenicity"] = "" + data["weight"] = 0.00 + data["height"] = 0.00 + data["gastrointestinal_health"] = "" + data["exercise_frequency"] = "" + data["sleep_duration"] = "" + data["eat_frequency"] = "" return data @@ -100,6 +167,7 @@ class IntoleranceSerializer(serializers.ModelSerializer): model = Intolerance fields = ["id", "name", "duration"] + class SymptomsSerializer(serializers.ModelSerializer): class Meta: model = Symptoms @@ -141,6 +209,7 @@ class BeverageRecordSerializer(serializers.ModelSerializer): "quantity_measure", ] + class MealRecordSerializer(serializers.ModelSerializer): food_records = FoodRecordSerializer(many=True) beverage_records = BeverageRecordSerializer(many=True) @@ -148,7 +217,15 @@ class MealRecordSerializer(serializers.ModelSerializer): class Meta: model = MealRecord - fields = ['id', 'date', 'time', 'meal_type', 'food_records', 'food_ingredient_records', 'beverage_records'] + fields = [ + "id", + "date", + "time", + "meal_type", + "food_records", + "food_ingredient_records", + "beverage_records", + ] def create(self, validated_data): food_record_data = validated_data.pop("food_records", []) @@ -210,6 +287,7 @@ class MealRecordSerializer(serializers.ModelSerializer): instance.save() return instance + class MedicineSerializer(serializers.ModelSerializer): class Meta: model = Medicine @@ -253,6 +331,7 @@ class BowelSerializer(serializers.ModelSerializer): "date", "time", "stool_type", + "stool_name", "duration", "completeness_of_evacuation", "urgency", @@ -314,12 +393,8 @@ class MealSymptomRecordSerializer(serializers.ModelSerializer): return meal_symptom_record def update(self, instance, validated_data): - instance.date = validated_data.get( - "date", instance.date - ) - instance.time = validated_data.get( - "time", instance.time - ) + instance.date = validated_data.get("date", instance.date) + instance.time = validated_data.get("time", instance.time) instance.symptoms_description = validated_data.get( "symptoms_description", instance.symptoms_description ) @@ -343,4 +418,4 @@ class MealSymptomRecordSerializer(serializers.ModelSerializer): instance.save() - return instance \ No newline at end of file + return instance diff --git a/module_activity/api/urls.py b/module_activity/api/urls.py index 9b4ca10..4bb9188 100644 --- a/module_activity/api/urls.py +++ b/module_activity/api/urls.py @@ -5,6 +5,7 @@ from . import views urlpatterns = [ path("profile/", views.ProfileAPIView.as_view()), + path("profile/complete/", views.ProfileCompleteAPIView.as_view()), path("daily-records/", views.DailyRecordAPIView.as_view()), @@ -26,4 +27,6 @@ urlpatterns = [ path("meal/", views.MealAPIView.as_view()), path("meal//", views.MealAPIView.as_view()), + path("report/", views.ReportAPIView.as_view()), + ] diff --git a/module_activity/api/views.py b/module_activity/api/views.py index 73036f8..aa700ca 100644 --- a/module_activity/api/views.py +++ b/module_activity/api/views.py @@ -1,11 +1,11 @@ -from datetime import datetime +from datetime import datetime, timedelta from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication -from django.db.models import Prefetch -from module_project import constants +from django.db.models import Prefetch, Count, Max, Min +from module_project import constants, date_utils from module_project.utils import ApiResponse from module_iam.models import IAmPrincipal from ..models import ( @@ -33,6 +33,8 @@ from .serializers import ( PrincipalAndHealthSerializer, ) +from module_project.service import OneSignalNotificationService + class ProfileAPIView(APIView): authentication_classes = [JWTAuthentication] @@ -51,7 +53,6 @@ class ProfileAPIView(APIView): return ApiResponse.error( status=status.HTTP_404_NOT_FOUND, message=constants.RECORD_NOT_FOUND ) - print(f"object data is {obj}") return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) def post(self, request): @@ -83,6 +84,8 @@ class ProfileAPIView(APIView): try: # with transaction.atomic(): # Ensure atomicity of database operations principal_instance = principal_serializer.save() + principal_instance.register_complete = True + principal_instance.save() # Check if health data already exists for the principal health_data_instance, created = PrincipalHealthData.objects.get_or_create( @@ -96,14 +99,23 @@ class ProfileAPIView(APIView): return ApiResponse.success(message=constants.SUCCESS) +class ProfileCompleteAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = IAmPrincipal + + def get(self, request): + user = IAmPrincipal.objects.filter(id=request.user.id).update(register_complete=True) + return ApiResponse.success(message=constants.SUCCESS) + class DailyRecordAPIView(APIView): def serialize_record(self, record): - time_obj = datetime.strptime(str(record.time), '%H:%M:%S') + time_obj = datetime.strptime(str(record.time), "%H:%M:%S") return { "id": record.id, "date": record.date, - "time": time_obj.strftime('%I:%M %p'), + "time": time_obj.strftime("%I:%M %p"), # Add other fields as needed } @@ -112,36 +124,40 @@ class DailyRecordAPIView(APIView): # date = datetime.now().date() if not date: - return ApiResponse.error(message=constants.FAILURE, errors="Date parameter is missing") + return ApiResponse.error( + message=constants.FAILURE, errors="Date parameter is missing" + ) try: # Convert the date string to a datetime object date_obj = datetime.strptime(date, "%Y-%m-%d").date() except ValueError: - return ApiResponse.error(message=constants.FAILURE, errors="Invalid date format") + return ApiResponse.error( + message=constants.FAILURE, errors="Invalid date format" + ) # Define prefetch related queries for filtering the record of paticular date of all related models meal_records_prefetch = Prefetch( "meal_principal", - queryset=MealRecord.objects.filter(date=date), + queryset=MealRecord.objects.filter(date=date, deleted=False), to_attr="filtered_meal_record", ) medication_prefetch = Prefetch( "medication_principal", - queryset=Medication.objects.filter(date=date), + queryset=Medication.objects.filter(date=date, deleted=False), to_attr="filtered_medication", ) bowel_prefetch = Prefetch( "bowel_principal", - queryset=Bowel.objects.filter(date=date), + queryset=Bowel.objects.filter(date=date, deleted=False), to_attr="filtered_bowel", ) meal_symptom_prefetch = Prefetch( "meal_symptom_principal", - queryset=MealSymptomRecord.objects.filter(date=date), + queryset=MealSymptomRecord.objects.filter(date=date, deleted=False), to_attr="filtered_meal_symptom", ) @@ -173,19 +189,17 @@ class DailyRecordAPIView(APIView): for record in principal.filtered_meal_symptom ] - all_records = (serialized_symptom + serialized_meal_records + serialized_medication + serialized_bowel) + all_records = ( + serialized_symptom + + serialized_meal_records + + serialized_medication + + serialized_bowel + ) # all_records_sorted = sorted(all_records, key=lambda x: x["time"], reverse=True) - all_records_sorted = sorted( - all_records, - key=lambda x: x["time"], - reverse=True - ) + all_records_sorted = sorted(all_records, key=lambda x: x["time"], reverse=True) - - return ApiResponse.success( - message=constants.SUCCESS, data=all_records_sorted - ) + return ApiResponse.success(message=constants.SUCCESS, data=all_records_sorted) class IntoleranceListCreateAPIView(APIView): @@ -615,3 +629,167 @@ class MealAPIView(APIView): return ApiResponse.success( message=constants.RECORD_DELETED, status=status.HTTP_204_NO_CONTENT ) + + +from collections import defaultdict + + +class ReportAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = MealRecord + + def get_user(self): + return self.request.user + + def enough_records_exist(self, start_date, end_date): + """ + Check if at least 7 days of records exist within the date range. + + This method calculates the minimum date by subtracting the required number of days + (min_days_required - 1) from the end_date. It then filters the queryset based on the + principal and date range. If any objects exist within this range, the method returns True, + indicating that there are at least 7 days of records. + + :param start_date: The initial date in the date range + :type start_date: datetime.date + :param end_date: The final date in the date range + :type end_date: datetime.date + :return: True if at least 7 days of records exist, False otherwise + :rtype: bool + """ + min_days_required = 7 + current_date = start_date + count = 0 + + while current_date <= end_date: + if self.model.objects.filter(principal=self.get_user(), date=current_date).exists(): + count += 1 + if count >= min_days_required: + return True + else: + count = 0 # Reset count if record is missing for any day + current_date += timedelta(days=1) + + return False + + def get_top_food_avoid(self, start_date, end_date): + """Get the top food to avoid.""" + food_counts = defaultdict(int) + ingredient_counts = defaultdict(int) + beverage_counts = defaultdict(int) + + symptom_records = MealSymptomRecord.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ) + + for symptom_record in symptom_records: + closest_meal = ( + MealRecord.objects.filter( + principal=symptom_record.principal, date__lte=symptom_record.date + ) + .order_by("-date", "-time") + .first() + ) + if closest_meal: + for food_record in closest_meal.food_records.all(): + food_counts[food_record.name] += 1 + for ingredient_record in closest_meal.food_ingredient_records.all(): + ingredient_counts[ingredient_record.name] += 1 + for beverage_record in closest_meal.beverage_records.all(): + beverage_counts[beverage_record.beverage_type] += 1 + + # Sort the dictionaries by their values in descending order and getting only top 3 record + food_counts = dict( + sorted(food_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + ingredient_counts = dict( + sorted(ingredient_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + beverage_counts = dict( + sorted(beverage_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + food_avoid = next(iter(food_counts), None) + return food_avoid, food_counts, ingredient_counts, beverage_counts + + def get_symptoms_frequency(self, start_date, end_date): + """Get the frequency of symptoms.""" + symptom_records = MealSymptomRecord.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ).annotate( + before_meal_count=Count("symptoms_before_meal"), + after_meal_count=Count("symptoms_after_meal"), + ) + symptoms_frequency = defaultdict(int) + + for record in symptom_records: + for symptom in record.symptoms_before_meal.all(): + symptoms_frequency[symptom.name] += record.before_meal_count + for symptom in record.symptoms_after_meal.all(): + symptoms_frequency[symptom.name] += record.after_meal_count + + sorted_symptoms_frequency = dict( + sorted(symptoms_frequency.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + return sorted_symptoms_frequency + + def get_stool_type_counts(self, start_date, end_date): + """Get the count of stool types.""" + stool_type_counts = ( + Bowel.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ) + .values("stool_type") + .annotate(stool_type_count=Count("stool_type")) + ) + stool_type_counts_dict = { + item["stool_type"]: item["stool_type_count"] for item in stool_type_counts + } + + stool_type_counts_sort = dict( + sorted(stool_type_counts_dict.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + highest_stool = next(iter(stool_type_counts_sort), None) + return stool_type_counts_sort, highest_stool + + def get(self, request): + date_range = request.GET.get("date_range") + start_date, end_date = date_utils.get_date_range(date_range) + print(f"start date is {start_date}, end_date is {end_date}") + + print(f"is dats exist {self.enough_records_exist(start_date, end_date)}") + + if not self.enough_records_exist(start_date, end_date): + return ApiResponse.error( + message="No report is generated. Minimum Previous 7 days of records required." + ) + + # Get top food to avoid + food_avoid, food_counts, ingredient_counts, beverage_counts = ( + self.get_top_food_avoid(start_date, end_date) + ) + + # Get symptoms frequency + sorted_symptoms_frequency = self.get_symptoms_frequency(start_date, end_date) + + # Get stool type counts + stool_type_counts_sort, highest_stool = self.get_stool_type_counts( + start_date, end_date + ) + + nested_json = { + "food_avoid": food_avoid, + "same_food_avoid": { + "food": food_counts, + "ingredient": ingredient_counts, + "beverage": beverage_counts, + }, + "symptoms_frequency": sorted_symptoms_frequency, + "highest_stool": highest_stool, + "stool_type": stool_type_counts_sort, + } + print(f"nested_json data is {nested_json}") + return ApiResponse.success(message=constants.SUCCESS, data=nested_json) diff --git a/module_activity/forms.py b/module_activity/forms.py new file mode 100644 index 0000000..b30b2d2 --- /dev/null +++ b/module_activity/forms.py @@ -0,0 +1,87 @@ +from django import forms +from module_project import constants +from .models import Intolerance, Symptoms, PastTreatment, ChronicCondition +from module_iam.models import IAmPrincipal + +class IntoleranceForm(forms.ModelForm): + class Meta: + model = Intolerance + fields = ['name', 'duration'] + label = { + "name": "intolerance", + "duration": "For how long have you been experiencing this intolerance" + } + + def save(self, principal_id, commit=True): + instance = super().save(commit=False) + instance.principal = IAmPrincipal.objects.get(pk=principal_id) + if commit: + instance.save() + return instance + + +class SymptomsForm(forms.ModelForm): + class Meta: + model = Symptoms + fields = ['name', 'duration'] + label = { + "name": "Symptoms", + "duration": "For how long have you been experiencing this intolerance" + } + + def save(self, principal_id, commit=True): + instance = super().save(commit=False) + instance.principal = IAmPrincipal.objects.get(pk=principal_id) + if commit: + instance.save() + return instance + +class SymptomsForm(forms.ModelForm): + class Meta: + model = Symptoms + fields = ['name', 'duration'] + label = { + "name": "Symptoms", + "duration": "For how long have you been experiencing this intolerance" + } + + def save(self, principal_id, commit=True): + instance = super().save(commit=False) + instance.principal = IAmPrincipal.objects.get(pk=principal_id) + if commit: + instance.save() + return instance + + +class PastTreatmentForm(forms.ModelForm): + class Meta: + model = PastTreatment + fields = ['name', 'duration'] + label = { + "name": "PastTreatment", + "duration": "Treatment Date" + } + + def save(self, principal_id, commit=True): + instance = super().save(commit=False) + instance.principal = IAmPrincipal.objects.get(pk=principal_id) + if commit: + instance.save() + return instance + + +class ChronicConditionForm(forms.ModelForm): + class Meta: + model = ChronicCondition + fields = ['name', 'duration'] + label = { + "name": "Chronic Condition", + "duration": "For how long have you been experiencing this disease" + } + + def save(self, principal_id, commit=True): + instance = super().save(commit=False) + instance.principal = IAmPrincipal.objects.get(pk=principal_id) + if commit: + instance.save() + return instance \ No newline at end of file diff --git a/module_activity/migrations/0007_alter_principalhealthdata_height_and_more.py b/module_activity/migrations/0007_alter_principalhealthdata_height_and_more.py new file mode 100644 index 0000000..1bac5ef --- /dev/null +++ b/module_activity/migrations/0007_alter_principalhealthdata_height_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2024-02-29 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_activity', '0006_mealrecord_meal_type'), + ] + + operations = [ + migrations.AlterField( + model_name='principalhealthdata', + name='height', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your height in centimeters.', max_digits=6, null=True, verbose_name='Height (cm)'), + ), + migrations.AlterField( + model_name='principalhealthdata', + name='weight', + field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your weight in kilograms.', max_digits=5, null=True, verbose_name='Weight (kg)'), + ), + ] diff --git a/module_activity/migrations/0008_bowel_stool_name.py b/module_activity/migrations/0008_bowel_stool_name.py new file mode 100644 index 0000000..6c480ce --- /dev/null +++ b/module_activity/migrations/0008_bowel_stool_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-03-01 07:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_activity', '0007_alter_principalhealthdata_height_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='bowel', + name='stool_name', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/module_activity/migrations/0009_bowel_active_bowel_created_by_bowel_created_on_and_more.py b/module_activity/migrations/0009_bowel_active_bowel_created_by_bowel_created_on_and_more.py new file mode 100644 index 0000000..d7ec7a3 --- /dev/null +++ b/module_activity/migrations/0009_bowel_active_bowel_created_by_bowel_created_on_and_more.py @@ -0,0 +1,141 @@ +# Generated by Django 5.0.2 on 2024-03-03 10:16 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_activity', '0008_bowel_stool_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='bowel', + name='active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='bowel', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='bowel', + name='created_on', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='bowel', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='bowel', + name='modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='bowel', + name='modified_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='mealrecord', + name='active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='mealrecord', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='mealrecord', + name='created_on', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='mealrecord', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='mealrecord', + name='modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='mealrecord', + name='modified_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='created_on', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='mealsymptomrecord', + name='modified_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='medication', + name='active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='medication', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='medication', + name='created_on', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='medication', + name='deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='medication', + name='modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='medication', + name='modified_on', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/module_activity/models.py b/module_activity/models.py index 4d6a1cb..f737746 100644 --- a/module_activity/models.py +++ b/module_activity/models.py @@ -49,6 +49,7 @@ class PrincipalHealthData(BaseModel): weight = models.DecimalField( max_digits=5, decimal_places=2, + default=0.0, blank=True, null=True, verbose_name="Weight (kg)", @@ -58,6 +59,7 @@ class PrincipalHealthData(BaseModel): height = models.DecimalField( max_digits=6, decimal_places=2, + default=0.0, blank=True, null=True, verbose_name="Height (cm)", @@ -171,7 +173,7 @@ class BeverageRecord(models.Model): class Meta: db_table = "beverage_record" -class MealRecord(models.Model): +class MealRecord(BaseModel): principal = models.ForeignKey( IAmPrincipal, related_name="meal_principal", on_delete=models.CASCADE ) @@ -217,7 +219,7 @@ class Medicine(models.Model): def __str__(self): return f"{self.name} Medicine" -class Medication(models.Model): +class Medication(BaseModel): principal = models.ForeignKey( IAmPrincipal, related_name="medication_principal", on_delete=models.CASCADE ) @@ -240,13 +242,14 @@ class MedicationMedicine(models.Model): db_table = "medication_medicine" -class Bowel(models.Model): +class Bowel(BaseModel): principal = models.ForeignKey( IAmPrincipal, related_name="bowel_principal", on_delete=models.CASCADE ) date = models.DateField() time = models.TimeField() stool_type = models.CharField(max_length=100, blank=True, null=True) + stool_name = models.CharField(max_length=100, blank=True, null=True) duration = models.DurationField(blank=True, null=True) completeness_of_evacuation = models.CharField(max_length=100, blank=True, null=True) urgency = models.CharField(max_length=100, blank=True, null=True) @@ -272,7 +275,7 @@ class SymptomTypeAfterMeal(models.Model): class Meta: db_table = "symptom_type_after_meal" -class MealSymptomRecord(models.Model): +class MealSymptomRecord(BaseModel): principal = models.ForeignKey(IAmPrincipal, related_name="meal_symptom_principal", on_delete=models.CASCADE) date = models.DateField() time = models.TimeField() diff --git a/module_activity/urls.py b/module_activity/urls.py index d39baa6..e335251 100644 --- a/module_activity/urls.py +++ b/module_activity/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from django.views.generic import TemplateView app_name = "module_activity" @@ -7,20 +8,33 @@ urlpatterns = [ path('intolerance//', views.IntoleranceView.as_view(), name='intolerance'), + path('intolerance//add/', views.CreateOrUpdateIntoleranceView.as_view(), name='intolerance_add'), + path('intolerance//edit/', views.CreateOrUpdateIntoleranceView.as_view(), name='intolerance_edit'), path('intolerance/list//', views.IntoleranceListJson.as_view(), name='intolerance_list'), path('intolerance/action/', views.IntoleranceActionView.as_view(), name='intolerance_action'), + path('intolerance/archive/list//', views.IntoleranceArchiveView.as_view(), name='intolerance_archive'), + path('symptoms//', views.SymptomsView.as_view(), name='symptoms'), + path('symptoms//add/', views.CreateOrUpdateSymptomsView.as_view(), name='symptoms_add'), + path('symptoms//edit/', views.CreateOrUpdateSymptomsView.as_view(), name='symptoms_edit'), path('symptoms/list//', views.SymptomsListJson.as_view(), name='symptoms_list'), path('symptoms/action/', views.SymptomsActionView.as_view(), name='symptoms_action'), + path('symptoms/archive/list//', views.SymptomsArchiveView.as_view(), name='symptoms_archive'), path('past_treatment//', views.PastTreatmentView.as_view(), name='past_treatment'), + path('past_treatment//add/', views.CreateOrUpdatePastTreatmentView.as_view(), name='past_treatment_add'), + path('past_treatment//edit/', views.CreateOrUpdatePastTreatmentView.as_view(), name='past_treatment_edit'), path('past_treatment/list//', views.PastTreatmentListJson.as_view(), name='past_treatment_list'), path('past_treatment/action/', views.PastTreatmentActionView.as_view(), name='past_treatment_action'), + path('past_treatment/archive/list//', views.PastTreatmentArchiveView.as_view(), name='past_treatment_archive'), path('chronic_condition//', views.ChronicConditionView.as_view(), name='chronic_condition'), + path('chronic_condition//add/', views.CreateOrUpdateChronicConditionView.as_view(), name='chronic_condition_add'), + path('chronic_condition//edit/', views.CreateOrUpdateChronicConditionView.as_view(), name='chronic_condition_edit'), path('chronic_condition/list//', views.ChronicConditionListJson.as_view(), name='chronic_condition_list'), path('chronic_condition/action/', views.ChronicConditionActionView.as_view(), name='chronic_condition_action'), + path('chronic_condition/archive/list//', views.ChronicConditionArchiveView.as_view(), name='chronic_condition_archive'), path('user_activity//', views.UserActivityRecordView.as_view(), name='activity_list'), path('meal_detail//', views.MealDetialView.as_view(), name='meal_detail'), @@ -28,4 +42,10 @@ urlpatterns = [ path('bowel_detail//', views.BowelDetailView.as_view(), name='bowel_detail'), path('meal_symptom_detail//', views.MealSymptomDetailView.as_view(), name='meal_symptom_detail'), + + path('daily_report/chart/count/', views.ReportChartView.as_view(), name='chart_data'), + + path('report//', views.ReportDataView.as_view(), name='report_data'), + + ] diff --git a/module_activity/views.py b/module_activity/views.py index dafe9d2..233c64a 100644 --- a/module_activity/views.py +++ b/module_activity/views.py @@ -1,23 +1,41 @@ import logging -from datetime import datetime +from collections import defaultdict +from datetime import datetime, timedelta from django.shortcuts import get_object_or_404, render, redirect from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse_lazy from django.views import generic -from django.db.models import Q, Prefetch -from .models import Intolerance, Symptoms, ChronicCondition, PastTreatment, MealRecord, Bowel, MealSymptomRecord, Medication +from django.db.models import Q, Prefetch, Count +from .models import ( + Intolerance, + Symptoms, + ChronicCondition, + PastTreatment, + MealRecord, + Bowel, + MealSymptomRecord, + Medication, +) +from .forms import ( + IntoleranceForm, + SymptomsForm, + PastTreatmentForm, + ChronicConditionForm, +) from django_datatables_view.base_datatable_view import BaseDatatableView from module_iam.models import IAmPrincipal -from module_project import constants +from module_iam import iam_constant +from module_project import constants, date_utils from module_project.utils import JsonResponseUtil from django.http import JsonResponse logger = logging.getLogger(__name__) + class BaseView(generic.TemplateView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_USER resource = None action = None template_name = None @@ -27,115 +45,236 @@ class BaseView(generic.TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name - context["principal_id"] = self.kwargs.get('principal_id') + context["principal_id"] = self.kwargs.get("principal_id") return context + +class BaseCreateOrUpdateView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_MANAGE_USER + page_title = None + model = None + template_name = "module_activity/base_add.html" + form_class = None + success_url = None + error_message = "An error occurred while saving the data." + + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "page_title": self.page_title, + "principal_id": self.kwargs.get("principal_id"), + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + principal_id = kwargs.get("principal_id") + self.object = self.get_object() + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save(principal_id=principal_id) + messages.success(self.request, self.get_success_message()) + success_url = reverse_lazy( + self.success_url, kwargs={"principal_id": principal_id} + ) + return redirect(success_url) + + class BaseListJson(BaseDatatableView): model = Intolerance columns = ["id", "name", "duration", "active", "deleted"] order_columns = ["id", "name", "duration", "active", "deleted"] def get_initial_queryset(self): - principal_id = self.kwargs.get('principal_id') - deleted_flag = self.request.GET.get('deleted_flag', None) + principal_id = self.kwargs.get("principal_id") + deleted_flag = self.request.GET.get("deleted_flag", None) - if deleted_flag == 'true': - # Show only deleted records - return self.model.objects.filter(principal=principal_id, deleted=True) - else: - # Show all records except deleted ones - return self.model.objects.filter(principal=principal_id, deleted=False) + return self.model.objects.filter(principal=principal_id, deleted=deleted_flag) def filter_queryset(self, qs): search_value = self.request.GET.get("search[value]", None) if search_value: qs = qs.filter( - Q(name__icontains=search_value) | - Q(duration__icontains=search_value) + Q(name__icontains=search_value) | Q(duration__icontains=search_value) ) return qs class BaseActionView(generic.View): - model = Intolerance + model = None def post(self, request, *args, **kwargs): - action = request.POST.get('action') # 'archive', 'active', or 'unarchive' - ids = request.POST.getlist('ids[]') # List of user IDs to perform action on - active = request.POST.get('active') + + if self.model is None: + raise NotImplementedError( + "Subclasses of BaseActionView must define a 'model' attribute." + ) + + action = request.POST.get("action") # 'archive', 'active', or 'unarchive' + ids = request.POST.getlist("ids[]") # List of user IDs to perform action on + active = request.POST.get("active") print(f"arhive action {action} and id is {ids} and active data is {active}") - if action == 'archive': + if action == "archive": # Update 'deleted' field to True for the selected users self.model.objects.filter(id__in=ids).update(deleted=True, active=False) - message = 'Record archived successfully.' - elif action == 'active': + message = "Record archived successfully." + elif action == "active": # Update 'active' field to True for the selected users self.model.objects.filter(id__in=ids).update(active=active.capitalize()) - message = 'Record activated successfully.' - elif action == 'unarchive': + message = "Record activated successfully." + elif action == "unarchive": # Update 'deleted' field to False for the selected users self.model.objects.filter(id__in=ids).update(deleted=False) - message = 'Record unarchived successfully.' + message = "Record unarchived successfully." else: return JsonResponseUtil.error(message="Invalid Action") return JsonResponseUtil.success(message=message) +class BaseArchiveView(generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER + template_name = None + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + data["principal_id"] = kwargs.get("principal_id") + data["page_name"] = self.page_name + return data + + class IntoleranceView(BaseView): model = Intolerance template_name = "module_activity/intolerance_list.html" + +class CreateOrUpdateIntoleranceView(BaseCreateOrUpdateView): + model = Intolerance + page_title = "Intolerance" + form_class = IntoleranceForm + success_url = "module_activity:intolerance" + + class IntoleranceListJson(BaseListJson): model = Intolerance + class IntoleranceActionView(BaseActionView): model = Intolerance + +class IntoleranceArchiveView(generic.TemplateView): + template_name = "module_activity/intolerance_archive_list.html" + + class SymptomsView(BaseView): model = Symptoms template_name = "module_activity/symptoms_list.html" + +class CreateOrUpdateSymptomsView(BaseCreateOrUpdateView): + model = Symptoms + page_title = "Symptoms" + form_class = SymptomsForm + success_url = "module_activity:symptoms" + + class SymptomsListJson(BaseListJson): model = Symptoms + class SymptomsActionView(BaseActionView): model = Symptoms + +class SymptomsArchiveView(generic.TemplateView): + template_name = "module_activity/symptoms_archive_list.html" + + class PastTreatmentView(BaseView): model = PastTreatment template_name = "module_activity/past_treatment_list.html" + +class CreateOrUpdatePastTreatmentView(BaseCreateOrUpdateView): + model = PastTreatment + page_title = "Past Treatment" + form_class = PastTreatmentForm + success_url = "module_activity:past_treatment" + + class PastTreatmentListJson(BaseListJson): model = PastTreatment + class PastTreatmentActionView(BaseActionView): model = PastTreatment + +class PastTreatmentArchiveView(generic.TemplateView): + template_name = "module_activity/past_treatment_archive_list.html" + + class ChronicConditionView(BaseView): model = ChronicCondition template_name = "module_activity/chronic_conditon_list.html" + +class CreateOrUpdateChronicConditionView(BaseCreateOrUpdateView): + model = ChronicCondition + page_title = "Chronic Conditon/Disease" + form_class = ChronicConditionForm + success_url = "module_activity:chronic_condition" + + class ChronicConditionListJson(BaseListJson): model = ChronicCondition + class ChronicConditionActionView(BaseActionView): model = ChronicCondition + +class ChronicConditionArchiveView(generic.TemplateView): + template_name = "module_activity/chronic_condition_archive_list.html" + + class UserActivityRecordView(generic.View): def serialize_record(self, record): - time_obj = datetime.strptime(str(record.time), '%H:%M:%S') + time_obj = datetime.strptime(str(record.time), "%H:%M:%S") return { "id": record.id, "date": record.date, - "time": time_obj.strftime('%I:%M %p'), + "time": time_obj.strftime("%I:%M %p"), } def get(self, request, *args, **kwargs): try: - principal_id = self.kwargs.get('principal_id') + principal_id = self.kwargs.get("principal_id") date = request.GET.get("date") - print(f"principal_id is {principal_id} data is {date} and type is {type(date)}") + print( + f"principal_id is {principal_id} data is {date} and type is {type(date)}" + ) if not date: return JsonResponseUtil.error(message="Date parameter is missing") @@ -145,10 +284,16 @@ class UserActivityRecordView(generic.View): return JsonResponseUtil.error(message="Invalid date format") # Retrieve data from different models - meal_records = MealRecord.objects.filter(principal=principal_id, date=date_obj) - medication_records = Medication.objects.filter(principal=principal_id, date=date_obj) + meal_records = MealRecord.objects.filter( + principal=principal_id, date=date_obj + ) + medication_records = Medication.objects.filter( + principal=principal_id, date=date_obj + ) bowel_records = Bowel.objects.filter(principal=principal_id, date=date_obj) - meal_symptom_records = MealSymptomRecord.objects.filter(principal=principal_id, date=date_obj) + meal_symptom_records = MealSymptomRecord.objects.filter( + principal=principal_id, date=date_obj + ) print(f"==================meal record {meal_records}") # Prepare combined results data = [] @@ -177,71 +322,257 @@ class UserActivityRecordView(generic.View): except Exception as e: return JsonResponseUtil.error(message="Something went wrong", errors=str(e)) + class MealDetialView(generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER template_name = "module_activity/meal_detail.html" model = MealRecord def get_record(self): - id = self.kwargs.get('pk') + id = self.kwargs.get("pk") meal_record = get_object_or_404( self.model.objects.prefetch_related( - 'food_records', 'beverage_records', 'food_ingredient_records' + "food_records", "beverage_records", "food_ingredient_records" ), - id=id + id=id, ) return meal_record def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['obj'] = self.get_record() + context["obj"] = self.get_record() + context["page_name"] = self.page_name return context class MedicationDetailView(generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER template_name = "module_activity/medication_detail.html" model = Medication def get_record(self): - id = self.kwargs.get('pk') - obj = get_object_or_404( - self.model.objects.prefetch_related('medicines'), - id=id - ) + id = self.kwargs.get("pk") + obj = get_object_or_404(self.model.objects.prefetch_related("medicines"), id=id) return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['obj'] = self.get_record() + context["obj"] = self.get_record() + context["page_name"] = self.page_name return context class BowelDetailView(generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER template_name = "module_activity/bowel_detail.html" model = Bowel def get_record(self): - id = self.kwargs.get('pk') + id = self.kwargs.get("pk") obj = get_object_or_404(self.model, id=id) return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['obj'] = self.get_record() + context["obj"] = self.get_record() + context["page_name"] = self.page_name return context + class MealSymptomDetailView(generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER template_name = "module_activity/meal_symptom_details.html" model = MealSymptomRecord def get_record(self): - pk = self.kwargs.get('pk') + pk = self.kwargs.get("pk") obj = get_object_or_404( - MealSymptomRecord.objects.prefetch_related('symptoms_before_meal', 'symptoms_after_meal'), - id=pk + MealSymptomRecord.objects.prefetch_related( + "symptoms_before_meal", "symptoms_after_meal" + ), + id=pk, ) return obj def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['obj'] = self.get_record() - return context \ No newline at end of file + context["obj"] = self.get_record() + context["page_name"] = self.page_name + return context + + +class ReportChartView(generic.View): + + def get(self, request, *args, **kwargs): + current_year = int(self.request.GET.get("year")) + + monthly_counts = { + 'Meal': [0] * 12, + 'Medication': [0] * 12, + 'Symptoms': [0] * 12, + 'Bowel': [0] * 12, + } + + for month in range(1, 13): + start_date = datetime(current_year, month, 1) + end_date = datetime(current_year, month + 1, 1) if month < 12 else datetime(current_year + 1, 1, 1) + + monthly_counts['Meal'][month - 1] = MealRecord.objects.filter(date__range=(start_date, end_date)).count() + monthly_counts['Medication'][month - 1] = Medication.objects.filter(date__range=(start_date, end_date)).count() + monthly_counts['Symptoms'][month - 1] = MealSymptomRecord.objects.filter(date__range=(start_date, end_date)).count() + monthly_counts['Bowel'][month - 1] = Bowel.objects.filter(date__range=(start_date, end_date)).count() + + print(f"===========================================================data is {monthly_counts}") + + return JsonResponseUtil.success(message=constants.SUCCESS, data=monthly_counts) + + +class ReportDataView(generic.View): + model = MealRecord + + def get_user(self, *args, **kwargs): + print(f"user id is {self.kwargs.get('principal_id')}") + user = IAmPrincipal.objects.filter(id=self.kwargs.get("principal_id")).first() + print(f"user is {user}") + return user + + def enough_records_exist(self, start_date, end_date): + + min_days_required = 7 + current_date = start_date + count = 0 + + obj = self.model.objects.filter(principal=self.get_user()) + + while current_date <= end_date: + if obj.filter(date=current_date).exists(): + count += 1 + if count >= min_days_required: + return True + else: + count = 0 # Reset count if record is missing for any day + current_date += timedelta(days=1) + + return False + + def get_top_food_avoid(self, start_date, end_date): + """Get the top food to avoid.""" + food_counts = defaultdict(int) + ingredient_counts = defaultdict(int) + beverage_counts = defaultdict(int) + + symptom_records = MealSymptomRecord.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ) + + for symptom_record in symptom_records: + closest_meal = ( + MealRecord.objects.filter( + principal=symptom_record.principal, date__lte=symptom_record.date + ) + .order_by("-date", "-time") + .first() + ) + if closest_meal: + for food_record in closest_meal.food_records.all(): + food_counts[food_record.name] += 1 + for ingredient_record in closest_meal.food_ingredient_records.all(): + ingredient_counts[ingredient_record.name] += 1 + for beverage_record in closest_meal.beverage_records.all(): + beverage_counts[beverage_record.beverage_type] += 1 + + # Sort the dictionaries by their values in descending order and getting only top 3 record + food_counts = dict( + sorted(food_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + ingredient_counts = dict( + sorted(ingredient_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + beverage_counts = dict( + sorted(beverage_counts.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + food_avoid = next(iter(food_counts), None) + return food_avoid, food_counts, ingredient_counts, beverage_counts + + def get_symptoms_frequency(self, start_date, end_date): + """Get the frequency of symptoms.""" + symptom_records = MealSymptomRecord.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ).annotate( + before_meal_count=Count("symptoms_before_meal"), + after_meal_count=Count("symptoms_after_meal"), + ) + symptoms_frequency = defaultdict(int) + + for record in symptom_records: + for symptom in record.symptoms_before_meal.all(): + symptoms_frequency[symptom.name] += record.before_meal_count + for symptom in record.symptoms_after_meal.all(): + symptoms_frequency[symptom.name] += record.after_meal_count + + sorted_symptoms_frequency = dict( + sorted(symptoms_frequency.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + return sorted_symptoms_frequency + + def get_stool_type_counts(self, start_date, end_date): + """Get the count of stool types.""" + stool_type_counts = ( + Bowel.objects.filter( + principal=self.get_user(), date__range=(start_date, end_date) + ) + .values("stool_type") + .annotate(stool_type_count=Count("stool_type")) + ) + stool_type_counts_dict = { + item["stool_type"]: item["stool_type_count"] for item in stool_type_counts + } + + stool_type_counts_sort = dict( + sorted(stool_type_counts_dict.items(), key=lambda x: x[1], reverse=True)[:3] + ) + + highest_stool = next(iter(stool_type_counts_sort), None) + return stool_type_counts_sort, highest_stool + + def get(self, request, *args, **kwargs): + date_range = request.GET.get("date_range") + start_date, end_date = date_utils.get_date_range(date_range) + print(f"start date is {start_date}, end_date is {end_date}") + + print(f"is dats exist {self.enough_records_exist(start_date, end_date)}") + + if not self.enough_records_exist(start_date, end_date): + print("report does not exist") + return JsonResponseUtil.success( + message="No report is generated. Minimum Previous 7 days of records required.", status=204 + ) + + # Get top food to avoid + food_avoid, food_counts, ingredient_counts, beverage_counts = ( + self.get_top_food_avoid(start_date, end_date) + ) + + # Get symptoms frequency + sorted_symptoms_frequency = self.get_symptoms_frequency(start_date, end_date) + + # Get stool type counts + stool_type_counts_sort, highest_stool = self.get_stool_type_counts( + start_date, end_date + ) + + nested_json = { + "food_avoid": food_avoid, + "same_food_avoid": { + "food": food_counts, + "ingredient": ingredient_counts, + "beverage": beverage_counts, + }, + "symptoms_frequency": sorted_symptoms_frequency, + "highest_stool": highest_stool, + "stool_type": stool_type_counts_sort, + } + print(f"nested_json data is {nested_json}") + return JsonResponseUtil.success(message=constants.SUCCESS, data=nested_json) diff --git a/module_auth/api/serializers.py b/module_auth/api/serializers.py index fd587ee..9d228f3 100644 --- a/module_auth/api/serializers.py +++ b/module_auth/api/serializers.py @@ -3,6 +3,7 @@ from rest_framework import serializers from module_iam.models import IAmPrincipal from module_project import constants from django.contrib.auth import authenticate +from rest_framework.validators import UniqueValidator # class BasePasswordSerializer(serializers.Serializer): # confirm_password = serializers.CharField(write_only=True, required=True) @@ -22,6 +23,10 @@ from django.contrib.auth import authenticate # return instance class RegistrationSerializer(serializers.ModelSerializer): + email = serializers.EmailField( + required=True, + validators=[UniqueValidator(queryset=IAmPrincipal.objects.all(), message="This email address is already in use.")] + ) password = serializers.CharField(write_only=True, required=True) confirm_password = serializers.CharField(write_only=True, required=True) diff --git a/module_auth/api/urls.py b/module_auth/api/urls.py index e82e204..d0e924e 100644 --- a/module_auth/api/urls.py +++ b/module_auth/api/urls.py @@ -12,5 +12,11 @@ urlpatterns = [ path("verify-otp/", views.OTPVerificationView.as_view()), path("forget-password/", views.ForgetPasswordView.as_view()), - # path("profile/", views.Profile) + path("account/deactivate/", views.AccountDeactivateView.as_view()), + + path('google-signin/', views.GoogleSignin.as_view(), name='google_signin'), + path('apple-signin/', views.AppleSignin.as_view(), name='apple_signin'), + + path('version-check/', views.VersionCheck.as_view(), name='version_check'), + ] diff --git a/module_auth/api/utils.py b/module_auth/api/utils.py index 8d9857e..3e7be9b 100644 --- a/module_auth/api/utils.py +++ b/module_auth/api/utils.py @@ -4,6 +4,7 @@ from module_project.utils import ApiResponse from module_iam.models import IAmPrincipal, IAmPrincipalOtp from rest_framework_simplejwt.tokens import RefreshToken from django.core.exceptions import ValidationError +import requests import logging logger = logging.getLogger(__name__) @@ -23,12 +24,21 @@ def generate_token_and_user_data(principal): data = { "access": str(refresh.access_token), "refresh": str(refresh), - "first_name": principal.first_name, - "phone_no": str(principal.phone_no), "complete": principal.register_complete, } return data +class GoogleAuthService(): + @staticmethod + def get_user_info(access_token): + headers = {'Authorization': f'Bearer {access_token}'} + response = requests.get( + 'https://www.googleapis.com/oauth2/v3/userinfo', + headers=headers, + ) + user_info = response.json() + return user_info + class AuthService: """ Provides authentication services for IAmPrincipal users. diff --git a/module_auth/api/views.py b/module_auth/api/views.py index 3a847e8..5a1509b 100644 --- a/module_auth/api/views.py +++ b/module_auth/api/views.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from rest_framework import status from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated @@ -6,14 +6,23 @@ from rest_framework_simplejwt.authentication import JWTAuthentication from module_project import constants from module_project.service import SMSService, EmailService from module_project.utils import ApiResponse -from .utils import AuthService -from module_iam.models import IAmPrincipal, IAmPrincipalOtp -from .serializers import RegistrationSerializer, LoginSerializer, OtpVerificationSerializer, PasswordResetSerializer +from .utils import AuthService, GoogleAuthService +from django.contrib.auth import authenticate +import requests +from module_iam.models import AppVersion, IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType, IAmPrincipalSource +from .serializers import ( + RegistrationSerializer, + LoginSerializer, + OtpVerificationSerializer, + PasswordResetSerializer, +) from django.conf import settings from rest_framework.response import Response from .utils import ( - generate_token_and_user_data, get_principal_by_email, authticate_with_otp_and_passsword + generate_token_and_user_data, + get_principal_by_email, + authticate_with_otp_and_passsword, ) @@ -36,14 +45,19 @@ class RegistrationView(APIView): try: instance = serializer.save() - principal = instance - token_data = generate_token_and_user_data(principal) + instance.last_login = datetime.now() + instance.principal_type = IAmPrincipalType.get_principal_user() + instance.principal_source = IAmPrincipalSource.get_principal_app() + instance.save() + token_data = generate_token_and_user_data(instance) except Exception as e: return ApiResponse.error( status=status.HTTP_403_FORBIDDEN, message=str(e), errors=str(e) ) - return ApiResponse.success(message=constants.REGISTRATION_SUCCESS, data=token_data) + return ApiResponse.success( + message=constants.REGISTRATION_SUCCESS, data=token_data + ) class LoginView(APIView): @@ -81,32 +95,9 @@ class LoginView(APIView): print("Errror reponse") return validation_result # Return the error response if validation fails - - # auth_service = AuthService(principal_model=IAmPrincipal) - - # try: - # principal = self.model.objects.get(email=email) - # except Exception as e: - # error_response = { - # "status": status.HTTP_403_FORBIDDEN, - # "message": constants.INVALID_EMAIL_PASSWORD, - # "errors": constants.INVALID_EMAIL_PASSWORD, - # } - # return ApiResponse.error(**error_response) - - # try: - # auth_service.authenticate(principal_id=principal.id, password=password) - # except Exception as e: - # error_response = { - # "status": status.HTTP_403_FORBIDDEN, - # "message": e, - # "errors": e, - # } - # return ApiResponse.error(**error_response) - try: principal.player_id = player_id - principal.last_login = datetime.datetime.now() + principal.last_login = datetime.now() principal.save() except Exception as e: error_response = { @@ -126,7 +117,9 @@ class OtpRequestView(APIView): def post(self, request): if "email" not in request.data: - return ApiResponse.error(message=constants.EMAIL_REQUIRED, errors=constants.EMAIL_REQUIRED) + return ApiResponse.error( + message=constants.EMAIL_REQUIRED, errors=constants.EMAIL_REQUIRED + ) print(f"email auth username: {settings.EMAIL_HOST_USER}") email = request.data.get("email") @@ -139,7 +132,9 @@ class OtpRequestView(APIView): # auth_service = AuthService(IAmPrincipal) # principal = auth_service.get_principal_by_email(request.data.get("email")) - otp_code = SMSService().create_otp(principal=principal, otp_purpose="Forget password") + otp_code = SMSService().create_otp( + principal=principal, otp_purpose="Forget password" + ) except Exception as e: return ApiResponse.error(message=str(e), errors=str(e)) @@ -147,18 +142,23 @@ class OtpRequestView(APIView): email_service = EmailService( subject="Forget Password", to=principal.email, - from_email=settings.EMAIL_HOST_USER + from_email=settings.EMAIL_HOST_USER, ) # Send the email try: - email_service.load_template("module_auth/email_template.html", context={"code": otp_code} ) + email_service.load_template( + "module_auth/email_template.html", context={"code": otp_code, "name": principal.first_name} + ) email_service.send() except Exception as e: - return ApiResponse.error(message=f"Error sending email: {str(e)}", errors=str(e)) + return ApiResponse.error( + message=f"Error sending email: {str(e)}", errors=str(e) + ) return ApiResponse.success(message=constants.SUCCESS) + class OTPVerificationView(APIView): authentication_classes = [] permission_classes = [] @@ -173,7 +173,7 @@ class OTPVerificationView(APIView): "errors": serializer.errors, } return ApiResponse.error(**error_response) - + email = serializer.validated_data.get("email") otp = serializer.validated_data.get("otp") @@ -181,18 +181,16 @@ class OTPVerificationView(APIView): if isinstance(principal, Response): return principal - - validation_result = authticate_with_otp_and_passsword( - principal, otp=otp - ) + + validation_result = authticate_with_otp_and_passsword(principal, otp=otp) print("pasword instance ", validation_result) if isinstance(validation_result, Response): print("Errror reponse") return validation_result # Return the error response if validation fails - token_data = generate_token_and_user_data(principal) - return ApiResponse.success(message=constants.SUCCESS, data=token_data) + return ApiResponse.success(message=constants.SUCCESS) + class ForgetPasswordView(APIView): authentication_classes = [JWTAuthentication] @@ -200,6 +198,18 @@ class ForgetPasswordView(APIView): serializer_class = PasswordResetSerializer def post(self, request): + email = request.data.get("email") + + principal = get_principal_by_email(email=email) + + otp_instance = IAmPrincipalOtp.objects.filter(principal=principal).last() + + if not otp_instance: + return ApiResponse.error(message=constants.SESSION_EXPIRED) + + if otp_instance.is_expired(): + return ApiResponse.error(message=constants.SESSION_EXPIRED) + serializer = self.serializer_class(request.user, data=request.data) if not serializer.is_valid(): error_response = { @@ -214,4 +224,142 @@ class ForgetPasswordView(APIView): except Exception as e: return ApiResponse.error(message=str(e), errors=str(e)) - return ApiResponse.success(message=constants.SUCCESS) \ No newline at end of file + return ApiResponse.success(message=constants.SUCCESS) + + +class AccountDeactivateView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def delete(self, request): + try: + user = IAmPrincipal.objects.get(id=request.user.id) + user.is_active = False + user.deleted = True + user.save() + except Exception as e: + return ApiResponse.error(message=constants.INTERNAL_SERVER_ERROR, errors=str(e)) + + return ApiResponse.success(message=constants.ACCOUNT_DEACTIVATED) + + +class GoogleSignin(APIView): + authentication_classes = [] + permission_classes = [] + + def post(self, request): + try: + access_token = request.data["access_token"] + user_info = GoogleAuthService.get_user_info(access_token) + + print(f"User Info : {user_info}") + + # Authenticate user with the email provided by Google + user = IAmPrincipal.objects.filter(email=user_info['email']).first( + ) or authenticate(email=user_info['email'], password=None) + + if user is None: + # Create a new user if not found + user = IAmPrincipal.objects.create_user( + username=user_info['email'], + email=user_info['email'], + first_name=f"{user_info['given_name']} {user_info['family_name']}", + last_login=datetime.now(), + principal_type=IAmPrincipalType.get_principal_user(), + principal_source=IAmPrincipalSource.get_principal_google() + ) + user.save() + + token_data = generate_token_and_user_data(user) + + # return Response({"token": token.key}, status=status.HTTP_200_OK) + return ApiResponse.success( + message=constants.SUCCESS, data=token_data + ) + + except Exception as e: + return ApiResponse.error(message=constants.FAILURE, errors=str(e)) + + +import jwt +class AppleSignin(APIView): + authentication_classes = [] + permission_classes = [] + + def post(self, request): + try: + authorization_code = request.data['authorization_code'] + headers = { + 'Authorization': f"Bearer {settings.SOCIAL_AUTH_APPLE_CLIENT_SECRET}" + } + + response = requests.post( + 'https://appleid.apple.com/auth/token', + data={ + 'client_id': settings.SOCIAL_AUTH_APPLE_CLIENT_ID, + 'code': authorization_code, + 'grant_type': 'authorization_code', + 'redirect_uri': False, + }, + headers=headers, + ) + + response_data = response.json() + id_token = response_data.get('id_token') + + decoded = jwt.decode( + id_token, + '', + algorithms=['ES256'], + options={ + 'verify_aud': False, + 'verify_exp': False, + 'verify_iat': False, + }, + ) + email = decoded.get('email') + full_name = f"{decoded.get('given_name')} {decoded.get('family_name')}" + if IAmPrincipal.objects.filter(email=email).exists(): + user = IAmPrincipal.objects.get(email=email) + else: + user = IAmPrincipal.objects.create_user( + username=email, + email=email, + first_name=full_name, + ) + user.save() + + token_data = generate_token_and_user_data(user) + + return ApiResponse.success( + message=constants.SUCCESS, data=token_data + ) + + except Exception as e: + return ApiResponse.error(message=constants.FAILURE, errors=str(e)) + + +class VersionCheck(APIView): + authentication_classes = [] + permission_classes = [] + + def get(self, request, *args, **kwargs): + app_version = request.GET.get('appVersion') + + # Query the database to retrieve the upgrade flags based on the app version + try: + version = AppVersion.objects.get(version=app_version) + except AppVersion.DoesNotExist: + version = None + + if version: + upgrade_flags = { + 'forceUpgrade': version.force_upgrade, + 'recommendUpgrade': version.recommend_upgrade, + } + else: + upgrade_flags = { + 'forceUpgrade': False, + 'recommendUpgrade': False, + } + return ApiResponse.success(message=constants.SUCCESS, data=upgrade_flags) \ No newline at end of file diff --git a/module_auth/forms.py b/module_auth/forms.py index 84d1611..0a90eb6 100644 --- a/module_auth/forms.py +++ b/module_auth/forms.py @@ -1,6 +1,7 @@ from django import forms from django.core import validators from module_project import constants +from module_iam.models import IAmPrincipal class LoginForm(forms.Form): email = forms.EmailField( @@ -12,4 +13,57 @@ class LoginForm(forms.Form): label="Password", strip=False, widget=forms.PasswordInput() - ) \ No newline at end of file + ) + + + +class UserForm(forms.ModelForm): + password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}), + validators=[ + validators.MinLengthValidator( + limit_value=6, message="Password must be at least 6 characters long. " + ) + ], + ) + confirm_password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}) + ) + + class Meta: + model = IAmPrincipal + fields = [ + "first_name", + "email", + "password", + "confirm_password", + ] + labels = { + "first_name": "Name", + } + + def clean_email(self): + email = self.cleaned_data.get('email') + if IAmPrincipal.objects.filter(email=email).exists(): + raise forms.ValidationError("This email address is already in use.") + return email + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get("password") + confirm_password = cleaned_data.get("confirm_password") + + if password and confirm_password and password != confirm_password: + self.add_error("confirm_password", "Passwords do not match.") + return cleaned_data + + def save(self, commit=True): + instance = super().save(commit=False) + # Check if it's a new object (create action) or an existing one (update action) + if not instance.pk: # pk is None for new objects + instance.username = self.cleaned_data["email"] + instance.set_password(self.cleaned_data["password"]) + if commit: + instance.save() + return instance + diff --git a/module_auth/urls.py b/module_auth/urls.py index 324493f..8deb491 100644 --- a/module_auth/urls.py +++ b/module_auth/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from django.views.generic import TemplateView app_name = "module_auth" @@ -11,7 +12,12 @@ urlpatterns = [ path('password-reset-confirm///', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('password-reset-complete/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'), path('users/', views.UserDashView.as_view(), name='users'), + path('users/add/', views.UserCreateOrUpdateView.as_view(), name='user_add'), + path('users/edit//', views.UserCreateOrUpdateView.as_view(), name='user_edit'), path('users/list/', views.UserListJson.as_view(), name='users_list'), + path('users/action/', views.UserActionView.as_view(), name='users_action'), path('user/view//', views.UserRecordView.as_view(), name='user_view'), + path('user/archive/list/', views.UserArchiveList.as_view(), name='user_archive'), + path('user/count/', views.UsersCountView.as_view(), name="user_count") ] diff --git a/module_auth/views.py b/module_auth/views.py index eec2e7e..b28597a 100644 --- a/module_auth/views.py +++ b/module_auth/views.py @@ -1,5 +1,6 @@ import logging +from datetime import datetime from django.db.models import Q, Prefetch from django.contrib import messages from django.contrib.auth import authenticate, login, logout @@ -16,10 +17,13 @@ from django.contrib.auth.views import ( from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse_lazy from django.views import generic -from .forms import LoginForm -from module_iam.models import IAmPrincipal +from .forms import LoginForm, UserForm +from module_iam.models import IAmPrincipal, IAmPrincipalType +from module_iam import iam_constant from module_activity.models import PrincipalHealthData, Intolerance, Symptoms, PastTreatment, ChronicCondition from django_datatables_view.base_datatable_view import BaseDatatableView +from module_project.mixins import ActionMixin +from module_project.utils import JsonResponseUtil from module_project import constants @@ -74,7 +78,7 @@ class CustomPasswordResetDoneView(PasswordResetDoneView): class UserDashView(LoginRequiredMixin, generic.TemplateView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_USER resource = None action = None template_name = "module_auth/users_list.html" @@ -86,20 +90,76 @@ class UserDashView(LoginRequiredMixin, generic.TemplateView): context["page_name"] = self.page_name return context +class UserCreateOrUpdateView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_MANAGE_USER + model = IAmPrincipal + form_class = UserForm + template_name = "module_auth/user_add.html" + success_url = reverse_lazy("module_auth:users") + success_message = "Saved Successfully" + error_message = "An error occurred while saving the data." + + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Edit" if self.object else "Add", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + # @transaction.atomic + def post(self, request, *args, **kwargs): + print(request.POST) + self.object = self.get_object() + form = self.form_class(request.POST, instance=self.object) + try: + if form.is_valid(): + principal = form.save(commit=False) + + # Check if it's a new object (create action) or an existing one (update action) + if not principal.pk: # pk is None for new objects + principal.created_by = request.user + principal.principal_type = IAmPrincipalType.objects.filter(name=iam_constant.PRINCIPAL_TYPE_USER).first() + principal.modified_by = request.user + principal.modified_on = datetime.now() + + # Save the object + principal.save() + + messages.success(request, "Form submitted successfully") + return redirect(self.success_url) + except Exception as e: + self.error_message = constants.ERROR_OCCURR.format(str(e)) + print(self.error_message) + messages.error(request, self.error_message) + + context = self.get_context_data(form=form) + return render(request, template_name=self.template_name, context=context) + class UserListJson(BaseDatatableView): model = IAmPrincipal columns = ["id", "first_name", "email", "phone_no", "date_of_birth", "is_active"] order_columns = ["id", "first_name", "email", "phone_no", "date_of_birth", "is_active"] + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', False) + return self.model.objects.filter(principal_type=IAmPrincipalType.get_principal_user(), deleted=deleted_flag) + def filter_queryset(self, qs): print(f"request is {self.request.GET}") search_value = self.request.GET.get("search[value]", None) if search_value: - # print(f"isdiget {search_value.isdigit()}") - # if search_value.isdigit(): - # qs = qs.filter(Q(id=search_value)) - qs = qs.filter( Q(id__icontains=search_value) | Q(first_name__icontains=search_value) @@ -115,9 +175,34 @@ class UserListJson(BaseDatatableView): return qs +class UserActionView(ActionMixin): + model = IAmPrincipal + + def post(self, request, *args, **kwargs): + + action = request.POST.get('action') # 'archive', 'active', or 'unarchive' + ids = request.POST.getlist('ids[]') # List of IDs to perform action on + active = request.POST.get('active') + print(f"arhive action {action} and id is {ids} and active data is {active}") + if action == 'archive': + # Update 'deleted' field to True for the selected users + self.model.objects.filter(id__in=ids).update(deleted=True, is_active=False) + message = 'Record archived successfully.' + elif action == 'active': + # Update 'active' field to True for the selected users + self.model.objects.filter(id__in=ids).update(is_active=active.capitalize()) + message = 'Record updated successfully.' + elif action == 'unarchive': + # Update 'deleted' field to False for the selected users + self.model.objects.filter(id__in=ids).update(deleted=False) + message = 'Record unarchived successfully.' + else: + return JsonResponseUtil.error(message="Invalid Action") + + return JsonResponseUtil.success(message=message) class UserRecordView(LoginRequiredMixin, generic.View): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_USER resource = None action = None model = IAmPrincipal @@ -160,39 +245,24 @@ class UserRecordView(LoginRequiredMixin, generic.View): chronic_prefetch ).get(id=id) - print(f"prefetch datatas") for data in obj.chronic_data: print(f"data is {data.name, data.duration}") # Render the template with the principal instance and related data - return render(request, self.template_name, {'obj': obj}) - - - - - - - - - - - - - - - - - - - - - - - - + return render(request, self.template_name, {'obj': obj, 'page_name': self.page_name}) +class UserArchiveList(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_USER + resource = None + action = None + template_name = "module_auth/users_archive_list.html" + model = IAmPrincipal + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context class CustomPasswordResetConfirmView(PasswordResetConfirmView): template_name = "module_auth/password_reset_confirm.html" @@ -201,3 +271,23 @@ class CustomPasswordResetConfirmView(PasswordResetConfirmView): class CustomPasswordResetCompleteView(PasswordResetCompleteView): template_name = "module_auth/password_reset_complete.html" + + +class UsersCountView(generic.View): + + def get(self, request): + current_year = int(self.request.GET.get("year")) + user_counts = [] + + # Iterate over each month from January to December + for month in range(1, 13): + # Calculate the start and end dates for the current month + start_date = datetime(current_year, month, 1) + end_date = datetime(current_year, month + 1, 1) if month < 12 else datetime(current_year + 1, 1, 1) + # Query the User model to count users created within the current month + user_count = IAmPrincipal.objects.filter(date_joined__range=(start_date, end_date)).count() + + # Append the count to the list + user_counts.append(user_count) + + return JsonResponseUtil.success(message=constants.SUCCESS, data=user_counts) \ No newline at end of file diff --git a/module_cms/api/serializers.py b/module_cms/api/serializers.py index f30c66e..e5b908c 100644 --- a/module_cms/api/serializers.py +++ b/module_cms/api/serializers.py @@ -7,11 +7,6 @@ class FaqSerializer(serializers.ModelSerializer): model = Faqs fields = ["id", "question", "answer"] -class FaqListSerializer(serializers.ModelSerializer): - class Meta: - model = Faqs - fields = "__all__" - class OrganizationSerializer(serializers.ModelSerializer): about_us = serializers.CharField(source='about_us.html', read_only=True) terms_condition = serializers.CharField(source='terms_condition.html', read_only=True) diff --git a/module_cms/api/views.py b/module_cms/api/views.py index c4e2140..4604042 100644 --- a/module_cms/api/views.py +++ b/module_cms/api/views.py @@ -19,8 +19,8 @@ class FaqListAPIView(APIView): return ApiResponse.success(message=constants.SUCCESS, data=serializer.data) class OrganizationAPIView(APIView): - authentication_classes = [JWTAuthentication] - permission_classes = [IsAuthenticated] + authentication_classes = [] + permission_classes = [] serializer_class = OrganizationSerializer model = Organization diff --git a/module_cms/forms.py b/module_cms/forms.py index 2918c27..ff681cc 100644 --- a/module_cms/forms.py +++ b/module_cms/forms.py @@ -68,16 +68,4 @@ class FaqsForm(forms.ModelForm): # "faq_category", "question", "answer", - "active", ] - # labels = {"faq_category": "Category"} - - def __init__(self, *args, **kwargs): - instance = kwargs.get("instance") - super().__init__(*args, **kwargs) - # Fetch the choices for the faq_category field from the database - # self.fields["faq_category"].queryset = FaqCategory.objects.all() - - if instance is None: - # This is an add operation, exclude the 'active' field - self.fields.pop("active") \ No newline at end of file diff --git a/module_cms/urls.py b/module_cms/urls.py index 12f3a39..dbae20e 100644 --- a/module_cms/urls.py +++ b/module_cms/urls.py @@ -7,6 +7,8 @@ urlpatterns = [ path('faq/', views.FaqView.as_view(), name="faq"), path('faq/list/', views.FaqListJson.as_view(), name="faq_list"), path('faq/add/', views.FaqCreateOrUpdateView.as_view(), name='faq_add'), + path('faq/edit//', views.FaqCreateOrUpdateView.as_view(), name='faq_edit'), + path('faq/action/', views.FaqActionView.as_view(), name='faq_action'), path('about-us/', views.AboutUsView.as_view(), name='about_us'), path('about-us/edit/', views.AboutUsCreateOrUpdateView.as_view(), name='about_us_add'), diff --git a/module_cms/views.py b/module_cms/views.py index f7d8bb5..ed65bb4 100644 --- a/module_cms/views.py +++ b/module_cms/views.py @@ -7,11 +7,12 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse_lazy from django.views import generic from module_iam.models import IAmPrincipal -from .forms import AboutUsForm, TermsAndConditionForm, FaqCategoryFrom, PrivacyPolicyForm +from module_iam import iam_constant +from .forms import AboutUsForm, TermsAndConditionForm, FaqsForm, PrivacyPolicyForm from .models import Faqs, Organization -from .api.serializers import FaqListSerializer from module_project.mixins import DatatablesMixin from django_datatables_view.base_datatable_view import BaseDatatableView +from module_project.mixins import ActionMixin from module_project import constants @@ -19,7 +20,7 @@ logger = logging.getLogger(__name__) class FaqView(LoginRequiredMixin, generic.TemplateView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_FAQS resource = None action = None template_name = "module_cms/faq.html" @@ -32,43 +33,16 @@ class FaqView(LoginRequiredMixin, generic.TemplateView): return context -# class FaqDatatableView(DatatablesMixin, LoginRequiredMixin, generic.View): -# model = Faqs - -# def get_queryset(self): -# return self.model.objects.filter(deleted=False) - -# def get(self, request): -# ( -# draw, -# start, -# length, -# order_columns, -# order_directions, -# search_value, -# ) = self.get_datatables_params(request) -# queryset = self.get_queryset() - -# page_obj, total_count, filtered_count = self.get_pagination( -# queryset, start, length -# ) - -# serializer = FaqListSerializer( -# page_obj.object_list, many=True -# ) - -# response = self.prepare_datatables_response( -# draw, total_count, filtered_count, serializer.data -# ) - -# return response - - class FaqListJson(BaseDatatableView): model = Faqs columns = ["id", "question", "answer", "active", "deleted"] order_columns = ["id", "question", "answer", "active", "deleted"] + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', None) + + return self.model.objects.filter(deleted=deleted_flag) + def filter_queryset(self, qs): # Implement your custom filtering logic here print(f"request is {self.request.GET}") @@ -88,14 +62,75 @@ class FaqListJson(BaseDatatableView): return qs -class FaqCreateOrUpdateView(generic.View): - pass +class FaqCreateOrUpdateView(LoginRequiredMixin, generic.View): + # Set the page_name and resource + page_name = iam_constant.RESOURCE_MANAGE_FAQS + resource = iam_constant.RESOURCE_MANAGE_FAQS + # Initialize the action as ACTION_CREATE (can change based on logic) + action = iam_constant.ACTION_CREATE # Default action + template_name = "module_cms/faq_add.html" + model = Faqs + form_class = FaqsForm + success_url = reverse_lazy("module_cms:faq") + error_message = "An error occurred while saving the data." + # Determine the success message dynamically based on whether it's an update or create + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + # Get the object (if exists) based on URL parameter 'pk + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + # Add page_name and operation to the context + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = iam_constant.ACTION_UPDATE + + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + print("Request data: ", request.POST) + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = iam_constant.ACTION_UPDATE + + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + +class FaqActionView(ActionMixin): + model = Faqs class AboutUsView(LoginRequiredMixin, generic.DetailView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_CMS template_name = "module_cms/about_us_view.html" model = Organization context_object_name = "organization" @@ -111,7 +146,7 @@ class AboutUsView(LoginRequiredMixin, generic.DetailView): class AboutUsCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_CMS resource = None # Initialize the action as ACTION_CREATE (can change based on logic) @@ -173,7 +208,7 @@ class AboutUsCreateOrUpdateView(LoginRequiredMixin, generic.View): class TermsConditionView(LoginRequiredMixin, generic.DetailView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_T_C resource = None action = None template_name = "module_cms/terms_and_condition_view.html" @@ -191,7 +226,7 @@ class TermsConditionView(LoginRequiredMixin, generic.DetailView): class TermsConditionCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_T_C resource = None # Initialize the action as ACTION_CREATE (can change based on logic) @@ -253,7 +288,7 @@ class TermsConditionCreateOrUpdateView(LoginRequiredMixin, generic.View): class PrivacyPolicyView(LoginRequiredMixin, generic.DetailView): - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY resource = None action = None template_name = "module_cms/privacy_policy_view.html" @@ -271,7 +306,7 @@ class PrivacyPolicyView(LoginRequiredMixin, generic.DetailView): class PrivacyPolicyCreateOrUpdateView(LoginRequiredMixin, generic.View): # Set the page_name and resource - page_name = None + page_name = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY resource = None # Initialize the action as ACTION_CREATE (can change based on logic) diff --git a/module_iam/fixtures/iam_actions_fixture.json b/module_iam/fixtures/iam_actions_fixture.json new file mode 100644 index 0000000..151cd0e --- /dev/null +++ b/module_iam/fixtures/iam_actions_fixture.json @@ -0,0 +1,46 @@ +[ + { + "model": "module_iam.iamappaction", + "pk": 1, + "fields": { + "name": "create", + "label": "create", + "slug": "create", + "created_on": "2024-03-10T01:39:43.656133", + "modified_on": "2024-03-10T01:39:43.656133" + } + }, + { + "model": "module_iam.iamappaction", + "pk": 2, + "fields": { + "name": "read", + "label": "read", + "slug": "read", + "created_on": "2024-03-10T01:39:43.656133", + "modified_on": "2024-03-10T01:39:43.656133" + } + }, + { + "model": "module_iam.iamappaction", + "pk": 3, + "fields": { + "name": "update", + "label": "update", + "slug": "update", + "created_on": "2024-03-10T01:39:43.656133", + "modified_on": "2024-03-10T01:39:43.656133" + } + }, + { + "model": "module_iam.iamappaction", + "pk": 4, + "fields": { + "name": "delete", + "label": "delete", + "slug": "delete", + "created_on": "2024-03-10T01:39:43.656133", + "modified_on": "2024-03-10T01:39:43.656133" + } + } +] \ No newline at end of file diff --git a/module_iam/fixtures/iam_principal_source_fixture.json b/module_iam/fixtures/iam_principal_source_fixture.json new file mode 100644 index 0000000..d973cb5 --- /dev/null +++ b/module_iam/fixtures/iam_principal_source_fixture.json @@ -0,0 +1,46 @@ +[ + { + "model": "module_iam.iamprincipalsource", + "pk": 1, + "fields": { + "name": "app", + "label": "app", + "slug": "app", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + }, + { + "model": "module_iam.iamprincipalsource", + "pk": 2, + "fields": { + "name": "web", + "label": "web", + "slug": "web", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + }, + { + "model": "module_iam.iamprincipalsource", + "pk": 3, + "fields": { + "name": "google", + "label": "google", + "slug": "google", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + }, + { + "model": "module_iam.iamprincipalsource", + "pk": 4, + "fields": { + "name": "apple", + "label": "apple", + "slug": "apple", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + } +] \ No newline at end of file diff --git a/module_iam/fixtures/iam_principal_type_fixture.json b/module_iam/fixtures/iam_principal_type_fixture.json new file mode 100644 index 0000000..94c774c --- /dev/null +++ b/module_iam/fixtures/iam_principal_type_fixture.json @@ -0,0 +1,35 @@ +[ + { + "model": "module_iam.iamprincipaltype", + "pk": 1, + "fields": { + "name": "admin", + "label": "admin", + "slug": "admin", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + }, + { + "model": "module_iam.iamprincipaltype", + "pk": 2, + "fields": { + "name": "subadmin", + "label": "subadmin", + "slug": "subadmin", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + }, + { + "model": "module_iam.iamprincipaltype", + "pk": 3, + "fields": { + "name": "user", + "label": "user", + "slug": "user", + "created_on": "2024-03-10T01:39:43.648496", + "modified_on": "2024-03-10T01:39:43.648496" + } + } +] \ No newline at end of file diff --git a/module_iam/fixtures/iam_resources_fixture.json b/module_iam/fixtures/iam_resources_fixture.json new file mode 100644 index 0000000..24caac2 --- /dev/null +++ b/module_iam/fixtures/iam_resources_fixture.json @@ -0,0 +1,172 @@ +[ + { + "model": "module_iam.iamappresource", + "pk": 1, + "fields": { + "name": "manage_dashboard", + "label": "manage_dashboard", + "slug": "manage_dashboard", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 2, + "fields": { + "name": "manage_iam", + "label": "manage_iam", + "slug": "manage_iam", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 3, + "fields": { + "name": "manage_user", + "label": "manage_user", + "slug": "manage_user", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 4, + "fields": { + "name": "manage_support", + "label": "manage_support", + "slug": "manage_support", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 5, + "fields": { + "name": "manage_contact_us", + "label": "manage_contact_us", + "slug": "manage_contact_us", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 6, + "fields": { + "name": "manage_feedback", + "label": "manage_feedback", + "slug": "manage_feedback", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 7, + "fields": { + "name": "manage_cms", + "label": "manage_cms", + "slug": "manage_cms", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 8, + "fields": { + "name": "manage_faqs", + "label": "manage_faqs", + "slug": "manage_faqs", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 9, + "fields": { + "name": "manage_tc", + "label": "manage_tc", + "slug": "manage_tc", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "model": "module_iam.iamappresource", + "pk": 10, + "fields": { + "name": "manage_privacypolicy", + "label": "manage_privacypolicy", + "slug": "manage_privacypolicy", + "created_on": "2024-03-10T01:39:43.657388", + "modified_on": "2024-03-10T01:39:43.657388", + "action": [ + 1, + 2, + 3, + 4 + ] + } + } +] \ No newline at end of file diff --git a/module_iam/forms.py b/module_iam/forms.py new file mode 100644 index 0000000..8ecffc7 --- /dev/null +++ b/module_iam/forms.py @@ -0,0 +1,325 @@ +from typing import Any + +from django import forms +from django.core.exceptions import ValidationError +from django.core import validators +from django.utils.translation import gettext_lazy as _ + +from module_project import constants + +from . import models +# from .backend import EmailBackend +# from phonenumber_field.formfields import PhoneNumberField +from .iam_constant import PRINCIPAL_TYPE_ADMIN, PRINCIPAL_TYPE_SUBADMIN +from django.contrib.auth import authenticate + + +class CustomAuthenticationForm(forms.Form): + email = forms.EmailField( + max_length=254, + widget=forms.TextInput(attrs={"autofocus": True}), + label=_("Email"), + ) + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}), + ) + + def clean(self): + email = self.cleaned_data.get("email") + password = self.cleaned_data.get("password") + self.user = None + if email and password: + + user = authenticate(email=email, password=password) + + if user is None: + raise ValidationError({"__all__": [constants.INVALID_EMAIL_PASSWORD]}) + elif not user.is_active: + raise ValidationError({"__all__": [constants.ACCOUNT_DEACTIVATED]}) + self.user = user + return self.cleaned_data + + +class IAmPrincipalForm(forms.ModelForm): + password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}), + validators=[ + validators.MinLengthValidator( + limit_value=6, message="Password must be at least 6 characters long. " + ) + ], + ) + confirm_password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}) + ) + + is_active = forms.BooleanField( + label="Active", + initial=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + required=False, + ) + + class Meta: + model = models.IAmPrincipal + fields = [ + "principal_type", + "first_name", + "last_name", + "email", + "password", + "confirm_password", + "is_active", + ] + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + super().__init__(*args, **kwargs) + self.fields["principal_type"].queryset = models.IAmPrincipalType.objects.filter( + active=True, deleted=False + ) + # If it's a create action, exclude 'is_active' field + if instance is None: + self.fields.pop("is_active", None) + else: + # Exclude 'password' and 'confirm_password' fields for updates + self.fields.pop("password", None) + self.fields.pop("confirm_password", None) + + # Make the 'email' field read-only + self.fields["email"].widget.attrs["readonly"] = True + + def clean_email(self): + email = self.cleaned_data.get("email") + # Skip uniqueness validation if it's an update action (instance exists) + if self.instance and self.instance.email == email: + return email + if models.IAmPrincipal.objects.filter(email=email).exists(): + raise forms.ValidationError(constants.EMAIL_EXISTS) + + return email + + def save(self, commit=True): + instance = super().save(commit=False) + # Check if it's a new object (create action) or an existing one (update action) + if not instance.pk: # pk is None for new objects + instance.username = self.cleaned_data["email"] + instance.set_password(self.cleaned_data["password"]) + + principal_type = self.cleaned_data.get("principal_type") + if principal_type is not None: + # Set is_superuser and is_staff based on principal_type + if principal_type == models.IAmPrincipalType.objects.get(name=PRINCIPAL_TYPE_ADMIN): + instance.is_superuser = True + elif principal_type == models.IAmPrincipalType.objects.get(name=PRINCIPAL_TYPE_SUBADMIN): + instance.is_staff = True + if commit: + instance.save() + return instance + + +class IAmPrincipalProfileForm(forms.ModelForm): + GENDER_CHOICES = ( + ("male", "Male"), + ("female", "Female"), + ("other", "Other"), + ) + first_name = forms.CharField(required=True) + last_name = forms.CharField(required=True) + email = forms.EmailField(required=True) + password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}) + ) + confirm_password = forms.CharField( + widget=forms.PasswordInput(attrs={"autocomplete": "off"}) + ) + # date_of_birth = forms.CharField(widget=forms.DateInput(attrs={'type': 'date'})) + phone_number = forms.CharField( + widget=forms.TextInput(), + ) + # is_staff = forms.BooleanField( + # label="Staff Status", + # label_suffix="", + # initial=True, + # required=False, + # help_text="Check this box to designate that this user will be assigned permissions in the future.", + # ) + # is_superuser = forms.BooleanField( + # label="SuperAdmin Status", + # label_suffix="", + # required=False, + # help_text="Check this box to designates that this user has all permissions without explicitly assigning them.", + # ) + # gender = forms.ChoiceField(choices=GENDER_CHOICES) + + class Meta: + model = models.IAmPrincipal + fields = [ + "principal_type", + "first_name", + "last_name", + "email", + "password", + "confirm_password", + # 'gender', + # 'date_of_birth', + "phone_number", + # 'address_line1', + # 'address_line2', + # 'city', + # 'state', + # 'country', + # 'post_code', + # 'profile_photo', + # "is_staff", + # "is_superuser", + ] + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + super().__init__(*args, **kwargs) + self.fields["principal_type"].queryset = models.IAmPrincipalType.objects.filter( + active=True, deleted=False + ) + # self.fields['principal_source'].queryset = models.IAmPrincipalSource.objects.filter(active=True, deleted=False) + # Check if an instance is provided and customize the form fields accordingly + if instance is not None: + # Exclude the 'password' and 'confirm_password' fields + self.fields.pop("password", None) + self.fields.pop("confirm_password", None) + + # Make the 'email' field read-only + self.fields["email"].widget.attrs["readonly"] = True + + # Modify the 'is_superuser' field to be not required + # self.fields["is_superuser"].required = False + # self.fields["is_staff"].required = False + + # Add or modify the 'is_active' field + self.fields["is_active"] = forms.BooleanField( + label="Active", + initial=instance.is_active, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + required=False, + ) + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get("password") + confirm_password = cleaned_data.get("confirm_password") + + if password and confirm_password and password != confirm_password: + self.add_error("confirm_password", "Password does not match") + return cleaned_data + + def save(self, commit=True): + user = super().save(commit=False) + user.set_password(self.cleaned_data["password"]) + if commit: + user.save() + return user + +class ProfileEditForm(forms.ModelForm): + gender = forms.ChoiceField(choices=(('Male', 'Male'),('Female', 'Female'),('Other', 'Other'))) + profile_photo = forms.ImageField(required=False) + + class Meta: + model = models.IAmPrincipal + fields = [ + "profile_photo", + "first_name", + "last_name", + "date_of_birth", + "gender", + "phone_no" + ] + + +class IAmPrincipalGroupLinkForm(forms.ModelForm): + + class Meta: + model = models.IAmPrincipal + fields = [ + # "principal_type", + "email", + "principal_group", + ] + + # principal_type = forms.ModelChoiceField( + # label="Principal Type", + # queryset=models.IAmPrincipalType.objects.filter(active=True, deleted=False), + # widget=forms.widgets.TextInput(attrs={"readonly": True}), + # ) + principal_group = forms.ModelMultipleChoiceField( + label="Groups", + queryset=models.IAmPrincipalGroup.objects.filter(active=True, deleted=False), + required=False, + widget=forms.widgets.SelectMultiple( + attrs={"class": "form_select js-example-basic-multiple"} + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Make the 'email' field read-only + # self.fields['principal_type'].widget.attrs['disabled'] = True + self.fields['email'].widget.attrs['readonly'] = True + + +class IAmPrincipalTypeForm(forms.ModelForm): + class Meta: + model = models.IAmPrincipalType + fields = ["name", "active"] + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + super().__init__(*args, **kwargs) + + if instance is None: + self.fields.pop("active") + + +class IAmPrincipalGroupRoleLinkForm(forms.ModelForm): + class Meta: + model = models.IAmPrincipalGroup + fields = ["name", "role", "active"] + + role = forms.ModelMultipleChoiceField( + queryset=models.IAmRole.objects.filter(active=True, deleted=False), + required=False, + widget=forms.widgets.SelectMultiple( + attrs={"class": "form-select js-example-basic-multiple"} + ), + ) + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + # data = kwargs.get('data') + super().__init__(*args, **kwargs) + + if instance is None: + # This is an add operation, exclude the 'active' field + self.fields.pop("active") + + +class IAmPrincipalRoleAppResourceActionLinkForm(forms.ModelForm): + class Meta: + model = models.IAmRole + fields = ["name", "active", "app_resource_action"] + required = {"app_resource_action": False} + + app_resource_action = forms.ModelMultipleChoiceField( + queryset=models.IAmAppResourceActionLink.objects.all(), + widget=forms.CheckboxSelectMultiple, + required=False, + ) + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + super().__init__(*args, **kwargs) + + if instance is None: + self.fields.pop("active") diff --git a/module_iam/resource_action.py b/module_iam/iam_constant.py similarity index 59% rename from module_iam/resource_action.py rename to module_iam/iam_constant.py index ab82c87..3b88562 100644 --- a/module_iam/resource_action.py +++ b/module_iam/iam_constant.py @@ -1,25 +1,34 @@ - +# principal type constant PRINCIPAL_TYPE_USER = "user" PRINCIPAL_TYPE_ADMIN = "admin" +PRINCIPAL_TYPE_SUBADMIN = "subadmin" +# principal source constant +PRINCIPAL_SOURCE_APP = "app" +PRINCIPAL_SOURCE_WEB = "web" +PRINCIPAL_SOURCE_GOOGLE = "google" +PRINCIPAL_SOURCE_APPLE = "apple" + +# app action constant ACTION_CREATE = "create" ACTION_READ = "read" ACTION_UPDATE = "update" ACTION_DELETE = "delete" + RESOURCE_MANAGE_DASHBOARD = "manage_dashboard" RESOURCE_MANAGE_IAM = "manage_iam" -RESOURCE_MANAGE_CUSTOMER = "manage_customer" -RESOURCE_MANAGE_WALLET = "manage_wallet" -RESOURCE_MANAGE_PAYMENT = "manage_payment" -RESOURCE_MANAGE_GAMES = "manage_games" +RESOURCE_MANAGE_USER = "manage_user" + +RESOURCE_MANAGE_SUPPORT = "manage_support" RESOURCE_MANAGE_CONTACT_US = "manage_contact_us" -RESOURCE_MANAGE_TICKET = "manage_ticket" -RESOURCE_MANAGE_CMS = "manage_cms" -RESOURCE_MANAGE_REPORTS = "manage_reports" -RESOURCE_MANAGE_COUPON = "manage_coupon" RESOURCE_MANAGE_FEEDBACK = "manage_feedback" -RESOURCE_MANAGE_STOCK = "manage_stock" +RESOURCE_MANAGE_NOTIFICATION = "manage_notification" + +RESOURCE_MANAGE_CMS = "manage_cms" +RESOURCE_MANAGE_FAQS = "manage_faqs" +RESOURCE_MANAGE_T_C = "manage_tc" +RESOURCE_MANAGE_PRIVACYPOLICY = "manage_privacypolicy" # These constants are used solely for managing the active and inactive state of pages diff --git a/module_iam/iam_context_processors.py b/module_iam/iam_context_processors.py new file mode 100644 index 0000000..b531f9e --- /dev/null +++ b/module_iam/iam_context_processors.py @@ -0,0 +1,60 @@ +from .iam_constant import ( + PRINCIPAL_TYPE_USER, + PRINCIPAL_TYPE_ADMIN, + PRINCIPAL_TYPE_SUBADMIN, + PRINCIPAL_SOURCE_APP, + PRINCIPAL_SOURCE_WEB, + PRINCIPAL_SOURCE_GOOGLE, + PRINCIPAL_SOURCE_APPLE, + ACTION_CREATE, + ACTION_READ, + ACTION_UPDATE, + ACTION_DELETE, + RESOURCE_MANAGE_DASHBOARD, + RESOURCE_MANAGE_IAM, + RESOURCE_MANAGE_USER, + RESOURCE_MANAGE_SUPPORT, + RESOURCE_MANAGE_CONTACT_US, + RESOURCE_MANAGE_FEEDBACK, + RESOURCE_MANAGE_NOTIFICATION, + RESOURCE_MANAGE_CMS, + RESOURCE_MANAGE_FAQS, + RESOURCE_MANAGE_T_C, + RESOURCE_MANAGE_PRIVACYPOLICY, + RESOURCE_IAM_PRINCIPAL, + RESOURCE_IAM_PRINCIPAL_GROUP, + RESOURCE_IAM_GROUP, + RESOURCE_IAM_ROLE, +) + +def iam_constants_context(request): + return { + 'iam_constants_context': { + 'PRINCIPAL_TYPE_USER': PRINCIPAL_TYPE_USER, + 'PRINCIPAL_TYPE_ADMIN': PRINCIPAL_TYPE_ADMIN, + 'PRINCIPAL_TYPE_SUBADMIN': PRINCIPAL_TYPE_SUBADMIN, + 'PRINCIPAL_SOURCE_APP': PRINCIPAL_SOURCE_APP, + 'PRINCIPAL_SOURCE_WEB': PRINCIPAL_SOURCE_WEB, + 'PRINCIPAL_SOURCE_GOOGLE': PRINCIPAL_SOURCE_GOOGLE, + 'PRINCIPAL_SOURCE_APPLE': PRINCIPAL_SOURCE_APPLE, + 'ACTION_CREATE': ACTION_CREATE, + 'ACTION_READ': ACTION_READ, + 'ACTION_UPDATE': ACTION_UPDATE, + 'ACTION_DELETE': ACTION_DELETE, + 'RESOURCE_MANAGE_DASHBOARD': RESOURCE_MANAGE_DASHBOARD, + 'RESOURCE_MANAGE_IAM': RESOURCE_MANAGE_IAM, + 'RESOURCE_MANAGE_USER': RESOURCE_MANAGE_USER, + 'RESOURCE_MANAGE_SUPPORT': RESOURCE_MANAGE_SUPPORT, + 'RESOURCE_MANAGE_CONTACT_US': RESOURCE_MANAGE_CONTACT_US, + 'RESOURCE_MANAGE_FEEDBACK': RESOURCE_MANAGE_FEEDBACK, + 'RESOURCE_MANAGE_NOTIFICATION': RESOURCE_MANAGE_NOTIFICATION, + 'RESOURCE_MANAGE_CMS': RESOURCE_MANAGE_CMS, + 'RESOURCE_MANAGE_FAQS': RESOURCE_MANAGE_FAQS, + 'RESOURCE_MANAGE_T_C': RESOURCE_MANAGE_T_C, + 'RESOURCE_MANAGE_PRIVACYPOLICY': RESOURCE_MANAGE_PRIVACYPOLICY, + 'RESOURCE_IAM_PRINCIPAL': RESOURCE_IAM_PRINCIPAL, + 'RESOURCE_IAM_PRINCIPAL_GROUP': RESOURCE_IAM_PRINCIPAL_GROUP, + 'RESOURCE_IAM_GROUP': RESOURCE_IAM_GROUP, + 'RESOURCE_IAM_ROLE': RESOURCE_IAM_ROLE, + } + } \ No newline at end of file diff --git a/module_iam/iam_fixture_script.py b/module_iam/iam_fixture_script.py new file mode 100644 index 0000000..b0eb7b4 --- /dev/null +++ b/module_iam/iam_fixture_script.py @@ -0,0 +1,169 @@ +from datetime import datetime + +from .iam_constant import ( + PRINCIPAL_TYPE_USER, + PRINCIPAL_TYPE_ADMIN, + PRINCIPAL_TYPE_SUBADMIN, + PRINCIPAL_SOURCE_APP, + PRINCIPAL_SOURCE_WEB, + PRINCIPAL_SOURCE_GOOGLE, + PRINCIPAL_SOURCE_APPLE, + ACTION_CREATE, + ACTION_READ, + ACTION_UPDATE, + ACTION_DELETE, + RESOURCE_MANAGE_DASHBOARD, + RESOURCE_MANAGE_IAM, + RESOURCE_MANAGE_USER, + RESOURCE_MANAGE_CONTACT_US, + RESOURCE_MANAGE_FEEDBACK, + RESOURCE_MANAGE_FAQS, + RESOURCE_MANAGE_T_C, + RESOURCE_MANAGE_CMS, + RESOURCE_MANAGE_PRIVACYPOLICY, + RESOURCE_MANAGE_SUPPORT +) + +class IAMPrincipalType: + ADMIN = PRINCIPAL_TYPE_ADMIN + SUBADMIN = PRINCIPAL_TYPE_SUBADMIN + USER = PRINCIPAL_TYPE_USER + + categories = [ + ADMIN, + SUBADMIN, + USER, + ] + + @staticmethod + def create_iam_principal_type_fixture_data(): + iam_category_fixture_data = [] + created_on = datetime.now().isoformat() + modified_on = datetime.now().isoformat() + for idx, category in enumerate(IAMPrincipalType.categories, start=1): + iam_category_fixture_data.append( + { + "model": "module_iam.iamprincipaltype", + "pk": idx, + "fields": { + "name": category, + "label": category, + "slug": category, + "created_on": created_on, + "modified_on": modified_on, + }, + } + ) + return iam_category_fixture_data + +class IAMPrincipalSource: + source = [ + PRINCIPAL_SOURCE_APP, + PRINCIPAL_SOURCE_WEB, + PRINCIPAL_SOURCE_GOOGLE, + PRINCIPAL_SOURCE_APPLE + ] + + @staticmethod + def create_iam_principal_source_fixture_data(): + iam_principal_source_fixture_data = [] + created_on = datetime.now().isoformat() + modified_on = datetime.now().isoformat() + + for idx, principal_source in enumerate(IAMPrincipalSource.source, start=1,): + iam_principal_source_fixture_data.append( + { + "model": "module_iam.iamprincipalsource", + "pk": idx, + "fields": { + "name": principal_source, + "label": principal_source, + "slug": principal_source, + "created_on": created_on, + "modified_on": modified_on, + }, + } + ) + + return iam_principal_source_fixture_data + +class IAMActions: + CREATE = ACTION_CREATE + READ = ACTION_READ + UPDATE = ACTION_UPDATE + DELETE = ACTION_DELETE + + actions = [ + CREATE, + READ, + UPDATE, + DELETE, + ] + + @staticmethod + def create_iam_action_fixture_data(): + iam_action_fixture_data = [] + created_on = datetime.now().isoformat() + modified_on = datetime.now().isoformat() + for idx, action in enumerate(IAMActions.actions, start=1): + iam_action_fixture_data.append( + { + "model": "module_iam.iamappaction", + "pk": idx, + "fields": { + "name": action, + "label": action, + "slug": action, + "created_on": created_on, + "modified_on": modified_on, + }, + } + ) + return iam_action_fixture_data + +class IAMResources: + DASHBOARD = RESOURCE_MANAGE_DASHBOARD + IAM = RESOURCE_MANAGE_IAM + USER = RESOURCE_MANAGE_USER + SUPPORT = RESOURCE_MANAGE_SUPPORT + CONTACT_US = RESOURCE_MANAGE_CONTACT_US + FEEDBACK = RESOURCE_MANAGE_FEEDBACK + CMS = RESOURCE_MANAGE_CMS + FAQS = RESOURCE_MANAGE_FAQS + T_C = RESOURCE_MANAGE_T_C + PRIVACYPOLICY = RESOURCE_MANAGE_PRIVACYPOLICY + + resources = [ + DASHBOARD, + IAM, + USER, + SUPPORT, + CONTACT_US, + FEEDBACK, + CMS, + FAQS, + T_C, + PRIVACYPOLICY, + ] + + @staticmethod + def create_iam_resource_fixture_data(): + iam_resource_fixture_data = [] + created_on = datetime.now().isoformat() + modified_on = datetime.now().isoformat() + for idx, resource in enumerate(IAMResources.resources, start=1): + iam_resource_fixture_data.append( + { + "model": "module_iam.iamappresource", + "pk": idx, + "fields": { + "name": resource, + "label": resource, + "slug": resource, + "created_on": created_on, + "modified_on": modified_on, + "action": [1, 2, 3, 4], + }, + } + ) + return iam_resource_fixture_data diff --git a/module_iam/management/commands/load_iam_fixture.py b/module_iam/management/commands/load_iam_fixture.py new file mode 100644 index 0000000..767a4a4 --- /dev/null +++ b/module_iam/management/commands/load_iam_fixture.py @@ -0,0 +1,122 @@ +import os +import json +import subprocess +from datetime import datetime +from tqdm import tqdm +from django.core.management.base import BaseCommand +from module_iam.iam_fixture_script import IAMPrincipalType, IAMActions, IAMResources, IAMPrincipalSource + + +class Command(BaseCommand): + help = "Load IAM fixtures data" + + def handle(self, *args, **options): + app_name = "module_iam" + try: + self.stdout.write(self.style.SUCCESS("IAM fixtures data loading started...")) + + # Ensure the fixture directory exists + fixture_directory = os.path.join(app_name, "fixtures") + if not os.path.exists(fixture_directory): + os.makedirs(fixture_directory) + + # Generate IAM category fixture data + principal_type_fixture_data = IAMPrincipalType.create_iam_principal_type_fixture_data() + + # Specify the app name and fixture filename for category fixtures + categories_fixture_filename = os.path.join(fixture_directory, "iam_principal_type_fixture.json") + + principal_type_fixture_data_list = [] + with tqdm(total=len(principal_type_fixture_data), desc="Loading IAM principal type fixture") as pbar: + for item in principal_type_fixture_data: + principal_type_fixture_data_list.append(item) + pbar.update(1) + + # Dump category fixture data as JSON + with open(categories_fixture_filename, "w") as fixture_file: + json.dump(principal_type_fixture_data, fixture_file, indent=4) + + self.stdout.write( + self.style.SUCCESS(f"IAM category fixture data has been loaded successfully. Fixture file location: {categories_fixture_filename}") + ) + + principal_source_fixture_data = IAMPrincipalSource.create_iam_principal_source_fixture_data() + + # Specify the app name and fixture filename for source fixtures + source_fixture_filename = os.path.join(fixture_directory, "iam_principal_source_fixture.json") + + principal_source_fixture_data_list = [] + with tqdm(total=len(principal_source_fixture_data), desc="Loading IAM principal source fixture") as pbar: + for item in principal_source_fixture_data: + principal_source_fixture_data_list.append(item) + pbar.update(1) + + # Dump category fixture data as JSON + with open(source_fixture_filename, "w") as fixture_file: + json.dump(principal_source_fixture_data, fixture_file, indent=4) + + self.stdout.write( + self.style.SUCCESS(f"IAM category fixture data has been loaded successfully. Fixture file location: {categories_fixture_filename}") + ) + + # Generate IAM action fixture data + action_fixture_data = IAMActions.create_iam_action_fixture_data() + + # Specify the fixture filename for action fixtures + action_fixture_filename = os.path.join(fixture_directory, "iam_actions_fixture.json") + + action_fixture_data_list = [] + with tqdm(total=len(action_fixture_data), desc="Loading IAM action fixture") as pbar: + for item in action_fixture_data: + action_fixture_data_list.append(item) + pbar.update(1) + + # Dump action fixture data as JSON + with open(action_fixture_filename, "w") as fixture_file: + json.dump(action_fixture_data, fixture_file, indent=4) + + self.stdout.write( + self.style.SUCCESS(f"IAM action fixture data has been loaded successfully. Fixture file location: {action_fixture_filename}") + ) + + # Generate IAM resource fixture data + resource_fixture_data = IAMResources.create_iam_resource_fixture_data() + + # Specify the fixture filename for resource fixtures + resource_fixture_filename = os.path.join(fixture_directory, "iam_resources_fixture.json") + + resource_fixture_data_list = [] + with tqdm(total=len(resource_fixture_data), desc="Loading IAM resource fixture") as pbar: + for item in resource_fixture_data: + resource_fixture_data_list.append(item) + pbar.update(1) + + # Dump resource fixture data as JSON + with open(resource_fixture_filename, "w") as fixture_file: + json.dump(resource_fixture_data, fixture_file, indent=4) + + self.stdout.write( + self.style.SUCCESS(f"IAM resource fixture data has been loaded successfully. Fixture file location: {resource_fixture_filename}") + ) + + # Run the loaddata command to load the created fixtures + loaddata_command_categories = f"python manage.py loaddata {categories_fixture_filename}" + subprocess.run(loaddata_command_categories, shell=True) + + loaddata_command_categories = f"python manage.py loaddata {source_fixture_filename}" + subprocess.run(loaddata_command_categories, shell=True) + + loaddata_command_actions = f"python manage.py loaddata {action_fixture_filename}" + subprocess.run(loaddata_command_actions, shell=True) + + loaddata_command_resources = f"python manage.py loaddata {resource_fixture_filename}" + subprocess.run(loaddata_command_resources, shell=True) + + self.stdout.write( + self.style.SUCCESS("IAM fixtures data loading completed successfully.") + ) + except Exception as e: + # Handle exceptions here + self.stderr.write( + self.style.ERROR(f"IAM fixtures data loading failed: {str(e)}") + ) \ No newline at end of file diff --git a/module_iam/migrations/0004_appversion.py b/module_iam/migrations/0004_appversion.py new file mode 100644 index 0000000..927cc56 --- /dev/null +++ b/module_iam/migrations/0004_appversion.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.2 on 2024-03-11 07:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_iam', '0003_alter_iamprincipal_gender'), + ] + + operations = [ + migrations.CreateModel( + name='AppVersion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(max_length=10)), + ('force_upgrade', models.BooleanField(default=False, help_text='Indicates whether a force upgrade is needed for this app version.')), + ('recommend_upgrade', models.BooleanField(default=False, help_text='Indicates whether a recommend upgrade is needed for this app version.')), + ], + ), + ] diff --git a/module_iam/migrations/0005_alter_appversion_table.py b/module_iam/migrations/0005_alter_appversion_table.py new file mode 100644 index 0000000..8c1cb80 --- /dev/null +++ b/module_iam/migrations/0005_alter_appversion_table.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-03-11 07:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_iam', '0004_appversion'), + ] + + operations = [ + migrations.AlterModelTable( + name='appversion', + table='app_version', + ), + ] diff --git a/module_iam/migrations/0006_alter_appversion_version.py b/module_iam/migrations/0006_alter_appversion_version.py new file mode 100644 index 0000000..408263b --- /dev/null +++ b/module_iam/migrations/0006_alter_appversion_version.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-03-11 08:18 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_iam', '0005_alter_appversion_table'), + ] + + operations = [ + migrations.AlterField( + model_name='appversion', + name='version', + field=models.CharField(max_length=10, validators=[django.core.validators.RegexValidator('^\\d+\\.\\d+\\.\\d+$')]), + ), + ] diff --git a/module_iam/models.py b/module_iam/models.py index dd26794..e26173c 100644 --- a/module_iam/models.py +++ b/module_iam/models.py @@ -2,17 +2,28 @@ from collections.abc import Iterable import datetime import random import string + # from manage_wallets.models import Wallet, Transaction, TransactionStatus, TransactionType from django.conf import settings +from django.core.cache import cache from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractUser, BaseUserManager from django.db import models from django.utils import timezone from django.utils.text import slugify + # from phonenumber_field.modelfields import PhoneNumberField from module_project.utils import RandomGenerator -from .resource_action import PRINCIPAL_TYPE_USER, PRINCIPAL_TYPE_ADMIN +from .iam_constant import ( + PRINCIPAL_TYPE_USER, + PRINCIPAL_TYPE_ADMIN, + PRINCIPAL_TYPE_SUBADMIN, + PRINCIPAL_SOURCE_APP, + PRINCIPAL_SOURCE_APPLE, + PRINCIPAL_SOURCE_GOOGLE, + PRINCIPAL_SOURCE_WEB, +) # from .utils import UserContext from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator @@ -102,14 +113,60 @@ class IAmPrincipalType(MasterModel): db_table = "iam_principal_type" @classmethod - def get_principal_type(cls, type): - return cls.objects.filter(name=type).first() + def get_principal_type(cls, name): + cache_key = f"principal_{name}" + principal = cache.get(cache_key) + + if not principal: + principal = cls.objects.filter(name=name).first() + cache.set(cache_key, principal, timeout=60 * 15) # Cache for 15 minutes + + return principal + + @classmethod + def get_principal_user(cls): + return cls.get_principal_type(PRINCIPAL_TYPE_USER) + + @classmethod + def get_principal_admin(cls): + return cls.get_principal_type(PRINCIPAL_TYPE_ADMIN) + + @classmethod + def get_principal_subadmin(cls): + return cls.get_principal_type(PRINCIPAL_TYPE_SUBADMIN) class IAmPrincipalSource(MasterModel): class Meta: db_table = "iam_principal_source" + @classmethod + def get_principal_source(cls, name): + cache_key = f"principal_{name}" + principal = cache.get(cache_key) + + if not principal: + principal = cls.objects.filter(name=name).first() + cache.set(cache_key, principal, timeout=60 * 15) # Cache for 15 minutes + + return principal + + @classmethod + def get_principal_web(cls): + return cls.get_principal_source(PRINCIPAL_SOURCE_WEB) + + @classmethod + def get_principal_app(cls): + return cls.get_principal_source(PRINCIPAL_SOURCE_APP) + + @classmethod + def get_principal_google(cls): + return cls.get_principal_source(PRINCIPAL_SOURCE_GOOGLE) + + @classmethod + def get_principal_apple(cls): + return cls.get_principal_source(PRINCIPAL_SOURCE_APPLE) + class IAmAppAction(MasterModel): class Meta: @@ -239,7 +296,7 @@ class IAmPrincipalManager(BaseUserManager): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("phone_no", "+919978895465") - extra_fields.setdefault("gender", "M") + extra_fields.setdefault("gender", "Male") extra_fields.setdefault("date_of_birth", timezone.now()) extra_fields.setdefault("created_by", None) extra_fields.setdefault("created_on", timezone.now()) @@ -298,7 +355,12 @@ class IAmPrincipal(AbstractUser): related_name="principal_groups", ) register_complete = models.BooleanField(default=False) - player_id = models.CharField(max_length=255, null=True, blank=True, help_text="OneSignal player id for push notification") + player_id = models.CharField( + max_length=255, + null=True, + blank=True, + help_text="OneSignal player id for push notification", + ) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] @@ -367,3 +429,15 @@ class IAmPrincipalBiometric(BaseModel): def __str__(self): return f"{self.principal.first_name}:{self.biometric_type}" + + +class AppVersion(models.Model): + version = models.CharField(max_length=10, validators=[RegexValidator(r'^\d+\.\d+\.\d+$')]) + force_upgrade = models.BooleanField(default=False, help_text='Indicates whether a force upgrade is needed for this app version.') + recommend_upgrade = models.BooleanField(default=False, help_text='Indicates whether a recommend upgrade is needed for this app version.') + + class Meta: + db_table = "app_version" + + def __str__(self): + return self.version \ No newline at end of file diff --git a/module_iam/urls.py b/module_iam/urls.py index 1a47c76..1aae51e 100644 --- a/module_iam/urls.py +++ b/module_iam/urls.py @@ -4,5 +4,32 @@ from . import views app_name = "module_iam" urlpatterns = [ - path('dashboard/', views.DashboardView.as_view(), name="dashboard") + path('dashboard/', views.DashboardView.as_view(), name="dashboard"), + + + # path('principal/', views.PrincipalListView.as_view(), name="principal_list"), + # path('principal/add/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_add"), + # path('principal/edit/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_edit"), + # path('principal/delete/', views.PrincipalDeleteView.as_view(), name="principal_delete"), + + path('principal/group/link/', views.PrincipalGroupLinkView.as_view(), name="principal_group_link"), + path('principal/group/link/', views.PrincipalGroupLinkAdminListJsonView.as_view(), name="principal_group_link_list"), + # path('principal/group/link/edit//', views.PrincipalGroupLinkEditView.as_view(), name="principal_group_link_edit"), + + + path('principal/group/', views.PrincipalGroupView.as_view(), name="principal_group"), + path('principal/group/list', views.PrincipalGroupListJsonView.as_view(), name="principal_group_list"), + path('principal/group/add/', views.PrincipalGroupCreateOrUpdateView.as_view(), name="principal_group_add"), + path('principal/group/edit//', views.PrincipalGroupCreateOrUpdateView.as_view(), name="principal_group_edit"), + path('principal/group/action//', views.PrincipalGroupActionView.as_view(), name="principal_group_action"), + + path('principal/role/', views.AppRoleView.as_view(), name="role"), + path('principal/role/list/', views.AppRoleListJsonView.as_view(), name="role_list"), + path('principal/role/add/', views.AppRoleCreateOrUpdateView.as_view(), name="role_add"), + path('principal/role/edit//', views.AppRoleCreateOrUpdateView.as_view(), name="role_edit"), + path('principal/role/action/', views.AppRoleActionView.as_view(), name="role_action"), + + path("profile/", views.PrincipalProfileView.as_view(), name="profile_details"), + path("profile/edit/", views.PrincipalProfileEditView.as_view(), name="profile_details_edit") + ] diff --git a/module_iam/views.py b/module_iam/views.py index 2fe17ca..4641cf1 100644 --- a/module_iam/views.py +++ b/module_iam/views.py @@ -1,7 +1,374 @@ -from django.shortcuts import render +from typing import Any +from django.db.models.base import Model as Model +from django.db.models.query import QuerySet from django.views import generic +import logging + +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.http import JsonResponse +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse_lazy +from module_iam import iam_constant +from module_project.mixins import DatatablesMixin +from django_datatables_view.base_datatable_view import BaseDatatableView +from module_project.mixins import ActionMixin +from .forms import ( + CustomAuthenticationForm, + IAmPrincipalForm, + IAmPrincipalGroupRoleLinkForm, + IAmPrincipalRoleAppResourceActionLinkForm, + IAmPrincipalGroupLinkForm, + ProfileEditForm +) +from .models import ( + IAmPrincipal, + IAmPrincipalType, + IAmAppResourceActionLink, + IAmPrincipalGroup, + IAmRole, +) + +from module_project import constants + +logger = logging.getLogger(__name__) # Create your views here. class DashboardView(generic.TemplateView): - template_name = "base_structure/layout/dashboard.html" \ No newline at end of file + page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD + template_name = "base_structure/layout/dashboard.html" + + def get_user_count(self): + obj = IAmPrincipal.objects.all() + # Count active users + active_user_count = obj.filter(is_active=True).count() + # Count total users + total_user_count = obj.count() + return active_user_count, total_user_count + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + active_user_count, total_user_count = self.get_user_count() + context['active_user_count'] = active_user_count + context['total_user_count'] = total_user_count + context['page_name'] = self.page_name + return context + +class PrincipalGroupLinkView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP + model = IAmPrincipal + template_name = "module_iam/iam_principal_group_link.html" + + def get_context_data(self, **kwargs) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + +class PrincipalGroupLinkAdminListJsonView(BaseDatatableView): + model = IAmPrincipal + columns = ["id", "first_name", "email", "principal_type__name", "is_active"], + order_columns = ["id", "first_name", "email"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', False) + return self.model.objects.filter(deleted=deleted_flag).exclude(principal_type__name=iam_constant.PRINCIPAL_TYPE_USER) + + +class PrincipalGroupView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_IAM_GROUP + model = IAmPrincipalGroup + template_name = "module_iam/iam_group.html" + + def get_context_data(self, **kwargs) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + def filter_queryset(self, qs): + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(name__icontains=search_value) + ) + return qs + + +class PrincipalGroupListJsonView(BaseDatatableView): + model = IAmPrincipalGroup + columns = ["id", "name", "active"] + order_columns = ["id", "name", "active"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', False) + return self.model.objects.filter(deleted=deleted_flag) + + def filter_queryset(self, qs): + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(name__icontains=search_value) + ) + return qs + + def generate_role_data(self, queryset): + roles_data = [] + for obj in queryset: + roles = [{'name': role.name} for role in obj.role.all()] + print(f"role data is this {roles}") + roles_data.append({ + 'id': obj.id, + 'name': obj.name, + 'active': str(obj.active), + 'roles': roles + }) + return roles_data + + def get_context_data(self, *args, **kwargs): + roles = self.filter_queryset(self.get_initial_queryset()) + role_data = self.generate_role_data(roles) + context = super().get_context_data(*args, **kwargs) + context['recordsTotal'] = len(role_data) + context['recordsFiltered'] = len(role_data) + context['data'] = role_data + context['result'] = 'ok' + return context + +class PrincipalGroupCreateOrUpdateView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_IAM_GROUP + page_title = "Principal Group" + model = IAmPrincipalGroup + template_name = "module_iam/iam_group_add.html" + form_class = IAmPrincipalGroupRoleLinkForm + success_url = reverse_lazy("module_iam:principal_group") + error_message = "An error occurred while saving the data." + + def get_success_message(self): + self.success_message = constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + return self.success_message + + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +class PrincipalGroupActionView(ActionMixin): + model = IAmPrincipalGroup + + +class AppRoleView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_IAM_ROLE + model = IAmRole + template_name = "module_iam/iam_role.html" + + def get_context_data(self, **kwargs) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + +class AppRoleListJsonView(BaseDatatableView): + model = IAmRole + columns = ["id", "name", "active", "resources"] + order_columns = ["id", "name"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', False) + return ( + super(AppRoleListJsonView, self) + .get_initial_queryset() + .prefetch_related( + "app_resource_action", + "app_resource_action__app_resource", + "app_resource_action__app_action", + ) + .filter(deleted=deleted_flag) + ) + + def filter_queryset(self, qs): + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(name__icontains=search_value) + | Q(app_resource_action__app_resource__name__icontains=search_value) + | Q(app_resource_action__app_action__name__icontains=search_value) + ) + return qs + + def generate_resource_data(self, roles): + role_data = [] + for role in roles: + role_info = { + "id": role.id, + "name": role.name, + "active": str(role.active), + "resources": {}, + } + + for link in role.app_resource_action.all(): + resource = link.app_resource.name + action = link.app_action.name + if resource in role_info["resources"]: + role_info["resources"][resource].append(action) + else: + role_info["resources"][resource] = [action] + role_data.append(role_info) + return role_data + + def get_context_data(self, *args, **kwargs): + roles = self.filter_queryset(self.get_initial_queryset()) + role_data = self.generate_resource_data(roles) + context = super().get_context_data(*args, **kwargs) + context['recordsTotal'] = len(role_data) + context['recordsFiltered'] = len(role_data) + context['data'] = role_data + context['result'] = 'ok' + return context + + +class AppRoleCreateOrUpdateView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_IAM_ROLE + model = IAmRole + template_name = "module_iam/iam_role_add.html" + form_class = IAmPrincipalRoleAppResourceActionLinkForm + success_url = reverse_lazy("module_iam:role") + success_message = "Saved Successfully" + error_message = "An error occurred while saving the data." + + def get_success_message(self): + self.success_message = ( + f"Record {'Created' if not self.object else 'Updated'} Successfully" + ) + return self.success_message + + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + "app_resource_action": IAmAppResourceActionLink.objects.generate_app_resource_action_data(), + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + try: + self.object = self.get_object() + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + except Exception as e: + messages.error(request, str(e)) + return redirect(self.success_url) + + def post(self, request, *args, **kwargs): + try: + self.object = self.get_object() + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + except Exception as e: + messages.error(self.request, str(e)) + return redirect(self.success_url) + + +class AppRoleActionView(LoginRequiredMixin, ActionMixin): + model = IAmRole + + +class PrincipalProfileView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD + model = IAmPrincipal + template_name = "module_iam/profile_details.html" + + def get_object(self, queryset=None): + user = self.request.user.id + return get_object_or_404(self.model.objects.select_related("principal_type", "principal_source"), pk=user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + context["data_obj"] = self.get_object() + return context + +class PrincipalProfileEditView(generic.View): + page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD + model = IAmPrincipal + template_name = "module_iam/profile_details_edit.html" + form_class = ProfileEditForm + success_url = reverse_lazy("module_iam:profile_details") + success_message = "Saved Successfully" + error_message = "An error occurred while saving the data." + + def get_success_message(self): + self.success_message = ( + f"Record {'Created' if not self.object else 'Updated'} Successfully" + ) + return self.success_message + + def get_object(self): + return self.request.user + + def get_context_data(self, **kwargs): + context = { + # "page_name": self.page_name, + "operation": "Edit", + "page_name": self.page_name + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + # try: + self.object = self.get_object() + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.form_class(request.POST, request.FILES, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) \ No newline at end of file diff --git a/module_notification/forms.py b/module_notification/forms.py new file mode 100644 index 0000000..6f54868 --- /dev/null +++ b/module_notification/forms.py @@ -0,0 +1,7 @@ +from django import forms +from .models import PushNotification + +class PushNotificationForm(forms.ModelForm): + class Meta: + model = PushNotification + fields = ('title', 'banner_image', 'message') \ No newline at end of file diff --git a/module_notification/migrations/0001_initial.py b/module_notification/migrations/0001_initial.py new file mode 100644 index 0000000..ebae061 --- /dev/null +++ b/module_notification/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0.2 on 2024-03-05 18:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PushNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('active', models.BooleanField(default=True)), + ('deleted', models.BooleanField(default=False)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('modified_on', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=255)), + ('banner_image', models.ImageField(blank=True, null=True, upload_to='push_notification_images/')), + ('message', models.TextField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'push_notification', + }, + ), + ] diff --git a/module_notification/models.py b/module_notification/models.py index 71a8362..792ec45 100644 --- a/module_notification/models.py +++ b/module_notification/models.py @@ -1,3 +1,15 @@ from django.db import models +from module_iam.models import BaseModel # Create your models here. +class PushNotification(BaseModel): + title = models.CharField(max_length=255) + banner_image = models.ImageField(upload_to='push_notification_images/', blank=True, null=True) + message = models.TextField() + timestamp = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "push_notification" + + def __str__(self): + return self.title diff --git a/module_notification/urls.py b/module_notification/urls.py new file mode 100644 index 0000000..98f6add --- /dev/null +++ b/module_notification/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from . import views +from django.views.generic import TemplateView + +app_name = "module_notification" + +urlpatterns = [ + + path("notification/", views.NotificationView.as_view(), name="notification"), + path("notification/add/", views.NotificationCreateOrUpdateView.as_view(), name="notification_add"), + path("notification/edit/", views.NotificationCreateOrUpdateView.as_view(), name="notification_edit"), + path("notification/list/", views.NotificationListJsonView.as_view(), name="notification_list"), + path("notification/action/", views.NotificationActionView.as_view(), name="notification_action"), + path("notification/send/", views.NotificationSendView.as_view(), name="notification_send"), + +] diff --git a/module_notification/views.py b/module_notification/views.py index 91ea44a..73cacf5 100644 --- a/module_notification/views.py +++ b/module_notification/views.py @@ -1,3 +1,164 @@ -from django.shortcuts import render +import logging + +from datetime import datetime, timedelta +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.http import HttpRequest +from django.http.response import HttpResponse as HttpResponse +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse_lazy +from django.views import generic +from module_iam.models import IAmPrincipal +from module_project.service import OneSignalService +from .models import PushNotification +from .forms import PushNotificationForm +from module_iam import iam_constant +from django_datatables_view.base_datatable_view import BaseDatatableView +from module_project.mixins import ActionMixin + +from module_project import constants +from module_project.utils import JsonResponseUtil + # Create your views here. +class NotificationView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_NOTIFICATION + resource = iam_constant.RESOURCE_MANAGE_NOTIFICATION + template_name = "module_notification/notification.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + +class NotificationListJsonView(BaseDatatableView): + model = PushNotification + columns = ["id", "title", "message", "active", "timestamp"] + order_columns = ["id", "title", "message", "active", "timestamp"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', None) + + return self.model.objects.filter(deleted=deleted_flag) + + def filter_queryset(self, qs): + # Implement your custom filtering logic here + print(f"request is {self.request.GET}") + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(question__icontains=search_value) + | Q(answer__icontains=search_value) + ) + + return qs + + +class NotificationCreateOrUpdateView(LoginRequiredMixin, generic.View): + # Set the page_name and resource + page_name = iam_constant.RESOURCE_MANAGE_NOTIFICATION + resource = iam_constant.RESOURCE_MANAGE_NOTIFICATION + + # Initialize the action as ACTION_CREATE (can change based on logic) + action = iam_constant.ACTION_CREATE # Default action + + template_name = "module_notification/add_notification.html" + model = PushNotification + form_class = PushNotificationForm + success_url = reverse_lazy("module_notification:notification") + error_message = "An error occurred while saving the data." + + # Determine the success message dynamically based on whether it's an update or create + def get_success_message(self): + self.success_message = ( + constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED + ) + return self.success_message + + # Get the object (if exists) based on URL parameter 'pk + def get_object(self): + pk = self.kwargs.get("pk") + return get_object_or_404(self.model, pk=pk) if pk else None + + # Add page_name and operation to the context + def get_context_data(self, **kwargs): + context = { + "page_name": self.page_name, + "operation": "Add" if not self.object else "Edit", + } + context.update(kwargs) # Include any additional context data passed to the view + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = iam_constant.ACTION_UPDATE + + form = self.form_class(instance=self.object) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + def post(self, request, *args, **kwargs): + print("Request data: ", request.POST) + self.object = self.get_object() + + # If an object is found, change action to ACTION_UPDATE + if self.object is not None: + self.action = iam_constant.ACTION_UPDATE + + form = self.form_class(request.POST, instance=self.object) + if not form.is_valid(): + print(form.errors) + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + + form.save() + messages.success(self.request, self.get_success_message()) + return redirect(self.success_url) + + +class NotificationActionView(ActionMixin): + model = PushNotification + + +class NotificationSendView(generic.View): + model = PushNotification + + def post(self, request, *args, **kwargs): + id = request.POST.get("id") + obj = self.model.objects.filter(pk=int(id)).first() + # Get the current date and subtract 15 days + fifteen_days_ago = datetime.now() - timedelta(days=3) + + # Filter the IAmPrincipal objects based on the last_login field being greater than or equal to fifteen_days_ago + player_ids = list(IAmPrincipal.objects.filter(last_login__gte=fifteen_days_ago).values_list('player_id', flat=True)) + + if not obj: + return JsonResponseUtil.error(message="No notification with such ID exists.") + + print(f"data type is ============ {type(player_ids)}") + print(f"player id aare {player_ids}") + try: + notification = OneSignalService() + response = notification.send_notification( + headings=obj.title, + contents=obj.message, + # include_player_ids=["5643e132-5266-4dc2-9131-1b4a81f0cbd0"], # single player id + include_player_ids=player_ids, + ) + print("pussh dtaa ===========", response) + except Exception as e: + print(f"Error is {e}") + error_response = { + "status": 400, + "message": constants.INTERNAL_SERVER_ERROR, + "errors": str(e), + } + return JsonResponseUtil.error(**error_response) + + return JsonResponseUtil.success(message="success") \ No newline at end of file diff --git a/module_project/date_utils.py b/module_project/date_utils.py index 54fbec1..7773f19 100644 --- a/module_project/date_utils.py +++ b/module_project/date_utils.py @@ -16,6 +16,11 @@ def get_current_date(): def get_current_time(): return datetime.now().time() +def get_date_range(days): + end_date = datetime.now() + start_date = end_date - timedelta(days=int(days)) + return start_date, end_date + # Get current date in a specific timezone from pytz import timezone def get_current_date_in_timezone(timezone_str='UTC'): diff --git a/module_project/mixins.py b/module_project/mixins.py index 271cca5..701a482 100644 --- a/module_project/mixins.py +++ b/module_project/mixins.py @@ -1,6 +1,8 @@ from django.db.models import Q from django.http.response import JsonResponse from django.core.paginator import Paginator +from .utils import JsonResponseUtil +from django.views import generic class DatatablesMixin: """ @@ -83,4 +85,34 @@ class DatatablesMixin: "recordsTotal": total_count, "recordsFiltered": filtered_count, "data": data - }) \ No newline at end of file + }) + + +class ActionMixin(generic.View): + model = None + + def post(self, request, *args, **kwargs): + + if self.model is None: + raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.") + + action = request.POST.get('action') # 'archive', 'active', or 'unarchive' + ids = request.POST.getlist('ids[]') # List of IDs to perform action on + active = request.POST.get('active') + print(f"arhive action {action} and id is {ids} and active data is {active}") + if action == 'archive': + # Update 'deleted' field to True for the selected users + self.model.objects.filter(id__in=ids).update(deleted=True, active=False) + message = 'Record archived successfully.' + elif action == 'active': + # Update 'active' field to True for the selected users + self.model.objects.filter(id__in=ids).update(active=active.capitalize()) + message = 'Record updated successfully.' + elif action == 'unarchive': + # Update 'deleted' field to False for the selected users + self.model.objects.filter(id__in=ids).update(deleted=False) + message = 'Record unarchived successfully.' + else: + return JsonResponseUtil.error(message="Invalid Action") + + return JsonResponseUtil.success(message=message) \ No newline at end of file diff --git a/module_project/service.py b/module_project/service.py index 6b7e83a..1b28e2a 100644 --- a/module_project/service.py +++ b/module_project/service.py @@ -15,9 +15,14 @@ from django.db.models import F from django.db import transaction from datetime import timedelta, time, datetime from django.utils import timezone -# from onesignal_sdk.client import Client as OneSignalClient +import requests +from onesignal_sdk.client import Client as OneSignalClient import logging +import onesignal +from onesignal.models import Notification +from onesignal.api import default_api + logger = logging.getLogger(__name__) @@ -123,6 +128,7 @@ class SMSService: # raise SMSError(message=str(e)) def create_otp(self, principal: IAmPrincipal, otp_purpose: str): + old_otp_change = IAmPrincipalOtp.objects.filter(principal=principal).update(is_used=True) otp = IAmPrincipalOtp.objects.create( principal=principal, otp_purpose=otp_purpose ) @@ -175,81 +181,115 @@ class SMSService: # self.send(phone_numbers, body) return otp_code +# by using office onesignal package onesignal-python-api +class OneSignalService: + def __init__(self): -# class OneSignalNotificationService: + # Get the OneSignal app key and user key from the environment variables + self.configuration = onesignal.Configuration( -# """ -# Class for sending notifications using the OneSignal API. + app_key=settings.ONESIGNAL_APP_ID, + api_key=settings.ONESIGNAL_REST_API_KEY + ) -# Provides a convenient way to create and send notifications to OneSignal users, -# with features like targeting specific devices or segments, customizing notification content, -# and handling errors gracefully. + # Create an instance of the OneSignal API + self.api_client = onesignal.ApiClient(self.configuration) + self.api_instance = default_api.DefaultApi(self.api_client) -# **Parameters:** + def send_notification(self, headings, contents, include_player_ids=None): + # Create a notification object using a dictionary + notification = Notification( + app_id=self.configuration.app_key, + include_player_ids=include_player_ids, + headings={"en": headings}, + contents={"en": contents} + ) + try: + # Send the notification + response = self.api_instance.create_notification( + notification=notification, + async_req=True + ) + except Exception as e: + raise Exception("Generic OneSignal error: {}".format(e)) + print("complete service is succeesss") + return response -# - **app_id** (str): Your OneSignal App ID. -# - **rest_api_key** (str): Your OneSignal REST API Key. -# - **user_auth_key** (str): Your OneSignal User Auth Key. +# by using community packgae onesignal-sdk +class OneSignalNotificationService: -# **Keyword Arguments:** + """ + Class for sending notifications using the OneSignal API. -# This method accepts additional keyword arguments (`**kwargs`) to customize the notification -# further, including: + Provides a convenient way to create and send notifications to OneSignal users, + with features like targeting specific devices or segments, customizing notification content, + and handling errors gracefully. -# - `url` (str): URL to open when the notification is clicked. -# - `data` (dict): Custom data to be sent with the notification. -# - `buttons` (list): List of action buttons to display within the notification. -# - `send_after` (str): Timestamp for scheduling the notification. -# - `delayed_option` (dict): Option for delayed delivery (Android-specific). -# - `android_channel_id` (str): Channel ID for Android notifications. -# - `ios_sound` (str): Sound to play for iOS notifications. -# - `ios_badgeType` (str): Badge type for iOS notifications. -# - `ios_badgeCount` (int): Badge count for iOS notifications. -# - `ios_thread_id` (str): Thread ID to group notifications in iOS. -# - `android_background_layout` (str): Layout for background notifications on Android. -# - `android_group` (str): Group notification on Android. -# - `android_group_message` (str): Summary for grouped notifications on Android. -# - `android_group_summary` (str): Summary for grouped notifications on Android. -# - `android_led_color` (str): LED color for Android notifications. -# - `android_accent_color` (str): Accent color for Android notifications. -# - `android_visibility` (str): Visibility settings for Android notifications. + **Parameters:** -# **Example usage:** + - **app_id** (str): Your OneSignal App ID. + - **rest_api_key** (str): Your OneSignal REST API Key. + - **user_auth_key** (str): Your OneSignal User Auth Key. -# notification = OneSignalNotificationService() -# response = notification.send_notification( -# headings="Welcome", -# message="Thanks for signing up!", -# player_tokens=["PLAYER_TOKEN1", "PLAYER_TOKEN2"], -# url="https://yourwebsite.com/welcome", -# data={"user_id": 123}, -# ) -# """ + **Keyword Arguments:** -# def __init__(self): -# self.config = OneSignalClient( -# app_id=settings.ONESIGNAL_APP_ID, -# rest_api_key=settings.ONESIGNAL_REST_API_KEY, -# user_auth_key=settings.ONESIGNAL_USER_AUTH_KEY -# ) + This method accepts additional keyword arguments (`**kwargs`) to customize the notification + further, including: -# # Set up logging -# self.logger = logging.getLogger(__name__) + - `url` (str): URL to open when the notification is clicked. + - `data` (dict): Custom data to be sent with the notification. + - `buttons` (list): List of action buttons to display within the notification. + - `send_after` (str): Timestamp for scheduling the notification. + - `delayed_option` (dict): Option for delayed delivery (Android-specific). + - `android_channel_id` (str): Channel ID for Android notifications. + - `ios_sound` (str): Sound to play for iOS notifications. + - `ios_badgeType` (str): Badge type for iOS notifications. + - `ios_badgeCount` (int): Badge count for iOS notifications. + - `ios_thread_id` (str): Thread ID to group notifications in iOS. + - `android_background_layout` (str): Layout for background notifications on Android. + - `android_group` (str): Group notification on Android. + - `android_group_message` (str): Summary for grouped notifications on Android. + - `android_group_summary` (str): Summary for grouped notifications on Android. + - `android_led_color` (str): LED color for Android notifications. + - `android_accent_color` (str): Accent color for Android notifications. + - `android_visibility` (str): Visibility settings for Android notifications. -# def send_notification(self, headings, message, player_tokens=None, **kwargs): -# notification_obj = { -# "headings": {"en": headings}, -# "contents": {"en": message}, -# **kwargs -# } + **Example usage:** -# if player_tokens: -# notification_obj["include_player_ids"] = player_tokens + notification = OneSignalNotificationService() + response = notification.send_notification( + headings="Welcome", + message="Thanks for signing up!", + player_tokens=["PLAYER_TOKEN1", "PLAYER_TOKEN2"], + url="https://yourwebsite.com/welcome", + data={"user_id": 123}, + ) + """ -# try: -# response = self.config.send_notification(notification_obj) -# self.logger.info(f"Notification send successfully : {response}") -# return response -# except Exception as e: -# self.logger.error(f"OneSignal error {e}") -# raise Exception("Generic OneSignal error: {}".format(e)) + def __init__(self): + self.config = OneSignalClient( + app_id=settings.ONESIGNAL_APP_ID, + rest_api_key=settings.ONESIGNAL_REST_API_KEY, + user_auth_key=settings.ONESIGNAL_USER_AUTH_KEY + ) + + # Set up logging + self.logger = logging.getLogger(__name__) + + def send_notification(self, headings, message, player_tokens=None, **kwargs): + notification_obj = { + "headings": {"en": headings}, + "contents": {"en": message}, + **kwargs + } + + if player_tokens: + notification_obj["include_player_ids"] = player_tokens + + try: + response = self.config.send_notification(notification_obj) + self.logger.info(f"Notification send successfully : {response}") + return response + except Exception as e: + self.logger.error(f"OneSignal error {e}") + raise Exception("Generic OneSignal error: {}".format(e)) diff --git a/module_project/settings/base.py b/module_project/settings/base.py index ff45bb9..f64b6e3 100644 --- a/module_project/settings/base.py +++ b/module_project/settings/base.py @@ -52,6 +52,7 @@ LOCAL_APPS = [ "module_activity", "module_cms", "module_support", + "module_notification", ] THIRD_PARTY_APPS = [ @@ -86,6 +87,7 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ + 'module_iam.iam_context_processors.iam_constants_context', 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', @@ -151,7 +153,7 @@ SHORT_DATE_FORMAT = "d-m-Y" TIME_FORMAT = "H:i p" # otp expire time limit -OTP_EXPIRE_TIME = 10 # mins +OTP_EXPIRE_TIME = 5 # mins APPEND_SLASH = True LOGIN_REDIRECT_URL = "/iam/dashboard/" @@ -212,6 +214,12 @@ EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD") EMAIL_PORT = env.str("EMAIL_PORT") EMAIL_USE_TLS = True +ONESIGNAL_APP_ID = env.str("ONESIGNAL_APP_ID") +ONESIGNAL_REST_API_KEY = env.str("ONESIGNAL_REST_API_KEY") +ONESIGNAL_USER_AUTH_KEY = env.str("ONESIGNAL_USER_AUTH_KEY") + + + # LOGGING # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/4.2/topics/logging/#logging @@ -251,8 +259,8 @@ LOGGING = { # jwt configuration # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html#settings SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=10), - "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=15), + "ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=20), + "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=30), "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": False, "UPDATE_LAST_LOGIN": False, @@ -274,3 +282,8 @@ SIMPLE_JWT = { "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", "JTI_CLAIM": "jti", } + + +SOCIAL_AUTH_APPLE_CLIENT_ID = '' +SOCIAL_AUTH_APPLE_CLIENT_SECRET = '' +SOCIAL_AUTH_APPLE_REDIRECT_URI = '' \ No newline at end of file diff --git a/module_project/urls.py b/module_project/urls.py index 5c71f99..b322818 100644 --- a/module_project/urls.py +++ b/module_project/urls.py @@ -30,11 +30,14 @@ urlpatterns = [ path('cms/', include('module_cms.urls')), path('api/cms/', include('module_cms.api.urls')), - # path('support/', include('module_support.urls')), + path('support/', include('module_support.urls')), path('api/support/', include('module_support.api.urls')), path('activity/', include("module_activity.urls")), path('api/activity/', include("module_activity.api.urls")), + + path('notification/', include("module_notification.urls")), + # path('api/activity/', include("module_activity.api.urls")), ] if settings.DEBUG: diff --git a/module_project/utils.py b/module_project/utils.py index 269d54a..9997fe1 100644 --- a/module_project/utils.py +++ b/module_project/utils.py @@ -36,25 +36,25 @@ class ApiResponse: if errors is not None: response_data["errors"] = errors return Response(response_data, status=status) - + # @staticmethod - # def validation_error(errors, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY): - # return ApiResponse.error("Validation error", errors, status_code) + # def validation_error(errors, status=status.HTTP_422_UNPROCESSABLE_ENTITY): + # return ApiResponse.error("Validation error", errors, status) class JsonResponseUtil: @staticmethod - def success(message, data=None, status_code=200): - response_data = {"success": True, "status": status_code, "message": message} + def success(message, data=None, status=200): + response_data = {"success": True, "status": status, "message": message} if data is not None: response_data["data"] = data - return JsonResponse(response_data, status=status_code) + return JsonResponse(response_data, status=status) @staticmethod - def error(message, errors=None, status_code=403): - response_data = {"success": False, "status": status_code, "message": message} + def error(message, errors=None, status=403): + response_data = {"success": False, "status": status, "message": message} if errors is not None: response_data["errors"] = errors - return JsonResponse(response_data, status=status_code) + return JsonResponse(response_data, status=status) class RandomGenerator: diff --git a/module_support/forms.py b/module_support/forms.py new file mode 100644 index 0000000..e69de29 diff --git a/module_support/urls.py b/module_support/urls.py index 89c49a7..c7298d3 100644 --- a/module_support/urls.py +++ b/module_support/urls.py @@ -1,11 +1,20 @@ from django.urls import path from . import views -app_name = "manage_support" +app_name = "module_support" urlpatterns = [ - # path('contact_us/', views.ContactUsListView.as_view(), name='contact_us_list'), - # path('contact_us/reply/', views.ContactUsReplyView.as_view(), name='contact_us_reply'), + + path('contact_us/', views.ContactUsView.as_view(), name="contact_us"), + path('contact_us/list/', views.ContactUsListJson.as_view(), name="contact_us_list"), + path('contact_us/reply//', views.ContactUsReplyView.as_view(), name='contact_us_reply'), + path('contact_us/action/', views.ContactUsActionView.as_view(), name='contact_us_action'), + path('contact_us/archive/list/', views.ContactUsArchiveView.as_view(), name='contact_us_archive'), + + path('feedback/', views.FeedbackView.as_view(), name="feedback"), + path('feedback/list/', views.FeedbackListJson.as_view(), name="feedback_list"), + path('feedback/action/', views.FeedbackActionView.as_view(), name='feedback_action'), + # path('feedback/', views.FeedbackListView.as_view(), name='feedback_list'), # path('feedback/delete/', views.FeedbackDeleteView.as_view(), name='feedback_delete'), diff --git a/module_support/views.py b/module_support/views.py index 91ea44a..00658c1 100644 --- a/module_support/views.py +++ b/module_support/views.py @@ -1,3 +1,152 @@ +from django.conf import settings from django.shortcuts import render - +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse_lazy +from django.views import generic +from module_iam.models import IAmPrincipal +from module_iam import iam_constant +from module_project.service import EmailService +from .models import ContactUs, Feedback +from module_project.mixins import DatatablesMixin +from django_datatables_view.base_datatable_view import BaseDatatableView +from module_project.mixins import ActionMixin +from module_project import constants +from module_project.utils import JsonResponseUtil # Create your views here. + + +class ContactUsView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US + resource = None + action = None + template_name = "module_support/contact_us.html" + model = ContactUs + context_objext_name = "obj" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + +class ContactUsListJson(BaseDatatableView): + model = ContactUs + columns = ["id", "email_address", "subject", "message", "active", "deleted"] + order_columns = ["id", "email_address", "subject", "message", "active", "deleted"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', None) + + return self.model.objects.filter(deleted=deleted_flag) + + def filter_queryset(self, qs): + # Implement your custom filtering logic here + print(f"request is {self.request.GET}") + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(question__icontains=search_value) + | Q(answer__icontains=search_value) + ) + + for column in self.columns: + search_value = self.request.GET.get(f'columns[{self.columns.index(column)}][search][value]', None) + if search_value: + qs = qs.filter(**{f"{column}__icontains": search_value}) + + return qs + + +class ContactUsActionView(ActionMixin): + model = ContactUs + +class ContactUsArchiveView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US + resource = None + action = None + template_name = "module_support/contactus_archive_list.html" + model = ContactUs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + +class ContactUsReplyView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US + model = ContactUs + success_message = constants.DATA_SAVED + + def post(self, request, *args, **kwargs): + id = self.kwargs.get("id") + message = request.POST.get("message") + + if id or message: + try: + instance = self.model.objects.get(id=id) + instance.reply = message + instance.save() + + email_service = EmailService( + subject=f"Reply of your inquiry - {instance.subject}", + body=message, + to=instance.email, + from_email=settings.EMAIL_HOST_USER, + ) + email_service.send() + JsonResponseUtil.success(message=self.success_message) + except self.model.DoesNotExist: + JsonResponseUtil.error(message=constants.FAILURE, errors="Invalid contact us ID.") + except Exception as e: + JsonResponseUtil.error(message=constants.FAILURE, errors=str(e)) + else: + JsonResponseUtil.error(message=constants.FAILURE, errors="Missing 'id' or 'message' in the request") + + # Redirect to the desired URL after form submission + return JsonResponseUtil.success(message=constants.SUCCESS) + + +class FeedbackView(LoginRequiredMixin, generic.TemplateView): + page_name = iam_constant.RESOURCE_MANAGE_FEEDBACK + resource = iam_constant.RESOURCE_MANAGE_FEEDBACK + action = None + template_name = "module_support/feedback.html" + model = Feedback + context_objext_name = "obj" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["page_name"] = self.page_name + return context + + +class FeedbackListJson(BaseDatatableView): + model = Feedback + columns = ["id", "principal.email", "feedback_reaction", "comment", "active"] + order_columns = ["id", "principal.email", "feedback_reaction", "comment", "active"] + + def get_initial_queryset(self): + deleted_flag = self.request.GET.get('deleted_flag', None) + + return self.model.objects.filter(deleted=deleted_flag) + + def filter_queryset(self, qs): + # Implement your custom filtering logic here + print(f"request is {self.request.GET}") + search_value = self.request.GET.get("search[value]", None) + if search_value: + qs = qs.filter( + Q(id__icontains=search_value) + | Q(feedback_reaction__icontains=search_value) + | Q(comment__icontains=search_value) + ) + return qs + + +class FeedbackActionView(ActionMixin): + model = Feedback + pass diff --git a/requirements.txt b/requirements.txt index 6d7ca78..9bd3499 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,12 @@ +anyio==4.3.0 asgiref==3.7.2 +certifi==2024.2.2 +cffi==1.16.0 +charset-normalizer==3.3.2 colorama==0.4.6 colorlog==6.7.0 +cryptography==42.0.5 +defusedxml==0.7.1 Django==5.0.2 django-cors-headers==4.3.1 django-datatables-view==1.20.0 @@ -12,10 +18,26 @@ django-taggit==5.0.1 django-widget-tweaks==1.5.0 djangorestframework==3.14.0 djangorestframework-simplejwt==5.3.1 +h11==0.14.0 +httpcore==1.0.4 +httpx==0.27.0 +idna==3.6 mysqlclient==2.2.4 +oauthlib==3.2.2 +onesignal-python-api==2.0.2 +onesignal-sdk==2.0.0 phonenumbers==8.13.30 pillow==10.2.0 +pycparser==2.21 PyJWT==2.8.0 +python-dateutil==2.9.0.post0 +python3-openid==3.2.0 pytz==2024.1 +requests==2.31.0 +requests-oauthlib==1.3.1 +six==1.16.0 +sniffio==1.3.1 sqlparse==0.4.4 +tqdm==4.66.2 tzdata==2023.4 +urllib3==2.2.1 diff --git a/static/img/bowel.png b/static/img/bowel.png new file mode 100644 index 0000000000000000000000000000000000000000..ae58ac624de1ad74a4ca98527404091b6bd4366c GIT binary patch literal 85125 zcmYg&2RN5u`?gg`$%-PGp=6bry_Hpz>{WI~itLea!s3WbtvGD1lxk|bH#m7P7l z^Vj>n$NzI2j)wX@&vW0`eO>2up6B&Mp3za?vx|8b5fRazQyMCIL`1|3_+F&giN86r zs}X-7BEG7pu0-^_{oo`K5j)W-6~%L2=D$-ZW#YasZ+{fgP0V5FV0WPFP&iC$!xxvU za&vcNj?{(5=Nd#T^)g*NwK7}-4{It3C9BFax<#GGcn(d}HpRw_X%oFX(7WN(EmYG%d6 z#qZiXJKIc8PFig*{b6A_fv=7H*RNkI8yi;|s;On))Zq4{=8`cy&n=u!nUS#f*Vwz3 zy*$K4l7US2)ARH5G5hxI8!jm+k-@KAPL7UbHGZp{U7ek8HvfE=sE?0lTv}T4e2ed; z?rwwb_I91OX=#VX?%t*5DeUO!X>Mt5?rCdiSkP8d8onJKu9g`UrJZ~8X7j`PeBSsd z-2W&ps-GqjFRVp*PlLNOJbdTfTeo(l;RD-U2WqYaZf$rEzl&yTlJ{Q|efIdVN>OgE zee2g37CWYX{gR4Q;pDpMe*LEtGO|2bK+_`h7lCJLF;IObAX&l96 zW%Pq~4|sJ0e$C9(`gdfimIXMHxnkjhTyXWz&YvIsELIu(oTDrJEI#+yGrHBq$p;J+ zp6AcUsW>=1wKOtHRPpdAvG?_rR=;rJ!i*htXk}GZRoSlA*4EcbT3hn`{7y5q?>VW5 zwQ2^rS!cl(y)uN)Jk)l_xa;p8;J^gHKMjwAn zm#4yAEQm=+MB6(#v2=V}9d{SIDYQBuKg<#R`28C}K0ZTvpT&ve92}3VadjX3Dnvp; zB4BN8onBs1!Q$=Z^-M}q(%8tz$k))&aL|_*4?s>%u5PEUu1?-FG&JO3V`*s_6tnf( zclq4X?{BYWetmtBr8rT)6vuzzT3l?b`Q8xQH%B>xt<;H{{CdQs3|1~sVE3$*l}Rn| zbnN*T?QCqceeqxOnS{(+`2+z09rrY6(e{Jg=fTenIP&I!5^FAiaU*Y?S8f0bpE^SftW z;&{F>@4^cPOCALai|x}|TCFEJV|w_vyf}!tg|AT{m{Us|lNKK+7#V$=NJ&X~Ix|q4 zKD+eCjErlrH#Rlh_?3G$ z>sDh^lYBI@*fTvt!*W9fg^Wk(f=jD&BO{ke#GJYt70OAW@ZlO$+5g4$C=D< zAmJMm{UI}P@Cij-b5%`E+Va-sI#rsCcjn64U#BOXot-i46MOdTk=f2OO=ypgj}PD% z5HORxKHb-PHcQPWnpL_~2N$}Pm{_5Mi$>}NY~TIy_t#g88n>Yvv@*Z`{P{CHH8r&^ z?KyjZJBZBwA<-?VKc#zo#DmD#i#8C+nH3ckbVBCUpEORLN=#2oJo5F&kGB4fj(RQ5 z%>g6l{T^4ZUM<7p2JT~E$iE*K=PzZ~o?>^xuH8?>rLR1ieS(bSQHTcj((a681`&_> zkyBK@zUwE1s|p>zJj+c;NHDj0M}>fWq>KQVC_dah;!1}5@j`ZqOOp?j`-6MkrJF97 zYG!IG6CM*2LovLEcEqi|Qi6h&iRr?6>=AwQ4=pWY@0*($v{EGJp4_ycc!ApyCnqK*xOsSZraG_sf4Yu`+)BnnF5Ti#9sKZ6h!b0Qp~ib*>{(x5-<+zd z>Q5&pCvPj`&3AuYaz)8<+Y&^6t|D^N3BT#U<>YjdM4u+yzU`!O*r+04bM3gS>}#hp zcBG^sp$Q55Uvqq!FeI!b1>aZ?1KfNVRc^0(+vL(-+-6B7prD(}~ zr_O3*&%o-+O3kU$rwx$cWX!6)uH3JzT>BZrrEWkUsgiJ|l(dbF*8Vc{TUt4eF) ziE#%twYR@HOpNmP)9ueDJJTBB(D3Z_t62Ap!+{;KP)R>&IpEiEmy_r&&0 z?cj3jxwS)o{ByRpul&`qRy%rLoz@gt-=zd0;7*FHS%qdq7cMXf(^Pr$ji*unMMwQDZN z%qm6APoI9~n5&z*{N$AaO$JVkzSmHqod3zYt?MLvXH-e9DG@cL=rB7ukCHfzgKor0ONNC<{cjBAxo2f9DHPd*>j{au90~c!jgSxl zA+yR$J)b`}eDCUNR5ITVk9mwN_N2Oc!^GczQ^U;c2N^AGjIrdtYr-2(z8*i@I$1LM z$(4Kd7n$3otzY@G?Zli!>Y+TmyiVNQ+!Kg($8Fh2DkS3M@bI?{s<#<`nBso4DXdvC zG9#WGU|6vF_SKvbx zbh7N_%PWo1c7D+ZrRMXVJ)1J);>x06B7&fdj# zjP_*Oa4YcRAUHfZ9Db7xwMd_^ng_Ap7H7@~r%?=z4d`;Sh*+yc#K#+CCMJIK-mxRh zj+2v9`6%L3+{(wNGV|80mJ@wRYIy>o0UW_w^a>&~@(ot5!77|Dv$M00uB@+D^}Y7J zu6Xex*IpJDmPZ8z1rOVLUS33gF=4cGa`L^ru`*|af9>C+oW)6`F%E#SxcmO^<_{m_ zedmUojvZx;Rz)#U*nuZK7bJ7;HhYZ5!T0aqQ=tgg3LZb+c`q?Bk$65(S?}TW^mM~n(TrCR$0{Fq(?2m-M{|+{&qs@@Zl#HJ_*jsa&alAb^EJrqeM)w5Hl>)%)P0i zB$Eujb3|O+v+4ZEr?Y*EzyA;|WdeSsAGxPk~+S=Fdd?R`he0oGf za#!8gLp{iZ&jAYWW@qzE<9i9;&&S4Wy9rnK{Ov*o|NL}314-6n~Xk@hGx~J#yIz~mhNR^S1k;|8CZCz~a>}H0BhaD|#Z2nkU zSvmY19JIHwwsy3!vYI6P8v>BAwzG3(q@l4N85?uqrHgDMwYXS(lk8sI42^=wHmhU9 z$N{>@#)NzK3g??+xP(({f>5g}UT=Qv5p!^u{P_0m+Z-&tYyE+54P@_wE+AFwl|OrS zy1Kad+BLw!Gk}GL*qY|b%9Ck;u7@P0o@Zr+saPlR3zewh3L&KFkpy9-X1VdE0i*mbm?dqPLoAYOJv$Ur{ zaSr4TvcAjuvzFps*5~EvA64TWS?UWzJ!khvh3vlrIMe#}EzhNf@I7|w(TB{!SGH1# z-177CE;`xU5B@{JD|0_DCCmd(CY&=glzg6$hUixZ2g z@)4oI?JSM$JF+s%Spr2t+k7h{f!nR!(Y3tmr1GTcgEyE+0ENwm>@Hq(x_I&8FS3{E zl2@!>=H^cD0-I$X(a*jVec(j40w52u+6KM^66|V)Mv%Q{XOJRZUf)n9bTv8I*@BD9 z%LDhYUzIsI5wu%|4cGGV_%D$j!lGCt(v)boXLR(>p5-93IDh`UUQ<&O*FbHcdbSE~apH{L_RKX~4TcTLtF|oai%b||`{&vFm zr@p>cQlWhVIkVqtb$`4Pm5^BQPoIi#2uPTYSl=Gc8Fv-h^}43+FgyF1J)A;EjwDKW zOm#&oDJkt39v+S(RN*k^J6?>kTW6I>{CR_{`>)$7&qx0L{ri-hoZK|cU^hO0b91jO zarbEnm%jbhR#sO$r@EdRDHqNA=QC6L^Ym%8 z;Qhyig%@?4w?E&E-?&O6jSfKvRm$(ml`9F^TgNy#^>%O>89p2v9$tIp_n$X26X?f7%%1q#{Cz*5DvSEvgGS=EtqIm5 zA}?EBUVO*cmLhe6zKHZL;&jo@b9Pz7vK;xHZA>;JKmYPIOUuD*)PG~Y)p;B4%L2b9 zCfE`AC3#Punn`=j4w~ceRuA2xTp!x$Bqv>cI^0tdo}s~ z{kycKI+5%QNsvt#9?`%s-oerF29fMEK&kFALBV(|Sj>lyAKA=|jEHWR9Z zRqqkYWFtevo)&~g75ea~!&Ef<{QReMb%om7+oyYO?4Z(%U`gxj)LUw4Ih}}A)|3m} z@>4^Qrj)r3w6ATfo?vRcvneU2lcpAa`MU6vwzf92W9T?!u;eWw7vEM#C=@=`&l#hn zqGGy(lju_Za-(cbsgLNGGQ+-or$)!dT7a27&YeBmF@ej(ys)Ufr98}W+{92kXwuKU zbte}WmjkNODq4@g!@Ru9yY3GXjiZ$duI!FEvI}d8?)f2~$k74_C0f+A&5fIoH;%qV!!*!F&`}F<* zan#qNNF5ROx#CxC*@s7kEjO($mxBx3GMA z-rnBx(QNW*UqE5(wx+fy+}du?qKJ%)eCpg+eua%!C&iGDi)-QZ`SX7(fKRz7ihey* zUJ<6tIDy;0)@`|YWw|X`LVsg(GnI>z)4;>i)BT?MPc%-guS!ehs@+Fh${*FF-bA*l z6P*EZ@eZru#C`a%1NKdOYkri<)z{b8m0QZ2fnqz{hCE?<9}UgQae!oAW@hFC_#CZ= zv9b9K3=E-9%^9Y*uC;8>9V6RQ3P>z>J8Fj0f zJzb%%XK>a_jvhU#%XrN2Ugp!MiO12)xuYjm;*jV#MwdZ}M-c7U9qU1&mNwp=YT~;% z(cz5y${ctQw&-%=gqYat%huNJUf9UjTBB@|0ybX zr`(De&0Vz?VwNbo8Excod=i^(lIW2@H>aQ{q{I|0zJI6U`oO!`$_8v0zI^=nl662p z&1EE`85iPt&+3_Fx}KgMV?3+5f}@rFJBQENHkuk5v0U2R!d-;+y@YYo#Gj6yUJU1| zc6{=4&Q;4;N|K$KDDv*evs<}@miy5yc6oUVL2+?|pMSoWot~VWJe@A*Kc#)~w0ZR5 zKs#WBG^wj&`T$Dg^gLS4iHP>^C^#efCyo7b-E(YT6Z+OyuU_%i`YyW+Crh}-9+LBu zWHRq#SLP_5iT<_=RLM!)v&F|Gv0tNvmJ*qC;GneUu|o}-Z95x^_<+Y#tnJCax`Fmd zsH;<0)_f>f5Zr9#zIGn1#4$EDwj97>d1Ou2kYA&9@znY+5QW2KEEhqfu#UXwZN1=+TwHZ$&ndg!pdx_|c*b-|~fr9*H^9qjMSEUi+IzruE@n zQ)7{$tMCZS7gvObUf#@kt6 ze=l>*^Yrkjmz$|AdhqhqtA=Y%4I}R#3;-)+AK~YhKc}bHQpbLTd9s+{S{6Xk;@c<| zt`3I2kRj6&b1LbM)Xkkg5G{80L=VOd82 zv67wjCn$(rfJ=U`GBRe9zn8N#X3b`fii-M}oSaOKtL;rZIQ8VolPpBX!+v0Vsr*!> zr4qJng$7R%3e#li>(e3F^}ba6hULJ4L_1(qFUjUXj}AqFibszW2NB4xKq5)5<3CRk zh>ATz{)NBE{K+{-t-96p^p0C-YySx@EnSMLxH{g?#K`!T{hgw-)Uk~V1T?(sFmr8A zb#)3CVEe0xh=_~Y+HD#%MF6jF%CG#~B`YK2xu1o_;>q|W?n@ik_jRy8Cv?(e93IBU zi`Twj=FP4 z)C=ABB}4XNO zhmboXYaRSGHKq1OgCx|szuJ59?%lg-K0ZEW_MaCl7M_7U+ip*lo;rK>Y#yN|#Kpx? zK1k=*%>ht-{4_6*w>?c(`r_8cs_vDpMW@Si&jWdhI_3!cP_W!}w63EF=gyIaa7$i_ z#n+MaytY6KeO;eEX>@-2bAR`xdbCU8?XU-J5fF%(^d<#uX zO}(Y0q!i=^o-_S_(B>}9O0-h1 zabZdm|D8I&IXGykDQW(on*1PHN^)|QB1oR6k&&ksJD+F@U8MP*A@Qy7Y6jZJ z4dkV-78UNQ?({^To3lFYLCjuslY| zc#nI2f#_}&GA*ZBKW39mTClBvgiBr70Ct+O{?J@?b@i!1!Hv0ladBu zDEJol@FA6&s_FrY%4_eB;mqiP+dgsUn&oX8t&2*cm64I}MR00GWj-WPpPUfBM%&r) z#!46X!P*7bNDo@<4qU6;rmA%sb)V+~=Mjza*5swQC~uPZptD z>M68R&L}AOqlyB|Y&^%Znc+%6I$Z(6X5+tp{rU||u=~gtt3A|u_osVaHhgVr8fAZd z>5A{dSkyUp_xYp{^1Ua&eEqsmf9<6AX)|+kx$ljQ+?Rh;dgNT;-@C^{?phcpnPs}q zwQI$vE?@qAD?dN~)W}1l=Hn8MUAcRaLAP)>7l0gB5;>_eI;_Bnr>G?{-*xUoBNJR+ zE@?(Fv9l~&EAjsIm02r00f7P*(c!V}YowghL&Zgfg@u1-)$?DydO#6a%>L~yzx9p*xE1g+)jV=$Z7+nt_*ZF!ybDp24MpO10I@v`)pVa z9=xn3_jJ##;KL*U)EAKT$dO|LEwr@mJSSP=3)-bkeU(H^Oe{-TNy#8h&VMZ$EdxJiPq!?+0UY_hvBJt$S=0HkS-_V6-$037$>?QL#Q9!>0lahO)%ZTF0F$$X@NDx-e&i>XQCYUCU7`oC_kuu8Op-|}U z0kryl^Cm??)BRu@K11CZY^zbZ+Yqar-FO4&#l3D9-U&}JY~~-mLFsD#6?A54aq{ykfn&$o zm5P|q?i_Vo39(@(_Bl-Sc?Jze>DT9GCMf5=l=F5d*e_YAH9-b1WaVUB@NtfKG$?Uv zmA#lVKQS>mIpT3nPM*RX`v`%kra5@9+6FoPTgYIi5L04ms_grQhK65je}5&JnVJ0_ zsT0TIM~jP#s}FV^M;6*WwrZ2<#YM+XGkA3rs-+mn#-b-rMqdP-#ZP-Gy`912*W9>q zW2NFv`}_+Oil=>207E4p(sT(en^K3{Qu))TlNHG9uC$DdE}2ErwWfW9jwCcR^sHm%9jj-@CVOpF>+64sw1f zY@B}F=JZ`ipdSH;>5-*tI8JiUUdNd`_nTf(QL!6)>Gv58_p<-a=*NJ??+|n#8Gdbj$_WztO+{c)KdrU-4k^U=uP^8 zt(zkG(i`-1WUWK|a5Mm8+rs>OZ(?lh<(OLfSS$KZ zKg=LwpCN0^J}B$cbwo(W_4umkUl4FzD9`497Z>}ak?o6!1sI|_1c(`~?vgM=a?_xq zx~&Xp+CsBmUe2jI|KUTy5UbNh|MLQv9X}`^;Kvbmds|@P@ZrPOkb_MCj~%~%`}QrF ze#>|2OSGV%ps%<~AI0P)>A(+?a-n4R6w)a~#l?5c;K=-1T^Mi6%gYPnm}1rA1VB9R zDkvbZX$27=01)>P0y2su;S)EE6t$-^ z%F7dk`1qP-MMb-&AZqlnPtDF3Ko#&p3{L~=1*{}z&xxEo44uAi4-+k|)n!-L84D8= zej?vOa9FErHffvh0Xt<{T0k4+z6F1Y4FuWw&lL^}mFtcv`y>m=_kzk?vd6?x7W8NEMn;DCeYNOO!lNF{Eo zr+c&eJ+57QUR7Sc(wZv0JjY&jvq}FZ5$Z=>cUPC2>yOGWLP)#^OH=E+KpL+7sH>~{ zi@X%KDJ+{2{bthkd#UpRu3h9nXvm~Ux^a|0*4Gd5@uclP(`Ub%a`(w#=+j5g&_qCs z%Tkq-SJcqR?QCecSO&aeB=g?wQ?h*>E3^ph$7tI=^z=xs0Pw!O*UPZv|K^kxLh9Rb zf|fKODm@r2>$l>z)m>l`T?;b^bG2KYm`zLU4OVH--^QMVjh={AwzrY8wGu9c@NBe;70>+upr< zEvSYmzT%<$_p-@VhZs<69yxlH?nkM!$zX}o*F_F@)(z0v&%fah)Fvk1uc~m)edIW4 zPDm0|B!mi&z`91YYnOZIXX+tW60)kTxp!>jKYsiGQMYUJIw`|~Trmg5Dv}1kH;-U& z23`5m%)rbr!VK1@`)iiBtjhpEH9zB_S0GUG)Y%{GZ$v#!&`KXY!7z}JRJOE8hW;ulDm7=i zh(+&WRn2IE&V44F+v@;;tnO}oICcYTtE(c?;4ykyB-l#5cm>0 zxdv)n2jp#oN8V;UHe9z1y9dg;=L4`)m>(Pw+O6P}}NxEa5+Zn80+onsRIDS$OS z6G;@QGTdM4oNS_@(Y!Q0?Gg?JkED7QN8xYb?b{#D8f!{xA3k=?Y&V+tq`gJ_I8mzH zyu3oexgPuDd}016udM8Byon@W*75C|)GUhDtr7db5VLK(e0+KdjMEOYWu0q>U{Sb5 zM{vCKq8|9fKYXb9<}#NlU}-%8A`&!327!Z7d-+h{w}W5h=WAJ_%i?|W=FOc!@t)q7 zSdc#md`Z!ag4yJ^nORwX%bdS^7UAFR*Q%4dPEH;{gzwGbV%celyol)ksJMg%x!kHD ze6XvlE9}g{-Nwj-e}B&oHP`~9wLiH*W!OkW5C)lU3hybw&w`qpHT2cg)Hw46{=hrL zliiKOpnT@cC(jEPzNVsGPhlTEc<^8pAqGLwQr90TB3`+aLCGzAhXNT%weiCTC*H;% z3ME#ts5z;D;9LJd4V-JB*zv<)U(*I#615)ZNl8gEi`PCLvRZk{X%~frgf2)(NTk=h zINZz~z-q3$b8Y4_pYT8OBwmeGkgyR~rRFnchEE;|Q@)yu4`pYS?mQqD@; zQ(#}b{H^G)7eKKK!a&e;1{u{&-4CPF+3^?K120R)h-|B=-<9cw#PPflb)r`| z2s@UO!>3WJ8M<4md(DVSlVNB(h~{;2vme=m-in$@D)?Z(=Pj> zB(mLq4S)3wPD<9UFJGE=%O4jPuh~ySV=2*NM+COHw;O^*6?!bPOR4nx_6aO6EL=5w z?elkEZ=aip4;-&|yZAx zhy#Kwp#{GfL;_nES0gJNgv_DuN4KA@7BViNX+HvU06~JZ*3!}nC6N4}WeO7E zGsy%~BtyLhcC(f5I3lg}pUeix<gDe8uFP!#Ow(X3g8LRsl~J%UZcARXwYmA;^=sF5!wDAU2ibKbXr>qKZ5r%N zzI)6trY0;uSRqwS$G`bT58B=q!HAt zz5Q@A5#<8&*R=Kb_eWRuZ0q-8T^O*9oM@fi25viiCBz>Dt4Rj9ZTJ6g{pvna)IGfq zp!2Q)Df;~~GwK4AE@|sKA3sj)`8^X6mq5OAr;EFr8wJ@}@;ms4#K3-A7Yn<+_4M`Q z#GQMu4WU)-6h7b7*7n1FxREjfL{s{K9$PghLKqOY&;-_l9H483-bZ?WVey{!44LZQHb)uKz zty^S3S7)Q568a=N+mn5rs_~i|evCtwcUuXUL0|xFrvA);_?)^nl1tqJhQXCaN$PL{$hZn{oyV zXwtucyis6I{d|LND_#BfSkGvJytvue(%Y&$|D05kb0#!BlvY+&$)Ta4ZYBa@CRWY2 zL10&8R8*{n!Nn)ApO}i0G7e35E)0~6XA=I%Cq>x*-NPbox&qs>=s`-5_8D$DwfssZgQT#EFQA@W;l*oyeUekN@X-&k!Y+%ov^M$f9m< zZx0wa(L@U^iCzcoK4n(l5#_+XKx6d3m)8UYHkMI7ih_4B9)#l81Tsvtq|ah*$-R4Leh&Id z6wB_J{)UILyH1-5*4!*zVw1-`|Td=ZDD@SB7$m$Bj!zhiP zh}E0WHwd#h1Y}#Lr*y#jQj{*b=Sd`s(}im7u-gBb`jV&S=;-*qYiS4JB0$PJgHZ&Z zqd0n}qF5ce5isO_^(v0@e6KRV{mzD*M}}kf=&wR7WeW@p)F5kUhUbM2F;oo{lh@@^ zos`7v0`y%75qQ*X{=h-n{f7>@gi%`M{-85Fgy;&+c_eRhI@_p)BG0%5gqFDyl6Jd2H#B9MMDcCpy=!FE^m`f4!|8&iL=( zFP_SPE?C#wdy<`#)0!2*0~=upIbeN5$Hzq@uHHExjho)^d=aH+QxEU4hXCKNrmij& z;@{V$_6P32Kcl0gB;P*{o`nCT9nJjuU@uLH3CX{0W~8j6dmIzf5eGTaJ}3|;s019V z1emM?u^Vi~%@f!CnCYx*AV5bGNGxLF(Vst^cB41SD=5(ZdvF}AHzP7;Fc4?{r~UBE zan#q>cXy?+uB6@#`iKQWKdm|lE}afw^#bmFl%vJJH9>@Ja71k1zBt~ld8^zw>=^ z1FwBAB=@c}5!{Vn(0DKm;2y#T1;USpk+D=qNhvdhk%iS?>E9>|Nig6vAKVZ;{;Tt0 zY#T<^UUO|wj685u2JGwQeddE*no_WNK+c+@5ZJ7Qmi)4vo%)xX8Wh!7>Z$-VixmncT3Y$@ zNOW$oN3tC@pG-$c5T+3NC>0JZgR@tMHl_?-A&C-qu!4j-2k z7wesdIZK&t8xFvVd+)Q~pTz>O!oFnr1RTxd_w^1nP4E=?fH zik-iU&w!ecXV*MPPp`iWCy%(XpPf<$P1=oNe?Fb1c;r~L@pOE z(pNiu734MjIq9_Au>P+yZF8J4Fc9}?OA?DAxCO!wd?a8*So#YR2wwD&#ahZwaS06$ z_F!Nx@E<+;*lPV@R#sLmC@OKsY?9WN7QHj*gkgm}Okxi@XWATgd@)paR!?yp41(8| zEf}$U1Qp;NRKe)*V^?66+}*gy3pC7iP$|Fj3ksAYG&k3cbt`EWphZ2smxk~;N3s;#Ad&;R z^tpe*<-lqAfQ@n_%uNxB)}R+bK}05A6%*0gevE{43SvJIeZDnZXjFIa+$qKw5f@?P zrs>@~CxXjG>iD`ViKq}EZ2Yk;;{?lA$U4RlK2-RxmokeUD`8$lXZP$^cR>~PsXdn8 z$!>a`IdhK`c<^!jUPEdW1laXx38&E6(UEz1W$vPC?mApv=GavMMjWq`eTEzPHMj&J z4W5vYkgrF{tcUf|3p7~?zd?RRFUL_NR65kLGT>@nDq;9JKLh%e;g~#|w{x3Y{+E%s zKgkmq>a<6BdA_yJpWn@aE!5J{(PtV*QM^20E3l%om;lwNp5^9VA}En>(%hkRe8QQu zS9x~lCWp;$0-uaN=()42D=)zoJKPe-f0AI1{#H93`QHt$dqF1&H}w;cb`GD2ckT>N z8yp;LOBQ#2W7Ez?giCN2RQ{03Ge<2!g^5A)AYoqPCq>$AKh~a_j2X778%c`SyK;Hsg9!&MgO|i@=i2?l znukQi#OCTcbHc(@HPBY>ll>B`SJU}8flbEi6Y%{XcDRntVz5b6O3M2^OhixbCxqcT z05&UNgW4HS-o_w>Hi+|ha9S=A8Rvvls{TEN1-ILDpVU=;{=845 zaEy!o&xyNNS4$6%|7C*l8}`2LhSq}6(8el|6@qawF-Mr&`|NdEB`){4$;rs{F=Uq! zhAXmja`^RssT8B5dWp!@@IeJ9n_vhJ4?hbtd3L#uT#7uWAz^~aK1$*2*_aU!ybp#X zFVkGpILgSzR(r!(I@Lakd_`H`8l{A_?XYINDPjH&TmUQk6gK=ErBBGjU-%~8y=!co zP>QceJYi=G-n{^bSRkV*Tsi$8f@w%p08xNptBwM2c4{QS3iHd$+oYWR5&ZN?v z{(U{3!$&u-C*NE5hoF5OzSLtR`r<$s^`P^Z|+T2s`^7^)$-;%T}~a^L#0I zyPE*v`({7ypkkD%0t|4+1mrHq*k~K}f7!ZjhQ1tSX&C3l)Y-vd2-H#V2^WX?`K0vAURNhxij;J_CU6}^@k9ISXytmW4dldkgBM>{CD>G*H6NKd;S#W) zEEqgDJ#b!M{{+ScOMdoMT>WM6!rVWMx!n5V#R~^udz-?ru@&YpWH1ZjgWB)q7#sD! zrLUvI@zTQhi8;)D9y>n(xcLX7ju1v7*~m+|dyB>Gt*N;#!asEtP2Vqe#XFB44V54( zUW6ELdphez-!II3*32Ql_xJrm9OaOZlK#1=VlDCHgWCFkYJ$Q65`fzq!auf#n{FSu zr`QGXGJz~pcp_3IkW|;Nc~dkzBI1|5qhq!nX%x`jSx}V) zG*Uul|6d$-%D#&7lB#{im1#6}5* z;(!O=hba9Si6|a&um6+F??asY&k!0ZgO7D}?4_u6ks%>jt4Iwe2=3=W zW7ay<$O2X)w5#jfUkvHIK{C~a;q@|=7};OUGG?Ozu^T+B1SLycM#ePVSbsg%UZyyb zPyrN3wzY`vzuSNaL(EM_sbj!@N=vIN8RW=4?Z2lzL?Zayp$0AxI7iUJ=Rrq&2KAq_ zVl?E|(oSdMOHKOCUF{`%bXl$Ic>s%W=I>%&W z{2FNT#w@-(dPbnJsNv{J0KFp=_SaEg|MNWn<{scX-!!bvpK*I!V?5Cojv(nAO{Pppq(tRT~$|A51~K{{a|9AEF2`%C}x z0xbPDIx(ws_Uzm$7=HHIN)NZ!%qyKwdB)_`3YEoL+uA;+%KCQu4dyKlFd|Byzin(h z7BO(lr1aP-)F1w*FVj<*JsSR-a|Q<}FJYM37=6+v$|WrZ8A_q=Y+2dZh>h&LdX65* zf$Rr-E^mR3h36I)BnXqOS1>W+9XlO*kJx^+CGIO>NNGqV707f49OTh_W`h4s3dFta zg-&0ezfMa_tFmJHjPW@tm;UOfN4U83RapA>++jLLd-bN}*RHN=a1qXXB!&9WLkkNF zyDik68yYB35YIqC>(kQJy|5w2LUVwb*-?#~?)(2G9en;7LqopjFJ1(4NlW`4ZFG{7 zmS&Sg7cJO3>tb1*aVvqKj*NoUXn=V~5@GQSYE+4eqT*1~ze|LMhbLX`D;L>K_xa8t z*|i0vSTVHc?*IQYaMCa{S6TtWj$3LLCnWSd1W0JdtgF%iXYbYZ{xhbnPq=1z$3sF5aMyV5a$J{}muMWRqN&wqFx&Oxd;%ST~y#yC0cLbAC zJ8w|ix!=?M{rKz2%?XL7Cg@=Pgt&Hr5O)=$Y~9G0>LJXuzuny1A#V7b*usGJc}#Df zQA8K)$0y3%P5hsLT+?AceAotqjJC&bN`X7v#3_G>b1nP3>Ts{`jbZB=7{UfWeLCZ` zpA{BoRaG@L=CjNV0Mt#8MfCxbc@*lD(YzsyCy=^U6wbN!mA^DJFu$ zX_h+<>pD2qlUTmdahJ_4R=5X>aMkM&=Ns8)1-Q8zTi?7H-k33In zHA3UhPQ1oVG#&;^IuAFPG-b#Piim{MP*Hq*+GtMGih!?F^@GlKNc1I&CI^aUf0p_Y zZ|84Cqs;`B0QrW7K-&^r`v26uONsU@{|WDjhjp-BG6E|!)kh|w7DR!~Gky;J_F>E) zQo%92y(HEQeCjI@2RayJdXB*Um;n2s$!YOrxC?9uw%MxOT#bYC(*z!xP~%hX+hjWb zHx6A%fQHJ^9esc$Is6Pg%-B6hgIqqQX{>G{*HPa7Vh%N&NEED0&%N4syaq+D=>6;J z>e3mg`#G><3PbXHhxn}AU$1j&_&@h8EJ`8cvuIB!6IlS%wl^>9YHOPbW7w+l_GPv+ zB0>TJtR?mxnImWh-hXPvsd9qfhx5p{5#V3UsS81KdNQw%C4&Eo8h8lLbDcNB?!nFN=y!YI4&Z;MGoA6g_g}ls56H{ZbV~tG%b+`)~cJ2CgdQfp62!21j^ds;%rf&eQo%1o@J%EM_FoBuV%$Jg+7=rHrso+!Xm3aFNv#5<5gA@*+Qf1oLk1S78s> zPt%POI4OXZIQTvd1ch-Jko$AtN}^+6*Z{d$bQyLe-Tyl|!H+Ztft1F;5bO&3W{y7T z0%$*~(9lrk$wzNVImZwgXr_14;;LE?c(jj}Umb5ZJw0X>q3;Sk^kXW#Tx9GpW4~2= zE8)|}qOmuX%wOf-ZIhD-3!gu8hL2B3h>enzVB_BZ+@dy3@Ob2sNvqZ8*HEFmu&lc+ z3#?!(@x_$D>*)J?w&#AT(_#$$I^Jb5IyyKwQSJFBsU7gxRli89nD8PVtcr4a(1Qn` zAAv`fR@DB|)s;qgC5558B5jTwE*b(kS?Pkia!R z$XnpO@Ba*B-?8KJ>Envu|0NObPk|)3i3dsYfm;h09GnL|A=X#pZ=w&L?bNn_ez#I> z(*{L(QXq^5 zV+pnh(+p9MYS72k!zor;Y`z>2iB|+!iyS{*xgS<%BHs^$L9!pEm_9f-6}A3t!#X#`6PaTGczmZ77alj5HyD+MuYq8Gp*v= z|5@~g+X||xVV~f*a3KtB;($oQZ5ajU!YSkIPeLLQ{$5a0X00qO+3oG^`>H&Cbz4Io z(tf(D)@#nX7q7{9076F{^&#MPRFqk%W&MqFr{f|#|9gVG=w|=^^>Dv(Wir(PK8Kox zi4HDS{C~wf(EyWHj$3A!4MD|s%~l$P3%U)$K)ZfXCi;BL!cs@58uD>Zh#a( zhGWnyQ_!j8<9j!Le+Pem>>f9%`TZfz?c}hqCn1T670K{Vh2V`(HDx$mB~Z6YXBQT} zZf-MlJIAI(u75c>?5`1|M|KBOHU!z8;x{;7sR?w#p0P*sYCQu@TJhM(N(>C7_oJuX*`BVj8cKj1uu%agz=UHgwji- zKqcIy5(XmQpwq!SucEj>+N~xhC6(h>#jZqV zd<(-Gj^IBHjg8IWHJxnR3TL9*??Pr7KZR^yiU<14u8>n+ZitzEjm*+ggA9!Hl)ZkP zng^l)I3lqQ1N8juZuNKRX&k(Wc>`nAhi6ldjnT}K)<$eUNDrG~Uzy!e8$mSW!7YeU29Aaa zcxVK)wY2yuQ#Ptb@Jz@%ubV%8nr%S_@&ci^jR#jWQBxb+oH;r7YMi~u?d_cdu5dtx z4fPX50DReO{g&F0rQ5%YY|!iN@UZn$n4A&;FZj_#<)Qy<2EG*mUVjy=6Eb__OvtAj zvp3ut+!-#}a9gm~T$OdEqoV@;G(%UN= zk9N5j$IhQ%bV-FGqXuW5V8rd)RCb@km2wWMdCWq7eGUHO^=zUcFzPkDzskpWcj&r+ zh`ifi9WdSX>4+s!Vc~KR!OmLm6^aWBg)f5mTtJlGLAlVb1)$U;CJqj_>Pjzs)^JZj zN9S%Q%sAuFoR8Yt*t`eF6uo1PCpt;7TJa9YIEhi+&~^6gZPCi`fqMU0)l$Wpy#Y%< zPsk7R$SWU;dJsr1AMr2@u3s8>LAQL;E|YI?MUUY15{_Y^q2@?31F?8H2HeDdUG|F~ zF-WAQ?8D1+yuUm1Is7jm`@MZle${S-)+LE0PvTbu*49+BLdvtJi>za-^B6|F=IUAA*RzZh}E+&*5ZQ*WcjF9*;ms$^C|(a zFME${`xxVns`g#EXDu*DEn=agBUt-l$M)3b|D)+D!>Y{M^`;v|T0l?~0Ria}2}w!m zkXGq#ltvLH1Oe%m?i7$#K)OLfy1P5hv*(;MKW4sb#t+yp>wV&`1-;@kbZ;h@DChy^ zx167w`v!go;j`i8UE%wp>*#cn;L<4Z07PL4HgS2_HzaV-QRP#wlX-0R{+zTqZy1oe z$a457^TV2C1ilbtP|$T*t76g_7RSeP(GA!!a6qkiUrke!7=-!z010@ScE^!I?-_Z< zMBC4HYvr*o93BCiiOTL9XL~R<+QK@9hFQj4!CKR6eCO6J<4O<*mE8=iw3}_lf=8*E z^_t`y{Et?ZR%5r&y&K8cbWa$ewBZ2Wy=6N;voB*&}1TVCV10Wv9f#tQZ~;6#x$U6~ z&}d_W#g~1eugYn4fC7vF^iYY~X*PJ9_QD%Qh#+Gp0vTPU_wr@uua*||_cF-VIhsKsr$rz;2QF&lhiN%2)`NMKh1bt?zwTdy)G3g7yh^a0@)0U4K|RO zD~FULyQyqDG;3(&Ls)Dl0>M3i!oJ-G>gpcMqo&#~U%msIsq|$i?Fs%Z5T5YxR7Apz zlOvW4633}oP;^MHa-JJEC>iUks{UeySOpC@ai~YhOJJV|saMasoUdm!h&o+^0S<>h zL;M5+?=C?=Ov)f1vy=LmcSrxes2n;hH}|1sRIEZz9|2d8(1NoaMMbk@(pQEe*9hY3 z#%(WClFMULOyiIX>7*Hv;PNJKkOut?nmG4n#PY(Tcc}Vfxw25<9pm|@r-z~ONT9Tj z_|xwiz(Z&&%gXQ?$M|&CDpv4rLWM6WhO`1S&)ZUqNMu6+5E-)%ipjq(;9J9_t4E~> zn+7sc>=3X^?Z|xmU|oQ<$DiE#IJsT?AXK9sTrTO+WH+jMq!tNZjbVN-iN z#M?OD)UBh}KA{Olc-4BT2YT0TPwOKvnGu8+=)0KVcld0m#Q=gU%68d7TN@oYTAqHg zcf@-lkY5Ha>sx#K`|s9qCst;JPu`WnB85i1oV1afA z5{%05lf1UJfWOM(;jm5e?@4g4+-G62q9-YZ)%_BgK=^MI_1vK$E>7nRxYVU)uCV&y zsi`}IKaMcyrsRmx4W-s$0@#9yffp7=Ds)1r(uEwSy|X=__hI%Cz6_Soe#ETwYvxoHh5-4qgS-=Mo~zIck9G*Kl{*q)=RpMWlg8JFd@9)hc@A}JgcZSD!Kk?2H5sTD% ze~*`Xx)S&P@w&Qf?#U`BD1ap-esz2M@gq!&L5`3np}TjRYP8EtQh~nC_YI}ntAtXV zIf4`}oS>@t%SJI%(uLz+%#%=?8JkWU zx%(tje=-5!+-m1wRrSG%YNKyJ=rm*{cv`{4?-|a(o3}X>J?bb*POVjD(%p!HhMA9A z0E_9B*G!{tsDn;cg?U-4p%;j#TS1%H1QoUtL>b|L_eziaIREdXz=TAJBw$bbbO5Gv zLE?n(Y>eV~FZ&wKrxzQ+HDhsle*X9^1RCm@CHIxKlugUeeI@$%8^N!hX@B;etMkq3 zn6qC)rcdwHjHed$O>|9wd7_k0a>oM$k05yCksjVn3&b3L5?c0Xj;iSwkn{2Zz8TzO zMSHk-K;1`*1&w=o1gc^f3?t;g2WoYN(mWZ5cj~;haK-dPXsaP)I6h0ZZMbx~OSawU zi^f+7yH10y^nhW{%5dIjB~0V4@71>9uO~*$=$<6+G^BJ4cwY+G!HOveeP7QW@MmVk z!?U)2Y=oUgXuu~ESB1-!Rp%rxD|2Q|>#kiZv@>k>k0xxm7?d1InJx&v6?P4u@EtR0 zpzyq7K`m}peH3q%NO9ItZ$^lS7j)gKb%X|CJvcBx0=_j$A;U%=-EdJUdUk0n_@-qh z&>QPSeAoF!L>f*VLU6ajkqFNf3@DM11M#lfbz5IXiYlnS}MJXP;$X>Me+3wd^F7N2fCNLu;qWwL69z63ff=Xf4VvdkBZduq9AOmpx9 z4KIF`2z&ABcOa1*fSbb9c*x+EAfvfnD=g%p`@=@aNP-&yt(^^awkyo%YAf5VLMCBU z7fWF)MsFYb$WBD-OVm({_VAjJbx|VP0$qFr(#9)Lki39D$ci6`AZBJry-oY5>j4g# zG=M|4IXg3x((w4{PlgKM9PJPk>aecKA!~;*u<~GrH|>Oo0GbYS4RiD**cOS20&YD2 z30*NBB;k`U3j^r&^a0oCfpCEgZjaYch*$dn@Ah;U22(8ZNfZM#=Lws0qa*OChJ{KcY<+>m+)nN8GUuKw|92~c69B5Y-Hbbek7&ITIXvj zEW@|^hld0;p#G-nTJNjP`7k(%Zyh137*Wc7`zby9XzM|NLR*GmPpR?!I)($NDwwA^ zTxI*Ur!f5Er=+UVOaDvpWpv%SMR)y)4+lQJ&Gyz7TYS7yME%l_DY;BX%T?4W)_Q8Qa#xE#11<-z}IwMIAbU4<{jg1S@!qfy{x`uFw87{ET zJkJhr#sTrYQN1=WIP55@Nv#Di>J8|lF_@dT5ce%IYA=Ju?1sEzG1ZB?b#XtBTlB;o z5Id(>kNXLT(Ow3a*jQU%3WVg zDM{$`BgiZif89G@inqP`0W)mTqJXZNe+5Y*zN;1>< zney+Z36W7{FCcQ(6rn}(f^QileoCgM*Hu#Do`nosZF= zkW}2>?+5MPD6~mCiOp?tX0vB9(%W!YGzxTRgzRQS@mO*}8-B3*@8_`;&G6+7PD3eB z+4)4?N%~Hn zQbMSPs!y=OqM6lYbIdK*Y?n)TPr6cL=cR)!uxMibDc?YUkZ|k7Dav9GoZ&xt%?B0? zHQHVn==b_|+0lVyrVe~y=ddkpbVM>aI>OGk`A99>?h#kHlQM%iyZ70FIT3n;q=bZ8 zNA%5_o zN|2(@?65gVl+oW*oN&tHF6$iDS9cneyPb|h$8|>Cy7b|8zSeVCM2pRdi15f=e#qHZ(U5Z|->jRDmYKX;o(*#b14bZx%fke3M z$ID8q-L<|{@zBKDj+-C-#B<~N>#K7EuzX>nIU%O{=ysoYaqm(FIlm^4 z3G%hO7n8SMl)H?6fk|+i5p8-$JT(BtYK_}*$+Yd^%C4ZA4!$VXxWH<-(#bn0vGmZ* zN?5q8N2B49P&FH46ktzx!C!2fUG-WJK&Po z{))8sXlP1Z>-PF4EFg!J&KtM6&0IkVt?fgO2*i06vm-%emuapY?U}}Ycet@EH@qyQ zF9un8YnccPdHXWt zqIsQMTt<@L_AmbK9~=lo<4@CoDh3s4ip3u_OEGaT8~RUZ0-q9j*Ij$Ca$Z47zUnzp z+xLEjWa-v3eRyUx4jrBb)Nhvll8{?G;42=0-=BPU*i3NMpo58!j@=1@T0YLr&j+EJ zA#{gi`ZK#EJ`TR`66KHE7?Wn={~TAzJVVl?`*uT~xc-wpBF(V*NQ@qGe~LLfs6|6* zlZvw36RKBG_D*P~UnSBD(oE~sy&awb)IZsABxr2+`!{s+cnExo7laeO1t%D9@U2KMggVZX%Gs7G}ly)Sm9FTs4A+-O{=o;On6aCMefse2j*A*Fc0 zkLta5YN^3pV2GEfk#e`hiDw_m%IVq3 z3A&GBgV*IqA&9+v0=O%@HdT`*A_B2t)AINYyZE(c4m#T2! zs+!&88Z0dR?}Vl?=&idG!SY@>Bj(t&A*U(ZyCVZ)G0fvc*UfR~e~>?SW;)-uo;Tn) z!-Zwo0<>BkhEe&8)Mv_2bUMO?Hdp=JVYJ}0g3+9hYy^lXPrzx*vvr|%|J^bW-1}f#p;dVPoT+h! zXNiQ|G00LUnfdw$b~7bPDYK*C+OQg$Qc(!wjS$M)?C&E%NIR!*zxalkgnvbaE`@kT znGD4N;fPIo5HauS^WFiQyj^<|1QdeESeYj0&gZ|&xY5J|MyXG)d3L|sFreL*y2&Bgo09M)^2$*`- zYO1Oxk?Y-arwjNML4bx$bZG?6Sq&^5_*gf)k>isHyepg8DEl(EJ^#d+MN^`fYjkg{ z`lfMrJgwtp4mg=2)-@!R!!8$SIV7Im%;j3CW;^jaFIIO z7w7;vYS#>C(q#)08C-V@T^S~ovr^OLp_3ax?nG7+7@O3qPgApsmVKBBHz4|^?#GW~ z8|oZDIa+2x^H9mf&FvANIH`iO1Pj0&9w=7HCH?pm?1tUB>Uo&$r0vLi4GN(Nj_*M+ z6N2sabpFS**ZRuL%nrko*pwnU^fw%{ziACgm+!ZcQlbuQm-|7fF1-$JN+hU5E%+%kz?(jY z=X@v^#eA6DLGg*c4sz(+K`Lel80I%V0fDg-;DRvVk?Mi;o$nM(J<;3nltw_5w1oTN znHV|pqKDMj*s)G%+)E?9gn`EZ6$^Bk6-YcEEhPu_4<(|tZ0I0E^%n_BXxop0^(=-f z8;d5&*uj>XUE!61qL7;1glVq%+i631WGrSca#S+#Kl)m~M$oMwQg+45o~(ZiL)*WA z)=OY)d?oVyn;_qNW+UkZyz%w9xxLp=v<#w{)z_Ay*-beIrL=sZz2U)KHRUJJW~^Ts z8_PnT9XY5s?LFWHyZf3R`Hw4r*y$PdtK4sGz#hj=$>be96g2qkGmK}9!d@3KpQpj@ zEDZOv)B#m~YG!JBj5>+ zV2HlMw&?=IfVG)Pt6Yk$D+ ztO+u=US<>qUsWfeRewOE0_eg9h9VKM35hupAG~rUM68*{$bn33xxrdj*Ia?s--Z@% zbUD;}7vs3z_Zx%sJ!$jH5JZj% z1@#R7V69b|CdS_deul?AaXfi?&}fpf(~lnrNsA7{dm=kkWuMOA#{%nuthA)00EplJ z0tnH;3k;MkNI)I7m+XZ@d2AErwJg6DY9`!ZY>WJjZ-|V{{|I_h4<80wW8=N?*48CS zxGv)H-|=yMm~aToe*9=Sda$Fu#R~w2vz(n>G2fat_%4f!8m``cm~{SpaE-E5byN=O zx>PvNQNW0Q1!g=RBsD&QEe-2r>DsL-cviTny(4aVPSz= zbrMeGGvKsdeKBP5q4f!(y0`W_K(<>mH;34xrScPkZCE#BMv*n9-l{MBifwoYr}Xd1 zYFU85zo{vG$^!66rGNJ|{hoYhdx!cm6Nh}ee}PaTVO1|R95x&+_d@Fabtma;VvPYb z$8X}1bs%c32cf$YRFy8Ry|7oPD?XXeQrYEztU?;oJ3wY{35(0(QePEBfb4@IYs62K z!0Yof;)1&*nG%ma4c&^==W0oj#6>M1f6@3z<$K`3FlrOZKGk+AE3^w*SQY2t)i~*b zgp^3ohm_oUAe&HHQsSQU@5-14U;Nv1`8aO#qXYp*218bl^UWXdn(FfNc1l#x?c88* z8ykaFbhCN$4SNZ|b>&|>k3bfFx~5m-G)6^As$LHESy$JMzx`9g!#vM|j@Au^9qFV6 zkUqm8IL2Gx6u0@DuE9=AKgwSu`XHMtkBZMkosMlYP>@S#YS~1-=6mXA;f7bT>X9u{ zA&iGGklqGUp9gP~rSY7p-q40b3eg1N9U+gCK5#l>CHwy~Q|=q-Vynb+7L@@CR)@oW z?zfSEAQ7jsJ4_XeFhr?C3bv&OAjw|vmKZ7@vC-+)H(7#t8i{QUeE`oILB!SVJuc@V5@GJzCL{s5fl8pl!= zD$_P787ghU?@1<^d`qfayh2%mGD#%>KU{JN$KmFL>pz$bF#nLYUrpFH5ZvZ`{YO^b z4`?=YkPFpESJIpm)qeRi4A(FO0pneZ?eZx!l+* z7{s^ccObPvB9oyp8g3h+dShxzuA2QK)${`{>1Rp55IA}a5Yyhs{V7}B^K4RHF%yu0 zW@xF)0IbW}?fXwtU+G>DcngLagVXL&Q;3PVRYz{uf{IVC%sbNbI?3*kcoD9m)Tivp z;&{@{+Y;w<0vc8te)CRB} z;=uMOE)97?B3uF{@^942TJ!A$xByP|z0bp}KLT;M7Wg?Pz^{+b zsG8Y@VQIOJK1P|`P1-1{S)fx9508xyW)}sB#BxAhY&O+dXmWwp1^O^#tG1=aw1uT! zH4E@p@xs<%_+j9ZtH$GOOOT=u+7aaHI(wQsp-drhO;9vz1Nl6@2F6Lqy(gHclz_0&)&2sP-Urt^ihSV38^ys0uMtOl5L1!hCJ?!dgR&y zrK=vW40?zJ@~%KHSg-c!ehbZx3Ye~UL@69uD2De=c4WveLixX87-IDiNu&zAQoYaq zIdkm*kr>Cp^Teb1$0zR-8lu&{-$aR_A!L5&70yYd5BX)*Z+`GUaAV*pNq@RRHA&Fd zin5YTJ3Z*#GaYLs6~7zihm0TWy*?OJ>c)j4szN0=cF0!FL#>MRiTgl-!BSOKeO0Jb zF4jd7xO=PnU)o5%?8{cixLHZd;Ov&5=A>Lw_pa+B)^7lzyg|aM!_D@y_1=>;^S+d(F}ONv=>4+_Sp6#D z7d4(tbIAOGv*aEu?L!b0$^*ZgEwm-@790P8DyiAgPvN5udR$&>P}e+SfKTuU^9Cj7 zf(T7J;S1o8Bz}RSO$xBuB1EzdV4|wfj@@;1Zigl?l80~`s|yOxp=RWe3S}xq7&g69 z_-hBD(kga*PQdfgf^K(D1Ll|>wIcn?#~^g|y9HA9gZQLpTM}-U=77hNmlCVPvbNb4^jPV3W;AA z&(c#M6C*pkYt=rWIN+ncu77)Hw)4+gn0|v~*lJ(uFI`}`z zFSXoH^I_PN9`2{lkz(~L!%jjc6(;vg$^T;gMqoFB!LjPb%?v_U)TJ=Sd^5C>+*TUv zbSCoNyL4hN&R)B_)dS;(3Cd1Bht33LMiO_>X>HX3+&Bv@F5kw??BqHJg@<1?`Q;g1 zrT@s?E-pkQX#2%NZu3Mo$MJfmNn$jm!ls>EG%V>52cw7X+F03g8pu(%dAYeS!l+JD z%;Vv{gPl!qrjYv>{249vx_rC%ba`xdpO3u(m>N}RA%u5ZETCV|(g?W~Bi;PY0=FF? zP3Cwg#7`{hXJUcCEykob87&edM`&yt<9T9;op@43xJYZ?t_-&}u+2 z;7?3Qu+oN%V=55YS^ETwIB+wtu;eSk7JJqF8q~adiSQ@Cg{LP?N)h{7!fvweZ#IG) zwzxSs+Iw4`4bn3R@&1M<%Qa}A9!I5M%AV8z1&^JRk57peNc2dU6KH|Rm|R|6t$xVm zeD$(30npTcz?Y38dxgL2-wT9r-~CkvBfAI+b2e07g#ou@6yvnmsCc37#noLEX7ud6 zFPx2^3dz+?bo-hT(0!hjp$4M$QDzk7dR1yhEsDmMw8b^nrst45HSwW4VWOiu6~J*Q z{CffdiLqC38$$%dyl4^GF~qri3hZvw@i8%G-MoHE zB=31LGCGeXG^I6GjFa{56*O<%7OdNyK+&cjlaRNa{d)l~NzxB^g^Gxj?#KP&*y^)LDfoqt_oE}rN~c5MZYW=? z1b*nP4=n04Ogul+dsUKuQmo3el5ZOlK6~NcN|4OWt9zla`SKu9Wun#fAe^|Ci9%)~ zTKj77@eSeBL^A^jHNOEDQB-@oB+aT1U%|I*1~_p4>TJEp1_D$}5d7sgHh=&9>}wQv zWRUyGGVL;VAQQt#d@xtNUb;J>De~lfFSRsv%Sqq?^K3%K&yO>Ns!zURb08w58Qs&+ zo+VUNI4{5mkp=IJ0Ha*upVnrrbFeb=dw>*c9M(zOqpg1vB>?Jrzm_Jx!d!yld|L!o zuxbhfq+lf@S4XKrse|=V(C#>|pZN*E00-l4Fy6c>(ytrGCt5ua zF-|ky|9yk+sfRSJK=(%1F01KF9I`n~))DEuXw#prR9RkI>4SAqw(QZ8JnWsxU`@GJ zhHqNIV=;u?ZC>zPu|6j~B#4|rF`z; zQsP;fKg=|iYt#Ay1rDF*saK*jpk=4-?!8M2dgzI3Ep#Nc^X+U*9fj}mN}H$5pRimE zeKhqcBoDeG??GV7DtaP`*oIg6TN1y0^CJM~J0cjkAG}KVjc-mF63xt2bCOv3jX*Dc zvE}j;N;N>O9s5fgwo;b9WK#CF{we-}N6_ytHqUgs9@R?Y(X9YJ)KYHq?<^X4ZI+8$ zEZ(>unjvr!Phk8BXm&j=4o3|>;saY$P;>c=y!RS&35W&ldcekv4A)>bmxBK5@rjLD zSu$PB9~11u{K~Yw5ZFG@IylHR(KFvZ{BQu`o9ZutQ)`1s>E^AUXiZcz@&>mn*!^-2 z+_20(#ilG+aPhF&6W`=BwfKzRF|SNk`vvh>W>qU;Xihk`KZ%T4Z3X&l2>Cle*EykG zGishYa_LbkA@|AwFzFM*?~&6F=UW{1c(+>qP~-0NX?7Gfp$LfSA0p0XL0>KDvIOQXeeZzM{L)h+q{OC%jL}n z6OGP$#-1=xHY7ryFoj<|LF{yr2G2qKR>(nPjGv}dTor>(H8UiQXLZO%411{-Kii|Q zSXz6}<0sIYN?bhq5G&>v>U|T&`zx<{?gof|11PpKljT$xYOz3DDW2P*f4EZ^4a*pP8Y z>;~elG+s}nh@oY?XOm+0XSlx&m4m@$rO(I_hE7ejJdI6bmoJhw**BzbEQ1s2>HYh! z`9*!Chba~qNHeiZjVHL`&{&dt1kYbuDDKkO>+aTN4+qVJDnva)$B^f!_d~_*41DH? z%^XXE*CPk>W81X%_2mi@%IRWZ)r%DO3I^wXzPiJLQ12l+h9{l-o&?-sd zp{A|&o1;JT-#~S945qc2%<>%+%FXvP$Sa#4#0-8&59M8E6t135v3mXbJIWSUN^5Vm zcqisn04jPTA19{@w^<*Z+e%+5aVb|`Tupt>+j=OJskI!|J2Opr5I-Qm&*fCkO7B;# z4DmF*b9=-xvd|dqi$^hj3yj;n|JcBp378~!9`|8 znuWTnRJ63)=sP`T>2FRCd~{WBY98xL4b(Ag{?aw%=HR*E!{_xervS#Cj^)V0UHmHr zv-hwOJ!^f)a`6)Q+90Hy1(bZvMP(i9j|+tSz9WAwWQzSfl61fu6u_Xf(yfie7`bhw zOQ9O`tx40O6&>H=C%&*s7Ud#$QI&5xP1@ln!;i}cZJad*Bos{#VLSZ;A@jkOa0yQ6 z%WcRhp0p8Kdky5Qvx!m$Q#@AIx5AfE5V z#LxUuhBfXfZw$Pb{D4-qn903}I0mzpKHQb?)(GCx4#>`1oq+P3z5f79;jW)OeCY^e zkw1Wjc1lqo&y~v{qA_^Xj>2m-j zvLg$jBS(Nb(#@kUMXbzXB;Q>{ukN~Xxhnk(0u;*`m!_FWsV|v~ME7uN?beGKPxlRIE=chmo86#W}1yyY!7}ZJNPUUoypf+TxbAavnYEG3dwraEU5r2R98_ zG$j<`GULOCJa>f7RZZ51yb)I&$Zc=$%d-k)P$nuNwKE}!iF#iJ_1_$xnzFY&T>`b3 z8%Tn%_Td%}25BiNvhek@N?mOd1TITa4yhu!7A}c!5rZvGXh0K3G>FC3so5BN0vrB#mdH z;-*sQn~ZfNgLI#vtQxK?g)PcTqcG6yQh|Bou{gWw1V^uXwBS)ZY%;WQfGsq_jwjTgDKfUZ1=sE5 zI>Aci4UW+g_@(L!0Q>y}-V(;@^(*`(q#98pPorQEJUu3)%xZzbfq`}r>wQ-+O`myP z9**YOE6X5B<^qF;JA7q}!ldE^$KkI{fwSp^>0&tWmLNU2k?^Q0YG{eVBqQEQef^n< zhs`ZvbnlT)joQ!VdmpO{01`uU7GW%4A zyiXJSffBIP-+cVI$bgF5>}KXVugSMNlc6x}<0{I@IZ90QoAiH3VbUY|!$Z`{lMz4l zF5my$r*a=-F;(>VrPAzTd-DYmEuzOlmuUEIxH0`(B4d z;>o=q)mzYu)H^ymA1#2=pA?q1t*x=LELHd-g(l7{!%^4xOYou0kT=(eS3QHgzWrVx z{rIBr457dZn8T)dMUetR!>&XSsTBTX6r-63?e6B%`uh1gVA2*GX))TtoQ^$e=nnSj zgP2w7723M;ZiSo1WS7zSey=pLV5np6mE+3(K*c-W)$;L`CAQNk$?vrMbSI6{OiPYg)kGId{F&se1vXb5 zr@9HRa=+f|AMKKRGou5_yPy^VGpN#Xw|T87OaNq%>fyDJL*5yH$7emh;9tq*rc6@B zlll;2uPa|#VU1{-s7ZIC;_*OJ+(*l7_|Y54ZB`QT_CbMTwUzE{e3+G@HDPT&EP0dh7onYbAkP2$tvPxeVf^mJfb{l(^^+#Y`agIJBPjq` zZv_mISAsSMhc(@)a-j10XgTwv0Nu;f96_!F$tG$IezT>M(DuTiP1^)KHFOCSsR#sr zWcvCi_tPd<)Y_5~dm+vi=hcBXsx~N2085U*1q6xUXVU^K>m&^Y^hei`S+raxq z*#A^XW@$3RUD)<4aBj8!X-PopcyaOZH$DXbkeKQXZ~mzH;7WqWF?h)`z&!p_ z7;a1V#}N+e^CRjoqvAC(`yTeXLZCi8$spl!m@H6I_*4CU*T8I=Un{J8+8GVHlab$@ z%+ve@Heu51STelp#hK+*Pdo-efGE-M9T0p>*y8yyQn;=l4b_@RO1h88< z+F;+r0lt81iYA00GM-AIHWsIGp!{iE?L!R5oTKM(ZPp>AXplf2*$Ke9SIPj+^+AWGPnDc)P z$p0eLo%Z|=eokLAZwkx2`R}^t>JyCIvyu3=man={lg7rz{(;Y}MiZ)&qmazk&F#^m z=_POzJ7WbDLY13=zWYXrdY`fj#9o4y@Tz#31vfNYM?#Rpx#g3UxY{pVOMq=n02nqk z@;3(b^-sXL=l+A?jpJD&((C~8wz+Z8j(fKJ%)OiOYvXq<)J2om3S3p@KmWF16f&`! zl69H0Wo7tEtA%0Uj^3SnXM#@Omx5m*Wj2d&keU7dp&Q|MlMiib8Uj`Ctik)RC)e4Q zzwVBsa0#{wNf1g7pw}NlhQ@7>jyghe3P;AG_(9@&jhFRG&Hy_lW4pVxiOJCbTx^{R zyy8R9!8xbFBC87uWvNT$zRO;cloCQ4CLJgiB4As}297Q+>0F{?zHD|GAe>@~d;J`mT}!6;I;dH1eY zjhK9TC#G8eyx-S}Wv1bT@4`;b9z^7Hp7X{0KvYYo{~S>kar;1TaexwJ`aQ>pGs+Bp zm|lfM!3u9sR@@t^*lgGnVrK4`g*=)9fag^9LDBLCfDnx>5QkL4yTdiVsV=|qY!
Tk+SCv_zauZ4%Ogv4b%m;zueKTc6jq< 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 literal 0 HcmV?d00001 diff --git a/static/img/default_profile.jpg b/static/img/default_profile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76443820190f1fb212c7dfae203c2fc3f6bb592f GIT binary patch literal 4426 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 RJqMw-QT9F=4dD6o{{hgl+3ElQ literal 0 HcmV?d00001 diff --git a/static/img/foods.png b/static/img/foods.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e0d1dcbf749708e03ae569377bb208d4ad18ee GIT binary patch literal 87570 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 L{)%;n$t(CDcrq0V literal 0 HcmV?d00001 diff --git a/static/src/assets/img/left-arrow.svg b/static/src/assets/img/left-arrow.svg new file mode 100644 index 0000000..27edffe --- /dev/null +++ b/static/src/assets/img/left-arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/templates/base_structure/elements/header.html b/templates/base_structure/elements/header.html index be30c37..5bb8cdc 100644 --- a/templates/base_structure/elements/header.html +++ b/templates/base_structure/elements/header.html @@ -39,7 +39,7 @@ - {% endcomment %} - - - - - @@ -122,14 +122,14 @@ --> - diff --git a/templates/base_structure/layout/base_template.html b/templates/base_structure/layout/base_template.html index 279274e..e571002 100644 --- a/templates/base_structure/layout/base_template.html +++ b/templates/base_structure/layout/base_template.html @@ -37,7 +37,7 @@ {% endcomment %} - {% include "base_structure/elements/header.html" with user=user %} + {% include "base_structure/elements/header.html" with user=request.user %} diff --git a/templates/base_structure/layout/dashboard.html b/templates/base_structure/layout/dashboard.html index 172fc24..1c29f3f 100644 --- a/templates/base_structure/layout/dashboard.html +++ b/templates/base_structure/layout/dashboard.html @@ -1,60 +1,70 @@ {% extends 'base_structure/layout/base_template.html' %} {% load static %} -{% block stylesheet %}{% endblock %} +{% block stylesheet %} +{% include "cdn_through_html/apexchart_cdn_css.html" %} +{% endblock %} {% block content %}
-
-
-
-
-
-
Active User
+
+
+
+
+
+
No of Active Users
+

{{active_user_count}}

-
- -
- -
-

4578

+
+
-
-
- -
-
-
-
-
-
Total User
+
+
+
+
+
No of Total Users
+

{{total_user_count}}

-
- -
- -
-

545454

+
+
-
-
+
+ +
+
+
+
+
Users Graph
+
+
+ +
+
+
+
+
+
-
-
Unique Visitors
+
+
Activity Graph
+
-
+
+
@@ -63,4 +73,178 @@ {% endblock content %} -{% block javascript %}{% endblock %} \ No newline at end of file +{% block javascript %} +{% include "cdn_through_html/apexchart_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/cdn_through_html/apexchart_cdn_css.html b/templates/cdn_through_html/apexchart_cdn_css.html new file mode 100644 index 0000000..ccf813b --- /dev/null +++ b/templates/cdn_through_html/apexchart_cdn_css.html @@ -0,0 +1,3 @@ +{%load static%} + + diff --git a/templates/cdn_through_html/apexchart_cdn_js.html b/templates/cdn_through_html/apexchart_cdn_js.html new file mode 100644 index 0000000..088ff67 --- /dev/null +++ b/templates/cdn_through_html/apexchart_cdn_js.html @@ -0,0 +1,3 @@ +{% load static%} + + \ No newline at end of file diff --git a/templates/module_activity/base_add.html b/templates/module_activity/base_add.html new file mode 100644 index 0000000..0860088 --- /dev/null +++ b/templates/module_activity/base_add.html @@ -0,0 +1,45 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% endblock %} + +{% block content %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'base_structure/includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_activity/chronic_condition_archive_list.html b/templates/module_activity/chronic_condition_archive_list.html new file mode 100644 index 0000000..3ee6ce4 --- /dev/null +++ b/templates/module_activity/chronic_condition_archive_list.html @@ -0,0 +1,229 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/switches_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} + +{% endblock %} + +{% block content %} + + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + +{% include "cdn_through_html/datatable_cdn_js.html" %} +{% include "cdn_through_html/datatable_button_cdn_js.html" %} +{% include "cdn_through_html/sweetalert2_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_activity/chronic_conditon_list.html b/templates/module_activity/chronic_conditon_list.html index 2212b2b..af28bfc 100644 --- a/templates/module_activity/chronic_conditon_list.html +++ b/templates/module_activity/chronic_conditon_list.html @@ -24,7 +24,7 @@ {% endcomment %} {% comment %} Add Category {% endcomment %} - {% comment %} Add User {% endcomment %} + Add
@@ -84,8 +84,9 @@ // Define DataTable instance var dataTableInstance var actionUrl = '{% url "module_activity:chronic_condition_action" %}' -var mainUrl = '{% url "module_activity:chronic_condition_list" principal_id=principal_id%}?deleted_flag=false'; - +var mainUrl = '{% url "module_activity:chronic_condition_list" principal_id=principal_id%}?deleted_flag=False'; +var editUrl = "{% url 'module_activity:chronic_condition_edit' principal_id=principal_id pk=0 %}" +var viewArchiveUrl = "{% url 'module_activity:chronic_condition_archive' principal_id=principal_id %}" // Entry point $(document).ready(function() { @@ -133,7 +134,7 @@ function initializeDataTable(dataTableInstance, mainUrl) { className: "btn btn-dark ", action: function () { // Add your action here, e.g., redirect to archive page - window.location.href = '/archive'; + window.location.href = viewArchiveUrl; } } ], @@ -183,7 +184,7 @@ function renderActions(data, type, row) {
`; } diff --git a/templates/module_activity/intolerance_archive_list.html b/templates/module_activity/intolerance_archive_list.html new file mode 100644 index 0000000..7f6c405 --- /dev/null +++ b/templates/module_activity/intolerance_archive_list.html @@ -0,0 +1,229 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/switches_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} + +{% endblock %} + +{% block content %} + + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + +{% include "cdn_through_html/datatable_cdn_js.html" %} +{% include "cdn_through_html/datatable_button_cdn_js.html" %} +{% include "cdn_through_html/sweetalert2_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_activity/intolerance_list.html b/templates/module_activity/intolerance_list.html index 417b865..3e7225b 100644 --- a/templates/module_activity/intolerance_list.html +++ b/templates/module_activity/intolerance_list.html @@ -24,7 +24,7 @@ {% endcomment %} {% comment %} Add Category {% endcomment %} - {% comment %} Add User {% endcomment %} + Add
@@ -84,19 +84,20 @@ // Define DataTable instance var dataTableInstance var actionUrl = '{% url "module_activity:intolerance_action" %}' -var mainUrl = '{% url "module_activity:intolerance_list" principal_id=principal_id%}?deleted_flag=false'; - +var mainUrl = '{% url "module_activity:intolerance_list" principal_id=principal_id%}?deleted_flag=False'; +var editUrl = "{% url 'module_activity:intolerance_edit' principal_id=principal_id pk=0 %}" +var viewArchiveUrl = "{% url 'module_activity:intolerance_archive' principal_id=principal_id %}" // Entry point $(document).ready(function() { - - dataTableInstance = initializeDataTable(dataTableInstance, mainUrl); + tableName = $('#table') + dataTableInstance = initializeDataTable(tableName, mainUrl); editClickEvent(); activeSwitchEventListener(); }); // Function to initialize DataTable -function initializeDataTable(dataTableInstance, mainUrl) { - return $('#table').DataTable({ +function initializeDataTable(tableName, mainUrl) { + return tableName.DataTable({ processing: true, serverSide: true, ajax: { @@ -133,7 +134,7 @@ function initializeDataTable(dataTableInstance, mainUrl) { className: "btn btn-dark ", action: function () { // Add your action here, e.g., redirect to archive page - window.location.href = '/archive'; + window.location.href = viewArchiveUrl; } } ], @@ -166,9 +167,7 @@ function renderCheckbox(data, type, row) { // Render switch function renderSwitch(data, type, row) { - console.log("data is ", data, "type is", typeof(data)) var checkedAttribute = data.toLowerCase() === 'true' ? 'checked' : ''; - console.log("check attribute", + checkedAttribute) var switchHTML = '
'; switchHTML += ''; switchHTML += '
'; @@ -183,7 +182,7 @@ function renderActions(data, type, row) {
`; } @@ -194,7 +193,7 @@ function initCompleteCallback() { // Add event listener for checkbox change $('body').on('change', 'input[type="checkbox"]', function () { - var checkedCount = $('#table tbody input.archive-checkbox:checked').length; + var checkedCount = $('tbody input.archive-checkbox:checked').length; var archiveButton = $('.buttons-archive'); archiveButton.toggle(checkedCount > 0); }); @@ -216,7 +215,7 @@ function archiveAction() { return; } // Get the IDs of the checked checkboxes - var userIds = checkedCheckboxes.map(function() { + var ids = checkedCheckboxes.map(function() { return $(this).val(); }).get(); // Perform archive action with the collected user IDs @@ -236,7 +235,7 @@ function archiveAction() { type: 'POST', data: { action: "archive", - ids: userIds, + ids: ids, csrfmiddlewaretoken: '{{csrf_token}}' }, success: function(response) { @@ -306,14 +305,6 @@ function activeSwitchEventListener() { } -// Function to handle click event for edit button -function editClickEvent() { - $('body').on('click', '.edit', function(){ - var id =$(this).data('id'); - console.log('Editing user with Id:', id); - }); -} - {% endblock %} \ No newline at end of file diff --git a/templates/module_activity/past_treatment_archive_list.html b/templates/module_activity/past_treatment_archive_list.html new file mode 100644 index 0000000..22ace35 --- /dev/null +++ b/templates/module_activity/past_treatment_archive_list.html @@ -0,0 +1,229 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/switches_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} + +{% endblock %} + +{% block content %} + + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + +{% include "cdn_through_html/datatable_cdn_js.html" %} +{% include "cdn_through_html/datatable_button_cdn_js.html" %} +{% include "cdn_through_html/sweetalert2_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_activity/past_treatment_list.html b/templates/module_activity/past_treatment_list.html index 4a4e4b3..762e073 100644 --- a/templates/module_activity/past_treatment_list.html +++ b/templates/module_activity/past_treatment_list.html @@ -24,7 +24,7 @@ {% endcomment %} {% comment %} Add Category {% endcomment %} - {% comment %} Add User {% endcomment %} + Add
@@ -46,9 +46,9 @@ # User Symptoms + colspan="1" style="width: 44.2344px;">User Past Treatment For how long have you been experiencing this Symptoms + colspan="1" style="width: 44.2344px;">Treatment date Active
`; } diff --git a/templates/module_activity/report_view.html b/templates/module_activity/report_view.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/module_activity/symptoms_archive_list.html b/templates/module_activity/symptoms_archive_list.html new file mode 100644 index 0000000..a792fe9 --- /dev/null +++ b/templates/module_activity/symptoms_archive_list.html @@ -0,0 +1,229 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/switches_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} + +{% endblock %} + +{% block content %} + + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + +{% include "cdn_through_html/datatable_cdn_js.html" %} +{% include "cdn_through_html/datatable_button_cdn_js.html" %} +{% include "cdn_through_html/sweetalert2_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_activity/symptoms_list.html b/templates/module_activity/symptoms_list.html index 06cc806..4c08e46 100644 --- a/templates/module_activity/symptoms_list.html +++ b/templates/module_activity/symptoms_list.html @@ -24,7 +24,7 @@ {% endcomment %} {% comment %} Add Category {% endcomment %} - {% comment %} Add User {% endcomment %} + Add
@@ -84,7 +84,9 @@ // Define DataTable instance var dataTableInstance var actionUrl = '{% url "module_activity:symptoms_action" %}' -var mainUrl = '{% url "module_activity:symptoms_list" principal_id=principal_id%}?deleted_flag=false'; +var mainUrl = '{% url "module_activity:symptoms_list" principal_id=principal_id%}?deleted_flag=False'; +var editUrl = "{% url 'module_activity:symptoms_edit' principal_id=principal_id pk=0 %}" +var viewArchiveUrl = "{% url 'module_activity:symptoms_archive' principal_id=principal_id %}" // Entry point $(document).ready(function() { @@ -132,8 +134,7 @@ function initializeDataTable(dataTableInstance, mainUrl) { text: 'View Archive List', className: "btn btn-dark ", action: function () { - // Add your action here, e.g., redirect to archive page - window.location.href = '/archive'; + window.location.href = viewArchiveUrl; } } ], @@ -183,7 +184,7 @@ function renderActions(data, type, row) {
`; } diff --git a/templates/module_auth/email_template.html b/templates/module_auth/email_template.html index 68d3985..4f2e214 100644 --- a/templates/module_auth/email_template.html +++ b/templates/module_auth/email_template.html @@ -1,13 +1,34 @@ - - Password Reset + + Password Reset + -

Need to reset your password?

-

Use your secret code:

+
+

Hello {{name}},

+

It looks like you've requested a password reset for your account.

+

To reset your password, please use the following secret code:

{{ code }}

-

If you did not forget your password, you can ignore this email.

+

If you didn't request a password reset, you can safely ignore this email.

+

Thank you,

+

The Support Team

+
- + \ No newline at end of file diff --git a/templates/module_auth/user_add.html b/templates/module_auth/user_add.html new file mode 100644 index 0000000..b55920b --- /dev/null +++ b/templates/module_auth/user_add.html @@ -0,0 +1,45 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% endblock %} + +{% block content %} + +
+
+ +
+
+
+
+ +
+ {% csrf_token %} + {% include 'base_structure/includes/dynamic_template_form.html' with form=form %} +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_auth/user_view.html b/templates/module_auth/user_view.html index 825260d..c1cd7ee 100644 --- a/templates/module_auth/user_view.html +++ b/templates/module_auth/user_view.html @@ -10,6 +10,13 @@ {% include "cdn_through_html/switches_cdn_css.html" %} {% include "cdn_through_html/sweetalert2_cdn_css.html" %} + {% endblock %} {% block content %} @@ -204,6 +211,21 @@ @@ -237,10 +259,13 @@ var mealUrl = "{% url 'module_activity:meal_detail' pk=0 %}" var medicationUrl = "{% url 'module_activity:medication_detail' pk=0 %}" var bowelUrl = "{% url 'module_activity:bowel_detail' pk=0 %}" var symptomUrl = "{% url 'module_activity:meal_symptom_detail' pk=0 %}" +var reportUrl = "{% url 'module_activity:report_data' principal_id=obj.id %}?date_range=7" + // Entry point $(document).ready(function() { dataTableInstance = initializeDataTable(dataTableInstance, mainUrl); + getReportData(); }); // Function to initialize DataTable @@ -333,5 +358,133 @@ function reloadDataTable() { } +function getReportData(timeRange){ + var url = timeRange ? reportUrl.replace("7", timeRange) : reportUrl + $('#pills-tab3Content').empty() + $.ajax({ + url: url, + type: 'GET', + success: function(response) { + console.log("reposne is ", response); + if (response.status == 200){ + setReportContent(response.data) + } + if (response.status == 204){ + console.log(response.message) + const errorCard = $('
'); + const title = $('
').text('Error'); + const message = $('

').text(response.message); + + errorCard.append(title); + errorCard.append(message); + + $('#pills-tab3Content').append(errorCard); + + } + }, + error: function(response) { + + } + }); +} + +function setReportContent(data) { + // Clear previous content + $('#pills-tab3Content').empty(); + + // Foods to Avoid and Bowel Report + if (data.food_avoid || data.highest_stool) { + const section = $('
'); + + if (data.food_avoid) { + createFoodToAvoid(section, data.food_avoid); + } + + if (data.highest_stool) { + createBowelReport(section, data.highest_stool); + } + + $('#pills-tab3Content').append(section); + } + + const tableSection = $('
'); + // Same Foods to Avoid + if (data.same_food_avoid) { + createTable('Same Foods to Avoid', tableSection, data.same_food_avoid.food); + } + + // Meal Symptoms Recorded + if (data.symptoms_frequency) { + createTable('Meal Symptoms Recorded', tableSection, data.symptoms_frequency); + } + + // Recorded Stool Type + if (data.stool_type) { + createTable('Recorded Stool Type', tableSection, data.stool_type); + } + $('#pills-tab3Content').append(tableSection); +} + +function createFoodToAvoid(parent, content) { + const col = $('
'); + const title = $('
').text('Foods to Avoid'); + const img = $('').attr('src', "{% static 'img/foods.png' %}"); + const h4 = $('

').text(content); + const h6 = $('
').html( + 'Based on the Symptoms added within the last 7 days, ' + content + ' should be avoided.' + ); + + col.append(title); + col.append(img); + col.append(h4); + col.append(h6); + + parent.append(col); +} + +function createBowelReport(parent, content) { + const col = $('
'); + const title = $('
').text('Bowel Report'); + const img = $('').attr('src', "{% static 'img/bowel.png' %}"); + const h4 = $('

').text(content); + const h6 = $('
').text('Your most recorded stool type is ' + content); + + col.append(title); + col.append(img); + col.append(h4); + col.append(h6); + + parent.append(col); +} + +function createTable(title, parent, data) { + const col = $('
'); + const table = $('
'); + const thead = $(''); + const tbody = $(''); + const headerRow = $(''); + const titleTh = $('').text(title); + + headerRow.append(titleTh); + thead.append(headerRow); + + for (const [key, value] of Object.entries(data)) { + const row = $(''); + const keyTd = $('').text(key); + const valueTd = $('').text(value); + + row.append(keyTd); + row.append(valueTd); + + tbody.append(row); + } + col.append(table) + table.append(thead); + table.append(tbody); + + parent.append(col) +} + + {% endblock %} \ No newline at end of file diff --git a/templates/module_auth/users_archive_list.html b/templates/module_auth/users_archive_list.html new file mode 100644 index 0000000..fe2f515 --- /dev/null +++ b/templates/module_auth/users_archive_list.html @@ -0,0 +1,257 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + +{% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/switches_cdn_css.html" %} +{% include "cdn_through_html/sweetalert2_cdn_css.html" %} + +{% endblock %} + +{% block content %} + + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock content %} + +{% block javascript %} + +{% include "cdn_through_html/datatable_cdn_js.html" %} +{% include "cdn_through_html/datatable_button_cdn_js.html" %} +{% include "cdn_through_html/sweetalert2_cdn_js.html" %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/module_auth/users_list.html b/templates/module_auth/users_list.html index d97486c..42123b5 100644 --- a/templates/module_auth/users_list.html +++ b/templates/module_auth/users_list.html @@ -3,6 +3,7 @@ {% block stylesheet %} {% include "cdn_through_html/datatable_cdn_css.html" %} +{% include "cdn_through_html/tabs_cdn_css.html" %} {% include "cdn_through_html/switches_cdn_css.html" %} {% include "cdn_through_html/sweetalert2_cdn_css.html" %} @@ -24,9 +25,10 @@ {% endcomment %} {% comment %} Add Category {% endcomment %} - Add User + Add User +
@@ -70,10 +72,10 @@ Active Action - - + + - +
@@ -98,24 +100,27 @@ // Define DataTable instance var dataTableInstance; +var mainUrl = "{% url 'module_auth:users_list' %}?deleted_flag=False" +var editUrl = "{% url 'module_auth:user_edit' pk=0 %}" +var actionUrl = "{% url 'module_auth:users_action' %}" +var viewUrl = '{% url "module_auth:user_view" id=0 %}'; +var viewArchiveUrl = "{% url 'module_auth:user_archive' %}" // Entry point $(document).ready(function() { - var viewUrl = '{% url "module_auth:user_view" id=0 %}'; - dataTableInstance = $('#table'); - initializeDataTable(dataTableInstance); - viewClickEvent(viewUrl); - editClickEvent(); - deleteClickEvent(); + + tableName = $('#table'); + dataTableInstance = initializeDataTable(tableName, mainUrl); + activeSwitchEventListener(); }); // Function to initialize DataTable -function initializeDataTable(dataTableInstance) { - return dataTableInstance.DataTable({ +function initializeDataTable(tableName, mainUrl) { + return tableName.DataTable({ processing: true, serverSide: true, ajax: { - url: "{% url 'module_auth:users_list'%}", + url: mainUrl, type: "GET", }, columns: [ @@ -148,7 +153,10 @@ function initializeDataTable(dataTableInstance) { { text: 'View Archive List', className: "btn btn-dark ", - action: redirectToArchive } + action: function(){ + window.location.href = viewArchiveUrl; + } + } ], oLanguage: { oPaginate: { "sPrevious": '', "sNext": '' }, @@ -166,7 +174,7 @@ function initializeDataTable(dataTableInstance) { // Function to reload the DataTable function reloadDataTable() { - dataTableInstance.Datatable().ajax.reload(); + dataTableInstance.ajax.reload(); } // Render checkbox @@ -179,10 +187,10 @@ function renderCheckbox(data, type, row) { // Render switch function renderSwitch(data, type, row) { - var checkedAttribute = data ? 'checked' : ''; + var checkedAttribute = data.toLowerCase() === 'true' ? 'checked' : ''; var switchHTML = '
'; - switchHTML += ''; - switchHTML += '
'; + switchHTML += ''; + switchHTML += '
'; return switchHTML; } @@ -194,17 +202,12 @@ function renderActions(data, type, row) { `; } -// Function to handle archive action -function archiveAction() { - window.location.href = '/archive'; -} // Function to redirect to archive @@ -238,39 +241,107 @@ function initCompleteCallback() { } -// Function to handle click event for view button -function viewClickEvent(viewUrl) { - $('body').on('click', '.view', function(){ - var id =$(this).data('id'); - window.location.href = viewUrl.replace('0', id); - console.log('Viewing user with Id:', id); - }); -} -// Function to handle click event for edit button -function editClickEvent() { - $('body').on('click', '.edit', function(){ - var id =$(this).data('id'); - console.log('Editing user with Id:', id); - }); -} - -// Function to handle click event for delete button -function deleteClickEvent() { - $('body').on('click', '.delete', function() { - var id = $(this).data('id'); - console.log('Deleting user with ID:', id); +// Function to handle archive action +function archiveAction() { + // Get all the checked checkboxes + var checkedCheckboxes = $('.archive-checkbox:checked'); + // If no checkboxes are checked, show an error message + if (checkedCheckboxes.length === 0) { Swal.fire({ - title: 'Are you sure?', - text: 'Once deleted, you will not be able to recover this user!', - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - confirmButtonText: 'Yes, delete it!' - }).then((result) => { - if (result.isConfirmed) { - console.log("success"); + title: 'No users selected', + text: 'Please select at least one user to archive.', + icon: 'error', + showConfirmButton: true + }); + return; + } + // Get the IDs of the checked checkboxes + var ids = checkedCheckboxes.map(function() { + return $(this).val(); + }).get(); + // Perform archive action with the collected user IDs + Swal.fire({ + title: 'Are you sure?', + text: 'Once archived, you will recover it from archive list!', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Yes, archive it!' + }).then((result) => { + if (result.isConfirmed) { + // Perform archive action + $.ajax({ + url: actionUrl, // Replace with your archive endpoint + type: 'POST', + data: { + action: "archive", + ids: ids, + csrfmiddlewaretoken: '{{csrf_token}}' + }, + success: function(response) { + // Show success message + Swal.fire({ + title: 'Done!', + text: response.msg, + icon: 'success', + showConfirmButton: true + }); + // Optionally, you can reload the DataTable after successful archive + reloadDataTable(); + }, + error: function(response) { + // Show error message + Swal.fire({ + title: 'Error!', + text: response.message, + icon: 'error', + showConfirmButton: true + }); + } + }); + } + }); +} + + +// Function to add event listener for switch +function activeSwitchEventListener() { + // Add event listener for switch change event + $('body').on('change', '.switch-input', function() { + var rowId = $(this).closest('tr').find('.switch-input').data('id'); + var isActive = $(this).prop('checked'); + console.log(rowId, isActive) + // Perform active toggle action for the current user + $.ajax({ + url: actionUrl, // Replace with your active toggle endpoint + type: 'POST', + data: { + action: "active", + ids: [rowId], + active: isActive, + csrfmiddlewaretoken: '{{csrf_token}}' + }, + success: function(response) { + // Show success message + Swal.fire({ + title: 'Done!', + text: response.msg, + icon: 'success', + showConfirmButton: true + }); + // Reload the DataTable after successful toggle + reloadDataTable(); + }, + error: function(response) { + // Show error message + Swal.fire({ + title: 'Error!', + text: response.message, + icon: 'error', + showConfirmButton: true + }); } }); }); diff --git a/templates/module_cms/faq.html b/templates/module_cms/faq.html index c18743e..2f5f1da 100644 --- a/templates/module_cms/faq.html +++ b/templates/module_cms/faq.html @@ -7,6 +7,7 @@ {% include "cdn_through_html/animate_cdn_css.html" %} {% include "cdn_through_html/modal_cdn_css.html" %} {% include "cdn_through_html/switches_cdn_css.html" %} + {% include "cdn_through_html/sweetalert2_cdn_css.html" %} {% endblock %} @@ -32,14 +33,17 @@
-