This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

0
contacts/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

55
contacts/admin.py Normal file
View File

@@ -0,0 +1,55 @@
from django.contrib import admin
from .models import Contact, ContactGroup, ContactInteraction, ContactImport
@admin.register(ContactGroup)
class ContactGroupAdmin(admin.ModelAdmin):
list_display = ('name', 'user', 'created_at')
list_filter = ('created_at',)
search_fields = ('name', 'user__email')
raw_id_fields = ('user',)
@admin.register(Contact)
class ContactAdmin(admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email', 'company', 'user', 'is_favorite', 'is_blocked', 'created_at')
list_filter = ('is_favorite', 'is_blocked', 'company', 'city', 'country', 'created_at')
search_fields = ('first_name', 'last_name', 'email', 'company', 'phone')
raw_id_fields = ('user', 'group')
date_hierarchy = 'created_at'
fieldsets = (
('Basic Information', {
'fields': ('user', 'group', 'first_name', 'last_name', 'email', 'phone')
}),
('Work Information', {
'fields': ('company', 'job_title', 'website')
}),
('Address', {
'fields': ('address_line1', 'address_line2', 'city', 'state', 'postal_code', 'country')
}),
('Social Media', {
'fields': ('linkedin', 'twitter', 'facebook')
}),
('Additional', {
'fields': ('avatar', 'notes', 'birthday', 'is_favorite', 'is_blocked')
}),
)
@admin.register(ContactInteraction)
class ContactInteractionAdmin(admin.ModelAdmin):
list_display = ('contact', 'interaction_type', 'subject', 'created_by', 'date')
list_filter = ('interaction_type', 'date')
search_fields = ('contact__first_name', 'contact__last_name', 'subject')
raw_id_fields = ('contact', 'created_by')
date_hierarchy = 'date'
@admin.register(ContactImport)
class ContactImportAdmin(admin.ModelAdmin):
list_display = ('filename', 'user', 'status', 'total_contacts', 'imported_contacts', 'failed_contacts', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('filename', 'user__email')
raw_id_fields = ('user',)
readonly_fields = ('status', 'total_contacts', 'imported_contacts', 'failed_contacts', 'error_log', 'created_at', 'completed_at')

6
contacts/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ContactsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'contacts'

View File

@@ -0,0 +1,129 @@
# Generated by Django 4.2.7 on 2025-09-14 20:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Contact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=50)),
('last_name', models.CharField(max_length=50)),
('email', models.EmailField(max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('company', models.CharField(blank=True, max_length=100)),
('job_title', models.CharField(blank=True, max_length=100)),
('avatar', models.ImageField(blank=True, null=True, upload_to='contact_avatars/')),
('notes', models.TextField(blank=True)),
('website', models.URLField(blank=True)),
('birthday', models.DateField(blank=True, null=True)),
('address_line1', models.CharField(blank=True, max_length=100)),
('address_line2', models.CharField(blank=True, max_length=100)),
('city', models.CharField(blank=True, max_length=50)),
('state', models.CharField(blank=True, max_length=50)),
('postal_code', models.CharField(blank=True, max_length=20)),
('country', models.CharField(blank=True, max_length=50)),
('linkedin', models.URLField(blank=True)),
('twitter', models.URLField(blank=True)),
('facebook', models.URLField(blank=True)),
('is_favorite', models.BooleanField(default=False)),
('is_blocked', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'contacts',
'ordering': ['first_name', 'last_name'],
},
),
migrations.CreateModel(
name='ContactInteraction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('interaction_type', models.CharField(choices=[('email_sent', 'Email Sent'), ('email_received', 'Email Received'), ('phone_call', 'Phone Call'), ('meeting', 'Meeting'), ('note', 'Note')], max_length=20)),
('subject', models.CharField(blank=True, max_length=200)),
('description', models.TextField(blank=True)),
('date', models.DateTimeField(auto_now_add=True)),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interactions', to='contacts.contact')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'contact_interactions',
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='ContactImport',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filename', models.CharField(max_length=255)),
('file', models.FileField(upload_to='contact_imports/')),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20)),
('total_contacts', models.IntegerField(default=0)),
('imported_contacts', models.IntegerField(default=0)),
('failed_contacts', models.IntegerField(default=0)),
('error_log', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_imports', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'contact_imports',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ContactGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('description', models.TextField(blank=True)),
('color', models.CharField(default='#007bff', max_length=7)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_groups', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'contact_groups',
'ordering': ['name'],
'unique_together': {('user', 'name')},
},
),
migrations.AddField(
model_name='contact',
name='group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contacts', to='contacts.contactgroup'),
),
migrations.AddField(
model_name='contact',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to=settings.AUTH_USER_MODEL),
),
migrations.AddIndex(
model_name='contact',
index=models.Index(fields=['user', 'email'], name='contacts_user_id_bbbaf8_idx'),
),
migrations.AddIndex(
model_name='contact',
index=models.Index(fields=['user', 'is_favorite'], name='contacts_user_id_80e197_idx'),
),
migrations.AddIndex(
model_name='contact',
index=models.Index(fields=['user', 'is_blocked'], name='contacts_user_id_0ee143_idx'),
),
migrations.AlterUniqueTogether(
name='contact',
unique_together={('user', 'email')},
),
]

