This commit is contained in:
Marwan Alwali 2025-09-21 17:35:20 +03:00
parent 1a0bb8c0c1
commit 0357921e3d
142 changed files with 6555 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

31
.idea/NorahUniversity.iml generated Normal file
View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="NorahUniversity/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="uv (NorahUniversity)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
</list>
</option>
</component>
</module>

View File

@ -0,0 +1,43 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="102" name="Python" />
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="app-emergency-department-patient-flow" />
<item index="7" class="java.lang.String" itemvalue="app-alarm-list" />
<item index="8" class="java.lang.String" itemvalue="app-root" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredNames">
<list>
<option value="id" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

17
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11 (NorahUniversity)" />
</component>
<component name="KubernetesApiProvider">{}</component>
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-4d33890d:18fe9fd09b1:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (NorahUniversity)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/NorahUniversity.iml" filepath="$PROJECT_DIR$/.idea/NorahUniversity.iml" />
</modules>
</component>
</project>

4
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
</project>

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
NorahUniversity/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for NorahUniversity project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
application = get_asgi_application()

186
NorahUniversity/settings.py Normal file
View File

@ -0,0 +1,186 @@
"""
Django settings for NorahUniversity project.
Generated by 'django-admin startproject' using Django 5.2.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
from django.templatetags.static import static
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-_!ew&)1&r--3h17knd27^x8(xu(&-f4q3%x543lv5vx2!784s*'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"unfold", # before django.contrib.admin
"unfold.contrib.filters", # optional, if special filters are needed
"unfold.contrib.forms", # optional, if special form elements are needed
"unfold.contrib.inlines", # optional, if special inlines are needed
"unfold.contrib.import_export", # optional, if django-import-export package is used
"unfold.contrib.guardian", # optional, if django-guardian package is used
"unfold.contrib.simple_history",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'recruitment',
'corsheaders',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.linkedin_oauth2',
'channels',
'django_filters',
]
SITE_ID = 1
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'allauth.account.middleware.AccountMiddleware',
]
ROOT_URLCONF = 'NorahUniversity.urls'
CORS_ALLOW_ALL_ORIGINS = True
ASGI_APPLICATION = 'hospital_recruitment.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
},
},
}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
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
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
STATICFILES_DIRS = [
BASE_DIR / 'static'
]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_ROOT = os.path.join(BASE_DIR, 'static/media')
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# LinkedIn OAuth Config
SOCIALACCOUNT_PROVIDERS = {
'linkedin_oauth2': {
'SCOPE': [
'r_liteprofile', 'r_emailaddress', 'w_member_social',
'rw_organization_admin', 'w_organization_social'
],
'PROFILE_FIELDS': [
'id', 'first-name', 'last-name', 'email-address'
]
}
}
UNFOLD = {
"DASHBOARD_CALLBACK": "recruitment.utils.dashboard_callback",
"STYLES": [
lambda request: static("unfold/css/styles.css"),
],
"SCRIPTS": [
lambda request: static("unfold/js/app.js"),
],
}

36
NorahUniversity/urls.py Normal file
View File

@ -0,0 +1,36 @@
"""
URL configuration for NorahUniversity project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from rest_framework.routers import DefaultRouter
from recruitment import views
router = DefaultRouter()
router.register(r'jobs', views.JobViewSet)
router.register(r'candidates', views.CandidateViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
path('accounts/', include('allauth.urls')),
path('', include('recruitment.urls')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
NorahUniversity/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for NorahUniversity project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
application = get_wsgi_application()

BIN
db.sqlite3 Normal file

Binary file not shown.

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'NorahUniversity.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

145
pyproject.toml Normal file
View File

@ -0,0 +1,145 @@
[project]
name = "norahuniversity"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = [
"annotated-types>=0.7.0",
"appdirs>=1.4.4",
"asgiref>=3.8.1",
"asteval>=1.0.6",
"astunparse>=1.6.3",
"attrs>=25.3.0",
"blinker>=1.9.0",
"blis>=1.3.0",
"boto3>=1.39.0",
"botocore>=1.39.0",
"bw-migrations>=0.2",
"bw-processing>=1.0",
"bw2parameters>=1.1.0",
"cached-property>=2.0.1",
"catalogue>=2.0.10",
"certifi>=2025.6.15",
"channels>=4.2.2",
"chardet>=5.2.0",
"charset-normalizer>=3.4.2",
"click>=8.2.1",
"cloudpathlib>=0.21.1",
"confection>=0.1.5",
"constructive-geometries>=1.0",
"country-converter>=1.3",
"cymem>=2.0.11",
"dataflows-tabulator>=1.54.3",
"datapackage>=1.15.4",
"deepdiff>=7.0.1",
"deprecated>=1.2.18",
"django>=5.2.3",
"django-allauth>=65.9.0",
"django-cors-headers>=4.7.0",
"django-filter>=25.1",
"django-unfold>=0.61.0",
"djangorestframework>=3.16.0",
"docopt>=0.6.2",
"en-core-web-sm",
"et-xmlfile>=2.0.0",
"faker>=37.4.0",
"flexcache>=0.3",
"flexparser>=0.4",
"fsspec>=2025.5.1",
"idna>=3.10",
"ijson>=3.4.0",
"isodate>=0.7.2",
"jinja2>=3.1.6",
"jmespath>=1.0.1",
"jsonlines>=4.0.0",
"jsonpointer>=3.0.0",
"jsonschema>=4.24.0",
"jsonschema-specifications>=2025.4.1",
"langcodes>=3.5.0",
"language-data>=1.3.0",
"linear-tsv>=1.1.0",
"llvmlite>=0.44.0",
"loguru>=0.7.3",
"lxml>=6.0.0",
"marisa-trie>=1.2.1",
"markdown-it-py>=3.0.0",
"markupsafe>=3.0.2",
"matrix-utils>=0.6",
"mdurl>=0.1.2",
"morefs>=0.2.2",
"mrio-common-metadata>=0.2.1",
"murmurhash>=1.0.13",
"numba>=0.61.2",
"numpy>=2.2.6",
"openpyxl>=3.1.5",
"ordered-set>=4.1.0",
"packaging>=25.0",
"pandas>=2.3.0",
"peewee>=3.18.1",
"pint>=0.24.4",
"platformdirs>=4.3.8",
"preshed>=3.0.10",
"prettytable>=3.16.0",
"pydantic>=2.11.7",
"pydantic-core>=2.33.2",
"pydantic-settings>=2.10.1",
"pyecospold>=4.0.0",
"pygments>=2.19.2",
"pyjwt>=2.10.1",
"pymupdf>=1.26.1",
"pyparsing>=3.2.3",
"pyprind>=2.11.3",
"python-dateutil>=2.9.0.post0",
"python-dotenv>=1.1.1",
"python-json-logger>=3.3.0",
"pytz>=2025.2",
"pyxlsb>=1.0.10",
"pyyaml>=6.0.2",
"randonneur>=0.6.2",
"randonneur-data>=0.6",
"rapidfuzz>=3.13.0",
"rdflib>=7.1.4",
"referencing>=0.36.2",
"requests>=2.32.4",
"rfc3986>=2.0.0",
"rich>=14.0.0",
"rpds-py>=0.26.0",
"s3transfer>=0.13.0",
"scipy>=1.16.0",
"shellingham>=1.5.4",
"simple-ats>=3.0.0",
"six>=1.17.0",
"smart-open>=7.3.0",
"snowflake-id>=1.0.2",
"spacy>=3.8.7",
"spacy-legacy>=3.0.12",
"spacy-loggers>=1.0.5",
"sparqlwrapper>=2.0.0",
"sparse>=0.17.0",
"sqlalchemy>=2.0.41",
"sqlparse>=0.5.3",
"srsly>=2.5.1",
"stats-arrays>=0.7",
"structlog>=25.4.0",
"tableschema>=1.21.0",
"thinc>=8.3.6",
"toolz>=1.0.0",
"tqdm>=4.67.1",
"typer>=0.16.0",
"typing-extensions>=4.14.0",
"typing-inspection>=0.4.1",
"tzdata>=2025.2",
"unicodecsv>=0.14.1",
"urllib3>=2.5.0",
"voluptuous>=0.15.2",
"wasabi>=1.1.3",
"wcwidth>=0.2.13",
"weasel>=0.4.1",
"wrapt>=1.17.2",
"wurst>=0.4",
"xlrd>=2.0.2",
"xlsxwriter>=3.2.5",
]
[tool.uv.sources]
en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" }

BIN
recruitment/.DS_Store vendored Normal file

Binary file not shown.

0
recruitment/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

56
recruitment/admin.py Normal file
View File

@ -0,0 +1,56 @@
from django.contrib import messages
from . import models
from .utils import extract_summary_from_pdf
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 unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
from unfold.admin import ModelAdmin
admin.site.unregister(User)
admin.site.unregister(Group)
@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin):
form = UserChangeForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm
@admin.register(Group)
class GroupAdmin(BaseGroupAdmin, ModelAdmin):
pass
@admin.register(models.Job)
class JobAdmin(ModelAdmin):
list_display = ('title', 'is_published', 'posted_to_linkedin', 'created_at')
list_filter = ('is_published', 'posted_to_linkedin')
search_fields = ('title', 'description_en', 'description_ar')
@admin.action(description="Parse selected resumes")
def parse_resumes(modeladmin, request, queryset):
for candidate in queryset:
if candidate.resume:
summary = extract_summary_from_pdf(candidate.resume.path)
candidate.parsed_summary = str(summary)
candidate.save()
messages.success(request, f"Parsed {queryset.count()} resumes successfully.")
@admin.register(models.Candidate)
class CandidateAdmin(ModelAdmin):
list_display = ('name', 'email', 'job', 'applied', 'created_at')
list_filter = ('applied', 'job')
search_fields = ('name', 'email')
# readonly_fields = ('parsed_summary',)
actions = [parse_resumes]
@admin.register(models.TrainingMaterial)
class TrainingMaterialAdmin(ModelAdmin):
list_display = ('title', 'created_by', 'created_at')
search_fields = ('title', 'content')

6
recruitment/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RecruitmentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'recruitment'

7
recruitment/dashboard.py Normal file
View File

@ -0,0 +1,7 @@
import pandas as pd
from . import models
def get_dashboard_data():
df = pd.DataFrame(list(models.Candidate.objects.all().values('applied', 'created_at')))
summary = df['applied'].value_counts().to_dict()
return summary

7
recruitment/forms.py Normal file
View File

@ -0,0 +1,7 @@
from django import forms
from . import models
class CandidateForm(forms.ModelForm):
class Meta:
model = models.Candidate
fields = ['name', 'email', 'resume']

26
recruitment/linkedin.py Normal file
View File

@ -0,0 +1,26 @@
import requests
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
class LinkedInService:
def __init__(self, access_token):
self.headers = {
'Authorization': f'Bearer {access_token}',
'X-Restli-Protocol-Version': '2.0.0',
'Content-Type': 'application/json'
}
def post_job(self, organization_id, job_data):
url = f"{LINKEDIN_API_BASE}/ugcPosts"
data = {
"author": f"urn:li:organization:{organization_id}",
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {"text": job_data['text']},
"shareMediaCategory": "NONE"
}
},
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
}
return requests.post(url, json=data, headers=self.headers)

