update on the form submittion
This commit is contained in:
parent
1aa8b6800a
commit
1212161493
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -6,7 +6,7 @@ from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
from .models import FormTemplate, FormStage, FormField, FormSubmission, FieldResponse
|
||||
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
|
||||
from unfold.admin import ModelAdmin
|
||||
|
||||
@ -54,3 +54,57 @@ class CandidateAdmin(ModelAdmin):
|
||||
class TrainingMaterialAdmin(ModelAdmin):
|
||||
list_display = ('title', 'created_by', 'created_at')
|
||||
search_fields = ('title', 'content')
|
||||
|
||||
|
||||
class FormFieldInline(admin.TabularInline):
|
||||
model = FormField
|
||||
extra = 0
|
||||
fields = ('label', 'field_type', 'required', 'order', 'is_predefined')
|
||||
ordering = ('order',)
|
||||
|
||||
class FormStageInline(admin.TabularInline):
|
||||
model = FormStage
|
||||
extra = 0
|
||||
fields = ('name', 'order', 'is_predefined')
|
||||
ordering = ('order',)
|
||||
inlines = [FormFieldInline]
|
||||
|
||||
@admin.register(FormTemplate)
|
||||
class FormTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'created_by', 'created_at', 'is_active', 'get_stage_count')
|
||||
list_filter = ('is_active', 'created_at', 'created_by')
|
||||
search_fields = ('name', 'description', 'created_by__username')
|
||||
inlines = [FormStageInline]
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
def get_stage_count(self, obj):
|
||||
return obj.get_stage_count()
|
||||
get_stage_count.short_description = 'Stages'
|
||||
|
||||
@admin.register(FormStage)
|
||||
class FormStageAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'template', 'order', 'is_predefined')
|
||||
list_filter = ('is_predefined', 'template')
|
||||
search_fields = ('name', 'template__name')
|
||||
ordering = ('template', 'order')
|
||||
|
||||
@admin.register(FormField)
|
||||
class FormFieldAdmin(admin.ModelAdmin):
|
||||
list_display = ('label', 'field_type', 'stage', 'required', 'order', 'is_predefined')
|
||||
list_filter = ('field_type', 'required', 'is_predefined', 'stage__template')
|
||||
search_fields = ('label', 'stage__name', 'stage__template__name')
|
||||
ordering = ('stage', 'order')
|
||||
|
||||
@admin.register(FormSubmission)
|
||||
class FormSubmissionAdmin(admin.ModelAdmin):
|
||||
list_display = ('template', 'applicant_name', 'applicant_email', 'submitted_at')
|
||||
list_filter = ('submitted_at', 'template')
|
||||
search_fields = ('applicant_name', 'applicant_email', 'template__name')
|
||||
readonly_fields = ('submitted_at',)
|
||||
|
||||
@admin.register(FieldResponse)
|
||||
class FieldResponseAdmin(admin.ModelAdmin):
|
||||
list_display = ('field', 'submission', 'display_value')
|
||||
list_filter = ('field__field_type', 'submission__template')
|
||||
search_fields = ('field__label', 'submission__applicant_name')
|
||||
readonly_fields = ('display_value',)
|
||||
@ -0,0 +1,156 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-05 09:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0012_form_formsubmission_uploadedfile'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FormField',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('label', models.CharField(help_text='Label for the field', max_length=200)),
|
||||
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
|
||||
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
|
||||
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
|
||||
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
|
||||
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
|
||||
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Field',
|
||||
'verbose_name_plural': 'Form Fields',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormStage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Name of the stage', max_length=200)),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Stage',
|
||||
'verbose_name_plural': 'Form Stages',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='formsubmission',
|
||||
name='form',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='uploadedfile',
|
||||
name='submission',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='formsubmission',
|
||||
options={'ordering': ['-submitted_at'], 'verbose_name': 'Form Submission', 'verbose_name_plural': 'Form Submissions'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='formsubmission',
|
||||
name='ip_address',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='formsubmission',
|
||||
name='submission_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='formsubmission',
|
||||
name='user_agent',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsubmission',
|
||||
name='applicant_email',
|
||||
field=models.EmailField(blank=True, help_text='Email of the applicant', max_length=254),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsubmission',
|
||||
name='applicant_name',
|
||||
field=models.CharField(blank=True, help_text='Name of the applicant', max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsubmission',
|
||||
name='submitted_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FieldResponse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
|
||||
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
|
||||
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Field Response',
|
||||
'verbose_name_plural': 'Field Responses',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formfield',
|
||||
name='stage',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Name of the form template', max_length=200)),
|
||||
('description', models.TextField(blank=True, help_text='Description of the form template')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this template is active')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Template',
|
||||
'verbose_name_plural': 'Form Templates',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formstage',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formsubmission',
|
||||
name='template',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Shared Form Template',
|
||||
'verbose_name_plural': 'Shared Form Templates',
|
||||
},
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Form',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UploadedFile',
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -282,34 +282,194 @@ class ZoomMeeting(Base):
|
||||
return self.topic
|
||||
|
||||
|
||||
class Form(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
structure = models.JSONField(default=dict) # Stores the form schema
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
class FormTemplate(models.Model):
|
||||
"""
|
||||
Represents a complete form template with multiple stages
|
||||
"""
|
||||
name = models.CharField(max_length=200, help_text="Name of the form template")
|
||||
description = models.TextField(blank=True, help_text="Description of the form template")
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='form_templates')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_active = models.BooleanField(default=True, help_text="Whether this template is active")
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
verbose_name = 'Form Template'
|
||||
verbose_name_plural = 'Form Templates'
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
return self.name
|
||||
|
||||
def get_stage_count(self):
|
||||
return self.stages.count()
|
||||
|
||||
def get_field_count(self):
|
||||
return sum(stage.fields.count() for stage in self.stages.all())
|
||||
|
||||
|
||||
class FormStage(models.Model):
|
||||
"""
|
||||
Represents a stage/section within a form template
|
||||
"""
|
||||
template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name='stages')
|
||||
name = models.CharField(max_length=200, help_text="Name of the stage")
|
||||
order = models.PositiveIntegerField(default=0, help_text="Order of the stage in the form")
|
||||
is_predefined = models.BooleanField(default=False, help_text="Whether this is a default resume stage")
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
verbose_name = 'Form Stage'
|
||||
verbose_name_plural = 'Form Stages'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.template.name} - {self.name}"
|
||||
|
||||
def clean(self):
|
||||
if self.order < 0:
|
||||
raise ValidationError("Order must be a positive integer")
|
||||
|
||||
|
||||
class FormField(models.Model):
|
||||
"""
|
||||
Represents a single field within a form stage
|
||||
"""
|
||||
FIELD_TYPES = [
|
||||
('text', 'Text Input'),
|
||||
('email', 'Email'),
|
||||
('phone', 'Phone'),
|
||||
('textarea', 'Text Area'),
|
||||
('file', 'File Upload'),
|
||||
('date', 'Date Picker'),
|
||||
('select', 'Dropdown'),
|
||||
('radio', 'Radio Buttons'),
|
||||
('checkbox', 'Checkboxes'),
|
||||
]
|
||||
|
||||
stage = models.ForeignKey(FormStage, on_delete=models.CASCADE, related_name='fields')
|
||||
label = models.CharField(max_length=200, help_text="Label for the field")
|
||||
field_type = models.CharField(max_length=20, choices=FIELD_TYPES, help_text="Type of the field")
|
||||
placeholder = models.CharField(max_length=200, blank=True, help_text="Placeholder text")
|
||||
required = models.BooleanField(default=False, help_text="Whether the field is required")
|
||||
order = models.PositiveIntegerField(default=0, help_text="Order of the field in the stage")
|
||||
is_predefined = models.BooleanField(default=False, help_text="Whether this is a default field")
|
||||
|
||||
# For selection fields (select, radio, checkbox)
|
||||
options = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text="Options for selection fields (stored as JSON array)"
|
||||
)
|
||||
|
||||
# For file upload fields
|
||||
file_types = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')"
|
||||
)
|
||||
max_file_size = models.PositiveIntegerField(
|
||||
default=5,
|
||||
help_text="Maximum file size in MB (default: 5MB)"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['order']
|
||||
verbose_name = 'Form Field'
|
||||
verbose_name_plural = 'Form Fields'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.stage.name} - {self.label}"
|
||||
|
||||
def clean(self):
|
||||
# Validate options for selection fields
|
||||
if self.field_type in ['select', 'radio', 'checkbox']:
|
||||
if not isinstance(self.options, list):
|
||||
raise ValidationError("Options must be a list for selection fields")
|
||||
else:
|
||||
# Clear options for non-selection fields
|
||||
if self.options:
|
||||
self.options = []
|
||||
|
||||
# Validate file settings for file fields
|
||||
if self.field_type == 'file':
|
||||
if not self.file_types:
|
||||
self.file_types = '.pdf,.doc,.docx'
|
||||
if self.max_file_size <= 0:
|
||||
raise ValidationError("Max file size must be greater than 0")
|
||||
else:
|
||||
# Clear file settings for non-file fields
|
||||
self.file_types = ''
|
||||
self.max_file_size = 0
|
||||
|
||||
if self.order < 0:
|
||||
raise ValidationError("Order must be a positive integer")
|
||||
|
||||
|
||||
class FormSubmission(models.Model):
|
||||
form = models.ForeignKey(Form, on_delete=models.CASCADE, related_name='submissions')
|
||||
submission_data = models.JSONField(default=dict) # Stores form responses
|
||||
"""
|
||||
Represents a completed form submission by an applicant
|
||||
"""
|
||||
template = models.ForeignKey(FormTemplate, on_delete=models.CASCADE, related_name='submissions')
|
||||
submitted_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='form_submissions')
|
||||
submitted_at = models.DateTimeField(auto_now_add=True)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent = models.TextField(blank=True)
|
||||
applicant_name = models.CharField(max_length=200, blank=True, help_text="Name of the applicant")
|
||||
applicant_email = models.EmailField(blank=True, help_text="Email of the applicant")
|
||||
|
||||
class Meta:
|
||||
ordering = ['-submitted_at']
|
||||
verbose_name = 'Form Submission'
|
||||
verbose_name_plural = 'Form Submissions'
|
||||
|
||||
class UploadedFile(models.Model):
|
||||
submission = models.ForeignKey(FormSubmission, on_delete=models.CASCADE, related_name='files')
|
||||
field_id = models.CharField(max_length=100)
|
||||
file = models.FileField(upload_to='form_uploads/%Y/%m/%d/')
|
||||
original_filename = models.CharField(max_length=255)
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
def __str__(self):
|
||||
return f"Submission for {self.template.name} - {self.submitted_at.strftime('%Y-%m-%d %H:%M')}"
|
||||
|
||||
|
||||
class FieldResponse(models.Model):
|
||||
"""
|
||||
Represents a response to a specific field in a form submission
|
||||
"""
|
||||
submission = models.ForeignKey(FormSubmission, on_delete=models.CASCADE, related_name='responses')
|
||||
field = models.ForeignKey(FormField, on_delete=models.CASCADE, related_name='responses')
|
||||
|
||||
# Store the response value as JSON to handle different data types
|
||||
value = models.JSONField(null=True, blank=True, help_text="Response value (stored as JSON)")
|
||||
|
||||
# For file uploads, store the file path
|
||||
uploaded_file = models.FileField(upload_to='form_uploads/', null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Field Response'
|
||||
verbose_name_plural = 'Field Responses'
|
||||
|
||||
def __str__(self):
|
||||
return f"Response to {self.field.label} in {self.submission}"
|
||||
|
||||
@property
|
||||
def display_value(self):
|
||||
"""Return a human-readable representation of the response value"""
|
||||
if self.uploaded_file:
|
||||
return f"File: {self.uploaded_file.name}"
|
||||
elif self.value is None:
|
||||
return ""
|
||||
elif isinstance(self.value, list):
|
||||
return ", ".join(str(v) for v in self.value)
|
||||
else:
|
||||
return str(self.value)
|
||||
|
||||
|
||||
# Optional: Create a model for form templates that can be shared across organizations
|
||||
class SharedFormTemplate(models.Model):
|
||||
"""
|
||||
Represents a form template that can be shared across different organizations/users
|
||||
"""
|
||||
template = models.OneToOneField(FormTemplate, on_delete=models.CASCADE)
|
||||
is_public = models.BooleanField(default=False, help_text="Whether this template is publicly available")
|
||||
shared_with = models.ManyToManyField(User, blank=True, related_name='shared_templates')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Shared Form Template'
|
||||
verbose_name_plural = 'Shared Form Templates'
|
||||
|
||||
def __str__(self):
|
||||
return f"Shared: {self.template.name}"
|
||||
1
recruitment/templatetags/__init__.py
Normal file
1
recruitment/templatetags/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# This file makes the templatetags directory a Python package
|
||||
BIN
recruitment/templatetags/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
recruitment/templatetags/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
52
recruitment/templatetags/form_filters.py
Normal file
52
recruitment/templatetags/form_filters.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django import template
|
||||
from rich import print
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def get_stage_responses(stage_responses, stage_id):
|
||||
"""
|
||||
Template tag to get responses for a specific stage.
|
||||
Usage: {% get_stage_responses stage_responses stage.id as stage_data %}
|
||||
"""
|
||||
|
||||
if stage_responses and stage_id in stage_responses:
|
||||
return stage_responses[stage_id]
|
||||
return []
|
||||
|
||||
@register.simple_tag
|
||||
def get_all_responses_flat(stage_responses):
|
||||
"""
|
||||
Template tag to get all responses flattened for table display.
|
||||
Usage: {% get_all_responses_flat stage_responses as all_responses %}
|
||||
"""
|
||||
all_responses = []
|
||||
if stage_responses:
|
||||
print(stage_responses.get(9).get("responses")[0].value)
|
||||
for stage_id, responses in stage_responses.items():
|
||||
for response in responses:
|
||||
# Check if response is an object or string
|
||||
if hasattr(response, 'stage') and hasattr(response, 'field'):
|
||||
stage_name = response.stage.name if hasattr(response.stage, 'name') else f"Stage {stage_id}"
|
||||
field_label = response.field.label if hasattr(response.field, 'label') else "Unknown Field"
|
||||
field_type = response.field.get_field_type_display() if hasattr(response.field, 'get_field_type_display') else "Unknown Type"
|
||||
required = response.field.required if hasattr(response.field, 'required') else False
|
||||
value = response.value if hasattr(response, 'value') else response
|
||||
uploaded_file = response.uploaded_file if hasattr(response, 'uploaded_file') else None
|
||||
else:
|
||||
stage_name = f"Stage {stage_id}"
|
||||
field_label = "Unknown Field"
|
||||
field_type = "Text"
|
||||
required = False
|
||||
value = response
|
||||
uploaded_file = None
|
||||
|
||||
all_responses.append({
|
||||
'stage_name': stage_name,
|
||||
'field_label': field_label,
|
||||
'field_type': field_type,
|
||||
'required': required,
|
||||
'value': value,
|
||||
'uploaded_file': uploaded_file
|
||||
})
|
||||
return all_responses
|
||||
@ -46,17 +46,28 @@ urlpatterns = [
|
||||
path('api/<slug:slug>/edit/', views.edit_job, name='edit_job_api'),
|
||||
|
||||
#
|
||||
path('form_builder/', views.form_builder, name='form_builder'),
|
||||
|
||||
# Form Preview URLs
|
||||
path('forms/', views.form_list, name='form_list'),
|
||||
path('forms/<int:form_id>/', views.form_preview, name='form_preview'),
|
||||
path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'),
|
||||
path('forms/<int:form_id>/embed/', views.form_embed, name='form_embed'),
|
||||
path('forms/<int:form_id>/submissions/', views.form_submissions, name='form_submissions'),
|
||||
path('forms/<int:form_id>/edit/', views.edit_form, name='edit_form'),
|
||||
path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
|
||||
path('api/forms/<int:form_id>/load/', views.load_form, name='load_form'),
|
||||
path('api/forms/<int:form_id>/update/', views.update_form_builder, name='update_form_builder'),
|
||||
# path('forms/', views.form_list, name='form_list'),
|
||||
path('forms/builder/', views.form_builder, name='form_builder'),
|
||||
path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'),
|
||||
path('forms/', views.form_templates_list, name='form_templates_list'),
|
||||
|
||||
path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'),
|
||||
|
||||
path('api/templates/', views.list_form_templates, name='list_form_templates'),
|
||||
path('api/templates/save/', views.save_form_template, name='save_form_template'),
|
||||
path('api/templates/<int:template_id>/', views.load_form_template, name='load_form_template'),
|
||||
path('api/templates/<int:template_id>/delete/', views.delete_form_template, name='delete_form_template'),
|
||||
# path('forms/<int:form_id>/', views.form_preview, name='form_preview'),
|
||||
# path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'),
|
||||
# path('forms/<int:form_id>/embed/', views.form_embed, name='form_embed'),
|
||||
# path('forms/<int:form_id>/submissions/', views.form_submissions, name='form_submissions'),
|
||||
# path('forms/<int:form_id>/edit/', views.edit_form, name='edit_form'),
|
||||
# path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
|
||||
# path('api/forms/<int:form_id>/load/', views.load_form, name='load_form'),
|
||||
# path('api/forms/<int:form_id>/update/', views.update_form_builder, name='update_form_builder'),
|
||||
|
||||
]
|
||||
|
||||
@ -3,7 +3,6 @@ import requests
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import JsonResponse
|
||||
from recruitment.models import FormSubmission,Form,UploadedFile
|
||||
from datetime import datetime
|
||||
from django.views import View
|
||||
from django.db.models import Q
|
||||
@ -14,11 +13,13 @@ from rest_framework import viewsets
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator
|
||||
from .linkedin_service import LinkedInService
|
||||
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission
|
||||
from .models import ZoomMeeting, Job, Candidate, JobPosting
|
||||
from .serializers import JobPostingSerializer, CandidateSerializer
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.views.generic import CreateView,UpdateView,DetailView,ListView
|
||||
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
import logging
|
||||
logger=logging.getLogger(__name__)
|
||||
@ -334,244 +335,484 @@ def applicant_job_detail(request,slug):
|
||||
job=get_object_or_404(JobPosting,slug=slug,status='ACTIVE')
|
||||
return render(request,'jobs/applicant_job_detail.html',{'job':job})
|
||||
|
||||
def form_builder(request):
|
||||
return render(request,'form_builder.html')
|
||||
|
||||
|
||||
# Form Preview Views
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.core.paginator import Paginator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
import json
|
||||
# from django.http import JsonResponse
|
||||
# from django.views.decorators.csrf import csrf_exempt
|
||||
# from django.core.paginator import Paginator
|
||||
# from django.contrib.auth.decorators import login_required
|
||||
# import json
|
||||
|
||||
def form_list(request):
|
||||
"""Display list of all available forms"""
|
||||
forms = Form.objects.filter(is_active=True).order_by('-created_at')
|
||||
# def form_list(request):
|
||||
# """Display list of all available forms"""
|
||||
# forms = Form.objects.filter(is_active=True).order_by('-created_at')
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(forms, 12)
|
||||
# # Pagination
|
||||
# paginator = Paginator(forms, 12)
|
||||
# page_number = request.GET.get('page')
|
||||
# page_obj = paginator.get_page(page_number)
|
||||
|
||||
# return render(request, 'forms/form_list.html', {
|
||||
# 'page_obj': page_obj
|
||||
# })
|
||||
|
||||
# def form_preview(request, form_id):
|
||||
# """Display form preview for end users"""
|
||||
# form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
|
||||
# # Get submission count for analytics
|
||||
# submission_count = form.submissions.count()
|
||||
|
||||
# return render(request, 'forms/form_preview.html', {
|
||||
# 'form': form,
|
||||
# 'submission_count': submission_count,
|
||||
# 'is_embed': request.GET.get('embed', 'false') == 'true'
|
||||
# })
|
||||
|
||||
# @csrf_exempt
|
||||
# def form_submit(request, form_id):
|
||||
# """Handle form submission via AJAX"""
|
||||
# if request.method != 'POST':
|
||||
# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
# form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
|
||||
# try:
|
||||
# # Parse form data
|
||||
# submission_data = {}
|
||||
# files = {}
|
||||
|
||||
# # Process regular form fields
|
||||
# for key, value in request.POST.items():
|
||||
# if key != 'csrfmiddlewaretoken':
|
||||
# submission_data[key] = value
|
||||
|
||||
# # Process file uploads
|
||||
# for key, file in request.FILES.items():
|
||||
# if file:
|
||||
# files[key] = file
|
||||
|
||||
# # Create form submission
|
||||
# submission = FormSubmission.objects.create(
|
||||
# form=form,
|
||||
# submission_data=submission_data,
|
||||
# ip_address=request.META.get('REMOTE_ADDR'),
|
||||
# user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
# )
|
||||
|
||||
# # Handle file uploads
|
||||
# for field_id, file in files.items():
|
||||
# UploadedFile.objects.create(
|
||||
# submission=submission,
|
||||
# field_id=field_id,
|
||||
# file=file,
|
||||
# original_filename=file.name
|
||||
# )
|
||||
|
||||
# # TODO: Send email notification if configured
|
||||
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'message': 'Form submitted successfully!',
|
||||
# 'submission_id': submission.id
|
||||
# })
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error submitting form {form_id}: {e}")
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'An error occurred while submitting the form. Please try again.'
|
||||
# }, status=500)
|
||||
|
||||
# def form_embed(request, form_id):
|
||||
# """Display embeddable version of form"""
|
||||
# form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
|
||||
# return render(request, 'forms/form_embed.html', {
|
||||
# 'form': form,
|
||||
# 'is_embed': True
|
||||
# })
|
||||
|
||||
# @login_required
|
||||
# def save_form_builder(request):
|
||||
# """Save form from builder to database"""
|
||||
# if request.method != 'POST':
|
||||
# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
# try:
|
||||
# data = json.loads(request.body)
|
||||
# form_data = data.get('form', {})
|
||||
|
||||
# # Check if this is an update or create
|
||||
# form_id = data.get('form_id')
|
||||
|
||||
# if form_id:
|
||||
# # Update existing form
|
||||
# form = Form.objects.get(id=form_id, created_by=request.user)
|
||||
# form.title = form_data.get('title', 'Untitled Form')
|
||||
# form.description = form_data.get('description', '')
|
||||
# form.structure = form_data
|
||||
# form.save()
|
||||
# else:
|
||||
# # Create new form
|
||||
# form = Form.objects.create(
|
||||
# title=form_data.get('title', 'Untitled Form'),
|
||||
# description=form_data.get('description', ''),
|
||||
# structure=form_data,
|
||||
# created_by=request.user
|
||||
# )
|
||||
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'form_id': form.id,
|
||||
# 'message': 'Form saved successfully!'
|
||||
# })
|
||||
|
||||
# except json.JSONDecodeError:
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'Invalid JSON data'
|
||||
# }, status=400)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error saving form: {e}")
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'An error occurred while saving the form'
|
||||
# }, status=500)
|
||||
|
||||
# @login_required
|
||||
# def load_form(request, form_id):
|
||||
# """Load form data for editing in builder"""
|
||||
# try:
|
||||
# form = get_object_or_404(Form, id=form_id, created_by=request.user)
|
||||
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'form': {
|
||||
# 'id': form.id,
|
||||
# 'title': form.title,
|
||||
# 'description': form.description,
|
||||
# 'structure': form.structure
|
||||
# }
|
||||
# })
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error loading form {form_id}: {e}")
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'An error occurred while loading the form'
|
||||
# }, status=500)
|
||||
|
||||
# @csrf_exempt
|
||||
# def update_form_builder(request, form_id):
|
||||
# """Update existing form from builder"""
|
||||
# if request.method != 'POST':
|
||||
# return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
# try:
|
||||
# form = get_object_or_404(Form, id=form_id)
|
||||
|
||||
# # Check if user has permission to edit this form
|
||||
# if form.created_by != request.user:
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'You do not have permission to edit this form'
|
||||
# }, status=403)
|
||||
|
||||
# data = json.loads(request.body)
|
||||
# form_data = data.get('form', {})
|
||||
|
||||
# # Update form
|
||||
# form.title = form_data.get('title', 'Untitled Form')
|
||||
# form.description = form_data.get('description', '')
|
||||
# form.structure = form_data
|
||||
# form.save()
|
||||
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'form_id': form.id,
|
||||
# 'message': 'Form updated successfully!'
|
||||
# })
|
||||
|
||||
# except json.JSONDecodeError:
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'Invalid JSON data'
|
||||
# }, status=400)
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error updating form {form_id}: {e}")
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': 'An error occurred while updating the form'
|
||||
# }, status=500)
|
||||
|
||||
# def edit_form(request, form_id):
|
||||
# """Display form edit page"""
|
||||
# form = get_object_or_404(Form, id=form_id)
|
||||
|
||||
# # Check if user has permission to edit this form
|
||||
# if form.created_by != request.user:
|
||||
# messages.error(request, 'You do not have permission to edit this form.')
|
||||
# return redirect('form_list')
|
||||
|
||||
# return render(request, 'forms/edit_form.html', {
|
||||
# 'form': form
|
||||
# })
|
||||
|
||||
# def form_submissions(request, form_id):
|
||||
# """View submissions for a specific form"""
|
||||
# form = get_object_or_404(Form, id=form_id, created_by=request.user)
|
||||
# submissions = form.submissions.all().order_by('-submitted_at')
|
||||
|
||||
# # Pagination
|
||||
# paginator = Paginator(submissions, 20)
|
||||
# page_number = request.GET.get('page')
|
||||
# page_obj = paginator.get_page(page_number)
|
||||
|
||||
# return render(request, 'forms/form_submissions.html', {
|
||||
# 'form': form,
|
||||
# 'page_obj': page_obj
|
||||
# })
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def form_builder(request, template_id=None):
|
||||
"""Render the form builder interface"""
|
||||
context = {}
|
||||
if template_id:
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
context['template_id'] = template.id
|
||||
context['template_name'] = template.name
|
||||
return render(request,'forms/form_builder.html',context)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def save_form_template(request):
|
||||
"""Save a new or existing form template"""
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
template_name = data.get('name', 'Untitled Form')
|
||||
stages_data = data.get('stages', [])
|
||||
template_id = data.get('template_id')
|
||||
|
||||
if template_id:
|
||||
# Update existing template
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
template.name = template_name
|
||||
template.save()
|
||||
# Clear existing stages and fields
|
||||
template.stages.all().delete()
|
||||
else:
|
||||
# Create new template
|
||||
template = FormTemplate.objects.create(
|
||||
name=template_name,
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
# Create stages and fields
|
||||
for stage_order, stage_data in enumerate(stages_data):
|
||||
stage = FormStage.objects.create(
|
||||
template=template,
|
||||
name=stage_data['name'],
|
||||
order=stage_order,
|
||||
is_predefined=stage_data.get('predefined', False)
|
||||
)
|
||||
|
||||
for field_order, field_data in enumerate(stage_data['fields']):
|
||||
options = field_data.get('options', [])
|
||||
if not isinstance(options, list):
|
||||
options = []
|
||||
|
||||
file_types = field_data.get('fileTypes', '')
|
||||
max_file_size = field_data.get('maxFileSize', 5)
|
||||
|
||||
FormField.objects.create(
|
||||
stage=stage,
|
||||
label=field_data.get('label', ''),
|
||||
field_type=field_data.get('type', 'text'),
|
||||
placeholder=field_data.get('placeholder', ''),
|
||||
required=field_data.get('required', False),
|
||||
order=field_order,
|
||||
is_predefined=field_data.get('predefined', False),
|
||||
options=options,
|
||||
file_types=file_types,
|
||||
max_file_size=max_file_size
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'template_id': template.id,
|
||||
'message': 'Form template saved successfully!'
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=400)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def load_form_template(request, template_id):
|
||||
"""Load an existing form template"""
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
|
||||
stages = []
|
||||
for stage in template.stages.all():
|
||||
fields = []
|
||||
for field in stage.fields.all():
|
||||
fields.append({
|
||||
'id': field.id,
|
||||
'type': field.field_type,
|
||||
'label': field.label,
|
||||
'placeholder': field.placeholder,
|
||||
'required': field.required,
|
||||
'options': field.options,
|
||||
'fileTypes': field.file_types,
|
||||
'maxFileSize': field.max_file_size,
|
||||
'predefined': field.is_predefined
|
||||
})
|
||||
stages.append({
|
||||
'id': stage.id,
|
||||
'name': stage.name,
|
||||
'predefined': stage.is_predefined,
|
||||
'fields': fields
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'template': {
|
||||
'id': template.id,
|
||||
'name': template.name,
|
||||
'description': template.description,
|
||||
'stages': stages
|
||||
}
|
||||
})
|
||||
|
||||
def form_templates_list(request):
|
||||
"""List all form templates for the current user"""
|
||||
query = request.GET.get('q', '')
|
||||
templates = FormTemplate.objects.filter(created_by=request.user)
|
||||
|
||||
if query:
|
||||
templates = templates.filter(
|
||||
Q(name__icontains=query) | Q(description__icontains=query)
|
||||
)
|
||||
|
||||
templates = templates.order_by('-created_at')
|
||||
paginator = Paginator(templates, 10) # Show 10 templates per page
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
return render(request, 'forms/form_list.html', {
|
||||
'page_obj': page_obj
|
||||
context = {
|
||||
'templates': page_obj,
|
||||
'query': query,
|
||||
}
|
||||
return render(request, 'forms/form_templates_list.html', context)
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def list_form_templates(request):
|
||||
"""List all form templates for the current user"""
|
||||
templates = FormTemplate.objects.filter(created_by=request.user).values(
|
||||
'id', 'name', 'description', 'created_at', 'updated_at'
|
||||
)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'templates': list(templates)
|
||||
})
|
||||
|
||||
def form_preview(request, form_id):
|
||||
"""Display form preview for end users"""
|
||||
form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_form_template(request, template_id):
|
||||
"""Delete a form template"""
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
template.delete()
|
||||
return JsonResponse({'success': True, 'message': 'Form template deleted successfully!'})
|
||||
|
||||
# Get submission count for analytics
|
||||
submission_count = form.submissions.count()
|
||||
|
||||
return render(request, 'forms/form_preview.html', {
|
||||
'form': form,
|
||||
'submission_count': submission_count,
|
||||
'is_embed': request.GET.get('embed', 'false') == 'true'
|
||||
})
|
||||
|
||||
@csrf_exempt
|
||||
def form_submit(request, form_id):
|
||||
"""Handle form submission via AJAX"""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
|
||||
def form_wizard_view(request, template_id):
|
||||
"""Display the form as a step-by-step wizard"""
|
||||
template = get_object_or_404(FormTemplate, id=template_id, is_active=True)
|
||||
return render(request, 'forms/form_wizard.html', {'template_id': template_id})
|
||||
@require_http_methods(["POST"])
|
||||
def submit_form(request, template_id):
|
||||
"""Handle form submission"""
|
||||
try:
|
||||
# Parse form data
|
||||
submission_data = {}
|
||||
files = {}
|
||||
|
||||
# Process regular form fields
|
||||
for key, value in request.POST.items():
|
||||
if key != 'csrfmiddlewaretoken':
|
||||
submission_data[key] = value
|
||||
|
||||
# Process file uploads
|
||||
for key, file in request.FILES.items():
|
||||
if file:
|
||||
files[key] = file
|
||||
template = get_object_or_404(FormTemplate, id=template_id)
|
||||
print(template)
|
||||
|
||||
# Create form submission
|
||||
submission = FormSubmission.objects.create(
|
||||
form=form,
|
||||
submission_data=submission_data,
|
||||
ip_address=request.META.get('REMOTE_ADDR'),
|
||||
user_agent=request.META.get('HTTP_USER_AGENT', '')
|
||||
template=template,
|
||||
applicant_name=request.POST.get('applicant_name', ''),
|
||||
applicant_email=request.POST.get('applicant_email', '')
|
||||
)
|
||||
|
||||
# Process field responses
|
||||
for field_id, value in request.POST.items():
|
||||
if field_id.startswith('field_'):
|
||||
actual_field_id = field_id.replace('field_', '')
|
||||
try:
|
||||
field = FormField.objects.get(id=actual_field_id, stage__template=template)
|
||||
FieldResponse.objects.create(
|
||||
submission=submission,
|
||||
field=field,
|
||||
value=value if value else None
|
||||
)
|
||||
except FormField.DoesNotExist:
|
||||
continue
|
||||
|
||||
# Handle file uploads
|
||||
for field_id, file in files.items():
|
||||
UploadedFile.objects.create(
|
||||
for field_id, uploaded_file in request.FILES.items():
|
||||
if field_id.startswith('field_'):
|
||||
actual_field_id = field_id.replace('field_', '')
|
||||
try:
|
||||
field = FormField.objects.get(id=actual_field_id, stage__template=template)
|
||||
FieldResponse.objects.create(
|
||||
submission=submission,
|
||||
field_id=field_id,
|
||||
file=file,
|
||||
original_filename=file.name
|
||||
field=field,
|
||||
uploaded_file=uploaded_file
|
||||
)
|
||||
|
||||
# TODO: Send email notification if configured
|
||||
except FormField.DoesNotExist:
|
||||
continue
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': 'Form submitted successfully!',
|
||||
'submission_id': submission.id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error submitting form {form_id}: {e}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'An error occurred while submitting the form. Please try again.'
|
||||
}, status=500)
|
||||
|
||||
def form_embed(request, form_id):
|
||||
"""Display embeddable version of form"""
|
||||
form = get_object_or_404(Form, id=form_id, is_active=True)
|
||||
|
||||
return render(request, 'forms/form_embed.html', {
|
||||
'form': form,
|
||||
'is_embed': True
|
||||
})
|
||||
|
||||
@login_required
|
||||
def save_form_builder(request):
|
||||
"""Save form from builder to database"""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
form_data = data.get('form', {})
|
||||
|
||||
# Check if this is an update or create
|
||||
form_id = data.get('form_id')
|
||||
|
||||
if form_id:
|
||||
# Update existing form
|
||||
form = Form.objects.get(id=form_id, created_by=request.user)
|
||||
form.title = form_data.get('title', 'Untitled Form')
|
||||
form.description = form_data.get('description', '')
|
||||
form.structure = form_data
|
||||
form.save()
|
||||
else:
|
||||
# Create new form
|
||||
form = Form.objects.create(
|
||||
title=form_data.get('title', 'Untitled Form'),
|
||||
description=form_data.get('description', ''),
|
||||
structure=form_data,
|
||||
created_by=request.user
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'form_id': form.id,
|
||||
'message': 'Form saved successfully!'
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Invalid JSON data'
|
||||
'error': str(e)
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving form: {e}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'An error occurred while saving the form'
|
||||
}, status=500)
|
||||
|
||||
@login_required
|
||||
def load_form(request, form_id):
|
||||
"""Load form data for editing in builder"""
|
||||
try:
|
||||
form = get_object_or_404(Form, id=form_id, created_by=request.user)
|
||||
def form_submission_details(request, form_id, submission_id):
|
||||
"""Display detailed view of a specific form submission"""
|
||||
# Get the form template and verify ownership
|
||||
form = get_object_or_404(FormTemplate, id=form_id, created_by=request.user)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'form': {
|
||||
'id': form.id,
|
||||
'title': form.title,
|
||||
'description': form.description,
|
||||
'structure': form.structure
|
||||
# Get the specific submission
|
||||
submission = get_object_or_404(FormSubmission, id=submission_id, template=form)
|
||||
|
||||
# Get all stages with their fields
|
||||
stages = form.stages.prefetch_related('fields').order_by('order')
|
||||
|
||||
# Get all responses for this submission, ordered by field order
|
||||
responses = submission.responses.select_related('field').order_by('field__order')
|
||||
|
||||
# Group responses by stage
|
||||
stage_responses = {}
|
||||
for stage in stages:
|
||||
stage_responses[stage.id] = {
|
||||
'stage': stage,
|
||||
'responses': responses.filter(field__stage=stage)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading form {form_id}: {e}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'An error occurred while loading the form'
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
def update_form_builder(request, form_id):
|
||||
"""Update existing form from builder"""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({'success': False, 'error': 'Only POST method allowed'}, status=405)
|
||||
|
||||
try:
|
||||
form = get_object_or_404(Form, id=form_id)
|
||||
|
||||
# Check if user has permission to edit this form
|
||||
if form.created_by != request.user:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'You do not have permission to edit this form'
|
||||
}, status=403)
|
||||
|
||||
data = json.loads(request.body)
|
||||
form_data = data.get('form', {})
|
||||
|
||||
# Update form
|
||||
form.title = form_data.get('title', 'Untitled Form')
|
||||
form.description = form_data.get('description', '')
|
||||
form.structure = form_data
|
||||
form.save()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'form_id': form.id,
|
||||
'message': 'Form updated successfully!'
|
||||
})
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Invalid JSON data'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating form {form_id}: {e}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'An error occurred while updating the form'
|
||||
}, status=500)
|
||||
|
||||
def edit_form(request, form_id):
|
||||
"""Display form edit page"""
|
||||
form = get_object_or_404(Form, id=form_id)
|
||||
|
||||
# Check if user has permission to edit this form
|
||||
if form.created_by != request.user:
|
||||
messages.error(request, 'You do not have permission to edit this form.')
|
||||
return redirect('form_list')
|
||||
|
||||
return render(request, 'forms/edit_form.html', {
|
||||
'form': form
|
||||
})
|
||||
|
||||
def form_submissions(request, form_id):
|
||||
"""View submissions for a specific form"""
|
||||
form = get_object_or_404(Form, id=form_id, created_by=request.user)
|
||||
submissions = form.submissions.all().order_by('-submitted_at')
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(submissions, 20)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
return render(request, 'forms/form_submissions.html', {
|
||||
# print(stages)
|
||||
return render(request, 'forms/form_submission_details.html', {
|
||||
'form': form,
|
||||
'page_obj': page_obj
|
||||
'submission': submission,
|
||||
'stages': stages,
|
||||
'responses': responses,
|
||||
'stage_responses': stage_responses
|
||||
})
|
||||
|
||||
69
shapes/1.html
Normal file
69
shapes/1.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Typeform Clone</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f5f8fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.floating-shapes {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -150px;
|
||||
right: -100px;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -100px;
|
||||
left: -50px;
|
||||
}
|
||||
|
||||
.shape-3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 50%;
|
||||
left: 70%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="floating-shapes">
|
||||
<div class="shape shape-1"></div>
|
||||
<div class="shape shape-2"></div>
|
||||
<div class="shape shape-3"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
181
shapes/2.html
Normal file
181
shapes/2.html
Normal file
@ -0,0 +1,181 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Typeform Clone - Nature Theme</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f0f7f4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nature-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, #f0f7f4 0%, #e6f2eb 100%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
transform-origin: center;
|
||||
animation: float 15s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(5deg); }
|
||||
}
|
||||
|
||||
.leaf-1 {
|
||||
top: 10%;
|
||||
left: 5%;
|
||||
font-size: 80px;
|
||||
color: #5c9d7a;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.leaf-2 {
|
||||
bottom: 15%;
|
||||
right: 10%;
|
||||
font-size: 100px;
|
||||
color: #5c9d7a;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.leaf-3 {
|
||||
top: 30%;
|
||||
right: 5%;
|
||||
font-size: 70px;
|
||||
color: #5c9d7a;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
.leaf-4 {
|
||||
bottom: 25%;
|
||||
left: 8%;
|
||||
font-size: 90px;
|
||||
color: #5c9d7a;
|
||||
animation-delay: 6s;
|
||||
}
|
||||
|
||||
.organic-card {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 10px 30px rgba(92, 157, 122, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
border: 2px solid #e0ebe8;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #5c9d7a;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 0 4px rgba(92, 157, 122, 0.1);
|
||||
}
|
||||
|
||||
.nature-button {
|
||||
background: linear-gradient(135deg, #5c9d7a 0%, #4a8b67 100%);
|
||||
color: white;
|
||||
padding: 14px 30px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nature-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.nature-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.nature-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(92, 157, 122, 0.3);
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex-grow: 1;
|
||||
height: 1px;
|
||||
background-color: #e0ebe8;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
margin: 0 15px;
|
||||
color: #5c9d7a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.social-button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: white;
|
||||
border: 2px solid #e0ebe8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.social-button:hover {
|
||||
border-color: #5c9d7a;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(92, 157, 122, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nature-bg"></div>
|
||||
|
||||
<i class="fas fa-leaf leaf leaf-1"></i>
|
||||
<i class="fas fa-leaf leaf leaf-2"></i>
|
||||
<i class="fas fa-seedling leaf leaf-3"></i>
|
||||
<i class="fas fa-leaf leaf leaf-4"></i>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
117
shapes/3.html
Normal file
117
shapes/3.html
Normal file
@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Typeform Clone - Dark Theme</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #0f0f0f;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.neon-grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 0, 255, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 0, 255, 0.1) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.neon-border {
|
||||
border: 1px solid rgba(255, 0, 255, 0.5);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.neon-border::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, #ff00ff, #00ffff, #ff00ff);
|
||||
z-index: -1;
|
||||
opacity: 0.7;
|
||||
filter: blur(5px);
|
||||
animation: neon-pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes neon-pulse {
|
||||
0% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.glow-text {
|
||||
text-shadow: 0 0 10px rgba(255, 0, 255, 0.7);
|
||||
}
|
||||
|
||||
.neon-button {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
border: 1px solid #ff00ff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.neon-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 0, 255, 0.3), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.neon-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.neon-button:hover {
|
||||
background: rgba(255, 0, 255, 0.1);
|
||||
box-shadow: 0 0 15px rgba(255, 0, 255, 0.7);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.floating-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 0, 255, 0.7) 0%, rgba(0, 255, 255, 0.7) 100%);
|
||||
filter: blur(40px);
|
||||
z-index: -1;
|
||||
opacity: 0.6;
|
||||
animation: float 10s infinite ease-in-out alternate;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
100% { transform: translate(30px, -30px) scale(1.1); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="neon-grid"></div>
|
||||
|
||||
<div class="floating-orb" style="width: 300px; height: 300px; top: 10%; left: 10%;"></div>
|
||||
<div class="floating-orb" style="width: 200px; height: 200px; bottom: 20%; right: 15%; animation-delay: 2s;"</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
154
shapes/4.html
Normal file
154
shapes/4.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Typeform Clone - Minimalist</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f5f5f7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.geometric-shapes {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 100px solid transparent;
|
||||
border-right: 100px solid transparent;
|
||||
border-bottom: 173px solid #000;
|
||||
}
|
||||
|
||||
.square {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.hexagon {
|
||||
width: 120px;
|
||||
height: 69.28px;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
margin: 34.64px 0;
|
||||
}
|
||||
|
||||
.hexagon:before,
|
||||
.hexagon:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
border-left: 60px solid transparent;
|
||||
border-right: 60px solid transparent;
|
||||
}
|
||||
|
||||
.hexagon:before {
|
||||
bottom: 100%;
|
||||
border-bottom: 34.64px solid #000;
|
||||
}
|
||||
|
||||
.hexagon:after {
|
||||
top: 100%;
|
||||
border-top: 34.64px solid #000;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: #007aff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007aff;
|
||||
color: white;
|
||||
padding: 14px 30px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #005ecb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 25px 0;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex-grow: 1;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
margin: 0 15px;
|
||||
color: #8e8e93;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="geometric-shapes">
|
||||
<div class="shape triangle" style="top: 10%; left: 5%;"></div>
|
||||
<div class="shape square" style="top: 20%; right: 10%;"></div>
|
||||
<div class="shape circle" style="bottom: 15%; left: 8%;"></div>
|
||||
<div class="shape hexagon" style="bottom: 25%; right: 15%;"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
static/media/form_uploads/Abdullah_Bakhsh_-_2025.pdf
Normal file
BIN
static/media/form_uploads/Abdullah_Bakhsh_-_2025.pdf
Normal file
Binary file not shown.
BIN
static/media/form_uploads/Summary-QVP-598973.pdf
Normal file
BIN
static/media/form_uploads/Summary-QVP-598973.pdf
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1738
templates/forms/form_builder.html
Normal file
1738
templates/forms/form_builder.html
Normal file
File diff suppressed because it is too large
Load Diff
422
templates/forms/form_submission_details.html
Normal file
422
templates/forms/form_submission_details.html
Normal file
@ -0,0 +1,422 @@
|
||||
{% extends "base.html" %}
|
||||
{% load form_filters %}
|
||||
|
||||
{% block title %}{{ form.name }} - Submission Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1><i class="fas fa-file-alt"></i> Submission Details</h1>
|
||||
<p class="text-muted mb-0">{{ form.name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="" class="btn btn-outline-primary me-2">
|
||||
<i class="fas fa-arrow-left"></i> Back to Submissions
|
||||
</a>
|
||||
<a href="" class="btn btn-outline-primary me-2" target="_blank">
|
||||
<i class="fas fa-eye"></i> Preview Form
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="exportSubmission()">
|
||||
<i class="fas fa-download"></i> Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submission Summary -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ submission.id }}</h4>
|
||||
<small>Submission ID</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-hashtag fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ submission.submitted_at|date:"M d, Y" }}</h4>
|
||||
<small>Submitted</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-calendar-check fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ responses|length }}</h4>
|
||||
<small>Fields Completed</small>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-tasks fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submission Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Submission Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Form:</strong></td>
|
||||
<td>{{ form.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Submitted:</strong></td>
|
||||
<td>{{ submission.submitted_at|date:"F d, Y H:i" }}</td>
|
||||
</tr>
|
||||
{% if submission.submitted_by %}
|
||||
<tr>
|
||||
<td><strong>Submitted By:</strong></td>
|
||||
<td>{{ submission.submitted_by.get_full_name|default:submission.submitted_by.username }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if submission.applicant_name %}
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Applicant Name:</strong></td>
|
||||
<td>{{ submission.applicant_name }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if submission.applicant_email %}
|
||||
<tr>
|
||||
<td><strong>Email:</strong></td>
|
||||
<td>{{ submission.applicant_email }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Responses by Stage -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-clipboard-list"></i> Submitted Responses</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for stage in stages %}
|
||||
<div class="mb-4">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-layer-group"></i> {{ stage.name }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% get_stage_responses stage_responses stage.id as stage_data %}
|
||||
{% if stage_data %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Field Label</th>
|
||||
<th>Field Type</th>
|
||||
<th>Response Value</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for response in stage_data %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ response.field.label }}
|
||||
{% if response.field.required %}
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ response.field.get_field_type_display }}</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
{% elif response.value %}
|
||||
{% if response.field.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for val in response.value %}
|
||||
<li><i class="fas fa-check text-success"></i> {{ val }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif response.field.field_type == 'radio' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% elif response.field.field_type == 'select' %}
|
||||
<span class="badge bg-secondary">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<p>No responses submitted for this stage.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not forloop.last %}
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||||
<h4>No stages found</h4>
|
||||
<p>This form doesn't have any stages defined.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Data Table -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-table"></i> All Responses (Raw Data)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% get_all_responses_flat stage_responses as all_responses %}
|
||||
{% if all_responses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Stage</th>
|
||||
<th>Field Label</th>
|
||||
<th>Field Type</th>
|
||||
<th>Required</th>
|
||||
<th>Response Value</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for response in all_responses %}
|
||||
<tr>
|
||||
<td>{{ response.stage_name }}</td>
|
||||
<td>
|
||||
{{ response.field_label }}
|
||||
{% if response.required %}
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ response.field_type }}</td>
|
||||
<td>
|
||||
{% if response.required %}
|
||||
<span class="badge bg-danger">Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
{% elif response.value %}
|
||||
{% if response.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for val in response.value %}
|
||||
<li><i class="fas fa-check text-success"></i> {{ val }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif response.field_type == 'radio' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% elif response.field_type == 'select' %}
|
||||
<span class="badge bg-secondary">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-3">
|
||||
<i class="fas fa-inbox fa-2x mb-2"></i>
|
||||
<p>No responses found for this submission.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
<i class="fas fa-exclamation-triangle text-danger"></i> Confirm Delete
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this submission? This action cannot be undone.</p>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Submission ID:</strong> {{ submission.id }}<br>
|
||||
<strong>Submitted:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="fas fa-trash"></i> Delete Submission
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.response-value {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.response-value ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function exportSubmission() {
|
||||
// Create export options modal or direct download
|
||||
const format = prompt('Export format (csv, json, pdf):', 'csv');
|
||||
if (format) {
|
||||
window.open(`/recruitment/api/forms/{{ form.id }}/submissions/{{ submission.id }}/export/?format=${format}`, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle delete confirmation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const deleteBtn = document.querySelector('a[href*="delete"]');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle confirm delete
|
||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function() {
|
||||
fetch(`/recruitment/api/forms/{{ form.id }}/submissions/{{ submission.id }}/delete/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrfToken(),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Submission deleted successfully!');
|
||||
|
||||
} else {
|
||||
alert('Error deleting submission: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting submission');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function getCsrfToken() {
|
||||
const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
|
||||
return cookie ? cookie.split('=')[1] : '';
|
||||
}
|
||||
|
||||
// Print functionality
|
||||
function printSubmission() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
// Add print button to header
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headerActions = document.querySelector('.d-flex.justify-content-between.align-items-center.mb-4');
|
||||
if (headerActions) {
|
||||
const printBtn = document.createElement('button');
|
||||
printBtn.className = 'btn btn-outline-secondary me-2';
|
||||
printBtn.innerHTML = '<i class="fas fa-print"></i> Print';
|
||||
printBtn.onclick = printSubmission;
|
||||
headerActions.insertBefore(printBtn, headerActions.firstChild);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
312
templates/forms/form_templates_list.html
Normal file
312
templates/forms/form_templates_list.html
Normal file
@ -0,0 +1,312 @@
|
||||
<!-- templates/form_templates_list.html -->
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Form Templates - ATS{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.template-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--bs-gray-400);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-actions .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0"><i class="fas fa-file-alt me-2"></i>Form Templates</h1>
|
||||
<a href="{% url 'form_builder' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i> Create New Template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="Search templates..." value="{{ query }}">
|
||||
<button class="btn btn-outline-secondary" type="button" id="searchBtn">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if templates %}
|
||||
<div class="row g-4">
|
||||
{% for template in templates %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card template-card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="h5 mb-2">{{ template.name }}</h3>
|
||||
<div class="d-flex justify-content-between text-muted small">
|
||||
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
|
||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="row text-center mb-3">
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_stage_count }}</div>
|
||||
<div class="stat-label">Stages</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_field_count }}</div>
|
||||
<div class="stat-label">Fields</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-text card-description flex-grow-1">
|
||||
{% if template.description %}
|
||||
{{ template.description|truncatewords:20 }}
|
||||
{% else %}
|
||||
<em class="text-muted">No description provided</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="card-actions d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-primary btn-sm action-btn">
|
||||
<i class="fas fa-edit me-1"></i> Edit
|
||||
</a>
|
||||
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||
data-template-id="{{ template.id }}"
|
||||
data-template-name="{{ template.name }}">
|
||||
<i class="fas fa-trash me-1"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if templates.has_other_pages %}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination mb-0">
|
||||
{% if templates.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ templates.previous_page_number }}{% if query %}&q={{ query }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in templates.paginator.page_range %}
|
||||
{% if templates.number == num %}
|
||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||
{% elif num > templates.number|add:'-3' and num < templates.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if query %}&q={{ query }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if templates.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ templates.next_page_number }}{% if query %}&q={{ query }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-file-contract"></i>
|
||||
<h3 class="h4 mb-3">No Form Templates Found</h3>
|
||||
<p class="mb-4">
|
||||
{% if query %}No templates match your search "{{ query }}".{% else %}You haven't created any form templates yet.{% endif %}
|
||||
</p>
|
||||
<a href="{% url 'form_builder' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i> Create Your First Template
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{% include 'includes/delete_modal.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Initialize Bootstrap modal
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
|
||||
// CSRF Token for AJAX requests
|
||||
|
||||
|
||||
// Create toast container if it doesn't exist
|
||||
let toastContainer = document.querySelector('.toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Function to create toast notification
|
||||
function createToast(message, type = 'success') {
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHtml = `
|
||||
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle text-success' : 'exclamation-circle text-danger'} me-2"></i>
|
||||
<strong class="me-auto">${type === 'success' ? 'Success' : 'Error'}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
|
||||
// Remove toast element after it's hidden
|
||||
toastElement.addEventListener('hidden.bs.toast', () => {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('searchBtn').addEventListener('click', function() {
|
||||
const query = document.getElementById('searchInput').value;
|
||||
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '?';
|
||||
});
|
||||
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById('searchBtn').click();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete modal functionality
|
||||
let templateToDelete = null;
|
||||
|
||||
document.querySelectorAll('.delete').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const templateId = this.dataset.templateId;
|
||||
const templateName = this.dataset.templateName;
|
||||
templateToDelete = templateId;
|
||||
document.getElementById('templateNameToDelete').textContent = templateName;
|
||||
deleteModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle form submission in delete modal
|
||||
document.getElementById('deleteForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!templateToDelete) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/templates/${templateToDelete}/delete/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Show success toast
|
||||
createToast(result.message);
|
||||
|
||||
// Hide the modal
|
||||
deleteModal.hide();
|
||||
|
||||
// Remove the template card from the DOM after a short delay
|
||||
setTimeout(() => {
|
||||
// Find and remove the card with matching template ID
|
||||
const cardToRemove = document.querySelector(`[data-template-id="${templateToDelete}"]`).closest('.col-lg-4');
|
||||
if (cardToRemove) {
|
||||
cardToRemove.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||
cardToRemove.style.opacity = '0';
|
||||
cardToRemove.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
cardToRemove.remove();
|
||||
|
||||
// Check if any templates remain
|
||||
const remainingCards = document.querySelectorAll('.col-lg-4');
|
||||
if (remainingCards.length === 0) {
|
||||
// Redirect to empty state
|
||||
window.location.reload();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
// Show error toast
|
||||
createToast('Error: ' + result.error, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
createToast('An error occurred while deleting the template.', 'error');
|
||||
}
|
||||
|
||||
templateToDelete = null;
|
||||
});
|
||||
|
||||
// Handle modal close event
|
||||
document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() {
|
||||
templateToDelete = null;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
1143
templates/forms/form_wizard.html
Normal file
1143
templates/forms/form_wizard.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user