View File

148
contacts/models.py Normal file
View File

@@ -0,0 +1,148 @@
from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import EmailValidator
import uuid
User = get_user_model()
class ContactGroup(models.Model):
"""Contact groups for organizing contacts."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='contact_groups')
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
color = models.CharField(max_length=7, default='#007bff') # Hex color
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'contact_groups'
unique_together = ['user', 'name']
ordering = ['name']
def __str__(self):
return f"{self.user.email} - {self.name}"
class Contact(models.Model):
"""Contact model for storing contact information."""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='contacts')
group = models.ForeignKey(ContactGroup, on_delete=models.SET_NULL, null=True, blank=True, related_name='contacts')
# Basic information
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
phone = models.CharField(max_length=20, blank=True)
company = models.CharField(max_length=100, blank=True)
job_title = models.CharField(max_length=100, blank=True)
# Additional information
avatar = models.ImageField(upload_to='contact_avatars/', null=True, blank=True)
notes = models.TextField(blank=True)
website = models.URLField(blank=True)
birthday = models.DateField(null=True, blank=True)
# Address information
address_line1 = models.CharField(max_length=100, blank=True)
address_line2 = models.CharField(max_length=100, blank=True)
city = models.CharField(max_length=50, blank=True)
state = models.CharField(max_length=50, blank=True)
postal_code = models.CharField(max_length=20, blank=True)
country = models.CharField(max_length=50, blank=True)
# Social media
linkedin = models.URLField(blank=True)
twitter = models.URLField(blank=True)
facebook = models.URLField(blank=True)
# Metadata
is_favorite = models.BooleanField(default=False)
is_blocked = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'contacts'
unique_together = ['user', 'email']
ordering = ['first_name', 'last_name']
indexes = [
models.Index(fields=['user', 'email']),
models.Index(fields=['user', 'is_favorite']),
models.Index(fields=['user', 'is_blocked']),
]
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.email})"
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
def get_address(self):
"""Get formatted address."""
address_parts = [
self.address_line1,
self.address_line2,
self.city,
self.state,
self.postal_code,
self.country
]
return ', '.join([part for part in address_parts if part])
class ContactInteraction(models.Model):
"""Track interactions with contacts."""
INTERACTION_TYPES = [
('email_sent', 'Email Sent'),
('email_received', 'Email Received'),
('phone_call', 'Phone Call'),
('meeting', 'Meeting'),
('note', 'Note'),
]
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='interactions')
interaction_type = models.CharField(max_length=20, choices=INTERACTION_TYPES)
subject = models.CharField(max_length=200, blank=True)
description = models.TextField(blank=True)
date = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
db_table = 'contact_interactions'
ordering = ['-date']
def __str__(self):
return f"{self.contact.get_full_name()} - {self.get_interaction_type_display()}"
class ContactImport(models.Model):
"""Track contact imports."""
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('completed', 'Completed'),
('failed', 'Failed'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='contact_imports')
filename = models.CharField(max_length=255)
file = models.FileField(upload_to='contact_imports/')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
total_contacts = models.IntegerField(default=0)
imported_contacts = models.IntegerField(default=0)
failed_contacts = models.IntegerField(default=0)
error_log = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'contact_imports'
ordering = ['-created_at']
def __str__(self):
return f"{self.user.email} - {self.filename}"

126
contacts/serializers.py Normal file
View File

@@ -0,0 +1,126 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Contact, ContactGroup, ContactInteraction, ContactImport
User = get_user_model()
class ContactGroupSerializer(serializers.ModelSerializer):
"""Serializer for contact groups."""
contact_count = serializers.SerializerMethodField()
class Meta:
model = ContactGroup
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
def get_contact_count(self, obj):
return obj.contacts.count()
class ContactSerializer(serializers.ModelSerializer):
"""Serializer for contacts."""
full_name = serializers.CharField(source='get_full_name', read_only=True)
address = serializers.CharField(source='get_address', read_only=True)
group_name = serializers.CharField(source='group.name', read_only=True)
interaction_count = serializers.SerializerMethodField()
class Meta:
model = Contact
fields = '__all__'
read_only_fields = ('user', 'created_at', 'updated_at')
def get_interaction_count(self, obj):
return obj.interactions.count()
class ContactCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating contacts."""
class Meta:
model = Contact
fields = (
'group', 'first_name', 'last_name', 'email', 'phone', 'company',
'job_title', 'notes', 'website', 'birthday', 'address_line1',
'address_line2', 'city', 'state', 'postal_code', 'country',
'linkedin', 'twitter', 'facebook', 'is_favorite', 'is_blocked'
)
def validate_email(self, value):
user = self.context['request'].user
if Contact.objects.filter(user=user, email=value).exists():
raise serializers.ValidationError("A contact with this email already exists.")
return value
class ContactInteractionSerializer(serializers.ModelSerializer):
"""Serializer for contact interactions."""
contact_name = serializers.CharField(source='contact.get_full_name', read_only=True)
created_by_name = serializers.CharField(source='created_by.get_full_name', read_only=True)
class Meta:
model = ContactInteraction
fields = '__all__'
read_only_fields = ('created_by', 'date')
class ContactImportSerializer(serializers.ModelSerializer):
"""Serializer for contact imports."""
class Meta:
model = ContactImport
fields = '__all__'
read_only_fields = ('user', 'status', 'total_contacts', 'imported_contacts', 'failed_contacts', 'error_log', 'created_at', 'completed_at')
class ContactBulkActionSerializer(serializers.Serializer):
"""Serializer for bulk contact actions."""
ACTION_CHOICES = [
('add_to_group', 'Add to group'),
('remove_from_group', 'Remove from group'),
('mark_favorite', 'Mark as favorite'),
('mark_unfavorite', 'Mark as unfavorite'),
('block', 'Block'),
('unblock', 'Unblock'),
('delete', 'Delete'),
]
contact_ids = serializers.ListField(
child=serializers.IntegerField(),
min_length=1
)
action = serializers.ChoiceField(choices=ACTION_CHOICES)
group_id = serializers.IntegerField(required=False)
def validate_contact_ids(self, value):
user = self.context['request'].user
# Verify all contacts belong to the user
contact_count = Contact.objects.filter(id__in=value, user=user).count()
if contact_count != len(value):
raise serializers.ValidationError("Some contacts don't exist or don't belong to you.")
return value
def validate(self, attrs):
action = attrs.get('action')
group_id = attrs.get('group_id')
if action in ['add_to_group', 'remove_from_group'] and not group_id:
raise serializers.ValidationError("group_id is required for group actions.")
return attrs
class ContactSearchSerializer(serializers.Serializer):
"""Serializer for contact search."""
query = serializers.CharField(required=False)
group = serializers.IntegerField(required=False)
is_favorite = serializers.BooleanField(required=False)
is_blocked = serializers.BooleanField(required=False)
company = serializers.CharField(required=False)
city = serializers.CharField(required=False)
country = serializers.CharField(required=False)

