diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index ddf3afb..9e1dff9 100644 Binary files a/NorahUniversity/__pycache__/settings.cpython-312.pyc and b/NorahUniversity/__pycache__/settings.cpython-312.pyc differ diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index c87beef..6be985e 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -132,23 +132,23 @@ 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.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'haikal_db', + 'USER': 'faheed', + 'PASSWORD': 'Faheed@215', + 'HOST': '127.0.0.1', + 'PORT': '5432', } -} +} + +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -178,6 +178,7 @@ ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*'] ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_EMAIL_VERIFICATION = 'none' ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True @@ -197,6 +198,10 @@ CRISPY_BS5 = { 'use_css_helpers': True, } +ACCOUNT_RATE_LIMITS = { + 'send_email_confirmation': None, # Disables the limit +} + # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index 3f269ca..9725eb3 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 3cb77d1..7bccb13 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 66d8262..0baf2e8 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 1c3f983..4c41dba 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index ec2186b..1794f3f 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -693,3 +693,7 @@ class StaffUserCreationForm(UserCreationForm): +class ToggleAccountForm(forms.Form): + pass + + diff --git a/recruitment/models.py b/recruitment/models.py index 4ebd9d8..0939404 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -29,6 +29,9 @@ class Profile(models.Model): profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/") user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") + def __str__(self): + return f"image for user {self.user}" + class JobPosting(Base): # Basic Job Information JOB_TYPES = [ @@ -301,6 +304,26 @@ class JobPosting(Base): @property def offer_candidates(self): return self.all_candidates.filter(stage="Offer") + + + #counts + @property + def all_candidates_count(self): + return self.candidates.annotate(sortable_score=Cast('ai_analysis_data__match_score',output_field=CharField())).order_by('-sortable_score').count() + @property + def screening_candidates_count(self): + return self.all_candidates.filter(stage="Applied").count() + + @property + def exam_candidates_count(self): + return self.all_candidates.filter(stage="Exam").count() + @property + def interview_candidates_count(self): + return self.all_candidates.filter(stage="Interview").count() + + @property + def offer_candidates_count(self): + return self.all_candidates.filter(stage="Offer").count() class JobPostingImage(models.Model): job=models.OneToOneField('JobPosting',on_delete=models.CASCADE,related_name='post_images') diff --git a/recruitment/urls.py b/recruitment/urls.py index c755962..8d3a50f 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -108,6 +108,7 @@ urlpatterns = [ path('jobs//candidates//schedule-meeting-page/', views.schedule_meeting_for_candidate, name='schedule_meeting_for_candidate'), path('jobs//candidates//delete_meeting_for_candidate//', views.delete_meeting_for_candidate, name='delete_meeting_for_candidate'), + # users urls path('user/',views.user_detail,name='user_detail'), path('user/user_profile_image_update/',views.user_profile_image_update,name='user_profile_image_update'), @@ -115,6 +116,10 @@ urlpatterns = [ path('settings/',views.admin_settings,name='admin_settings'), path('staff/create',views.create_staff_user,name='create_staff_user'), path('set_staff_password//',views.set_staff_password,name='set_staff_password'), + path('account_toggle_status/',views.account_toggle_status,name='account_toggle_status'), + + + # Meeting Comments URLs path('meetings//comments/add/', views.add_meeting_comment, name='add_meeting_comment'), diff --git a/recruitment/views.py b/recruitment/views.py index bdfb538..dbede05 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -26,8 +26,10 @@ from .forms import ( BreakTimeFormSet, JobPostingImageForm, ProfileImageUploadForm, - StaffUserCreationForm - ,MeetingCommentForm + StaffUserCreationForm, + MeetingCommentForm, + ToggleAccountForm, + ) from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent from rest_framework import viewsets @@ -1940,37 +1942,51 @@ def schedule_meeting_for_candidate(request, slug, candidate_pk): from django.core.exceptions import ObjectDoesNotExist def user_profile_image_update(request, pk): + user = get_object_or_404(User, pk=pk) + + # 2. Ensure Profile exists and get the instance try: - instance =user.profile - - except ObjectDoesNotExist as e: - Profile.objects.create(user=user) - + + profile_instance = user.profile + except ObjectDoesNotExist: + + profile_instance = Profile.objects.create(user=user) + if request.method == 'POST': - profile_form = ProfileImageUploadForm(request.POST, request.FILES, instance=user.profile) + + profile_form = ProfileImageUploadForm( + request.POST, + request.FILES, + instance=profile_instance # <--- USE profile_instance HERE + ) + if profile_form.is_valid(): profile_form.save() - messages.success(request, 'Image uploaded successfully') - return redirect('user_detail', pk=user.pk) + messages.success(request, 'Image uploaded successfully.') + return redirect('user_detail', pk=user.pk) else: - messages.error(request, 'An error occurred while uploading the image') + messages.error(request, 'An error occurred while uploading the image. Please check the errors below.') else: - profile_form = ProfileImageUploadForm(instance=user.profile) - + # + profile_form = ProfileImageUploadForm(instance=profile_instance) context = { 'profile_form': profile_form, 'user': user, } return render(request, 'user/profile.html', context) - - def user_detail(request, pk): user = get_object_or_404(User, pk=pk) + + + try: + profile_instance = user.profile + profile_form = ProfileImageUploadForm(instance=profile_instance) + except: + profile_form = ProfileImageUploadForm() - profile_form = ProfileImageUploadForm() if request.method == 'POST': first_name=request.POST.get('first_name') last_name=request.POST.get('last_name') @@ -2068,15 +2084,17 @@ def create_staff_user(request): @user_passes_test(is_superuser_check) def admin_settings(request): staffs=User.objects.filter(is_superuser=False) + form = ToggleAccountForm() context={ - 'staffs':staffs + 'staffs':staffs, + 'form':form } return render(request,'user/admin_settings.html',context) from django.contrib.auth.forms import SetPasswordForm - +@user_passes_test(is_superuser_check) def set_staff_password(request,pk): user=get_object_or_404(User,pk=pk) print(request.POST) @@ -2085,10 +2103,11 @@ def set_staff_password(request,pk): if form.is_valid(): form.save() messages.success(request,f'Password successfully changed') + return redirect('admin_settings') else: form=SetPasswordForm(user=user) messages.error(request,f'Password does not match please try again.') - return redirect('set_staff_password',user=user) + return redirect('admin_settings') else: form=SetPasswordForm(user=user) @@ -2096,6 +2115,28 @@ def set_staff_password(request,pk): +@user_passes_test(is_superuser_check) +def account_toggle_status(request,pk): + user=get_object_or_404(User,pk=pk) + if request.method=='POST': + print(user.is_active) + form=ToggleAccountForm(request.POST) + if form.is_valid(): + if user.is_active: + user.is_active=False + user.save() + messages.success(request,f'Staff with email: {user.email} deactivated successfully') + return redirect('admin_settings') + else: + user.is_active=True + user.save() + messages.success(request,f'Staff with email: {user.email} activated successfully') + return redirect('admin_settings') + else: + messages.error(f'Please correct the error below') + + + # @login_required # def user_detail(requests,pk): diff --git a/templates/account/account_inactive.html b/templates/account/account_inactive.html new file mode 100644 index 0000000..c64e2ad --- /dev/null +++ b/templates/account/account_inactive.html @@ -0,0 +1,155 @@ +{% load static i18n %} + + + + + + {% translate "Account Inactive" %} - KAAUH ATS + + + {# Include Font Awesome for icons #} + + + + + + +
+ +
+
+