View File

View File

@ -0,0 +1,48 @@
import os
import random
from django.core.management.base import BaseCommand
from faker import Faker
from recruitment.models import Job, Candidate
fake = Faker()
class Command(BaseCommand):
help = 'Generate 20 fake jobs and 50 fake candidates'
def handle(self, *args, **kwargs):
# Clear existing fake data (optional)
Job.objects.filter(title__startswith='Fake Job').delete()
Candidate.objects.filter(name__startswith='Candidate ').delete()
self.stdout.write("Creating fake jobs...")
jobs = []
for i in range(20):
job = Job.objects.create(
title=f"Fake Job {i+1}",
description_en=fake.paragraph(nb_sentences=5),
description_ar=fake.text(max_nb_chars=200),
is_published=True,
posted_to_linkedin=random.choice([True, False])
)
jobs.append(job)
self.stdout.write("Creating fake candidates...")
for i in range(50):
job = random.choice(jobs)
resume_path = f"resumes/fake_resume_{i+1}.pdf"
parsed = {
'name': fake.name(),
'skills': [fake.job() for _ in range(5)],
'summary': fake.text(max_nb_chars=300)
}
Candidate.objects.create(
job=job,
name=f"Candidate {i+1}",
email=fake.email(),
resume=resume_path, # You can create dummy files if needed
parsed_summary=str(parsed),
applied=random.choice([True, False])
)
self.stdout.write(self.style.SUCCESS("✔️ Successfully generated 20 jobs and 50 candidates"))

