├── .dockerignore ├── .env.dev.sample ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── pythonapp.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── Dockerfile.prod ├── Jenkinsfile ├── LICENSE ├── Makefile ├── Procfile ├── Procfile.windows ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── custom_claims.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── apps.py ├── fixtures │ └── accounts_initial_data.json ├── forms.py ├── graphql │ ├── __init__.py │ ├── constants.py │ ├── exceptions.py │ ├── graphql_mixins.py │ ├── mutations.py │ └── sub_mutations.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20190326_1754.py │ ├── 0003_alter_user_first_name.py │ ├── 0004_alter_user_gender.py │ └── __init__.py ├── models.py ├── pipeline.py ├── urls.py └── views.py ├── categories ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── deployment ├── jenkins_deploy_prod.sh ├── jenkins_deploy_prod_docker.sh └── nginx │ ├── Dockerfile │ ├── nginx.conf │ └── sites-enabled │ └── django_project ├── docker-compose.prod.yml ├── docker-compose.yml ├── entrypoint.prod.sh ├── entrypoint.sh ├── jobs ├── __init__.py ├── middlewares.py ├── schema.py ├── settings.py ├── sitemaps.py ├── urls.py └── wsgi.py ├── jobsapp ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── custom_exception.py │ ├── permissions.py │ ├── serializers.py │ ├── urls.py │ └── views │ │ ├── __init__.py │ │ ├── common.py │ │ ├── employee.py │ │ └── employer.py ├── apps.py ├── decorators.py ├── documents.py ├── forms.py ├── graphql │ ├── __init__.py │ ├── exceptions.py │ ├── graphql_base.py │ ├── graphql_mixins.py │ ├── input_types.py │ ├── mutations.py │ ├── permissions.py │ ├── queries.py │ ├── sub_mutations.py │ └── types.py ├── manager.py ├── metrics.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20190405_1920.py │ ├── 0003_job_created_at.py │ ├── 0004_job_filled.py │ ├── 0005_applicant.py │ ├── 0006_auto_20190408_2005.py │ ├── 0007_job_salary.py │ ├── 0008_auto_20200810_1925.py │ ├── 0009_favorite.py │ ├── 0010_auto_20201107_1404.py │ ├── 0011_auto_20201118_1751.py │ ├── 0012_job_tags.py │ ├── 0013_job_vacancy.py │ └── __init__.py ├── mixins.py ├── models.py ├── templatetags │ ├── __init__.py │ ├── is_already_applied.py │ ├── is_favorited.py │ ├── tag_exists.py │ └── url_replace.py ├── urls.py └── views │ ├── __init__.py │ ├── employee.py │ ├── employer.py │ └── home.py ├── locale └── bn │ └── LC_MESSAGES │ └── django.po ├── manage.py ├── media └── resumes │ └── 2022 │ └── 03 │ └── 28 │ ├── 1VGE2R2iED2J3VaE5s20KXcLWPXrr6.png │ ├── GobYkNGjKLMq57nrJbbr3m2fMpYZeW.png │ ├── SBH1pvrgUztNXXKL3eRS5GbcF0EHiC.png │ └── rrh11hPn7oiHIiVFt3S1rv28xv9qBY.png ├── provisioning ├── grafana │ ├── dashboards │ │ ├── app-dashboard.json │ │ ├── dashboard.yaml │ │ └── device-dashboard.json │ └── datasources │ │ └── datasource.yaml └── prometheus │ ├── prometheus.yml │ └── rules.yml ├── pyproject.toml ├── requirements.txt ├── resume_cv ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_resumecvcategory.py │ ├── 0003_resumecvtemplate.py │ ├── 0004_alter_resumecvcategory_thumbnail.py │ ├── 0005_resumecv_template.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── runtime.txt ├── screenshots ├── five.png ├── four.png ├── illustration.png ├── one.png ├── seven.png ├── six.png ├── three.png └── two.png ├── static ├── css │ ├── builder.css │ ├── custom.css │ ├── customize.css │ ├── font-awesome.css │ ├── select2-bootstrap.css │ ├── style.default.css │ └── templates.css ├── img │ ├── 1875187.jpg │ ├── avatar.png │ ├── company-1.png │ ├── featured1.jpg │ ├── logo-small.png │ └── meeting.jpg ├── js │ ├── builder.js │ ├── custom-builder.js │ ├── custom.js │ ├── front.js │ └── preset.js ├── vendor │ ├── bootstrap-select │ │ ├── css │ │ │ └── bootstrap-select.min.css │ │ └── js │ │ │ ├── bootstrap-select.js.map │ │ │ └── bootstrap-select.min.js │ ├── bootstrap │ │ ├── css │ │ │ └── bootstrap.min.css │ │ └── js │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ ├── font-awesome │ │ └── css │ │ │ └── font-awesome.min.css │ ├── jquery.cookie │ │ └── jquery.cookie.js │ ├── jquery │ │ └── jquery.min.js │ ├── owl.carousel │ │ ├── assets │ │ │ ├── owl.carousel.css │ │ │ ├── owl.theme.default.css │ │ │ └── owl.video.play.png │ │ └── owl.carousel.min.js │ └── popper.js │ │ └── umd │ │ └── popper.min.js └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── tags ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── templates ├── about_us.html ├── accounts │ ├── employee │ │ └── register.html │ ├── employer │ │ └── register.html │ └── login.html ├── base.html ├── flatpages │ └── default.html ├── home.html ├── jobs │ ├── create.html │ ├── details.html │ ├── employee │ │ ├── edit-profile.html │ │ ├── favorites.html │ │ └── my-applications.html │ ├── employer │ │ ├── all-applicants.html │ │ ├── applicants.html │ │ ├── applied-applicant-view.html │ │ ├── dashboard.html │ │ └── edit-profile.html │ ├── jobs.html │ ├── search.html │ └── update.html ├── lang_selector.html └── resumes │ ├── builder.html │ ├── templates.html │ └── user_resumes.html ├── tests ├── __init__.py ├── accounts │ ├── __init__.py │ ├── test_api_views.py │ ├── test_forms.py │ ├── test_models.py │ ├── test_serializers.py │ └── test_views.py ├── factories │ ├── __init__.py │ ├── category_factory.py │ ├── job_factory.py │ ├── tag_factory.py │ └── user_factory.py ├── jobsapp │ ├── __init__.py │ ├── test_api_views.py │ ├── test_forms.py │ ├── test_models.py │ ├── test_serializers.py │ └── test_views.py └── tags │ ├── __init__.py │ └── test_api_views.py └── utils ├── __init__.py ├── filename.py └── namedtuples.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /.env.dev.sample: -------------------------------------------------------------------------------- 1 | # Third-party login keys 2 | SOCIAL_AUTH_GITHUB_KEY=github-key 3 | SOCIAL_AUTH_GITHUB_SECRET=github-secret 4 | 5 | SOCIAL_AUTH_FACEBOOK_KEY=key 6 | SOCIAL_AUTH_FACEBOOK_SECRET=secret 7 | 8 | SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY=key 9 | SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET=secret 10 | 11 | SOCIAL_AUTH_GOOGLE_OAUTH2_KEY=key 12 | SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET=secret 13 | 14 | RECAPTCHA_PUBLIC_KEY=key 15 | RECAPTCHA_PRIVATE_KEY=secret 16 | 17 | # To enable prometheus monitoring 18 | ENABLE_PROMETHEUS=0 19 | 20 | DEBUG=True 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.py linguist-detectable=true 2 | *.js linguist-detectable=false 3 | *.css linguist-detectable=false 4 | *.html linguist-detectable=false 5 | *.xml linguist-detectable=false 6 | 7 | *.* linguist-language=Python 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Checklist** 11 | 12 | - [ ] I have verified that that issue exists against the `master` branch. 13 | - [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. 14 | - [ ] I have reduced the issue to the simplest possible case. 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is. 18 | 19 | **Specify the team with the correct label** 20 | Add a label in the issue, selecting the correct team that this issue belongs to. 21 | 22 | **To Reproduce** 23 | Steps to reproduce the behavior: 24 | 1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error 28 | 29 | **Expected behavior** 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Screenshots** 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | **Desktop (please complete the following information):** 36 | - OS: [e.g. Arch Linux 2020.10.01, macOS 10.15.6] 37 | - Browser [e.g. brave, chrome, safari] 38 | - Version [e.g. 22] 39 | 40 | **Smartphone (please complete the following information):** 41 | - Device: [e.g. Xiaomi Redmi Note Pro 9, iPhone6] 42 | - OS: [e.g. iOS 8.1] 43 | - Browser [e.g. stock browser, safari] 44 | - Version [e.g. 22] 45 | 46 | **Additional context** 47 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: job-portal 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - run: | 13 | git fetch --prune --unshallow 14 | - name: Set up Python 3.11 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.11 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | - name: Run black code formatter 23 | run: | 24 | black --check --exclude='./*/migrations/' ./ 25 | - name: Run tests 26 | run: | 27 | python manage.py collectstatic 28 | python manage.py test # Don't forget to run tests 29 | # - name: Deploy to Heroku 30 | # env: 31 | # HEROKU_API_TOKEN: ${{ secrets.HEROKU_API_TOKEN }} 32 | # HEROKU_APP_NAME: ${{ secrets.HEROKU_APP_NAME }} 33 | # if: github.ref == 'refs/heads/master' && job.status == 'success' 34 | # run: | 35 | # git remote add heroku https://heroku:$HEROKU_API_TOKEN@git.heroku.com/$HEROKU_APP_NAME.git 36 | # git push heroku HEAD:master -f 37 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 21.6b0 4 | hooks: 5 | - id: black 6 | exclude: ^migrations/ 7 | 8 | - repo: https://github.com/pre-commit/mirrors-isort 9 | rev: 'v5.9.1' 10 | hooks: 11 | - id: isort 12 | exclude: ^migrations/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /app 4 | 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | COPY requirements.txt requirements.txt 9 | 10 | # RUN python -m pip install --upgrade pip 11 | RUN pip install -r requirements.txt 12 | 13 | COPY . . 14 | 15 | RUN cp .env.dev.sample .env 16 | 17 | #EXPOSE 8000 18 | 19 | RUN chmod +x entrypoint.sh 20 | 21 | ENV APP_HOME=/app 22 | ENV DEBUG=1 23 | RUN mkdir $APP_HOME/staticfiles 24 | RUN mkdir $APP_HOME/mediafiles 25 | 26 | CMD ["sh", "entrypoint.sh"] 27 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Copy only requirements first to leverage Docker cache 9 | COPY requirements.txt . 10 | 11 | # Install system dependencies and Python packages in a single layer 12 | RUN apk update \ 13 | && apk add --no-cache \ 14 | nano \ 15 | gobject-introspection-dev \ 16 | pango-dev \ 17 | cairo-dev \ 18 | libffi-dev \ 19 | libmagic \ 20 | libxml2-dev \ 21 | libxslt-dev \ 22 | && python -m pip install --upgrade pip \ 23 | && pip install -r requirements.txt 24 | 25 | # Copy the rest of the application 26 | COPY . . 27 | 28 | RUN cp .env.dev.sample .env \ 29 | && chmod +x entrypoint.prod.sh 30 | 31 | ENV APP_HOME=/usr/src/app 32 | # No need to create directories twice 33 | RUN mkdir -p $APP_HOME/staticfiles \ 34 | && mkdir -p $APP_HOME/mediafiles 35 | 36 | RUN echo "Running from production dockerfile" 37 | 38 | # Collect static files 39 | RUN python manage.py collectstatic --noinput 40 | 41 | CMD ["sh", "entrypoint.prod.sh"] 42 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | // node { 4 | // 5 | // try { 6 | // stage 'Checkout' 7 | // checkout scm 8 | // 9 | // sh 'git log HEAD^..HEAD --pretty="%h %an - %s" > GIT_CHANGES' 10 | // def lastChanges = readFile('GIT_CHANGES') 11 | // //slackSend color: "warning", message: "Started `${env.JOB_NAME}#${env.BUILD_NUMBER}`\n\n_The changes:_\n${lastChanges}" 12 | // 13 | // stage 'Deploy' 14 | // sh './deployment/deploy_prod.sh' 15 | // 16 | // stage 'Publish results' 17 | // echo "Deployment successful" 18 | // //slackSend color: "good", message: "Build successful: `${env.JOB_NAME}#${env.BUILD_NUMBER}` <${env.BUILD_URL}|Open in Jenkins>" 19 | // } 20 | // 21 | // catch (err) { 22 | // echo "Error found" 23 | // //slackSend color: "danger", message: "Build failed :face_with_head_bandage: \n`${env.JOB_NAME}#${env.BUILD_NUMBER}` <${env.BUILD_URL}|Open in Jenkins>" 24 | // 25 | // throw err 26 | // } 27 | // 28 | // } 29 | 30 | // pipeline { 31 | // agent any 32 | // 33 | // stages { 34 | // stage('Checkout') { 35 | // steps { 36 | // git 'https://github.com/manjurulhoque/django-job-portal.git' 37 | // } 38 | // } 39 | // 40 | // stage('Build and Deploy') { 41 | // steps { 42 | // script { 43 | // // navigate into the correct directory 44 | // sh 'cd projects/django/job-portal/' 45 | // 46 | // // build and run docker-compose 47 | // sh 'docker-compose -f docker-compose.prod.yml up --build -d' 48 | // } 49 | // } 50 | // } 51 | // } 52 | // } 53 | 54 | node { 55 | 56 | try { 57 | stage 'Checkout' 58 | checkout scm 59 | 60 | sh 'git log HEAD^..HEAD --pretty="%h %an - %s" > GIT_CHANGES' 61 | def lastChanges = readFile('GIT_CHANGES') 62 | //slackSend color: "warning", message: "Started `${env.JOB_NAME}#${env.BUILD_NUMBER}`\n\n_The changes:_\n${lastChanges}" 63 | 64 | stage 'Deploy' 65 | // sh 'cd projects/django/job-portal/' 66 | sh './deployment/jenkins_deploy_prod_docker.sh' 67 | 68 | stage 'Publish results' 69 | echo "Deployment successful" 70 | //slackSend color: "good", message: "Build successful: `${env.JOB_NAME}#${env.BUILD_NUMBER}` <${env.BUILD_URL}|Open in Jenkins>" 71 | } 72 | 73 | catch (err) { 74 | echo "Error found" 75 | //slackSend color: "danger", message: "Build failed :face_with_head_bandage: \n`${env.JOB_NAME}#${env.BUILD_NUMBER}` <${env.BUILD_URL}|Open in Jenkins>" 76 | 77 | throw err 78 | } 79 | 80 | } 81 | 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Manjurul Hoque Rumi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := dev 2 | 3 | dev: 4 | python manage.py migrate && python manage.py runserver 5 | 6 | test: 7 | python manage.py test 8 | 9 | bi: 10 | isort . && black . 11 | 12 | delete: 13 | find . -path "*/migrations/*.py" -not -name "__init__.py" -delete && find . -path "*/migrations/*.pyc" -delete 14 | 15 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn jobs.wsgi --log-file - -------------------------------------------------------------------------------- /Procfile.windows: -------------------------------------------------------------------------------- 1 | web: python manage.py runserver 0.0.0.0:5000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Job Interview 4 | 5 | # Django Job Portal 6 | 7 |
8 | 9 | ## Django Job Portal 10 | 11 | #### An open source online job portal. 12 | 13 |

14 | forks 15 | stars 16 | watchers 17 | github Actions 18 |

