update
This commit is contained in:
parent
1a0bb8c0c1
commit
0357921e3d
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
31
.idea/NorahUniversity.iml
generated
Normal 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="<map/>" />
|
||||
<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>
|
||||
43
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
43
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
17
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
4
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings" defaultProject="true" />
|
||||
</project>
|
||||
0
NorahUniversity/__init__.py
Normal file
0
NorahUniversity/__init__.py
Normal file
BIN
NorahUniversity/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
NorahUniversity/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
NorahUniversity/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/settings.cpython-311.pyc
Normal file
BIN
NorahUniversity/__pycache__/settings.cpython-311.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/settings.cpython-312.pyc
Normal file
BIN
NorahUniversity/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/urls.cpython-311.pyc
Normal file
BIN
NorahUniversity/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/urls.cpython-312.pyc
Normal file
BIN
NorahUniversity/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/wsgi.cpython-311.pyc
Normal file
BIN
NorahUniversity/__pycache__/wsgi.cpython-311.pyc
Normal file
Binary file not shown.
BIN
NorahUniversity/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
NorahUniversity/__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
16
NorahUniversity/asgi.py
Normal file
16
NorahUniversity/asgi.py
Normal 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
186
NorahUniversity/settings.py
Normal 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
36
NorahUniversity/urls.py
Normal 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
16
NorahUniversity/wsgi.py
Normal 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
BIN
db.sqlite3
Normal file
Binary file not shown.
22
manage.py
Executable file
22
manage.py
Executable 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()
|
||||
BIN
media/resumes/Abdullah_Bakhsh_-_2025.pdf
Normal file
BIN
media/resumes/Abdullah_Bakhsh_-_2025.pdf
Normal file
Binary file not shown.
BIN
media/resumes/CV_Marwan_Alwali.pdf.pdf
Normal file
BIN
media/resumes/CV_Marwan_Alwali.pdf.pdf
Normal file
Binary file not shown.
BIN
media/resumes/Hilayel_Al-Rawi_Resume.pdf
Normal file
BIN
media/resumes/Hilayel_Al-Rawi_Resume.pdf
Normal file
Binary file not shown.
BIN
media/resumes/Raed_Serhan_Resume_2024.pdf
Normal file
BIN
media/resumes/Raed_Serhan_Resume_2024.pdf
Normal file
Binary file not shown.
145
pyproject.toml
Normal file
145
pyproject.toml
Normal 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
BIN
recruitment/.DS_Store
vendored
Normal file
Binary file not shown.
0
recruitment/__init__.py
Normal file
0
recruitment/__init__.py
Normal file
BIN
recruitment/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/admin.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/admin.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/admin.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/apps.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/apps.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/apps.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/dashboard.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/dashboard.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/dashboard.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/dashboard.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/forms.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/forms.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/models.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/models.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/models.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/serializers.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/serializers.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/urls.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/urls.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/urls.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/utils.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/utils.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/utils.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/views.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/views.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/views.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/views_frontend.cpython-311.pyc
Normal file
BIN
recruitment/__pycache__/views_frontend.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/__pycache__/views_frontend.cpython-312.pyc
Normal file
BIN
recruitment/__pycache__/views_frontend.cpython-312.pyc
Normal file
Binary file not shown.
56
recruitment/admin.py
Normal file
56
recruitment/admin.py
Normal 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
6
recruitment/apps.py
Normal 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
7
recruitment/dashboard.py
Normal 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
7
recruitment/forms.py
Normal 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
26
recruitment/linkedin.py
Normal 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)
|
||||
0
recruitment/management/__init__.py
Normal file
0
recruitment/management/__init__.py
Normal file
BIN
recruitment/management/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
recruitment/management/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
0
recruitment/management/commands/__init__.py
Normal file
0
recruitment/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
48
recruitment/management/commands/generate_data.py
Normal file
48
recruitment/management/commands/generate_data.py
Normal 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"))
|
||||
40
recruitment/migrations/0001_initial.py
Normal file
40
recruitment/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
28
recruitment/migrations/0002_trainingmaterial.py
Normal file
28
recruitment/migrations/0002_trainingmaterial.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
0
recruitment/migrations/__init__.py
Normal file
0
recruitment/migrations/__init__.py
Normal file
BIN
recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
recruitment/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
recruitment/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal 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.
BIN
recruitment/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
recruitment/migrations/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
recruitment/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
recruitment/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
39
recruitment/models.py
Normal file
39
recruitment/models.py
Normal 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
|
||||
12
recruitment/serializers.py
Normal file
12
recruitment/serializers.py
Normal 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
3
recruitment/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
recruitment/urls.py
Normal file
11
recruitment/urls.py
Normal 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
46
recruitment/utils.py
Normal 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
14
recruitment/views.py
Normal 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
|
||||
|
||||
54
recruitment/views_frontend.py
Normal file
54
recruitment/views_frontend.py
Normal 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
31
recruitment/zoom_api.py
Normal 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
134
requirements.txt
Normal 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
BIN
static/.DS_Store
vendored
Normal file
Binary file not shown.
295
static/admin/js/admin/RelatedObjectLookups.js
Normal file
295
static/admin/js/admin/RelatedObjectLookups.js
Normal 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
474
static/admin/js/inlines.js
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
BIN
static/media/resumes/Abdullah_Bakhsh_-_2025.pdf
Normal file
BIN
static/media/resumes/Abdullah_Bakhsh_-_2025.pdf
Normal file
Binary file not shown.
21
static/unfold/css/simplebar/LICENSE
Normal file
21
static/unfold/css/simplebar/LICENSE
Normal 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.
|
||||
230
static/unfold/css/simplebar/simplebar.css
Normal file
230
static/unfold/css/simplebar/simplebar.css
Normal 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;
|
||||
}
|
||||
2
static/unfold/css/styles.css
Normal file
2
static/unfold/css/styles.css
Normal file
File diff suppressed because one or more lines are too long
BIN
static/unfold/fonts/inter/Inter-Bold.woff2
Normal file
BIN
static/unfold/fonts/inter/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
static/unfold/fonts/inter/Inter-Medium.woff2
Normal file
BIN
static/unfold/fonts/inter/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
static/unfold/fonts/inter/Inter-Regular.woff2
Normal file
BIN
static/unfold/fonts/inter/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
static/unfold/fonts/inter/Inter-SemiBold.woff2
Normal file
BIN
static/unfold/fonts/inter/Inter-SemiBold.woff2
Normal file
Binary file not shown.
92
static/unfold/fonts/inter/LICENSE
Normal file
92
static/unfold/fonts/inter/LICENSE
Normal 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.
|
||||
31
static/unfold/fonts/inter/styles.css
Normal file
31
static/unfold/fonts/inter/styles.css
Normal 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
Loading…
x
Reference in New Issue
Block a user