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)
DEFAULT_FROM_EMAIL=noreply@localhost
ADMIN_EMAIL=admin@localhost
ADMIN_PASSWORD=admin
# CORS
CORS_ALLOW_ALL=True

View file

@ -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
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

View file

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

View file

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

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
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

View file

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

View file

@ -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 = {

View file

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

View file

@ -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.

View file

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

View file

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

View file

@ -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