From da32da71fffcd7efaba9fb42899445622bd29484 Mon Sep 17 00:00:00 2001 From: Dimas Date: Fri, 19 Sep 2025 14:21:13 +0100 Subject: [PATCH 1/5] Switch broker from Redis to RabbitMQ and add sample task --- .run/Run Server.run.xml | 27 -------- Makefile | 63 +++++++++++++++---- deployment/.template.env | 4 +- deployment/docker-compose.yml | 30 +++++++-- deployment/docker/requirements.txt | 3 +- django_project/core/__init__.py | 3 + django_project/core/celery.py | 2 - django_project/core/settings/base.py | 14 +++-- django_project/core/settings/contrib.py | 5 ++ django_project/core/settings/project.py | 1 + django_project/core/urls.py | 1 + django_project/frontend/webpack.config.js | 54 ++++++++-------- django_project/profiles/__init__.py | 0 django_project/profiles/admin.py | 39 ++++++++++++ django_project/profiles/apps.py | 6 ++ .../profiles/migrations/__init__.py | 0 django_project/profiles/models.py | 3 + .../profiles/serializers/__init__.py | 0 .../profiles/serializers/profile.py | 16 +++++ django_project/profiles/tasks/__init__.py | 0 django_project/profiles/tasks/username.py | 27 ++++++++ django_project/profiles/tests.py | 3 + django_project/profiles/urls.py | 5 ++ django_project/profiles/views/__init__.py | 0 django_project/profiles/views/profile.py | 11 ++++ 25 files changed, 232 insertions(+), 85 deletions(-) delete mode 100644 .run/Run Server.run.xml create mode 100644 django_project/profiles/__init__.py create mode 100644 django_project/profiles/admin.py create mode 100644 django_project/profiles/apps.py create mode 100644 django_project/profiles/migrations/__init__.py create mode 100644 django_project/profiles/models.py create mode 100644 django_project/profiles/serializers/__init__.py create mode 100644 django_project/profiles/serializers/profile.py create mode 100644 django_project/profiles/tasks/__init__.py create mode 100644 django_project/profiles/tasks/username.py create mode 100644 django_project/profiles/tests.py create mode 100644 django_project/profiles/urls.py create mode 100644 django_project/profiles/views/__init__.py create mode 100644 django_project/profiles/views/profile.py diff --git a/.run/Run Server.run.xml b/.run/Run Server.run.xml deleted file mode 100644 index e254447..0000000 --- a/.run/Run Server.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Makefile b/Makefile index bf14a5a..5a3a271 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,15 @@ export COMPOSE_FILE=deployment/docker-compose.yml:deployment/docker-compose.override.yml SHELL := /bin/bash +# Paths for dev override/template +OVERRIDE_PATH := deployment/docker-compose.override.yml +OVERRIDE_TEMPLATE_PATH := deployment/docker-compose.override.template.yml + +ENV_PATH := deployment/.env +ENV_TEMPLATE_PATH := deployment/.template.env + +.PHONY: build up dev down flake wait-db frontend-test dev-ci-test dev-entrypoint dev-initialize dev-runserver dev-test sleep ensure-dev-files + build: @echo @echo "------------------------------------------------------------------" @@ -8,19 +17,19 @@ build: @echo "------------------------------------------------------------------" @docker compose build -up: +up: ensure-dev-files @echo @echo "------------------------------------------------------------------" @echo "Running in production mode" @echo "------------------------------------------------------------------" - @docker compose ${ARGS} up -d nginx django + @docker compose $(ARGS) up -d nginx django -dev: +dev: ensure-dev-files @echo @echo "------------------------------------------------------------------" @echo "Running in dev mode" @echo "------------------------------------------------------------------" - @docker compose ${ARGS} up -d dev worker + @docker compose $(ARGS) up -d dev worker down: @echo @@ -38,28 +47,27 @@ flake: @flake8 wait-db: - @docker compose ${ARGS} exec -T db su - postgres -c "until pg_isready; do sleep 5; done" - + @docker compose $(ARGS) exec -T db su - postgres -c "until pg_isready; do sleep 5; done" frontend-test: - @docker compose exec -T dev sh -c "cd /home/web/django_project/frontend && npm install && npm test" + @docker compose $(ARGS) exec -T dev sh -c "cd /home/web/django_project/frontend && npm install && npm test" # ----------------------------------------------------------------------------- # ----------------------------------- D E V ----------------------------------- # ----------------------------------------------------------------------------- -dev-ci-test: +dev-ci-test: ensure-dev-files @echo @echo "------------------------------------------------------------------" @echo "Running in DEVELOPMENT mode for CI test" @echo "------------------------------------------------------------------" - @docker compose ${ARGS} up --no-recreate --no-deps -d db worker redis dev + @docker compose $(ARGS) up --no-recreate --no-deps -d db worker redis dev dev-entrypoint: @echo @echo "------------------------------------------------------------------" @echo "Running entrypoint.sh in DEVELOPMENT mode" @echo "------------------------------------------------------------------" - @docker compose ${ARGS} exec -T dev "/home/web/django_project/entrypoint.sh" + @docker compose $(ARGS) exec -T dev "/home/web/django_project/entrypoint.sh" dev-initialize: @echo @@ -80,11 +88,42 @@ dev-test: @echo "------------------------------------------------------------------" @echo "Run tests" @echo "------------------------------------------------------------------" - @docker compose exec -T dev python manage.py test --keepdb --noinput + @docker compose $(ARGS) exec -T dev python manage.py test --keepdb --noinput # ----------------------------------------------------------------------------- # --------------------------------- U T I L S --------------------------------- # ----------------------------------------------------------------------------- sleep: @sleep 50 - @echo "Done" \ No newline at end of file + @echo "Done" + +# ----------------------------------------------------------------------------- +# ---------------------------- P R E - F L I G H T ----------------------------- +# ----------------------------------------------------------------------------- +ensure-dev-files: + @echo + @echo "------------------------------------------------------------------" + @echo "Pre-flight: ensuring dev override and env files exist" + @echo "------------------------------------------------------------------" + @# Ensure deployment/docker-compose.override.yml + @if [ ! -f "$(OVERRIDE_PATH)" ]; then \ + if [ -f "$(OVERRIDE_TEMPLATE_PATH)" ]; then \ + cp "$(OVERRIDE_TEMPLATE_PATH)" "$(OVERRIDE_PATH)"; \ + echo "Created $(OVERRIDE_PATH) from $(OVERRIDE_TEMPLATE_PATH)."; \ + else \ + echo "$(OVERRIDE_PATH) not found AND no template at $(OVERRIDE_TEMPLATE_PATH)."; \ + fi; \ + else \ + echo "$(OVERRIDE_PATH) already exists; leaving it untouched."; \ + fi + @# Ensure .env from .template.env + @if [ ! -f "$(ENV_PATH)" ]; then \ + if [ -f "$(ENV_TEMPLATE_PATH)" ]; then \ + cp "$(ENV_TEMPLATE_PATH)" "$(ENV_PATH)"; \ + echo "Created .env from .template.env."; \ + else \ + echo ".env not found AND no .template.env present."; \ + fi; \ + else \ + echo ".env already exists; leaving it untouched."; \ + fi diff --git a/deployment/.template.env b/deployment/.template.env index 80af92a..86f791e 100644 --- a/deployment/.template.env +++ b/deployment/.template.env @@ -12,8 +12,8 @@ DATABASE_USERNAME=docker DATABASE_PASSWORD=docker DATABASE_HOST=db -REDIS_HOST=redis -REDIS_PASSWORD=redis_password +RABBITMQ_DEFAULT_USER=guest +RABBITMQ_DEFAULT_PASS=password RABBITMQ_HOST=rabbitmq SENTRY_DSN= diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 20c6336..ac70750 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -19,10 +19,21 @@ x-common-django: restart: on-failure services: - redis: - image: bitnamilegacy/redis:8.2.1 + rabbitmq: + image: rabbitmq:3-management + hostname: rabbitmq environment: - - REDIS_PASSWORD=${REDIS_PASSWORD:-redis_password} + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-guest} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS:-guest} + ports: + - "5672:5672" + - "15672:15672" # Management UI (optional) + restart: on-failure + + memcached: + image: memcached:1.6-alpine + command: ["memcached", "-m", "128", "-I", "5m"] + restart: on-failure db: image: kartoza/postgis:17-3.5 @@ -58,9 +69,14 @@ services: django: <<: *default-common-django command: 'uwsgi --ini /uwsgi.conf' + depends_on: + - db + - rabbitmq + - memcached links: - db - - worker + - rabbitmq + - memcached worker: <<: *default-common-django @@ -68,7 +84,8 @@ services: command: 'celery -A core worker -l info' links: - db - - redis + - rabbitmq + - memcached celery_beat: <<: *default-common-django @@ -76,7 +93,8 @@ services: command: 'celery -A core beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler' links: - db - - redis + - rabbitmq + - memcached nginx: image: nginx diff --git a/deployment/docker/requirements.txt b/deployment/docker/requirements.txt index 2496005..e27636e 100644 --- a/deployment/docker/requirements.txt +++ b/deployment/docker/requirements.txt @@ -33,5 +33,4 @@ psycopg2-binary==2.9.10 # This extension enables you to store the periodic task schedule in the database. django-celery-beat==2.8.1 -# Python client for Redis database and key-value store -redis==6.4.0 +pymemcache>=4.0 \ No newline at end of file diff --git a/django_project/core/__init__.py b/django_project/core/__init__.py index e69de29..a04b708 100644 --- a/django_project/core/__init__.py +++ b/django_project/core/__init__.py @@ -0,0 +1,3 @@ +# core/__init__.py +from .celery import app as celery_app +__all__ = ("celery_app",) diff --git a/django_project/core/celery.py b/django_project/core/celery.py index 727e0df..91fd3dd 100644 --- a/django_project/core/celery.py +++ b/django_project/core/celery.py @@ -25,7 +25,5 @@ # Load task modules from all registered Django app configs. app.autodiscover_tasks() -app.conf.broker_url = BASE_REDIS_URL - # this allows you to schedule items in the Django admin. app.conf.beat_scheduler = 'django_celery_beat.schedulers.DatabaseScheduler' diff --git a/django_project/core/settings/base.py b/django_project/core/settings/base.py index 287cc9a..18753b6 100644 --- a/django_project/core/settings/base.py +++ b/django_project/core/settings/base.py @@ -134,12 +134,14 @@ ) CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': ( - f'redis://default:{os.environ.get("REDIS_PASSWORD", "")}' - f'@{os.environ.get("REDIS_HOST", "")}', - ) + "default": { + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", + "LOCATION": "memcached:11211", + "TIMEOUT": 600, + "OPTIONS": { + "no_delay": True, + "ignore_exc": True, + }, } } diff --git a/django_project/core/settings/contrib.py b/django_project/core/settings/contrib.py index 90245ac..ab7ded4 100644 --- a/django_project/core/settings/contrib.py +++ b/django_project/core/settings/contrib.py @@ -44,6 +44,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' CELERY_RESULT_BACKEND = 'django-db' +RABBITMQ_HOST = os.getenv('RABBITMQ_HOST', 'rabbitmq') +RABBITMQ_USER = os.getenv('RABBITMQ_DEFAULT_USER', 'guest') +RABBITMQ_PASS = os.getenv('RABBITMQ_DEFAULT_PASS', 'password') +CELERY_BROKER_URL = f'amqp://{RABBITMQ_USER}:{RABBITMQ_PASS}@{RABBITMQ_HOST}:5672//' + TEMPLATES[0]['OPTIONS']['context_processors'] += [ 'django.template.context_processors.request', ] diff --git a/django_project/core/settings/project.py b/django_project/core/settings/project.py index 55ece2c..ba2cd8a 100644 --- a/django_project/core/settings/project.py +++ b/django_project/core/settings/project.py @@ -32,6 +32,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'core', 'frontend', + 'profiles' ) TEMPLATES[0]['DIRS'] += [ diff --git a/django_project/core/urls.py b/django_project/core/urls.py index 1688eac..3e5b509 100644 --- a/django_project/core/urls.py +++ b/django_project/core/urls.py @@ -23,6 +23,7 @@ # Always put this to bottom path('', include('frontend.urls')), + path('profiles/', include('profiles.urls')), ] if settings.DEBUG: diff --git a/django_project/frontend/webpack.config.js b/django_project/frontend/webpack.config.js index 9533731..41f3725 100644 --- a/django_project/frontend/webpack.config.js +++ b/django_project/frontend/webpack.config.js @@ -86,34 +86,32 @@ let conf = { } }; if (isServe) { - if (isDev) { - conf['output'] = { - path: path.resolve(__dirname, "./bundles/frontend"), - filename: filename + '.js', - publicPath: 'http://localhost:9000/static/', - } - } - conf['devServer'] = { - hot: true, - port: 9000, - headers: { - 'Access-Control-Allow-Origin': '*' - }, - devMiddleware: { - writeToDisk: true, - }, - allowedHosts: 'all', - compress: true, - } - conf['devtool'] = 'inline-source-map', - conf['output'] = { - path: path.resolve(__dirname, "./bundles/frontend"), - filename: filename + '.js', - publicPath: 'http://localhost:9000/static/', - } - conf['plugins'].push( - new ReactRefreshWebpackPlugin() - ) + // Single, consistent output + conf.output = { + path: path.resolve(__dirname, "./bundles/frontend"), + filename: filename + ".js", + publicPath: "http://dev.local:9000/static/", + }; + + conf.devServer = { + hot: true, + port: 9000, + allowedHosts: "all", + compress: true, + + // 👇 prevent webpack-dev-server from serving ./public and using serve-index + static: false, + + devMiddleware: { + publicPath: "/static/", + writeToDisk: true, + }, + + headers: { "Access-Control-Allow-Origin": "*" }, + }; + + conf.devtool = "inline-source-map"; + conf.plugins.push(new ReactRefreshWebpackPlugin()); } else if (isDev) { conf['output'] = { path: path.resolve(__dirname, "./bundles/frontend"), diff --git a/django_project/profiles/__init__.py b/django_project/profiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_project/profiles/admin.py b/django_project/profiles/admin.py new file mode 100644 index 0000000..dac9923 --- /dev/null +++ b/django_project/profiles/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from profiles.tasks.username import normalize_username + +User = get_user_model() + + +@admin.action(description="Normalize name via Celery") +def normalize_name_action(modeladmin, request, queryset): + count = 0 + for uid in queryset.values_list("id", flat=True): + normalize_username.delay(uid) + count += 1 + modeladmin.message_user( + request, + f"Queued normalize_user_name for {count} user(s).") + + +@admin.action(description="Normalize name now (sync)") +def normalize_name_sync_action(modeladmin, request, queryset): + count = 0 + changed = 0 + for uid in queryset.values_list("id", flat=True): + result = normalize_username.run(uid) + changed += 1 if result.get("changed") else 0 + count += 1 + modeladmin.message_user( + request, + f"Ran sync normalization for {count} user(s); {changed} changed." + ) + + +class UserAdmin(DjangoUserAdmin): + actions = [normalize_name_action, normalize_name_sync_action] + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) diff --git a/django_project/profiles/apps.py b/django_project/profiles/apps.py new file mode 100644 index 0000000..d6cbb9b --- /dev/null +++ b/django_project/profiles/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "profiles" diff --git a/django_project/profiles/migrations/__init__.py b/django_project/profiles/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_project/profiles/models.py b/django_project/profiles/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/django_project/profiles/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django_project/profiles/serializers/__init__.py b/django_project/profiles/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_project/profiles/serializers/profile.py b/django_project/profiles/serializers/profile.py new file mode 100644 index 0000000..e7253f5 --- /dev/null +++ b/django_project/profiles/serializers/profile.py @@ -0,0 +1,16 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + +User = get_user_model() + + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ( + "id", + "username", + "first_name", + "last_name", + "email" + ) diff --git a/django_project/profiles/tasks/__init__.py b/django_project/profiles/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_project/profiles/tasks/username.py b/django_project/profiles/tasks/username.py new file mode 100644 index 0000000..724bf20 --- /dev/null +++ b/django_project/profiles/tasks/username.py @@ -0,0 +1,27 @@ +from celery import shared_task +from django.contrib.auth import get_user_model + +User = get_user_model() + + +@shared_task(bind=True, autoretry_for=(User.DoesNotExist,), retry_backoff=True, max_retries=1) +def normalize_username(self, user_id: int) -> dict: + """Normalize a user's first and last name. + """ + user = User.objects.get(pk=user_id) + + new_first = (user.first_name or "").strip().title() + new_last = (user.last_name or "").strip().title() + + changed = (new_first != user.first_name) or (new_last != user.last_name) + if changed: + user.first_name = new_first + user.last_name = new_last + user.save(update_fields=["first_name", "last_name"]) + + return { + "user_id": user.id, + "changed": changed, + "first_name": user.first_name, + "last_name": user.last_name, + } diff --git a/django_project/profiles/tests.py b/django_project/profiles/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/django_project/profiles/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/django_project/profiles/urls.py b/django_project/profiles/urls.py new file mode 100644 index 0000000..712c809 --- /dev/null +++ b/django_project/profiles/urls.py @@ -0,0 +1,5 @@ +from django.urls import path +from profiles.views.profile import ProfileView + + +urlpatterns = [path("me/", ProfileView.as_view(), name="me")] diff --git a/django_project/profiles/views/__init__.py b/django_project/profiles/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_project/profiles/views/profile.py b/django_project/profiles/views/profile.py new file mode 100644 index 0000000..8dc64eb --- /dev/null +++ b/django_project/profiles/views/profile.py @@ -0,0 +1,11 @@ +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from profiles.serializers.profile import ProfileSerializer + + +class ProfileView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + return Response(ProfileSerializer(request.user).data) From a2a5d023f07c6168946caf861393c96adf8c7aa8 Mon Sep 17 00:00:00 2001 From: Dimas Date: Fri, 19 Sep 2025 14:22:19 +0100 Subject: [PATCH 2/5] Update webpack.config.js --- django_project/frontend/webpack.config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/django_project/frontend/webpack.config.js b/django_project/frontend/webpack.config.js index 41f3725..953b9ce 100644 --- a/django_project/frontend/webpack.config.js +++ b/django_project/frontend/webpack.config.js @@ -86,11 +86,10 @@ let conf = { } }; if (isServe) { - // Single, consistent output conf.output = { path: path.resolve(__dirname, "./bundles/frontend"), filename: filename + ".js", - publicPath: "http://dev.local:9000/static/", + publicPath: "http://localhost:9000/static/", }; conf.devServer = { @@ -99,7 +98,6 @@ if (isServe) { allowedHosts: "all", compress: true, - // 👇 prevent webpack-dev-server from serving ./public and using serve-index static: false, devMiddleware: { From 2d984256a96310f4fe2fb112a5815df4881a8150 Mon Sep 17 00:00:00 2001 From: Dimas Date: Fri, 19 Sep 2025 15:11:31 +0100 Subject: [PATCH 3/5] update flake config --- .flake8 | 36 ++++++++++++++++++ django_project/__init__.py | 1 + django_project/core/context_processors.py | 22 ++++------- django_project/frontend/views.py | 3 +- django_project/frontend/webpack.config.js | 2 +- django_project/profiles/admin.py | 37 +++++++++++++++---- django_project/profiles/models.py | 2 +- .../profiles/serializers/profile.py | 13 +++---- django_project/profiles/tests.py | 2 +- django_project/profiles/views/profile.py | 9 ++++- 10 files changed, 93 insertions(+), 34 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b4bfd12 --- /dev/null +++ b/.flake8 @@ -0,0 +1,36 @@ +# .flake8 +[flake8] +# Core style +max-line-length = 100 +max-complexity = 10 +extend-ignore = + E203,W503, + D200,D212,D411,D415, + D203,D100,D104,D101 +select = E,F,W,C90,D + +# Docstrings (flake8-docstrings / pydocstyle) +docstring-convention = google + +# Project hygiene +exclude = + .git, + __pycache__, + .venv, + venv, + .tox, + .mypy_cache, + .pytest_cache, + node_modules, + build, + dist, + core/settings/utils.py, + *.egg-info, + */static/*, + */media/* + +# Per-file relaxations +per-file-ignores = + __init__.py:F401 + core/settings/*.py:F401,F403,F405,D100 + */migrations/*:D100,D101,D102,D103,D104,D105,D106,D107 diff --git a/django_project/__init__.py b/django_project/__init__.py index e69de29..143f486 100644 --- a/django_project/__init__.py +++ b/django_project/__init__.py @@ -0,0 +1 @@ +# __init__.py diff --git a/django_project/core/context_processors.py b/django_project/core/context_processors.py index 3a3ff11..974fb1b 100644 --- a/django_project/core/context_processors.py +++ b/django_project/core/context_processors.py @@ -1,21 +1,15 @@ """Context processors.""" from django.conf import settings +from django.http import HttpRequest -def sentry_dsn(request): - """ - Return the Sentry DSN setting. +def sentry_dsn(request: HttpRequest) -> dict[str, str]: + """Inject Sentry DSN into the template context. - Parameters - ---------- - request : django.http.HttpRequest - The incoming HTTP request (not used in this function). + Args: + request: Incoming HTTP request (unused). - Returns - ------- - dict - Dictionary containing the Sentry DSN with key ``"SENTRY_DSN"``. + Returns: + dict[str, str]: Mapping with the ``"SENTRY_DSN"`` key. """ - return { - 'SENTRY_DSN': settings.SENTRY_DSN - } + return {"SENTRY_DSN": settings.SENTRY_DSN} diff --git a/django_project/frontend/views.py b/django_project/frontend/views.py index bedb1ec..e3fc508 100644 --- a/django_project/frontend/views.py +++ b/django_project/frontend/views.py @@ -15,9 +15,10 @@ class FrontendView(TemplateView): @method_decorator(csrf_exempt, name="dispatch") class SentryProxyView(View): sentry_key = settings.SENTRY_DSN + def post(self, request): + """Sentry proxy post method""" host = "sentry.io" - known_project_ids = [36] # Add your Sentry project IDs here envelope = request.body.decode("utf-8") pieces = envelope.split("\n", 1) diff --git a/django_project/frontend/webpack.config.js b/django_project/frontend/webpack.config.js index 953b9ce..ce86d3b 100644 --- a/django_project/frontend/webpack.config.js +++ b/django_project/frontend/webpack.config.js @@ -89,7 +89,7 @@ if (isServe) { conf.output = { path: path.resolve(__dirname, "./bundles/frontend"), filename: filename + ".js", - publicPath: "http://localhost:9000/static/", + publicPath: "http://dev.local:9000/static/", }; conf.devServer = { diff --git a/django_project/profiles/admin.py b/django_project/profiles/admin.py index dac9923..193fa86 100644 --- a/django_project/profiles/admin.py +++ b/django_project/profiles/admin.py @@ -1,24 +1,47 @@ +"""Admin customizations for User with normalize-name actions.""" + from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from django.db.models import QuerySet +from django.http import HttpRequest + from profiles.tasks.username import normalize_username User = get_user_model() @admin.action(description="Normalize name via Celery") -def normalize_name_action(modeladmin, request, queryset): +def normalize_name_action( + modeladmin: admin.ModelAdmin, request: HttpRequest, queryset: QuerySet[User] +) -> None: + """Queue Celery tasks to normalize selected users' names. + + Args: + modeladmin: The admin instance invoking this action. + request: Current HTTP request. + queryset: Selected users to process. + """ count = 0 for uid in queryset.values_list("id", flat=True): normalize_username.delay(uid) count += 1 - modeladmin.message_user( - request, - f"Queued normalize_user_name for {count} user(s).") + modeladmin.message_user(request, f"Queued normalize_username for {count} user(s).") @admin.action(description="Normalize name now (sync)") -def normalize_name_sync_action(modeladmin, request, queryset): +def normalize_name_sync_action( + modeladmin: admin.ModelAdmin, request: HttpRequest, queryset: QuerySet[User] +) -> None: + """Synchronously normalize names for selected users. + + Useful in development when the broker/worker is unavailable. + + Args: + modeladmin: The admin instance invoking this action. + request: Current HTTP request. + queryset: Selected users to process. + """ count = 0 changed = 0 for uid in queryset.values_list("id", flat=True): @@ -26,12 +49,12 @@ def normalize_name_sync_action(modeladmin, request, queryset): changed += 1 if result.get("changed") else 0 count += 1 modeladmin.message_user( - request, - f"Ran sync normalization for {count} user(s); {changed} changed." + request, f"Ran sync normalization for {count} user(s); {changed} changed." ) class UserAdmin(DjangoUserAdmin): + """User admin with normalize-name actions.""" actions = [normalize_name_action, normalize_name_sync_action] diff --git a/django_project/profiles/models.py b/django_project/profiles/models.py index 71a8362..0b4331b 100644 --- a/django_project/profiles/models.py +++ b/django_project/profiles/models.py @@ -1,3 +1,3 @@ -from django.db import models +# from django.db import models # Create your models here. diff --git a/django_project/profiles/serializers/profile.py b/django_project/profiles/serializers/profile.py index e7253f5..e674e2c 100644 --- a/django_project/profiles/serializers/profile.py +++ b/django_project/profiles/serializers/profile.py @@ -1,3 +1,5 @@ +"""Serializers for profile endpoints.""" + from django.contrib.auth import get_user_model from rest_framework import serializers @@ -5,12 +7,9 @@ class ProfileSerializer(serializers.ModelSerializer): + """Serializer exposing public fields of the authenticated user.""" + class Meta: + """Configuration for ProfileSerializer.""" model = User - fields = ( - "id", - "username", - "first_name", - "last_name", - "email" - ) + fields = ("id", "username", "first_name", "last_name", "email") diff --git a/django_project/profiles/tests.py b/django_project/profiles/tests.py index 7ce503c..a79ca8b 100644 --- a/django_project/profiles/tests.py +++ b/django_project/profiles/tests.py @@ -1,3 +1,3 @@ -from django.test import TestCase +# from django.test import TestCase # Create your tests here. diff --git a/django_project/profiles/views/profile.py b/django_project/profiles/views/profile.py index 8dc64eb..ced56f3 100644 --- a/django_project/profiles/views/profile.py +++ b/django_project/profiles/views/profile.py @@ -1,3 +1,4 @@ +from rest_framework.request import Request from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -5,7 +6,11 @@ class ProfileView(APIView): + """Return the authenticated user's profile data.""" + permission_classes = [IsAuthenticated] - def get(self, request): - return Response(ProfileSerializer(request.user).data) + def get(self, request: Request) -> Response: + """Retrieve the current user's profile.""" + serializer = ProfileSerializer(request.user) + return Response(serializer.data) From 7d10905fcbb3aa3413c39b884af1927b5817f3f5 Mon Sep 17 00:00:00 2001 From: Dimas Date: Fri, 19 Sep 2025 15:18:05 +0100 Subject: [PATCH 4/5] Update flake8 --- .flake8 | 4 +++- .github/workflows/tests.yaml | 2 +- django_project/frontend/webpack.config.js | 2 +- docs/create-uuid.py | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.flake8 b/.flake8 index b4bfd12..e442e73 100644 --- a/.flake8 +++ b/.flake8 @@ -17,6 +17,7 @@ exclude = .git, __pycache__, .venv, + ./venv, venv, .tox, .mypy_cache, @@ -24,7 +25,8 @@ exclude = node_modules, build, dist, - core/settings/utils.py, + */core/settings/utils.py, + */core/settings/, *.egg-info, */static/*, */media/* diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7a37f90..7a84534 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -28,7 +28,7 @@ jobs: run: pip install flake8 flake8-docstrings pydoclint[flake8] - name: Run flake8 - run: flake8 + run: flake8 --config=.flake8 build_image: needs: flake diff --git a/django_project/frontend/webpack.config.js b/django_project/frontend/webpack.config.js index ce86d3b..953b9ce 100644 --- a/django_project/frontend/webpack.config.js +++ b/django_project/frontend/webpack.config.js @@ -89,7 +89,7 @@ if (isServe) { conf.output = { path: path.resolve(__dirname, "./bundles/frontend"), filename: filename + ".js", - publicPath: "http://dev.local:9000/static/", + publicPath: "http://localhost:9000/static/", }; conf.devServer = { diff --git a/docs/create-uuid.py b/docs/create-uuid.py index 7cd8a6b..6bfa7c3 100755 --- a/docs/create-uuid.py +++ b/docs/create-uuid.py @@ -2,4 +2,3 @@ import shortuuid uuid = shortuuid.uuid() -print (uuid) From 77fce866c8e445808f868083433479bb512725b7 Mon Sep 17 00:00:00 2001 From: Dimas Date: Fri, 19 Sep 2025 15:27:22 +0100 Subject: [PATCH 5/5] Remove redis from docker-compose test --- Makefile | 2 +- .../docker-compose.override.template.yml | 4 ++-- deployment/docker-compose.test.yml | 20 +++++++++++-------- django_project/core/celery.py | 6 ------ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 5a3a271..02aa14e 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ dev-ci-test: ensure-dev-files @echo "------------------------------------------------------------------" @echo "Running in DEVELOPMENT mode for CI test" @echo "------------------------------------------------------------------" - @docker compose $(ARGS) up --no-recreate --no-deps -d db worker redis dev + @docker compose $(ARGS) up --no-recreate --no-deps -d db worker rabbitmq dev dev-entrypoint: @echo diff --git a/deployment/docker-compose.override.template.yml b/deployment/docker-compose.override.template.yml index c83aa44..8196c54 100644 --- a/deployment/docker-compose.override.template.yml +++ b/deployment/docker-compose.override.template.yml @@ -71,7 +71,6 @@ services: image: kartoza/${COMPOSE_PROJECT_NAME:-django_react_base}:dev env_file: - .env - entrypoint: [ "/home/web/django_project/entrypoint.dev.sh" ] volumes: - ../django_project:/home/web/django_project - ./volumes/static:/home/web/static @@ -82,7 +81,8 @@ services: - "8001:22" links: - db - - redis + - rabbitmq + - memcached - worker - webpack depends_on: diff --git a/deployment/docker-compose.test.yml b/deployment/docker-compose.test.yml index 1d9c434..580d7a2 100644 --- a/deployment/docker-compose.test.yml +++ b/deployment/docker-compose.test.yml @@ -25,9 +25,10 @@ services: - INITIAL_FIXTURES=True - SECRET_KEY=SECRET_KEY - # Redis config - - REDIS_HOST=redis - - REDIS_PASSWORD=redis_password + # Rabbitmq config + - RABBITMQ_DEFAULT_USER=guest + - RABBITMQ_DEFAULT_PASS=password + - RABBITMQ_HOST=rabbitmq # Email where alters should be sent. This will be used by let's encrypt and as the django admin email. - ADMIN_USERNAME=admin @@ -40,7 +41,8 @@ services: entrypoint: [ ] links: - db - - redis + - rabbitmq + - memcached working_dir: /home/web/django_project dev: @@ -48,7 +50,8 @@ services: container_name: "django_react_base-dev" links: - db - - redis + - rabbitmq + - memcached - worker volumes: - media-data:/home/web/media @@ -66,9 +69,10 @@ services: - INITIAL_FIXTURES=True - SECRET_KEY=SECRET_KEY - # Redis config - - REDIS_HOST=redis - - REDIS_PASSWORD=redis_password + # Rabbitmq config + - RABBITMQ_DEFAULT_USER=guest + - RABBITMQ_DEFAULT_PASS=password + - RABBITMQ_HOST=rabbitmq # Email where alters should be sent. This will be used by let's encrypt and as the django admin email. - ADMIN_USERNAME=admin diff --git a/django_project/core/celery.py b/django_project/core/celery.py index 91fd3dd..514b6a4 100644 --- a/django_project/core/celery.py +++ b/django_project/core/celery.py @@ -8,12 +8,6 @@ # this is also used in manage.py os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') -# Get the base REDIS URL, default to redis' default -BASE_REDIS_URL = ( - f'redis://default:{os.environ.get("REDIS_PASSWORD", "")}' - f'@{os.environ.get("REDIS_HOST", "")}', -) - app = Celery('django_react_base') # Using a string here means the worker don't have to serialize