View File

@ -0,0 +1,40 @@
# Generated by Django 5.2.1 on 2025-05-18 17:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Job',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('description_en', models.TextField()),
('description_ar', models.TextField()),
('is_published', models.BooleanField(default=False)),
('posted_to_linkedin', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('email', models.EmailField(max_length=254)),
('resume', models.FileField(upload_to='resumes/')),
('parsed_summary', models.TextField(blank=True)),
('status', models.CharField(default='Applied', max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.job')),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.1 on 2025-05-18 17:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TrainingMaterial',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('content', models.TextField(blank=True)),
('video_link', models.URLField(blank=True)),
('file', models.FileField(blank=True, upload_to='training_materials/')),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.2.1 on 2025-05-18 18:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_trainingmaterial'),
]
operations = [
migrations.AddField(
model_name='candidate',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='job',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='trainingmaterial',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 5.2.1 on 2025-05-18 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_candidate_updated_at_job_updated_at_and_more'),
]
operations = [
migrations.RemoveField(
model_name='candidate',
name='status',
),
migrations.AddField(
model_name='candidate',
name='applied',
field=models.BooleanField(default=False),
),
]

View File

39
recruitment/models.py Normal file
View File

@ -0,0 +1,39 @@
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Job(models.Model):
title = models.CharField(max_length=255)
description_en = models.TextField()
description_ar = models.TextField()
is_published = models.BooleanField(default=False)
posted_to_linkedin = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class Candidate(models.Model):
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='candidates')
name = models.CharField(max_length=255)
email = models.EmailField()
resume = models.FileField(upload_to='resumes/')
parsed_summary = models.TextField(blank=True)
applied = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class TrainingMaterial(models.Model):
title = models.CharField(max_length=255)
content = models.TextField(blank=True)
video_link = models.URLField(blank=True)
file = models.FileField(upload_to='training_materials/', blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title

View File

@ -0,0 +1,12 @@
from rest_framework import serializers
from . import models
class JobSerializer(serializers.ModelSerializer):
class Meta:
model = models.Job
fields = '__all__'
class CandidateSerializer(serializers.ModelSerializer):
class Meta:
model = models.Candidate
fields = '__all__'

3
recruitment/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
recruitment/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path
from . import views_frontend
urlpatterns = [
path('careers/', views_frontend.job_list, name='job_list'),
path('careers/<int:job_id>/', views_frontend.job_detail, name='job_detail'),
path('training/', views_frontend.training_list, name='training_list'),
path('candidate/<int:candidate_id>/view/', views_frontend.candidate_detail, name='candidate_detail'),
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
]

46
recruitment/utils.py Normal file
View File

@ -0,0 +1,46 @@
import fitz # PyMuPDF
import spacy
import os
from recruitment import models
nlp = spacy.load("en_core_web_sm")
def extract_text_from_pdf(pdf_path):
text = ""
with fitz.open(pdf_path) as doc:
for page in doc:
text += page.get_text()
return text
def extract_summary_from_pdf(pdf_path):
if not os.path.exists(pdf_path):
return {'error': 'File not found'}
text = extract_text_from_pdf(pdf_path)
doc = nlp(text)
summary = {
'name': doc.ents[0].text if doc.ents else '',
'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1],
'summary': text[:500]
}
return summary
def dashboard_callback(request, context):
total_jobs = models.Job.objects.count()
total_candidates = models.Candidate.objects.count()
jobs = models.Job.objects.all()
job_titles = [job.title for job in jobs]
job_app_counts = [job.candidates.count() for job in jobs]
context.update({
"total_jobs": total_jobs,
"total_candidates": total_candidates,
"job_titles": job_titles,
"job_app_counts": job_app_counts,
})
return context

14
recruitment/views.py Normal file
View File

@ -0,0 +1,14 @@
from django.shortcuts import render
from rest_framework import viewsets
from . import models
from . import serializers
class JobViewSet(viewsets.ModelViewSet):
queryset = models.Job.objects.all()
serializer_class = serializers.JobSerializer
class CandidateViewSet(viewsets.ModelViewSet):
queryset = models.Candidate.objects.all()
serializer_class = serializers.CandidateSerializer

View File

@ -0,0 +1,54 @@
from django.shortcuts import render, get_object_or_404
from . import models
from django.utils.translation import get_language
from . import forms
from django.contrib.auth.decorators import login_required, user_passes_test
import ast
from .dashboard import get_dashboard_data
def job_list(request):
jobs = models.Job.objects.filter(is_published=True).order_by('-created_at')
lang = get_language()
return render(request, 'recruitment/job_list.html', {'jobs': jobs, 'lang': lang})
def job_detail(request, job_id):
job = get_object_or_404(models.Job, id=job_id, is_published=True)
form = forms.CandidateForm()
return render(request, 'recruitment/job_detail.html', {'job': job, 'form': form})
@login_required
def training_list(request):
materials = models.TrainingMaterial.objects.all().order_by('-created_at')
return render(request, 'recruitment/training_list.html', {'materials': materials})
def candidate_detail(request, candidate_id):
candidate = get_object_or_404(models.Candidate, id=candidate_id)
try:
parsed = ast.literal_eval(candidate.parsed_summary)
except:
parsed = {}
return render(request, 'recruitment/candidate_detail.html', {
'candidate': candidate,
'parsed': parsed,
})
def dashboard_view(request):
total_jobs = models.Job.objects.count()
total_candidates = models.Candidate.objects.count()
jobs = models.Job.objects.all()
job_titles = [job.title for job in jobs]
job_app_counts = [job.candidates.count() for job in jobs]
average_applications = round(sum(job_app_counts) / total_jobs, 2) if total_jobs > 0 else 0
context = {
'total_jobs': total_jobs,
'total_candidates': total_candidates,
'job_titles': job_titles,
'job_app_counts': job_app_counts,
'average_applications': average_applications,
}
return render(request, 'recruitment/dashboard.html', context)

31
recruitment/zoom_api.py Normal file
View File

@ -0,0 +1,31 @@
import requests
import jwt
import time
ZOOM_API_KEY = 'your_zoom_api_key'
ZOOM_API_SECRET = 'your_zoom_api_secret'
def generate_zoom_jwt():
payload = {
'iss': ZOOM_API_KEY,
'exp': time.time() + 3600
}
token = jwt.encode(payload, ZOOM_API_SECRET, algorithm='HS256')
return token
def create_zoom_meeting(topic, start_time, duration, host_email):
jwt_token = generate_zoom_jwt()
headers = {
'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json'
}
data = {
"topic": topic,
"type": 2,
"start_time": start_time,
"duration": duration,
"schedule_for": host_email,
"settings": {"join_before_host": True}
}
url = f"https://api.zoom.us/v2/users/{host_email}/meetings"
return requests.post(url, json=data, headers=headers)

134
requirements.txt Normal file
View File

@ -0,0 +1,134 @@
annotated-types
appdirs
asgiref
asteval
astunparse
attrs
blinker
blis
boto3
botocore
bw-migrations
bw2parameters
bw_processing
cached-property
catalogue
certifi
channels
chardet
charset-normalizer
click
cloudpathlib
confection
constructive_geometries
country_converter
cymem
dataflows-tabulator
datapackage
deepdiff
Deprecated
Django
django-allauth
django-cors-headers
django-filter
django-unfold
djangorestframework
docopt
en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl#sha256=1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85
et_xmlfile
Faker
flexcache
flexparser
fsspec
idna
ijson
isodate
Jinja2
jmespath
jsonlines
jsonpointer
jsonschema
jsonschema-specifications
langcodes
language_data
linear-tsv
llvmlite
loguru
lxml
marisa-trie
markdown-it-py
MarkupSafe
matrix_utils
mdurl
morefs
mrio-common-metadata
murmurhash
numba
numpy
openpyxl
ordered-set
packaging
pandas
peewee
Pint
platformdirs
preshed
prettytable
pydantic
pydantic-settings
pydantic_core
pyecospold
Pygments
PyJWT
PyMuPDF
pyparsing
PyPrind
python-dateutil
python-dotenv
python-json-logger
pytz
pyxlsb
PyYAML
randonneur
randonneur_data
RapidFuzz
rdflib
referencing
requests
rfc3986
rich
rpds-py
s3transfer
scipy
shellingham
six
smart-open
snowflake-id
spacy
spacy-legacy
spacy-loggers
SPARQLWrapper
sparse
SQLAlchemy
sqlparse
srsly
stats_arrays
structlog
tableschema
thinc
toolz
tqdm
typer
typing-inspection
typing_extensions
tzdata
unicodecsv
urllib3
voluptuous
wasabi
wcwidth
weasel
wrapt
wurst
xlrd
XlsxWriter

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,295 @@
/*global SelectBox, interpolate*/
// Handles related-objects functionality: lookup link for raw_id_fields
// and Add Another links.
"use strict";
{
const $ = django.jQuery;
let popupIndex = 0;
const relatedWindows = [];
function dismissChildPopups() {
relatedWindows.forEach(function (win) {
if (!win.closed) {
win.dismissChildPopups();
win.close();
}
});
}
function setPopupIndex() {
if (document.getElementsByName("_popup").length > 0) {
const index = window.name.lastIndexOf("__") + 2;
popupIndex = parseInt(window.name.substring(index));
} else {
popupIndex = 0;
}
}
function addPopupIndex(name) {
return name + "__" + (popupIndex + 1);
}
function removePopupIndex(name) {
return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), "");
}
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ""));
const href = new URL(triggeringLink.href);
if (add_popup) {
href.searchParams.set("_popup", 1);
}
const win = window.open(
href,
name,
"height=768,width=1024,resizable=yes,scrollbars=yes"
);
relatedWindows.push(win);
win.focus();
return false;
}
function showRelatedObjectLookupPopup(triggeringLink) {
return showAdminPopup(triggeringLink, /^lookup_/, true);
}
function dismissRelatedLookupPopup(win, chosenId) {
const name = removePopupIndex(win.name);
const elem = document.getElementById(name);
if (elem.classList.contains("vManyToManyRawIdAdminField") && elem.value) {
elem.value += "," + chosenId;
} else {
document.getElementById(name).value = chosenId;
}
const index = relatedWindows.indexOf(win);
if (index > -1) {
relatedWindows.splice(index, 1);
}
win.close();
}
function showRelatedObjectPopup(triggeringLink) {
return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
}
function updateRelatedObjectLinks(triggeringLink) {
const $this = $(triggeringLink);
const siblings = $this.nextAll(
".view-related, .change-related, .delete-related"
);
if (!siblings.length) {
return;
}
const value = $this.val();
if (value) {
siblings.each(function () {
const elm = $(this);
elm.attr(
"href",
elm.attr("data-href-template").replace("__fk__", value)
);
elm.removeAttr("aria-disabled");
});
} else {
siblings.removeAttr("href");
siblings.attr("aria-disabled", true);
}
}
function updateRelatedSelectsOptions(
currentSelect,
win,
objId,
newRepr,
newId,
skipIds = []
) {
// After create/edit a model from the options next to the current
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
// in the page.
const path = win.location.pathname;
// Extract the model from the popup url '.../<model>/add/' or
// '.../<model>/<id>/change/' depending the action (add or change).
const modelName = path.split("/")[path.split("/").length - (objId ? 4 : 3)];
// Select elements with a specific model reference and context of "available-source".
const selectsRelated = document.querySelectorAll(
`[data-model-ref="${modelName}"] [data-context="available-source"]`
);
selectsRelated.forEach(function (select) {
if (
currentSelect === select ||
(skipIds && skipIds.includes(select.id))
) {
return;
}
let option = select.querySelector(`option[value="${objId}"]`);
if (!option) {
option = new Option(newRepr, newId);
select.options.add(option);
// Update SelectBox cache for related fields.
if (
window.SelectBox !== undefined &&
!SelectBox.cache[currentSelect.id]
) {
SelectBox.add_to_cache(select.id, option);
SelectBox.redisplay(select.id);
}
return;
}
option.textContent = newRepr;
option.value = newId;
});
}
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
const name = removePopupIndex(win.name);
const elem = document.getElementById(name);
if (elem) {
const elemName = elem.nodeName.toUpperCase();
if (elemName === "SELECT") {
elem.options[elem.options.length] = new Option(
newRepr,
newId,
true,
true
);
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
} else if (elemName === "INPUT") {
if (
elem.classList.contains("vManyToManyRawIdAdminField") &&
elem.value
) {
elem.value += "," + newId;
} else {
elem.value = newId;
}
}
// Trigger a change event to update related links if required.
$(elem).trigger("change");
} else {
const toId = name + "_to";
const toElem = document.getElementById(toId);
const o = new Option(newRepr, newId);
SelectBox.add_to_cache(toId, o);
SelectBox.redisplay(toId);
if (toElem && toElem.nodeName.toUpperCase() === "SELECT") {
const skipIds = [name + "_from"];
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
}
}
const index = relatedWindows.indexOf(win);
if (index > -1) {
relatedWindows.splice(index, 1);
}
win.close();
}
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
const id = removePopupIndex(win.name.replace(/^edit_/, ""));
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
const selects = $(selectsSelector);
selects
.find("option")
.each(function () {
if (this.value === objId) {
this.textContent = newRepr;
this.value = newId;
}
})
.trigger("change");
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
selects
.next()
.find(".select2-selection__rendered")
.each(function () {
// The element can have a clear button as a child.
// Use the lastChild to modify only the displayed value.
this.lastChild.textContent = newRepr;
this.title = newRepr;
});
const index = relatedWindows.indexOf(win);
if (index > -1) {
relatedWindows.splice(index, 1);
}
win.close();
}
function dismissDeleteRelatedObjectPopup(win, objId) {
const id = removePopupIndex(win.name.replace(/^delete_/, ""));
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
const selects = $(selectsSelector);
selects
.find("option")
.each(function () {
if (this.value === objId) {
$(this).remove();
}
})
.trigger("change");
const index = relatedWindows.indexOf(win);
if (index > -1) {
relatedWindows.splice(index, 1);
}
win.close();
}
window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
window.showRelatedObjectPopup = showRelatedObjectPopup;
window.updateRelatedObjectLinks = updateRelatedObjectLinks;
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
window.dismissChildPopups = dismissChildPopups;
// Kept for backward compatibility
window.showAddAnotherPopup = showRelatedObjectPopup;
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
window.addEventListener("unload", function (evt) {
window.dismissChildPopups();
});
$(document).ready(function () {
setPopupIndex();
$("a[data-popup-opener]").on("click", function (event) {
event.preventDefault();
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
});
$("body").on(
"click",
'.related-widget-wrapper-link[data-popup="yes"]',
function (e) {
e.preventDefault();
if (this.href) {
const event = $.Event("django:show-related", { href: this.href });
$(this).trigger(event);
if (!event.isDefaultPrevented()) {
showRelatedObjectPopup(this);
}
}
}
);
$("body").on("change", ".related-widget-wrapper select", function (e) {
const event = $.Event("django:update-related");
$(this).trigger(event);
if (!event.isDefaultPrevented()) {
updateRelatedObjectLinks(this);
}
});
$(".related-widget-wrapper select").trigger("change");
$("body").on("click", ".related-lookup", function (e) {
e.preventDefault();
const event = $.Event("django:lookup-related");
$(this).trigger(event);
if (!event.isDefaultPrevented()) {
showRelatedObjectLookupPopup(this);
}
});
});
}

