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 <noreply@anthropic.com>
This commit is contained in:
ashwin31 2026-02-17 10:27:47 +05:30
parent 1f6b797909
commit cbcab8636a
13 changed files with 94 additions and 72 deletions

View file

@ -28,6 +28,7 @@ CELERY_RESULT_BACKEND=redis://redis:6379/0
# Email (console backend in dev — emails print to stdout) # Email (console backend in dev — emails print to stdout)
DEFAULT_FROM_EMAIL=noreply@localhost DEFAULT_FROM_EMAIL=noreply@localhost
ADMIN_EMAIL=admin@localhost ADMIN_EMAIL=admin@localhost
ADMIN_PASSWORD=admin
# CORS # CORS
CORS_ALLOW_ALL=True CORS_ALLOW_ALL=True

View file

@ -1,43 +1,41 @@
# Environment variables # 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" 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_BUCKET_NAME=""
AWS_ACCESS_KEY_ID="" AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY="" AWS_SECRET_ACCESS_KEY=""
AWS_SES_REGION_NAME="" AWS_SES_REGION_NAME=""
AWS_SES_REGION_ENDPOINT="" AWS_SES_REGION_ENDPOINT=""
# DB # DB
DBNAME="" DBNAME="crm_db"
DBUSER="" DBUSER="postgres"
DBPASSWORD="" DBPASSWORD="postgres"
DBHOST="" DBHOST="localhost"
DBPORT="" DBPORT="5432"
# Sentry # Sentry (optional)
SENTRY_DSN="" SENTRY_DSN=""
# Celery # Celery
CELERY_BROKER_URL="redis://localhost:6379/0" CELERY_BROKER_URL="redis://localhost:6379/0"
CELERY_RESULT_BACKEND="redis://localhost:6379/0" CELERY_RESULT_BACKEND="redis://localhost:6379/0"
# Swagger # Google OAuth (optional — leave blank to disable)
SWAGGER_ROOT_URL=""
# Google OAuth
GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET="" GOOGLE_CLIENT_SECRET=""
# Email # Email
DEFAULT_FROM_EMAIL="" DEFAULT_FROM_EMAIL="noreply@localhost"
ADMIN_EMAIL="" ADMIN_EMAIL="admin@localhost"
ALLOWED_HOSTS=yourdomain.com ALLOWED_HOSTS=localhost,127.0.0.1
CORS_ALLOWED_ORIGINS=https://yourdomain.com CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
CSRF_TRUSTED_ORIGINS=https://yourdomain.com CSRF_TRUSTED_ORIGINS=http://localhost:5173

View file

@ -1,4 +1,4 @@
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.template import Context, Template from django.template import Context, Template
@ -8,10 +8,8 @@ from accounts.models import Account, AccountEmail, AccountEmailLog
from common.models import Profile from common.models import Profile
from common.tasks import set_rls_context from common.tasks import set_rls_context
app = Celery("redis://")
@shared_task
@app.task
def send_email(email_obj_id, org_id): def send_email(email_obj_id, org_id):
set_rls_context(org_id) set_rls_context(org_id)
email_obj = AccountEmail.objects.filter(id=email_obj_id).first() email_obj = AccountEmail.objects.filter(id=email_obj_id).first()
@ -58,7 +56,7 @@ def send_email(email_obj_id, org_id):
pass pass
@app.task @shared_task
def send_email_to_assigned_user(recipients, account_id, org_id): def send_email_to_assigned_user(recipients, account_id, org_id):
"""Send Mail To Users When they are assigned to an account""" """Send Mail To Users When they are assigned to an account"""
set_rls_context(org_id) set_rls_context(org_id)

View file

@ -1,4 +1,4 @@
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -7,10 +7,8 @@ from cases.models import Case
from common.models import Profile from common.models import Profile
from common.tasks import set_rls_context from common.tasks import set_rls_context
app = Celery("redis://")
@shared_task
@app.task
def send_email_to_assigned_user(recipients, case_id, org_id): def send_email_to_assigned_user(recipients, case_id, org_id):
"""Send Mail To Users When they are assigned to a case""" """Send Mail To Users When they are assigned to a case"""
set_rls_context(org_id) set_rls_context(org_id)

View file

@ -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}"))

View file