137
contacts/tasks.py Normal file
View File

@@ -0,0 +1,137 @@
from celery import shared_task
import csv
import io
import logging
from django.utils import timezone
from django.core.files.base import ContentFile
from .models import Contact, ContactGroup, ContactImport
logger = logging.getLogger(__name__)
@shared_task
def process_contact_import(import_id):
"""Process contact import from CSV file."""
try:
import_obj = ContactImport.objects.get(id=import_id)
import_obj.status = 'processing'
import_obj.save()
# Read CSV file
csv_file = import_obj.file.read().decode('utf-8')
csv_reader = csv.DictReader(io.StringIO(csv_file))
total_contacts = 0
imported_contacts = 0
failed_contacts = 0
error_log = []
for row in csv_reader:
total_contacts += 1
try:
# Extract contact data
first_name = row.get('First Name', '').strip()
last_name = row.get('Last Name', '').strip()
email = row.get('Email', '').strip()
if not email:
error_log.append(f"Row {total_contacts}: Email is required")
failed_contacts += 1
continue
# Check if contact already exists
if Contact.objects.filter(user=import_obj.user, email=email).exists():
error_log.append(f"Row {total_contacts}: Contact with email {email} already exists")
failed_contacts += 1
continue
# Get or create group
group = None
group_name = row.get('Group', '').strip()
if group_name:
group, created = ContactGroup.objects.get_or_create(
user=import_obj.user,
name=group_name,
defaults={'description': f'Imported from {import_obj.filename}'}
)
# Create contact
contact = Contact.objects.create(
user=import_obj.user,
group=group,
first_name=first_name,
last_name=last_name,
email=email,
phone=row.get('Phone', '').strip(),
company=row.get('Company', '').strip(),
job_title=row.get('Job Title', '').strip(),
address_line1=row.get('Address Line 1', '').strip(),
address_line2=row.get('Address Line 2', '').strip(),
city=row.get('City', '').strip(),
state=row.get('State', '').strip(),
postal_code=row.get('Postal Code', '').strip(),
country=row.get('Country', '').strip(),
website=row.get('Website', '').strip(),
linkedin=row.get('LinkedIn', '').strip(),
twitter=row.get('Twitter', '').strip(),
facebook=row.get('Facebook', '').strip(),
notes=row.get('Notes', '').strip(),
is_favorite=row.get('Is Favorite', '').lower() == 'true',
is_blocked=row.get('Is Blocked', '').lower() == 'true',
)
imported_contacts += 1
logger.info(f"Imported contact {contact.id} for user {import_obj.user.id}")
except Exception as e:
error_log.append(f"Row {total_contacts}: {str(e)}")
failed_contacts += 1
logger.error(f"Error importing contact row {total_contacts}: {str(e)}")
# Update import status
import_obj.status = 'completed'
import_obj.total_contacts = total_contacts
import_obj.imported_contacts = imported_contacts
import_obj.failed_contacts = failed_contacts
import_obj.error_log = '\n'.join(error_log)
import_obj.completed_at = timezone.now()
import_obj.save()
logger.info(f"Contact import {import_id} completed: {imported_contacts} imported, {failed_contacts} failed")
except Exception as e:
logger.error(f"Failed to process contact import {import_id}: {str(e)}")
try:
import_obj = ContactImport.objects.get(id=import_id)
import_obj.status = 'failed'
import_obj.error_log = str(e)
import_obj.save()
except ContactImport.DoesNotExist:
pass
@shared_task
def cleanup_contact_imports():
"""Clean up old contact import files."""
try:
from django.utils import timezone
from datetime import timedelta
# Delete import files older than 7 days
cutoff_date = timezone.now() - timedelta(days=7)
old_imports = ContactImport.objects.filter(
created_at__lt=cutoff_date,
status__in=['completed', 'failed']
)
count = old_imports.count()
for import_obj in old_imports:
if import_obj.file:
import_obj.file.delete()
import_obj.delete()
logger.info(f"Cleaned up {count} old contact import files")
except Exception as e:
logger.error(f"Failed to cleanup contact imports: {str(e)}")