19 | 20 | Live: [Demo](http://djp.manjurulhoque.com/en/) 21 | 22 | Used Tech Stack 23 | 24 | 1. Django 25 | 2. Sqlite 26 | 27 | ### Screenshots 28 | 29 | ## Home page 30 | 31 | 32 | ## Resume template page 33 | 34 | 35 | 36 | ## Login page 37 | login 38 | 39 | ## Add new position as employer 40 | form 41 | 42 | ## Job details 43 | details 44 | 45 | ## Swagger API 46 | 47 | 48 | 49 | ### Local environment 50 | 51 | #### Install 52 | 53 | 1. Create a virtual environment 54 | 55 | `virtualenv venv` 56 | 57 | Or 58 | 59 | `python3.11 -m venv venv` 60 | 61 | 2. Activate it 62 | 63 | `source venv/bin/activate` 64 | 65 | 3. Clone the repository and install the packages in the virtual env: 66 | 67 | `pip install -r requirements.txt` 68 | 69 | 4. Add `.env` file. 70 | 71 | `cp .env.dev.sample .env` 72 | 73 | 5. Add Github client ID and client secret in the `.env` file 74 | 75 | #### Run 76 | 77 | 1. With the venv activate it, execute: 78 | 79 | python manage.py collectstatic 80 | 81 | *Note* : Collect static is not necessary when debug is True (in dev mode) 82 | 83 | 2. Create initial database: 84 | 85 | `python manage.py migrate` 86 | 87 | 3. Load demo data (optional): 88 | 89 | `python manage.py loaddata fixtures/app_name_initial_data.json --app app.model_name` 90 | 91 | 4. Run server: 92 | 93 | `python manage.py runserver` 94 | 95 | 5. Default django admin credentials: 96 | 97 | `email: admin@admin.com` 98 | `password: admin` 99 | 100 | #### Run test: 101 | ``python manage.py test`` 102 | 103 | #### To dump data: 104 | ``python manage.py dumpdata --format=json --indent 4 app_name > app_name/fixtures/app_name_initial_data.json`` 105 | 106 | ![Analytics](https://repobeats.axiom.co/api/embed/2ed66773eb8b3acef70bc808ddc6701253f2fc09.svg "Repobeats analytics image") 107 | 108 | Show your support by 🌟 the project!! 109 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /accounts/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/accounts/api/__init__.py -------------------------------------------------------------------------------- /accounts/api/custom_claims.py: -------------------------------------------------------------------------------- 1 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 2 | from rest_framework_simplejwt.views import TokenObtainPairView 3 | 4 | from .serializers import UserSerializer 5 | 6 | 7 | class MyTokenObtainPairSerializer(TokenObtainPairSerializer): 8 | @classmethod 9 | def get_token(cls, user): 10 | token = super(MyTokenObtainPairSerializer, cls).get_token(user) 11 | 12 | # Add custom claims 13 | token["user"] = UserSerializer(user, many=False).data 14 | 15 | return token 16 | 17 | 18 | class MyTokenObtainPairView(TokenObtainPairView): 19 | serializer_class = MyTokenObtainPairSerializer 20 | -------------------------------------------------------------------------------- /accounts/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from ..models import * 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | def __init__(self, *args, **kwargs): 8 | kwargs["partial"] = True 9 | super(UserSerializer, self).__init__(*args, **kwargs) 10 | 11 | class Meta: 12 | model = User 13 | # fields = "__all__" 14 | exclude = ("password", "user_permissions", "groups", "is_staff", "is_active", "is_superuser", "last_login") 15 | 16 | 17 | class UserCreateSerializer(serializers.ModelSerializer): 18 | password = serializers.CharField(write_only=True, required=True, style={"input_type": "password"}) 19 | password2 = serializers.CharField(style={"input_type": "password"}, write_only=True, label="Confirm password") 20 | 21 | class Meta: 22 | model = User 23 | fields = ["email", "password", "password2", "gender", "role"] 24 | extra_kwargs = {"password": {"write_only": True}} 25 | 26 | def validate(self, attrs): 27 | if attrs.get("password") != attrs.get("password2"): 28 | raise serializers.ValidationError({"password2": "Password fields didn't match."}) 29 | return attrs 30 | 31 | def validate_email(self, value): 32 | if User.objects.filter(email=value).exists(): 33 | raise serializers.ValidationError({"email": "Email addresses must be unique."}) 34 | return value 35 | 36 | def create(self, validated_data): 37 | email = validated_data["email"] 38 | password = validated_data["password"] 39 | gender = validated_data["gender"] 40 | role = validated_data["role"] 41 | user = User(email=email, gender=gender, role=role) 42 | user.set_password(password) 43 | user.save() 44 | return user 45 | 46 | 47 | class SocialSerializer(serializers.Serializer): 48 | """ 49 | Serializer which accepts an OAuth2 access token and provider. 50 | """ 51 | 52 | provider = serializers.CharField(max_length=255, required=True) 53 | access_token = serializers.CharField(max_length=4096, required=True, trim_whitespace=True) 54 | -------------------------------------------------------------------------------- /accounts/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework_simplejwt.views import TokenRefreshView 3 | 4 | from .custom_claims import MyTokenObtainPairView 5 | from .views import EditEmployeeProfileAPIView, SocialLoginAPIView, registration 6 | 7 | app_name = "accounts.api" 8 | 9 | urlpatterns = [ 10 | # path('login/', TokenObtainPairView.as_view()), 11 | path("register/", registration, name="register"), 12 | path("login/", MyTokenObtainPairView.as_view(), name="login"), 13 | path("token/refresh/", TokenRefreshView.as_view(), name="token-refresh"), 14 | path("employee/", include([path("profile/", EditEmployeeProfileAPIView.as_view(), name="employee-profile")])), 15 | path("oauth/login/", SocialLoginAPIView.as_view(), name="oauth-login"), 16 | ] 17 | -------------------------------------------------------------------------------- /accounts/api/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model, login 2 | from requests.exceptions import HTTPError 3 | from rest_framework import decorators, permissions, response, status 4 | from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView 5 | from rest_framework.permissions import IsAuthenticated 6 | from rest_framework.response import Response 7 | from rest_framework_simplejwt.tokens import RefreshToken 8 | from social_core.backends.oauth import BaseOAuth2 9 | from social_core.exceptions import AuthForbidden, AuthTokenError, MissingBackend 10 | from social_django.utils import load_backend, load_strategy 11 | 12 | from jobsapp.api.permissions import IsEmployee 13 | 14 | from .custom_claims import MyTokenObtainPairSerializer 15 | from .serializers import SocialSerializer, UserCreateSerializer, UserSerializer 16 | 17 | User = get_user_model() 18 | 19 | 20 | @decorators.api_view(["POST"]) 21 | @decorators.permission_classes([permissions.AllowAny]) 22 | def registration(request): 23 | serializer = UserCreateSerializer(data=request.data) 24 | if not serializer.is_valid(raise_exception=True): 25 | return response.Response(serializer.errors, status.HTTP_400_BAD_REQUEST) 26 | user = serializer.save() 27 | res = {"status": True, "message": "Successfully registered"} 28 | return response.Response(res, status.HTTP_201_CREATED) 29 | 30 | 31 | class EditEmployeeProfileAPIView(RetrieveUpdateAPIView): 32 | serializer_class = UserSerializer 33 | http_method_names = ["get", "put"] 34 | permission_classes = [IsAuthenticated, IsEmployee] 35 | 36 | def get_object(self): 37 | return self.request.user 38 | 39 | 40 | class SocialLoginAPIView(GenericAPIView): 41 | """Log in using facebook""" 42 | 43 | serializer_class = SocialSerializer 44 | permission_classes = [permissions.AllowAny] 45 | 46 | def post(self, request): 47 | """Authenticate user through the provider and access_token""" 48 | serializer = self.serializer_class(data=request.data) 49 | serializer.is_valid(raise_exception=True) 50 | provider = serializer.data.get("provider", None) 51 | strategy = load_strategy(request) 52 | 53 | try: 54 | backend = load_backend(strategy=strategy, name=provider, redirect_uri=None) 55 | 56 | except MissingBackend: 57 | return Response({"error": "Please provide a valid provider"}, status=status.HTTP_400_BAD_REQUEST) 58 | try: 59 | if isinstance(backend, BaseOAuth2): 60 | access_token = serializer.data.get("access_token") 61 | user = backend.do_auth(access_token) 62 | except HTTPError as error: 63 | return Response( 64 | {"error": {"access_token": "Invalid token", "details": str(error)}}, status=status.HTTP_400_BAD_REQUEST 65 | ) 66 | except AuthTokenError as error: 67 | return Response({"error": "Invalid credentials", "details": str(error)}, status=status.HTTP_400_BAD_REQUEST) 68 | 69 | try: 70 | authenticated_user = backend.do_auth(access_token, user=user) 71 | 72 | except HTTPError as error: 73 | return Response({"error": "invalid token", "details": str(error)}, status=status.HTTP_400_BAD_REQUEST) 74 | 75 | except AuthForbidden as error: 76 | return Response({"error": "invalid token", "details": str(error)}, status=status.HTTP_400_BAD_REQUEST) 77 | 78 | if authenticated_user and authenticated_user.is_active: 79 | # generate JWT token 80 | login(request, authenticated_user) 81 | # data = {"token": jwt_encode_handler(jwt_payload_handler(user))} 82 | # token = RefreshToken.for_user(user) 83 | token = MyTokenObtainPairSerializer.get_token(user) 84 | # customized response 85 | context = { 86 | "email": authenticated_user.email, 87 | "username": authenticated_user.username, 88 | "access": str(token.access_token), 89 | "refresh": str(token), 90 | } 91 | return Response(status=status.HTTP_200_OK, data=context) 92 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = "accounts" 6 | -------------------------------------------------------------------------------- /accounts/fixtures/accounts_initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "accounts.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$120000$lMGS0X5aMFo3$JgB1wTvD8sw2ewPR+eXj5ulmlKWGZjVws44nba0jQs0=", 7 | "last_login": "2019-04-10T13:48:14.243Z", 8 | "is_superuser": false, 9 | "first_name": "Google", 10 | "last_name": "Ltd.", 11 | "is_staff": false, 12 | "is_active": true, 13 | "date_joined": "2019-04-03T18:24:13.950Z", 14 | "role": "employer", 15 | "gender": "", 16 | "email": "contact@google.com", 17 | "groups": [], 18 | "user_permissions": [] 19 | } 20 | }, 21 | { 22 | "model": "accounts.user", 23 | "pk": 2, 24 | "fields": { 25 | "password": "pbkdf2_sha256$120000$YvEf7TARwKVv$ncxLBUeiMnfM2rgFvwP2CfWPwPq6SZw4QtzHXKwMEU0=", 26 | "last_login": "2019-12-10T06:08:46.324Z", 27 | "is_superuser": false, 28 | "first_name": "Audacity Ltd", 29 | "last_name": "Dhaka", 30 | "is_staff": false, 31 | "is_active": true, 32 | "date_joined": "2019-04-05T17:45:34.906Z", 33 | "role": "employer", 34 | "gender": "", 35 | "email": "audacity@gmail.com", 36 | "groups": [], 37 | "user_permissions": [] 38 | } 39 | }, 40 | { 41 | "model": "accounts.user", 42 | "pk": 3, 43 | "fields": { 44 | "password": "pbkdf2_sha256$180000$TAnAtvkQPoTu$eucaT9RTbOY772KxyQtQ+3/+dDPiSFqSi3lWU8k4Pv0=", 45 | "last_login": "2019-04-10T17:34:02.495Z", 46 | "is_superuser": false, 47 | "first_name": "Manjurul", 48 | "last_name": "Hoque", 49 | "is_staff": false, 50 | "is_active": true, 51 | "date_joined": "2019-04-06T17:13:25.494Z", 52 | "role": "employee", 53 | "gender": "male", 54 | "email": "rumi@gmail.com", 55 | "groups": [], 56 | "user_permissions": [] 57 | } 58 | }, 59 | { 60 | "model": "accounts.user", 61 | "pk": 4, 62 | "fields": { 63 | "password": "pbkdf2_sha256$120000$1oPPPSsl5L3z$Po2qjbQeM1YRiwXdGHnbgm4Ocf9XIeL0aDqdcdiu8Wc=", 64 | "last_login": "2019-04-10T17:07:34.287Z", 65 | "is_superuser": false, 66 | "first_name": "Sadman", 67 | "last_name": "Shamsuddin", 68 | "is_staff": false, 69 | "is_active": true, 70 | "date_joined": "2019-04-10T17:07:24.608Z", 71 | "role": "employee", 72 | "gender": "male", 73 | "email": "sadman@gmail.com", 74 | "groups": [], 75 | "user_permissions": [] 76 | } 77 | }, 78 | { 79 | "model": "accounts.user", 80 | "pk": 5, 81 | "fields": { 82 | "password": "pbkdf2_sha256$180000$oRp4hGWs2fcI$huokZAAKIhxJ5jP4FE8g3N9qV60P2Ql7e0vHfCZcDdc=", 83 | "last_login": null, 84 | "is_superuser": false, 85 | "first_name": "", 86 | "last_name": "", 87 | "is_staff": false, 88 | "is_active": true, 89 | "date_joined": "2020-07-30T18:26:13.419Z", 90 | "role": "employee", 91 | "gender": "Male", 92 | "email": "rumi2@gmail.com", 93 | "groups": [], 94 | "user_permissions": [] 95 | } 96 | }, 97 | { 98 | "model": "accounts.user", 99 | "pk": 6, 100 | "fields": { 101 | "password": "pbkdf2_sha256$180000$DHoxFqmkJCkx$swNxDaGno4fTU+if5ZMYqpT4AbZU3bBF3R0S6N9xMBA=", 102 | "last_login": null, 103 | "is_superuser": false, 104 | "first_name": "", 105 | "last_name": "", 106 | "is_staff": false, 107 | "is_active": true, 108 | "date_joined": "2020-07-30T18:27:12.479Z", 109 | "role": "employee", 110 | "gender": "Male", 111 | "email": "rumi22@gmail.com", 112 | "groups": [], 113 | "user_permissions": [] 114 | } 115 | } 116 | ] 117 | -------------------------------------------------------------------------------- /accounts/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/accounts/graphql/__init__.py -------------------------------------------------------------------------------- /accounts/graphql/constants.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext as _ 2 | 3 | 4 | class Messages: 5 | INVALID_PASSWORD = [{"message": _("Invalid password."), "code": "invalid_password"}] 6 | UNAUTHENTICATED = [{"message": _("Unauthenticated."), "code": "unauthenticated"}] 7 | INVALID_TOKEN = [{"message": _("Invalid token."), "code": "invalid_token"}] 8 | EXPIRED_TOKEN = [{"message": _("Expired token."), "code": "expired_token"}] 9 | ALREADY_VERIFIED = [{"message": _("Account already verified."), "code": "already_verified"}] 10 | EMAIL_FAIL = [{"message": _("Failed to send email."), "code": "email_fail"}] 11 | INVALID_CREDENTIALS = [{"message": _("Please, enter valid credentials."), "code": "invalid_credentials"}] 12 | NOT_VERIFIED = [{"message": _("Please verify your account."), "code": "not_verified"}] 13 | NOT_VERIFIED_PASSWORD_RESET = [ 14 | {"message": _("Verify your account. A new verification email was sent."), "code": "not_verified"} 15 | ] 16 | EMAIL_IN_USE = [{"message": _("A user with that email already exists."), "code": "unique"}] 17 | USERNAME_NOT_FOUND = [{"message": _("No user with given username found."), "code": "invalid_username"}] 18 | DATABASE_ERROR = [{"message": _("Internal server error."), "code": "internal_server_error"}] 19 | PERMISSION_DENIED_ERROR = [{"message": None, "code": "permission_denied"}] 20 | -------------------------------------------------------------------------------- /accounts/graphql/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext as _ 2 | 3 | from jobsapp.graphql.exceptions import GraphQLError 4 | 5 | 6 | class UserAlreadyVerified(GraphQLError): 7 | default_message = _("User already verified.") 8 | 9 | 10 | class InvalidCredentials(GraphQLError): 11 | default_message = _("Invalid credentials.") 12 | 13 | 14 | class UserNotVerified(GraphQLError): 15 | default_message = _("User is not verified.") 16 | 17 | 18 | class EmailAlreadyInUse(GraphQLError): 19 | default_message = _("This email is already in use.") 20 | -------------------------------------------------------------------------------- /accounts/graphql/graphql_mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import transaction 3 | 4 | from accounts.forms import EmployeeRegistrationForm, EmployerRegistrationForm 5 | from jobsapp.graphql.graphql_base import Output 6 | 7 | UserModel = get_user_model() 8 | 9 | 10 | class EmployeeRegisterMixin(Output): 11 | form = EmployeeRegistrationForm 12 | 13 | @classmethod 14 | def resolve_mutation(cls, root, info, **kwargs): 15 | with transaction.atomic(): 16 | f = cls.form(kwargs) 17 | 18 | if f.is_valid(): 19 | user = f.save() 20 | user = f.save(commit=False) 21 | password = f.cleaned_data.get("password1") 22 | user.set_password(password) 23 | user.save() 24 | return cls(success=True) 25 | else: 26 | return cls(success=False, errors=f.errors.get_json_data()) 27 | 28 | 29 | class EmployerRegisterMixin(Output): 30 | form = EmployerRegistrationForm 31 | 32 | @classmethod 33 | def resolve_mutation(cls, root, info, **kwargs): 34 | with transaction.atomic(): 35 | first_name = kwargs.pop("company_name") 36 | last_name = kwargs.pop("company_address") 37 | kwargs.update(first_name=first_name, last_name=last_name) 38 | f = cls.form(kwargs) 39 | 40 | if f.is_valid(): 41 | user = f.save() 42 | user = f.save(commit=False) 43 | password = f.cleaned_data.get("password1") 44 | user.set_password(password) 45 | user.save() 46 | return cls(success=True) 47 | else: 48 | return cls(success=False, errors=f.errors.get_json_data()) 49 | -------------------------------------------------------------------------------- /accounts/graphql/mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import graphql_jwt 3 | from . import sub_mutations as user_mutations 4 | 5 | 6 | class AuthMutation(graphene.ObjectType): 7 | login = graphql_jwt.ObtainJSONWebToken.Field() 8 | verify_token = graphql_jwt.Verify.Field() 9 | refresh_token = graphql_jwt.Refresh.Field() 10 | employee_register = user_mutations.EmployeeRegister.Field() 11 | employer_register = user_mutations.EmployerRegister.Field() 12 | -------------------------------------------------------------------------------- /accounts/graphql/sub_mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import graphql_jwt 3 | 4 | from accounts.graphql.graphql_mixins import EmployeeRegisterMixin, EmployerRegisterMixin 5 | from jobsapp.graphql.graphql_mixins import DynamicArgsMixin, MutationMixin 6 | 7 | 8 | class EmployeeRegister(MutationMixin, DynamicArgsMixin, EmployeeRegisterMixin, graphene.Mutation): 9 | __doc__ = EmployeeRegisterMixin.__doc__ 10 | _required_args = { 11 | "first_name": "String", 12 | "last_name": "String", 13 | "email": "String", 14 | "password1": "String", 15 | "password2": "String", 16 | "gender": "String", 17 | } 18 | 19 | 20 | class EmployerRegister(MutationMixin, DynamicArgsMixin, EmployerRegisterMixin, graphene.Mutation): 21 | __doc__ = EmployerRegisterMixin.__doc__ 22 | _required_args = { 23 | "company_name": "String", 24 | "company_address": "String", 25 | "email": "String", 26 | "password1": "String", 27 | "password2": "String", 28 | } 29 | -------------------------------------------------------------------------------- /accounts/managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import BaseUserManager 2 | 3 | 4 | class UserManager(BaseUserManager): 5 | """Define a model manager for User model with no username field.""" 6 | 7 | use_in_migrations = True 8 | 9 | def _create_user(self, email, password, **extra_fields): 10 | """Create and save a User with the given email and password.""" 11 | if not email: 12 | raise ValueError("The given email must be set") 13 | email = self.normalize_email(email) 14 | user = self.model(email=email, **extra_fields) 15 | user.set_password(password) 16 | user.save(using=self._db) 17 | return user 18 | 19 | def create_user(self, email, password=None, **extra_fields): 20 | """Create and save a regular User with the given email and password.""" 21 | extra_fields.setdefault("is_staff", False) 22 | extra_fields.setdefault("is_superuser", False) 23 | return self._create_user(email, password, **extra_fields) 24 | 25 | def create_superuser(self, email, password, **extra_fields): 26 | """Create and save a SuperUser with the given email and password.""" 27 | extra_fields.setdefault("is_staff", True) 28 | extra_fields.setdefault("is_superuser", True) 29 | 30 | if extra_fields.get("is_staff") is not True: 31 | raise ValueError("Superuser must have is_staff=True.") 32 | if extra_fields.get("is_superuser") is not True: 33 | raise ValueError("Superuser must have is_superuser=True.") 34 | 35 | return self._create_user(email, password, **extra_fields) 36 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-26 10:27 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0009_alter_user_last_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 29 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 30 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 31 | ('role', models.CharField(error_messages={'required': 'Role must be provided'}, max_length=12)), 32 | ('gender', models.CharField(blank=True, default='', max_length=10, null=True)), 33 | ('email', models.EmailField(error_messages={'unique': 'A user with that email already exists.'}, max_length=254, unique=True)), 34 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 35 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /accounts/migrations/0002_auto_20190326_1754.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-26 11:54 2 | 3 | import accounts.managers 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('accounts', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelManagers( 15 | name='user', 16 | managers=[ 17 | ('objects', accounts.managers.UserManager()), 18 | ], 19 | ), 20 | migrations.RemoveField( 21 | model_name='user', 22 | name='username', 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /accounts/migrations/0003_alter_user_first_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-08 17:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0002_auto_20190326_1754'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='first_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /accounts/migrations/0004_alter_user_gender.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2025-03-30 11:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0003_alter_user_first_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='gender', 16 | field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], default='', max_length=10, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | 4 | from accounts.managers import UserManager 5 | 6 | GENDER_CHOICES = [ 7 | ("male", "Male"), 8 | ("female", "Female"), 9 | ] 10 | 11 | 12 | class User(AbstractUser): 13 | username = None 14 | role = models.CharField(max_length=12, error_messages={"required": "Role must be provided"}) 15 | gender = models.CharField(max_length=10, blank=True, null=True, default="", choices=GENDER_CHOICES) 16 | email = models.EmailField( 17 | unique=True, blank=False, error_messages={"unique": "A user with that email already exists."} 18 | ) 19 | 20 | USERNAME_FIELD = "email" 21 | REQUIRED_FIELDS = [] 22 | 23 | def __unicode__(self): 24 | return self.email 25 | 26 | objects = UserManager() 27 | -------------------------------------------------------------------------------- /accounts/pipeline.py: -------------------------------------------------------------------------------- 1 | def update_user(backend, user, response, *args, **kwargs): 2 | user.role = "employee" 3 | user.save() 4 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.urls import path 4 | 5 | from jobsapp.views import EditProfileView, EmployerProfileEditView 6 | from .views import * 7 | 8 | app_name = "accounts" 9 | 10 | urlpatterns = [ 11 | path("employee/register/", RegisterEmployeeView.as_view(), name="employee-register"), 12 | path("employer/register/", RegisterEmployerView.as_view(), name="employer-register"), 13 | path("employee/profile/update/", EditProfileView.as_view(), name="employee-profile-update"), 14 | path("employer/profile/update/", EmployerProfileEditView.as_view(), name="employer-profile-update"), 15 | path("logout/", LogoutView.as_view(), name="logout"), 16 | path("login/", LoginView.as_view(), name="login"), 17 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 18 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import auth, messages 2 | from django.http import HttpResponseRedirect 3 | from django.shortcuts import redirect, render 4 | from django.views.generic import CreateView, FormView, RedirectView 5 | 6 | from accounts.forms import * 7 | from accounts.models import User 8 | 9 | 10 | class RegisterEmployeeView(CreateView): 11 | model = User 12 | form_class = EmployeeRegistrationForm 13 | template_name = "accounts/employee/register.html" 14 | success_url = "/" 15 | 16 | extra_context = {"title": "Register"} 17 | 18 | def dispatch(self, request, *args, **kwargs): 19 | if request.user.is_authenticated: 20 | return HttpResponseRedirect(self.success_url) 21 | return super().dispatch(self.request, *args, **kwargs) 22 | 23 | def post(self, request, *args, **kwargs): 24 | 25 | form = self.form_class(data=request.POST) 26 | 27 | if form.is_valid(): 28 | user = form.save(commit=False) 29 | password = form.cleaned_data.get("password1") 30 | user.set_password(password) 31 | user.save() 32 | return redirect("accounts:login") 33 | else: 34 | return render(request, "accounts/employee/register.html", {"form": form}) 35 | 36 | 37 | class RegisterEmployerView(CreateView): 38 | model = User 39 | form_class = EmployerRegistrationForm 40 | template_name = "accounts/employer/register.html" 41 | success_url = "/" 42 | 43 | extra_context = {"title": "Register"} 44 | 45 | def dispatch(self, request, *args, **kwargs): 46 | if request.user.is_authenticated: 47 | return HttpResponseRedirect(self.success_url) 48 | return super().dispatch(self.request, *args, **kwargs) 49 | 50 | def post(self, request, *args, **kwargs): 51 | 52 | form = self.form_class(data=request.POST) 53 | 54 | if form.is_valid(): 55 | user = form.save(commit=False) 56 | password = form.cleaned_data.get("password1") 57 | user.set_password(password) 58 | user.save() 59 | return redirect("accounts:login") 60 | else: 61 | return render(request, "accounts/employer/register.html", {"form": form}) 62 | 63 | 64 | class LoginView(FormView): 65 | """ 66 | Provides the ability to login as a user with an email and password 67 | """ 68 | 69 | success_url = "/" 70 | form_class = UserLoginForm 71 | template_name = "accounts/login.html" 72 | 73 | extra_context = {"title": "Login"} 74 | 75 | def dispatch(self, request, *args, **kwargs): 76 | if self.request.user.is_authenticated: 77 | return HttpResponseRedirect(self.get_success_url()) 78 | return super().dispatch(self.request, *args, **kwargs) 79 | 80 | def get_success_url(self): 81 | if "next" in self.request.GET and self.request.GET["next"] != "": 82 | return self.request.GET["next"] 83 | else: 84 | return self.success_url 85 | 86 | def get_form_class(self): 87 | return self.form_class 88 | 89 | def form_valid(self, form): 90 | auth.login(self.request, form.get_user()) 91 | return HttpResponseRedirect(self.get_success_url()) 92 | 93 | def form_invalid(self, form): 94 | """If the form is invalid, render the invalid form.""" 95 | return self.render_to_response(self.get_context_data(form=form)) 96 | 97 | 98 | class LogoutView(RedirectView): 99 | """ 100 | Provides users the ability to logout 101 | """ 102 | 103 | url = "/login" 104 | 105 | def get(self, request, *args, **kwargs): 106 | auth.logout(request) 107 | messages.success(request, "You are now logged out") 108 | return super(LogoutView, self).get(request, *args, **kwargs) 109 | -------------------------------------------------------------------------------- /categories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/categories/__init__.py -------------------------------------------------------------------------------- /categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "categories" 7 | -------------------------------------------------------------------------------- /categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.2 on 2025-03-30 11:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255, unique=True)), 19 | ('slug', models.SlugField(max_length=255, unique=True)), 20 | ('description', models.TextField()), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /categories/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/categories/migrations/__init__.py -------------------------------------------------------------------------------- /categories/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.text import slugify 3 | 4 | 5 | class Category(models.Model): 6 | name = models.CharField(max_length=255, unique=True) 7 | slug = models.SlugField(max_length=255, unique=True) 8 | description = models.TextField() 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | class Meta: 14 | ordering = ["id"] 15 | verbose_name = "Category" 16 | verbose_name_plural = "Categories" 17 | 18 | def save(self, *args, **kwargs): 19 | if not self.slug: 20 | self.slug = slugify(self.name) 21 | super().save(*args, **kwargs) 22 | -------------------------------------------------------------------------------- /categories/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from categories.models import Category 4 | 5 | 6 | class CategorySerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Category 9 | fields = "__all__" 10 | -------------------------------------------------------------------------------- /categories/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import * 4 | 5 | app_name = "categories" 6 | 7 | urlpatterns = [ 8 | path("categories/", CategoryListAPIView.as_view(), name="categories-list"), 9 | ] 10 | -------------------------------------------------------------------------------- /categories/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListAPIView 2 | from rest_framework.permissions import AllowAny 3 | 4 | from categories.models import Category 5 | from categories.serializers import CategorySerializer 6 | 7 | 8 | class CategoryListAPIView(ListAPIView): 9 | queryset = Category.objects.all() 10 | serializer_class = CategorySerializer 11 | permission_classes = [AllowAny] 12 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/db.sqlite3 -------------------------------------------------------------------------------- /deployment/jenkins_deploy_prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ssh root@103.108.140.130 <", load_builder, name="resume-cv.load.builder"), 45 | path("templates/update-builder/", csrf_exempt(update_builder), name="resume-cv.update.builder"), 46 | path( 47 | "api/", 48 | include( 49 | [ 50 | path("swagger", schema_view.with_ui("swagger", cache_timeout=0)), 51 | path("", include("accounts.api.urls")), 52 | path("", include("jobsapp.api.urls")), 53 | path("", include("tags.api.urls")), 54 | path("", include("categories.urls")), 55 | # path('auth/oauth/', include('rest_framework_social_oauth2.urls')) 56 | ] 57 | ), 58 | ), 59 | path("social-auth/", include("social_django.urls", namespace="social")), 60 | # url(r"^(?P.*/)$", flatpages_views.flatpage), 61 | path("sitemap.xml/", sitemap, {"sitemaps": dict(Sitemaps())}, name="django.contrib.sitemaps.views.sitemap"), 62 | path("graphql/", csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True))), 63 | ] 64 | 65 | if settings.ENABLE_PROMETHEUS: 66 | urlpatterns.append(path("", include("django_prometheus.urls"))) 67 | 68 | if bool(settings.DEBUG): 69 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 70 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 71 | -------------------------------------------------------------------------------- /jobs/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | from whitenoise import WhiteNoise 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jobs.settings") 7 | 8 | application = get_wsgi_application() 9 | # application = WhiteNoise(application) 10 | -------------------------------------------------------------------------------- /jobsapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/jobsapp/__init__.py -------------------------------------------------------------------------------- /jobsapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.flatpages.admin import FlatPageAdmin 3 | from django.contrib.flatpages.models import FlatPage 4 | 5 | # Register your models here. 6 | from jobsapp.models import Job 7 | 8 | 9 | @admin.register(Job) 10 | class JobAdmin(admin.ModelAdmin): 11 | list_display = [ 12 | "title", 13 | "salary", 14 | "location", 15 | "type", 16 | "category", 17 | "company_name", 18 | "last_date", 19 | "created_at", 20 | "filled", 21 | "user", 22 | ] 23 | list_filter = ["salary", "last_date", "created_at", "user"] 24 | date_hierarchy = "created_at" 25 | -------------------------------------------------------------------------------- /jobsapp/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/jobsapp/api/__init__.py -------------------------------------------------------------------------------- /jobsapp/api/custom_exception.py: -------------------------------------------------------------------------------- 1 | from rest_framework.exceptions import ValidationError 2 | from rest_framework.views import exception_handler 3 | 4 | 5 | def custom_exception_handler(exc, context): 6 | """ 7 | Override drf default validation error message response and sends as a list 8 | :param exc: 9 | :param context: 10 | :return: response obj 11 | """ 12 | 13 | response = exception_handler(exc, context) 14 | if response is not None: 15 | data = response.data 16 | response.data = {} 17 | errors = [] 18 | for field, value in data.items(): 19 | if type(value) is list: 20 | my_dict = {field: value[0]} 21 | # errors.append("{}: {}".format(field, value[0])) 22 | errors.append(my_dict) 23 | else: 24 | errors.append({field: value}) 25 | # errors.append("{}: {}".format(field, value)) 26 | 27 | response.data["errors"] = errors 28 | response.data["status"] = False 29 | 30 | if type(exc) is ValidationError: 31 | response.data["message"] = "" 32 | for field, value in data.items(): 33 | response.data["message"] += value[0] + " " 34 | else: 35 | response.data["message"] = str(exc) 36 | 37 | return response 38 | 39 | 40 | """ 41 | # to work with this custom error handler, save this file in your project, 42 | #add this in settings.py 43 | REST_FRAMEWORK = { 44 | 'EXCEPTION_HANDLER': 'core.api.custom_exception.custom_exception_handler', #location for your file 45 | } 46 | # and add raise_exception=True in is_valid() method 47 | if serializer.is_valid(raise_exception=True): 48 | """ 49 | -------------------------------------------------------------------------------- /jobsapp/api/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | 3 | from jobsapp.models import Job 4 | 5 | 6 | class IsEmployer(BasePermission): 7 | def has_permission(self, request, view): 8 | return request.user and request.user.role == "employer" 9 | 10 | 11 | class IsEmployee(BasePermission): 12 | def has_permission(self, request, view): 13 | return request.user and request.user.role == "employee" 14 | 15 | 16 | class IsJobCreator(BasePermission): 17 | # message = 'Permission denied' 18 | 19 | def has_permission(self, request, view): 20 | job_id = view.kwargs.get("job_id") 21 | if job_id: 22 | if Job.objects.filter(id=job_id, user=request.user).exists(): 23 | return True 24 | else: 25 | return False 26 | return False 27 | -------------------------------------------------------------------------------- /jobsapp/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from accounts.api.serializers import UserSerializer 4 | from tags.api.serializers import TagSerializer 5 | 6 | from ..models import * 7 | 8 | 9 | class JobSerializer(serializers.ModelSerializer): 10 | user = UserSerializer(read_only=True) 11 | job_tags = serializers.SerializerMethodField() 12 | 13 | class Meta: 14 | model = Job 15 | fields = "__all__" 16 | 17 | def get_job_tags(self, obj): 18 | if obj.tags: 19 | return TagSerializer(obj.tags.all(), many=True).data 20 | else: 21 | return None 22 | 23 | 24 | class DashboardJobSerializer(serializers.ModelSerializer): 25 | user = UserSerializer(read_only=True) 26 | job_tags = serializers.SerializerMethodField() 27 | total_candidates = serializers.SerializerMethodField() 28 | 29 | class Meta: 30 | model = Job 31 | fields = "__all__" 32 | 33 | def get_job_tags(self, obj): 34 | if obj.tags: 35 | return TagSerializer(obj.tags.all(), many=True).data 36 | else: 37 | return None 38 | 39 | def get_total_candidates(self, obj): 40 | return obj.applicants.count() 41 | 42 | 43 | class NewJobSerializer(serializers.ModelSerializer): 44 | user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), default=serializers.CurrentUserDefault()) 45 | 46 | class Meta: 47 | model = Job 48 | fields = "__all__" 49 | 50 | 51 | class ApplyJobSerializer(serializers.ModelSerializer): 52 | class Meta: 53 | model = Applicant 54 | fields = ("job",) 55 | 56 | def validate(self, attrs): 57 | if Applicant.objects.filter(user=self.context.get("request", None).user, job=attrs.get("job")).exists(): 58 | raise serializers.ValidationError("You have already applied to this job") 59 | return attrs 60 | 61 | 62 | class ApplicantSerializer(serializers.ModelSerializer): 63 | applied_user = serializers.SerializerMethodField() 64 | job = serializers.SerializerMethodField() 65 | status = serializers.SerializerMethodField() 66 | 67 | class Meta: 68 | model = Applicant 69 | fields = ( 70 | "id", 71 | "job_id", 72 | "applied_user", 73 | "job", 74 | "status", 75 | "created_at", 76 | "comment", 77 | ) 78 | 79 | def get_status(self, obj): 80 | return obj.get_status 81 | 82 | def get_job(self, obj): 83 | return JobSerializer(obj.job).data 84 | 85 | def get_applied_user(self, obj): 86 | return UserSerializer(obj.user).data 87 | 88 | 89 | class AppliedJobSerializer(serializers.ModelSerializer): 90 | user = UserSerializer(read_only=True) 91 | applicant = serializers.SerializerMethodField("_applicant") 92 | 93 | class Meta: 94 | model = Job 95 | fields = "__all__" 96 | 97 | def _applicant(self, obj): 98 | user = self.context.get("request", None).user 99 | return ApplicantSerializer(Applicant.objects.get(user=user, job=obj)).data 100 | -------------------------------------------------------------------------------- /jobsapp/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from drf_yasg import openapi 3 | from drf_yasg.views import get_schema_view 4 | from rest_framework import permissions 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from .views import ( 8 | ApplicantsListAPIView, 9 | ApplicantsPerJobListAPIView, 10 | AppliedJobsAPIView, 11 | ApplyJobApiView, 12 | DashboardAPIView, 13 | JobCreateAPIView, 14 | JobViewSet, 15 | SearchApiView, 16 | UpdateApplicantStatusAPIView, 17 | already_applied_api_view, 18 | ) 19 | 20 | app_name = "jobs-api" 21 | 22 | router = DefaultRouter() 23 | router.register("jobs", JobViewSet) 24 | 25 | schema_view = get_schema_view( 26 | openapi.Info( 27 | title="Django Job Portal API", 28 | default_version="v1", 29 | description="Django job portal API description", 30 | terms_of_service="https://www.google.com/policies/terms/", 31 | contact=openapi.Contact(email="manzurulhoquerumi@gmail.com"), 32 | license=openapi.License(name="BSD License"), 33 | ), 34 | public=True, 35 | permission_classes=(permissions.AllowAny,), 36 | ) 37 | 38 | urlpatterns = [ 39 | path("docs/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), 40 | path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), 41 | path("search/", SearchApiView.as_view(), name="search"), 42 | path("apply-job//", ApplyJobApiView.as_view(), name="apply-job"), 43 | path("applied-jobs/", AppliedJobsAPIView.as_view(), name="applied-jobs"), 44 | path("applied-for-job//", already_applied_api_view, name="applied-for-job"), 45 | path( 46 | "employer/", 47 | include( 48 | [ 49 | path("dashboard/", DashboardAPIView.as_view(), name="employer-dashboard"), 50 | path("jobs/create/", JobCreateAPIView.as_view(), name="employer-job-create"), 51 | path("applicants/", ApplicantsListAPIView.as_view(), name="employer-applicants-list"), 52 | path("applicants//", ApplicantsPerJobListAPIView.as_view(), name="employer-applicants-per-job-list"), 53 | path("applicants///update/", UpdateApplicantStatusAPIView.as_view(), name="employer-update-applicant-status"), 54 | ] 55 | ), 56 | ), 57 | ] 58 | 59 | urlpatterns += router.urls 60 | -------------------------------------------------------------------------------- /jobsapp/api/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | from .employee import * 3 | from .employer import * 4 | -------------------------------------------------------------------------------- /jobsapp/api/views/common.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.generics import ListAPIView 3 | from rest_framework.permissions import AllowAny 4 | 5 | from jobsapp.api.serializers import JobSerializer 6 | 7 | 8 | class JobViewSet(viewsets.ReadOnlyModelViewSet): 9 | serializer_class = JobSerializer 10 | queryset = serializer_class.Meta.model.objects.unfilled() 11 | permission_classes = [AllowAny] 12 | 13 | 14 | class SearchApiView(ListAPIView): 15 | serializer_class = JobSerializer 16 | permission_classes = [AllowAny] 17 | 18 | def get_queryset(self): 19 | if "location" in self.request.GET and "position" in self.request.GET: 20 | return self.serializer_class.Meta.model.objects.unfilled( 21 | location__contains=self.request.GET["location"], 22 | title__contains=self.request.GET["position"], 23 | ) 24 | else: 25 | return self.serializer_class.Meta.model.objects.unfilled() 26 | -------------------------------------------------------------------------------- /jobsapp/api/views/employee.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.decorators import api_view, permission_classes 3 | from rest_framework.generics import CreateAPIView, ListAPIView 4 | from rest_framework.permissions import IsAuthenticated 5 | from rest_framework.response import Response 6 | 7 | from jobsapp.api.permissions import IsEmployee 8 | from jobsapp.api.serializers import AppliedJobSerializer, ApplyJobSerializer 9 | from jobsapp.models import Applicant, Job 10 | 11 | 12 | class ApplyJobApiView(CreateAPIView): 13 | serializer_class = ApplyJobSerializer 14 | http_method_names = ["post"] 15 | permission_classes = [IsAuthenticated, IsEmployee] 16 | 17 | def perform_create(self, serializer): 18 | serializer.save(user=self.request.user) 19 | 20 | def create(self, request, *args, **kwargs): 21 | serializer = self.get_serializer(data=request.data) 22 | serializer.is_valid(raise_exception=True) 23 | self.perform_create(serializer) 24 | headers = self.get_success_headers(serializer.data) 25 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 26 | 27 | 28 | class AppliedJobsAPIView(ListAPIView): 29 | serializer_class = AppliedJobSerializer 30 | permission_classes = [IsAuthenticated, IsEmployee] 31 | 32 | def get_queryset(self): 33 | applied_jobs_id = list(Applicant.objects.filter(user=self.request.user).values_list("job_id", flat=True)) 34 | return Job.objects.filter(id__in=applied_jobs_id) 35 | 36 | 37 | @api_view(["GET"]) 38 | @permission_classes([IsAuthenticated, IsEmployee]) 39 | def already_applied_api_view(request, job_id): 40 | is_applied = Applicant.objects.filter(user=request.user, job_id=job_id).exists() 41 | content = {"is_applied": is_applied} 42 | return Response(content) 43 | -------------------------------------------------------------------------------- /jobsapp/api/views/employer.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from rest_framework.generics import CreateAPIView, ListAPIView 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.views import APIView 5 | from rest_framework import status 6 | 7 | from jobsapp.api.permissions import IsEmployer, IsJobCreator 8 | from jobsapp.api.serializers import ( 9 | ApplicantSerializer, 10 | DashboardJobSerializer, 11 | NewJobSerializer, 12 | ) 13 | from jobsapp.models import Applicant 14 | 15 | 16 | class DashboardAPIView(ListAPIView): 17 | serializer_class = DashboardJobSerializer 18 | permission_classes = [IsAuthenticated, IsEmployer] 19 | 20 | def get_queryset(self): 21 | return self.serializer_class.Meta.model.objects.filter( 22 | user_id=self.request.user.id 23 | ) 24 | 25 | 26 | class JobCreateAPIView(CreateAPIView): 27 | serializer_class = NewJobSerializer 28 | permission_classes = [IsAuthenticated, IsEmployer] 29 | 30 | 31 | class ApplicantsListAPIView(ListAPIView): 32 | serializer_class = ApplicantSerializer 33 | permission_classes = [IsAuthenticated, IsEmployer] 34 | 35 | def get_queryset(self): 36 | user = self.request.user 37 | return Applicant.objects.filter(job__user_id=user.id) 38 | 39 | 40 | class ApplicantsPerJobListAPIView(ListAPIView): 41 | serializer_class = ApplicantSerializer 42 | permission_classes = [IsAuthenticated, IsEmployer, IsJobCreator] 43 | 44 | def get_queryset(self): 45 | return Applicant.objects.filter(job_id=self.kwargs["job_id"]).order_by("id") 46 | 47 | 48 | class UpdateApplicantStatusAPIView(APIView): 49 | permission_classes = [IsAuthenticated, IsEmployer] 50 | 51 | def post(self, request, *args, **kwargs): 52 | applicant_id = kwargs.get("applicant_id") 53 | status_code = kwargs.get("status_code") 54 | try: 55 | applicant = Applicant.objects.select_related("job__user").get( 56 | id=applicant_id 57 | ) 58 | except Applicant.DoesNotExist: 59 | data = {"message": "Applicant not found"} 60 | return JsonResponse(data, status=status.HTTP_404_NOT_FOUND) 61 | 62 | if applicant.job.user != request.user: 63 | data = {"errors": "You are not authorized"} 64 | return JsonResponse(data, status=status.HTTP_403_FORBIDDEN) 65 | if status_code not in [1, 2]: 66 | status_code = 3 67 | 68 | applicant.status = status_code 69 | applicant.comment = request.data.get("comment", "") 70 | applicant.save() 71 | data = {"message": "Applicant status updated"} 72 | return JsonResponse(data, status=status.HTTP_200_OK) 73 | -------------------------------------------------------------------------------- /jobsapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JobsappConfig(AppConfig): 5 | name = "jobsapp" 6 | -------------------------------------------------------------------------------- /jobsapp/decorators.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import PermissionDenied 2 | 3 | 4 | def user_is_employer(function): 5 | def wrap(request, *args, **kwargs): 6 | user = request.user 7 | if user.role == "employer": 8 | return function(request, *args, **kwargs) 9 | else: 10 | raise PermissionDenied 11 | 12 | return wrap 13 | 14 | 15 | def user_is_employee(function): 16 | def wrap(request, *args, **kwargs): 17 | user = request.user 18 | if user.role == "employee": 19 | return function(request, *args, **kwargs) 20 | else: 21 | raise PermissionDenied 22 | 23 | return wrap 24 | -------------------------------------------------------------------------------- /jobsapp/documents.py: -------------------------------------------------------------------------------- 1 | from django_elasticsearch_dsl import Document 2 | from django_elasticsearch_dsl.registries import registry 3 | 4 | from .models import Job 5 | 6 | 7 | # TODO disabled for testing purpose 8 | # @registry.register_document 9 | class JobDocument(Document): 10 | class Index: 11 | # Name of the Elasticsearch index 12 | name = "jobs" 13 | # See Elasticsearch Indices API reference for available settings 14 | settings = {"number_of_shards": 2} 15 | # settings = {'number_of_shards': 1, 16 | # 'number_of_replicas': 0} 17 | 18 | class Django: 19 | model = Job # The model associated with this Document 20 | 21 | # The fields of the model you want to be indexed in Elasticsearch 22 | fields = ["title", "location"] 23 | 24 | # Ignore auto updating of Elasticsearch when a model is saved 25 | # or deleted: 26 | # ignore_signals = True 27 | 28 | # Don't perform an index refresh after every update (overrides global setting): 29 | # auto_refresh = False 30 | 31 | # Paginate the django queryset used to populate the index with the specified size 32 | # (by default it uses the database driver's default setting) 33 | # queryset_pagination = 5000 34 | 35 | # def get_queryset(self): 36 | # """Not mandatory but to improve performance we can select related in one sql request""" 37 | # return super(JobDocument, self).get_queryset().select_related('study') 38 | -------------------------------------------------------------------------------- /jobsapp/forms.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django import forms 4 | from django.core.exceptions import ValidationError 5 | 6 | from jobsapp.models import Applicant, Job 7 | 8 | 9 | class CreateJobForm(forms.ModelForm): 10 | class Meta: 11 | model = Job 12 | exclude = ("user", "created_at") 13 | labels = { 14 | "last_date": "Last Date", 15 | "company_name": "Company Name", 16 | "company_description": "Company Description", 17 | } 18 | 19 | def is_valid(self): 20 | valid = super(CreateJobForm, self).is_valid() 21 | 22 | # if already valid, then return True 23 | if valid: 24 | return valid 25 | return valid 26 | 27 | def clean_last_date(self): 28 | date = self.cleaned_data["last_date"] 29 | if date.date() < datetime.now().date(): 30 | raise ValidationError("Last date can't be before from today") 31 | return date 32 | 33 | def clean_tags(self): 34 | tags = self.cleaned_data["tags"] 35 | if len(tags) > 6: 36 | raise forms.ValidationError("You can't add more than 6 tags") 37 | return tags 38 | 39 | def save(self, commit=True): 40 | job = super(CreateJobForm, self).save(commit=False) 41 | if commit: 42 | job.save() 43 | for tag in self.cleaned_data["tags"]: 44 | job.tags.add(tag) 45 | return job 46 | 47 | 48 | class ApplyJobForm(forms.ModelForm): 49 | class Meta: 50 | model = Applicant 51 | fields = ("job",) 52 | -------------------------------------------------------------------------------- /jobsapp/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/jobsapp/graphql/__init__.py -------------------------------------------------------------------------------- /jobsapp/graphql/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import gettext as _ 2 | 3 | 4 | class NotAllowedException(Exception): 5 | def __init__(self, message): 6 | self.message = message 7 | 8 | 9 | class GraphQLError(Exception): 10 | default_message = None 11 | 12 | def __init__(self, message=None): 13 | if message is None: 14 | message = self.default_message 15 | 16 | super().__init__(message) 17 | 18 | 19 | class WrongUsage(GraphQLError): 20 | """ 21 | Internal exception 22 | """ 23 | 24 | default_message = _("Wrong usage, check your code!.") 25 | 26 | 27 | class PermissionDeniedError(GraphQLError): 28 | default_message = (_("You don't have permission to access this resource."),) 29 | -------------------------------------------------------------------------------- /jobsapp/graphql/graphql_base.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from jobsapp.graphql.types import ExpectedErrorType 4 | 5 | 6 | class Output: 7 | """ 8 | A class to all public classes extend to 9 | patronize the output 10 | """ 11 | 12 | success = graphene.Boolean(default_value=True) 13 | errors = graphene.Field(ExpectedErrorType) 14 | -------------------------------------------------------------------------------- /jobsapp/graphql/input_types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class TagInput(graphene.InputObjectType): 5 | pk = graphene.Int() 6 | -------------------------------------------------------------------------------- /jobsapp/graphql/mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from . import sub_mutations as job_mutations 4 | 5 | 6 | class JobMutation(graphene.ObjectType): 7 | create_job = job_mutations.CreateNewJob.Field() 8 | update_job = job_mutations.UpdateJob.Field() 9 | -------------------------------------------------------------------------------- /jobsapp/graphql/permissions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.utils.translation import gettext_lazy as _ 3 | 4 | from utils.namedtuples import Checking 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class BasePermission: 10 | message = None 11 | 12 | def has_permission(self, request, **kwargs): 13 | return True 14 | 15 | def has_object_permission(self, request, obj, **kwargs): 16 | return True 17 | 18 | def set_message_and_get_checking_status(self, checking: Checking, message=None) -> bool: 19 | self.message = message or checking.message 20 | return checking.passed 21 | 22 | 23 | class AllowAny(BasePermission): 24 | def has_permission(self, request, **kwargs): 25 | return True 26 | 27 | 28 | class IsAuthenticated(BasePermission): 29 | message = _("Unauthenticated request.") 30 | 31 | def has_permission(self, request, **kwargs): 32 | return bool(request.user and request.user.is_authenticated) 33 | 34 | 35 | class IsEmployer(BasePermission): 36 | message = _("Your user account must be an employer") 37 | 38 | def has_permission(self, request, **kwargs): 39 | return bool(request.user and request.user.role == "employer") 40 | 41 | 42 | class IsEmployee(BasePermission): 43 | message = _("Your user account must be an employee") 44 | 45 | def has_permission(self, request, **kwargs): 46 | return bool(request.user and request.user.role == "employee") 47 | -------------------------------------------------------------------------------- /jobsapp/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import JobGQLType 4 | from jobsapp.models import Job 5 | from .exceptions import GraphQLError 6 | 7 | 8 | class JobQuery(graphene.ObjectType): 9 | jobs = graphene.List(JobGQLType) 10 | job = graphene.Field(JobGQLType, pk=graphene.Int()) 11 | 12 | def resolve_jobs(self, info): 13 | return Job.objects.all() 14 | 15 | def resolve_job(self, info, pk, **kwargs): 16 | if pk: 17 | try: 18 | return Job.objects.get(pk=pk) 19 | except Job.DoesNotExist: 20 | return GraphQLError("Job doesn't exists") 21 | return None 22 | -------------------------------------------------------------------------------- /jobsapp/graphql/sub_mutations.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from jobsapp.graphql.graphql_mixins import ( 4 | DynamicArgsMixin, 5 | MutationMixin, 6 | CreateNewJobMixin, 7 | UpdateJobMixin, 8 | SingleObjectMixin, 9 | ) 10 | from jobsapp.graphql.input_types import TagInput 11 | from jobsapp.graphql.permissions import IsAuthenticated, IsEmployer 12 | from graphene.types import Int 13 | 14 | from jobsapp.graphql.types import JobGQLType 15 | from jobsapp.models import Job 16 | 17 | 18 | class CreateNewJob(MutationMixin, DynamicArgsMixin, CreateNewJobMixin, graphene.Mutation): 19 | __doc__ = CreateNewJobMixin.__doc__ 20 | _required_args = { 21 | "title": "String", 22 | "description": "String", 23 | "location": "String", 24 | "type": "String", 25 | "category": "String", 26 | "last_date": "String", 27 | "company_name": "String", 28 | "company_description": "String", 29 | "website": "String", 30 | "salary": "Int", 31 | } 32 | permission_classes = [IsAuthenticated, IsEmployer] 33 | 34 | class Arguments: 35 | tags = graphene.List(Int, required=True) 36 | 37 | 38 | class UpdateJob(MutationMixin, DynamicArgsMixin, SingleObjectMixin, UpdateJobMixin, graphene.Mutation): 39 | job = graphene.Field(JobGQLType) 40 | __doc__ = UpdateJobMixin.__doc__ 41 | _required_args = {"pk": "ID"} 42 | _args = { 43 | "title": "String", 44 | "description": "String", 45 | "location": "String", 46 | "type": "String", 47 | "category": "String", 48 | "last_date": "String", 49 | "company_name": "String", 50 | "company_description": "String", 51 | "website": "String", 52 | "salary": "Int", 53 | } 54 | 55 | class Arguments: 56 | tags = graphene.List(Int, required=False) 57 | 58 | permission_classes = [IsAuthenticated, IsEmployer] 59 | model = Job 60 | check_object_level_permission: bool = False 61 | -------------------------------------------------------------------------------- /jobsapp/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene_django import DjangoObjectType 2 | import graphene 3 | from graphene_django.utils import camelize 4 | 5 | from .exceptions import WrongUsage 6 | 7 | from jobsapp.models import Job 8 | 9 | 10 | class JobGQLType(DjangoObjectType): 11 | class Meta: 12 | model = Job 13 | fields = "__all__" 14 | 15 | 16 | class ExpectedErrorType(graphene.Scalar): 17 | class Meta: 18 | description = """ 19 | Errors messages and codes mapped to fields or non fields errors. 20 | Example: 21 | { 22 | field_name: [ 23 | { 24 | "message": "error message", 25 | "code": "error_code" 26 | } 27 | ], 28 | other_field: [ 29 | { 30 | "message": "error message", 31 | "code": "error_code" 32 | } 33 | ], 34 | nonFieldErrors: [ 35 | { 36 | "message": "error message", 37 | "code": "error_code" 38 | } 39 | ] 40 | } 41 | """ 42 | 43 | @staticmethod 44 | def serialize(errors): 45 | if isinstance(errors, dict): 46 | if errors.get("__all__", False): 47 | errors["non_field_errors"] = errors.pop("__all__") 48 | return camelize(errors) 49 | elif isinstance(errors, list): 50 | return {"nonFieldErrors": errors} 51 | raise WrongUsage("`errors` must be list or dict!") 52 | -------------------------------------------------------------------------------- /jobsapp/manager.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class JobManager(models.Manager): 5 | def filled(self, *args, **kwargs): 6 | return self.filter(filled=True, *args, **kwargs) 7 | 8 | def unfilled(self, *args, **kwargs): 9 | return self.filter(filled=False, *args, **kwargs) 10 | -------------------------------------------------------------------------------- /jobsapp/metrics.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import Counter, Enum, Gauge, Histogram, Info, Summary 2 | 3 | info = Info(name="app", documentation="Information about the application") 4 | info.info({"version": "1.0", "language": "python", "framework": "django"}) 5 | 6 | requests_total = Counter( 7 | name="app_requests_total", 8 | documentation="Total number of various requests.", 9 | labelnames=["endpoint", "method", "user"], 10 | ) 11 | last_user_activity_time = Gauge( 12 | name="app_last_user_activity_time_seconds", 13 | documentation="The last time when user was active.", 14 | labelnames=["user"], 15 | ) 16 | 17 | response_time_histogram = Histogram( 18 | name="app_response_time_seconds", documentation="Response time for requests", labelnames=["method", "endpoint"] 19 | ) 20 | 21 | error_rates_counter = Counter( 22 | name="app_error_rates_total", documentation="The total number of errors", labelnames=["status_code", "endpoint"] 23 | ) 24 | -------------------------------------------------------------------------------- /jobsapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-03 18:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('accounts', '0002_auto_20190326_1754'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Job', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=300)), 21 | ('description', models.TextField()), 22 | ('location', models.CharField(max_length=150)), 23 | ('type', models.CharField(choices=[(1, 'Full time'), (2, 'Part time'), (3, 'Internship')], max_length=10)), 24 | ('category', models.CharField(max_length=100)), 25 | ('last_date', models.DateTimeField()), 26 | ('company_name', models.CharField(max_length=100)), 27 | ('company_description', models.CharField(max_length=300)), 28 | ('website', models.CharField(default='', max_length=100)), 29 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.User')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /jobsapp/migrations/0002_auto_20190405_1920.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-05 13:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='job', 15 | name='type', 16 | field=models.CharField(choices=[('1', 'Full time'), ('2', 'Part time'), ('3', 'Internship')], max_length=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobsapp/migrations/0003_job_created_at.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-06 02:20 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('jobsapp', '0002_auto_20190405_1920'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='job', 16 | name='created_at', 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /jobsapp/migrations/0004_job_filled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-06 15:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0003_job_created_at'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='job', 15 | name='filled', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobsapp/migrations/0005_applicant.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-06 17:12 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('accounts', '0002_auto_20190326_1754'), 12 | ('jobsapp', '0004_job_filled'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Applicant', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 21 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jobsapp.Job')), 22 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.User')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /jobsapp/migrations/0006_auto_20190408_2005.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-08 14:05 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('jobsapp', '0005_applicant'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='applicant', 16 | name='job', 17 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applicants', to='jobsapp.Job'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /jobsapp/migrations/0007_job_salary.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-12-10 08:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0006_auto_20190408_2005'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='job', 15 | name='salary', 16 | field=models.IntegerField(blank=True, default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobsapp/migrations/0008_auto_20200810_1925.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-08-10 19:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0002_auto_20190326_1754'), 10 | ('jobsapp', '0007_job_salary'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name='applicant', 16 | unique_together={('user', 'job')}, 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobsapp/migrations/0009_favorite.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-07 11:38 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('accounts', '0002_auto_20190326_1754'), 12 | ('jobsapp', '0008_auto_20200810_1925'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Favorite', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 21 | ('soft_deleted', models.BooleanField(default=False)), 22 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='jobsapp.Job')), 23 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.User')), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /jobsapp/migrations/0010_auto_20201107_1404.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-07 14:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0009_favorite'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='applicant', 15 | name='comment', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='applicant', 20 | name='status', 21 | field=models.SmallIntegerField(default=1), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /jobsapp/migrations/0011_auto_20201118_1751.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-18 17:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0010_auto_20201107_1404'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='applicant', 15 | options={'ordering': ['id']}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='job', 19 | options={'ordering': ['id']}, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /jobsapp/migrations/0012_job_tags.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-26 13:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tags', '0001_initial'), 10 | ('jobsapp', '0011_auto_20201118_1751'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='job', 16 | name='tags', 17 | field=models.ManyToManyField(to='tags.Tag'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /jobsapp/migrations/0013_job_vacancy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-10-12 13:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobsapp', '0012_job_tags'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='job', 15 | name='vacancy', 16 | field=models.IntegerField(default=1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobsapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/jobsapp/migrations/__init__.py -------------------------------------------------------------------------------- /jobsapp/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import AccessMixin 2 | 3 | 4 | class EmployeeRequiredMixin(AccessMixin): 5 | """Verify that the current user is employee.""" 6 | 7 | def dispatch(self, request, *args, **kwargs): 8 | user = request.user 9 | if user.role != "employee": 10 | return self.handle_no_permission() 11 | return super().dispatch(request, *args, **kwargs) 12 | 13 | 14 | class EmployerRequiredMixin(AccessMixin): 15 | """Verify that the current user is employee.""" 16 | 17 | def dispatch(self, request, *args, **kwargs): 18 | user = request.user 19 | if user.role != "employer": 20 | return self.handle_no_permission() 21 | return super().dispatch(request, *args, **kwargs) 22 | -------------------------------------------------------------------------------- /jobsapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | from django.utils import timezone 4 | 5 | from accounts.models import User 6 | from tags.models import Tag 7 | 8 | from .manager import JobManager 9 | 10 | JOB_TYPE = (("1", "Full time"), ("2", "Part time"), ("3", "Internship")) 11 | 12 | 13 | class Job(models.Model): 14 | user = models.ForeignKey(User, on_delete=models.CASCADE) 15 | title = models.CharField(max_length=300) 16 | description = models.TextField() 17 | location = models.CharField(max_length=150) 18 | type = models.CharField(choices=JOB_TYPE, max_length=10) 19 | category = models.CharField(max_length=100) 20 | last_date = models.DateTimeField() 21 | company_name = models.CharField(max_length=100) 22 | company_description = models.CharField(max_length=300) 23 | website = models.CharField(max_length=100, default="") 24 | created_at = models.DateTimeField(default=timezone.now) 25 | filled = models.BooleanField(default=False) 26 | salary = models.IntegerField(default=0, blank=True) 27 | tags = models.ManyToManyField(Tag) 28 | vacancy = models.IntegerField(default=1) 29 | 30 | objects = JobManager() 31 | 32 | class Meta: 33 | ordering = ["id"] 34 | 35 | def get_absolute_url(self): 36 | return reverse("jobs:jobs-detail", args=[self.id]) 37 | 38 | def __str__(self): 39 | return self.title 40 | 41 | 42 | class Applicant(models.Model): 43 | user = models.ForeignKey(User, on_delete=models.CASCADE) 44 | job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="applicants") 45 | created_at = models.DateTimeField(default=timezone.now) 46 | comment = models.TextField(blank=True, null=True) 47 | status = models.SmallIntegerField(default=1) 48 | 49 | class Meta: 50 | ordering = ["id"] 51 | unique_together = ["user", "job"] 52 | 53 | def __str__(self): 54 | return self.user.get_full_name() 55 | 56 | @property 57 | def get_status(self): 58 | if self.status == 1: 59 | return "Pending" 60 | elif self.status == 2: 61 | return "Accepted" 62 | else: 63 | return "Rejected" 64 | 65 | 66 | class Favorite(models.Model): 67 | user = models.ForeignKey(User, on_delete=models.CASCADE) 68 | job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="favorites") 69 | created_at = models.DateTimeField(default=timezone.now) 70 | soft_deleted = models.BooleanField(default=False) 71 | 72 | def __str__(self): 73 | return self.job.title 74 | -------------------------------------------------------------------------------- /jobsapp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/jobsapp/templatetags/__init__.py -------------------------------------------------------------------------------- /jobsapp/templatetags/is_already_applied.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from jobsapp.models import Applicant 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag(name="is_already_applied") 9 | def is_already_applied(job, user): 10 | applied = Applicant.objects.filter(job=job, user=user) 11 | if applied: 12 | return True 13 | else: 14 | return False 15 | -------------------------------------------------------------------------------- /jobsapp/templatetags/is_favorited.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from jobsapp.models import Favorite 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag(name="is_favorited", takes_context=True) 9 | def is_favorited(context, job): 10 | request = context["request"] 11 | return Favorite.objects.filter(job_id=job.id, user_id=request.user.id, soft_deleted=False).exists() 12 | -------------------------------------------------------------------------------- /jobsapp/templatetags/tag_exists.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag(name="tag_exists") 7 | def tag_exists(id, tags): 8 | return True if tags is not None and len(tags) > 0 and str(id) in tags else False 9 | 10 | 11 | @register.filter 12 | def get_item(dictionary, key): 13 | return dict(dictionary).get(key) 14 | -------------------------------------------------------------------------------- /jobsapp/templatetags/url_replace.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.simple_tag(takes_context=True) 7 | def url_replace(context, field, value): 8 | request = context["request"] 9 | dict_ = request.GET.copy() 10 | 11 | dict_[field] = value 12 | 13 | return dict_.urlencode() 14 | -------------------------------------------------------------------------------- /jobsapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from .views import * 4 | 5 | app_name = "jobs" 6 | 7 | urlpatterns = [ 8 | path("", HomeView.as_view(), name="home"), 9 | path("favorite/", favorite, name="favorite"), 10 | path("search/", SearchView.as_view(), name="search"), 11 | path( 12 | "employer/dashboard/", 13 | include( 14 | [ 15 | path("", DashboardView.as_view(), name="employer-dashboard"), 16 | path("all-applicants/", ApplicantsListView.as_view(), name="employer-all-applicants"), 17 | path("applicants//", ApplicantPerJobView.as_view(), name="employer-dashboard-applicants"), 18 | path( 19 | "applied-applicant//view/", 20 | AppliedApplicantView.as_view(), 21 | name="applied-applicant-view", 22 | ), 23 | path("mark-filled//", filled, name="job-mark-filled"), 24 | path("send-response/", SendResponseView.as_view(), name="applicant-send-response"), 25 | path("jobs/create/", JobCreateView.as_view(), name="employer-jobs-create"), 26 | path("jobs//edit/", JobUpdateView.as_view(), name="employer-jobs-edit"), 27 | ] 28 | ), 29 | ), 30 | path( 31 | "employee/", 32 | include( 33 | [ 34 | path("my-applications", EmployeeMyJobsListView.as_view(), name="employee-my-applications"), 35 | path("favorites", FavoriteListView.as_view(), name="employee-favorites"), 36 | ] 37 | ), 38 | ), 39 | path("apply-job//", ApplyJobView.as_view(), name="apply-job"), 40 | path("jobs/", JobListView.as_view(), name="jobs"), 41 | path("jobs//", JobDetailsView.as_view(), name="jobs-detail"), 42 | path("about-us/", AboutUsView.as_view(), name="about-us"), 43 | ] 44 | -------------------------------------------------------------------------------- /jobsapp/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .employee import * 2 | from .employer import * 3 | from .home import * 4 | -------------------------------------------------------------------------------- /jobsapp/views/employee.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.http import Http404 3 | from django.urls import reverse_lazy 4 | from django.utils.decorators import method_decorator 5 | from django.views.generic import ListView, UpdateView 6 | 7 | from accounts.forms import EmployeeProfileUpdateForm 8 | from accounts.models import User 9 | from jobsapp.decorators import user_is_employee 10 | from jobsapp.models import Applicant, Favorite 11 | 12 | 13 | @method_decorator(login_required(login_url=reverse_lazy("accounts:login")), name="dispatch") 14 | @method_decorator(user_is_employee, name="dispatch") 15 | class EmployeeMyJobsListView(ListView): 16 | model = Applicant 17 | template_name = "jobs/employee/my-applications.html" 18 | context_object_name = "applicants" 19 | paginate_by = 6 20 | 21 | def get_queryset(self): 22 | self.queryset = ( 23 | self.model.objects.select_related("job").filter(user_id=self.request.user.id).order_by("-created_at") 24 | ) 25 | if ( 26 | "status" in self.request.GET 27 | and len(self.request.GET.get("status")) > 0 28 | and int(self.request.GET.get("status")) > 0 29 | ): 30 | self.queryset = self.queryset.filter(status=int(self.request.GET.get("status"))) 31 | return self.queryset 32 | 33 | 34 | class EditProfileView(UpdateView): 35 | model = User 36 | form_class = EmployeeProfileUpdateForm 37 | context_object_name = "employee" 38 | template_name = "jobs/employee/edit-profile.html" 39 | success_url = reverse_lazy("accounts:employee-profile-update") 40 | 41 | @method_decorator(login_required(login_url=reverse_lazy("accounts:login"))) 42 | @method_decorator(user_is_employee) 43 | def dispatch(self, request, *args, **kwargs): 44 | return super().dispatch(self.request, *args, **kwargs) 45 | 46 | def get(self, request, *args, **kwargs): 47 | try: 48 | self.object = self.get_object() 49 | except Http404: 50 | raise Http404("User doesn't exists") 51 | # context = self.get_context_data(object=self.object) 52 | return self.render_to_response(self.get_context_data()) 53 | 54 | def get_object(self, queryset=None): 55 | obj = self.request.user 56 | if obj is None: 57 | raise Http404("Job doesn't exists") 58 | return obj 59 | 60 | 61 | @method_decorator(login_required(login_url=reverse_lazy("accounts:login")), name="dispatch") 62 | @method_decorator(user_is_employee, name="dispatch") 63 | class FavoriteListView(ListView): 64 | model = Favorite 65 | template_name = "jobs/employee/favorites.html" 66 | context_object_name = "favorites" 67 | 68 | def get_queryset(self): 69 | return self.model.objects.select_related("job__user").filter(soft_deleted=False, user=self.request.user) 70 | -------------------------------------------------------------------------------- /locale/bn/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-11-18 20:15+0600\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: jobs/settings.py:118 22 | msgid "English" 23 | msgstr "English" 24 | 25 | #: jobs/settings.py:119 26 | msgid "Bengali" 27 | msgstr "বাংলা" 28 | 29 | #: templates/home.html:23 30 | msgid "Position" 31 | msgstr "অবস্থান" 32 | 33 | #: templates/lang_selector.html:9 34 | msgid "Language" 35 | msgstr "ভাষা" 36 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jobs.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /media/resumes/2022/03/28/1VGE2R2iED2J3VaE5s20KXcLWPXrr6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/media/resumes/2022/03/28/1VGE2R2iED2J3VaE5s20KXcLWPXrr6.png -------------------------------------------------------------------------------- /media/resumes/2022/03/28/GobYkNGjKLMq57nrJbbr3m2fMpYZeW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/media/resumes/2022/03/28/GobYkNGjKLMq57nrJbbr3m2fMpYZeW.png -------------------------------------------------------------------------------- /media/resumes/2022/03/28/SBH1pvrgUztNXXKL3eRS5GbcF0EHiC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/media/resumes/2022/03/28/SBH1pvrgUztNXXKL3eRS5GbcF0EHiC.png -------------------------------------------------------------------------------- /media/resumes/2022/03/28/rrh11hPn7oiHIiVFt3S1rv28xv9qBY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/media/resumes/2022/03/28/rrh11hPn7oiHIiVFt3S1rv28xv9qBY.png -------------------------------------------------------------------------------- /provisioning/grafana/dashboards/dashboard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - type: file 5 | options: 6 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /provisioning/grafana/datasources/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | orgId: 1 8 | url: http://prometheus:9090/ 9 | isDefault: true 10 | version: 1 11 | editable: false 12 | -------------------------------------------------------------------------------- /provisioning/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | 4 | alerting: 5 | alertmanagers: 6 | - static_configs: 7 | - targets: 8 | - alertmanager:9093 9 | 10 | rule_files: 11 | - /etc/prometheus/rules.yml 12 | 13 | scrape_configs: 14 | - job_name: web-apps 15 | scrape_interval: 10s 16 | static_configs: 17 | - targets: 18 | - web:8000 19 | 20 | - job_name: node-exporter 21 | static_configs: 22 | - targets: 23 | - node-exporter:9100 24 | 25 | -------------------------------------------------------------------------------- /provisioning/prometheus/rules.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: nodes 3 | rules: 4 | - alert: App server is down 5 | expr: up{instance="web:8000"} == 0 6 | for: 1m 7 | annotations: 8 | description: App server is not running 9 | summary: App server is down and not running 10 | 11 | - name: cpu 12 | rules: 13 | - alert: High CPU Usage 14 | expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 15 | for: 5m 16 | annotations: 17 | description: CPU usage is above 80% for more than 5 minutes. 18 | summary: High CPU usage detected. 19 | 20 | - name: memory 21 | rules: 22 | - alert: High Memory Usage 23 | expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100 > 90 24 | for: 5m 25 | annotations: 26 | description: Memory usage is above 90% for more than 5 minutes. 27 | summary: High memory usage detected. 28 | 29 | - name: disk 30 | rules: 31 | - alert: High Disk Space Usage 32 | expr: (node_filesystem_size_bytes{fstype!~"tmpfs|fuse.lxcfs"} - node_filesystem_free_bytes{fstype!~"tmpfs|fuse.lxcfs"}) / node_filesystem_size_bytes{fstype!~"tmpfs|fuse.lxcfs"} * 100 > 90 33 | for: 5m 34 | annotations: 35 | description: Disk space usage is above 90% for more than 5 minutes. 36 | summary: High disk space usage detected. 37 | 38 | - name: latency 39 | rules: 40 | - alert: High Request Latency 41 | expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 1 42 | for: 1m 43 | annotations: 44 | description: Request latency is above 1 second for more than 1 minute. 45 | summary: High request latency detected. 46 | 47 | - name: errors 48 | rules: 49 | - alert: High Error Rate 50 | expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05 51 | for: 1m 52 | annotations: 53 | description: Error rate is above 5% for more than 1 minute. 54 | summary: High error rate detected. -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | include = '\.pyi?$' 4 | exclude = "/(migrations|venv)/" 5 | force-exclude = ''' 6 | /( 7 | \.git 8 | | \.hg 9 | | \.mypy_cache 10 | | \.pytest_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | )/ 18 | | ./*/migrations 19 | ''' 20 | 21 | [tool.isort] 22 | profile="black" 23 | atomic=true 24 | extend_skip_glob="*/migrations/*.py" 25 | line_length=88 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==9.0.1 2 | appdirs==1.4.4 3 | asgiref==3.8.1 4 | black==24.10.0 5 | Brotli==1.1.0 6 | CacheControl==0.14.0 7 | certifi==2024.8.30 8 | cffi==1.17.1 9 | cfgv==3.4.0 10 | chardet==5.2.0 11 | charset-normalizer==3.4.0 12 | click==8.1.7 13 | colorama==0.4.6 14 | contextlib2==21.6.0 15 | coreapi==2.3.3 16 | coreschema==0.0.4 17 | cryptography==43.0.1 18 | cssselect2==0.7.0 19 | defusedxml==0.7.1 20 | distlib==0.3.9 21 | distro==1.9.0 22 | Django==5.1.2 23 | django-braces==1.16.0 24 | django-cors-headers==4.4.0 25 | django-elasticsearch-dsl==8.0 26 | django-environ==0.11.2 27 | django-extensions==3.2.3 28 | django-graphql-jwt==0.4.0 29 | django-oauth-toolkit==3.0.1 30 | django-prometheus==2.3.1 31 | django-rest-framework-social-oauth2==1.2.0 32 | djangorestframework==3.15.2 33 | djangorestframework-simplejwt==5.3.1 34 | drf-yasg==1.21.7 35 | elastic-transport==8.15.1 36 | elasticsearch==8.15.1 37 | elasticsearch-dsl==8.15.4 38 | factory_boy==3.3.3 39 | Faker==37.1.0 40 | filelock==3.16.1 41 | fonttools==4.54.1 42 | graphene==3.3 43 | graphene-django==3.2.2 44 | graphene-file-upload==1.3.0 45 | graphql-core==3.2.4 46 | graphql-relay==3.2.0 47 | gunicorn==23.0.0 48 | html5lib==1.1 49 | identify==2.6.1 50 | idna==3.10 51 | inflection==0.5.1 52 | iniconfig==2.1.0 53 | ipaddr==2.2.0 54 | isort==5.13.2 55 | itypes==1.2.0 56 | Jinja2==3.1.4 57 | jwcrypto==1.5.6 58 | lockfile==0.12.2 59 | MarkupSafe==3.0.1 60 | msgpack==1.1.0 61 | mypy-extensions==1.0.0 62 | nodeenv==1.9.1 63 | oauthlib==3.2.2 64 | openapi-codec==1.3.2 65 | packaging==24.1 66 | pathspec==0.12.1 67 | pep517==0.13.1 68 | pillow==10.4.0 69 | platformdirs==4.3.6 70 | pluggy==1.5.0 71 | pre_commit==4.0.1 72 | progress==1.6 73 | prometheus_client==0.21.0 74 | promise==2.3 75 | psycopg2-binary==2.9.9 76 | pur==7.3.2 77 | pycparser==2.22 78 | pydyf==0.11.0 79 | Pygments==2.18.0 80 | PyJWT==2.9.0 81 | pyparsing==3.1.4 82 | pyphen==0.15.0 83 | pytest==8.3.5 84 | pytest-django==4.10.0 85 | python-dateutil==2.9.0.post0 86 | python3-openid==3.2.0 87 | pytoml==0.1.21 88 | pytz==2024.2 89 | PyYAML==6.0.2 90 | regex==2024.9.11 91 | requests==2.32.3 92 | requests-oauthlib==2.0.0 93 | retrying==1.3.4 94 | ruamel.yaml==0.18.6 95 | ruamel.yaml.clib==0.2.8 96 | Rx==3.2.0 97 | simplejson==3.19.3 98 | singledispatch==4.1.0 99 | six==1.16.0 100 | social-auth-app-django==5.4.2 101 | social-auth-core==4.5.4 102 | sqlparse==0.5.1 103 | text-unidecode==1.3 104 | tinycss2==1.4.0 105 | tinyhtml5==2.0.0 106 | toml==0.10.2 107 | typed-ast==1.5.5 108 | typing_extensions==4.12.2 109 | tzdata==2025.2 110 | uritemplate==4.1.1 111 | urllib3==2.2.3 112 | virtualenv==20.26.6 113 | weasyprint==63.0 114 | webencodings==0.5.1 115 | whitenoise==6.7.0 116 | zopfli==0.2.3.post1 117 | -------------------------------------------------------------------------------- /resume_cv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/resume_cv/__init__.py -------------------------------------------------------------------------------- /resume_cv/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from resume_cv.models import ResumeCv, ResumeCvCategory, ResumeCvTemplate 4 | 5 | admin.site.register(ResumeCv) 6 | admin.site.register(ResumeCvCategory) 7 | admin.site.register(ResumeCvTemplate) 8 | -------------------------------------------------------------------------------- /resume_cv/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ResumeCvConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "resume_cv" 7 | -------------------------------------------------------------------------------- /resume_cv/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from resume_cv.models import ResumeCv 4 | 5 | 6 | class ResumeCvForm(forms.ModelForm): 7 | class Meta: 8 | model = ResumeCv 9 | exclude = ("user", "view_count", "is_published") 10 | -------------------------------------------------------------------------------- /resume_cv/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-03-27 18:23 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import uuid 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='ResumeCv', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('code', models.UUIDField(default=uuid.uuid4, editable=False)), 23 | ('name', models.CharField(max_length=255)), 24 | ('content', models.TextField(blank=True, null=True)), 25 | ('style', models.TextField(blank=True, null=True)), 26 | ('is_published', models.BooleanField(default=True)), 27 | ('view_count', models.IntegerField(default=0)), 28 | ('created_at', models.DateTimeField(auto_now_add=True)), 29 | ('updated_at', models.DateTimeField(auto_now=True)), 30 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='resume_cvs', to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /resume_cv/migrations/0002_resumecvcategory.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-03-28 14:37 2 | 3 | from django.db import migrations, models 4 | import resume_cv.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resume_cv', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ResumeCvCategory', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255)), 19 | ('thumbnail', models.ImageField(upload_to=resume_cv.models.resume_cv_directory_path)), 20 | ('color', models.CharField(max_length=20)), 21 | ('created_at', models.DateTimeField(auto_now_add=True)), 22 | ('updated_at', models.DateTimeField(auto_now=True)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /resume_cv/migrations/0003_resumecvtemplate.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-03-28 14:46 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import resume_cv.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('resume_cv', '0002_resumecvcategory'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ResumeCvTemplate', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=255)), 20 | ('thumbnail', models.ImageField(blank=True, null=True, upload_to=resume_cv.models.resume_cv_directory_path)), 21 | ('content', models.TextField(blank=True, null=True)), 22 | ('style', models.TextField(blank=True, null=True)), 23 | ('active', models.BooleanField(default=False)), 24 | ('is_premium', models.BooleanField(default=False)), 25 | ('created_at', models.DateTimeField(auto_now_add=True)), 26 | ('updated_at', models.DateTimeField(auto_now=True)), 27 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='resume_cv.resumecvcategory')), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /resume_cv/migrations/0004_alter_resumecvcategory_thumbnail.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-03-28 14:54 2 | 3 | from django.db import migrations, models 4 | import resume_cv.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resume_cv', '0003_resumecvtemplate'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='resumecvcategory', 16 | name='thumbnail', 17 | field=models.ImageField(blank=True, null=True, upload_to=resume_cv.models.resume_cv_directory_path), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /resume_cv/migrations/0005_resumecv_template.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-04-19 14:43 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('resume_cv', '0004_alter_resumecvcategory_thumbnail'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='resumecv', 16 | name='template', 17 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='resume_cvs', to='resume_cv.resumecvtemplate'), 18 | preserve_default=False, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /resume_cv/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/resume_cv/migrations/__init__.py -------------------------------------------------------------------------------- /resume_cv/models.py: -------------------------------------------------------------------------------- 1 | from time import strftime 2 | 3 | from django.db import models 4 | import uuid 5 | 6 | from accounts.models import User 7 | from utils.filename import generate_file_name 8 | 9 | 10 | def resume_cv_directory_path(instance, filename): 11 | return "resumes/{0}/{1}".format(strftime("%Y/%m/%d"), generate_file_name() + "." + filename.split(".")[-1]) 12 | 13 | 14 | class ResumeCvCategory(models.Model): 15 | name = models.CharField(max_length=255) 16 | thumbnail = models.ImageField(upload_to=resume_cv_directory_path, blank=True, null=True) 17 | color = models.CharField(max_length=20) 18 | 19 | created_at = models.DateTimeField(auto_now_add=True) 20 | updated_at = models.DateTimeField(auto_now=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class ResumeCvTemplate(models.Model): 27 | category = models.ForeignKey(ResumeCvCategory, on_delete=models.CASCADE, related_name="templates") 28 | name = models.CharField(max_length=255) 29 | thumbnail = models.ImageField(upload_to=resume_cv_directory_path, blank=True, null=True) 30 | content = models.TextField(null=True, blank=True) 31 | style = models.TextField(null=True, blank=True) 32 | active = models.BooleanField(default=False) 33 | is_premium = models.BooleanField(default=False) 34 | 35 | created_at = models.DateTimeField(auto_now_add=True) 36 | updated_at = models.DateTimeField(auto_now=True) 37 | 38 | def __str__(self): 39 | return self.name 40 | 41 | 42 | class ResumeCv(models.Model): 43 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="resume_cvs") 44 | template = models.ForeignKey(ResumeCvTemplate, on_delete=models.CASCADE, related_name="resume_cvs") 45 | code = models.UUIDField(default=uuid.uuid4, editable=False) 46 | name = models.CharField(max_length=255) 47 | content = models.TextField(null=True, blank=True) 48 | style = models.TextField(null=True, blank=True) 49 | is_published = models.BooleanField(default=True) 50 | view_count = models.IntegerField(default=0) 51 | 52 | created_at = models.DateTimeField(auto_now_add=True) 53 | updated_at = models.DateTimeField(auto_now=True) 54 | 55 | def __str__(self): 56 | return self.name 57 | -------------------------------------------------------------------------------- /resume_cv/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /resume_cv/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | TemplateListView, 5 | ResumeCVCreateView, 6 | resume_builder, 7 | UserResumeListView, 8 | download_resume, 9 | ) 10 | 11 | app_name = "resume_cv" 12 | 13 | urlpatterns = [ 14 | path("templates", TemplateListView.as_view(), name="templates"), 15 | path("resume-cv/create", ResumeCVCreateView.as_view(), name="create"), 16 | path("templates/builder/", resume_builder, name="builder"), 17 | path("resumes/", UserResumeListView.as_view(), name="resumes"), 18 | path("download-as-pdf//", download_resume, name="export.pdf"), 19 | ] 20 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.10 2 | -------------------------------------------------------------------------------- /screenshots/five.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/five.png -------------------------------------------------------------------------------- /screenshots/four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/four.png -------------------------------------------------------------------------------- /screenshots/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/illustration.png -------------------------------------------------------------------------------- /screenshots/one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/one.png -------------------------------------------------------------------------------- /screenshots/seven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/seven.png -------------------------------------------------------------------------------- /screenshots/six.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/six.png -------------------------------------------------------------------------------- /screenshots/three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/three.png -------------------------------------------------------------------------------- /screenshots/two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/screenshots/two.png -------------------------------------------------------------------------------- /static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* your styles go here */ 2 | 3 | .favorite { 4 | color: #ff7473 !important; 5 | } -------------------------------------------------------------------------------- /static/css/templates.css: -------------------------------------------------------------------------------- 1 | img { 2 | vertical-align: middle; 3 | border-style: none; 4 | } 5 | 6 | .badge { 7 | display: inline-block; 8 | padding: .25em .4em; 9 | font-size: 75%; 10 | font-weight: 700; 11 | line-height: 1; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25rem; 16 | transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; 17 | } 18 | 19 | @media (prefers-reduced-motion: reduce) { 20 | .badge { 21 | transition: none; 22 | } 23 | } 24 | 25 | .badge:empty { 26 | display: none; 27 | } 28 | 29 | .badge-success { 30 | color: #fff; 31 | background-color: #28a745; 32 | } 33 | 34 | .clearfix::after { 35 | display: block; 36 | clear: both; 37 | content: ""; 38 | } 39 | 40 | .d-flex { 41 | display: -ms-flexbox !important; 42 | display: flex !important; 43 | } 44 | 45 | .flex-column { 46 | -ms-flex-direction: column !important; 47 | flex-direction: column !important; 48 | } 49 | 50 | @media (min-width: 992px) { 51 | .flex-lg-row { 52 | -ms-flex-direction: row !important; 53 | flex-direction: row !important; 54 | } 55 | } 56 | 57 | .mr-2 { 58 | margin-right: .5rem !important; 59 | } 60 | 61 | .p-1 { 62 | padding: .25rem !important; 63 | } 64 | 65 | .pt-1 { 66 | padding-top: .25rem !important; 67 | } 68 | 69 | @media print { 70 | 71 | a:not(.btn) { 72 | text-decoration: underline; 73 | } 74 | 75 | img { 76 | page-break-inside: avoid; 77 | } 78 | 79 | .badge { 80 | border: 1px solid #000; 81 | } 82 | } 83 | 84 | .btn, .btn:focus { 85 | outline: none !important; 86 | -webkit-box-shadow: none !important; 87 | box-shadow: none !important; 88 | } 89 | 90 | a { 91 | text-decoration: none !important; 92 | outline: none; 93 | } 94 | 95 | .btn { 96 | font-size: 15px; 97 | padding: 0.7rem 1.4rem; 98 | -webkit-transition: all 0.4s; 99 | transition: all 0.4s; 100 | font-weight: 600; 101 | } 102 | 103 | .btn:hover { 104 | outline: none; 105 | text-decoration: none; 106 | -webkit-transform: translateY(-4px); 107 | transform: translateY(-4px); 108 | } 109 | 110 | .btn-sm { 111 | padding: 0.4rem 1rem; 112 | font-size: 0.875rem; 113 | } 114 | 115 | .btn-primary { 116 | background: #4353ff !important; 117 | border-color: #4353ff !important; 118 | } 119 | 120 | .btn-primary:hover, .btn-primary:focus, .btn-primary:active { 121 | background: #2f40ff; 122 | border-color: #2f40ff; 123 | } 124 | 125 | .blog { 126 | overflow: hidden; 127 | box-shadow: 0px 0px 26px 0px rgba(22, 73, 172, 0.12); 128 | margin-bottom: 30px; 129 | } 130 | 131 | .blog:hover { 132 | box-shadow: 0px 5px 20px 5px rgb(94 96 101 / 12%); 133 | } 134 | 135 | .blog.blogitemlarge .image-blog { 136 | display: block; 137 | position: relative; 138 | text-align: center; 139 | } 140 | 141 | .blog.blogitemlarge .image-blog img { 142 | width: 100%; 143 | height: auto; 144 | } 145 | 146 | .blog.blogitemlarge .image-blog:before { 147 | content: ""; 148 | width: 100%; 149 | height: 25%; 150 | position: absolute; 151 | bottom: 0; 152 | left: 0; 153 | } 154 | 155 | .blog.blogitemlarge .image-blog .post-date { 156 | color: #fff; 157 | position: absolute; 158 | bottom: 15px; 159 | left: 15px; 160 | margin-bottom: 0; 161 | padding: 6px; 162 | font-weight: 400; 163 | } 164 | 165 | .blog .post-date { 166 | margin-bottom: 15px; 167 | color: #83898c; 168 | font-size: 15px; 169 | } 170 | 171 | .blog .content_blog { 172 | padding: 10px; 173 | } 174 | 175 | a { 176 | color: #272f46; 177 | } -------------------------------------------------------------------------------- /static/img/1875187.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/1875187.jpg -------------------------------------------------------------------------------- /static/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/avatar.png -------------------------------------------------------------------------------- /static/img/company-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/company-1.png -------------------------------------------------------------------------------- /static/img/featured1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/featured1.jpg -------------------------------------------------------------------------------- /static/img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/logo-small.png -------------------------------------------------------------------------------- /static/img/meeting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/img/meeting.jpg -------------------------------------------------------------------------------- /static/js/custom.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; // Start of use strict 3 | 4 | $(".btn_builder_template").on("click", function (e) { 5 | e.preventDefault(); 6 | let id = $(this).data('id'); 7 | console.log(id) 8 | $('#template_id_builder').val(id); 9 | }); 10 | 11 | })(jQuery); // End of use strict -------------------------------------------------------------------------------- /static/vendor/jquery.cookie/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | }(function ($) { 20 | 21 | var pluses = /\+/g; 22 | 23 | function encode(s) { 24 | return config.raw ? s : encodeURIComponent(s); 25 | } 26 | 27 | function decode(s) { 28 | return config.raw ? s : decodeURIComponent(s); 29 | } 30 | 31 | function stringifyCookieValue(value) { 32 | return encode(config.json ? JSON.stringify(value) : String(value)); 33 | } 34 | 35 | function parseCookieValue(s) { 36 | if (s.indexOf('"') === 0) { 37 | // This is a quoted cookie as according to RFC2068, unescape... 38 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 39 | } 40 | 41 | try { 42 | // Replace server-side written pluses with spaces. 43 | // If we can't decode the cookie, ignore it, it's unusable. 44 | // If we can't parse the cookie, ignore it, it's unusable. 45 | s = decodeURIComponent(s.replace(pluses, ' ')); 46 | return config.json ? JSON.parse(s) : s; 47 | } catch(e) {} 48 | } 49 | 50 | function read(s, converter) { 51 | var value = config.raw ? s : parseCookieValue(s); 52 | return $.isFunction(converter) ? converter(value) : value; 53 | } 54 | 55 | var config = $.cookie = function (key, value, options) { 56 | 57 | // Write 58 | 59 | if (value !== undefined && !$.isFunction(value)) { 60 | options = $.extend({}, config.defaults, options); 61 | 62 | if (typeof options.expires === 'number') { 63 | var days = options.expires, t = options.expires = new Date(); 64 | t.setTime(+t + days * 864e+5); 65 | } 66 | 67 | return (document.cookie = [ 68 | encode(key), '=', stringifyCookieValue(value), 69 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 70 | options.path ? '; path=' + options.path : '', 71 | options.domain ? '; domain=' + options.domain : '', 72 | options.secure ? '; secure' : '' 73 | ].join('')); 74 | } 75 | 76 | // Read 77 | 78 | var result = key ? undefined : {}; 79 | 80 | // To prevent the for loop in the first place assign an empty array 81 | // in case there are no cookies at all. Also prevents odd result when 82 | // calling $.cookie(). 83 | var cookies = document.cookie ? document.cookie.split('; ') : []; 84 | 85 | for (var i = 0, l = cookies.length; i < l; i++) { 86 | var parts = cookies[i].split('='); 87 | var name = decode(parts.shift()); 88 | var cookie = parts.join('='); 89 | 90 | if (key && key === name) { 91 | // If second argument (value) is a function it's a converter... 92 | result = read(cookie, value); 93 | break; 94 | } 95 | 96 | // Prevent storing a cookie that we couldn't decode. 97 | if (!key && (cookie = read(cookie)) !== undefined) { 98 | result[name] = cookie; 99 | } 100 | } 101 | 102 | return result; 103 | }; 104 | 105 | config.defaults = {}; 106 | 107 | $.removeCookie = function (key, options) { 108 | if ($.cookie(key) === undefined) { 109 | return false; 110 | } 111 | 112 | // Must not alter options, thus extending a fresh object... 113 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 114 | return !$.cookie(key); 115 | }; 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /static/vendor/owl.carousel/assets/owl.theme.default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Owl Carousel v2.3.4 3 | * Copyright 2013-2018 David Deutsch 4 | * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE 5 | */ 6 | /* 7 | * Default theme - Owl Carousel CSS File 8 | */ 9 | .owl-theme .owl-nav { 10 | margin-top: 10px; 11 | text-align: center; 12 | -webkit-tap-highlight-color: transparent; } 13 | .owl-theme .owl-nav [class*='owl-'] { 14 | color: #FFF; 15 | font-size: 14px; 16 | margin: 5px; 17 | padding: 4px 7px; 18 | background: #D6D6D6; 19 | display: inline-block; 20 | cursor: pointer; 21 | border-radius: 3px; } 22 | .owl-theme .owl-nav [class*='owl-']:hover { 23 | background: #869791; 24 | color: #FFF; 25 | text-decoration: none; } 26 | .owl-theme .owl-nav .disabled { 27 | opacity: 0.5; 28 | cursor: default; } 29 | 30 | .owl-theme .owl-nav.disabled + .owl-dots { 31 | margin-top: 10px; } 32 | 33 | .owl-theme .owl-dots { 34 | text-align: center; 35 | -webkit-tap-highlight-color: transparent; } 36 | .owl-theme .owl-dots .owl-dot { 37 | display: inline-block; 38 | zoom: 1; 39 | *display: inline; } 40 | .owl-theme .owl-dots .owl-dot span { 41 | width: 10px; 42 | height: 10px; 43 | margin: 5px 7px; 44 | background: #D6D6D6; 45 | display: block; 46 | -webkit-backface-visibility: visible; 47 | transition: opacity 200ms ease; 48 | border-radius: 30px; } 49 | .owl-theme .owl-dots .owl-dot.active span, .owl-theme .owl-dots .owl-dot:hover span { 50 | background: #869791; } 51 | -------------------------------------------------------------------------------- /static/vendor/owl.carousel/assets/owl.video.play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/vendor/owl.carousel/assets/owl.video.play.png -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /static/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /static/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /static/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/static/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tags/__init__.py -------------------------------------------------------------------------------- /tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Tag 4 | 5 | admin.site.register(Tag) 6 | -------------------------------------------------------------------------------- /tags/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tags/api/__init__.py -------------------------------------------------------------------------------- /tags/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from tags.models import Tag 4 | 5 | 6 | class TagSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Tag 9 | fields = "__all__" 10 | -------------------------------------------------------------------------------- /tags/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import TagListAPIView 4 | 5 | app_name = "tags-api" 6 | 7 | urlpatterns = [path("tags/", TagListAPIView.as_view(), name="tag-list")] 8 | -------------------------------------------------------------------------------- /tags/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListAPIView 2 | from rest_framework.permissions import AllowAny 3 | 4 | from tags.api.serializers import TagSerializer 5 | 6 | 7 | class TagListAPIView(ListAPIView): 8 | serializer_class = TagSerializer 9 | queryset = serializer_class.Meta.model.objects.all() 10 | permission_classes = [AllowAny] 11 | -------------------------------------------------------------------------------- /tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagsConfig(AppConfig): 5 | name = "tags" 6 | -------------------------------------------------------------------------------- /tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-11-26 13:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Tag', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /tags/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tags/migrations/__init__.py -------------------------------------------------------------------------------- /tags/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Tag(models.Model): 5 | name = models.CharField(max_length=100) 6 | 7 | def __str__(self): 8 | return self.name 9 | -------------------------------------------------------------------------------- /tags/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tags/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /templates/about_us.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block title %} 5 | About Us 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
12 |
13 |

About Us

14 |

Your trusted partner in job search and career growth

15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |

Our Mission

28 |

We are dedicated to connecting talented professionals with their dream careers. Our platform serves as a bridge between job seekers and employers, making the hiring process seamless and efficient.

29 | 30 |

What We Offer

31 |
    32 |
  • Easy job search and application process
  • 33 |
  • Professional resume builder
  • 34 |
  • Direct communication with employers
  • 35 |
  • Career growth resources
  • 36 |
37 |
38 |
39 | About Us 40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 |

Ready to start your career journey?

54 |

Browse Jobs

55 |
56 |
57 |
58 |
59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /templates/accounts/employee/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Employee Register 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |

New account for employee

11 |

Not our registered yet?

12 |

If you have any questions, please feel free to contact us, 13 | our customer service center is working for you 24/7.

14 | {% if form.errors %} 15 | {% for error in form.non_field_errors %} 16 |
17 | × 18 | {{ error|escape }} 19 |
20 | {% endfor %} 21 | {% endif %} 22 |
23 | {% csrf_token %} 24 | {% for field in form %} 25 | {% if field.name == 'gender' %} 26 | 27 |
28 | 29 |
30 |
31 | 33 | 34 |
35 |
36 | 38 | 39 |
40 |
41 | {% for error in field.errors %} 42 |
43 | × 44 | {{ error|escape }} 45 |
46 | {% endfor %} 47 | 48 | {% else %} 49 | 50 |
51 | 52 | 57 | {% for error in field.errors %} 58 |
59 | × 60 | {{ error|escape }} 61 |
62 | {% endfor %} 63 |
64 | 65 | {% endif %} 66 | {% endfor %} 67 |
68 | 70 |
71 |
72 |
73 |
74 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /templates/accounts/employer/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | Employer Register 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |

New account for employers

11 |

Not our registered yet?

12 |

If you have any questions, please feel free to contact us, 13 | our customer service center is working for you 24/7.

14 | {% if form.errors %} 15 | {% for error in form.non_field_errors %} 16 |
17 | × 18 | {{ error|escape }} 19 |
20 | {% endfor %} 21 | {% endif %} 22 |
23 | {% csrf_token %} 24 | {% for field in form %} 25 |
26 | 27 | 32 | {% for error in field.errors %} 33 |
34 | × 35 | {{ error|escape }} 36 |
37 | {% endfor %} 38 |
39 | {% endfor %} 40 |
41 | 43 |
44 |
45 |
46 |
47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %} 3 | {{ title }} 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |
10 |

