├── .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 |
4 |
5 | # Django Job Portal
6 |
7 |
8 |
9 | ## Django Job Portal
10 |
11 | #### An open source online job portal.
12 |
13 |
14 |
15 |
16 |
17 |
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 |
38 |
39 | ## Add new position as employer
40 |
41 |
42 | ## Job details
43 |
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 | 
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 |
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 |
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 |
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 |
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 |
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 |
20 |
21 |
22 |
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 |
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 |
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 |
6 |
8 |
9 | {% trans "Language" %}
10 |
11 |
12 |
16 |
17 |
24 |
--------------------------------------------------------------------------------
/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 |
40 |
41 |
42 |
46 |
47 | {% for template in templates %}
48 |
49 |
50 |
52 |
53 |
{{ template.name }}
54 |
55 |
56 |
57 | {% endfor %}
58 |
59 |
60 |
61 |
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 |
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 |
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 |
--------------------------------------------------------------------------------