Merge pull request #6 from WDI-Ideas/development

Development
This commit is contained in:
BOBBY VISHWAKARMA
2024-03-18 11:45:58 +05:30
committed by GitHub
20 changed files with 594 additions and 318 deletions

View File

@@ -155,7 +155,7 @@ def authticate_with_otp_and_passsword(principal: IAmPrincipal, otp=None, passwor
if otp:
otp_instance = IAmPrincipalOtp.objects.filter(
principal=principal, otp_code=otp
principal=principal, otp_code=otp, is_used=False
).last()
if not otp_instance:

View File

@@ -137,14 +137,14 @@ class OtpRequestView(APIView):
# principal = auth_service.get_principal_by_email(request.data.get("email"))
otp_code = SMSService().create_otp(
principal=principal, otp_purpose="Forget password"
principal=principal, otp_purpose="Password Reset Request"
)
except Exception as e:
return ApiResponse.error(message=str(e), errors=str(e))
email_service = EmailService(
subject="Forget Password",
subject="Password Reset Request",
to=principal.email,
from_email=settings.EMAIL_HOST_USER,
)
@@ -197,24 +197,27 @@ class OTPVerificationView(APIView):
class ForgetPasswordView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
authentication_classes = []
permission_classes = []
serializer_class = PasswordResetSerializer
def post(self, request):
email = request.data.get("email")
print("email for password reset", email)
principal = get_principal_by_email(email=email)
otp_instance = IAmPrincipalOtp.objects.filter(principal=principal).last()
if isinstance(principal, Response):
return principal
otp_instance = IAmPrincipalOtp.objects.filter(principal=principal, is_used=True).last()
if not otp_instance:
return ApiResponse.error(message=constants.SESSION_EXPIRED)
return ApiResponse.error(message=constants.PASSWORD_RESET_SESSION_EXPIRE)
if otp_instance.is_expired():
return ApiResponse.error(message=constants.SESSION_EXPIRED)
return ApiResponse.error(message=constants.PASSWORD_RESET_SESSION_EXPIRE)
serializer = self.serializer_class(request.user, data=request.data)
serializer = self.serializer_class(principal, data=request.data)
if not serializer.is_valid():
error_response = {
"status": status.HTTP_403_FORBIDDEN,

View File

@@ -1,10 +1,12 @@
from django.urls import path
from . import views
from django.views.generic import TemplateView
from django.views.generic import TemplateView, RedirectView
app_name = "module_auth"
urlpatterns = [
# redirect to different url
path('', RedirectView.as_view(url='login'), name='index'),
path('login/', views.AdminLoginView.as_view(), name="login"),
path('logout/', views.AdminLogoutView.as_view(), name="logout"),
path('password-reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),

View File

@@ -55,13 +55,6 @@ class IAmPrincipalForm(forms.ModelForm):
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 = [
@@ -71,26 +64,19 @@ class IAmPrincipalForm(forms.ModelForm):
"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
active=True, deleted=False, name__in=(PRINCIPAL_TYPE_ADMIN, PRINCIPAL_TYPE_SUBADMIN)
)
# 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
if instance is not None and instance.pk is not None:
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)
@@ -237,21 +223,20 @@ class ProfileEditForm(forms.ModelForm):
]
class IAmPrincipalGroupLinkForm(forms.ModelForm):
class IAmPrincipalGroupLinkForm(IAmPrincipalForm):
class Meta:
model = models.IAmPrincipal
fields = [
# "principal_type",
"principal_type",
"first_name",
"last_name",
"email",
"password",
"confirm_password",
"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),
@@ -261,13 +246,18 @@ class IAmPrincipalGroupLinkForm(forms.ModelForm):
),
)
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
def save(self, commit=True):
# First, save the instance of the IAmPrincipal model as usual
principal = super().save(commit=False)
# If the principal_group field has data
if self.cleaned_data['principal_group']:
# Get the principal_group data
principal_group_data = self.cleaned_data['principal_group']
# Update the many-to-many relationship
principal.principal_group.set(principal_group_data)
# Save the instance to the database
if commit:
principal.save()
class IAmPrincipalTypeForm(forms.ModelForm):
class Meta:

View File

@@ -9,19 +9,20 @@ urlpatterns = [
# path('principal/', views.PrincipalListView.as_view(), name="principal_list"),
# path('principal/add/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_add"),
# path('principal/edit/<int:pk>', views.PrincipalCreateOrUpdateView.as_view(), name="principal_edit"),
path('principal/edit/<int:pk>', views.PrincipalCreateOrUpdateView.as_view(), name="principal_edit"),
# path('principal/delete/<int:pk>', 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/<int:pk>/', views.PrincipalGroupLinkEditView.as_view(), name="principal_group_link_edit"),
path('principal/group/link/list/admin/', views.PrincipalGroupLinkAdminListJsonView.as_view(), name="principal_group_link_list"),
path('principal/group/link/list/subadmin/', views.PrincipalGroupLinkSubAdminListJsonView.as_view(), name="principal_group_link_list_sub"),
path('principal/group/link/edit/<int:pk>/', views.PrincipalGroupLinkEditView.as_view(), name="principal_group_link_edit"),
path('principal/group/link/action/', views.PrincipalGroupLinkActionView.as_view(), name="principal_group_link_action"),
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/<int:pk>/', views.PrincipalGroupCreateOrUpdateView.as_view(), name="principal_group_edit"),
path('principal/group/action//', views.PrincipalGroupActionView.as_view(), name="principal_group_action"),
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"),

View File

@@ -1,9 +1,13 @@
from typing import Any
from django.db import transaction
from django.db.models.base import Model as Model
from django.db.models.query import QuerySet
from django.views import generic
import logging
from datetime import datetime
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
@@ -14,13 +18,14 @@ 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 module_project.utils import JsonResponseUtil
from .forms import (
CustomAuthenticationForm,
IAmPrincipalForm,
IAmPrincipalGroupRoleLinkForm,
IAmPrincipalRoleAppResourceActionLinkForm,
IAmPrincipalGroupLinkForm,
ProfileEditForm
ProfileEditForm,
)
from .models import (
IAmPrincipal,
@@ -36,6 +41,7 @@ logger = logging.getLogger(__name__)
# Create your views here.
class DashboardView(generic.TemplateView):
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
template_name = "base_structure/layout/dashboard.html"
@@ -51,11 +57,68 @@ class DashboardView(generic.TemplateView):
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
context["active_user_count"] = active_user_count
context["total_user_count"] = total_user_count
context["page_name"] = self.page_name
return context
class PrincipalCreateOrUpdateView(LoginRequiredMixin, generic.View):
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL
model = IAmPrincipal
form_class = IAmPrincipalForm
template_name = "module_iam/iam_principal_add.html"
success_url = reverse_lazy("module_iam:principal_group_link")
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.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 PrincipalGroupLinkView(LoginRequiredMixin, generic.TemplateView):
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
model = IAmPrincipal
@@ -66,14 +129,121 @@ class PrincipalGroupLinkView(LoginRequiredMixin, generic.TemplateView):
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"]
columns = ["id", "first_name", "email", "is_active"]
order_columns = ["id", "first_name", "email", "is_active"]
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)
deleted_flag = self.request.GET.get("deleted_flag", False)
return self.model.objects.filter(deleted=deleted_flag, principal_type__name=iam_constant.PRINCIPAL_TYPE_ADMIN)
def render_column(self, row, column):
if column == "principal_type_name":
return row.principal_type.name if row.principal_type else None
return super().render_column(row, column)
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(first_name__icontains=search_value) | Q(email__icontains=search_value)
)
return qs
class PrincipalGroupLinkSubAdminListJsonView(BaseDatatableView):
model = IAmPrincipal
columns = ["id", "first_name", "email", "is_active"]
order_columns = ["id", "first_name", "email", "is_active"]
def get_initial_queryset(self):
deleted_flag = self.request.GET.get("deleted_flag", False)
return self.model.objects.filter(deleted=deleted_flag, principal_type__name=iam_constant.PRINCIPAL_TYPE_SUBADMIN)
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()]
return super().render_column(row, column)
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(first_name__icontains=search_value) | Q(email__icontains=search_value)
)
return qs
class PrincipalGroupLinkEditView(LoginRequiredMixin, generic.View):
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
model = IAmPrincipal
template_name = "module_iam/iam_principal_group_link_edit.html"
form_class = IAmPrincipalGroupLinkForm
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 PrincipalGroupLinkActionView(generic.View):
model = IAmPrincipal
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, 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 PrincipalGroupView(LoginRequiredMixin, generic.TemplateView):
@@ -86,15 +256,6 @@ class PrincipalGroupView(LoginRequiredMixin, generic.TemplateView):
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
@@ -102,41 +263,43 @@ class PrincipalGroupListJsonView(BaseDatatableView):
order_columns = ["id", "name", "active"]
def get_initial_queryset(self):
deleted_flag = self.request.GET.get('deleted_flag', False)
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)
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()]
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
})
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'
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"
@@ -147,7 +310,9 @@ class PrincipalGroupCreateOrUpdateView(LoginRequiredMixin, generic.View):
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
self.success_message = (
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
)
return self.success_message
def get_object(self):
@@ -194,13 +359,14 @@ class AppRoleView(LoginRequiredMixin, generic.TemplateView):
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)
deleted_flag = self.request.GET.get("deleted_flag", False)
return (
super(AppRoleListJsonView, self)
.get_initial_queryset()
@@ -247,10 +413,10 @@ class AppRoleListJsonView(BaseDatatableView):
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'
context["recordsTotal"] = len(role_data)
context["recordsFiltered"] = len(role_data)
context["data"] = role_data
context["result"] = "ok"
return context
@@ -319,7 +485,10 @@ class PrincipalProfileView(LoginRequiredMixin, generic.TemplateView):
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)
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)
@@ -327,6 +496,7 @@ class PrincipalProfileView(LoginRequiredMixin, generic.TemplateView):
context["data_obj"] = self.get_object()
return context
class PrincipalProfileEditView(generic.View):
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
model = IAmPrincipal
@@ -349,7 +519,7 @@ class PrincipalProfileEditView(generic.View):
context = {
# "page_name": self.page_name,
"operation": "Edit",
"page_name": self.page_name
"page_name": self.page_name,
}
context.update(kwargs) # Include any additional context data passed to the view
return context
@@ -368,7 +538,7 @@ class PrincipalProfileEditView(generic.View):
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)
return redirect(self.success_url)