Login

11 |

Already our have account?

12 | {% if form.errors %} 13 | {% for error in form.non_field_errors %} 14 |
15 | × 16 | {{ error|escape }} 17 |
18 | {% endfor %} 19 | {% endif %} 20 |
21 | {% csrf_token %} 22 | {% for field in form %} 23 |
24 | 25 | 30 | {% for error in field.errors %} 31 |
32 | × 33 | {{ error|escape }} 34 |
35 | {% endfor %} 36 |
37 | {% endfor %} 38 |
39 | 41 |
42 | 46 | 50 | 54 | 58 |
59 |
60 |
61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block title %} 5 | {% trans flatpage.title %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% trans flatpage.content %} 11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /templates/jobs/employee/edit-profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %} 4 | Edit Profile 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

Edit Profile

11 | {% if form.errors %} 12 | {% for field in form %} 13 | {% for error in field.errors %} 14 |
15 | × 16 | {{ error|escape }} 17 |
18 | {% endfor %} 19 | {% endfor %} 20 | {% for error in form.non_field_errors %} 21 |
22 | × 23 | {{ error|escape }} 24 |
25 | {% endfor %} 26 | {% endif %} 27 |
28 | {% csrf_token %} 29 | {% for field in form %} 30 | {% if field.name == 'gender' %} 31 | 32 |
33 | 34 |
35 |
36 | 39 | 40 |
41 |
42 | 45 | 46 |
47 |
48 | 49 | {% else %} 50 | 51 |
52 | 53 | 59 |
60 | 61 | {% endif %} 62 | {% endfor %} 63 |
64 | 67 |
68 |
69 |
70 |
71 | {% endblock %} -------------------------------------------------------------------------------- /templates/jobs/employee/favorites.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %} 4 | Favorite List 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