25
contacts/urls.py Normal file
View File

@@ -0,0 +1,25 @@
from django.urls import path
from . import views
urlpatterns = [
# Contact groups
path('groups/', views.ContactGroupListCreateView.as_view(), name='contact-group-list'),
path('groups/<int:pk>/', views.ContactGroupDetailView.as_view(), name='contact-group-detail'),
# Contacts
path('', views.ContactListCreateView.as_view(), name='contact-list'),
path('<int:pk>/', views.ContactDetailView.as_view(), name='contact-detail'),
path('search/', views.ContactSearchView.as_view(), name='contact-search'),
path('bulk-action/', views.ContactBulkActionView.as_view(), name='contact-bulk-action'),
path('suggestions/', views.contact_suggestions, name='contact-suggestions'),
path('stats/', views.contact_stats, name='contact-stats'),
path('export/', views.export_contacts, name='export-contacts'),
# Contact interactions
path('<int:contact_id>/interactions/', views.ContactInteractionListCreateView.as_view(), name='contact-interaction-list'),
path('interactions/<int:pk>/', views.ContactInteractionDetailView.as_view(), name='contact-interaction-detail'),
# Contact imports
path('import/', views.ContactImportView.as_view(), name='contact-import'),
path('imports/<int:pk>/', views.ContactImportDetailView.as_view(), name='contact-import-detail'),
]