View File

@@ -129,6 +129,12 @@ class NotificationActionView(ActionMixin):
class NotificationSendView(generic.View):
model = PushNotification
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 ""
def post(self, request, *args, **kwargs):
id = request.POST.get("id")
obj = self.model.objects.filter(pk=int(id)).first()
@@ -141,8 +147,6 @@ class NotificationSendView(generic.View):
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(
@@ -151,9 +155,7 @@ class NotificationSendView(generic.View):
# 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,

View File

@@ -31,6 +31,7 @@ LOGIN_REQUIRED = "Login required to perform this action."
LOGIN_SUCCESS = "Login successful."
LOGOUT_SUCCESS = "Logout successful."
SESSION_EXPIRED = "Your session has expired. Please log in again."
PASSWORD_RESET_SESSION_EXPIRE = "Password reset session has expired"
ACCOUNT_DEACTIVATED = "Your account is inactive. Please contact support."
EMAIL_EXISTS = "This email address is already in use. Please use a different email."
INVALID_EMAIL_PASSWORD = "Invalid email or password."

View File

@@ -28,7 +28,7 @@ if READ_DOT_ENV_FILE:
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# SECRET_KEY = 'django-insecure-#7rdu=fr58ba9_!n3$l5pm!xs8l%6%8xt@vb8$&o@hqhd@rtd%'
SECRET_KEY = env.str("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
@@ -108,7 +108,7 @@ WSGI_APPLICATION = 'module_project.wsgi.application'
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "digest_db",
"NAME": env.str("DB_DATABASE"),
"HOST": env.str("DB_HOST"),
"USER": env.str("DB_USERNAME"),
"PASSWORD": env.str("DB_PASSWORD"),
@@ -154,12 +154,12 @@ SHORT_DATE_FORMAT = "d-m-Y"
TIME_FORMAT = "H:i p"
# otp expire time limit
OTP_EXPIRE_TIME = 5 # mins
OTP_EXPIRE_TIME = 2 # mins
APPEND_SLASH = True
LOGIN_REDIRECT_URL = "/iam/dashboard/"
LOGIN_URL = "/auth/login/"
LOGOUT_REDIRECT_URL = "/auth/login/"
LOGIN_URL = "/login/"
LOGOUT_REDIRECT_URL = "/login/"
# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model
@@ -219,8 +219,6 @@ 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
@@ -260,7 +258,7 @@ LOGGING = {
# jwt configuration
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html#settings
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=20),
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=30),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
@@ -283,8 +281,3 @@ SIMPLE_JWT = {
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
}
SOCIAL_AUTH_APPLE_CLIENT_ID = '<YOUR_APPLE_CLIENT_ID>'
SOCIAL_AUTH_APPLE_CLIENT_SECRET = '<YOUR_APPLE_CLIENT_SECRET>'
SOCIAL_AUTH_APPLE_REDIRECT_URI = '<YOUR_APPLE_REDIRECT_URI>'

View File

@@ -5,14 +5,14 @@ import colorlog
from logging.handlers import TimedRotatingFileHandler
DEBUG = False
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["staging.eatwithdigest.com"]
# CORS_ALLOWED_ORIGINS = [
# "http://127.0.0.1:3000",
# ]
# CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_ALLOW_ALL = True
# CORS_ORIGIN_WHITELIST = ("http://localhost:3000",)
@@ -69,7 +69,7 @@ LOGGING = {
},
}
BASE_DOMAIN = ""
BASE_DOMAIN = "https://staging.eatwithdigest.com"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