Wish list

11 | {% if favorites.count > 0 %} 12 | {% for trending in favorites %} 13 |
14 |
15 |
16 |
17 |
18 | ShareBoardd 20 |
21 |
22 |

23 | {{ trending.job.title }} 24 |

25 |

26 | {{ trending.job.company_name }} 27 |

28 |
29 |
30 |
31 |
32 | {{ trending.job.location }} 33 |
34 |
35 |

Posted {{ trending.job.created_at|timesince }}

36 |
37 |
38 |
39 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | {% endfor %} 49 | {% else %} 50 |

Empty favorite list!

51 | {% endif %} 52 |
53 |
54 | {% endblock %} 55 | 56 | 57 | {% block javascripts %} 58 | 59 | 106 | 107 | {% endblock %} -------------------------------------------------------------------------------- /templates/jobs/employer/edit-profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %} 4 | Edit Profile 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

Edit Company Profile

11 | {% if form.errors %} 12 | {% for field in form %} 13 | {% for error in field.errors %} 14 |
15 | × 16 | {{ error|escape }} 17 |
18 | {% endfor %} 19 | {% endfor %} 20 | {% for error in form.non_field_errors %} 21 |
22 | × 23 | {{ error|escape }} 24 |
25 | {% endfor %} 26 | {% endif %} 27 |
28 | {% csrf_token %} 29 | 30 | {% for field in form %} 31 |
32 | 33 | 39 |
40 | {% endfor %} 41 |
42 | 45 |
46 |
47 |
48 |
49 | {% endblock %} -------------------------------------------------------------------------------- /templates/lang_selector.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% get_available_languages as languages %} 3 | {% get_current_language as LANGUAGE_CODE %} 4 | 5 | -------------------------------------------------------------------------------- /templates/resumes/builder.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ resume.name }} - My resume 19 | 20 | 22 | 23 | 24 | 29 | 30 | 31 | 32 |
33 |
34 |

