From db2fab79fa109e010d7a9c27b2a554c4abf18527 Mon Sep 17 00:00:00 2001 From: bobbyvish Date: Mon, 1 Apr 2024 11:31:16 +0530 Subject: [PATCH] Refactor i am principal --- module_auth/views.py | 9 +- module_iam/fixtures/iam_actions_fixture.json | 16 +-- .../iam_principal_source_fixture.json | 16 +-- .../fixtures/iam_principal_type_fixture.json | 12 +-- .../fixtures/iam_resources_fixture.json | 100 +++++++++--------- module_iam/forms.py | 37 +++++++ module_iam/iam_fixture_script.py | 7 +- .../0007_iamprincipalresourcelink_and_more.py | 31 ++++++ module_iam/models.py | 20 ++++ module_iam/urls.py | 1 + module_iam/views.py | 45 +++++++- module_notification/api/serializers.py | 9 ++ module_notification/api/urls.py | 11 ++ module_notification/api/views.py | 49 +++++++++ module_notification/cron_job.py | 46 ++++++++ .../management/commands/send_notification.py | 9 ++ .../migrations/0002_inappnotification.py | 34 ++++++ .../0003_alter_inappnotification_table.py | 17 +++ module_notification/models.py | 24 ++++- module_notification/views.py | 3 +- module_project/settings/base.py | 12 ++- module_project/urls.py | 2 +- .../base_structure/elements/sidebar.html | 4 +- .../module_iam/iam_principal_group_link.html | 66 +++++------- ...am_principal_resource_permission_edit.html | 64 +++++++++++ 25 files changed, 518 insertions(+), 126 deletions(-) create mode 100644 module_iam/migrations/0007_iamprincipalresourcelink_and_more.py create mode 100644 module_notification/api/serializers.py create mode 100644 module_notification/api/urls.py create mode 100644 module_notification/api/views.py create mode 100644 module_notification/cron_job.py create mode 100644 module_notification/management/commands/send_notification.py create mode 100644 module_notification/migrations/0002_inappnotification.py create mode 100644 module_notification/migrations/0003_alter_inappnotification_table.py create mode 100644 templates/module_iam/iam_principal_resource_permission_edit.html diff --git a/module_auth/views.py b/module_auth/views.py index cb0d69c..5c5493f 100644 --- a/module_auth/views.py +++ b/module_auth/views.py @@ -153,6 +153,11 @@ 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"] + FILTER_ICONTAINS = "icontains" + + def get_filter_method(self): + """Returns preferred filter method""" + return self.FILTER_ICONTAINS def get_initial_queryset(self): deleted_flag = self.request.GET.get('deleted_flag', False) @@ -166,9 +171,9 @@ class UserListJson(BaseDatatableView): for column in self.columns: print(f" columen index pattern {self.request.GET.get(f'columns[{self.columns.index(column)+2}][search][value]', None)}") - search_value = self.request.GET.get(f'columns[{self.columns.index(column)+1}][search][value]', None) + search_value = self.request.GET.get(f'columns[{self.columns.index(column)+2}][search][value]', None) if search_value: - column_data = self.request.GET.get(f'columns[{self.columns.index(column)+1}][data]') + column_data = self.request.GET.get(f'columns[{self.columns.index(column)+2}][data]') if column_data == "is_active": qs = qs.filter(**{f"{column}": search_value}) else: diff --git a/module_iam/fixtures/iam_actions_fixture.json b/module_iam/fixtures/iam_actions_fixture.json index 32a9f23..86aec3e 100644 --- a/module_iam/fixtures/iam_actions_fixture.json +++ b/module_iam/fixtures/iam_actions_fixture.json @@ -6,8 +6,8 @@ "name": "create", "label": "create", "slug": "create", - "created_on": "2024-03-18T12:21:41.416386", - "modified_on": "2024-03-18T12:21:41.416386" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -17,8 +17,8 @@ "name": "read", "label": "read", "slug": "read", - "created_on": "2024-03-18T12:21:41.416386", - "modified_on": "2024-03-18T12:21:41.416386" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -28,8 +28,8 @@ "name": "update", "label": "update", "slug": "update", - "created_on": "2024-03-18T12:21:41.416386", - "modified_on": "2024-03-18T12:21:41.416386" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -39,8 +39,8 @@ "name": "delete", "label": "delete", "slug": "delete", - "created_on": "2024-03-18T12:21:41.416386", - "modified_on": "2024-03-18T12:21:41.416386" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } } ] \ 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 index 433f53a..64492e6 100644 --- a/module_iam/fixtures/iam_principal_source_fixture.json +++ b/module_iam/fixtures/iam_principal_source_fixture.json @@ -6,8 +6,8 @@ "name": "app", "label": "app", "slug": "app", - "created_on": "2024-03-18T12:21:41.414515", - "modified_on": "2024-03-18T12:21:41.414515" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -17,8 +17,8 @@ "name": "web", "label": "web", "slug": "web", - "created_on": "2024-03-18T12:21:41.414515", - "modified_on": "2024-03-18T12:21:41.414515" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -28,8 +28,8 @@ "name": "google", "label": "google", "slug": "google", - "created_on": "2024-03-18T12:21:41.414515", - "modified_on": "2024-03-18T12:21:41.414515" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } }, { @@ -39,8 +39,8 @@ "name": "apple", "label": "apple", "slug": "apple", - "created_on": "2024-03-18T12:21:41.414515", - "modified_on": "2024-03-18T12:21:41.414515" + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813" } } ] \ 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 index 9062ad8..c17c89d 100644 --- a/module_iam/fixtures/iam_principal_type_fixture.json +++ b/module_iam/fixtures/iam_principal_type_fixture.json @@ -6,8 +6,8 @@ "name": "admin", "label": "admin", "slug": "admin", - "created_on": "2024-03-18T12:21:41.407938", - "modified_on": "2024-03-18T12:21:41.407938" + "created_on": "2024-04-01T11:17:39.364808", + "modified_on": "2024-04-01T11:17:39.364808" } }, { @@ -17,8 +17,8 @@ "name": "subadmin", "label": "subadmin", "slug": "subadmin", - "created_on": "2024-03-18T12:21:41.407938", - "modified_on": "2024-03-18T12:21:41.407938" + "created_on": "2024-04-01T11:17:39.364808", + "modified_on": "2024-04-01T11:17:39.364808" } }, { @@ -28,8 +28,8 @@ "name": "user", "label": "user", "slug": "user", - "created_on": "2024-03-18T12:21:41.407938", - "modified_on": "2024-03-18T12:21:41.407938" + "created_on": "2024-04-01T11:17:39.364808", + "modified_on": "2024-04-01T11:17:39.364808" } } ] \ No newline at end of file diff --git a/module_iam/fixtures/iam_resources_fixture.json b/module_iam/fixtures/iam_resources_fixture.json index ed95323..4735314 100644 --- a/module_iam/fixtures/iam_resources_fixture.json +++ b/module_iam/fixtures/iam_resources_fixture.json @@ -3,11 +3,11 @@ "model": "module_iam.iamappresource", "pk": 1, "fields": { - "name": "manage_dashboard", - "label": "manage_dashboard", - "slug": "manage_dashboard", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_iam", + "label": "manage_iam", + "slug": "manage_iam", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -20,11 +20,11 @@ "model": "module_iam.iamappresource", "pk": 2, "fields": { - "name": "manage_iam", - "label": "manage_iam", - "slug": "manage_iam", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_user", + "label": "manage_user", + "slug": "manage_user", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -37,11 +37,11 @@ "model": "module_iam.iamappresource", "pk": 3, "fields": { - "name": "manage_user", - "label": "manage_user", - "slug": "manage_user", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_support", + "label": "manage_support", + "slug": "manage_support", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -54,11 +54,11 @@ "model": "module_iam.iamappresource", "pk": 4, "fields": { - "name": "manage_support", - "label": "manage_support", - "slug": "manage_support", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_contact_us", + "label": "manage_contact_us", + "slug": "manage_contact_us", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -71,11 +71,11 @@ "model": "module_iam.iamappresource", "pk": 5, "fields": { - "name": "manage_contact_us", - "label": "manage_contact_us", - "slug": "manage_contact_us", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_feedback", + "label": "manage_feedback", + "slug": "manage_feedback", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -88,11 +88,11 @@ "model": "module_iam.iamappresource", "pk": 6, "fields": { - "name": "manage_feedback", - "label": "manage_feedback", - "slug": "manage_feedback", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_cms", + "label": "manage_cms", + "slug": "manage_cms", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -105,11 +105,11 @@ "model": "module_iam.iamappresource", "pk": 7, "fields": { - "name": "manage_cms", - "label": "manage_cms", - "slug": "manage_cms", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_faqs", + "label": "manage_faqs", + "slug": "manage_faqs", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -122,11 +122,11 @@ "model": "module_iam.iamappresource", "pk": 8, "fields": { - "name": "manage_faqs", - "label": "manage_faqs", - "slug": "manage_faqs", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_tc", + "label": "manage_tc", + "slug": "manage_tc", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -139,11 +139,11 @@ "model": "module_iam.iamappresource", "pk": 9, "fields": { - "name": "manage_tc", - "label": "manage_tc", - "slug": "manage_tc", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_privacypolicy", + "label": "manage_privacypolicy", + "slug": "manage_privacypolicy", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, @@ -156,11 +156,11 @@ "model": "module_iam.iamappresource", "pk": 10, "fields": { - "name": "manage_privacypolicy", - "label": "manage_privacypolicy", - "slug": "manage_privacypolicy", - "created_on": "2024-03-18T12:21:41.417445", - "modified_on": "2024-03-18T12:21:41.417445", + "name": "manage_notification", + "label": "manage_notification", + "slug": "manage_notification", + "created_on": "2024-04-01T11:17:39.378813", + "modified_on": "2024-04-01T11:17:39.378813", "action": [ 1, 2, diff --git a/module_iam/forms.py b/module_iam/forms.py index 9509b30..f171d74 100644 --- a/module_iam/forms.py +++ b/module_iam/forms.py @@ -223,6 +223,43 @@ class ProfileEditForm(forms.ModelForm): ] +class IAmPrincipalResourceLinkForm(IAmPrincipalForm): + + class Meta: + model = models.IAmPrincipal + fields = [ + "principal_type", + "first_name", + "last_name", + "email", + "password", + "confirm_password", + "principal_resource", + ] + + principal_resource = forms.ModelMultipleChoiceField( + label="Module Permission", + queryset=models.IAmAppResource.objects.filter(active=True, deleted=False), + required=False, + widget=forms.widgets.SelectMultiple( + attrs={"class": "form_select js-example-basic-multiple"} + ), + ) + + def save(self, commit=True): + # First, save the instance of the IAmPrincipal model as usual + principal = super().save(commit=False) + # If the principal_resource field has data + if self.cleaned_data['principal_resource']: + # Get the principal_resource data + principal_resource_data = self.cleaned_data['principal_resource'] + # Update the many-to-many relationship + principal.principal_resource.set(principal_resource_data) + # Save the instance to the database + if commit: + principal.save() + + class IAmPrincipalGroupLinkForm(IAmPrincipalForm): class Meta: diff --git a/module_iam/iam_fixture_script.py b/module_iam/iam_fixture_script.py index b0eb7b4..09dd3e3 100644 --- a/module_iam/iam_fixture_script.py +++ b/module_iam/iam_fixture_script.py @@ -21,7 +21,8 @@ from .iam_constant import ( RESOURCE_MANAGE_T_C, RESOURCE_MANAGE_CMS, RESOURCE_MANAGE_PRIVACYPOLICY, - RESOURCE_MANAGE_SUPPORT + RESOURCE_MANAGE_SUPPORT, + RESOURCE_MANAGE_NOTIFICATION ) class IAMPrincipalType: @@ -122,7 +123,6 @@ class IAMActions: return iam_action_fixture_data class IAMResources: - DASHBOARD = RESOURCE_MANAGE_DASHBOARD IAM = RESOURCE_MANAGE_IAM USER = RESOURCE_MANAGE_USER SUPPORT = RESOURCE_MANAGE_SUPPORT @@ -132,9 +132,9 @@ class IAMResources: FAQS = RESOURCE_MANAGE_FAQS T_C = RESOURCE_MANAGE_T_C PRIVACYPOLICY = RESOURCE_MANAGE_PRIVACYPOLICY + NOTIFICATION = RESOURCE_MANAGE_NOTIFICATION resources = [ - DASHBOARD, IAM, USER, SUPPORT, @@ -144,6 +144,7 @@ class IAMResources: FAQS, T_C, PRIVACYPOLICY, + NOTIFICATION, ] @staticmethod diff --git a/module_iam/migrations/0007_iamprincipalresourcelink_and_more.py b/module_iam/migrations/0007_iamprincipalresourcelink_and_more.py new file mode 100644 index 0000000..d5f6e84 --- /dev/null +++ b/module_iam/migrations/0007_iamprincipalresourcelink_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.2 on 2024-03-31 09:38 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_iam', '0006_alter_appversion_version'), + ] + + operations = [ + migrations.CreateModel( + name='IAmPrincipalResourceLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='principal_resource_link_principal', to=settings.AUTH_USER_MODEL)), + ('principal_resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='principal_resource_link_resource', to='module_iam.iamappresource')), + ], + options={ + 'db_table': 'iam_principal_resource_group_link', + }, + ), + migrations.AddField( + model_name='iamprincipal', + name='principal_resource', + field=models.ManyToManyField(related_name='principal_resources', through='module_iam.IAmPrincipalResourceLink', to='module_iam.iamappresource'), + ), + ] diff --git a/module_iam/models.py b/module_iam/models.py index 6b15257..0c2cab5 100644 --- a/module_iam/models.py +++ b/module_iam/models.py @@ -357,6 +357,11 @@ class IAmPrincipal(AbstractUser): blank=True, help_text="OneSignal player id for push notification", ) + principal_resource = models.ManyToManyField( + IAmAppResource, + through="IAmPrincipalResourceLink", + related_name="principal_resources", + ) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] @@ -370,6 +375,21 @@ class IAmPrincipal(AbstractUser): return f"{self.email}" +class IAmPrincipalResourceLink(models.Model): + principal = models.ForeignKey( + IAmPrincipal, + related_name="principal_resource_link_principal", + on_delete=models.CASCADE, + ) + principal_resource = models.ForeignKey( + IAmAppResource, + related_name="principal_resource_link_resource", + on_delete=models.CASCADE, + ) + + class Meta: + db_table = "iam_principal_resource_group_link" + class IAmPrincipalGroupLink(models.Model): principal = models.ForeignKey( IAmPrincipal, diff --git a/module_iam/urls.py b/module_iam/urls.py index 41cd53e..61accb9 100644 --- a/module_iam/urls.py +++ b/module_iam/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ # path('principal/delete/', views.PrincipalDeleteView.as_view(), name="principal_delete"), path('principal/archive/', views.PrincipalArchiveView.as_view(), name="principal_archive"), path('principal/archive/list/', views.PrincipalArchiveListJsonView.as_view(), name="principal_archive_list"), + path('principal/resource/permission/edit//', views.PrincipalResourcePermissionEditView.as_view(), name="principal_resource_permission_edit"), path('principal/group/link/', views.PrincipalGroupLinkView.as_view(), name="principal_group_link"), path('principal/group/link/list/admin/', views.PrincipalGroupLinkAdminListJsonView.as_view(), name="principal_group_link_list"), diff --git a/module_iam/views.py b/module_iam/views.py index bf2cf6a..6606f18 100644 --- a/module_iam/views.py +++ b/module_iam/views.py @@ -19,7 +19,7 @@ from module_project import constants from module_project.mixins import ActionMixin, DatatablesMixin from module_project.utils import JsonResponseUtil -from .forms import (CustomAuthenticationForm, IAmPrincipalForm, +from .forms import (CustomAuthenticationForm, IAmPrincipalForm,IAmPrincipalResourceLinkForm, IAmPrincipalGroupLinkForm, IAmPrincipalGroupRoleLinkForm, IAmPrincipalRoleAppResourceActionLinkForm, ProfileEditForm) from .models import (IAmAppResourceActionLink, IAmPrincipal, IAmPrincipalGroup, @@ -138,6 +138,45 @@ class PrincipalArchiveListJsonView(BaseDatatableView): return qs + +class PrincipalResourcePermissionEditView(LoginRequiredMixin, generic.View): + page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP + model = IAmPrincipal + template_name = "module_iam/iam_principal_resource_permission_edit.html" + form_class = IAmPrincipalResourceLinkForm + success_url = reverse_lazy("module_iam:principal_group_link") + success_message = "Record Updated 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", + } + 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(): + context = self.get_context_data(form=form) + return render(request, self.template_name, context=context) + form.save() + messages.success(request, self.success_message) + return redirect(self.success_url) + + class PrincipalGroupLinkView(LoginRequiredMixin, generic.TemplateView): page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP model = IAmPrincipal @@ -197,8 +236,8 @@ class PrincipalGroupLinkSubAdminListJsonView(BaseDatatableView): def render_column(self, row, column): if column == "principal_type_name": return row.principal_type.name if row.principal_type else None - if column == "groups": - return [{"name": group.name} for group in row.principal_group.all()] + if column == "permission": + return [{"name": resource.name} for resource in row.principal_resource.all()] return super().render_column(row, column) def filter_queryset(self, qs): diff --git a/module_notification/api/serializers.py b/module_notification/api/serializers.py new file mode 100644 index 0000000..1a83285 --- /dev/null +++ b/module_notification/api/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from ..models import InAppNotification + +class InAppNotificationSerializer(serializers.ModelSerializer): + created_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S") + + class Meta: + model = InAppNotification + fields = ['id', 'message', 'is_read', 'created_on'] diff --git a/module_notification/api/urls.py b/module_notification/api/urls.py new file mode 100644 index 0000000..abb39de --- /dev/null +++ b/module_notification/api/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + + path("count/", views.InAppNotificationCountAPIView.as_view(), name="inapp_notification_count"), + path("list/", views.InAppNotificationListAPIView.as_view(), name="inapp_notification_list"), + path("read/", views.InAppNotificationReadAPIView.as_view(), name="inapp_notification_read"), + +] diff --git a/module_notification/api/views.py b/module_notification/api/views.py new file mode 100644 index 0000000..be510dc --- /dev/null +++ b/module_notification/api/views.py @@ -0,0 +1,49 @@ +from datetime import datetime + +import requests +from django.conf import settings +from django.contrib.auth import authenticate +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.authentication import JWTAuthentication +from module_project.utils import ApiResponse +from module_project import constants + +from .serializers import InAppNotificationSerializer + +from ..models import InAppNotification + + +class InAppNotificationCountAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = InAppNotification + + def get(self, request, *args, **kwargs): + count = InAppNotification.pending_read_count(user=request.user) + return ApiResponse.success(message=constants.SUCCESS, data={"count": count}) + + +class InAppNotificationListAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = InAppNotification + serializer_class = InAppNotificationSerializer + + def get(self, request, *args, **kwargs): + obj = InAppNotification.latest_15(user=request.user) + serializer_obj = self.serializer_class(obj, many=True) + return ApiResponse.success(message=constants.SUCCESS, data=serializer_obj.data) + + +class InAppNotificationReadAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = InAppNotification + serializer_class = InAppNotificationSerializer + + def get(self, request, *args, **kwargs): + obj = InAppNotification.objects.filter(user=request.user).update(is_read=True) + return ApiResponse.success(message=constants.SUCCESS) \ No newline at end of file diff --git a/module_notification/cron_job.py b/module_notification/cron_job.py new file mode 100644 index 0000000..b5c04d2 --- /dev/null +++ b/module_notification/cron_job.py @@ -0,0 +1,46 @@ +from datetime import datetime, timedelta +from .models import InAppNotification +from module_iam.models import IAmPrincipal, IAmPrincipalType +from module_activity.models import MealRecord, Bowel, MealSymptomRecord, Medication + + +def notification_for_meal_and_medication(): + current_date = datetime.now() + fifteen_days_ago = current_date - timedelta(days=20) + + users = IAmPrincipal.objects.filter( + last_login__gte=fifteen_days_ago, + principal_type=IAmPrincipalType.get_principal_user(), + ).values_list("id", flat=True) + + meal_obj = MealRecord.objects.filter(date=current_date).values_list( + "principal", flat=True + ) + medication_obj = Medication.objects.filter(date=current_date).values_list( + "principal", flat=True + ) + + # Remove IDs of users who have recorded meals for the current day + users_without_meals = set(users) - set(meal_obj) + users_without_medications = set(users) - set(medication_obj) + print(f"user id {set(users)}") + print( + f"userwithoutmeal {users_without_meals} and users_without_medication {users_without_medications}" + ) + + notifications_to_create = [] + for user_id in users_without_meals: + message = "Have you eaten yet? it's been a whiile since you logged a meal." + notifications_to_create.append( + InAppNotification(user_id=user_id, message=message) + ) + + for user_id in users_without_medications: + message = "Have you taken your medication today? Remember to log your medication to stay on track with your treatment!" + notifications_to_create.append( + InAppNotification(user_id=user_id, message=message) + ) + + # Bulk create notifications + if notifications_to_create: + InAppNotification.objects.bulk_create(notifications_to_create) diff --git a/module_notification/management/commands/send_notification.py b/module_notification/management/commands/send_notification.py new file mode 100644 index 0000000..ef52552 --- /dev/null +++ b/module_notification/management/commands/send_notification.py @@ -0,0 +1,9 @@ + +from django.core.management.base import BaseCommand +from ...cron_job import notification_for_meal_and_medication + +class Command(BaseCommand): + help = 'Sends notifications to users' + + def handle(self, *args, **kwargs): + notification_for_meal_and_medication() \ No newline at end of file diff --git a/module_notification/migrations/0002_inappnotification.py b/module_notification/migrations/0002_inappnotification.py new file mode 100644 index 0000000..2ca2fe3 --- /dev/null +++ b/module_notification/migrations/0002_inappnotification.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.2 on 2024-03-30 18:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_notification', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='InAppNotification', + 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)), + ('message', models.CharField(max_length=255)), + ('is_read', models.BooleanField(default=False)), + ('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)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_on'], + }, + ), + ] diff --git a/module_notification/migrations/0003_alter_inappnotification_table.py b/module_notification/migrations/0003_alter_inappnotification_table.py new file mode 100644 index 0000000..a5af5dc --- /dev/null +++ b/module_notification/migrations/0003_alter_inappnotification_table.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-03-30 18:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('module_notification', '0002_inappnotification'), + ] + + operations = [ + migrations.AlterModelTable( + name='inappnotification', + table='inapp_notification', + ), + ] diff --git a/module_notification/models.py b/module_notification/models.py index 3d37c81..b904cf5 100644 --- a/module_notification/models.py +++ b/module_notification/models.py @@ -1,9 +1,29 @@ from django.db import models -from module_iam.models import BaseModel +from module_iam.models import BaseModel, IAmPrincipal + + +class InAppNotification(BaseModel): + user = models.ForeignKey(IAmPrincipal, on_delete=models.CASCADE, related_name='notifications') + message = models.CharField(max_length=255) + is_read = models.BooleanField(default=False) + + class Meta: + db_table = "inapp_notification" + ordering = ['-created_on'] + + def __str__(self): + return self.message + + @classmethod + def latest_15(cls, user): + return cls.objects.filter(user=user).order_by('-created_on')[:15] + + @classmethod + def pending_read_count(cls, user): + return cls.objects.filter(user=user, is_read=False).count() -# 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) diff --git a/module_notification/views.py b/module_notification/views.py index 441bdfe..b87b095 100644 --- a/module_notification/views.py +++ b/module_notification/views.py @@ -198,7 +198,6 @@ class NotificationSendView(generic.View): 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, ) except Exception as e: @@ -210,3 +209,5 @@ class NotificationSendView(generic.View): return JsonResponseUtil.error(**error_response) return JsonResponseUtil.success(message="success") + + diff --git a/module_project/settings/base.py b/module_project/settings/base.py index 82c437e..55251cc 100644 --- a/module_project/settings/base.py +++ b/module_project/settings/base.py @@ -62,6 +62,7 @@ THIRD_PARTY_APPS = [ "rest_framework_simplejwt", "taggit", "django_quill", + "django_crontab", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -259,8 +260,8 @@ LOGGING = { # jwt configuration # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html#settings SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=30), - "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=30), + "ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=15), + "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=15), "ROTATE_REFRESH_TOKENS": False, "BLACKLIST_AFTER_ROTATION": False, "UPDATE_LAST_LOGIN": False, @@ -282,3 +283,10 @@ SIMPLE_JWT = { "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", "JTI_CLAIM": "jti", } + +CRONJOBS = [ + ('* * * * *', 'manage_notification.cron_job.notification_for_meal_and_medication'), +] + +# Additional configuration for cron jobs +CRONTAB_LOCK_JOBS = True \ No newline at end of file diff --git a/module_project/urls.py b/module_project/urls.py index 36a85d0..16d785a 100644 --- a/module_project/urls.py +++ b/module_project/urls.py @@ -37,7 +37,7 @@ urlpatterns = [ path('api/activity/', include("module_activity.api.urls")), path('notification/', include("module_notification.urls")), - # path('api/activity/', include("module_activity.api.urls")), + path('api/notification/', include("module_notification.api.urls")), ] if settings.DEBUG: diff --git a/templates/base_structure/elements/sidebar.html b/templates/base_structure/elements/sidebar.html index 974637b..0210168 100644 --- a/templates/base_structure/elements/sidebar.html +++ b/templates/base_structure/elements/sidebar.html @@ -49,11 +49,11 @@
  • IAM Principal
  • -
  • +
  • diff --git a/templates/module_iam/iam_principal_group_link.html b/templates/module_iam/iam_principal_group_link.html index 5cc759a..c4b2613 100644 --- a/templates/module_iam/iam_principal_group_link.html +++ b/templates/module_iam/iam_principal_group_link.html @@ -52,24 +52,24 @@ + # Id - Id + style="width: 69.2656px;">Record Id Name Email - Permission - Pricipal_type - Id - # + Id + style="width: 69.2656px;"> Record Id Name Email - Groups - Permission + Pricipal_type ${group.name}`; +function renderPermission(data, type, row) { + if (type === 'display' && row.permission) { + let html = '
    • '; + for (const [index, permission] of Object.entries(row.permission)) { + html += `${permission.name}`; } - html += '
    '; + html += ''; return html; - } else if (type === 'display' && !row.groups) { + } else if (type === 'display' && !row.permission) { return 'No Group assigned'; } else { return ''; diff --git a/templates/module_iam/iam_principal_resource_permission_edit.html b/templates/module_iam/iam_principal_resource_permission_edit.html new file mode 100644 index 0000000..f240d8e --- /dev/null +++ b/templates/module_iam/iam_principal_resource_permission_edit.html @@ -0,0 +1,64 @@ +{% extends 'base_structure/layout/base_template.html' %} +{% load static %} +{% block stylesheet %} + + + + +{% endblock %} + +{% block content %} + +
    +
    +
    +
    +

    {{operation}} Principal Group Link

    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + {% csrf_token %} + {% include 'base_structure/includes/dynamic_template_form.html' with form=form %} +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +{% endblock content %} + +{% block javascript %} + + + + + +{% endblock %} \ No newline at end of file