474
static/admin/js/inlines.js Normal file
View File

@ -0,0 +1,474 @@
/*global DateTimeShortcuts, SelectFilter*/
/**
* Django admin inlines
*
* Based on jQuery Formset 1.1
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Spiced up with Code from Zain Memon's GSoC project 2009
* and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip.
*
* Licensed under the New BSD License
* See: https://opensource.org/licenses/bsd-license.php
*/
"use strict";
{
const $ = django.jQuery;
$.fn.formset = function (opts) {
const options = $.extend({}, $.fn.formset.defaults, opts);
const $this = $(this);
const $parent = $this.parent();
const updateElementIndex = function (el, prefix, ndx) {
const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
const replacement = prefix + "-" + ndx;
if ($(el).prop("for")) {
$(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
// !CHANGED from original
// el.name = el.name.replace(id_regex, replacement);
el.setAttribute("name", el.name.replace(id_regex, replacement));
}
};
const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop(
"autocomplete",
"off"
);
let nextIndex = parseInt(totalForms.val(), 10);
const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop(
"autocomplete",
"off"
);
const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop(
"autocomplete",
"off"
);
let addButton;
/**
* The "Add another MyModel" button below the inline forms.
*/
const addInlineAddButton = function () {
if (addButton === null) {
if ($this.prop("tagName") === "TR") {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
const numCols = $this.eq(-1).children().length;
$parent.append(
'<tr class="' +
options.addCssClass +
'"><td colspan="' +
numCols +
'"><a href="#">' +
options.addText +
"</a></tr>"
);
addButton = $parent.find("tr:last a");
} else {
// Otherwise, insert it immediately after the last form:
$this
.filter(":last")
.after(
'<div class="' +
options.addCssClass +
'"><a href="#">' +
options.addText +
"</a></div>"
);
addButton = $this.filter(":last").next().find("a");
}
}
addButton.on("click", addInlineClickHandler);
};
const addInlineClickHandler = function (e) {
e.preventDefault();
const template = $("#" + options.prefix + "-empty");
const row = template.clone(true);
row
.removeClass(options.emptyCssClass)
.addClass(options.formCssClass)
.attr("id", options.prefix + "-" + nextIndex);
addInlineDeleteButton(row);
row.find("*").each(function () {
updateElementIndex(this, options.prefix, totalForms.val());
});
// Insert the new form when it has been fully edited.
// !CHANGED from original
if ($(template).parent().is("tbody")) {
row
.wrap('<tbody class="template"></tbody>')
.parent()
.insertBefore($(template).parent());
} else {
row.insertBefore($(template));
}
// Update number of total forms.
$(totalForms).val(parseInt(totalForms.val(), 10) + 1);
nextIndex += 1;
// Hide the add button if there's a limit and it's been reached.
if (maxForms.val() !== "" && maxForms.val() - totalForms.val() <= 0) {
addButton.parent().hide();
}
// Show the remove buttons if there are more than min_num.
toggleDeleteButtonVisibility(row.closest(".inline-group"));
// Pass the new form to the post-add callback, if provided.
if (options.added) {
options.added(row);
}
row.get(0).dispatchEvent(
new CustomEvent("formset:added", {
bubbles: true,
detail: {
formsetName: options.prefix,
},
})
);
};
/**
* The "X" button that is part of every unsaved inline.
* (When saved, it is replaced with a "Delete" checkbox.)
*/
const addInlineDeleteButton = function (row) {
if (row.is("tr")) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row
.children(":last")
.append(
'<div><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></div>"
);
} else if (row.is("ul") || row.is("ol")) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append(
'<li><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></li>"
);
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row
.children(":first")
.append(
'<span><a class="' +
options.deleteCssClass +
'" href="#">' +
options.deleteText +
"</a></span>"
);
}
// Add delete handler for each row.
row
.find("a." + options.deleteCssClass)
.on("click", inlineDeleteHandler.bind(this));
};
const inlineDeleteHandler = function (e1) {
e1.preventDefault();
const deleteButton = $(e1.target);
const row = deleteButton.closest("." + options.formCssClass);
const inlineGroup = row.closest(".inline-group");
// Remove the parent form containing this button,
// and also remove the relevant row with non-field errors:
const prevRow = row.prev();
if (prevRow.length && prevRow.hasClass("row-form-errors")) {
prevRow.remove();
}
// !CHANGED from original
if (deleteButton.parent().parent().parent().parent().is("tbody")) {
row.parent().remove();
} else {
row.remove();
}
nextIndex -= 1;
// Pass the deleted form to the post-delete callback, if provided.
if (options.removed) {
options.removed(row);
}
document.dispatchEvent(
new CustomEvent("formset:removed", {
detail: {
formsetName: options.prefix,
},
})
);
// Update the TOTAL_FORMS form count.
const forms = $("." + options.formCssClass);
$("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
// Show add button again once below maximum number.
if (maxForms.val() === "" || maxForms.val() - forms.length > 0) {
addButton.parent().show();
}
// Hide the remove buttons if at min_num.
toggleDeleteButtonVisibility(inlineGroup);
// Also, update names and ids for all remaining form controls so
// they remain in sequence:
let i, formCount;
const updateElementCallback = function () {
updateElementIndex(this, options.prefix, i);
};
for (i = 0, formCount = forms.length; i < formCount; i++) {
updateElementIndex($(forms).get(i), options.prefix, i);
$(forms.get(i)).find("*").each(updateElementCallback);
}
};
const toggleDeleteButtonVisibility = function (inlineGroup) {
if (minForms.val() !== "" && minForms.val() - totalForms.val() >= 0) {
inlineGroup.find(".inline-deletelink").hide();
} else {
inlineGroup.find(".inline-deletelink").show();
}
};
// !CHANGED from original. Business logic for tabular inlines is different.
if ($this.parent().is("tbody")) {
$this
.parent()
.parent()
.find("tr.form-row")
.each(function (i) {
$(this)
.not("." + options.emptyCssClass)
.addClass(options.formCssClass);
});
} else {
$this.each(function (i) {
$(this)
.not("." + options.emptyCssClass)
.addClass(options.formCssClass);
});
}
// Create the delete buttons for all unsaved inlines:
// !CHANGED from original, added parent() and used find() instead of filter()
$this
.parent()
.parent()
.find(
"." +
options.formCssClass +
":not(.has_original):not(." +
options.emptyCssClass +
")"
)
.each(function () {
addInlineDeleteButton($(this));
});
toggleDeleteButtonVisibility($this);
// Create the add button, initially hidden.
addButton = options.addButton;
addInlineAddButton();
// Show the add button if allowed to add more items.
// Note that max_num = None translates to a blank string.
const showAddButton =
maxForms.val() === "" || maxForms.val() - totalForms.val() > 0;
if ($this.length && showAddButton) {
addButton.parent().show();
} else {
addButton.parent().hide();
}
return this;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: "form", // The form prefix for your django formset
addText: "add another", // Text for the add link
deleteText: "remove", // Text for the delete link
addCssClass: "add-row", // CSS class applied to the add link
deleteCssClass: "delete-row", // CSS class applied to the delete link
emptyCssClass: "empty-row", // CSS class applied to the empty row
formCssClass: "dynamic-form", // CSS class applied to each form in a formset
added: null, // Function called each time a new form is added
removed: null, // Function called each time a form is deleted
addButton: null, // Existing add button to use
};
// Tabular inlines ---------------------------------------------------------
$.fn.tabularFormset = function (selector, options) {
const $rows = $(this);
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function () {
// If any SelectFilter widgets are a part of the new form,
// instantiate a new SelectFilter instance for it.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
dependencies.push(
"#" +
row
.find(".field-" + field_name)
.find("input, select, textarea")
.attr("id")
);
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
},
addButton: options.addButton,
});
return $rows;
};
// Stacked inlines ---------------------------------------------------------
$.fn.stackedFormset = function (selector, options) {
const $rows = $(this);
const updateInlineLabel = function (row) {
$(selector)
.find(".inline_label")
.each(function (i) {
const count = i + 1;
$(this).html(
$(this)
.html()
.replace(/(#\d+)/g, "#" + count)
);
});
};
const reinitDateTimeShortCuts = function () {
// Reinitialize the calendar and clock widgets by force, yuck.
if (typeof DateTimeShortcuts !== "undefined") {
$(".datetimeshortcuts").remove();
DateTimeShortcuts.init();
}
};
const updateSelectFilter = function () {
// If any SelectFilter widgets were added, instantiate a new instance.
if (typeof SelectFilter !== "undefined") {
$(".selectfilter").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, false);
});
$(".selectfilterstacked").each(function (index, value) {
SelectFilter.init(value.id, this.dataset.fieldName, true);
});
}
};
const initPrepopulatedFields = function (row) {
row.find(".prepopulated_field").each(function () {
const field = $(this),
input = field.find("input, select, textarea"),
dependency_list = input.data("dependency_list") || [],
dependencies = [];
$.each(dependency_list, function (i, field_name) {
// Dependency in a fieldset.
let field_element = row.find(".form-row .field-" + field_name);
// Dependency without a fieldset.
if (!field_element.length) {
field_element = row.find(".form-row.field-" + field_name);
}
dependencies.push(
"#" + field_element.find("input, select, textarea").attr("id")
);
});
if (dependencies.length) {
input.prepopulate(dependencies, input.attr("maxlength"));
}
});
};
$rows.formset({
prefix: options.prefix,
addText: options.addText,
formCssClass: "dynamic-" + options.prefix,
deleteCssClass: "inline-deletelink",
deleteText: options.deleteText,
emptyCssClass: "empty-form",
removed: updateInlineLabel,
added: function (row) {
initPrepopulatedFields(row);
reinitDateTimeShortCuts();
updateSelectFilter();
updateInlineLabel(row);
},
addButton: options.addButton,
});
return $rows;
};
$(document).ready(function () {
$(".js-inline-admin-formset").each(function () {
const data = $(this).data(),
inlineOptions = data.inlineFormset;
let selector;
switch (data.inlineType) {
case "stacked":
selector = inlineOptions.name + "-group .inline-related";
$(selector).stackedFormset(selector, inlineOptions.options);
break;
case "tabular":
selector =
inlineOptions.name +
"-group .tabular.inline-related tbody:last > tr.form-row";
$(selector).tabularFormset(selector, inlineOptions.options);
break;
}
});
});
}

