diff --git a/README.md b/README.md index 32ef72d95..4434085fe 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,10 @@ pip install -r requirements.txt ### Env variables * Then refer to `env.md` for environment variables and keep those in the `.env` file in the current folder as your project is in. - +``` +cp ENV.md .env +sed -i "s|^SECRET_KEY=.*|SECRET_KEY=\"$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')\"|" .env +``` ### Docker / docker-compose in order to use docker, please run the next commands after cloning repo: @@ -107,9 +110,10 @@ docker-compose -f docker/docker-compose.yml up ``` python manage.py migrate +uvicorn apiv2.main:app --reload python manage.py runserver ``` -- Then open http://localhost:8000/swagger/ in your browser to explore API. +- Then open http://localhost:8000/docs/ in your browser to explore API. - After running API, Go to Frontend UI [React CRM](https://github.com/MicroPyramid/react-crm "React CRM") project to configure Fronted UI to interact with API. diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 830bf3797..7615c4892 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.db import migrations, models import phonenumber_field.modelfields @@ -78,20 +78,4 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), - migrations.CreateModel( - name='Tags', - fields=[ - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(max_length=20)), - ('slug', models.CharField(blank=True, max_length=20, unique=True)), - ], - options={ - 'verbose_name': 'Tag', - 'verbose_name_plural': 'Tags', - 'db_table': 'tags', - 'ordering': ('-created_at',), - }, - ), ] diff --git a/accounts/migrations/0002_initial.py b/accounts/migrations/0002_initial.py index 580d36592..6bd2d0830 100644 --- a/accounts/migrations/0002_initial.py +++ b/accounts/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -10,25 +10,15 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('common', '0001_initial'), + ('contacts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('leads', '0001_initial'), ('teams', '0001_initial'), ('accounts', '0001_initial'), - ('contacts', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('common', '0001_initial'), ] operations = [ - migrations.AddField( - model_name='tags', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - migrations.AddField( - model_name='tags', - name='updated_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), - ), migrations.AddField( model_name='accountemaillog', name='contact', @@ -82,7 +72,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='account', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='account_created_by', to='common.profile'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), ), migrations.AddField( model_name='account', @@ -97,7 +87,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='account', name='tags', - field=models.ManyToManyField(blank=True, to='accounts.tags'), + field=models.ManyToManyField(blank=True, to='common.tag'), ), migrations.AddField( model_name='account', diff --git a/accounts/migrations/0003_alter_account_created_by.py b/accounts/migrations/0003_alter_account_created_by.py deleted file mode 100644 index d13a2698c..000000000 --- a/accounts/migrations/0003_alter_account_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-13 08:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('accounts', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='account', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/accounts/models.py b/accounts/models.py index 8b42ad143..cc4c8274f 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -6,31 +6,31 @@ from phonenumber_field.modelfields import PhoneNumberField from common import utils -from common.models import Org, Profile +from common.models import Org, Profile, Tag from common.utils import COUNTRIES, INDCHOICES from contacts.models import Contact from teams.models import Teams from common.base import BaseModel -class Tags(BaseModel): - name = models.CharField(max_length=20) - slug = models.CharField(max_length=20, unique=True, blank=True) +# class Tags(BaseModel): +# name = models.CharField(max_length=20) +# slug = models.CharField(max_length=20, unique=True, blank=True) - class Meta: - verbose_name = "Tag" - verbose_name_plural = "Tags" - db_table = "tags" - ordering = ("-created_at",) +# class Meta: +# verbose_name = "Tag" +# verbose_name_plural = "Tags" +# db_table = "tags" +# ordering = ("-created_at",) - def __str__(self): - return f"{self.name}" +# def __str__(self): +# return f"{self.name}" - def save(self, *args, **kwargs): - self.slug = slugify(self.name) - super().save(*args, **kwargs) +# def save(self, *args, **kwargs): +# self.slug = slugify(self.name) +# super().save(*args, **kwargs) class Account(BaseModel): @@ -65,7 +65,7 @@ class Account(BaseModel): # Profile, related_name="account_created_by", on_delete=models.SET_NULL, null=True # ) is_active = models.BooleanField(default=False) - tags = models.ManyToManyField(Tags, blank=True) + tags = models.ManyToManyField(Tag, blank=True) status = models.CharField( choices=ACCOUNT_STATUS_CHOICE, max_length=64, default="open" ) diff --git a/accounts/serializer.py b/accounts/serializer.py index 922247d10..bf227bf31 100644 --- a/accounts/serializer.py +++ b/accounts/serializer.py @@ -1,6 +1,7 @@ from rest_framework import serializers -from accounts.models import Account, AccountEmail, Tags, AccountEmailLog +from accounts.models import Account, AccountEmail, AccountEmailLog +from common.models import Tag from common.serializer import ( AttachmentsSerializer, OrganizationSerializer, @@ -14,7 +15,7 @@ class TagsSerailizer(serializers.ModelSerializer): class Meta: - model = Tags + model = Tag fields = ("id", "name", "slug") diff --git a/accounts/views.py b/accounts/views.py index c8b908896..432917f7b 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -14,7 +14,7 @@ from rest_framework.generics import GenericAPIView from accounts import swagger_params1 -from accounts.models import Account, Tags +from accounts.models import Account from accounts.serializer import ( AccountCreateSerializer, AccountSerializer, @@ -29,7 +29,7 @@ from teams.serializer import TeamsSerializer from accounts.tasks import send_email, send_email_to_assigned_user from cases.serializer import CaseSerializer -from common.models import Attachments, Comment, Profile +from common.models import Attachments, Comment, Profile, Tag from leads.models import Lead from leads.serializer import LeadSerializer @@ -130,7 +130,7 @@ def get_context_data(self, **kwargs): context["countries"] = COUNTRIES context["industries"] = INDCHOICES - tags = Tags.objects.all() + tags = Tag.objects.all() tags = TagsSerailizer(tags, many=True).data context["tags"] = tags diff --git a/apiv2/__init__.py b/apiv2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiv2/main.py b/apiv2/main.py new file mode 100644 index 000000000..9fe660066 --- /dev/null +++ b/apiv2/main.py @@ -0,0 +1,36 @@ +# fastapi_app/main.py +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "crm.settings") +django.setup() + +from fastapi import FastAPI, Response +from apiv2.routers import users, leads, auth, org +from fastapi import Request + +app = FastAPI(title="CRM API", version="1.0.0") + + +@app.middleware("http") +async def log_request(request: Request, call_next): + body = await request.body() + print("Request body:", body) # or use proper logging + # response = await call_next(request) + + org = request.headers.get("org") + if org is None: + # Optionally, handle the missing header (e.g., assign a default or raise an error) + org = "default_org_value" + # Store it in request.state for later access in endpoints + request.state.org = org + response: Response = await call_next(request) + return response + +# Include routers +app.include_router(auth.router, prefix="/api/auth", tags=["Auth"]) +app.include_router(org.router, prefix="/api/org", tags=["Org"]) +app.include_router(users.router, prefix="/api/users", tags=["Users"]) +app.include_router(leads.router, prefix="/api/leads", tags=["Leads"]) +# app.include_router(leads.router, prefix="/api/leads", tags=["Leads"]) +# app.include_router(tasks.router, prefix="/api/tasks", tags=["Tasks"]) diff --git a/apiv2/routers/auth.py b/apiv2/routers/auth.py new file mode 100644 index 000000000..e315214bb --- /dev/null +++ b/apiv2/routers/auth.py @@ -0,0 +1,52 @@ +# fastapi_app/routers/users.py +from common.models import User # Django model +import requests +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel +from rest_framework_simplejwt.tokens import RefreshToken +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import BaseUserManager +router = APIRouter() + +class SocialLoginSerializer(BaseModel): + token: str + +@router.post("/google", summary="Login through Google") +def google_login(payload: SocialLoginSerializer, request: Request): + access_token = payload.token + + + # Validate token with Google + print("Google response:", access_token) + response = requests.get( + 'https://www.googleapis.com/oauth2/v2/userinfo', + params={'access_token': access_token} + ) + data = response.json() + + print("Google response data:", data) + if 'error' in data: + raise HTTPException(status_code=400, detail="Invalid or expired Google token") + + # Get or create user using Django ORM + try: + user = User.objects.get(email=data['email']) + except User.DoesNotExist: + user = User( + email=data['email'], + profile_pic=data.get('picture', ''), + password=make_password(BaseUserManager().make_random_password()) + ) + user.save() + + # Generate JWT tokens using Django's SimpleJWT + token = RefreshToken.for_user(user) + return { + "username": user.email, + "access_token": str(token.access_token), + "refresh_token": str(token), + "user_id": user.id + } + + + diff --git a/apiv2/routers/leads.py b/apiv2/routers/leads.py new file mode 100644 index 000000000..38930395e --- /dev/null +++ b/apiv2/routers/leads.py @@ -0,0 +1,1129 @@ +from typing import List, Optional, Any, Dict +from fastapi import APIRouter, Depends, HTTPException, Query, File, UploadFile, Form, status, Path, Request +from pydantic import BaseModel +import os +from datetime import datetime +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q # Added missing Q import + +from apiv2.utils import verify_token +from common.models import Attachments, Comment, Org, Profile, Tag, User, AuditLog +from leads.models import Lead, Company +from teams.models import Teams +from contacts.models import Contact +from common.utils import COUNTRIES, INDCHOICES, LEAD_SOURCE, LEAD_STATUS +from leads.tasks import create_lead_from_file, send_email_to_assigned_user + +# Pydantic models for request/response validation +class TagResponse(BaseModel): + id: int + name: str + + class Config: + from_attributes =True + +class CommentCreate(BaseModel): + comment: str + +class CommentUpdate(BaseModel): + comment: str + +class CommentResponse(BaseModel): + id: int + comment: str + commented_by: Optional[int] = None + commented_on: datetime + + class Config: + from_attributes =True + +class AttachmentResponse(BaseModel): + id: int + file_name: str + attachment: str + created_on: datetime + + class Config: + from_attributes =True + +class LeadCreate(BaseModel): + title: str + first_name: Optional[str] = None + last_name: Optional[str] = None + email: str + phone: Optional[str] = None + status: str = "open" + source: Optional[str] = None + website: Optional[str] = None + description: Optional[str] = None + address_line: Optional[str] = None + street: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + postcode: Optional[str] = None + country: Optional[str] = None + industry: Optional[str] = None + skype_ID: Optional[str] = None + probability: Optional[int] = None + opportunity_amount: Optional[int] = None + assigned_to: Optional[List[int]] = None + tags: Optional[List[int]] = None + contacts: Optional[List[int]] = None + teams: Optional[List[int]] = None + +class LeadUpdate(LeadCreate): + pass + +class LeadResponse(BaseModel): + id: int + title: str + first_name: Optional[str] = None + last_name: Optional[str] = None + email: str + status: str + created_on: datetime + + class Config: + from_attributes =True + +class CompanyCreate(BaseModel): + name: str + website: Optional[str] = None + address: Optional[str] = None + +class CompanyUpdate(CompanyCreate): + pass + +class CompanyResponse(BaseModel): + id: int + name: str + website: Optional[str] = None + created_on: datetime + + class Config: + from_attributes =True + +class SuccessResponse(BaseModel): + error: bool = False + message: str + +router = APIRouter(tags=["Leads"]) + +def force_strings(choices): + return [(key, str(value)) for key, value in choices] + + +@router.get("/meta/", response_model=Dict[str, Any]) +def get_leads( + request: Request, + token_payload: dict = Depends(verify_token) +): + + return { + "status": force_strings(LEAD_STATUS), + "source": force_strings(LEAD_SOURCE), + "countries": force_strings(COUNTRIES), + "industries": force_strings(INDCHOICES), + } + +@router.get("/", response_model=Dict[str, Any]) +def get_leads( + request: Request, + name: Optional[str] = None, + title: Optional[str] = None, + source: Optional[str] = None, + assigned_to: Optional[List[int]] = Query(None), + lead_status: Optional[str] = Query(None), + tags: Optional[List[int]] = Query(None), + city: Optional[str] = None, + email: Optional[str] = None, + limit: int = 10, + offset: int = 0, + token_payload: dict = Depends(verify_token) +): + # ... (initial authentication and profile retrieval) + + # Main queryset + queryset = Lead.objects.filter(org=request.state.org).exclude(status="converted").order_by("-id") + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user, org=request.state.org) + # Check permissions + if profile.role != "ADMIN" and not user.is_superuser: + queryset = queryset.filter(Q(assigned_to=profile) | Q(created_by=user)) + + # Apply filters using query parameters + if name: + queryset = queryset.filter(Q(first_name__icontains=name) | Q(last_name__icontains=name)) + if title: + queryset = queryset.filter(title__icontains=title) + if source: + queryset = queryset.filter(source=source) + if assigned_to: + queryset = queryset.filter(assigned_to__in=assigned_to) + if lead_status: + queryset = queryset.filter(status=lead_status) + if tags: + queryset = queryset.filter(tags__in=tags) + if city: + queryset = queryset.filter(city__icontains=city) + if email: + queryset = queryset.filter(email__icontains=email) + + # Pagination: open and closed leads + queryset_open = queryset.exclude(status="closed") + open_leads_count = queryset_open.count() + open_leads = list(queryset_open[offset:offset+limit].values()) + + queryset_closed = queryset.filter(status="closed") + closed_leads_count = queryset_closed.count() + closed_leads = list(queryset_closed[offset:offset+limit].values()) + + # Retrieve additional context + contacts = Contact.objects.filter(org=profile.org).values('id', 'first_name') + companies = Company.objects.filter(org=profile.org) + # Rename the variable to avoid conflict with the query parameter "tags" + available_tags = Tag.objects.all() + users = Profile.objects.filter(is_active=True, org=profile.org).values('id', 'user__email') + + # Log the view action using the original query parameter values + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id="list", + action="view", + data={ + "filters": { + "name": name, + "title": title, + "source": source, + "assigned_to": assigned_to, + "status": lead_status, + "tags": tags, # use the query parameter here, not the QuerySet + "city": city, + "email": email, + "limit": limit, + "offset": offset + } + }, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return { + "per_page": limit, + "page_number": (offset // limit) + 1, + "open_leads": { + "leads_count": open_leads_count, + "open_leads": open_leads, + "offset": offset + min(limit, len(open_leads)), + }, + # "close_leads": { + # "leads_count": closed_leads_count, + # "close_leads": closed_leads, + # "offset": offset + min(limit, len(closed_leads)), + # }, + "contacts": list(contacts), + # "status": LEAD_STATUS, + # "source": LEAD_SOURCE, + # "companies": list(companies.values('id', 'name')), + "tags": list(available_tags.values('id', 'name')), + "users": list(users), + # "countries": COUNTRIES, + # "industries": INDCHOICES, + } + +@router.post("/", response_model=SuccessResponse) +def create_lead( + request: Request, + lead_data: LeadCreate, + token_payload: dict = Depends(verify_token) +): + org = request.state.org + print("org:", org) + org = Org.objects.get(id=org) + """ + Create a new lead + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user, org=org) + + # Create lead + lead_obj = Lead( + title=lead_data.title, + first_name=lead_data.first_name, + last_name=lead_data.last_name, + email=lead_data.email, + phone=lead_data.phone, + status=lead_data.status, + source=lead_data.source, + website=lead_data.website, + description=lead_data.description, + address_line=lead_data.address_line, + street=lead_data.street, + city=lead_data.city, + state=lead_data.state, + postcode=lead_data.postcode, + country=lead_data.country, + industry=lead_data.industry, + skype_ID=lead_data.skype_ID, + probability=lead_data.probability, + opportunity_amount=lead_data.opportunity_amount, + org=profile.org, + created_by=user + ) + lead_obj.save() + + # Handle tags + if lead_data.tags: + tag_objs = Tag.objects.filter(id__in=lead_data.tags) + lead_obj.tags.add(*tag_objs) + + # Handle contacts + if lead_data.contacts: + contact_objs = Contact.objects.filter(id__in=lead_data.contacts) + lead_obj.contacts.add(*contact_objs) + + # Send email notification + if lead_data.assigned_to: + send_email_to_assigned_user.delay(lead_data.assigned_to, lead_obj.id) + + # Handle teams + if lead_data.teams: + team_objs = Teams.objects.filter(id__in=lead_data.teams) + lead_obj.teams.add(*team_objs) + + # Handle assigned_to + if lead_data.assigned_to: + assigned_profiles = Profile.objects.filter(id__in=lead_data.assigned_to) + lead_obj.assigned_to.add(*assigned_profiles) + + # Handle converted status + if lead_data.status == "converted": + # Conversion logic would go here + pass + + # Log the lead creation + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id=str(lead_obj.id), + action="create", + data={ + "title": lead_obj.title, + "email": lead_obj.email, + "source": lead_obj.source, + "status": lead_obj.status, + "assigned_to": list(lead_obj.assigned_to.values_list('id', flat=True)) if lead_data.assigned_to else [], + "teams": list(lead_obj.teams.values_list('id', flat=True)) if lead_data.teams else [], + "has_attachment": False # removed file attachment + }, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return {"error": False, "message": "Lead Created Successfully"} + +@router.get("/{lead_id}", response_model=Dict[str, Any]) +def get_lead_detail( + request: Request, + lead_id: int = Path(...), + token_payload: dict = Depends(verify_token) +): + """ + Get lead details by ID + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + lead_obj = Lead.objects.get(id=lead_id) + except Lead.DoesNotExist: + raise HTTPException(status_code=404, detail="Lead not found") + + # Permission check + if lead_obj.org != profile.org: + raise HTTPException(status_code=403, detail="You don't have permission to view this lead") + + user_assigned_list = [assigned.id for assigned in lead_obj.assigned_to.all()] + if user.id == lead_obj.created_by.id: + user_assigned_list.append(profile.id) + + if profile.role != "ADMIN" and not user.is_superuser: + if profile.id not in user_assigned_list: + raise HTTPException( + status_code=403, + detail="You do not have Permission to perform this action" + ) + + # Get comments and attachments + comments = Comment.objects.filter(lead=lead_obj).order_by("-id") + attachments = Attachments.objects.filter(lead=lead_obj).order_by("-id") + + # Assigned data + assigned_data = [] + for assigned in lead_obj.assigned_to.all(): + assigned_data.append({ + "id": assigned.id, + "name": assigned.user.email + }) + + # Users mention + if user.is_superuser or profile.role == "ADMIN": + users_mention = User.objects.filter( + profile__is_active=True, + profile__org=profile.org + ).values_list('email', flat=True) + elif user.id != lead_obj.created_by.id: + users_mention = [{"username": lead_obj.created_by.username}] + else: + users_mention = [assigned.user.email for assigned in lead_obj.assigned_to.all()] + + # Get users for assignment + if profile.role == "ADMIN" or user.is_superuser: + users = Profile.objects.filter( + is_active=True, + org=profile.org + ).order_by('user__email') + else: + users = Profile.objects.filter( + role="ADMIN", + org=profile.org + ).order_by('user__email') + + # Team users logic + team_ids = [user.id for user in lead_obj.get_team_users()] + all_user_ids = [user.id for user in users] + users_excluding_team_id = set(all_user_ids) - set(team_ids) + users_excluding_team = Profile.objects.filter(id__in=users_excluding_team_id) + + # Teams + teams = Teams.objects.filter(org=profile.org) + + # Log the lead view + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id=str(lead_id), + action="view", + data=None, # No specific data needed for view action + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return { + "lead_obj": { + "id": lead_obj.id, + "title": lead_obj.title, + "first_name": lead_obj.first_name, + "last_name": lead_obj.last_name, + "email": lead_obj.email, + "phone": lead_obj.phone, + "status": lead_obj.status, + "source": lead_obj.source, + "website": lead_obj.website, + "description": lead_obj.description, + # Add other fields as needed + }, + "attachments": list(attachments.values()), + "comments": list(comments.values()), + "users_mention": list(users_mention), + "assigned_data": assigned_data, + "users": list(users.values('id', 'user__email')), + "users_excluding_team": list(users_excluding_team.values('id', 'user__email')), + "source": LEAD_SOURCE, + "status": LEAD_STATUS, + "teams": list(teams.values('id', 'name')), + "countries": COUNTRIES + } + +@router.post("/{lead_id}/comment", response_model=Dict[str, Any]) +def add_lead_comment( + request: Request, + lead_id: int, + comment_data: CommentCreate, + token_payload: dict = Depends(verify_token), + attachment: Optional[UploadFile] = File(None) +): + """ + Add comment to a lead + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + lead_obj = Lead.objects.get(id=lead_id) + except Lead.DoesNotExist: + raise HTTPException(status_code=404, detail="Lead not found") + + if lead_obj.org != profile.org: + raise HTTPException( + status_code=403, + detail="User company does not match with header" + ) + + # Permission check + if profile.role != "ADMIN" and not user.is_superuser: + if not ((user.id == lead_obj.created_by.id) or + (profile in lead_obj.assigned_to.all())): + raise HTTPException( + status_code=403, + detail="You don't have permission to perform this action" + ) + + # Add comment + if comment_data.comment: + comment_obj = Comment( + comment=comment_data.comment, + lead=lead_obj, + commented_by=profile, + org=profile.org + ) + comment_obj.save() + + # Log the comment action + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Comment), + object_id=str(comment_obj.id), + action="create", + data={ + "lead_id": lead_id, + "comment": comment_data.comment, + "has_attachment": bool(attachment) + }, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + # Add attachment + if attachment: + attachment_obj = Attachments( + attachment=attachment.filename, + lead=lead_obj, + created_by=user, + file_name=attachment.filename, + org=profile.org + ) + attachment_obj.save() + + # Save file + file_content = attachment.file.read() + file_path = f"media/leads/{lead_obj.id}/{attachment.filename}" + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "wb") as f: + f.write(file_content) + + # Get updated comments and attachments + comments = Comment.objects.filter(lead=lead_obj).order_by("-id") + attachments = Attachments.objects.filter(lead=lead_obj).order_by("-id") + + return { + "lead_obj": { + "id": lead_obj.id, + "title": lead_obj.title, + # Add other fields as needed + }, + "attachments": list(attachments.values()), + "comments": list(comments.values()) + } + +@router.put("/{lead_id}", response_model=SuccessResponse) +def update_lead( + request: Request, + lead_id: int, + lead_data: LeadUpdate, + token_payload: dict = Depends(verify_token), + lead_attachment: Optional[UploadFile] = File(None) +): + """ + Update a lead + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + lead_obj = Lead.objects.get(id=lead_id) + except Lead.DoesNotExist: + raise HTTPException(status_code=404, detail="Lead not found") + + if lead_obj.org != profile.org: + raise HTTPException( + status_code=403, + detail="User company does not match with header" + ) + + # Track changes for audit log + original_data = { + "title": lead_obj.title, + "first_name": lead_obj.first_name, + "last_name": lead_obj.last_name, + "email": lead_obj.email, + "phone": lead_obj.phone, + "status": lead_obj.status, + "source": lead_obj.source, + "website": lead_obj.website, + "description": lead_obj.description, + "assigned_to": list(lead_obj.assigned_to.values_list('id', flat=True)), + "tags": list(lead_obj.tags.values_list('id', flat=True)) + } + + # Update lead attributes + previous_assigned_to = list(lead_obj.assigned_to.all().values_list('id', flat=True)) + + # Update basic fields + lead_obj.title = lead_data.title if lead_data.title else lead_obj.title + lead_obj.first_name = lead_data.first_name if lead_data.first_name is not None else lead_obj.first_name + lead_obj.last_name = lead_data.last_name if lead_data.last_name is not None else lead_obj.last_name + lead_obj.email = lead_data.email if lead_data.email else lead_obj.email + lead_obj.phone = lead_data.phone if lead_data.phone is not None else lead_obj.phone + lead_obj.status = lead_data.status if lead_data.status else lead_obj.status + lead_obj.source = lead_data.source if lead_data.source is not None else lead_obj.source + lead_obj.website = lead_data.website if lead_data.website is not None else lead_obj.website + lead_obj.description = lead_data.description if lead_data.description is not None else lead_obj.description + lead_obj.address_line = lead_data.address_line if lead_data.address_line is not None else lead_obj.address_line + lead_obj.street = lead_data.street if lead_data.street is not None else lead_obj.street + lead_obj.city = lead_data.city if lead_data.city is not None else lead_obj.city + lead_obj.state = lead_data.state if lead_data.state is not None else lead_obj.state + lead_obj.postcode = lead_data.postcode if lead_data.postcode is not None else lead_obj.postcode + lead_obj.country = lead_data.country if lead_data.country is not None else lead_obj.country + lead_obj.industry = lead_data.industry if lead_data.industry is not None else lead_obj.industry + lead_obj.opportunity_amount = lead_data.opportunity_amount if lead_data.opportunity_amount is not None else lead_obj.opportunity_amount + + lead_obj.save() + + # Handle tags + if lead_data.tags is not None: + lead_obj.tags.clear() + if lead_data.tags: + tag_objs = Tag.objects.filter(id__in=lead_data.tags) + lead_obj.tags.add(*tag_objs) + + # Handle assigned_to + if lead_data.assigned_to is not None: + lead_obj.assigned_to.clear() + if lead_data.assigned_to: + assigned_profiles = Profile.objects.filter(id__in=lead_data.assigned_to) + lead_obj.assigned_to.add(*assigned_profiles) + + # Send notification emails + new_assigned_to = list(lead_obj.assigned_to.all().values_list('id', flat=True)) + recipients = list(set(new_assigned_to) - set(previous_assigned_to)) + if recipients: + send_email_to_assigned_user.delay(recipients, lead_obj.id) + + # Handle attachment + if lead_attachment: + attachment_obj = Attachments( + attachment=lead_attachment.filename, + lead=lead_obj, + created_by=user, + file_name=lead_attachment.filename, + org=profile.org + ) + attachment_obj.save() + + # Save file + file_content = lead_attachment.file.read() + file_path = f"media/leads/{lead_obj.id}/{lead_attachment.filename}" + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "wb") as f: + f.write(file_content) + + # Handle contacts + if lead_data.contacts is not None: + lead_obj.contacts.clear() + if lead_data.contacts: + contact_objs = Contact.objects.filter(id__in=lead_data.contacts) + lead_obj.contacts.add(*contact_objs) + + # Handle teams + if lead_data.teams is not None: + lead_obj.teams.clear() + if lead_data.teams: + team_objs = Teams.objects.filter(id__in=lead_data.teams) + lead_obj.teams.add(*team_objs) + + # Handle conversion + if lead_data.status == "converted": + # Conversion logic would go here + pass + + # Prepare changed data for audit log + changed_data = {} + for field, original_value in original_data.items(): + if field == "assigned_to" and lead_data.assigned_to is not None: + changed_data[field] = lead_data.assigned_to + elif field == "tags" and lead_data.tags is not None: + changed_data[field] = lead_data.tags + elif hasattr(lead_data, field) and getattr(lead_data, field) is not None and getattr(lead_data, field) != original_value: + changed_data[field] = getattr(lead_data, field) + + # Log the lead update + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id=str(lead_id), + action="update", + data={ + "changed_fields": changed_data, + "has_attachment": bool(lead_attachment) + }, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return {"error": False, "message": "Lead updated Successfully"} + +@router.delete("/{lead_id}", response_model=SuccessResponse) +def delete_lead( + request: Request, + lead_id: int, + token_payload: dict = Depends(verify_token) +): + """ + Delete a lead + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + lead_obj = Lead.objects.get(id=lead_id) + except Lead.DoesNotExist: + raise HTTPException(status_code=404, detail="Lead not found") + + if lead_obj.org != profile.org: + raise HTTPException( + status_code=403, + detail="User company does not match with header" + ) + + # Capture lead data before deletion for audit log + lead_data = { + "id": lead_obj.id, + "title": lead_obj.title, + "email": lead_obj.email, + "status": lead_obj.status, + "created_on": str(lead_obj.created_on) + } + + # Permission check + if profile.role == "ADMIN" or user.is_superuser or user.id == lead_obj.created_by.id: + lead_obj.delete() + + # Log the lead deletion + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id=str(lead_id), + action="delete", + data=lead_data, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return {"error": False, "message": "Lead deleted Successfully"} + + raise HTTPException( + status_code=403, + detail="You don't have permission to delete this lead" + ) + +@router.post("/upload", response_model=SuccessResponse) +def upload_leads( + request: Request, + file: UploadFile = File(...), + token_payload: dict = Depends(verify_token) +): + """ + Upload leads from a file + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + # Validate file + if not file.filename.endswith(('.xls', '.xlsx', '.csv')): + raise HTTPException( + status_code=400, + detail="Invalid file format. Please upload .xls, .xlsx or .csv file" + ) + + # Process file + file_content = file.read() + file_path = f"/tmp/{file.filename}" + + # Save file temporarily + with open(file_path, "wb") as f: + f.write(file_content) + + # Create leads from file (process asynchronously) + create_lead_from_file.delay(file_path, user.id, profile.org.id) + + # Log the bulk upload action + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Lead), + object_id="bulk_upload", + action="create", + data={ + "filename": file.filename, + "file_size": file.size + }, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent", "") + ) + + return {"error": False, "message": "Leads uploaded successfully. Processing..."} + +@router.put("/comment/{comment_id}", response_model=SuccessResponse) +def update_lead_comment( + comment_id: int, + comment_data: CommentUpdate, + token_payload: dict = Depends(verify_token) +): + """ + Update a lead comment + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + comment_obj = Comment.objects.get(id=comment_id) + except Comment.DoesNotExist: + raise HTTPException(status_code=404, detail="Comment not found") + + # Permission check + if profile.role == "ADMIN" or user.is_superuser or profile.id == comment_obj.commented_by.id: + comment_obj.comment = comment_data.comment + comment_obj.save() + return {"error": False, "message": "Comment updated successfully"} + + raise HTTPException( + status_code=403, + detail="You don't have permission to perform this action" + ) + +@router.delete("/comment/{comment_id}", response_model=SuccessResponse) +def delete_lead_comment( + comment_id: int, + token_payload: dict = Depends(verify_token) +): + """ + Delete a lead comment + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + comment_obj = Comment.objects.get(id=comment_id) + except Comment.DoesNotExist: + raise HTTPException(status_code=404, detail="Comment not found") + + # Permission check + if profile.role == "ADMIN" or user.is_superuser or profile.id == comment_obj.commented_by.id: + comment_obj.delete() + return {"error": False, "message": "Comment deleted successfully"} + + raise HTTPException( + status_code=403, + detail="You don't have permission to perform this action" + ) + +@router.delete("/attachment/{attachment_id}", response_model=SuccessResponse) +def delete_lead_attachment( + attachment_id: int, + token_payload: dict = Depends(verify_token) +): + """ + Delete a lead attachment + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + attachment_obj = Attachments.objects.get(id=attachment_id) + except Attachments.DoesNotExist: + raise HTTPException(status_code=404, detail="Attachment not found") + + # Permission check + if profile.role == "ADMIN" or user.is_superuser or user.id == attachment_obj.created_by.id: + # Delete the file if exists + file_path = f"media/{attachment_obj.attachment}" + if os.path.exists(file_path): + os.remove(file_path) + + attachment_obj.delete() + return {"error": False, "message": "Attachment deleted successfully"} + + raise HTTPException( + status_code=403, + detail="You don't have permission to perform this action" + ) + +@router.post("/site-api", response_model=SuccessResponse) +def create_lead_from_site( + lead_data: LeadCreate, + apikey: str +): + """ + Create a lead from external site + """ + # Validate API key + from common.models import APISettings + try: + api_setting = APISettings.objects.get(apikey=apikey) + except APISettings.DoesNotExist: + raise HTTPException( + status_code=400, + detail="Invalid API key" + ) + + # Validate required fields + if not lead_data.email or not lead_data.title: + raise HTTPException( + status_code=400, + detail="Email and title are required fields" + ) + + # Create lead + lead_obj = Lead( + title=lead_data.title, + first_name=lead_data.first_name, + last_name=lead_data.last_name, + email=lead_data.email, + phone=lead_data.phone, + description=lead_data.description, + status="open", + source="website", + org=api_setting.org, + created_by=api_setting.created_by + ) + lead_obj.save() + + return {"error": False, "message": "Lead created successfully"} + +# Company related endpoints + +@router.get("/companies", response_model=List[dict]) +def get_companies( + token_payload: dict = Depends(verify_token) +): + """ + Get list of companies + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + companies = Company.objects.filter(org=profile.org) + return list(companies.values('id', 'name', 'website', 'created_on')) + +@router.post("/companies", response_model=Dict[str, Any]) +def create_company( + company_data: CompanyCreate, + token_payload: dict = Depends(verify_token) +): + """ + Create a new company + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + # Check if company already exists + if Company.objects.filter(name=company_data.name, org=profile.org).exists(): + return { + "error": True, + "message": "Company with this name already exists" + } + + # Create company + company_obj = Company( + name=company_data.name, + website=company_data.website, + address=company_data.address, + org=profile.org, + created_by=user + ) + company_obj.save() + + return { + "error": False, + "message": "Company created successfully", + "company_id": company_obj.id + } + +@router.get("/companies/{company_id}", response_model=Dict[str, Any]) +def get_company_detail( + company_id: int, + token_payload: dict = Depends(verify_token) +): + """ + Get company details + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + company = Company.objects.get(id=company_id) + except Company.DoesNotExist: + raise HTTPException(status_code=404, detail="Company not found") + + return { + "error": False, + "data": { + "id": company.id, + "name": company.name, + "website": company.website, + "address": company.address, + "created_on": company.created_on + } + } + +@router.put("/companies/{company_id}", response_model=SuccessResponse) +def update_company( + company_id: int, + company_data: CompanyUpdate, + token_payload: dict = Depends(verify_token) +): + """ + Update a company + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + company = Company.objects.get(id=company_id) + except Company.DoesNotExist: + raise HTTPException(status_code=404, detail="Company not found") + + # Update company fields + company.name = company_data.name if company_data.name else company.name + company.website = company_data.website if company_data.website is not None else company.website + company.address = company_data.address if company_data.address is not None else company.address + + company.save() + + return {"error": False, "message": "Company updated successfully"} + +@router.delete("/companies/{company_id}", response_model=SuccessResponse) +def delete_company( + company_id: int, + token_payload: dict = Depends(verify_token) +): + """ + Delete a company + """ + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + profile = Profile.objects.get(user=user) + + try: + company = Company.objects.get(id=company_id) + except Company.DoesNotExist: + raise HTTPException(status_code=404, detail="Company not found") + + company.delete() + + return {"error": False, "message": "Company deleted successfully"} \ No newline at end of file diff --git a/apiv2/routers/org.py b/apiv2/routers/org.py new file mode 100644 index 000000000..11bd07598 --- /dev/null +++ b/apiv2/routers/org.py @@ -0,0 +1,68 @@ +from pydantic import BaseModel +from django.core.exceptions import ValidationError +from fastapi import APIRouter, Depends, HTTPException, status +from apiv2.utils import verify_token +from common.models import Profile, User, Org, AuditLog +from django.contrib.contenttypes.models import ContentType + +router = APIRouter() +# You can update tokenUrl to match an actual token endpoint if needed + +class OrgCreate(BaseModel): + org_name: str + + +@router.get("/", summary="List all orgs") +def list_products(token_payload: dict = Depends(verify_token)): + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + user = User.objects.get(id=user_id) + profiles = Profile.objects.filter(user=user) + profiles_serialized = list(profiles.values('org_id', 'role')) # Convert queryset to list of dicts + org = Org.objects.filter(id__in=[profile['org_id'] for profile in profiles_serialized]) + org_serialized = list(org.values('id', 'name')) + print(org_serialized) + return { + "orgs": org_serialized, + } + + +@router.post("/", summary="Create orgs") +def create_org(org_data: OrgCreate, token_payload: dict = Depends(verify_token)): + user_id = token_payload.get("user_id") + if not user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User ID missing in token payload" + ) + + user = User.objects.get(id=user_id) + org = Org(name=org_data.org_name, created_by=user, updated_by=user) + + try: + org.full_clean() + except ValidationError as ve: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ve.message_dict + ) + + org.save() + Profile.objects.create(user=user, org=org, role="ADMIN") + + # Create an audit log entry + AuditLog.objects.create( + user=user, + content_type=ContentType.objects.get_for_model(Org), + object_id=org.id, + action="create", + data={"org_name": org_data.org_name}, + ip_address=token_payload.get("ip_address"), + user_agent=token_payload.get("user_agent"), + ) + + return {"org": org} diff --git a/apiv2/routers/tasks.py b/apiv2/routers/tasks.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiv2/routers/users.py b/apiv2/routers/users.py new file mode 100644 index 000000000..7427259fb --- /dev/null +++ b/apiv2/routers/users.py @@ -0,0 +1,9 @@ +# fastapi_app/routers/users.py +from fastapi import APIRouter +from common.models import User # Django model + +router = APIRouter() + +@router.get("/") +def list_users(): + return [{"id": user.id, "email": user.email} for user in User.objects.all()] diff --git a/apiv2/schemas/user.py b/apiv2/schemas/user.py new file mode 100644 index 000000000..80fd92a1e --- /dev/null +++ b/apiv2/schemas/user.py @@ -0,0 +1,9 @@ +# fastapi_app/schemas/user.py +from pydantic import BaseModel + +class UserOut(BaseModel): + id: int + email: str + + class Config: + from_attributes =True diff --git a/apiv2/utils.py b/apiv2/utils.py new file mode 100644 index 000000000..aed1d7c71 --- /dev/null +++ b/apiv2/utils.py @@ -0,0 +1,27 @@ +# fastapi_app/main.py +import os +import django +# fastapi_app/routers/products.py +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from rest_framework_simplejwt.backends import TokenBackend +from rest_framework_simplejwt.exceptions import TokenError, InvalidToken, TokenBackendError +from crm.settings import SECRET_KEY # Use Django settings for shared configuration + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/google") + +# Use Django's secret key and the SIMPLE_JWT algorithm configuration (defaulting to HS256) +ALGORITHM = 'HS256' + +def verify_token(token: str = Depends(oauth2_scheme)): + token_backend = TokenBackend(algorithm=ALGORITHM, signing_key=SECRET_KEY) + try: + # Decode the token and verify its signature and claims. + validated_token = token_backend.decode(token, verify=True) + return validated_token + except (TokenError, InvalidToken, TokenBackendError): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + headers={"WWW-Authenticate": "Bearer"} + ) \ No newline at end of file diff --git a/cases/migrations/0001_initial.py b/cases/migrations/0001_initial.py index accb8d224..9c9e9c6b7 100644 --- a/cases/migrations/0001_initial.py +++ b/cases/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.db import migrations, models import django.db.models.deletion diff --git a/cases/migrations/0002_initial.py b/cases/migrations/0002_initial.py index 36256f5a4..10e2813ac 100644 --- a/cases/migrations/0002_initial.py +++ b/cases/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -10,11 +10,11 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('teams', '0001_initial'), - ('contacts', '0001_initial'), ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('cases', '0001_initial'), + ('contacts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('teams', '0001_initial'), ] operations = [ @@ -31,7 +31,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='case', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='case_created_by', to='common.profile'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), ), migrations.AddField( model_name='case', diff --git a/cases/migrations/0003_alter_case_created_by.py b/cases/migrations/0003_alter_case_created_by.py deleted file mode 100644 index 350a4bfd9..000000000 --- a/cases/migrations/0003_alter_case_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('cases', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='case', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/cms/migrations/0001_initial.py b/cms/migrations/0001_initial.py index 4d054cb55..f9cd88c21 100644 --- a/cms/migrations/0001_initial.py +++ b/cms/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 import cms.blocks from django.db import migrations, models diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py index 15f3db3ea..97999224c 100644 --- a/common/migrations/0001_initial.py +++ b/common/migrations/0001_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 import common.models from django.conf import settings -import django.contrib.auth.models from django.db import migrations, models import django.db.models.deletion import phonenumber_field.modelfields @@ -14,6 +13,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), ] operations = [ @@ -25,10 +25,11 @@ class Migration(migrations.Migration): ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')), - ('profile_pic', models.FileField(blank=True, max_length=1000, null=True, upload_to=common.models.img_url)), + ('profile_pic', models.CharField(blank=True, max_length=1000, null=True)), ('activation_key', models.CharField(blank=True, max_length=150, null=True)), ('key_expires', models.DateTimeField(blank=True, null=True)), ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False, verbose_name='staff status')), ], options={ 'verbose_name': 'User', @@ -36,9 +37,6 @@ class Migration(migrations.Migration): 'db_table': 'users', 'ordering': ('-is_active',), }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], ), migrations.CreateModel( name='Address', @@ -93,6 +91,25 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), + migrations.CreateModel( + name='AuditLog', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('object_id', models.CharField(max_length=255)), + ('action', models.CharField(choices=[('create', 'Created'), ('update', 'Updated'), ('delete', 'Deleted'), ('view', 'Viewed'), ('export', 'Exported')], max_length=50)), + ('data', models.JSONField(blank=True, null=True)), + ('ip_address', models.GenericIPAddressField(blank=True, null=True)), + ('user_agent', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'verbose_name': 'Audit Log', + 'verbose_name_plural': 'Audit Logs', + 'db_table': 'audit_logs', + 'ordering': ('-created_at',), + }, + ), migrations.CreateModel( name='Comment', fields=[ @@ -125,13 +142,53 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), + migrations.CreateModel( + name='CustomField', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=100)), + ('field_type', models.CharField(choices=[('text', 'Text'), ('number', 'Number'), ('date', 'Date'), ('boolean', 'Boolean'), ('dropdown', 'Dropdown'), ('email', 'Email'), ('url', 'URL'), ('phone', 'Phone')], max_length=20)), + ('model_name', models.CharField(choices=[('lead', 'Lead'), ('account', 'Account'), ('contact', 'Contact'), ('opportunity', 'Opportunity'), ('case', 'Case'), ('task', 'Task'), ('event', 'Event')], max_length=50)), + ('required', models.BooleanField(default=False)), + ('choices', models.JSONField(blank=True, null=True)), + ('default_value', models.JSONField(blank=True, null=True)), + ('help_text', models.CharField(blank=True, max_length=255)), + ('is_active', models.BooleanField(default=True)), + ('display_order', models.PositiveIntegerField(default=0)), + ], + options={ + 'verbose_name': 'Custom Field', + 'verbose_name_plural': 'Custom Fields', + 'db_table': 'custom_fields', + 'ordering': ('display_order', 'name'), + }, + ), + migrations.CreateModel( + name='CustomFieldValue', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('object_id', models.CharField(max_length=255)), + ('value', models.JSONField(null=True)), + ], + options={ + 'verbose_name': 'Custom Field Value', + 'verbose_name_plural': 'Custom Field Values', + 'db_table': 'custom_field_values', + }, + ), migrations.CreateModel( name='Org', fields=[ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('name', models.CharField(blank=True, max_length=100, null=True)), + ('name', models.CharField(error_messages={'max_length': 'Organization name must be at most 100 characters long.'}, max_length=100)), + ('api_key', models.TextField(default=common.models.generate_unique_key, editable=False, unique=True)), + ('is_active', models.BooleanField(default=True)), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ], @@ -142,6 +199,66 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), + migrations.CreateModel( + name='Tag', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('name', models.CharField(max_length=100)), + ('color', models.CharField(default='#808080', max_length=7)), + ('description', models.CharField(blank=True, max_length=255)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='common.org')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + 'db_table': 'tags', + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='UserPreference', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('theme', models.CharField(choices=[('light', 'Light'), ('dark', 'Dark'), ('system', 'System Default')], default='light', max_length=20)), + ('email_notifications', models.BooleanField(default=True)), + ('dashboard_layout', models.JSONField(blank=True, default=dict)), + ('timezone', models.CharField(default='UTC', max_length=50)), + ('language', models.CharField(default='en-us', max_length=10)), + ('items_per_page', models.PositiveIntegerField(default=25)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'User Preference', + 'verbose_name_plural': 'User Preferences', + 'db_table': 'user_preferences', + }, + ), + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('object_id', models.CharField(max_length=255)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='common.tag')), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ], + options={ + 'verbose_name': 'Tagged Item', + 'verbose_name_plural': 'Tagged Items', + 'db_table': 'tagged_items', + }, + ), migrations.CreateModel( name='Profile', fields=[ @@ -149,7 +266,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), ('phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)), - ('alternate_phone', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)), + ('alternate_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None)), ('role', models.CharField(choices=[('ADMIN', 'ADMIN'), ('USER', 'USER')], default='USER', max_length=50)), ('has_sales_access', models.BooleanField(default=False)), ('has_marketing_access', models.BooleanField(default=False)), @@ -169,6 +286,30 @@ class Migration(migrations.Migration): 'ordering': ('-created_at',), }, ), + migrations.CreateModel( + name='Notification', + fields=[ + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('title', models.CharField(max_length=200)), + ('message', models.TextField()), + ('link', models.CharField(blank=True, max_length=255, null=True)), + ('read', models.BooleanField(default=False)), + ('notification_type', models.CharField(choices=[('system', 'System'), ('assignment', 'Assignment'), ('mention', 'Mention'), ('due_date', 'Due Date'), ('follow_up', 'Follow Up'), ('comment', 'Comment')], max_length=20)), + ('object_id', models.CharField(blank=True, max_length=255, null=True)), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), + ], + options={ + 'verbose_name': 'Notification', + 'verbose_name_plural': 'Notifications', + 'db_table': 'notifications', + 'ordering': ('-created_at',), + }, + ), migrations.CreateModel( name='Document', fields=[ diff --git a/common/migrations/0002_initial.py b/common/migrations/0002_initial.py index aa21817c1..b9e624fe7 100644 --- a/common/migrations/0002_initial.py +++ b/common/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -10,17 +10,18 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('leads', '0001_initial'), - ('invoices', '0001_initial'), - ('teams', '0001_initial'), - ('auth', '0012_alter_user_first_name_max_length'), ('accounts', '0002_initial'), - ('opportunity', '0001_initial'), - ('contacts', '0001_initial'), ('common', '0001_initial'), + ('events', '0001_initial'), + ('contacts', '0001_initial'), ('tasks', '0001_initial'), ('cases', '0002_initial'), - ('events', '0001_initial'), + ('teams', '0001_initial'), + ('leads', '0001_initial'), + ('invoices', '0001_initial'), + ('opportunity', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -34,6 +35,41 @@ class Migration(migrations.Migration): name='updated_by', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), ), + migrations.AddField( + model_name='customfieldvalue', + name='content_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='customfieldvalue', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AddField( + model_name='customfieldvalue', + name='custom_field', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='common.customfield'), + ), + migrations.AddField( + model_name='customfieldvalue', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AddField( + model_name='customfield', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AddField( + model_name='customfield', + name='org', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='common.org'), + ), + migrations.AddField( + model_name='customfield', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), migrations.AddField( model_name='commentfiles', name='comment', @@ -109,6 +145,26 @@ class Migration(migrations.Migration): name='updated_by', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), ), + migrations.AddField( + model_name='auditlog', + name='content_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='auditlog', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), + ), + migrations.AddField( + model_name='auditlog', + name='updated_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By'), + ), + migrations.AddField( + model_name='auditlog', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL), + ), migrations.AddField( model_name='attachments', name='account', @@ -127,7 +183,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='attachments', name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='attachment_created_by', to='common.profile'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='attachment_created_by', to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='attachments', @@ -174,11 +230,6 @@ class Migration(migrations.Migration): name='org', field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='org_api_settings', to='common.org'), ), - migrations.AddField( - model_name='apisettings', - name='tags', - field=models.ManyToManyField(blank=True, to='accounts.tags'), - ), migrations.AddField( model_name='apisettings', name='updated_by', @@ -204,8 +255,24 @@ class Migration(migrations.Migration): name='user_permissions', field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), ), + migrations.AlterUniqueTogether( + name='taggeditem', + unique_together={('tag', 'content_type', 'object_id')}, + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together={('org', 'name')}, + ), migrations.AlterUniqueTogether( name='profile', unique_together={('user', 'org')}, ), + migrations.AlterUniqueTogether( + name='customfieldvalue', + unique_together={('custom_field', 'content_type', 'object_id')}, + ), + migrations.AlterUniqueTogether( + name='customfield', + unique_together={('org', 'name', 'model_name')}, + ), ] diff --git a/common/migrations/0003_alter_user_profile_pic.py b/common/migrations/0003_alter_user_profile_pic.py deleted file mode 100644 index c6fa937f8..000000000 --- a/common/migrations/0003_alter_user_profile_pic.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-12 09:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0002_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='profile_pic', - field=models.CharField(blank=True, max_length=1000, null=True), - ), - ] diff --git a/common/migrations/0004_alter_profile_alternate_phone.py b/common/migrations/0004_alter_profile_alternate_phone.py deleted file mode 100644 index 7a83303a0..000000000 --- a/common/migrations/0004_alter_profile_alternate_phone.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-21 11:23 - -from django.db import migrations -import phonenumber_field.modelfields - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0003_alter_user_profile_pic'), - ] - - operations = [ - migrations.AlterField( - model_name='profile', - name='alternate_phone', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), - ), - ] diff --git a/common/migrations/0005_org_api_key.py b/common/migrations/0005_org_api_key.py deleted file mode 100644 index 2fb1db59e..000000000 --- a/common/migrations/0005_org_api_key.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:19 - -import common.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0004_alter_profile_alternate_phone'), - ] - - operations = [ - migrations.AddField( - model_name='org', - name='api_key', - field=models.TextField(default=common.models.generate_unique_key, editable=False), - ), - ] diff --git a/common/migrations/0006_alter_org_api_key.py b/common/migrations/0006_alter_org_api_key.py deleted file mode 100644 index fb672699d..000000000 --- a/common/migrations/0006_alter_org_api_key.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:20 - -import common.models -from django.db import migrations, models -import uuid - -def generate_unique_key(): - return str(uuid.uuid4()) - -def set_unique_api_keys(apps, schema_editor): - Org = apps.get_model('common', 'Org') - for org in Org.objects.all(): - org.api_key = generate_unique_key() - org.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0005_org_api_key'), - ] - - operations = [ - migrations.RunPython(set_unique_api_keys), - migrations.AlterField( - model_name='org', - name='api_key', - field=models.TextField(default=common.models.generate_unique_key, editable=False, unique=True), - ), - ] diff --git a/common/migrations/0007_org_is_active.py b/common/migrations/0007_org_is_active.py deleted file mode 100644 index 0f274f212..000000000 --- a/common/migrations/0007_org_is_active.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-02 11:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0006_alter_org_api_key'), - ] - - operations = [ - migrations.AddField( - model_name='org', - name='is_active', - field=models.BooleanField(default=True), - ), - ] diff --git a/common/migrations/0008_alter_user_managers.py b/common/migrations/0008_alter_user_managers.py deleted file mode 100644 index a54700998..000000000 --- a/common/migrations/0008_alter_user_managers.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2024-02-14 06:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0007_org_is_active'), - ] - - operations = [ - migrations.AlterModelManagers( - name='user', - managers=[ - ], - ), - ] diff --git a/common/migrations/0009_user_is_staff.py b/common/migrations/0009_user_is_staff.py deleted file mode 100644 index 99541d50e..000000000 --- a/common/migrations/0009_user_is_staff.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2024-02-14 07:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0008_alter_user_managers'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='is_staff', - field=models.BooleanField(default=False, verbose_name='staff status'), - ), - ] diff --git a/common/models.py b/common/models.py index ad158fe77..6782cbaa4 100644 --- a/common/models.py +++ b/common/models.py @@ -11,6 +11,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey from common.templatetags.common_tags import ( is_document_file_audio, @@ -64,6 +66,29 @@ def __str__(self): # self.key_expires = timezone.now() + datetime.timedelta(hours=2) # super().save(*args, **kwargs) + +class UserPreference(BaseModel): + """ + Stores user-specific preferences for UI and functionality + """ + user = models.OneToOneField(User, related_name="preferences", on_delete=models.CASCADE) + theme = models.CharField(max_length=20, default="light", choices=( + ("light", "Light"), + ("dark", "Dark"), + ("system", "System Default") + )) + email_notifications = models.BooleanField(default=True) + dashboard_layout = models.JSONField(default=dict, blank=True) + timezone = models.CharField(max_length=50, default="UTC") + language = models.CharField(max_length=10, default="en-us") + items_per_page = models.PositiveIntegerField(default=25) + + class Meta: + verbose_name = "User Preference" + verbose_name_plural = "User Preferences" + db_table = "user_preferences" + + class Address(BaseModel): address_line = models.CharField( _("Address"), max_length=255, blank=True, default="" @@ -116,11 +141,16 @@ def get_complete_address(self): address += self.get_country_display() return address + def generate_unique_key(): return str(uuid.uuid4()) + class Org(BaseModel): - name = models.CharField(max_length=100, blank=True, null=True) + name = models.CharField(max_length=100, + error_messages={ + 'max_length': 'Organization name must be at most 100 characters long.' + }) api_key = models.TextField( default=generate_unique_key, unique=True, editable=False ) @@ -139,54 +169,6 @@ def __str__(self): return str(self.name) -# class User(AbstractBaseUser, PermissionsMixin): -# email = models.EmailField(_("email address"), blank=True, unique=True) -# profile_pic = models.FileField( -# max_length=1000, upload_to=img_url, null=True, blank=True -# ) -# activation_key = models.CharField(max_length=150, null=True, blank=True) -# key_expires = models.DateTimeField(null=True, blank=True) - - -# USERNAME_FIELD = "email" -# REQUIRED_FIELDS = ["username"] - -# objects = UserManager() - -# def get_short_name(self): -# return self.username - -# def documents(self): -# return self.document_uploaded.all() - -# def get_full_name(self): -# full_name = None -# if self.first_name or self.last_name: -# full_name = self.first_name + " " + self.last_name -# elif self.username: -# full_name = self.username -# else: -# full_name = self.email -# return full_name - -# @property -# def created_on_arrow(self): -# return arrow.get(self.date_joined).humanize() - -# class Meta: -# ordering = ["-is_active"] - -# def __str__(self): -# return self.email - -# def save(self, *args, **kwargs): -# """by default the expiration time is set to 2 hours""" -# self.key_expires = timezone.now() + datetime.timedelta(hours=2) -# super().save(*args, **kwargs) - - - - class Profile(BaseModel): user = models.ForeignKey(User, on_delete=models.CASCADE) org = models.ForeignKey( @@ -280,7 +262,6 @@ class Comment(BaseModel): related_name="user_comments", on_delete=models.CASCADE, ) - task = models.ForeignKey( "tasks.Task", blank=True, @@ -288,7 +269,6 @@ class Comment(BaseModel): related_name="tasks_comments", on_delete=models.CASCADE, ) - invoice = models.ForeignKey( "invoices.Invoice", blank=True, @@ -296,7 +276,6 @@ class Comment(BaseModel): related_name="invoice_comments", on_delete=models.CASCADE, ) - event = models.ForeignKey( "events.Event", blank=True, @@ -390,7 +369,6 @@ class Attachments(BaseModel): on_delete=models.CASCADE, related_name="case_attachment", ) - task = models.ForeignKey( "tasks.Task", blank=True, @@ -398,7 +376,6 @@ class Attachments(BaseModel): related_name="tasks_attachment", on_delete=models.CASCADE, ) - invoice = models.ForeignKey( "invoices.Invoice", blank=True, @@ -462,7 +439,6 @@ def document_path(self, filename): class Document(BaseModel): - DOCUMENT_STATUS_CHOICE = (("active", "active"), ("inactive", "inactive")) title = models.TextField(blank=True, null=True) @@ -474,7 +450,6 @@ class Document(BaseModel): null=True, blank=True, ) - status = models.CharField( choices=DOCUMENT_STATUS_CHOICE, max_length=64, default="active" ) @@ -555,7 +530,6 @@ class APISettings(BaseModel): lead_assigned_to = models.ManyToManyField( Profile, related_name="lead_assignee_users" ) - tags = models.ManyToManyField("accounts.Tags", blank=True) created_by = models.ForeignKey( Profile, related_name="settings_created_by", @@ -570,7 +544,6 @@ class APISettings(BaseModel): null=True, related_name="org_api_settings", ) - class Meta: verbose_name = "APISetting" @@ -580,9 +553,170 @@ class Meta: def __str__(self): return f"{self.title}" - def save(self, *args, **kwargs): if not self.apikey or self.apikey is None or self.apikey == "": self.apikey = generate_key() super().save(*args, **kwargs) + + +class AuditLog(BaseModel): + """ + Comprehensive audit trail system to track all user actions + """ + user = models.ForeignKey(User, related_name="audit_logs", on_delete=models.SET_NULL, null=True) + content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE) + object_id = models.CharField(max_length=255) + content_object = GenericForeignKey('content_type', 'object_id') + action = models.CharField(max_length=50, choices=( + ('create', 'Created'), + ('update', 'Updated'), + ('delete', 'Deleted'), + ('view', 'Viewed'), + ('export', 'Exported'), + )) + data = models.JSONField(null=True, blank=True) # Store changed fields + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.CharField(max_length=255, blank=True, null=True) + + class Meta: + verbose_name = "Audit Log" + verbose_name_plural = "Audit Logs" + db_table = "audit_logs" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.user} {self.action} {self.content_type} - {self.created_at}" + + +class Tag(BaseModel): + """ + Enhanced tagging system with color support and organization scoping + """ + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="tags") + name = models.CharField(max_length=100) + color = models.CharField(max_length=7, default="#808080") # Hex color code + description = models.CharField(max_length=255, blank=True) + + class Meta: + verbose_name = "Tag" + verbose_name_plural = "Tags" + db_table = "tags" + unique_together = ["org", "name"] + ordering = ("name",) + + def __str__(self): + return self.name + + +class TaggedItem(BaseModel): + """ + Associates tags with any model using generic relations + """ + tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name="tagged_items") + content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE) + object_id = models.CharField(max_length=255) + content_object = GenericForeignKey('content_type', 'object_id') + + class Meta: + verbose_name = "Tagged Item" + verbose_name_plural = "Tagged Items" + db_table = "tagged_items" + unique_together = ["tag", "content_type", "object_id"] + + def __str__(self): + return f"{self.tag.name} - {self.content_type.name}" + + +class CustomField(BaseModel): + """ + Allow administrators to define custom fields for different entity types + """ + org = models.ForeignKey(Org, on_delete=models.CASCADE, related_name="custom_fields") + name = models.CharField(max_length=100) + field_type = models.CharField(max_length=20, choices=( + ("text", "Text"), + ("number", "Number"), + ("date", "Date"), + ("boolean", "Boolean"), + ("dropdown", "Dropdown"), + ("email", "Email"), + ("url", "URL"), + ("phone", "Phone"), + )) + model_name = models.CharField(max_length=50, choices=( + ("lead", "Lead"), + ("account", "Account"), + ("contact", "Contact"), + ("opportunity", "Opportunity"), + ("case", "Case"), + ("task", "Task"), + ("event", "Event"), + )) + required = models.BooleanField(default=False) + choices = models.JSONField(blank=True, null=True) # For dropdown type + default_value = models.JSONField(blank=True, null=True) + help_text = models.CharField(max_length=255, blank=True) + is_active = models.BooleanField(default=True) + display_order = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name = "Custom Field" + verbose_name_plural = "Custom Fields" + db_table = "custom_fields" + unique_together = ["org", "name", "model_name"] + ordering = ("display_order", "name") + + def __str__(self): + return f"{self.name} ({self.get_model_name_display()})" + + +class CustomFieldValue(BaseModel): + """ + Stores the values for custom fields + """ + custom_field = models.ForeignKey(CustomField, on_delete=models.CASCADE) + content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE) + object_id = models.CharField(max_length=255) + content_object = GenericForeignKey('content_type', 'object_id') + value = models.JSONField(null=True) + + class Meta: + verbose_name = "Custom Field Value" + verbose_name_plural = "Custom Field Values" + db_table = "custom_field_values" + unique_together = ["custom_field", "content_type", "object_id"] + + def __str__(self): + return f"{self.custom_field.name}: {self.value}" + + +class Notification(BaseModel): + """ + Comprehensive notification system for users + """ + recipient = models.ForeignKey(User, related_name="notifications", on_delete=models.CASCADE) + title = models.CharField(max_length=200) + message = models.TextField() + link = models.CharField(max_length=255, null=True, blank=True) + read = models.BooleanField(default=False) + notification_type = models.CharField(max_length=20, choices=( + ("system", "System"), + ("assignment", "Assignment"), + ("mention", "Mention"), + ("due_date", "Due Date"), + ("follow_up", "Follow Up"), + ("comment", "Comment"), + )) + content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE, null=True, blank=True) + object_id = models.CharField(max_length=255, null=True, blank=True) + content_object = GenericForeignKey('content_type', 'object_id') + + class Meta: + verbose_name = "Notification" + verbose_name_plural = "Notifications" + db_table = "notifications" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.title} - {self.recipient.email}" diff --git a/common/utils.py b/common/utils.py index 908169c95..b2b8f37a6 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,5 +1,4 @@ import pytz -from django.utils.translation import gettext_lazy as _ def jwt_payload_handler(user): @@ -28,19 +27,19 @@ def jwt_payload_handler(user): INDCHOICES = ( ("ADVERTISING", "ADVERTISING"), ("AGRICULTURE", "AGRICULTURE"), - ("APPAREL & ACCESSORIES", "APPAREL & ACCESSORIES"), + ("APPAREL and ACCESSORIES", "APPAREL and ACCESSORIES"), ("AUTOMOTIVE", "AUTOMOTIVE"), ("BANKING", "BANKING"), ("BIOTECHNOLOGY", "BIOTECHNOLOGY"), - ("BUILDING MATERIALS & EQUIPMENT", "BUILDING MATERIALS & EQUIPMENT"), + ("BUILDING MATERIALS and EQUIPMENT", "BUILDING MATERIALS and EQUIPMENT"), ("CHEMICAL", "CHEMICAL"), ("COMPUTER", "COMPUTER"), ("EDUCATION", "EDUCATION"), ("ELECTRONICS", "ELECTRONICS"), ("ENERGY", "ENERGY"), - ("ENTERTAINMENT & LEISURE", "ENTERTAINMENT & LEISURE"), + ("ENTERTAINMENT and LEISURE", "ENTERTAINMENT and LEISURE"), ("FINANCE", "FINANCE"), - ("FOOD & BEVERAGE", "FOOD & BEVERAGE"), + ("FOOD and BEVERAGE", "FOOD and BEVERAGE"), ("GROCERY", "GROCERY"), ("HEALTHCARE", "HEALTHCARE"), ("INSURANCE", "INSURANCE"), @@ -71,6 +70,7 @@ def jwt_payload_handler(user): ) LEAD_STATUS = ( + ("new", "New"), ("assigned", "Assigned"), ("in process", "In Process"), ("converted", "Converted"), @@ -146,412 +146,412 @@ def jwt_payload_handler(user): COUNTRIES = ( - ("GB", _("United Kingdom")), - ("AF", _("Afghanistan")), - ("AX", _("Aland Islands")), - ("AL", _("Albania")), - ("DZ", _("Algeria")), - ("AS", _("American Samoa")), - ("AD", _("Andorra")), - ("AO", _("Angola")), - ("AI", _("Anguilla")), - ("AQ", _("Antarctica")), - ("AG", _("Antigua and Barbuda")), - ("AR", _("Argentina")), - ("AM", _("Armenia")), - ("AW", _("Aruba")), - ("AU", _("Australia")), - ("AT", _("Austria")), - ("AZ", _("Azerbaijan")), - ("BS", _("Bahamas")), - ("BH", _("Bahrain")), - ("BD", _("Bangladesh")), - ("BB", _("Barbados")), - ("BY", _("Belarus")), - ("BE", _("Belgium")), - ("BZ", _("Belize")), - ("BJ", _("Benin")), - ("BM", _("Bermuda")), - ("BT", _("Bhutan")), - ("BO", _("Bolivia")), - ("BA", _("Bosnia and Herzegovina")), - ("BW", _("Botswana")), - ("BV", _("Bouvet Island")), - ("BR", _("Brazil")), - ("IO", _("British Indian Ocean Territory")), - ("BN", _("Brunei Darussalam")), - ("BG", _("Bulgaria")), - ("BF", _("Burkina Faso")), - ("BI", _("Burundi")), - ("KH", _("Cambodia")), - ("CM", _("Cameroon")), - ("CA", _("Canada")), - ("CV", _("Cape Verde")), - ("KY", _("Cayman Islands")), - ("CF", _("Central African Republic")), - ("TD", _("Chad")), - ("CL", _("Chile")), - ("CN", _("China")), - ("CX", _("Christmas Island")), - ("CC", _("Cocos (Keeling) Islands")), - ("CO", _("Colombia")), - ("KM", _("Comoros")), - ("CG", _("Congo")), - ("CD", _("Congo, The Democratic Republic of the")), - ("CK", _("Cook Islands")), - ("CR", _("Costa Rica")), - ("CI", _("Cote d'Ivoire")), - ("HR", _("Croatia")), - ("CU", _("Cuba")), - ("CY", _("Cyprus")), - ("CZ", _("Czech Republic")), - ("DK", _("Denmark")), - ("DJ", _("Djibouti")), - ("DM", _("Dominica")), - ("DO", _("Dominican Republic")), - ("EC", _("Ecuador")), - ("EG", _("Egypt")), - ("SV", _("El Salvador")), - ("GQ", _("Equatorial Guinea")), - ("ER", _("Eritrea")), - ("EE", _("Estonia")), - ("ET", _("Ethiopia")), - ("FK", _("Falkland Islands (Malvinas)")), - ("FO", _("Faroe Islands")), - ("FJ", _("Fiji")), - ("FI", _("Finland")), - ("FR", _("France")), - ("GF", _("French Guiana")), - ("PF", _("French Polynesia")), - ("TF", _("French Southern Territories")), - ("GA", _("Gabon")), - ("GM", _("Gambia")), - ("GE", _("Georgia")), - ("DE", _("Germany")), - ("GH", _("Ghana")), - ("GI", _("Gibraltar")), - ("GR", _("Greece")), - ("GL", _("Greenland")), - ("GD", _("Grenada")), - ("GP", _("Guadeloupe")), - ("GU", _("Guam")), - ("GT", _("Guatemala")), - ("GG", _("Guernsey")), - ("GN", _("Guinea")), - ("GW", _("Guinea-Bissau")), - ("GY", _("Guyana")), - ("HT", _("Haiti")), - ("HM", _("Heard Island and McDonald Islands")), - ("VA", _("Holy See (Vatican City State)")), - ("HN", _("Honduras")), - ("HK", _("Hong Kong")), - ("HU", _("Hungary")), - ("IS", _("Iceland")), - ("IN", _("India")), - ("ID", _("Indonesia")), - ("IR", _("Iran, Islamic Republic of")), - ("IQ", _("Iraq")), - ("IE", _("Ireland")), - ("IM", _("Isle of Man")), - ("IL", _("Israel")), - ("IT", _("Italy")), - ("JM", _("Jamaica")), - ("JP", _("Japan")), - ("JE", _("Jersey")), - ("JO", _("Jordan")), - ("KZ", _("Kazakhstan")), - ("KE", _("Kenya")), - ("KI", _("Kiribati")), - ("KP", _("Korea, Democratic People's Republic of")), - ("KR", _("Korea, Republic of")), - ("KW", _("Kuwait")), - ("KG", _("Kyrgyzstan")), - ("LA", _("Lao People's Democratic Republic")), - ("LV", _("Latvia")), - ("LB", _("Lebanon")), - ("LS", _("Lesotho")), - ("LR", _("Liberia")), - ("LY", _("Libyan Arab Jamahiriya")), - ("LI", _("Liechtenstein")), - ("LT", _("Lithuania")), - ("LU", _("Luxembourg")), - ("MO", _("Macao")), - ("MK", _("Macedonia, The Former Yugoslav Republic of")), - ("MG", _("Madagascar")), - ("MW", _("Malawi")), - ("MY", _("Malaysia")), - ("MV", _("Maldives")), - ("ML", _("Mali")), - ("MT", _("Malta")), - ("MH", _("Marshall Islands")), - ("MQ", _("Martinique")), - ("MR", _("Mauritania")), - ("MU", _("Mauritius")), - ("YT", _("Mayotte")), - ("MX", _("Mexico")), - ("FM", _("Micronesia, Federated States of")), - ("MD", _("Moldova")), - ("MC", _("Monaco")), - ("MN", _("Mongolia")), - ("ME", _("Montenegro")), - ("MS", _("Montserrat")), - ("MA", _("Morocco")), - ("MZ", _("Mozambique")), - ("MM", _("Myanmar")), - ("NA", _("Namibia")), - ("NR", _("Nauru")), - ("NP", _("Nepal")), - ("NL", _("Netherlands")), - ("AN", _("Netherlands Antilles")), - ("NC", _("New Caledonia")), - ("NZ", _("New Zealand")), - ("NI", _("Nicaragua")), - ("NE", _("Niger")), - ("NG", _("Nigeria")), - ("NU", _("Niue")), - ("NF", _("Norfolk Island")), - ("MP", _("Northern Mariana Islands")), - ("NO", _("Norway")), - ("OM", _("Oman")), - ("PK", _("Pakistan")), - ("PW", _("Palau")), - ("PS", _("Palestinian Territory, Occupied")), - ("PA", _("Panama")), - ("PG", _("Papua New Guinea")), - ("PY", _("Paraguay")), - ("PE", _("Peru")), - ("PH", _("Philippines")), - ("PN", _("Pitcairn")), - ("PL", _("Poland")), - ("PT", _("Portugal")), - ("PR", _("Puerto Rico")), - ("QA", _("Qatar")), - ("RE", _("Reunion")), - ("RO", _("Romania")), - ("RU", _("Russian Federation")), - ("RW", _("Rwanda")), - ("BL", _("Saint Barthelemy")), - ("SH", _("Saint Helena")), - ("KN", _("Saint Kitts and Nevis")), - ("LC", _("Saint Lucia")), - ("MF", _("Saint Martin")), - ("PM", _("Saint Pierre and Miquelon")), - ("VC", _("Saint Vincent and the Grenadines")), - ("WS", _("Samoa")), - ("SM", _("San Marino")), - ("ST", _("Sao Tome and Principe")), - ("SA", _("Saudi Arabia")), - ("SN", _("Senegal")), - ("RS", _("Serbia")), - ("SC", _("Seychelles")), - ("SL", _("Sierra Leone")), - ("SG", _("Singapore")), - ("SK", _("Slovakia")), - ("SI", _("Slovenia")), - ("SB", _("Solomon Islands")), - ("SO", _("Somalia")), - ("ZA", _("South Africa")), - ("GS", _("South Georgia and the South Sandwich Islands")), - ("ES", _("Spain")), - ("LK", _("Sri Lanka")), - ("SD", _("Sudan")), - ("SR", _("Suriname")), - ("SJ", _("Svalbard and Jan Mayen")), - ("SZ", _("Swaziland")), - ("SE", _("Sweden")), - ("CH", _("Switzerland")), - ("SY", _("Syrian Arab Republic")), - ("TW", _("Taiwan, Province of China")), - ("TJ", _("Tajikistan")), - ("TZ", _("Tanzania, United Republic of")), - ("TH", _("Thailand")), - ("TL", _("Timor-Leste")), - ("TG", _("Togo")), - ("TK", _("Tokelau")), - ("TO", _("Tonga")), - ("TT", _("Trinidad and Tobago")), - ("TN", _("Tunisia")), - ("TR", _("Turkey")), - ("TM", _("Turkmenistan")), - ("TC", _("Turks and Caicos Islands")), - ("TV", _("Tuvalu")), - ("UG", _("Uganda")), - ("UA", _("Ukraine")), - ("AE", _("United Arab Emirates")), - ("US", _("United States")), - ("UM", _("United States Minor Outlying Islands")), - ("UY", _("Uruguay")), - ("UZ", _("Uzbekistan")), - ("VU", _("Vanuatu")), - ("VE", _("Venezuela")), - ("VN", _("Viet Nam")), - ("VG", _("Virgin Islands, British")), - ("VI", _("Virgin Islands, U.S.")), - ("WF", _("Wallis and Futuna")), - ("EH", _("Western Sahara")), - ("YE", _("Yemen")), - ("ZM", _("Zambia")), - ("ZW", _("Zimbabwe")), + ("GB", "United Kingdom"), + ("AF", "Afghanistan"), + ("AX", "Aland Islands"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei Darussalam"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("CV", "Cape Verde"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CG", "Congo"), + ("CD", "Congo, The Democratic Republic of the"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("CI", "Cote d'Ivoire"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CY", "Cyprus"), + ("CZ", "Czech Republic"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands (Malvinas)"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "Guernsey"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Holy See (Vatican City State)"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran, Islamic Republic of"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "Isle of Man"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "Jersey"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "Korea, Democratic People's Republic of"), + ("KR", "Korea, Republic of"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Lao People's Democratic Republic"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libyan Arab Jamahiriya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MK", "Macedonia, The Former Yugoslav Republic of"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia, Federated States of"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("AN", "Netherlands Antilles"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "Nicaragua"), + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Palestinian Territory, Occupied"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RE", "Reunion"), + ("RO", "Romania"), + ("RU", "Russian Federation"), + ("RW", "Rwanda"), + ("BL", "Saint Barthelemy"), + ("SH", "Saint Helena"), + ("KN", "Saint Kitts and Nevis"), + ("LC", "Saint Lucia"), + ("MF", "Saint Martin"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "Saint Vincent and the Grenadines"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and the South Sandwich Islands"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SZ", "Swaziland"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syrian Arab Republic"), + ("TW", "Taiwan, Province of China"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania, United Republic of"), + ("TH", "Thailand"), + ("TL", "Timor-Leste"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("US", "United States"), + ("UM", "United States Minor Outlying Islands"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Viet Nam"), + ("VG", "Virgin Islands, British"), + ("VI", "Virgin Islands, U.S."), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), ) CURRENCY_CODES = ( - ("AED", _("AED, Dirham")), - ("AFN", _("AFN, Afghani")), - ("ALL", _("ALL, Lek")), - ("AMD", _("AMD, Dram")), - ("ANG", _("ANG, Guilder")), - ("AOA", _("AOA, Kwanza")), - ("ARS", _("ARS, Peso")), - ("AUD", _("AUD, Dollar")), - ("AWG", _("AWG, Guilder")), - ("AZN", _("AZN, Manat")), - ("BAM", _("BAM, Marka")), - ("BBD", _("BBD, Dollar")), - ("BDT", _("BDT, Taka")), - ("BGN", _("BGN, Lev")), - ("BHD", _("BHD, Dinar")), - ("BIF", _("BIF, Franc")), - ("BMD", _("BMD, Dollar")), - ("BND", _("BND, Dollar")), - ("BOB", _("BOB, Boliviano")), - ("BRL", _("BRL, Real")), - ("BSD", _("BSD, Dollar")), - ("BTN", _("BTN, Ngultrum")), - ("BWP", _("BWP, Pula")), - ("BYR", _("BYR, Ruble")), - ("BZD", _("BZD, Dollar")), - ("CAD", _("CAD, Dollar")), - ("CDF", _("CDF, Franc")), - ("CHF", _("CHF, Franc")), - ("CLP", _("CLP, Peso")), - ("CNY", _("CNY, Yuan Renminbi")), - ("COP", _("COP, Peso")), - ("CRC", _("CRC, Colon")), - ("CUP", _("CUP, Peso")), - ("CVE", _("CVE, Escudo")), - ("CZK", _("CZK, Koruna")), - ("DJF", _("DJF, Franc")), - ("DKK", _("DKK, Krone")), - ("DOP", _("DOP, Peso")), - ("DZD", _("DZD, Dinar")), - ("EGP", _("EGP, Pound")), - ("ERN", _("ERN, Nakfa")), - ("ETB", _("ETB, Birr")), - ("EUR", _("EUR, Euro")), - ("FJD", _("FJD, Dollar")), - ("FKP", _("FKP, Pound")), - ("GBP", _("GBP, Pound")), - ("GEL", _("GEL, Lari")), - ("GHS", _("GHS, Cedi")), - ("GIP", _("GIP, Pound")), - ("GMD", _("GMD, Dalasi")), - ("GNF", _("GNF, Franc")), - ("GTQ", _("GTQ, Quetzal")), - ("GYD", _("GYD, Dollar")), - ("HKD", _("HKD, Dollar")), - ("HNL", _("HNL, Lempira")), - ("HRK", _("HRK, Kuna")), - ("HTG", _("HTG, Gourde")), - ("HUF", _("HUF, Forint")), - ("IDR", _("IDR, Rupiah")), - ("ILS", _("ILS, Shekel")), - ("INR", _("INR, Rupee")), - ("IQD", _("IQD, Dinar")), - ("IRR", _("IRR, Rial")), - ("ISK", _("ISK, Krona")), - ("JMD", _("JMD, Dollar")), - ("JOD", _("JOD, Dinar")), - ("JPY", _("JPY, Yen")), - ("KES", _("KES, Shilling")), - ("KGS", _("KGS, Som")), - ("KHR", _("KHR, Riels")), - ("KMF", _("KMF, Franc")), - ("KPW", _("KPW, Won")), - ("KRW", _("KRW, Won")), - ("KWD", _("KWD, Dinar")), - ("KYD", _("KYD, Dollar")), - ("KZT", _("KZT, Tenge")), - ("LAK", _("LAK, Kip")), - ("LBP", _("LBP, Pound")), - ("LKR", _("LKR, Rupee")), - ("LRD", _("LRD, Dollar")), - ("LSL", _("LSL, Loti")), - ("LTL", _("LTL, Litas")), - ("LVL", _("LVL, Lat")), - ("LYD", _("LYD, Dinar")), - ("MAD", _("MAD, Dirham")), - ("MDL", _("MDL, Leu")), - ("MGA", _("MGA, Ariary")), - ("MKD", _("MKD, Denar")), - ("MMK", _("MMK, Kyat")), - ("MNT", _("MNT, Tugrik")), - ("MOP", _("MOP, Pataca")), - ("MRO", _("MRO, Ouguiya")), - ("MUR", _("MUR, Rupee")), - ("MVR", _("MVR, Rufiyaa")), - ("MWK", _("MWK, Kwacha")), - ("MXN", _("MXN, Peso")), - ("MYR", _("MYR, Ringgit")), - ("MZN", _("MZN, Metical")), - ("NAD", _("NAD, Dollar")), - ("NGN", _("NGN, Naira")), - ("NIO", _("NIO, Cordoba")), - ("NOK", _("NOK, Krone")), - ("NPR", _("NPR, Rupee")), - ("NZD", _("NZD, Dollar")), - ("OMR", _("OMR, Rial")), - ("PAB", _("PAB, Balboa")), - ("PEN", _("PEN, Sol")), - ("PGK", _("PGK, Kina")), - ("PHP", _("PHP, Peso")), - ("PKR", _("PKR, Rupee")), - ("PLN", _("PLN, Zloty")), - ("PYG", _("PYG, Guarani")), - ("QAR", _("QAR, Rial")), - ("RON", _("RON, Leu")), - ("RSD", _("RSD, Dinar")), - ("RUB", _("RUB, Ruble")), - ("RWF", _("RWF, Franc")), - ("SAR", _("SAR, Rial")), - ("SBD", _("SBD, Dollar")), - ("SCR", _("SCR, Rupee")), - ("SDG", _("SDG, Pound")), - ("SEK", _("SEK, Krona")), - ("SGD", _("SGD, Dollar")), - ("SHP", _("SHP, Pound")), - ("SLL", _("SLL, Leone")), - ("SOS", _("SOS, Shilling")), - ("SRD", _("SRD, Dollar")), - ("SSP", _("SSP, Pound")), - ("STD", _("STD, Dobra")), - ("SYP", _("SYP, Pound")), - ("SZL", _("SZL, Lilangeni")), - ("THB", _("THB, Baht")), - ("TJS", _("TJS, Somoni")), - ("TMT", _("TMT, Manat")), - ("TND", _("TND, Dinar")), - ("TOP", _("TOP, Paanga")), - ("TRY", _("TRY, Lira")), - ("TTD", _("TTD, Dollar")), - ("TWD", _("TWD, Dollar")), - ("TZS", _("TZS, Shilling")), - ("UAH", _("UAH, Hryvnia")), - ("UGX", _("UGX, Shilling")), - ("USD", _("$, Dollar")), - ("UYU", _("UYU, Peso")), - ("UZS", _("UZS, Som")), - ("VEF", _("VEF, Bolivar")), - ("VND", _("VND, Dong")), - ("VUV", _("VUV, Vatu")), - ("WST", _("WST, Tala")), - ("XAF", _("XAF, Franc")), - ("XCD", _("XCD, Dollar")), - ("XOF", _("XOF, Franc")), - ("XPF", _("XPF, Franc")), - ("YER", _("YER, Rial")), - ("ZAR", _("ZAR, Rand")), - ("ZMK", _("ZMK, Kwacha")), - ("ZWL", _("ZWL, Dollar")), + ("AED", "AED, Dirham"), + ("AFN", "AFN, Afghani"), + ("ALL", "ALL, Lek"), + ("AMD", "AMD, Dram"), + ("ANG", "ANG, Guilder"), + ("AOA", "AOA, Kwanza"), + ("ARS", "ARS, Peso"), + ("AUD", "AUD, Dollar"), + ("AWG", "AWG, Guilder"), + ("AZN", "AZN, Manat"), + ("BAM", "BAM, Marka"), + ("BBD", "BBD, Dollar"), + ("BDT", "BDT, Taka"), + ("BGN", "BGN, Lev"), + ("BHD", "BHD, Dinar"), + ("BIF", "BIF, Franc"), + ("BMD", "BMD, Dollar"), + ("BND", "BND, Dollar"), + ("BOB", "BOB, Boliviano"), + ("BRL", "BRL, Real"), + ("BSD", "BSD, Dollar"), + ("BTN", "BTN, Ngultrum"), + ("BWP", "BWP, Pula"), + ("BYR", "BYR, Ruble"), + ("BZD", "BZD, Dollar"), + ("CAD", "CAD, Dollar"), + ("CDF", "CDF, Franc"), + ("CHF", "CHF, Franc"), + ("CLP", "CLP, Peso"), + ("CNY", "CNY, Yuan Renminbi"), + ("COP", "COP, Peso"), + ("CRC", "CRC, Colon"), + ("CUP", "CUP, Peso"), + ("CVE", "CVE, Escudo"), + ("CZK", "CZK, Koruna"), + ("DJF", "DJF, Franc"), + ("DKK", "DKK, Krone"), + ("DOP", "DOP, Peso"), + ("DZD", "DZD, Dinar"), + ("EGP", "EGP, Pound"), + ("ERN", "ERN, Nakfa"), + ("ETB", "ETB, Birr"), + ("EUR", "EUR, Euro"), + ("FJD", "FJD, Dollar"), + ("FKP", "FKP, Pound"), + ("GBP", "GBP, Pound"), + ("GEL", "GEL, Lari"), + ("GHS", "GHS, Cedi"), + ("GIP", "GIP, Pound"), + ("GMD", "GMD, Dalasi"), + ("GNF", "GNF, Franc"), + ("GTQ", "GTQ, Quetzal"), + ("GYD", "GYD, Dollar"), + ("HKD", "HKD, Dollar"), + ("HNL", "HNL, Lempira"), + ("HRK", "HRK, Kuna"), + ("HTG", "HTG, Gourde"), + ("HUF", "HUF, Forint"), + ("IDR", "IDR, Rupiah"), + ("ILS", "ILS, Shekel"), + ("INR", "INR, Rupee"), + ("IQD", "IQD, Dinar"), + ("IRR", "IRR, Rial"), + ("ISK", "ISK, Krona"), + ("JMD", "JMD, Dollar"), + ("JOD", "JOD, Dinar"), + ("JPY", "JPY, Yen"), + ("KES", "KES, Shilling"), + ("KGS", "KGS, Som"), + ("KHR", "KHR, Riels"), + ("KMF", "KMF, Franc"), + ("KPW", "KPW, Won"), + ("KRW", "KRW, Won"), + ("KWD", "KWD, Dinar"), + ("KYD", "KYD, Dollar"), + ("KZT", "KZT, Tenge"), + ("LAK", "LAK, Kip"), + ("LBP", "LBP, Pound"), + ("LKR", "LKR, Rupee"), + ("LRD", "LRD, Dollar"), + ("LSL", "LSL, Loti"), + ("LTL", "LTL, Litas"), + ("LVL", "LVL, Lat"), + ("LYD", "LYD, Dinar"), + ("MAD", "MAD, Dirham"), + ("MDL", "MDL, Leu"), + ("MGA", "MGA, Ariary"), + ("MKD", "MKD, Denar"), + ("MMK", "MMK, Kyat"), + ("MNT", "MNT, Tugrik"), + ("MOP", "MOP, Pataca"), + ("MRO", "MRO, Ouguiya"), + ("MUR", "MUR, Rupee"), + ("MVR", "MVR, Rufiyaa"), + ("MWK", "MWK, Kwacha"), + ("MXN", "MXN, Peso"), + ("MYR", "MYR, Ringgit"), + ("MZN", "MZN, Metical"), + ("NAD", "NAD, Dollar"), + ("NGN", "NGN, Naira"), + ("NIO", "NIO, Cordoba"), + ("NOK", "NOK, Krone"), + ("NPR", "NPR, Rupee"), + ("NZD", "NZD, Dollar"), + ("OMR", "OMR, Rial"), + ("PAB", "PAB, Balboa"), + ("PEN", "PEN, Sol"), + ("PGK", "PGK, Kina"), + ("PHP", "PHP, Peso"), + ("PKR", "PKR, Rupee"), + ("PLN", "PLN, Zloty"), + ("PYG", "PYG, Guarani"), + ("QAR", "QAR, Rial"), + ("RON", "RON, Leu"), + ("RSD", "RSD, Dinar"), + ("RUB", "RUB, Ruble"), + ("RWF", "RWF, Franc"), + ("SAR", "SAR, Rial"), + ("SBD", "SBD, Dollar"), + ("SCR", "SCR, Rupee"), + ("SDG", "SDG, Pound"), + ("SEK", "SEK, Krona"), + ("SGD", "SGD, Dollar"), + ("SHP", "SHP, Pound"), + ("SLL", "SLL, Leone"), + ("SOS", "SOS, Shilling"), + ("SRD", "SRD, Dollar"), + ("SSP", "SSP, Pound"), + ("STD", "STD, Dobra"), + ("SYP", "SYP, Pound"), + ("SZL", "SZL, Lilangeni"), + ("THB", "THB, Baht"), + ("TJS", "TJS, Somoni"), + ("TMT", "TMT, Manat"), + ("TND", "TND, Dinar"), + ("TOP", "TOP, Paanga"), + ("TRY", "TRY, Lira"), + ("TTD", "TTD, Dollar"), + ("TWD", "TWD, Dollar"), + ("TZS", "TZS, Shilling"), + ("UAH", "UAH, Hryvnia"), + ("UGX", "UGX, Shilling"), + ("USD", "$, Dollar"), + ("UYU", "UYU, Peso"), + ("UZS", "UZS, Som"), + ("VEF", "VEF, Bolivar"), + ("VND", "VND, Dong"), + ("VUV", "VUV, Vatu"), + ("WST", "WST, Tala"), + ("XAF", "XAF, Franc"), + ("XCD", "XCD, Dollar"), + ("XOF", "XOF, Franc"), + ("XPF", "XPF, Franc"), + ("YER", "YER, Rial"), + ("ZAR", "ZAR, Rand"), + ("ZMK", "ZMK, Kwacha"), + ("ZWL", "ZWL, Dollar"), ) diff --git a/common/views.py b/common/views.py index 94785d15a..3bc416335 100644 --- a/common/views.py +++ b/common/views.py @@ -32,7 +32,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Account, Contact, Tags +from accounts.models import Account, Contact from accounts.serializer import AccountSerializer from cases.models import Case from cases.serializer import CaseSerializer diff --git a/contacts/migrations/0001_initial.py b/contacts/migrations/0001_initial.py index 87991dd55..661804c65 100644 --- a/contacts/migrations/0001_initial.py +++ b/contacts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -13,8 +13,8 @@ class Migration(migrations.Migration): dependencies = [ ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('teams', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -33,19 +33,19 @@ class Migration(migrations.Migration): ('primary_email', models.EmailField(max_length=254, unique=True)), ('secondary_email', models.EmailField(blank=True, default='', max_length=254)), ('mobile_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None, unique=True)), - ('secondary_number', phonenumber_field.modelfields.PhoneNumberField(max_length=128, null=True, region=None)), + ('secondary_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None)), ('department', models.CharField(max_length=255, null=True, verbose_name='Department')), ('language', models.CharField(max_length=255, null=True, verbose_name='Language')), ('do_not_call', models.BooleanField(default=False)), ('description', models.TextField(blank=True, null=True)), ('linked_in_url', models.URLField(blank=True, null=True)), ('facebook_url', models.URLField(blank=True, null=True)), - ('twitter_username', models.CharField(max_length=255, null=True)), + ('twitter_username', models.CharField(blank=True, max_length=255, null=True)), ('is_active', models.BooleanField(default=False)), ('country', models.CharField(blank=True, choices=[('GB', 'United Kingdom'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albania'), ('DZ', 'Algeria'), ('AS', 'American Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarctica'), ('AG', 'Antigua and Barbuda'), ('AR', 'Argentina'), ('AM', 'Armenia'), ('AW', 'Aruba'), ('AU', 'Australia'), ('AT', 'Austria'), ('AZ', 'Azerbaijan'), ('BS', 'Bahamas'), ('BH', 'Bahrain'), ('BD', 'Bangladesh'), ('BB', 'Barbados'), ('BY', 'Belarus'), ('BE', 'Belgium'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivia'), ('BA', 'Bosnia and Herzegovina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brazil'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgaria'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Cambodia'), ('CM', 'Cameroon'), ('CA', 'Canada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Central African Republic'), ('TD', 'Chad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Colombia'), ('KM', 'Comoros'), ('CG', 'Congo'), ('CD', 'Congo, The Democratic Republic of the'), ('CK', 'Cook Islands'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Croatia'), ('CU', 'Cuba'), ('CY', 'Cyprus'), ('CZ', 'Czech Republic'), ('DK', 'Denmark'), ('DJ', 'Djibouti'), ('DM', 'Dominica'), ('DO', 'Dominican Republic'), ('EC', 'Ecuador'), ('EG', 'Egypt'), ('SV', 'El Salvador'), ('GQ', 'Equatorial Guinea'), ('ER', 'Eritrea'), ('EE', 'Estonia'), ('ET', 'Ethiopia'), ('FK', 'Falkland Islands (Malvinas)'), ('FO', 'Faroe Islands'), ('FJ', 'Fiji'), ('FI', 'Finland'), ('FR', 'France'), ('GF', 'French Guiana'), ('PF', 'French Polynesia'), ('TF', 'French Southern Territories'), ('GA', 'Gabon'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Germany'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Greece'), ('GL', 'Greenland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard Island and McDonald Islands'), ('VA', 'Holy See (Vatican City State)'), ('HN', 'Honduras'), ('HK', 'Hong Kong'), ('HU', 'Hungary'), ('IS', 'Iceland'), ('IN', 'India'), ('ID', 'Indonesia'), ('IR', 'Iran, Islamic Republic of'), ('IQ', 'Iraq'), ('IE', 'Ireland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italy'), ('JM', 'Jamaica'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kazakhstan'), ('KE', 'Kenya'), ('KI', 'Kiribati'), ('KP', "Korea, Democratic People's Republic of"), ('KR', 'Korea, Republic of'), ('KW', 'Kuwait'), ('KG', 'Kyrgyzstan'), ('LA', "Lao People's Democratic Republic"), ('LV', 'Latvia'), ('LB', 'Lebanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyan Arab Jamahiriya'), ('LI', 'Liechtenstein'), ('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MO', 'Macao'), ('MK', 'Macedonia, The Former Yugoslav Republic of'), ('MG', 'Madagascar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Maldives'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauritania'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexico'), ('FM', 'Micronesia, Federated States of'), ('MD', 'Moldova'), ('MC', 'Monaco'), ('MN', 'Mongolia'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Morocco'), ('MZ', 'Mozambique'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Netherlands'), ('AN', 'Netherlands Antilles'), ('NC', 'New Caledonia'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norway'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palestinian Territory, Occupied'), ('PA', 'Panama'), ('PG', 'Papua New Guinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippines'), ('PN', 'Pitcairn'), ('PL', 'Poland'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Qatar'), ('RE', 'Reunion'), ('RO', 'Romania'), ('RU', 'Russian Federation'), ('RW', 'Rwanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts and Nevis'), ('LC', 'Saint Lucia'), ('MF', 'Saint Martin'), ('PM', 'Saint Pierre and Miquelon'), ('VC', 'Saint Vincent and the Grenadines'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('SN', 'Senegal'), ('RS', 'Serbia'), ('SC', 'Seychelles'), ('SL', 'Sierra Leone'), ('SG', 'Singapore'), ('SK', 'Slovakia'), ('SI', 'Slovenia'), ('SB', 'Solomon Islands'), ('SO', 'Somalia'), ('ZA', 'South Africa'), ('GS', 'South Georgia and the South Sandwich Islands'), ('ES', 'Spain'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard and Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Sweden'), ('CH', 'Switzerland'), ('SY', 'Syrian Arab Republic'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tajikistan'), ('TZ', 'Tanzania, United Republic of'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad and Tobago'), ('TN', 'Tunisia'), ('TR', 'Turkey'), ('TM', 'Turkmenistan'), ('TC', 'Turks and Caicos Islands'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'United Arab Emirates'), ('US', 'United States'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Uzbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Viet Nam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, U.S.'), ('WF', 'Wallis and Futuna'), ('EH', 'Western Sahara'), ('YE', 'Yemen'), ('ZM', 'Zambia'), ('ZW', 'Zimbabwe')], max_length=3, null=True)), ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='adress_contacts', to='common.address')), ('assigned_to', models.ManyToManyField(related_name='contact_assigned_users', to='common.profile')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_created_by', to='common.profile')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), ('teams', models.ManyToManyField(related_name='contact_teams', to='teams.teams')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), diff --git a/contacts/migrations/0002_alter_contact_created_by.py b/contacts/migrations/0002_alter_contact_created_by.py deleted file mode 100644 index 117678bbb..000000000 --- a/contacts/migrations/0002_alter_contact_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-29 07:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/contacts/migrations/0003_alter_contact_secondary_number_and_more.py b/contacts/migrations/0003_alter_contact_secondary_number_and_more.py deleted file mode 100644 index 227439f19..000000000 --- a/contacts/migrations/0003_alter_contact_secondary_number_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-21 11:23 - -from django.db import migrations, models -import phonenumber_field.modelfields - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0002_alter_contact_created_by'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='secondary_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), - ), - migrations.AlterField( - model_name='contact', - name='twitter_username', - field=models.CharField(blank=True, max_length=255, null=True), - ), - ] diff --git a/contacts/migrations/0004_alter_contact_address.py b/contacts/migrations/0004_alter_contact_address.py deleted file mode 100644 index 2f4ab9d6e..000000000 --- a/contacts/migrations/0004_alter_contact_address.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-23 05:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0003_alter_contact_secondary_number_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='address', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/contacts/migrations/0005_alter_contact_address.py b/contacts/migrations/0005_alter_contact_address.py deleted file mode 100644 index b02342469..000000000 --- a/contacts/migrations/0005_alter_contact_address.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.1 on 2023-11-23 05:59 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('common', '0007_org_is_active'), - ('contacts', '0004_alter_contact_address'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='address', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='adress_contacts', to='common.address'), - ), - ] diff --git a/crm/settings.py b/crm/settings.py index aa2b8b23f..18abe3d81 100644 --- a/crm/settings.py +++ b/crm/settings.py @@ -3,7 +3,20 @@ from corsheaders.defaults import default_headers from dotenv import load_dotenv +import environ +env = environ.Env() + +# Set the project base directory +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Take environment variables from .env file +environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + + +GOOGLE_CLIENT_ID = env("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = env("GOOGLE_CLIENT_SECRET") +GOOGLE_LOGIN_DOMAIN = env("GOOGLE_LOGIN_DOMAIN") # JWT_AUTH = { # 'JWT_PAYLOAD_GET_USERNAME_HANDLER': # 'path.to.custom_jwt_payload_handler', diff --git a/emails/migrations/0001_initial.py b/emails/migrations/0001_initial.py index 346d070a9..681e384a8 100644 --- a/emails/migrations/0001_initial.py +++ b/emails/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models diff --git a/events/migrations/0001_initial.py b/events/migrations/0001_initial.py index 0371fea6a..d3d9c4460 100644 --- a/events/migrations/0001_initial.py +++ b/events/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -11,8 +11,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('contacts', '0001_initial'), ('common', '0001_initial'), + ('contacts', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('teams', '0001_initial'), ] diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py index a218a9dbe..9afb70e5f 100644 --- a/invoices/migrations/0001_initial.py +++ b/invoices/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -12,10 +12,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('accounts', '0001_initial'), ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('teams', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0001_initial'), ] operations = [ diff --git a/leads/migrations/0001_initial.py b/leads/migrations/0001_initial.py index 425942829..bfa18a1ee 100644 --- a/leads/migrations/0001_initial.py +++ b/leads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -12,9 +12,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), ('common', '0001_initial'), + ('teams', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contacts', '0001_initial'), ] @@ -72,9 +71,9 @@ class Migration(migrations.Migration): ('assigned_to', models.ManyToManyField(related_name='lead_assigned_users', to='common.profile')), ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_company', to='leads.company')), ('contacts', models.ManyToManyField(related_name='lead_contacts', to='contacts.contact')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_created_by', to='common.profile')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lead_org', to='common.org')), - ('tags', models.ManyToManyField(blank=True, to='accounts.tags')), + ('tags', models.ManyToManyField(blank=True, to='common.tag')), ('teams', models.ManyToManyField(related_name='lead_teams', to='teams.teams')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ], diff --git a/leads/migrations/0002_alter_lead_created_by.py b/leads/migrations/0002_alter_lead_created_by.py deleted file mode 100644 index 97a183630..000000000 --- a/leads/migrations/0002_alter_lead_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-06-29 07:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('leads', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='lead', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/leads/models.py b/leads/models.py index 7294d5c45..cdf1a4f79 100644 --- a/leads/models.py +++ b/leads/models.py @@ -4,8 +4,7 @@ from django.utils.translation import pgettext_lazy from phonenumber_field.modelfields import PhoneNumberField -from accounts.models import Tags -from common.models import Org, Profile +from common.models import Org, Profile, Tag from common.base import BaseModel from common.utils import ( COUNTRIES, @@ -62,7 +61,7 @@ class Lead(BaseModel): ) is_active = models.BooleanField(default=False) enquiry_type = models.CharField(max_length=255, blank=True, null=True) - tags = models.ManyToManyField(Tags, blank=True) + tags = models.ManyToManyField(Tag, blank=True) contacts = models.ManyToManyField(Contact, related_name="lead_contacts") created_from_site = models.BooleanField(default=False) teams = models.ManyToManyField(Teams, related_name="lead_teams") diff --git a/leads/serializer.py b/leads/serializer.py index fd766ac5f..185986439 100644 --- a/leads/serializer.py +++ b/leads/serializer.py @@ -1,6 +1,7 @@ from rest_framework import serializers -from accounts.models import Account, Tags +from accounts.models import Account +from common.models import Tag from common.serializer import ( AttachmentsSerializer, LeadCommentSerializer, @@ -15,7 +16,7 @@ class TagsSerializer(serializers.ModelSerializer): class Meta: - model = Tags + model = Tag fields = ("id", "name", "slug") diff --git a/leads/views.py b/leads/views.py index fcd7f03a1..a0a9b1bd2 100644 --- a/leads/views.py +++ b/leads/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Account, Tags +from accounts.models import Account from common.models import APISettings, Attachments, Comment, Profile #from common.external_auth import CustomDualAuthentication diff --git a/opportunity/migrations/0001_initial.py b/opportunity/migrations/0001_initial.py index 9cc9679b6..d82db6235 100644 --- a/opportunity/migrations/0001_initial.py +++ b/opportunity/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -11,11 +11,11 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contacts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('teams', '0001_initial'), + ('accounts', '0001_initial'), ] operations = [ @@ -38,9 +38,9 @@ class Migration(migrations.Migration): ('assigned_to', models.ManyToManyField(related_name='opportunity_assigned_to', to='common.profile')), ('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oppurtunity_closed_by', to='common.profile')), ('contacts', models.ManyToManyField(to='contacts.contact')), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opportunity_created_by', to='common.profile')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oppurtunity_org', to='common.org')), - ('tags', models.ManyToManyField(blank=True, to='accounts.tags')), + ('tags', models.ManyToManyField(blank=True, to='common.tag')), ('teams', models.ManyToManyField(related_name='oppurtunity_teams', to='teams.teams')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ], diff --git a/opportunity/migrations/0002_alter_opportunity_created_by.py b/opportunity/migrations/0002_alter_opportunity_created_by.py deleted file mode 100644 index f4b2ec97e..000000000 --- a/opportunity/migrations/0002_alter_opportunity_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:35 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('opportunity', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='opportunity', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/opportunity/models.py b/opportunity/models.py index be39c6476..7a8895034 100644 --- a/opportunity/models.py +++ b/opportunity/models.py @@ -3,8 +3,8 @@ from django.utils.translation import gettext_lazy as _ from django.utils.translation import pgettext_lazy -from accounts.models import Account, Tags -from common.models import Org, Profile +from accounts.models import Account +from common.models import Org, Profile, Tag from common.base import BaseModel from common.utils import CURRENCY_CODES, SOURCES, STAGES from contacts.models import Contact @@ -48,7 +48,7 @@ class Opportunity(BaseModel): Profile, related_name="opportunity_assigned_to" ) is_active = models.BooleanField(default=False) - tags = models.ManyToManyField(Tags, blank=True) + tags = models.ManyToManyField(Tag, blank=True) teams = models.ManyToManyField(Teams, related_name="oppurtunity_teams") org = models.ForeignKey( Org, diff --git a/opportunity/serializer.py b/opportunity/serializer.py index 436b3f31d..9338e03b7 100644 --- a/opportunity/serializer.py +++ b/opportunity/serializer.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from accounts.models import Tags from accounts.serializer import AccountSerializer +from common.models import Tag from common.serializer import AttachmentsSerializer, ProfileSerializer,UserSerializer from contacts.serializer import ContactSerializer from opportunity.models import Opportunity @@ -10,7 +10,7 @@ class TagsSerializer(serializers.ModelSerializer): class Meta: - model = Tags + model = Tag fields = ("id", "name", "slug") diff --git a/opportunity/views.py b/opportunity/views.py index 63ee66eb0..0000820aa 100644 --- a/opportunity/views.py +++ b/opportunity/views.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from accounts.models import Account, Tags +from accounts.models import Account from accounts.serializer import AccountSerializer, TagsSerailizer from common.models import Attachments, Comment, Profile @@ -89,7 +89,7 @@ def get_context_data(self, **kwargs): context["opportunities"] = opportunities context["accounts_list"] = AccountSerializer(accounts, many=True).data context["contacts_list"] = ContactSerializer(contacts, many=True).data - context["tags"] = TagsSerailizer(Tags.objects.filter(), many=True).data + context["tags"] = TagsSerailizer(Tag.objects.filter(), many=True).data context["stage"] = STAGES context["lead_source"] = SOURCES context["currency"] = CURRENCY_CODES diff --git a/planner/migrations/0001_initial.py b/planner/migrations/0001_initial.py index a50ab301b..a39e22bcf 100644 --- a/planner/migrations/0001_initial.py +++ b/planner/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -11,10 +11,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), ('leads', '0001_initial'), - ('contacts', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ('contacts', '0001_initial'), ] operations = [ diff --git a/requirements.txt b/requirements.txt index 129686d6b..280d34fed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ -django==4.2.1 +django==4.2.20 celery==5.2.7 python-dotenv==1.0.0 django-cors-headers==4.0.0 djangorestframework==3.14.0 djangorestframework-simplejwt==5.2.2 -drf-spectacular==0.26.2 +drf-spectacular==0.28.0 django-ses==3.5.0 psycopg2-binary==2.9.6 whitenoise==6.4.0 sentry-sdk==1.24.0 -wagtail==5.0.1 +wagtail==5.0.5 django_extensions==3.2.1 django_storages==1.13.2 @@ -20,3 +20,8 @@ Redis==4.6.0 django-phonenumber-field==7.1.0 arrow==1.2.3 phonenumbers==8.13.13 +uvicorn==0.34.0 +fastapi==0.115.12 +django-environ==0.12.0 +httpx==0.28.1 +python-multipart==0.0.20 \ No newline at end of file diff --git a/tasks/migrations/0001_initial.py b/tasks/migrations/0001_initial.py index 1929aff39..91657103f 100644 --- a/tasks/migrations/0001_initial.py +++ b/tasks/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -11,11 +11,11 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('teams', '0001_initial'), - ('accounts', '0001_initial'), ('common', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contacts', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('teams', '0001_initial'), + ('accounts', '0001_initial'), ] operations = [ @@ -32,7 +32,7 @@ class Migration(migrations.Migration): ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='accounts_tasks', to='accounts.account')), ('assigned_to', models.ManyToManyField(related_name='users_tasks', to='common.profile')), ('contacts', models.ManyToManyField(related_name='contacts_tasks', to='contacts.contact')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_created', to='common.profile')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_org', to='common.org')), ('teams', models.ManyToManyField(related_name='tasks_teams', to='teams.teams')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), diff --git a/tasks/migrations/0002_alter_task_created_by.py b/tasks/migrations/0002_alter_task_created_by.py deleted file mode 100644 index 3460261ff..000000000 --- a/tasks/migrations/0002_alter_task_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-10-31 12:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tasks', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='task', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ] diff --git a/teams/migrations/0001_initial.py b/teams/migrations/0001_initial.py index 37a5f8079..102d9faaf 100644 --- a/teams/migrations/0001_initial.py +++ b/teams/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.1 on 2023-06-19 13:08 +# Generated by Django 4.2.1 on 2025-04-06 07:18 from django.conf import settings from django.db import migrations, models @@ -25,7 +25,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=100)), ('description', models.TextField()), ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='Created on')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='teams_created', to='common.profile')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.org')), ('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')), ('users', models.ManyToManyField(related_name='user_teams', to='common.profile')), @@ -33,7 +33,7 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'Team', 'verbose_name_plural': 'Teams', - 'db_table': 'eams', + 'db_table': 'teams', 'ordering': ('-created_at',), }, ), diff --git a/teams/migrations/0002_alter_teams_table.py b/teams/migrations/0002_alter_teams_table.py deleted file mode 100644 index e4fe82d42..000000000 --- a/teams/migrations/0002_alter_teams_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-12 09:52 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('teams', '0001_initial'), - ] - - operations = [ - migrations.AlterModelTable( - name='teams', - table='teams', - ), - ] diff --git a/teams/migrations/0003_alter_teams_created_by.py b/teams/migrations/0003_alter_teams_created_by.py deleted file mode 100644 index 8acdbeccd..000000000 --- a/teams/migrations/0003_alter_teams_created_by.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.1 on 2023-07-24 08:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('teams', '0002_alter_teams_table'), - ] - - operations = [ - migrations.AlterField( - model_name='teams', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By'), - ), - ]