diff --git a/deployment/scripts/backend/start.sh b/deployment/scripts/backend/start.sh index 74df41f..a29740b 100644 --- a/deployment/scripts/backend/start.sh +++ b/deployment/scripts/backend/start.sh @@ -4,10 +4,10 @@ if [ "$APP_ENV" != "prod" ]; then python manage.py makemigrations --noinput python manage.py migrate --noinput + python manage.py runserver "$APP_HOST":"$APP_PORT" else python manage.py makemigrations --noinput python manage.py migrate --noinput python manage.py collectstatic --noinput + gunicorn "$APP_NAME".wsgi:application --bind "$APP_HOST":"$APP_PORT" --workers 3 --log-level=debug fi - -gunicorn "$APP_NAME".wsgi:application --bind "$APP_HOST":"$APP_PORT" --workers 3 --log-level=debug diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 9649d2b..82c2ec2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -14,6 +14,7 @@ services: - ./src:/usr/src/app/ - ./deployment/scripts:/app/deployment/scripts/ - static_files:/usr/src/app/static + - media_files:/usr/src/app/media labels: - "traefik.enable=true" - "traefik.http.routers.${APP_NAME}-backend.rule=Host(`${APP_DOMAIN}`)" @@ -21,6 +22,8 @@ services: - "traefik.http.services.${APP_NAME}-backend.loadbalancer.server.port=${APP_PORT}" - "traefik.http.routers.${APP_NAME}-backend.tls.certresolver=letsencrypt" env_file: .env + expose: + - "${APP_PORT:-8000}" depends_on: - db - redis @@ -33,8 +36,8 @@ services: volumes: - postgres_data_dir:/var/lib/postgresql/data/ env_file: .env - ports: - - "5432:5432" + expose: + - "${POSTGRES_PORT:-5432}" shm_size: 1g @@ -70,9 +73,10 @@ services: volumes: - ./deployment/scripts/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - static_files:/usr/src/app/static + - media_files:/usr/src/app/media labels: - "traefik.enable=true" - - "traefik.http.routers.${APP_NAME}-nginx.rule=Host(`${APP_HOST}`) && PathPrefix(`/static`)" + - "traefik.http.routers.${APP_NAME}-nginx.rule=Host(`${APP_HOST}`) && (PathPrefix(`/static`) || PathPrefix(`/media`))" - "traefik.http.routers.${APP_NAME}-nginx.entrypoints=web" - "traefik.http.services.${APP_NAME}-nginx.loadbalancer.server.port=80" depends_on: @@ -101,6 +105,7 @@ services: volumes: static_files: + media_files: postgres_data_dir: redis_data: letsencrypt: diff --git a/docker-compose.yml b/docker-compose.yml index a6a4952..f7a5b84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,13 +13,9 @@ services: volumes: - ./src:/usr/src/app/ - ./deployment/scripts:/app/deployment/scripts/ - - static_files:/usr/src/app/static - labels: - - "traefik.enable=true" - - "traefik.http.routers.${APP_NAME}-backend.rule=Host(`${APP_HOST}`)" - - "traefik.http.routers.${APP_NAME}-backend.entrypoints=web" - - "traefik.http.services.${APP_NAME}-backend.loadbalancer.server.port=${APP_PORT}" env_file: .env + ports: + - "${APP_PORT}:${APP_PORT}" depends_on: - db - redis @@ -32,8 +28,8 @@ services: volumes: - postgres_data_dir:/var/lib/postgresql/data/ env_file: .env - ports: - - "5432:5432" + expose: + - "${POSTGRES_PORT:-5432}" shm_size: 1g @@ -63,41 +59,6 @@ services: container_name: "${APP_NAME}-celery-beat" command: [ "/bin/sh", "/app/deployment/scripts/celery/start-beat.sh" ] - nginx: - image: nginx:latest - container_name: "${APP_NAME}-nginx" - volumes: - - ./deployment/scripts/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - static_files:/usr/src/app/static - labels: - - "traefik.enable=true" - - "traefik.http.routers.${APP_NAME}-nginx.rule=Host(`${APP_HOST}`) && PathPrefix(`/static`)" - - "traefik.http.routers.${APP_NAME}-nginx.entrypoints=web" - - "traefik.http.services.${APP_NAME}-nginx.loadbalancer.server.port=80" - depends_on: - - backend - - traefik: - image: traefik:v2.5 - container_name: "${APP_NAME}-traefik" - command: - - "--api.insecure=true" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--entrypoints.web.address=:80" - ports: - - "80:80" - - "8080:8080" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - networks: - - default - depends_on: - - db - - backend - - redis - volumes: postgres_data_dir: redis_data: - static_files: diff --git a/pyproject.toml b/pyproject.toml index 32b6d08..d5d240f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,11 @@ testpaths = [ ] pythonpath = [".", "src"] python_files = "tests.py test_*.py *_tests.py" -DJANGO_SETTINGS_MODULE = "conf.settings.test" +DJANGO_SETTINGS_MODULE = "project_name.settings.test" +filterwarnings = [ + 'ignore::DeprecationWarning:kombu.*:', + 'ignore::DeprecationWarning:celery.*:', +] [tool.coverage.report] fail_under = 85 @@ -58,6 +62,7 @@ extend-exclude = ''' [tool.ruff] +format = "grouped" line-length = 88 # black default extend-exclude = [ "*/migrations/*", diff --git a/scripts/format.sh b/scripts/format.sh index 78ed311..be52db8 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,5 +2,5 @@ APP_PATH="src" -black $APP_PATH ruff $APP_PATH --fix +black $APP_PATH \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh index a9d2fd3..0a26b43 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -2,5 +2,5 @@ APP_PATH="src" -black $APP_PATH --check ruff $APP_PATH +black $APP_PATH --check diff --git a/src/manage.py b/src/manage.py index 1a1453b..b6dadb6 100755 --- a/src/manage.py +++ b/src/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/project_name/__init__.py b/src/project_name/__init__.py index 15d7c50..5568b6d 100644 --- a/src/project_name/__init__.py +++ b/src/project_name/__init__.py @@ -2,4 +2,4 @@ # Django starts so that shared_task will use this app. from .celery import app as celery_app -__all__ = ('celery_app',) +__all__ = ("celery_app",) diff --git a/src/project_name/asgi.py b/src/project_name/asgi.py index bfc120f..3ee8cc1 100644 --- a/src/project_name/asgi.py +++ b/src/project_name/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings") application = get_asgi_application() diff --git a/src/project_name/celery.py b/src/project_name/celery.py index 60de013..5b5fb52 100644 --- a/src/project_name/celery.py +++ b/src/project_name/celery.py @@ -1,17 +1,18 @@ import os from celery import Celery +from celery.schedules import crontab # Set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings.dev') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings.dev") -app = Celery('project_name') +app = Celery("project_name") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django apps. app.autodiscover_tasks() @@ -19,4 +20,15 @@ app.autodiscover_tasks() @app.task(bind=True) def debug_task(self): - print(f'Request: {self.request!r}') + print(f"Request: {self.request!r}") + + +app.conf.beat_schedule = { + "delete_job_files": { + "task": "test_periodic_task", + # Every 1 minute for testing purposes + "schedule": crontab(minute="*/1"), + }, +} + +app.conf.timezone = "UTC" diff --git a/src/project_name/settings/base.py b/src/project_name/settings/base.py index 3653ba2..6bc4b8c 100644 --- a/src/project_name/settings/base.py +++ b/src/project_name/settings/base.py @@ -6,7 +6,9 @@ from django.core.management.utils import get_random_secret_key # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent -SECRET_KEY = os_getenv("SECRET_KEY", get_random_secret_key()) # If SECRET_KEY is not set, generate a random one +SECRET_KEY = os_getenv( + "SECRET_KEY", get_random_secret_key() +) # If SECRET_KEY is not set, generate a random one APP_ENV = os_getenv("APP_ENV", "dev") DEBUG = os_getenv("DEBUG", "true").lower() in ["True", "true", "1", "yes", "y"] @@ -16,61 +18,65 @@ ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",") if DEBUG: CORS_ORIGIN_ALLOW_ALL = True else: - CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split(",") - CORS_ALLOWED_ORIGINS = os_getenv("CORS_ALLOWED_ORIGINS", "http://localhost").split(",") + CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split( + "," + ) + CORS_ALLOWED_ORIGINS = os_getenv("CORS_ALLOWED_ORIGINS", "http://localhost").split( + "," + ) # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", "corsheaders", "test_app", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', + "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", - 'django.contrib.sessions.middleware.SessionMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", # CorsMiddleware should be placed as high as possible, - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'project_name.urls' +ROOT_URLCONF = "project_name.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - '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', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "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 = 'project_name.wsgi.application' +WSGI_APPLICATION = "project_name.wsgi.application" # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - 'default': { + "default": { "ENGINE": "django.db.backends.postgresql", - 'NAME': os_getenv("POSTGRES_DB", "postgres"), + "NAME": os_getenv("POSTGRES_DB", "postgres"), "USER": os_getenv("POSTGRES_USER", "postgres"), "PASSWORD": os_getenv("POSTGRES_PASSWORD", "postgres"), "HOST": os_getenv("POSTGRES_HOST", "db"), @@ -83,25 +89,25 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -110,15 +116,15 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' -STATIC_ROOT = os_path.join(BASE_DIR, 'static') -MEDIA_URL = 'media/' -MEDIA_ROOT = os_path.join(BASE_DIR, 'media') +STATIC_URL = "static/" +STATIC_ROOT = os_path.join(BASE_DIR, "static") +MEDIA_URL = "media/" +MEDIA_ROOT = os_path.join(BASE_DIR, "media") # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Redis REDIS_DB_KEYS = { diff --git a/src/project_name/settings/dev.py b/src/project_name/settings/dev.py index 44c8b96..4b40b38 100644 --- a/src/project_name/settings/dev.py +++ b/src/project_name/settings/dev.py @@ -1 +1 @@ -from .base import * # noqa +from .base import * # noqa diff --git a/src/project_name/settings/prod.py b/src/project_name/settings/prod.py index a31da0a..595032e 100644 --- a/src/project_name/settings/prod.py +++ b/src/project_name/settings/prod.py @@ -1,4 +1,4 @@ -from .base import * # noqa +from .base import * # noqa STORAGES = { "staticfiles": { diff --git a/src/project_name/settings/test.py b/src/project_name/settings/test.py index 44c8b96..85075e7 100644 --- a/src/project_name/settings/test.py +++ b/src/project_name/settings/test.py @@ -1 +1,9 @@ -from .base import * # noqa +from .base import * # noqa + + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} diff --git a/src/project_name/urls.py b/src/project_name/urls.py index 8063e4b..3ad4b1d 100644 --- a/src/project_name/urls.py +++ b/src/project_name/urls.py @@ -5,7 +5,7 @@ from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import path, include urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), path("test_app/", include("test_app.urls")), ] diff --git a/src/project_name/wsgi.py b/src/project_name/wsgi.py index 461bec5..a13618a 100644 --- a/src/project_name/wsgi.py +++ b/src/project_name/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings.dev') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project_name.settings.dev") application = get_wsgi_application() diff --git a/src/test_app/admin.py b/src/test_app/admin.py index a4e11e9..a099ca8 100644 --- a/src/test_app/admin.py +++ b/src/test_app/admin.py @@ -1,3 +1,8 @@ -from django.contrib import admin # noqa +from django.contrib import admin -# Register your models here. +from test_app.models import TestModel + + +@admin.register(TestModel) +class TestModelAdmin(admin.ModelAdmin): + list_display = ("name", "description") diff --git a/src/test_app/apps.py b/src/test_app/apps.py index 7635468..953db29 100644 --- a/src/test_app/apps.py +++ b/src/test_app/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class TestAppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'test_app' + default_auto_field = "django.db.models.BigAutoField" + name = "test_app" diff --git a/src/test_app/models.py b/src/test_app/models.py index 56b48a5..85c0dd1 100644 --- a/src/test_app/models.py +++ b/src/test_app/models.py @@ -1,3 +1,10 @@ -from django.db import models # noqa +from django.db import models # noqa -# Create your models here. + +class TestModel(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + file = models.FileField(upload_to="test_app/files/") + + def __str__(self): + return self.name diff --git a/src/test_app/tasks.py b/src/test_app/tasks.py new file mode 100644 index 0000000..f627d2f --- /dev/null +++ b/src/test_app/tasks.py @@ -0,0 +1,6 @@ +from project_name.celery import app + + +@app.task(bind=True, name="test_periodic_task") +def test_periodic_task(self): # noqa: Adding self since we are using bind=True + print("Hello from periodic task") diff --git a/src/test_app/tests.py b/src/test_app/tests.py deleted file mode 100644 index b8ab89e..0000000 --- a/src/test_app/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase # noqa - -# Create your tests here. diff --git a/src/test_app/urls.py b/src/test_app/urls.py index c49391a..50b902f 100644 --- a/src/test_app/urls.py +++ b/src/test_app/urls.py @@ -1,4 +1,4 @@ -from django.urls import path # noqa +from django.urls import path # noqa urlpatterns = [ # ... other urls diff --git a/src/test_app/views.py b/src/test_app/views.py index 6921674..9f8192a 100644 --- a/src/test_app/views.py +++ b/src/test_app/views.py @@ -1,3 +1 @@ -from django.shortcuts import render # noqa - -# Create your views here. +from django.shortcuts import render # noqa diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 3e57bcd..55e9c79 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -13,7 +13,5 @@ def enable_db_access_for_all_tests(db): @pytest.fixture def test_user(): return User.objects.create_user( - username='test_user', - email="test_user@test.com", - password="test_password" + username="test_user", email="test_user@test.com", password="test_password" ) diff --git a/src/tests/test_app_1/test_example.py b/src/tests/test_app_1/test_example.py index 4c218a1..dcef714 100644 --- a/src/tests/test_app_1/test_example.py +++ b/src/tests/test_app_1/test_example.py @@ -1,5 +1,3 @@ - def test_example(test_user): - assert test_user.username == 'test_user' + assert test_user.username == "test_user" assert test_user.email is not None -