From cbcab8636a511a256280aa8fdb36786f6b5168e8 Mon Sep 17 00:00:00 2001 From: ashwin31 Date: Tue, 17 Feb 2026 10:27:47 +0530 Subject: [PATCH] fix: resolve setup and Docker issues preventing project from running Use shared_task instead of standalone Celery apps so celery -A crm worker discovers all tasks. Add safe defaults for all required env vars in settings.py so missing .env no longer crashes Django. Create idempotent management command to bootstrap a superuser in Docker. Populate .env.example with working dev defaults. Fixes #490, #493, #508, #388, #383, #382, #381, #441, #519 Co-Authored-By: Claude Opus 4.6 --- .env.docker | 1 + backend/.env.example | 38 +++++++++---------- backend/accounts/tasks.py | 8 ++-- backend/cases/tasks.py | 6 +-- .../commands/create_default_admin.py | 25 ++++++++++++ backend/common/tasks.py | 18 ++++----- backend/contacts/tasks.py | 6 +-- backend/crm/celery.py | 1 + backend/crm/settings.py | 28 +++++++------- backend/leads/tasks.py | 12 +++--- backend/opportunity/tasks.py | 6 +-- backend/tasks/celery_tasks.py | 6 +-- docker/backend/entrypoint.sh | 11 ++++++ 13 files changed, 94 insertions(+), 72 deletions(-) create mode 100644 backend/common/management/commands/create_default_admin.py diff --git a/.env.docker b/.env.docker index b6fd3705..98ca9d5a 100644 --- a/.env.docker +++ b/.env.docker @@ -28,6 +28,7 @@ CELERY_RESULT_BACKEND=redis://redis:6379/0 # Email (console backend in dev — emails print to stdout) DEFAULT_FROM_EMAIL=noreply@localhost ADMIN_EMAIL=admin@localhost +ADMIN_PASSWORD=admin # CORS CORS_ALLOW_ALL=True diff --git a/backend/.env.example b/backend/.env.example index 34162368..87dfbdd9 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,43 +1,41 @@ # Environment variables +# Copy this file to .env and adjust as needed: cp .env.example .env -SECRET_KEY="" +SECRET_KEY="django-insecure-dev-key-please-change-in-production" ENV_TYPE="dev" -DOMAIN_NAME="" +DEBUG=True +DOMAIN_NAME="http://localhost:8000" +SWAGGER_ROOT_URL="http://localhost:8000" -# AWS +# AWS (only needed for production / S3 storage) AWS_BUCKET_NAME="" - AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" AWS_SES_REGION_NAME="" AWS_SES_REGION_ENDPOINT="" - # DB -DBNAME="" -DBUSER="" -DBPASSWORD="" -DBHOST="" -DBPORT="" +DBNAME="crm_db" +DBUSER="postgres" +DBPASSWORD="postgres" +DBHOST="localhost" +DBPORT="5432" -# Sentry +# Sentry (optional) SENTRY_DSN="" # Celery CELERY_BROKER_URL="redis://localhost:6379/0" CELERY_RESULT_BACKEND="redis://localhost:6379/0" -# Swagger -SWAGGER_ROOT_URL="" - -# Google OAuth +# Google OAuth (optional — leave blank to disable) GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET="" # Email -DEFAULT_FROM_EMAIL="" -ADMIN_EMAIL="" +DEFAULT_FROM_EMAIL="noreply@localhost" +ADMIN_EMAIL="admin@localhost" -ALLOWED_HOSTS=yourdomain.com -CORS_ALLOWED_ORIGINS=https://yourdomain.com -CSRF_TRUSTED_ORIGINS=https://yourdomain.com \ No newline at end of file +ALLOWED_HOSTS=localhost,127.0.0.1 +CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +CSRF_TRUSTED_ORIGINS=http://localhost:5173 diff --git a/backend/accounts/tasks.py b/backend/accounts/tasks.py index 2acb9f6f..5493cdc0 100644 --- a/backend/accounts/tasks.py +++ b/backend/accounts/tasks.py @@ -1,4 +1,4 @@ -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage from django.template import Context, Template @@ -8,10 +8,8 @@ from accounts.models import Account, AccountEmail, AccountEmailLog from common.models import Profile from common.tasks import set_rls_context -app = Celery("redis://") - -@app.task +@shared_task def send_email(email_obj_id, org_id): set_rls_context(org_id) email_obj = AccountEmail.objects.filter(id=email_obj_id).first() @@ -58,7 +56,7 @@ def send_email(email_obj_id, org_id): pass -@app.task +@shared_task def send_email_to_assigned_user(recipients, account_id, org_id): """Send Mail To Users When they are assigned to an account""" set_rls_context(org_id) diff --git a/backend/cases/tasks.py b/backend/cases/tasks.py index c53a84f3..716210f8 100644 --- a/backend/cases/tasks.py +++ b/backend/cases/tasks.py @@ -1,4 +1,4 @@ -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage from django.template.loader import render_to_string @@ -7,10 +7,8 @@ from cases.models import Case from common.models import Profile from common.tasks import set_rls_context -app = Celery("redis://") - -@app.task +@shared_task def send_email_to_assigned_user(recipients, case_id, org_id): """Send Mail To Users When they are assigned to a case""" set_rls_context(org_id) diff --git a/backend/common/management/commands/create_default_admin.py b/backend/common/management/commands/create_default_admin.py new file mode 100644 index 00000000..d5e96ee5 --- /dev/null +++ b/backend/common/management/commands/create_default_admin.py @@ -0,0 +1,25 @@ +import os + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + +User = get_user_model() + + +class Command(BaseCommand): + help = "Create a default superuser if none exists (for Docker bootstrap)" + + def handle(self, *args, **options): + if User.objects.filter(is_superuser=True).exists(): + self.stdout.write(self.style.SUCCESS("Superuser already exists — skipping.")) + return + + email = os.environ.get("ADMIN_EMAIL", "admin@localhost") + password = os.environ.get("ADMIN_PASSWORD", "admin") + + User.objects.create_superuser( + username=email, + email=email, + password=password, + ) + self.stdout.write(self.style.SUCCESS(f"Created default superuser: {email}")) diff --git a/backend/common/tasks.py b/backend/common/tasks.py index 71f9c4b2..02f1cfbd 100644 --- a/backend/common/tasks.py +++ b/backend/common/tasks.py @@ -1,6 +1,6 @@ import datetime -from celery import Celery +from celery import shared_task from django.conf import settings from django.contrib.auth.tokens import default_token_generator from django.core.mail import EmailMessage @@ -13,8 +13,6 @@ from django.utils.http import urlsafe_base64_encode from common.models import Comment, Profile, Teams, User from common.token_generator import account_activation_token -app = Celery("redis://") - def set_rls_context(org_id): """ @@ -33,7 +31,7 @@ def set_rls_context(org_id): ) -@app.task +@shared_task def send_email_to_new_user(user_id): """Send Mail To Users When their account is created""" user_obj = User.objects.filter(id=user_id).first() @@ -75,7 +73,7 @@ def send_email_to_new_user(user_id): msg.send() -@app.task +@shared_task def send_email_user_mentions( comment_id, called_from, @@ -145,7 +143,7 @@ def send_email_user_mentions( msg.send() -@app.task +@shared_task def send_email_user_status( user_id, status_changed_user="", @@ -185,7 +183,7 @@ def send_email_user_status( msg.send() -@app.task +@shared_task def send_email_user_delete( user_email, deleted_by="", @@ -211,7 +209,7 @@ def send_email_user_delete( msg.send() -@app.task +@shared_task def resend_activation_link_to_user( user_email="", ): @@ -260,7 +258,7 @@ def resend_activation_link_to_user( msg.send() -@app.task +@shared_task def remove_users(removed_users_list, team_id, org_id=None): # Set RLS context for org-scoped queries set_rls_context(org_id) @@ -311,7 +309,7 @@ def remove_users(removed_users_list, team_id, org_id=None): invoice.assigned_to.remove(user) -@app.task +@shared_task def update_team_users(team_id, org_id=None): """this function updates assigned_to field on all models when a team is updated""" # Set RLS context for org-scoped queries diff --git a/backend/contacts/tasks.py b/backend/contacts/tasks.py index 6b1cd12d..06df42d3 100644 --- a/backend/contacts/tasks.py +++ b/backend/contacts/tasks.py @@ -1,4 +1,4 @@ -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage from django.template.loader import render_to_string @@ -7,10 +7,8 @@ from common.models import Profile from common.tasks import set_rls_context from contacts.models import Contact -app = Celery("redis://") - -@app.task +@shared_task def send_email_to_assigned_user(recipients, contact_id, org_id): """Send Mail To Users When they are assigned to a contact""" set_rls_context(org_id) diff --git a/backend/crm/celery.py b/backend/crm/celery.py index 8464f124..83dc52eb 100644 --- a/backend/crm/celery.py +++ b/backend/crm/celery.py @@ -20,6 +20,7 @@ app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django app configs. app.autodiscover_tasks() +app.autodiscover_tasks(related_name="celery_tasks") # tasks app uses celery_tasks.py # Celery Beat Schedule for recurring tasks app.conf.beat_schedule = { diff --git a/backend/crm/settings.py b/backend/crm/settings.py index 58b9c8d5..0540b6bc 100644 --- a/backend/crm/settings.py +++ b/backend/crm/settings.py @@ -10,7 +10,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) load_dotenv() # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ["SECRET_KEY"] +SECRET_KEY = os.environ.get("SECRET_KEY", "django-insecure-dev-key-please-change-in-production") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get("DEBUG", "False").lower() == "true" @@ -86,11 +86,11 @@ WSGI_APPLICATION = "crm.wsgi.application" DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "NAME": os.environ["DBNAME"], - "USER": os.environ["DBUSER"], - "PASSWORD": os.environ["DBPASSWORD"], - "HOST": os.environ["DBHOST"], - "PORT": os.environ["DBPORT"], + "NAME": os.environ.get("DBNAME", "crm_db"), + "USER": os.environ.get("DBUSER", "postgres"), + "PASSWORD": os.environ.get("DBPASSWORD", "postgres"), + "HOST": os.environ.get("DBHOST", "localhost"), + "PORT": os.environ.get("DBPORT", "5432"), } } @@ -131,20 +131,20 @@ STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") STATIC_URL = "/static/" STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] -ENV_TYPE = os.environ["ENV_TYPE"] +ENV_TYPE = os.environ.get("ENV_TYPE", "dev") if ENV_TYPE == "dev": MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/" elif ENV_TYPE == "prod": from .server_settings import * -DEFAULT_FROM_EMAIL = os.environ["DEFAULT_FROM_EMAIL"] -ADMIN_EMAIL = os.environ["ADMIN_EMAIL"] +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@localhost") +ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL", "admin@localhost") # celery Tasks -CELERY_BROKER_URL = os.environ["CELERY_BROKER_URL"] -CELERY_RESULT_BACKEND = os.environ["CELERY_RESULT_BACKEND"] +CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0") +CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0") LOGGING = { @@ -306,7 +306,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' -DOMAIN_NAME = os.getenv("DOMAIN_NAME") +DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "http://localhost:8000") SIMPLE_JWT = { @@ -330,8 +330,8 @@ SIMPLE_JWT = { JWT_ALGO = "HS256" -DOMAIN_NAME = os.environ["DOMAIN_NAME"] -SWAGGER_ROOT_URL = os.environ["SWAGGER_ROOT_URL"] +DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "http://localhost:8000") +SWAGGER_ROOT_URL = os.environ.get("SWAGGER_ROOT_URL", "http://localhost:8000") # Google OAuth Configuration GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "") diff --git a/backend/leads/tasks.py b/backend/leads/tasks.py index d2777a8b..85cbaab5 100644 --- a/backend/leads/tasks.py +++ b/backend/leads/tasks.py @@ -1,6 +1,6 @@ import re -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage, EmailMultiAlternatives from django.db.models import Q @@ -11,15 +11,13 @@ from common.models import Org, Profile from common.tasks import set_rls_context from leads.models import Lead -app = Celery("redis://") - def get_rendered_html(template_name, context={}): html_content = render_to_string(template_name, context) return html_content -@app.task +@shared_task def send_email( subject, html_content, @@ -45,7 +43,7 @@ def send_email( email.send() -@app.task +@shared_task def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address, org_id): set_rls_context(org_id) lead_instance = Lead.objects.filter( @@ -76,7 +74,7 @@ def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address, org_i send_email.delay(**mail_kwargs) -@app.task +@shared_task def send_email_to_assigned_user(recipients, lead_id, org_id, source=""): """Send Mail To Users When they are assigned to a lead""" set_rls_context(org_id) @@ -102,7 +100,7 @@ def send_email_to_assigned_user(recipients, lead_id, org_id, source=""): msg.send() -@app.task +@shared_task def create_lead_from_file(validated_rows, invalid_rows, user_id, source, company_id): """Parameters : validated_rows, invalid_rows, user_id. This function is used to create leads from a given file. diff --git a/backend/opportunity/tasks.py b/backend/opportunity/tasks.py index 1fc5fb0e..5bc13428 100644 --- a/backend/opportunity/tasks.py +++ b/backend/opportunity/tasks.py @@ -1,4 +1,4 @@ -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage from django.template.loader import render_to_string @@ -7,10 +7,8 @@ from common.models import Profile from common.tasks import set_rls_context from opportunity.models import Opportunity -app = Celery("redis://") - -@app.task +@shared_task def send_email_to_assigned_user(recipients, opportunity_id, org_id): """Send Mail To Users When they are assigned to an opportunity""" set_rls_context(org_id) diff --git a/backend/tasks/celery_tasks.py b/backend/tasks/celery_tasks.py index c6afc47c..6b72d47e 100644 --- a/backend/tasks/celery_tasks.py +++ b/backend/tasks/celery_tasks.py @@ -1,4 +1,4 @@ -from celery import Celery +from celery import shared_task from django.conf import settings from django.core.mail import EmailMessage from django.shortcuts import reverse @@ -10,10 +10,8 @@ from common.tasks import set_rls_context from contacts.models import Contact from tasks.models import Task -app = Celery("redis://") - -@app.task +@shared_task def send_email( task_id, recipients, org_id, domain="demo.django-crm.io", protocol="http" ): diff --git a/docker/backend/entrypoint.sh b/docker/backend/entrypoint.sh index b5a61914..30527ef4 100755 --- a/docker/backend/entrypoint.sh +++ b/docker/backend/entrypoint.sh @@ -2,12 +2,20 @@ set -e echo "Waiting for PostgreSQL..." +retries=0 +max_retries=30 while ! python -c " import socket, os s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((os.environ['DBHOST'], int(os.environ['DBPORT']))) s.close() " 2>/dev/null; do + retries=$((retries + 1)) + if [ "$retries" -ge "$max_retries" ]; then + echo "ERROR: Could not connect to PostgreSQL after $max_retries attempts." + exit 1 + fi + echo " PostgreSQL not ready yet (attempt $retries/$max_retries)..." sleep 1 done echo "PostgreSQL is ready." @@ -15,6 +23,9 @@ echo "PostgreSQL is ready." echo "Running migrations..." python manage.py migrate --noinput +echo "Creating default admin user (if needed)..." +python manage.py create_default_admin + echo "Collecting static files..." python manage.py collectstatic --noinput