commit 52233d002892841cf0f69e7ec314027792123df1 Author: Data-Name-ID Date: Tue Apr 2 00:51:49 2024 +0300 [init] project diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100755 index 0000000..5c12f8a --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,63 @@ +name: Django CI/CD + +on: [push, pull_request] + +jobs: + migrations: + runs-on: ubuntu-latest + + env: + DJANGO_DEBUG: True + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install production dependencies + run: pip install -r backend/requirements/prod.txt + - name: Check for pending migrations + run: cd backend/project && python manage.py makemigrations --check --dry-run + + linting: + runs-on: ubuntu-latest + needs: migrations + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install linting dependencies + run: pip install -r backend/requirements/dev.txt + - name: Lint with ruff + run: cd backend && ruff check . + + testing: + runs-on: self-hosted + needs: linting + + env: + DJANGO_DEBUG: True + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install test dependencies + run: pip install -r backend/requirements/test.txt + - name: + run: cd backend/project && python manage.py test + + test_build: + runs-on: ubuntu-latest + needs: testing + + steps: + - uses: actions/checkout@v3 + - name: Build Docker image + run: docker build -t skillhub_backend backend/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d93590d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,18 @@ +name: Deployment + +on: + workflow_run: + workflows: ["Frontend CI/CD"] + types: + - completed + +jobs: + deploy: + runs-on: self-hosted + if: ${{ github.event.workflow_run.conclusion == 'success' }} && ${{ github.ref == 'refs/heads/main' }} + + steps: + - uses: actions/checkout@v3 + - name: Pull Docker images and start containers + run: | + sudo docker compose up -d --build diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 0000000..670ad9f --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,32 @@ +name: Frontend CI/CD + +on: + workflow_run: + workflows: ["Django CI/CD"] + types: + - completed + +jobs: + linting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + - name: Install dependencies + run: cd frontend && npm install + - name: Linting + run: cd frontend && npm run lint + continue-on-error: true + + test_build: + runs-on: ubuntu-latest + needs: linting + + steps: + - uses: actions/checkout@v3 + - name: Build Docker image + run: docker build -t skillhub-backend frontend/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1345501 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# SkillHub diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..93d78d9 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +# Folders +venv/ +__pycache__/ diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100755 index 0000000..2156ea5 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,165 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +*.sqlite3 +*.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Django stuff +cache +media +static/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..30b3d00 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements/prod.txt . + +RUN pip install --no-cache-dir -r prod.txt + +COPY . . diff --git a/backend/Makefile b/backend/Makefile new file mode 100755 index 0000000..a756af9 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,33 @@ +dump: + @cd project && python -Xutf8 manage.py dumpdata --format json --indent 4 -o fixtures/data.json + +load: + @cd project && python -Xutf8 manage.py loaddata fixtures/data.json + +mig: + @cd project && python manage.py makemigrations + @cd project && python manage.py migrate + +check: test + @ruff check --fix + +test: + @cd project && python manage.py test + +run: + @cd project && python manage.py runserver + +su: + @cd project && python manage.py createsuperuser + +loc-m: + @cd project && django-admin makemessages -l ru -l en + +loc-c: + @cd project && django-admin compilemessages + +help: + @cd project && python manage.py help + +sort: + sort-requirements requirements/prod.txt requirements/test.txt requirements/dev.txt diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..4687b02 --- /dev/null +++ b/backend/README.md @@ -0,0 +1 @@ +# SkillHub Backend diff --git a/backend/project/api/__init__.py b/backend/project/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/project/api/apps.py b/backend/project/api/apps.py new file mode 100644 index 0000000..878e7d5 --- /dev/null +++ b/backend/project/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/backend/project/api/urls.py b/backend/project/api/urls.py new file mode 100644 index 0000000..5826b60 --- /dev/null +++ b/backend/project/api/urls.py @@ -0,0 +1,3 @@ +from django.urls import include, path + +urlpatterns = [] diff --git a/backend/project/config/__init__.py b/backend/project/config/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/backend/project/config/asgi.py b/backend/project/config/asgi.py new file mode 100755 index 0000000..8d8a76c --- /dev/null +++ b/backend/project/config/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/backend/project/config/settings.py b/backend/project/config/settings.py new file mode 100755 index 0000000..6110d21 --- /dev/null +++ b/backend/project/config/settings.py @@ -0,0 +1,148 @@ +import sys +from pathlib import Path + +from environs import Env + +BASE_DIR = Path(__file__).resolve().parent.parent + +env = Env() +env.read_env(BASE_DIR.parent / ".env") + +DEFAULT_HOSTS = ["127.0.0.1", "localhost"] + +with env.prefixed("DJANGO_"): + SECRET_KEY = env("SECRET_KEY", "change-me") + DEBUG = env.bool("DEBUG", False) + ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", DEFAULT_HOSTS) + INTERNAL_IPS = env.list("INTERNAL_IPS", ALLOWED_HOSTS) + CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", []) + DB_NAME = env("DB_NAME", "db.sqlite3") + +with env.prefixed("POSTGRES_"): + if not DEBUG: + POSTGRES_DB = env("DB", "postgres") + POSTGRES_USER = env("USER", "postgres") + POSTGRES_PASSWORD = env("PASSWORD", "postgres") + POSTGRES_HOST = env("HOST", "localhost") + POSTGRES_PORT = env("PORT", "5432") + + +TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" +MIGRATING = len(sys.argv) > 1 and ( + "migrate" in sys.argv[1] or "makemigrations" in sys.argv[1] +) + + +INSTALLED_APPS = [ + # Built-in apps + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # Third-party apps + "rest_framework", + "corsheaders", + "drf_yasg", + # Developed apps +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES_DIR = BASE_DIR / "templates" +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [TEMPLATES_DIR], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + +if DEBUG: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / DB_NAME, + }, + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": POSTGRES_DB, + "USER": POSTGRES_USER, + "PASSWORD": POSTGRES_PASSWORD, + "HOST": POSTGRES_HOST, + "PORT": POSTGRES_PORT, + }, + } + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": ( + "django.contrib.auth.password_validation" + ".UserAttributeSimilarityValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.MinimumLengthValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation" + ".CommonPasswordValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation" + ".NumericPasswordValidator" + ), + }, +] + +USE_I18N = True +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" +USE_TZ = True + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "static" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_FILTER_BACKENDS": [ + "django_filters.rest_framework.DjangoFilterBackend" + ], +} + +if DEBUG and not (TESTING or MIGRATING): + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/backend/project/config/urls.py b/backend/project/config/urls.py new file mode 100755 index 0000000..2ca14e4 --- /dev/null +++ b/backend/project/config/urls.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.contrib import admin +from django.urls import include, path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view + +schema_view = get_schema_view( + openapi.Info(title="SkillHub API", default_version="1") +) + +urlpatterns = [ + # Built-in urls + path("admin/", admin.site.urls), + # API documentation + path( + "swagger/", + schema_view.with_ui("swagger"), + name="swagger", + ), + path( + "redoc/", + schema_view.with_ui("redoc"), + name="redoc", + ), + # API + path("api/", include("api.urls")), +] + +if settings.DEBUG and not (settings.TESTING or settings.MIGRATING): + urlpatterns += (path("__debug__/", include("debug_toolbar.urls")),) diff --git a/backend/project/config/wsgi.py b/backend/project/config/wsgi.py new file mode 100755 index 0000000..5b3778a --- /dev/null +++ b/backend/project/config/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/backend/project/manage.py b/backend/project/manage.py new file mode 100755 index 0000000..2ced894 --- /dev/null +++ b/backend/project/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + msg = ( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise ImportError(msg) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100755 index 0000000..349c235 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,10 @@ +[tool.ruff] +line-length = 79 +exclude = ["migrations"] + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["D", "ANN", "EXE002", "RUF012", "RUF001", "COM812", "ISC001"] + +[tool.ruff.format] +skip-magic-trailing-comma = false diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt new file mode 100644 index 0000000..edc67e4 --- /dev/null +++ b/backend/requirements/dev.txt @@ -0,0 +1,4 @@ +django-debug-toolbar==4.3.0 + +-r test.txt +-r lint.txt diff --git a/backend/requirements/lint.txt b/backend/requirements/lint.txt new file mode 100644 index 0000000..40b7f22 --- /dev/null +++ b/backend/requirements/lint.txt @@ -0,0 +1,2 @@ +sort-requirements +ruff diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt new file mode 100644 index 0000000..87e90ed --- /dev/null +++ b/backend/requirements/prod.txt @@ -0,0 +1,10 @@ +django==4.2.11 +environs==11.0.0 +gunicorn==21.2.0 +psycopg2-binary==2.9.9 +djangorestframework==3.15.1 +django-filter==24.2 +Pillow==10.2.0 +drf-yasg==1.21.7 +django-cors-headers +setuptools diff --git a/backend/requirements/test.txt b/backend/requirements/test.txt new file mode 100644 index 0000000..ca0c6f8 --- /dev/null +++ b/backend/requirements/test.txt @@ -0,0 +1 @@ +-r prod.txt diff --git a/backend/template.env b/backend/template.env new file mode 100755 index 0000000..b161781 --- /dev/null +++ b/backend/template.env @@ -0,0 +1,18 @@ +# For django app + +DJANGO_SECRET_KEY = your-secret-key +DJANGO_DEBUG = False +DJANGO_ALLOWED_HOSTS = 127.0.0.1,localhost +DJANGO_INTERNAL_IPS = 127.0.0.1,localhost +DJANGO_CSRF_TRUSTED_ORIGINS = http://127.0.0.1:8000,localhost + +# For docker(remove if you want to keep defaults) + +POSTGRES_PORT = # default: 5432 +POSTGRES_DB = # default: postgres +POSTGRES_USER = # default: postgres +POSTGRES_PASSWORD = # default: postgres + +PGADMIN_PORT = # default: 5050 +PGADMIN_EMAIL = # default: admin@mail.com +PGADMIN_PASSWORD = # default: admin diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0e9dbac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +name: skillhub + +services: + postgres: + image: postgres:16.2-alpine + container_name: postgres + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + environment: + POSTGRES_DB: ${POSTGRES_DB:-postgres} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + backend: + build: ./backend + container_name: backend + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + expose: + - 8000 + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + POSTGRES_DB: ${POSTGRES_DB:-postgres} + POSTGRES_HOST: postgres + + DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-secret_key} + DJANGO_DEBUG: ${DJANGO_DEBUG:-false} + DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-*} + DJANGO_INTERNAL_IPS: ${DJANGO_INTERNAL_IPS:-127.0.0.1} + command: ["sh", "-c", "cd project && python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8080"] + ports: + - 8080:8080 + + frontend: + container_name: frontend + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:3000" + restart: always + + pgadmin: + image: dpage/pgadmin4:8.4 + container_name: pgadmin + depends_on: + postgres: + condition: service_healthy + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@mail.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin} + ports: + - "${PGADMIN_PORT:-5050}:80" + restart: always + volumes: + - pgadmin_data:/var/lib/pgadmin + +volumes: + postgres_data: + redis_data: + pgadmin_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..9414382 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5ce410b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,19 @@ +FROM node:lts-alpine3.19 AS builder + +ENV NODE_ENV production + +WORKDIR /app + +COPY ./package.json ./ + +RUN npm install + +COPY . . + +RUN npm run build + +FROM nginx:stable-alpine3.17-slim + +COPY --from=builder /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..183d8c8 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1 @@ +# SkillHub Frontend