diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index 8d89095..ddf3afb 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/__pycache__/urls.cpython-312.pyc b/NorahUniversity/__pycache__/urls.cpython-312.pyc index e44eacc..df79ce7 100644 Binary files a/NorahUniversity/__pycache__/urls.cpython-312.pyc and b/NorahUniversity/__pycache__/urls.cpython-312.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index 605ed40..c87beef 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -132,16 +132,24 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application' # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql_psycopg2', +# 'NAME': 'norahuniversity', +# 'USER': 'norahuniversity', +# 'PASSWORD': 'norahuniversity', +# 'HOST': '127.0.0.1', +# 'PORT': '5432', +# } +# } + DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'norahuniversity', - 'USER': 'norahuniversity', - 'PASSWORD': 'norahuniversity', - 'HOST': '127.0.0.1', - 'PORT': '5432', + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', } -} +} + # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 07aeb8c..0b0a860 100644 Binary files a/recruitment/__pycache__/admin.cpython-312.pyc and b/recruitment/__pycache__/admin.cpython-312.pyc differ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index bba0bac..3f269ca 100644 Binary files a/recruitment/__pycache__/forms.cpython-312.pyc and b/recruitment/__pycache__/forms.cpython-312.pyc differ diff --git a/recruitment/__pycache__/models.cpython-312.pyc b/recruitment/__pycache__/models.cpython-312.pyc index fa57734..3cb77d1 100644 Binary files a/recruitment/__pycache__/models.cpython-312.pyc and b/recruitment/__pycache__/models.cpython-312.pyc differ diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index b4efb3c..66d8262 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/utils.cpython-312.pyc b/recruitment/__pycache__/utils.cpython-312.pyc index b9c315a..83c8083 100644 Binary files a/recruitment/__pycache__/utils.cpython-312.pyc and b/recruitment/__pycache__/utils.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index cf28cc0..1c3f983 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views_frontend.cpython-312.pyc b/recruitment/__pycache__/views_frontend.cpython-312.pyc index a6f5a1f..188826c 100644 Binary files a/recruitment/__pycache__/views_frontend.cpython-312.pyc and b/recruitment/__pycache__/views_frontend.cpython-312.pyc differ diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 1895908..3cfbbcb 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-17 19:41 +# Generated by Django 5.2.7 on 2025-10-19 15:50 import django.core.validators import django.db.models.deletion @@ -99,8 +99,8 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('topic', models.CharField(max_length=255, verbose_name='Topic')), - ('meeting_id', models.CharField(max_length=20, unique=True, verbose_name='Meeting ID')), - ('start_time', models.DateTimeField(verbose_name='Start Time')), + ('meeting_id', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID')), + ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), ('duration', models.PositiveIntegerField(verbose_name='Duration')), ('timezone', models.CharField(max_length=50, verbose_name='Timezone')), ('join_url', models.URLField(verbose_name='Join URL')), @@ -110,7 +110,7 @@ class Migration(migrations.Migration): ('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')), ('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')), ('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')), - ('status', models.CharField(blank=True, max_length=20, null=True, verbose_name='Status')), + ('status', models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status')), ], options={ 'abstract': False, @@ -142,41 +142,6 @@ class Migration(migrations.Migration): 'ordering': ['order'], }, ), - migrations.CreateModel( - name='FormSubmission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('submitted_at', models.DateTimeField(auto_now_add=True)), - ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), - ('applicant_email', models.EmailField(blank=True, help_text='Email of the applicant', max_length=254)), - ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Form Submission', - 'verbose_name_plural': 'Form Submissions', - 'ordering': ['-submitted_at'], - }, - ), - migrations.CreateModel( - name='FieldResponse', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('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/')), - ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), - ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), - ], - options={ - 'verbose_name': 'Field Response', - 'verbose_name_plural': 'Field Responses', - }, - ), migrations.CreateModel( name='FormTemplate', fields=[ @@ -195,10 +160,24 @@ class Migration(migrations.Migration): 'ordering': ['-created_at'], }, ), - migrations.AddField( - model_name='formsubmission', - name='template', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate'), + migrations.CreateModel( + name='FormSubmission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)), + ('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)), + ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')), + ], + options={ + 'verbose_name': 'Form Submission', + 'verbose_name_plural': 'Form Submissions', + 'ordering': ['-submitted_at'], + }, ), migrations.AddField( model_name='formstage', @@ -214,7 +193,7 @@ class Migration(migrations.Migration): ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), ('first_name', models.CharField(max_length=255, verbose_name='First Name')), ('last_name', models.CharField(max_length=255, verbose_name='Last Name')), - ('email', models.EmailField(max_length=254, verbose_name='Email')), + ('email', models.EmailField(db_index=True, max_length=254, verbose_name='Email')), ('phone', models.CharField(max_length=20, verbose_name='Phone')), ('address', models.TextField(max_length=200, verbose_name='Address')), ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')), @@ -222,19 +201,16 @@ class Migration(migrations.Migration): ('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('applied', models.BooleanField(default=False, verbose_name='Applied')), - ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')), + ('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')), ('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')), ('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')), ('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')), ('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')), - ('interview_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Interview Status')), + ('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')), ('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')), ('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')), ('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')), - ('match_score', models.IntegerField(blank=True, null=True)), - ('strengths', models.TextField(blank=True)), - ('weaknesses', models.TextField(blank=True)), - ('criteria_checklist', models.JSONField(blank=True, default=dict)), + ('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')), ('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')), ], options={ @@ -261,22 +237,23 @@ class Migration(migrations.Migration): ('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])), ('application_start_date', models.DateField(blank=True, null=True)), - ('application_deadline', models.DateField(blank=True, null=True)), + ('application_deadline', models.DateField(blank=True, db_index=True, null=True)), ('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)), ('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)), ('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)), - ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20)), + ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)), ('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])), ('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)), ('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')), ('posted_to_linkedin', models.BooleanField(default=False)), ('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)), ('linkedin_posted_at', models.DateTimeField(blank=True, null=True)), - ('published_at', models.DateTimeField(blank=True, null=True)), + ('published_at', models.DateTimeField(blank=True, db_index=True, null=True)), ('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)), ('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)), ('joining_date', models.DateField(blank=True, help_text='Desired start date', null=True)), ('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')), + ('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)), ('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')), ('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')), ('cancelled_at', models.DateTimeField(blank=True, null=True)), @@ -293,24 +270,22 @@ class Migration(migrations.Migration): name='InterviewSchedule', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('start_date', models.DateField(verbose_name='Start Date')), - ('end_date', models.DateField(verbose_name='End Date')), + ('start_date', models.DateField(db_index=True, verbose_name='Start Date')), + ('end_date', models.DateField(db_index=True, verbose_name='End Date')), ('working_days', models.JSONField(verbose_name='Working Days')), ('start_time', models.TimeField(verbose_name='Start Time')), ('end_time', models.TimeField(verbose_name='End Time')), - ('breaks', models.JSONField(blank=True, default=list, verbose_name='Break Times')), + ('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')), + ('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')), ('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')), ('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('candidates', models.ManyToManyField(related_name='interview_schedules', to='recruitment.candidate')), + ('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate')), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')), ], - options={ - 'abstract': False, - }, ), migrations.AddField( model_name='formtemplate', @@ -402,9 +377,9 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('interview_date', models.DateField(verbose_name='Interview Date')), + ('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')), ('interview_time', models.TimeField(verbose_name='Interview Time')), - ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)), + ('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')), @@ -412,8 +387,92 @@ class Migration(migrations.Migration): ('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')), ('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')), ], + ), + migrations.CreateModel( + name='MeetingComment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')), + ('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')), + ], options={ - 'abstract': False, + 'verbose_name': 'Meeting Comment', + 'verbose_name_plural': 'Meeting Comments', + 'ordering': ['-created_at'], }, ), + migrations.CreateModel( + name='FieldResponse', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), + ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), + ('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/')), + ('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')), + ], + options={ + 'verbose_name': 'Field Response', + 'verbose_name_plural': 'Field Responses', + 'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')], + }, + ), + migrations.AddIndex( + model_name='formsubmission', + index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'), + ), + migrations.AddIndex( + model_name='interviewschedule', + index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), + ), + migrations.AddIndex( + model_name='formtemplate', + index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), + ), + migrations.AddIndex( + model_name='candidate', + index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'), + ), + migrations.AddIndex( + model_name='candidate', + index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), + ), + migrations.AddIndex( + model_name='jobposting', + index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), + ), + migrations.AddIndex( + model_name='scheduledinterview', + index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'), + ), ] diff --git a/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py b/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py deleted file mode 100644 index ac56849..0000000 --- a/recruitment/migrations/0014_formtemplate_close_at_formtemplate_max_applications_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0013_alter_formtemplate_created_by'), - ] - - operations = [ - migrations.AddField( - model_name='formtemplate', - name='close_at', - field=models.DateTimeField(blank=True, help_text='Date and time at which applications close', null=True), - ), - migrations.AddField( - model_name='formtemplate', - name='max_applications', - field=models.PositiveIntegerField(default=1000, help_text='Maximum number of applications allowed'), - ), - migrations.AlterField( - model_name='formtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True), - ), - migrations.AlterField( - model_name='formtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True), - ), - ] diff --git a/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py b/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py deleted file mode 100644 index b519f91..0000000 --- a/recruitment/migrations/0015_remove_formtemplate_close_at_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0014_formtemplate_close_at_formtemplate_max_applications_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='formtemplate', - name='close_at', - ), - migrations.AlterField( - model_name='formtemplate', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - migrations.AlterField( - model_name='formtemplate', - name='updated_at', - field=models.DateTimeField(auto_now=True, verbose_name='Updated at'), - ), - ] diff --git a/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py b/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py deleted file mode 100644 index e660370..0000000 --- a/recruitment/migrations/0016_remove_formtemplate_max_applications_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 10:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0015_remove_formtemplate_close_at_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='formtemplate', - name='max_applications', - ), - migrations.AddField( - model_name='jobposting', - name='max_applications', - field=models.PositiveIntegerField(default=1000, help_text='Maximum number of applications allowed'), - ), - ] diff --git a/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py b/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py deleted file mode 100644 index 4961d4d..0000000 --- a/recruitment/migrations/0017_remove_interviewschedule_breaks_and_more.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 15:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0016_remove_formtemplate_max_applications_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='interviewschedule', - name='breaks', - ), - migrations.AddField( - model_name='interviewschedule', - name='break_end', - field=models.TimeField(blank=True, null=True, verbose_name='Break End Time'), - ), - migrations.AddField( - model_name='interviewschedule', - name='break_start', - field=models.TimeField(blank=True, null=True, verbose_name='Break Start Time'), - ), - ] diff --git a/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py b/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py deleted file mode 100644 index 16b2ed2..0000000 --- a/recruitment/migrations/0018_rename_break_end_interviewschedule_break_end_time_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 15:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0017_remove_interviewschedule_breaks_and_more'), - ] - - operations = [ - migrations.RenameField( - model_name='interviewschedule', - old_name='break_end', - new_name='break_end_time', - ), - migrations.RenameField( - model_name='interviewschedule', - old_name='break_start', - new_name='break_start_time', - ), - ] diff --git a/recruitment/migrations/0019_alter_interviewschedule_candidates.py b/recruitment/migrations/0019_alter_interviewschedule_candidates.py deleted file mode 100644 index d10d068..0000000 --- a/recruitment/migrations/0019_alter_interviewschedule_candidates.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 16:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0018_rename_break_end_interviewschedule_break_end_time_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='candidates', - field=models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate'), - ), - ] diff --git a/recruitment/migrations/0020_alter_interviewschedule_created_at.py b/recruitment/migrations/0020_alter_interviewschedule_created_at.py deleted file mode 100644 index 3824c35..0000000 --- a/recruitment/migrations/0020_alter_interviewschedule_created_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-15 16:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0019_alter_interviewschedule_candidates'), - ] - - operations = [ - migrations.AlterField( - model_name='interviewschedule', - name='created_at', - field=models.DateTimeField(auto_now_add=True, verbose_name='Created at'), - ), - ] diff --git a/recruitment/migrations/0021_meetingcomment.py b/recruitment/migrations/0021_meetingcomment.py deleted file mode 100644 index 734d421..0000000 --- a/recruitment/migrations/0021_meetingcomment.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 13:52 - -import django.db.models.deletion -import django_ckeditor_5.fields -import django_extensions.db.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0020_alter_interviewschedule_created_at'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='MeetingComment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), - ('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')), - ('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')), - ('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')), - ], - options={ - 'verbose_name': 'Meeting Comment', - 'verbose_name_plural': 'Meeting Comments', - 'ordering': ['-created_at'], - }, - ), - ] diff --git a/recruitment/migrations/0022_candidate_resume_parsed_category.py b/recruitment/migrations/0022_candidate_resume_parsed_category.py deleted file mode 100644 index f767301..0000000 --- a/recruitment/migrations/0022_candidate_resume_parsed_category.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 19:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0021_meetingcomment'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='resume_parsed_category', - field=models.TextField(blank=True, verbose_name='Resume Parsed Category'), - ), - ] diff --git a/recruitment/migrations/0023_alter_jobposting_max_applications.py b/recruitment/migrations/0023_alter_jobposting_max_applications.py deleted file mode 100644 index 94104e1..0000000 --- a/recruitment/migrations/0023_alter_jobposting_max_applications.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-16 19:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0022_candidate_resume_parsed_category'), - ] - - operations = [ - migrations.AlterField( - model_name='jobposting', - name='max_applications', - field=models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True), - ), - ] diff --git a/recruitment/migrations/0024_alter_zoommeeting_status.py b/recruitment/migrations/0024_alter_zoommeeting_status.py deleted file mode 100644 index 9a69166..0000000 --- a/recruitment/migrations/0024_alter_zoommeeting_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 20:58 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0023_alter_jobposting_max_applications'), - ] - - operations = [ - migrations.AlterField( - model_name='zoommeeting', - name='status', - field=models.CharField(blank=True, default='waiting', max_length=20, null=True, verbose_name='Status'), - ), - ] diff --git a/recruitment/migrations/0025_candidate_recommendation.py b/recruitment/migrations/0025_candidate_recommendation.py deleted file mode 100644 index 061dbf7..0000000 --- a/recruitment/migrations/0025_candidate_recommendation.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 21:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0024_alter_zoommeeting_status'), - ] - - operations = [ - migrations.AddField( - model_name='candidate', - name='recommendation', - field=models.TextField(blank=True, verbose_name='Recommendation'), - ), - ] diff --git a/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py b/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py deleted file mode 100644 index 1a06a70..0000000 --- a/recruitment/migrations/0026_remove_candidate_resume_parsed_category_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-17 21:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0025_candidate_recommendation'), - ] - - operations = [ - migrations.RemoveField( - model_name='candidate', - name='resume_parsed_category', - ), - migrations.AddField( - model_name='candidate', - name='major_category_name', - field=models.TextField(blank=True, verbose_name='Major Category Name'), - ), - ] diff --git a/recruitment/migrations/0027_alter_candidate_email_and_more.py b/recruitment/migrations/0027_alter_candidate_email_and_more.py deleted file mode 100644 index af3b674..0000000 --- a/recruitment/migrations/0027_alter_candidate_email_and_more.py +++ /dev/null @@ -1,159 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-18 17:51 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0026_remove_candidate_resume_parsed_category_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='email', - field=models.EmailField(db_index=True, max_length=254, verbose_name='Email'), - ), - migrations.AlterField( - model_name='candidate', - name='major_category_name', - field=models.TextField(blank=True, db_index=True, verbose_name='Major Category Name'), - ), - migrations.AlterField( - model_name='candidate', - name='match_score', - field=models.IntegerField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='candidate', - name='stage', - field=models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage'), - ), - migrations.AlterField( - model_name='formsubmission', - name='applicant_email', - field=models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254), - ), - migrations.AlterField( - model_name='formsubmission', - name='submitted_at', - field=models.DateTimeField(auto_now_add=True, db_index=True), - ), - migrations.AlterField( - model_name='interviewschedule', - name='end_date', - field=models.DateField(db_index=True, verbose_name='End Date'), - ), - migrations.AlterField( - model_name='interviewschedule', - name='start_date', - field=models.DateField(db_index=True, verbose_name='Start Date'), - ), - migrations.AlterField( - model_name='jobposting', - name='application_deadline', - field=models.DateField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='jobposting', - name='published_at', - field=models.DateTimeField(blank=True, db_index=True, null=True), - ), - migrations.AlterField( - model_name='jobposting', - name='status', - field=models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20), - ), - migrations.AlterField( - model_name='scheduledinterview', - name='interview_date', - field=models.DateField(db_index=True, verbose_name='Interview Date'), - ), - migrations.AlterField( - model_name='scheduledinterview', - name='status', - field=models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20), - ), - migrations.AlterField( - model_name='zoommeeting', - name='meeting_id', - field=models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='start_time', - field=models.DateTimeField(db_index=True, verbose_name='Start Time'), - ), - migrations.AlterField( - model_name='zoommeeting', - name='status', - field=models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['job', 'stage'], name='recruitment_job_id_766dbe_idx'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['job', 'stage', 'match_score'], name='recruitment_job_id_bd6512_idx'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'), - ), - migrations.AddIndex( - model_name='fieldresponse', - index=models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), - ), - migrations.AddIndex( - model_name='fieldresponse', - index=models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx'), - ), - migrations.AddIndex( - model_name='formsubmission', - index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'), - ), - migrations.AddIndex( - model_name='formtemplate', - index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'), - ), - migrations.AddIndex( - model_name='formtemplate', - index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'), - ), - migrations.AddIndex( - model_name='interviewschedule', - index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['status', 'created_at'], name='recruitment_status_42c036_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'), - ), - migrations.AddIndex( - model_name='scheduledinterview', - index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'), - ), - ] diff --git a/recruitment/migrations/0028_alter_candidate_interview_status.py b/recruitment/migrations/0028_alter_candidate_interview_status.py deleted file mode 100644 index f9b6c05..0000000 --- a/recruitment/migrations/0028_alter_candidate_interview_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-18 21:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0027_alter_candidate_email_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='candidate', - name='interview_status', - field=models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status'), - ), - ] diff --git a/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py b/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py deleted file mode 100644 index 1ab4af2..0000000 --- a/recruitment/migrations/0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 5.2.4 on 2025-10-19 10:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0028_alter_candidate_interview_status'), - ] - - operations = [ - migrations.RemoveIndex( - model_name='candidate', - name='recruitment_job_id_766dbe_idx', - ), - migrations.RemoveIndex( - model_name='candidate', - name='recruitment_job_id_bd6512_idx', - ), - migrations.RemoveIndex( - model_name='jobposting', - name='recruitment_status_42c036_idx', - ), - migrations.RemoveField( - model_name='candidate', - name='criteria_checklist', - ), - migrations.RemoveField( - model_name='candidate', - name='major_category_name', - ), - migrations.RemoveField( - model_name='candidate', - name='match_score', - ), - migrations.RemoveField( - model_name='candidate', - name='recommendation', - ), - migrations.RemoveField( - model_name='candidate', - name='strengths', - ), - migrations.RemoveField( - model_name='candidate', - name='weaknesses', - ), - migrations.AddField( - model_name='candidate', - name='ai_analysis_data', - field=models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data'), - ), - migrations.AddIndex( - model_name='candidate', - index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'), - ), - migrations.AddIndex( - model_name='jobposting', - index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'), - ), - ] diff --git a/recruitment/migrations/0030_alter_candidate_options.py b/recruitment/migrations/0030_alter_candidate_options.py deleted file mode 100644 index eff8232..0000000 --- a/recruitment/migrations/0030_alter_candidate_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-19 13:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0029_remove_candidate_recruitment_job_id_766dbe_idx_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='candidate', - options={'ordering': ['-ai_analysis_data__match_score', '-created_at'], 'verbose_name': 'Candidate', 'verbose_name_plural': 'Candidates'}, - ), - ] diff --git a/recruitment/migrations/0031_alter_candidate_options.py b/recruitment/migrations/0031_alter_candidate_options.py deleted file mode 100644 index ffdc406..0000000 --- a/recruitment/migrations/0031_alter_candidate_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.6 on 2025-10-19 13:43 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('recruitment', '0030_alter_candidate_options'), - ] - - operations = [ - migrations.AlterModelOptions( - name='candidate', - options={'verbose_name': 'Candidate', 'verbose_name_plural': 'Candidates'}, - ), - ] diff --git a/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc index d53ed79..479f42f 100644 Binary files a/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc and b/recruitment/templatetags/__pycache__/form_filters.cpython-312.pyc differ diff --git a/recruitment/views.py b/recruitment/views.py index cf030aa..bdfb538 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -752,6 +752,15 @@ def form_wizard_view(request, template_id): """Display the form as a step-by-step wizard""" template = get_object_or_404(FormTemplate, pk=template_id, is_active=True) job_id = template.job.internal_job_id + job=template.job + is_limit_exceeded=job.is_application_limit_reached + if is_limit_exceeded: + messages.error( + request, + 'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.' + ) + return redirect('job_detail_candidate',slug=job.slug) + return render( request, "forms/form_wizard.html", @@ -763,6 +772,8 @@ def form_wizard_view(request, template_id): def submit_form(request, template_id): """Handle form submission""" template = get_object_or_404(FormTemplate, id=template_id) + + if request.method == "POST": try: with transaction.atomic(): @@ -829,7 +840,7 @@ def submit_form(request, template_id): job=submission.template.job, ) - return redirect('application_success') + return redirect('application_success',slug=job.slug) except Exception as e: logger.error(f"Candidate creation failed,{e}") @@ -2086,10 +2097,10 @@ def set_staff_password(request,pk): -@login_required -def user_detail(requests,pk): - user=get_object_or_404(User,pk=pk) - return render(requests,'user/profile.html') +# @login_required +# def user_detail(requests,pk): +# user=get_object_or_404(User,pk=pk) +# return render(requests,'user/profile.html') @csrf_exempt @@ -2173,3 +2184,11 @@ def edit_meeting_comment(request, slug, comment_id): return redirect('meeting_details', slug=slug) else: form = MeetingCommentForm(instance=comment) + + +def delete_meeting_comment(request): + pass + + +def set_meeting_candidate(request): + pass \ No newline at end of file diff --git a/templates/account/password_reset_done.html b/templates/account/password_reset_done.html new file mode 100644 index 0000000..ca26bef --- /dev/null +++ b/templates/account/password_reset_done.html @@ -0,0 +1,187 @@ +{% load static i18n %} + + +
+ + ++ {% blocktrans %} + We've **sent an email** to the address you provided with instructions on how to reset your password. + {% endblocktrans %} +
+ ++ {% trans 'Please enter your new password below. You can then log in.' %} +
+ + + + {% else %} + {# Message when the reset key is invalid or expired #} ++ {% trans "The password reset link is invalid or has expired." %} +
+ + {% endif %} + + +
-
+
@@ -179,7 +179,7 @@
class="dropdown-item py-2 px-4 text-danger d-flex align-items-center border-0 bg-transparent text-start"
aria-label="{% trans 'Sign out' %}"
>
-
+
{% trans "Sign Out" %}
@@ -221,7 +221,7 @@
-