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 0000000..ae58ac6 Binary files /dev/null and b/static/img/bowel.png differ diff --git a/static/img/default_profile.jpg b/static/img/default_profile.jpg new file mode 100644 index 0000000..7644382 Binary files /dev/null and b/static/img/default_profile.jpg differ diff --git a/static/img/foods.png b/static/img/foods.png new file mode 100644 index 0000000..a8e0d1d Binary files /dev/null and b/static/img/foods.png differ 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 @@
-