Binary file not shown.

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jonathan Nicol
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,230 @@
[data-simplebar] {
position: relative;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: flex-start;
}
.simplebar-wrapper {
overflow: hidden;
width: inherit;
height: inherit;
max-width: inherit;
max-height: inherit;
}
.simplebar-mask {
direction: inherit;
position: absolute;
overflow: hidden;
padding: 0;
margin: 0;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: auto !important;
height: auto !important;
z-index: 0;
}
.simplebar-offset {
direction: inherit !important;
box-sizing: inherit !important;
resize: none !important;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0;
margin: 0;
-webkit-overflow-scrolling: touch;
}
.simplebar-content-wrapper {
direction: inherit;
box-sizing: border-box !important;
position: relative;
display: block;
height: 100%; /* Required for horizontal native scrollbar to not appear if parent is taller than natural height */
width: auto;
max-width: 100%; /* Not required for horizontal scroll to trigger */
max-height: 100%; /* Needed for vertical scroll to trigger */
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.simplebar-content-wrapper::-webkit-scrollbar,
.simplebar-hide-scrollbar::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.simplebar-content:before,
.simplebar-content:after {
content: ' ';
display: table;
}
.simplebar-placeholder {
max-height: 100%;
max-width: 100%;
width: 100%;
pointer-events: none;
}
.simplebar-height-auto-observer-wrapper {
box-sizing: inherit !important;
height: 100%;
width: 100%;
max-width: 1px;
position: relative;
float: left;
max-height: 1px;
overflow: hidden;
z-index: -1;
padding: 0;
margin: 0;
pointer-events: none;
flex-grow: inherit;
flex-shrink: 0;
flex-basis: 0;
}
.simplebar-height-auto-observer {
box-sizing: inherit;
display: block;
opacity: 0;
position: absolute;
top: 0;
left: 0;
height: 1000%;
width: 1000%;
min-height: 1px;
min-width: 1px;
overflow: hidden;
pointer-events: none;
z-index: -1;
}
.simplebar-track {
z-index: 1;
position: absolute;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
}
[data-simplebar].simplebar-dragging {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
[data-simplebar].simplebar-dragging .simplebar-content {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
[data-simplebar].simplebar-dragging .simplebar-track {
pointer-events: all;
}
.simplebar-scrollbar {
position: absolute;
left: 0;
right: 0;
min-height: 10px;
}
.simplebar-scrollbar:before {
position: absolute;
content: '';
background: black;
border-radius: 7px;
left: 2px;
right: 2px;
opacity: 0;
transition: opacity 0.2s 0.5s linear;
}
.simplebar-scrollbar.simplebar-visible:before {
opacity: 0.5;
transition-delay: 0s;
transition-duration: 0s;
}
.simplebar-track.simplebar-vertical {
top: 0;
width: 11px;
}
.simplebar-scrollbar:before {
top: 2px;
bottom: 2px;
left: 2px;
right: 2px;
}
.simplebar-track.simplebar-horizontal {
left: 0;
height: 11px;
}
.simplebar-track.simplebar-horizontal .simplebar-scrollbar {
right: auto;
left: 0;
top: 0;
bottom: 0;
min-height: 0;
min-width: 10px;
width: auto;
}
/* Rtl support */
[data-simplebar-direction='rtl'] .simplebar-track.simplebar-vertical {
right: auto;
left: 0;
}
.simplebar-dummy-scrollbar-size {
direction: rtl;
position: fixed;
opacity: 0;
visibility: hidden;
height: 500px;
width: 500px;
overflow-y: hidden;
overflow-x: scroll;
-ms-overflow-style: scrollbar !important;
}
.simplebar-dummy-scrollbar-size > div {
width: 200%;
height: 200%;
margin: 10px 0;
}
.simplebar-hide-scrollbar {
position: fixed;
left: 0;
visibility: hidden;
overflow-y: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,31 @@
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(Inter-Regular.woff2) format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(Inter-Medium.woff2) format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(Inter-SemiBold.woff2) format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(Inter-Bold.woff2) format("woff2");
}

Some files were not shown because too many files have changed in this diff Show More