View File

@@ -1,123 +0,0 @@
"""
Django settings for module_project project.
Generated by 'django-admin startproject' using Django 4.2.5.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-#7rdu=fr58ba9_!n3$l5pm!xs8l%6%8xt@vb8$&o@hqhd@rtd%'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'module_project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'module_project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@@ -23,8 +23,8 @@ urlpatterns = [
path('admin/', admin.site.urls),
path('iam/', include('module_iam.urls')),
path('auth/', include('module_auth.urls')),
path('', include('module_auth.urls')),
path('api/auth/', include('module_auth.api.urls')),
path('cms/', include('module_cms.urls')),

View File

@@ -8,6 +8,11 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
import sys
sys.path.append('/var/www/testing_django/testing')
sys.path.append('/var/www/testing_django/testing/testing')
sys.path.append('/var/www/testing_django/testing/venv/lib/python3.11/site-packages')
from django.core.wsgi import get_wsgi_application

View File

@@ -30,6 +30,7 @@
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
</div>
{% elif field.field.widget.input_type == 'checkbox' %}
<div class="form-group mb-3">
<div class="">
@@ -67,4 +68,4 @@
</div>
{% endif %}
{% endfor %}
</div>
</div>

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no">
<title>Nifty11 </title>
<title>Digest</title>
{% load static %}
<link rel="icon" type="image/x-icon" href="../src/assets/img/favicon.ico"/>
<link href="../layouts/collapsible-menu/css/light/loader.css" rel="stylesheet" type="text/css" />

View File

@@ -28,7 +28,7 @@
<p><strong>{{ code }}</strong></p>
<p>If you didn't request a password reset, you can safely ignore this email.</p>
<p>Thank you,</p>
<p>The Support Team</p>
<p>Team Digest</p>
</div>
</body>
</html>

View File

@@ -173,8 +173,8 @@ function initializeDataTable(tableName, mainUrl) {
function renderRole(data, type, row) {
if (type === 'display' && row.roles) {
let html = '<ul>';
for (const [name] of Object.entries(row.roles)) {
html += `<li class="mb-1"><span class="badge badge-primary">${name}</span></li>`;
for (const [index, role] of Object.entries(row.roles)) {
html += `<li class="mb-1"><span class="badge badge-primary">${role.name}</span></li>`;
}
html += '</ul>';
return html;

View File

@@ -0,0 +1,48 @@
{% extends 'base_structure/layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>{{operation}} Principal</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post">
{% csrf_token %}
{% include 'base_structure/includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
{% endblock %}

View File

@@ -56,21 +56,33 @@
<thead>
<tr role="row">
<th class="sorting_asc text-center" tabindex="0"
aria-controls="style-3" aria-sort="ascending"
style="width: 50.2656px;">#</th>
<th class="sorting text-center" tabindex="1" aria-controls="style-3"
colspan="1"
style="width: 44.2344px;">Time</th>
<th class="sorting text-center" tabindex="2" aria-controls="style-3"
colspan="1"
style="width: 44.2344px;">Meal</th>
<th class="sorting text-center" tabindex="3" aria-controls="style-3"
style="width: 79.7969px;">Medication</th>
<th class="sorting text-center" tabindex="4" aria-controls="style-3"
style="width: 79.7969px;">Bowel Movement</th>
<th class="sorting text-center" tabindex="5" aria-controls="style-3"
style="width: 79.7969px;">Symptoms</th>
<th class="checkbox-column text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
aria-label=" Record Id : activate to sort column descending"
style="width: 69.2656px;"> Id </th>
<th class="checkbox-column text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
aria-label=" Record Id : activate to sort column descending"
style="width: 69.2656px;"> Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1" aria-label="First Name: activate to sort column ascending"
style="width: 44.2344px;">Name</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1" aria-label="Email: activate to sort column ascending"
style="width: 44.2344px;">Email</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="Permission: activate to sort column ascending"
style="width: 79.7969px;">Permission</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="First Name: activate to sort column ascending"
style="width: 79.7969px;">Pricipal_type</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="First Name: activate to sort column ascending"
style="width: 79.7969px;">Active</th>
<th class="text-center dt-no-sorting sorting" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1"
aria-label="Action: activate to sort column ascending"
style="width: 51.625px;">Action</th>
</tr>
</thead>
<tbody>
@@ -100,17 +112,30 @@
<tr role="row">
<th class="checkbox-column text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
style="width: 69.2656px;"> Record Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 79.7969px;">Name</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 143.516px;">Email</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 143.516px;">Permission</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
style="width: 98.875px;">Principal Type</th>
<th class="text-center dt-no-sorting" tabindex="0"
aria-label=" Record Id : activate to sort column descending"
style="width: 69.2656px;"> Id </th>
<th class="checkbox-column text-center sorting_asc" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1" aria-sort="ascending"
aria-label=" Record Id : activate to sort column descending"
style="width: 69.2656px;"> Id </th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1" aria-label="First Name: activate to sort column ascending"
style="width: 44.2344px;">Name</th>
<th class="text-center sorting" tabindex="0" aria-controls="style-3" rowspan="1"
colspan="1" aria-label="Email: activate to sort column ascending"
style="width: 44.2344px;">Email</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="Permission: activate to sort column ascending"
style="width: 79.7969px;">Groups</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="First Name: activate to sort column ascending"
style="width: 79.7969px;">Pricipal_type</th>
<th class="sorting" tabindex="0" aria-controls="style-3" rowspan="1" colspan="1"
aria-label="First Name: activate to sort column ascending"
style="width: 79.7969px;">Active</th>
<th class="text-center dt-no-sorting sorting" tabindex="0"
aria-controls="style-3" rowspan="1" colspan="1"
aria-label="Action: activate to sort column ascending"
style="width: 51.625px;">Action</th>
</tr>
</thead>
@@ -153,26 +178,42 @@
// Define DataTable instance
var dataTableInstance;
var mainUrl = "{% url 'module_iam:role_list' %}?deleted_flag=False"
var editUrl = "{% url 'module_iam:role_edit' pk=0 %}"
var actionUrl = "{% url 'module_iam:role_action' %}"
var adminMainUrl = "{% url 'module_iam:principal_group_link_list' %}?deleted_flag=False"
var editUrl = "{% url 'module_iam:principal_edit' pk=0 %}"
var actionUrl = "{% url 'module_iam:principal_group_link_action' %}"
// Entry point
$(document).ready(function() {
const table1Settings = {
tableId: '#table',
MainUrl: "{% url 'module_iam:principal_group_link_list' %}?deleted_flag=False",
actionUrl: "{% url 'module_iam:principal_group_link_action' %}",
editUrl: "{% url 'module_iam:principal_edit' pk=0 %}"
};
tableName = $('#table');
dataTableInstance = initializeDataTable(tableName, mainUrl);
viewClickEvent(dataTableInstance)
activeSwitchEventListener()
const table2Settings = {
tableId: '#table2',
MainUrl: "{% url 'module_iam:principal_group_link_list_sub' %}?deleted_flag=False",
actionUrl: "{% url 'module_iam:principal_group_link_action' %}",
editUrl: "{% url 'module_iam:principal_group_link_edit' pk=0 %}"
};
dataTableInstance = initializeDataTable(table1Settings);
activeSwitchEventListener(dataTableInstance);
dataTable2Instance = initialize2DataTable(table2Settings);
activeSwitchEventListener(dataTable2Instance);
});
// Function to initialize DataTable
function initializeDataTable(tableName, mainUrl) {
return tableName.DataTable({
function initializeDataTable(tableSettings) {
return $(tableSettings.tableId).DataTable({
processing: true,
serverSide: true,
ajax: {
url: mainUrl,
url: tableSettings.MainUrl,
type: "GET",
},
columns: [
@@ -186,9 +227,13 @@ function initializeDataTable(tableName, mainUrl) {
return `<ul><span class="badge badge-success">All Access Permission</span></ul>`
}
},
{ data: "principal_type__name", className: "text-center"},
{ data: "principal_type_name", className: "text-center" },
{ data: "is_active", className: "text-center", render: renderSwitch },
{ data: null, className: "text-center", render: renderActions }
{ data: null, className: "text-center",
render: (data, type, row) => {
return renderActions(data, type, row, tableSettings.editUrl);
}
}
],
debug: true,
columnDefs: [
@@ -215,7 +260,9 @@ function initializeDataTable(tableName, mainUrl) {
{
text: 'Archive',
className: "btn btn-dark buttons-archive",
action: archiveAction,
action: function (e, dt, node, config) {
archiveAction(e, dt, node, config, tableSettings);
},
init: function(api, node, config){
$(node).hide();
}
@@ -243,28 +290,106 @@ function initializeDataTable(tableName, mainUrl) {
});
}
function renderResources(data, type, row) {
if (type === 'display' && row.resources) {
let html = '<ul>';
for (const [resource, actions] of Object.entries(row.resources)) {
html += `<li class="mb-1"><span class="badge badge-primary">${resource}</span>`;
for (const action of actions) {
html += `<span class="badge badge-secondary">${action}</span>`;
// Function to initialize DataTable
function initialize2DataTable(tableSettings) {
return $(tableSettings.tableId).DataTable({
processing: true,
serverSide: true,
ajax: {
url: tableSettings.MainUrl,
type: "GET",
},
columns: [
{ data: null, className: "text-center", render: renderCheckbox },
{ data: "id", className: "text-center" },
{ data: "first_name" },
{ data: "email" },
{
data: "groups",
render: renderGroups
},
{ data: "principal_type_name", className: "text-center" },
{ data: "is_active", className: "text-center", render: renderSwitch },
{ data: null, className: "text-center",
render: (data, type, row) => {
return renderActions(data, type, row, tableSettings.editUrl);
}
}
html += '</li>';
],
debug: true,
columnDefs: [
{
targets: [1, 2],
searchable: true,
orderable: true
},
{
targets: [3],
searchable: true,
orderable: false
},
{
targets: [0,-1], // Targeting the last column (action column)
searchable: false,
orderable: false
},
],
dom: "<'dt--top-section'<'row'<'col-12 col-sm-6 d-flex justify-content-sm-start justify-content-center'l><'col-12 col-sm-6 d-flex justify-content-sm-end justify-content-center mt-sm-0 mt-3'Bf>>>" +
"<'table-responsive'tr>" +
"<'dt--bottom-section d-sm-flex justify-content-sm-between text-center'<'dt--pages-count mb-sm-0 mb-3'i><'dt--pagination'p>>",
buttons: [
{
text: 'Archive',
className: "btn btn-dark buttons-archive",
action: function (e, dt, node, config) {
archiveAction(e, dt, node, config, tableSettings);
},
init: function(api, node, config){
$(node).hide();
}
},
{
text: 'View Archive List',
className: "btn btn-dark ",
action: function () {
// Add your action here, e.g., redirect to archive page
window.location.href = '/archive';
}
}
],
oLanguage: {
oPaginate: { "sPrevious": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>', "sNext": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' },
sInfo: "Showing page _PAGE_ of _PAGES_",
sSearch: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>',
sSearchPlaceholder: "Search...",
sLengthMenu: " _MENU_",
},
stripeClasses: [],
lengthMenu: [5, 10, 20, 50],
pageLength: 10,
initComplete: initCompleteCallback
});
}
function renderGroups(data, type, row) {
if (type === 'display' && row.groups) {
let html = '<ul>';
for (const [index, group] of Object.entries(row.groups)) {
html += `<li class="mb-1"><span class="badge badge-warning">${group.name}</span></li>`;
}
html += '</ul>';
return html;
} else if (type === 'display' && !row.resources) {
return '<span class="badge badge-danger">No Permission assigned</span>';
} else if (type === 'display' && !row.groups) {
return '<span class="badge badge-danger">No Group assigned</span>';
} else {
return '';
}
}
// Function to reload the DataTable
function reloadDataTable() {
dataTableInstance.ajax.reload();
function reloadDataTable(tableInstance) {
tableInstance.ajax.reload();
}
// Render checkbox
@@ -286,7 +411,7 @@ function renderSwitch(data, type, row) {
}
// Render actions
function renderActions(data, type, row) {
function renderActions(data, type, row, editUrl) {
return `
<ul class="table-controls">
<li>
@@ -299,15 +424,30 @@ function renderActions(data, type, row) {
</ul>`;
}
// Callback function for DataTable initialization complete event
function initCompleteCallback(settings) {
var tableId = '#' + settings.sTableId; // Get the ID of the current table
var api = this.api();
// Add event listener for checkbox change
$(document).on('change', tableId + ' input[type="checkbox"]', function () {
var checkedCount = $(tableId + ' tbody input.archive-checkbox:checked').length;
var archiveButton = $(' .buttons-archive');
console.log("checkbox is checked", checkedCount);
archiveButton.toggle(checkedCount > 0);
});
}
// Function to handle archive action
function archiveAction() {
function archiveAction(e, dt, node, config, tableSettings) {
// Get all the checked checkboxes
var checkedCheckboxes = $('.archive-checkbox:checked');
var checkedCheckboxes = dt.$('.archive-checkbox:checked');
// If no checkboxes are checked, show an error message
if (checkedCheckboxes.length === 0) {
Swal.fire({
title: 'No users selected',
text: 'Please select at least one user to archive.',
title: 'No Record selected',
text: 'Please select at least one Record to archive.',
icon: 'error',
showConfirmButton: true
});
@@ -330,7 +470,7 @@ function archiveAction() {
if (result.isConfirmed) {
// Perform archive action
$.ajax({
url: actionUrl, // Replace with your archive endpoint
url: tableSettings.actionUrl, // Replace with your archive endpoint
type: 'POST',
data: {
action: "archive",
@@ -346,7 +486,7 @@ function archiveAction() {
showConfirmButton: true
});
// Optionally, you can reload the DataTable after successful archive
reloadDataTable();
reloadDataTable(dt);
},
error: function(response) {
// Show error message
@@ -362,33 +502,12 @@ function archiveAction() {
});
}
// Callback function for DataTable initialization complete event
function initCompleteCallback() {
var api = this.api();
// Add event listener for checkbox change
$('body').on('change', 'input[type="checkbox"]', function () {
var checkedCount = $('tbody input.archive-checkbox:checked').length;
var archiveButton = $('.buttons-archive');
console.log("checkbox is checked", + checkedCount)
archiveButton.toggle(checkedCount > 0);
});
}
// Function to handle click event for view button
function viewClickEvent(dataTableInstance) {
$('body').on('click', '.view', function(){
var id =$(this).data('id');
var rowData = dataTableInstance.row($(this).closest('tr')).data();
});
}
// Function to add event listener for switch
function activeSwitchEventListener() {
function activeSwitchEventListener(tableInstance) {
// Add event listener for switch change event
$('body').on('change', '.switch-input', function() {
tableInstance.on('change', '.switch-input', function() {
var rowId = $(this).closest('tr').find('.switch-input').data('id');
var isActive = $(this).prop('checked');
console.log(rowId, isActive)

View File

@@ -0,0 +1,64 @@
{% extends 'base_structure/layout/base_template.html' %}
{% load static %}
{% block stylesheet %}
<!-- include required css cdn link through html here -->
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
{% endblock %}
{% block content %}
<div class="row layout-top-spacing">
<div class="col-lg-12">
<div class="row mb-2">
<div class="col">
<h3>{{operation}} Principal Group Link</h3>
</div>
<div class="col text-end">
<button class="btn btn-dark mb-2 me-4" onclick="history.back()">
<i class="fa fa-arrow-left"></i>
Back
</button>
</div>
</div>
<div class="row layout-spacing">
<div class="col-lg-12">
<div class="statbox widget box box-shadow">
<div class="widget-content widget-content-area">
<form method="post" autocomplete="off">
{% csrf_token %}
{% include 'base_structure/includes/dynamic_template_form.html' with form=form %}
<div class="mt-4 mb-0">
<div class="d-grid"><button class="btn btn-primary btn-block" type="submit">Submit</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block javascript %}
<!-- include required js cdn link through html here -->
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('.js-example-basic-multiple').select2({
placeholder: 'Select options',
allowClear: true,
tags: true, // Allow the user to enter custom tags
tokenSeparators: [',', ' '], // Customize token separators
closeOnSelect: false // Keep the dropdown open after selection
});
});
</script>
{% endblock %}