+ +
+
{% translate "جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية" %}
+
{% translate "ومستشفى الملك عبدالله بن عبدالعزيز التخصصي" %}
+
{% translate "Princess Nourah bint Abdulrahman University" %}
+
{% translate "King Abdullah bin Abdulaziz University Hospital" %}
+
+
+

+ Powered By TENHAL | تنحل +
+
+ +
+ +
+ + + +

{% translate "Account Inactive" %}

+ +
+

+ {% translate "Access denied. This account has been marked as inactive by an administrator." %} +

+

+ {% translate "If you believe this is an error, please contact the system administrator for assistance." %} +

+ + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/templates/account/email.html b/templates/account/email.html new file mode 100644 index 0000000..5e62859 --- /dev/null +++ b/templates/account/email.html @@ -0,0 +1,125 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} +{% load crispy_forms_tags %} + +{% block title %}{% translate "Email Addresses" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Settings" %}

+

{% translate "Manage your personal details and security." %}

+
+
+ +
+ + {# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #} +
+
+
+
+ {# Assuming a main 'Profile' or 'Personal Information' page exists #} + + {% translate "Personal Information" %} + + + {# Highlight the current page (Email) as active #} + + {% translate "Email Addresses" %} + + + {% translate "Change Password" %} + + + + {% translate "Sign Out" %} + +
+
+
+
+ + {# ------------------- RIGHT COLUMN: EMAIL MANAGEMENT ------------------- #} +
+
+
+ +
{% translate "Email Addresses" %}
+

{% translate "These email addresses are linked to your account. You can set the primary address, resend verification, or remove an address." %}

+ + {% if emailaddresses %} + {% for emailaddress in emailaddresses %} +
+ +

+ {{ emailaddress.email }} + + {# Status Badges: Using rounded-pill and appropriate colors #} + {% if emailaddress.primary %} + {% translate "Primary" %} + {% endif %} + {% if emailaddress.verified %} + {% translate "Verified" %} + {% else %} + {% translate "Unverified" %} + {% endif %} +

+ +
+ + {# 1. MAKE PRIMARY ACTION #} + {% if not emailaddress.primary %} +
+ {% csrf_token %} + + + +
+ {% endif %} + + {# 2. RESEND VERIFICATION ACTION #} + {% if not emailaddress.verified %} +
+ {% csrf_token %} + + + +
+ {% endif %} + + {# 3. REMOVE ACTION #} + {% if not emailaddress.primary %} +
+ {% csrf_token %} + + + +
+ {% endif %} +
+
+ {% endfor %} + {% else %} +

{% translate "No email addresses found." %}

+ {% endif %} + +
+ + {# ------------------- ADD EMAIL FORM ------------------- #} +
{% translate "Add Email Address" %}
+
+ {% csrf_token %} + {{ form|crispy }} + {# Teal/Dark Green button consistent with "Save Changes" #} + +
+
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/email/password_reset_key_message.html b/templates/account/email/password_reset_key_message.html new file mode 100644 index 0000000..74b722d --- /dev/null +++ b/templates/account/email/password_reset_key_message.html @@ -0,0 +1,39 @@ +{% load i18n %} +{% load static %} +{% autoescape off %} + +
+ +
+

{% trans "Password Reset Request" %}

+
+ +
+

{% trans "Hello," %}

+ +

{% trans "You are receiving this email because you or someone else has requested a password reset for your account at" %} {{ current_site.name }}.

+ +

+ + {% trans "Click Here to Reset Your Password" %} + +

+ +

{% trans "This link is only valid for a limited time." %}

+ +

{% trans "If you did not request a password reset, please ignore this email. Your password will remain unchanged." %}

+ +

+ {% trans "Thank you," %}
+ {% trans "KAAUH ATS Team" %} +

+
+ +
+ {% trans "If the button above does not work, copy and paste the following link into your browser:" %}
+ {{ password_reset_url }} +
+
+ +{% endautoescape %} \ No newline at end of file diff --git a/templates/account/email_confirm.html b/templates/account/email_confirm.html new file mode 100644 index 0000000..3b802d6 --- /dev/null +++ b/templates/account/email_confirm.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} + +{% block title %}{% translate "Confirm Email Address" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Verification" %}

+

{% translate "Verify your email to secure your account and unlock full features." %}

+
+
+ +
+
+
+
+ + {% with emailaddress.email as email %} + + {% if confirmation %} + + {# ------------------- CONFIRMATION REQUEST (GET) ------------------- #} + {% user_display confirmation.email_address.user as user_display %} + + +

{% translate "Confirm Your Email Address" %}

+ +

+ {% blocktrans with email as email %}Please confirm that **{{ email }}** is an email address for user **{{ user_display }}**.{% endblocktrans %} +

+ + {# Confirmation Form #} +
+ {% csrf_token %} + + {# Teal/Dark Green button consistent with the UI theme #} + +
+ + {% else %} + + {# ------------------- CONFIRMATION FAILED (Error) ------------------- #} + +

{% translate "Invalid Link" %}

+ +

+ {% translate "The email confirmation link has expired or is invalid." %} +

+

+ {% translate "Please request a new verification email from your account settings page." %} +

+ + + {% translate "Go to Settings" %} + + + {% endif %} + + {% endwith %} +
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/logout.html b/templates/account/logout.html new file mode 100644 index 0000000..4af2a35 --- /dev/null +++ b/templates/account/logout.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} +{% load i18n %} +{% load account %} + +{% block title %}{% translate "Sign Out" %}{% endblock %} + +{% block content %} +
+ +
+
+

{% translate "Account Settings" %}

+

{% translate "Manage your personal details and security." %}

+
+
+ +
+ + {# ------------------- LEFT COLUMN: ACCOUNT MENU (New Card Style) ------------------- #} +
+
+
+
+ {# Assuming a main 'Profile' or 'Personal Information' page exists #} + + {% translate "Personal Information" %} + + + {% translate "Email Addresses" %} + + + {% translate "Change Password" %} + + + {# Highlight the current page (Sign Out) as active #} + + {% translate "Sign Out" %} + +
+
+
+
+ + {# ------------------- RIGHT COLUMN: LOGOUT CONFIRMATION ------------------- #} +
+
+
+ + +

{% translate "Confirm Sign Out" %}

+ +

{% translate "Are you sure you want to sign out of your account?" %}

+ +
+ {% csrf_token %} + + {% if redirect_field_value %} + + {% endif %} + +
+ {# Sign Out button in danger color #} + + + {# Cancel/Go Back button with outline #} + + {% translate "Cancel" %} + +
+
+
+
+
+
+
+{% endblock content %} \ No newline at end of file diff --git a/templates/account/password_change.html b/templates/account/password_change.html index b05d6df..2e5a12f 100644 --- a/templates/account/password_change.html +++ b/templates/account/password_change.html @@ -7,7 +7,7 @@ {% endblock %} {% block content %} -
+
diff --git a/templates/account/password_reset_from_key.html b/templates/account/password_reset_from_key.html index 2a208a9..a873279 100644 --- a/templates/account/password_reset_from_key.html +++ b/templates/account/password_reset_from_key.html @@ -7,120 +7,39 @@ {% trans "Set New Password" %} - KAAUH ATS + {% get_current_language as LANGUAGE_CODE %} @@ -153,14 +72,17 @@
{% if form %} +

+ {% trans 'Please enter your new password below.' %} +

- {% trans 'Please enter your new password below. You can then log in.' %} + {% trans 'You can then log in.' %}

-
+ {% csrf_token %} - {# Display any general form errors #} + {# Non-Field Errors (General errors like tokens or passwords not matching) #} {% if form.non_field_errors %}
- + +