Builder doesn't work on mobile

35 | Back 36 |
37 |
38 | 39 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /templates/resumes/templates.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %} 4 | All templates 5 | {% endblock %} 6 | 7 | {% block styles %} 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 |
14 |
15 |

Choose a template for your Resume/CV

16 | All Templates 17 | {% for category in categories %} 18 | {{ category.name }} 19 | {% endfor %} 20 | 21 |
22 | {% for template in templates %} 23 |
24 |
25 | 27 | {{ template.name }} 30 | 31 | 32 |
33 |
34 | {{ template.name }} 35 |
36 |
37 | Builder 39 |
40 |
41 |
42 |
43 | {% endfor %} 44 |
45 |
46 | 47 | 76 |
77 | 78 | {% endblock %} -------------------------------------------------------------------------------- /templates/resumes/user_resumes.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block title %} 6 | My resumes 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

Your Resumes

14 | Create New Resume 15 |
16 |
17 |
18 | {% for resume in resumes %} 19 |
20 |
21 | ... 22 |
23 |
{{ resume.name }}
24 | Edit 25 |
26 |
27 |
28 | {% endfor %} 29 |
30 |
31 | {% endblock %} -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tests/__init__.py -------------------------------------------------------------------------------- /tests/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tests/accounts/__init__.py -------------------------------------------------------------------------------- /tests/accounts/test_api_views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase, APIClient 4 | from django.contrib.auth import get_user_model 5 | 6 | User = get_user_model() 7 | 8 | 9 | class RegistrationAPITestCase(APITestCase): 10 | @classmethod 11 | def setUpTestData(cls): 12 | cls.invalid_data = { 13 | "email": "invalid-email", 14 | "password": "short", # too short 15 | "username": "test", 16 | } 17 | 18 | def setUp(self): 19 | self.url = reverse("accounts.api:register") 20 | self.valid_data = { 21 | "email": "test@example.com", 22 | "password": "testpass123", 23 | "password2": "testpass123", 24 | "gender": "male", 25 | "role": "employee", 26 | } 27 | 28 | def test_registration_success(self): 29 | response = self.client.post(self.url, self.valid_data, format="json") 30 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 31 | self.assertTrue(response.data["status"]) 32 | self.assertEqual(response.data["message"], "Successfully registered") 33 | 34 | # Verify user was created 35 | self.assertTrue(User.objects.filter(email=self.valid_data["email"]).exists()) 36 | 37 | def test_registration_invalid_data(self): 38 | response = self.client.post(self.url, self.invalid_data, format="json") 39 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 40 | 41 | def test_registration_duplicate_email(self): 42 | # Create a user with the same email 43 | User.objects.create_user(email=self.valid_data["email"], password="testpass123", role="employee") 44 | response = self.client.post(self.url, self.valid_data, format="json") 45 | """ 46 | response 47 | { 48 | "status": False, 49 | "message": "A user with that email already exists. ", 50 | "errors": { 51 | "email": ["A user with that email already exists."] 52 | } 53 | } 54 | """ 55 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 56 | self.assertEqual(response.data["errors"][0]["email"], "A user with that email already exists.") 57 | self.assertFalse(response.data["status"]) 58 | self.assertEqual(response.data["message"], "A user with that email already exists. ") 59 | 60 | def tearDown(self): 61 | # Clean up created data 62 | User.objects.all().delete() 63 | 64 | 65 | class EditEmployeeProfileAPITestCase(APITestCase): 66 | def setUp(self): 67 | self.user = User.objects.create_user(email="test@example.com", password="testpass123", role="employee") 68 | self.client = APIClient() 69 | self.client.force_authenticate(user=self.user) 70 | self.url = reverse("accounts.api:employee-profile") 71 | self.valid_data = { 72 | "first_name": "John", 73 | "last_name": "Doe", 74 | "email": "john@example.com", 75 | } 76 | 77 | def test_get_profile(self): 78 | response = self.client.get(self.url) 79 | self.assertEqual(response.status_code, status.HTTP_200_OK) 80 | self.assertEqual(response.data["email"], self.user.email) 81 | self.assertEqual(response.data["role"], self.user.role) 82 | 83 | def test_update_profile(self): 84 | response = self.client.put(self.url, self.valid_data, format="json") 85 | self.assertEqual(response.status_code, status.HTTP_200_OK) 86 | self.user.refresh_from_db() 87 | self.assertEqual(self.user.first_name, self.valid_data["first_name"]) 88 | self.assertEqual(self.user.last_name, self.valid_data["last_name"]) 89 | self.assertEqual(self.user.email, self.valid_data["email"]) 90 | 91 | def test_unauthorized_access(self): 92 | self.client.force_authenticate(user=None) 93 | response = self.client.get(self.url) 94 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 95 | -------------------------------------------------------------------------------- /tests/accounts/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from accounts.forms import EmployeeRegistrationForm, EmployerRegistrationForm 4 | from accounts.models import User 5 | 6 | 7 | class TestEmployeeRegistrationForm(TestCase): 8 | fixtures = ["accounts_initial_data.json"] 9 | 10 | def setUp(self) -> None: 11 | self.valid_user = { 12 | "first_name": "Manjurul", 13 | "last_name": "Hoque", 14 | "role": "employee", 15 | "gender": "male", 16 | "email": "rumi1@gmail.com", 17 | "password1": "123456", 18 | "password2": "123456", 19 | } 20 | 21 | def test_field_required(self): 22 | form = EmployeeRegistrationForm(data={}) 23 | 24 | self.assertEqual(form.errors["gender"], ["Gender is required"]) 25 | self.assertEqual(form.errors["email"], ["This field is required."]) 26 | self.assertEqual(form.errors["password1"], ["This field is required."]) 27 | self.assertEqual(form.errors["password2"], ["This field is required."]) 28 | 29 | def test_employee_registration_form_valid(self): 30 | form = EmployeeRegistrationForm(data=self.valid_user) 31 | self.assertEqual(True, form.is_valid(), "Invalid form") 32 | 33 | def test_invalid_email(self): 34 | data = self.valid_user 35 | data["email"] = "test" 36 | form = EmployeeRegistrationForm(data=data) 37 | self.assertFalse(form.is_valid(), "Invalid email") 38 | 39 | def test_too_short_password(self): 40 | data = self.valid_user 41 | data["password1"] = "test" 42 | form = EmployeeRegistrationForm(data=data) 43 | self.assertFalse(form.is_valid()) 44 | 45 | def test_meta_data(self): 46 | self.assertEqual(EmployeeRegistrationForm._meta.model, User) 47 | 48 | expected_fields = ["first_name", "last_name", "email", "password1", "password2", "gender"] 49 | for field in expected_fields: 50 | self.assertIn(field, EmployeeRegistrationForm._meta.fields) 51 | 52 | def test_password_mismatch(self): 53 | # Set confirm password field to a different value 54 | data = self.valid_user 55 | data["password2"] = "54321" 56 | 57 | form = EmployeeRegistrationForm(data) 58 | 59 | self.assertFalse(form.is_valid()) 60 | self.assertEqual(form.errors["password2"][0], "The two password fields didn’t match.") 61 | 62 | def test_valid_and_save_form(self): 63 | form = EmployeeRegistrationForm(data=self.valid_user) 64 | form.is_valid() 65 | user = form.save() 66 | self.assertIsInstance(user, User, "Not an user") 67 | 68 | 69 | class TestEmployerRegistrationForm(TestCase): 70 | fixtures = ["accounts_initial_data.json"] 71 | 72 | def setUp(self) -> None: 73 | self.valid_user = { 74 | "first_name": "John", 75 | "last_name": "Doe", 76 | "email": "employer@gmail.com", 77 | "password1": "123456", 78 | "password2": "123456", 79 | } 80 | 81 | def test_field_required(self): 82 | form = EmployerRegistrationForm(data={}) 83 | 84 | self.assertEqual(form.errors["email"], ["This field is required."]) 85 | self.assertEqual(form.errors["password1"], ["This field is required."]) 86 | self.assertEqual(form.errors["password2"], ["This field is required."]) 87 | 88 | def test_employee_registration_form_valid(self): 89 | form = EmployerRegistrationForm(data=self.valid_user) 90 | self.assertEqual(True, form.is_valid(), "Invalid form") 91 | 92 | def test_invalid_email(self): 93 | data = self.valid_user 94 | data["email"] = "test" 95 | form = EmployerRegistrationForm(data=data) 96 | self.assertFalse(form.is_valid(), "Invalid email") 97 | 98 | def test_too_short_password(self): 99 | data = self.valid_user 100 | data["password1"] = "test" 101 | form = EmployeeRegistrationForm(data=data) 102 | self.assertFalse(form.is_valid()) 103 | 104 | def test_meta_data(self): 105 | self.assertEqual(EmployerRegistrationForm._meta.model, User) 106 | 107 | expected_fields = ["first_name", "last_name", "email", "password1", "password2"] 108 | for field in expected_fields: 109 | self.assertIn(field, EmployeeRegistrationForm._meta.fields) 110 | 111 | def test_password_mismatch(self): 112 | # Set confirm password field to a different value 113 | data = self.valid_user 114 | data["password2"] = "54321" 115 | 116 | form = EmployerRegistrationForm(data) 117 | 118 | self.assertFalse(form.is_valid()) 119 | self.assertEqual(form.errors["password2"][0], "The two password fields didn’t match.") 120 | 121 | def test_valid_and_save_form(self): 122 | form = EmployerRegistrationForm(data=self.valid_user) 123 | form.is_valid() 124 | user = form.save() 125 | self.assertIsInstance(user, User, "Not an user") 126 | -------------------------------------------------------------------------------- /tests/accounts/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from accounts.models import User 4 | 5 | 6 | class TestUserModel(TestCase): 7 | def setUp(self) -> None: 8 | self.valid_user = { 9 | "first_name": "Manjurul", 10 | "last_name": "Hoque", 11 | "role": "employee", 12 | "gender": "male", 13 | "email": "rumi1@gmail.com", 14 | "password": "123456", 15 | } 16 | self.user = User.objects.create(**self.valid_user) 17 | 18 | def test_string_representation(self): 19 | self.assertEqual(str(self.user), self.user.email) 20 | 21 | def test_verbose_name_plural(self): 22 | self.assertEqual(str(User._meta.verbose_name_plural), "users") 23 | 24 | def test_full_name(self): 25 | self.assertEqual(self.user.get_full_name(), "Manjurul Hoque") 26 | 27 | def test_email_label(self): 28 | user = User.objects.get(id=1) 29 | field_label = user._meta.get_field("email").verbose_name 30 | self.assertEqual(field_label, "email") 31 | -------------------------------------------------------------------------------- /tests/accounts/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from accounts.api.serializers import UserSerializer, UserCreateSerializer, SocialSerializer 4 | from accounts.models import User 5 | 6 | 7 | class TestUserSerializer(TestCase): 8 | def setUp(self): 9 | self.user_data = {"email": "test@example.com", "password": "testpass123", "role": "employee"} 10 | self.user = User.objects.create_user(**self.user_data) 11 | 12 | def test_user_serializer(self): 13 | serializer = UserSerializer(self.user) 14 | self.assertEqual(serializer.data["email"], "test@example.com") 15 | self.assertEqual(serializer.data["role"], "employee") 16 | # Verify excluded fields are not in serialized data 17 | self.assertNotIn("password", serializer.data) 18 | self.assertNotIn("is_staff", serializer.data) 19 | 20 | def test_user_serializer_with_partial_update(self): 21 | update_data = {"role": "employer"} 22 | serializer = UserSerializer(self.user, data=update_data, partial=True) 23 | self.assertTrue(serializer.is_valid()) 24 | updated_user = serializer.save() 25 | self.assertEqual(updated_user.role, "employer") 26 | 27 | 28 | class TestUserCreateSerializer(TestCase): 29 | def setUp(self): 30 | self.valid_payload = { 31 | "email": "newuser@example.com", 32 | "password": "testpass123", 33 | "password2": "testpass123", 34 | "gender": "male", 35 | "role": "employee", 36 | } 37 | 38 | def test_create_user_with_valid_data(self): 39 | serializer = UserCreateSerializer(data=self.valid_payload) 40 | self.assertTrue(serializer.is_valid()) 41 | user = serializer.save() 42 | self.assertEqual(user.email, self.valid_payload["email"]) 43 | self.assertEqual(user.role, self.valid_payload["role"]) 44 | self.assertTrue(user.check_password(self.valid_payload["password"])) 45 | 46 | def test_create_user_with_mismatched_passwords(self): 47 | invalid_payload = self.valid_payload.copy() 48 | invalid_payload["password2"] = "differentpass" 49 | serializer = UserCreateSerializer(data=invalid_payload) 50 | self.assertFalse(serializer.is_valid()) 51 | self.assertIn("password2", serializer.errors) 52 | 53 | def test_create_user_with_existing_email(self): 54 | # Create a user first 55 | User.objects.create_user(email=self.valid_payload["email"], password="somepass", role="employee") 56 | # Try to create another user with same email 57 | serializer = UserCreateSerializer(data=self.valid_payload) 58 | self.assertFalse(serializer.is_valid()) 59 | self.assertIn("email", serializer.errors) 60 | -------------------------------------------------------------------------------- /tests/accounts/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | from django.utils import translation 4 | 5 | from accounts.models import User 6 | 7 | 8 | class BaseTest(TestCase): 9 | def setUp(self) -> None: 10 | self.language_code = translation.get_language() 11 | self.user = User.objects.create_user(password="Abcdefgh.1", email="test@test.com") 12 | 13 | 14 | class TestLoginView(BaseTest): 15 | def setUp(self) -> None: 16 | super(TestLoginView, self).setUp() 17 | self.response = self.client.get(reverse("accounts:login")) 18 | 19 | def test_csrf(self): 20 | self.assertTemplateUsed(self.response, "accounts/login.html") 21 | self.assertContains(self.response, "csrfmiddlewaretoken") 22 | 23 | def test_redirect_if_authenticated(self): 24 | self.client.login(email="test@test.com", password="Abcdefgh.1") 25 | response = self.client.get(reverse("accounts:login")) 26 | self.assertURLEqual(reverse("jobs:home"), "/" + self.language_code + response.url) 27 | self.client.logout() 28 | 29 | def test_submit_form(self): 30 | response = self.client.post(reverse("accounts:login"), {"email": "test@test.com", "password": "Abcdefgh.1"}) 31 | self.assertURLEqual(reverse("jobs:home"), "/" + self.language_code + response.url) 32 | 33 | 34 | class TestLogoutView(BaseTest): 35 | def setUp(self) -> None: 36 | super(TestLogoutView, self).setUp() 37 | self.client.login(email="test@test.com", password="Abcdefgh.1") 38 | 39 | def test_redirect_after_logout(self): 40 | response = self.client.get(reverse("accounts:logout")) 41 | self.assertEqual(response.status_code, 302) 42 | self.assertURLEqual(reverse("accounts:login"), "/" + self.language_code + response.url + "/") 43 | -------------------------------------------------------------------------------- /tests/factories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tests/factories/__init__.py -------------------------------------------------------------------------------- /tests/factories/category_factory.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from categories.models import Category 3 | from faker import Faker 4 | from django.utils.text import slugify 5 | 6 | faker = Faker() 7 | 8 | 9 | class CategoryFactory(factory.django.DjangoModelFactory): 10 | class Meta: 11 | model = Category 12 | 13 | name = factory.Sequence(lambda n: f"{faker.word()}-{n + 1}") 14 | slug = factory.LazyAttribute(lambda obj: slugify(obj.name)) 15 | description = faker.sentence() 16 | -------------------------------------------------------------------------------- /tests/factories/job_factory.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.utils import timezone 3 | from datetime import timedelta 4 | import random 5 | 6 | from jobsapp.models import Job, JOB_TYPE 7 | from tests.factories.category_factory import CategoryFactory 8 | from tests.factories.user_factory import UserFactory 9 | 10 | 11 | class JobFactory(factory.django.DjangoModelFactory): 12 | class Meta: 13 | model = Job 14 | 15 | title = factory.Sequence(lambda n: f"Job {n}") 16 | description = factory.Faker("sentence") 17 | location = factory.Faker("city") 18 | salary = factory.Faker("random_int", min=1000, max=100000) 19 | type = factory.Faker("random_element", elements=JOB_TYPE) 20 | # last_date = factory.Faker("date_between", start_date="-30d", end_date="now") 21 | last_date = factory.LazyFunction(lambda: timezone.now() + timedelta(days=random.randint(1, 30))) 22 | category = factory.SubFactory(CategoryFactory) 23 | user = factory.SubFactory(UserFactory) 24 | -------------------------------------------------------------------------------- /tests/factories/tag_factory.py: -------------------------------------------------------------------------------- 1 | from factory import Faker 2 | from factory.django import DjangoModelFactory 3 | 4 | from tags.models import Tag 5 | 6 | 7 | class TagFactory(DjangoModelFactory): 8 | class Meta: 9 | model = Tag 10 | 11 | name = Faker("word") 12 | -------------------------------------------------------------------------------- /tests/factories/user_factory.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.contrib.auth import get_user_model 3 | 4 | User = get_user_model() 5 | 6 | 7 | class UserFactory(factory.django.DjangoModelFactory): 8 | class Meta: 9 | model = User 10 | 11 | email = factory.Sequence(lambda n: f"user_{n + 1}@example.com") 12 | password = factory.PostGenerationMethodCall("set_password", "password") 13 | -------------------------------------------------------------------------------- /tests/jobsapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tests/jobsapp/__init__.py -------------------------------------------------------------------------------- /tests/jobsapp/test_forms.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.test import TestCase 4 | from django.utils import timezone 5 | 6 | from accounts.models import User 7 | from jobsapp.forms import CreateJobForm 8 | from jobsapp.models import Job 9 | from tags.models import Tag 10 | 11 | 12 | class TestCreateJobForm(TestCase): 13 | def setUp(self) -> None: 14 | self.valid_job = { 15 | "title": "Junior Software Engineer", 16 | "description": "Looking for Python developer", 17 | "vacancy": 2, 18 | "salary": 35000, 19 | "location": "Dhaka, Bangladesh", 20 | "type": "1", 21 | "category": "web-development", 22 | "last_date": timezone.now() + timedelta(days=30), 23 | "company_name": "Dev Soft", 24 | "company_description": "A foreign country", 25 | "website": "www.devsoft.com", 26 | "tags": [Tag.objects.create(name="Development").id], 27 | } 28 | self.employer = { 29 | "first_name": "John", 30 | "last_name": "Doe", 31 | "email": "employer@gmail.com", 32 | "role": "employer", 33 | "password": "123456", 34 | } 35 | self.user = User.objects.create(**self.employer) 36 | 37 | def test_valid_and_save_form(self): 38 | form = CreateJobForm(data=self.valid_job) 39 | valid = form.is_valid() 40 | self.assertTrue(valid) 41 | 42 | job = form.save(commit=False) 43 | job.user = self.user 44 | job.save() 45 | 46 | self.assertIsInstance(job, Job, "Not a job") 47 | 48 | def test_field_required(self): 49 | form = CreateJobForm(data={}) 50 | form.is_valid() 51 | 52 | self.assertEqual(form.errors["title"], ["This field is required."]) 53 | -------------------------------------------------------------------------------- /tests/jobsapp/test_models.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.test import TestCase 4 | from django.utils import translation, timezone 5 | 6 | from accounts.models import User 7 | from jobsapp.models import Applicant, Job 8 | 9 | 10 | class BaseTest(TestCase): 11 | def setUp(self) -> None: 12 | self.language_code = translation.get_language() 13 | 14 | @classmethod 15 | def setUpTestData(cls) -> None: 16 | cls.valid_job = { 17 | "title": "Junior Software Engineer", 18 | "description": "Looking for Python developer", 19 | "salary": 35000, 20 | "location": "Dhaka, Bangladesh", 21 | "type": "1", 22 | "category": "web-development", 23 | "last_date": timezone.now() + timedelta(days=30), 24 | "company_name": "Dev Soft", 25 | "company_description": "A foreign country", 26 | "website": "www.devsoft.com", 27 | } 28 | cls.employer = { 29 | "first_name": "John", 30 | "last_name": "Doe", 31 | "email": "employer@gmail.com", 32 | "role": "employer", 33 | "password": "123456", 34 | } 35 | cls.user = User.objects.create(**cls.employer) 36 | cls.job = Job(**cls.valid_job) 37 | cls.job.user = cls.user 38 | cls.job.save() 39 | 40 | 41 | class TestJobModel(BaseTest): 42 | def test_get_absolute_url(self): 43 | self.assertURLEqual(self.job.get_absolute_url(), f"/{self.language_code}/jobs/1/") 44 | 45 | def test_title_max_length(self): 46 | max_length = self.job._meta.get_field("title").max_length 47 | self.assertEqual(max_length, 300) 48 | 49 | def test_title_label(self): 50 | field_label = self.job._meta.get_field("title").verbose_name 51 | self.assertEqual(field_label, "title") 52 | 53 | 54 | class TestApplicantModel(BaseTest): 55 | @classmethod 56 | def setUpTestData(cls) -> None: 57 | super(TestApplicantModel, cls).setUpTestData() 58 | cls.applicant = Applicant.objects.create(user=cls.user, job=cls.job) 59 | 60 | def test_str(self): 61 | self.assertEqual(self.applicant.__str__(), self.user.get_full_name()) 62 | -------------------------------------------------------------------------------- /tests/jobsapp/test_serializers.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from jobsapp.api.serializers import JobSerializer, NewJobSerializer 3 | from django.utils import timezone 4 | from datetime import timedelta 5 | from jobsapp.models import Job 6 | from accounts.models import User 7 | from tags.models import Tag 8 | from django.forms.models import model_to_dict 9 | 10 | 11 | class TestJobSerializer(TestCase): 12 | """ 13 | Unit tests for the JobSerializer 14 | """ 15 | 16 | def setUp(self): 17 | self.job_data = { 18 | "title": "Test Job", 19 | "description": "Test Description", 20 | "location": "Test Location", 21 | "salary": 100000, 22 | "created_at": timezone.now(), 23 | "last_date": timezone.now() + timedelta(days=30), 24 | "user": User.objects.create_user( 25 | email="testuser@example.com", 26 | password="testpassword", 27 | ), 28 | } 29 | 30 | def test_job_serializer(self): 31 | """ 32 | Test the JobSerializer 33 | """ 34 | job = Job.objects.create(**self.job_data) 35 | serializer = JobSerializer(job) 36 | self.assertEqual(serializer.data["title"], "Test Job") 37 | self.assertEqual(serializer.data["description"], "Test Description") 38 | self.assertEqual(serializer.data["location"], "Test Location") 39 | self.assertEqual(serializer.data["salary"], 100000) 40 | # Format the datetime to match DRF's UTC format 41 | expected_datetime = self.job_data["created_at"].strftime("%Y-%m-%dT%H:%M:%S.%fZ") 42 | self.assertEqual(serializer.data["created_at"], expected_datetime) 43 | 44 | 45 | class TestNewJobSerializer(TestCase): 46 | """ 47 | Unit tests for the NewJobSerializer 48 | """ 49 | 50 | def setUp(self): 51 | self.user = User.objects.create_user( 52 | email="employer1@example.com", 53 | password="testpassword", 54 | role="employer", 55 | ) 56 | self.tag = Tag.objects.create(name="Python") 57 | self.job_data = { 58 | "title": "Test Job", 59 | "description": "Test Description", 60 | "location": "Test Location", 61 | "salary": 100000, 62 | "created_at": timezone.now(), 63 | "last_date": timezone.now() + timedelta(days=30), 64 | "type": "1", # Full time 65 | "category": "web-development", 66 | "company_name": "Test Company", 67 | "company_description": "A great company to work for", 68 | "website": "www.testcompany.com", 69 | "tags": [self.tag.id], 70 | "user": self.user.id, 71 | } 72 | 73 | def tearDown(self): 74 | self.user.delete() 75 | self.tag.delete() 76 | 77 | def test_new_job_serializer(self): 78 | # Create serializer with context 79 | # serializer = NewJobSerializer(data=self.job_data, context={'request': type('Request', (), {'user': self.user})()}) 80 | serializer = NewJobSerializer(data=self.job_data) 81 | self.assertTrue(serializer.is_valid(), serializer.errors) 82 | job = serializer.save() 83 | self.assertEqual(job.title, self.job_data["title"]) 84 | self.assertEqual(job.description, self.job_data["description"]) 85 | self.assertEqual(job.location, self.job_data["location"]) 86 | self.assertEqual(job.salary, self.job_data["salary"]) 87 | self.assertEqual(job.type, self.job_data["type"]) 88 | self.assertEqual(job.category, self.job_data["category"]) 89 | self.assertEqual(job.company_name, self.job_data["company_name"]) 90 | self.assertEqual(job.company_description, self.job_data["company_description"]) 91 | self.assertEqual(job.website, self.job_data["website"]) 92 | self.assertEqual(list(job.tags.values_list("id", flat=True)), self.job_data["tags"]) 93 | self.assertEqual(job.user, self.user) 94 | 95 | def test_new_job_with_logged_in_user(self): 96 | self.client.force_login(self.user) 97 | serializer = NewJobSerializer( 98 | data=self.job_data, 99 | context={"request": type("Request", (), {"user": self.user})()}, 100 | ) 101 | self.assertTrue(serializer.is_valid(), serializer.errors) 102 | job = serializer.save() 103 | self.assertEqual(job.user, self.user) 104 | -------------------------------------------------------------------------------- /tests/jobsapp/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from jobsapp.models import Job 5 | 6 | 7 | class TestHomeView(TestCase): 8 | def test_context(self): 9 | response = self.client.get(reverse("jobs:home")) 10 | self.assertGreaterEqual(len(response.context["jobs"]), 0) 11 | 12 | def test_template_used(self): 13 | response = self.client.get(reverse("jobs:home")) 14 | self.assertEqual(response.status_code, 200) 15 | self.assertTemplateUsed(response, "home.html") 16 | 17 | 18 | class TestSearchView(TestCase): 19 | def setUp(self): 20 | self.url = reverse("jobs:search") 21 | super().setUp() 22 | 23 | def test_empty_query(self): 24 | jobs = Job.objects.filter(title__contains="software") 25 | response = self.client.get(self.url + "?position=software") 26 | self.assertFalse(b"We have found %a jobs" % str(jobs.count()) in response.content.lower()) 27 | 28 | 29 | class TestJobDetailsView(TestCase): 30 | def test_details(self): 31 | response = self.client.get(reverse("jobs:jobs-detail", args=(1,))) 32 | self.assertEqual(response.status_code, 404) 33 | -------------------------------------------------------------------------------- /tests/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/tests/tags/__init__.py -------------------------------------------------------------------------------- /tests/tags/test_api_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.test import APITestCase 3 | from django.urls import reverse 4 | from tests.factories.tag_factory import TagFactory 5 | 6 | 7 | class TagListAPIViewTestCase(APITestCase): 8 | def setUp(self): 9 | TagFactory.create_batch(3) 10 | 11 | def test_tag_list_api_view(self): 12 | """Test the tag list API endpoint""" 13 | url = reverse("tags-api:tag-list") 14 | response = self.client.get(url) 15 | self.assertEqual(response.status_code, status.HTTP_200_OK) 16 | self.assertEqual(len(response.json()), 3) 17 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manjurulhoque/django-job-portal/22a1e464de6d88d694fc8d7e62562c2666c47c6b/utils/__init__.py -------------------------------------------------------------------------------- /utils/filename.py: -------------------------------------------------------------------------------- 1 | import string 2 | from random import choice 3 | 4 | 5 | def generate_file_name(length=30): 6 | letters = string.ascii_letters + string.digits 7 | return "".join(choice(letters) for _ in range(length)) 8 | -------------------------------------------------------------------------------- /utils/namedtuples.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Optional 2 | 3 | 4 | class Checking(NamedTuple): 5 | passed: bool = True 6 | message: Optional[str] = None 7 | params: dict = dict() 8 | --------------------------------------------------------------------------------