@ -1,6 +1,6 @@
import datetime import datetime
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.core.mail import EmailMessage 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.models import Comment, Profile, Teams, User
from common.token_generator import account_activation_token from common.token_generator import account_activation_token
app = Celery("redis://")
def set_rls_context(org_id): 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): def send_email_to_new_user(user_id):
"""Send Mail To Users When their account is created""" """Send Mail To Users When their account is created"""
user_obj = User.objects.filter(id=user_id).first() user_obj = User.objects.filter(id=user_id).first()
@ -75,7 +73,7 @@ def send_email_to_new_user(user_id):
msg.send() msg.send()
@app.task @shared_task
def send_email_user_mentions( def send_email_user_mentions(
comment_id, comment_id,
called_from, called_from,
@ -145,7 +143,7 @@ def send_email_user_mentions(
msg.send() msg.send()
@app.task @shared_task
def send_email_user_status( def send_email_user_status(
user_id, user_id,
status_changed_user="", status_changed_user="",
@ -185,7 +183,7 @@ def send_email_user_status(
msg.send() msg.send()
@app.task @shared_task
def send_email_user_delete( def send_email_user_delete(
user_email, user_email,
deleted_by="", deleted_by="",
@ -211,7 +209,7 @@ def send_email_user_delete(
msg.send() msg.send()
@app.task @shared_task
def resend_activation_link_to_user( def resend_activation_link_to_user(
user_email="", user_email="",
): ):
@ -260,7 +258,7 @@ def resend_activation_link_to_user(
msg.send() msg.send()
@app.task @shared_task
def remove_users(removed_users_list, team_id, org_id=None): def remove_users(removed_users_list, team_id, org_id=None):
# Set RLS context for org-scoped queries # Set RLS context for org-scoped queries
set_rls_context(org_id) 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) invoice.assigned_to.remove(user)
@app.task @shared_task
def update_team_users(team_id, org_id=None): def update_team_users(team_id, org_id=None):
"""this function updates assigned_to field on all models when a team is updated""" """this function updates assigned_to field on all models when a team is updated"""
# Set RLS context for org-scoped queries # Set RLS context for org-scoped queries

View file

@ -1,4 +1,4 @@
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.template.loader import render_to_string 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 common.tasks import set_rls_context
from contacts.models import Contact from contacts.models import Contact
app = Celery("redis://")
@shared_task
@app.task
def send_email_to_assigned_user(recipients, contact_id, org_id): def send_email_to_assigned_user(recipients, contact_id, org_id):
"""Send Mail To Users When they are assigned to a contact""" """Send Mail To Users When they are assigned to a contact"""
set_rls_context(org_id) set_rls_context(org_id)

View file

@ -20,6 +20,7 @@ app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
app.autodiscover_tasks(related_name="celery_tasks") # tasks app uses celery_tasks.py
# Celery Beat Schedule for recurring tasks # Celery Beat Schedule for recurring tasks
app.conf.beat_schedule = { app.conf.beat_schedule = {

View file

@ -10,7 +10,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv() load_dotenv()
# SECURITY WARNING: keep the secret key used in production secret! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "False").lower() == "true" DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
@ -86,11 +86,11 @@ WSGI_APPLICATION = "crm.wsgi.application"
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["DBNAME"], "NAME": os.environ.get("DBNAME", "crm_db"),
"USER": os.environ["DBUSER"], "USER": os.environ.get("DBUSER", "postgres"),
"PASSWORD": os.environ["DBPASSWORD"], "PASSWORD": os.environ.get("DBPASSWORD", "postgres"),
"HOST": os.environ["DBHOST"], "HOST": os.environ.get("DBHOST", "localhost"),
"PORT": os.environ["DBPORT"], "PORT": os.environ.get("DBPORT", "5432"),
} }
} }
@ -131,20 +131,20 @@ STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATICFILES_DIRS = [os.path.join(BASE_DIR, "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": if ENV_TYPE == "dev":
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
elif ENV_TYPE == "prod": elif ENV_TYPE == "prod":
from .server_settings import * from .server_settings import *
DEFAULT_FROM_EMAIL = os.environ["DEFAULT_FROM_EMAIL"] DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@localhost")
ADMIN_EMAIL = os.environ["ADMIN_EMAIL"] ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL", "admin@localhost")
# celery Tasks # celery Tasks
CELERY_BROKER_URL = os.environ["CELERY_BROKER_URL"] CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0")
CELERY_RESULT_BACKEND = os.environ["CELERY_RESULT_BACKEND"] CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
LOGGING = { LOGGING = {
@ -306,7 +306,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' # 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 = { SIMPLE_JWT = {
@ -330,8 +330,8 @@ SIMPLE_JWT = {
JWT_ALGO = "HS256" JWT_ALGO = "HS256"
DOMAIN_NAME = os.environ["DOMAIN_NAME"] DOMAIN_NAME = os.environ.get("DOMAIN_NAME", "http://localhost:8000")
SWAGGER_ROOT_URL = os.environ["SWAGGER_ROOT_URL"] SWAGGER_ROOT_URL = os.environ.get("SWAGGER_ROOT_URL", "http://localhost:8000")
# Google OAuth Configuration # Google OAuth Configuration
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "") GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", "")

View file

@ -1,6 +1,6 @@
import re import re
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.db.models import Q from django.db.models import Q
@ -11,15 +11,13 @@ from common.models import Org, Profile
from common.tasks import set_rls_context from common.tasks import set_rls_context
from leads.models import Lead from leads.models import Lead
app = Celery("redis://")
def get_rendered_html(template_name, context={}): def get_rendered_html(template_name, context={}):
html_content = render_to_string(template_name, context) html_content = render_to_string(template_name, context)
return html_content return html_content
@app.task @shared_task
def send_email( def send_email(
subject, subject,
html_content, html_content,
@ -45,7 +43,7 @@ def send_email(
email.send() email.send()
@app.task @shared_task
def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address, org_id): def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address, org_id):
set_rls_context(org_id) set_rls_context(org_id)
lead_instance = Lead.objects.filter( 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) send_email.delay(**mail_kwargs)
@app.task @shared_task
def send_email_to_assigned_user(recipients, lead_id, org_id, source=""): def send_email_to_assigned_user(recipients, lead_id, org_id, source=""):
"""Send Mail To Users When they are assigned to a lead""" """Send Mail To Users When they are assigned to a lead"""
set_rls_context(org_id) set_rls_context(org_id)
@ -102,7 +100,7 @@ def send_email_to_assigned_user(recipients, lead_id, org_id, source=""):
msg.send() msg.send()
@app.task @shared_task
def create_lead_from_file(validated_rows, invalid_rows, user_id, source, company_id): def create_lead_from_file(validated_rows, invalid_rows, user_id, source, company_id):
"""Parameters : validated_rows, invalid_rows, user_id. """Parameters : validated_rows, invalid_rows, user_id.
This function is used to create leads from a given file. This function is used to create leads from a given file.

View file

@ -1,4 +1,4 @@
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.template.loader import render_to_string 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 common.tasks import set_rls_context
from opportunity.models import Opportunity from opportunity.models import Opportunity
app = Celery("redis://")
@shared_task
@app.task
def send_email_to_assigned_user(recipients, opportunity_id, org_id): def send_email_to_assigned_user(recipients, opportunity_id, org_id):
"""Send Mail To Users When they are assigned to an opportunity""" """Send Mail To Users When they are assigned to an opportunity"""
set_rls_context(org_id) set_rls_context(org_id)

View file

@ -1,4 +1,4 @@
from celery import Celery from celery import shared_task
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.shortcuts import reverse from django.shortcuts import reverse
@ -10,10 +10,8 @@ from common.tasks import set_rls_context
from contacts.models import Contact from contacts.models import Contact
from tasks.models import Task from tasks.models import Task
app = Celery("redis://")
@shared_task
@app.task
def send_email( def send_email(
task_id, recipients, org_id, domain="demo.django-crm.io", protocol="http" task_id, recipients, org_id, domain="demo.django-crm.io", protocol="http"
): ):

View file

@ -2,12 +2,20 @@
set -e set -e
echo "Waiting for PostgreSQL..." echo "Waiting for PostgreSQL..."
retries=0
max_retries=30
while ! python -c " while ! python -c "
import socket, os import socket, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((os.environ['DBHOST'], int(os.environ['DBPORT']))) s.connect((os.environ['DBHOST'], int(os.environ['DBPORT'])))
s.close() s.close()
" 2>/dev/null; do " 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 sleep 1
done done
echo "PostgreSQL is ready." echo "PostgreSQL is ready."
@ -15,6 +23,9 @@ echo "PostgreSQL is ready."
echo "Running migrations..." echo "Running migrations..."
python manage.py migrate --noinput python manage.py migrate --noinput
echo "Creating default admin user (if needed)..."
python manage.py create_default_admin
echo "Collecting static files..." echo "Collecting static files..."
python manage.py collectstatic --noinput python manage.py collectstatic --noinput