299
contacts/views.py Normal file
View File

@@ -0,0 +1,299 @@
from rest_framework import generics, status, permissions, filters
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.views import APIView
from django_filters.rest_framework import DjangoFilterBackend
from django.db import models
from django.db.models import Q, Count
from django.shortcuts import get_object_or_404
from django_ratelimit.decorators import ratelimit
from django.http import HttpResponse
import csv
import io
from datetime import datetime
from .models import Contact, ContactGroup, ContactInteraction, ContactImport
from .serializers import (
ContactSerializer, ContactCreateSerializer, ContactGroupSerializer,
ContactInteractionSerializer, ContactImportSerializer, ContactBulkActionSerializer,
ContactSearchSerializer
)
from .tasks import process_contact_import
class ContactGroupListCreateView(generics.ListCreateAPIView):
"""List and create contact groups."""
serializer_class = ContactGroupSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return ContactGroup.objects.filter(user=self.request.user)
class ContactGroupDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete contact group."""
serializer_class = ContactGroupSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return ContactGroup.objects.filter(user=self.request.user)
class ContactListCreateView(generics.ListCreateAPIView):
"""List and create contacts."""
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['group', 'is_favorite', 'is_blocked', 'company', 'city', 'country']
search_fields = ['first_name', 'last_name', 'email', 'company', 'phone']
ordering_fields = ['first_name', 'last_name', 'email', 'created_at']
ordering = ['first_name', 'last_name']
def get_serializer_class(self):
if self.request.method == 'POST':
return ContactCreateSerializer
return ContactSerializer
def get_queryset(self):
return Contact.objects.filter(user=self.request.user).select_related('group')
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class ContactDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete contact."""
serializer_class = ContactSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Contact.objects.filter(user=self.request.user).select_related('group')
class ContactSearchView(generics.ListAPIView):
"""Search contacts with advanced filters."""
serializer_class = ContactSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
queryset = Contact.objects.filter(user=self.request.user)
# Get search parameters
query = self.request.query_params.get('q', '')
group = self.request.query_params.get('group')
is_favorite = self.request.query_params.get('is_favorite')
is_blocked = self.request.query_params.get('is_blocked')
company = self.request.query_params.get('company')
city = self.request.query_params.get('city')
country = self.request.query_params.get('country')
# Apply filters
if query:
queryset = queryset.filter(
Q(first_name__icontains=query) |
Q(last_name__icontains=query) |
Q(email__icontains=query) |
Q(company__icontains=query) |
Q(phone__icontains=query)
)
if group:
queryset = queryset.filter(group_id=group)
if is_favorite is not None:
queryset = queryset.filter(is_favorite=is_favorite.lower() == 'true')
if is_blocked is not None:
queryset = queryset.filter(is_blocked=is_blocked.lower() == 'true')
if company:
queryset = queryset.filter(company__icontains=company)
if city:
queryset = queryset.filter(city__icontains=city)
if country:
queryset = queryset.filter(country__icontains=country)
return queryset.select_related('group')
class ContactBulkActionView(APIView):
"""Perform bulk actions on contacts."""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
serializer = ContactBulkActionSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
contact_ids = serializer.validated_data['contact_ids']
action = serializer.validated_data['action']
group_id = serializer.validated_data.get('group_id')
contacts = Contact.objects.filter(id__in=contact_ids, user=request.user)
if action == 'add_to_group':
group = get_object_or_404(ContactGroup, id=group_id, user=request.user)
contacts.update(group=group)
elif action == 'remove_from_group':
contacts.update(group=None)
elif action == 'mark_favorite':
contacts.update(is_favorite=True)
elif action == 'mark_unfavorite':
contacts.update(is_favorite=False)
elif action == 'block':
contacts.update(is_blocked=True)
elif action == 'unblock':
contacts.update(is_blocked=False)
elif action == 'delete':
contacts.delete()
return Response({'message': f'Bulk action {action} completed successfully'})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ContactInteractionListCreateView(generics.ListCreateAPIView):
"""List and create contact interactions."""
serializer_class = ContactInteractionSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
contact_id = self.kwargs.get('contact_id')
return ContactInteraction.objects.filter(
contact_id=contact_id,
contact__user=self.request.user
).order_by('-date')
def perform_create(self, serializer):
contact_id = self.kwargs.get('contact_id')
contact = get_object_or_404(Contact, id=contact_id, user=self.request.user)
serializer.save(contact=contact, created_by=self.request.user)
class ContactInteractionDetailView(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete contact interaction."""
serializer_class = ContactInteractionSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return ContactInteraction.objects.filter(contact__user=self.request.user)
class ContactImportView(generics.CreateAPIView):
"""Import contacts from CSV file."""
serializer_class = ContactImportSerializer
permission_classes = [permissions.IsAuthenticated]
def perform_create(self, serializer):
import_obj = serializer.save(user=self.request.user)
# Process import asynchronously
process_contact_import.delay(import_obj.id)
class ContactImportDetailView(generics.RetrieveAPIView):
"""Retrieve contact import status."""
serializer_class = ContactImportSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return ContactImport.objects.filter(user=self.request.user)
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def contact_stats(request):
"""Get contact statistics."""
user = request.user
stats = {
'total_contacts': Contact.objects.filter(user=user).count(),
'favorite_contacts': Contact.objects.filter(user=user, is_favorite=True).count(),
'blocked_contacts': Contact.objects.filter(user=user, is_blocked=True).count(),
'total_groups': ContactGroup.objects.filter(user=user).count(),
'recent_interactions': ContactInteraction.objects.filter(
contact__user=user
).count(),
'group_stats': ContactGroup.objects.filter(user=user).annotate(
contact_count=models.Count('contacts')
).values('name', 'contact_count'),
}
return Response(stats)
@api_view(['POST'])
@permission_classes([permissions.IsAuthenticated])
def export_contacts(request):
"""Export contacts to CSV."""
user = request.user
format_type = request.data.get('format', 'csv')
if format_type == 'csv':
# Create CSV response
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="contacts.csv"'
writer = csv.writer(response)
writer.writerow([
'First Name', 'Last Name', 'Email', 'Phone', 'Company', 'Job Title',
'Address Line 1', 'Address Line 2', 'City', 'State', 'Postal Code',
'Country', 'Website', 'LinkedIn', 'Twitter', 'Facebook', 'Notes',
'Birthday', 'Group', 'Is Favorite', 'Is Blocked'
])
contacts = Contact.objects.filter(user=user).select_related('group')
for contact in contacts:
writer.writerow([
contact.first_name,
contact.last_name,
contact.email,
contact.phone,
contact.company,
contact.job_title,
contact.address_line1,
contact.address_line2,
contact.city,
contact.state,
contact.postal_code,
contact.country,
contact.website,
contact.linkedin,
contact.twitter,
contact.facebook,
contact.notes,
contact.birthday,
contact.group.name if contact.group else '',
contact.is_favorite,
contact.is_blocked,
])
return response
return Response({'error': 'Unsupported format'}, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@permission_classes([permissions.IsAuthenticated])
def contact_suggestions(request):
"""Get contact suggestions based on email addresses."""
email = request.query_params.get('email', '')
if not email:
return Response({'suggestions': []})
# Find contacts with similar email addresses
suggestions = Contact.objects.filter(
user=request.user,
email__icontains=email
).values('id', 'first_name', 'last_name', 'email', 'company')[:10]
return Response({'suggestions': list(suggestions)})