parent
02659f631a
commit
c31b677f98
@ -0,0 +1 @@ |
||||
This is a django customer relationship managemement (CRM) app. |
||||
@ -0,0 +1,96 @@ |
||||
from django.contrib import admin |
||||
from django.utils.html import format_html |
||||
from .models import ( |
||||
Prospect, |
||||
Status, |
||||
ProspectStatus, |
||||
Event, |
||||
EmailCampaign, |
||||
EmailTracker |
||||
) |
||||
|
||||
@admin.register(Prospect) |
||||
class ProspectAdmin(admin.ModelAdmin): |
||||
list_display = ('name', 'email', 'region', 'created_at') |
||||
list_filter = ('region', 'created_at') |
||||
search_fields = ('name', 'email', 'region') |
||||
filter_horizontal = ('users',) |
||||
date_hierarchy = 'created_at' |
||||
|
||||
@admin.register(Status) |
||||
class StatusAdmin(admin.ModelAdmin): |
||||
list_display = ('name', 'created_at') |
||||
search_fields = ('name',) |
||||
|
||||
@admin.register(ProspectStatus) |
||||
class ProspectStatusAdmin(admin.ModelAdmin): |
||||
list_display = ('prospect', 'status', 'created_at') |
||||
list_filter = ('status', 'created_at') |
||||
search_fields = ('prospect__name', 'prospect__email') |
||||
date_hierarchy = 'created_at' |
||||
|
||||
@admin.register(Event) |
||||
class EventAdmin(admin.ModelAdmin): |
||||
list_display = ('get_event_display', 'type', 'date', 'status', 'created_at') |
||||
list_filter = ('type', 'status', 'date') |
||||
search_fields = ('description',) |
||||
filter_horizontal = ('prospects',) |
||||
date_hierarchy = 'date' |
||||
|
||||
def get_event_display(self, obj): |
||||
return str(obj) |
||||
get_event_display.short_description = 'Event' |
||||
|
||||
@admin.register(EmailCampaign) |
||||
class EmailCampaignAdmin(admin.ModelAdmin): |
||||
list_display = ('subject', 'event', 'sent_at') |
||||
list_filter = ('sent_at',) |
||||
search_fields = ('subject', 'content') |
||||
date_hierarchy = 'sent_at' |
||||
readonly_fields = ('sent_at',) |
||||
|
||||
@admin.register(EmailTracker) |
||||
class EmailTrackerAdmin(admin.ModelAdmin): |
||||
list_display = ( |
||||
'campaign', |
||||
'prospect', |
||||
'tracking_id', |
||||
'sent_status', |
||||
'opened_status', |
||||
'clicked_status' |
||||
) |
||||
list_filter = ('sent', 'opened', 'clicked') |
||||
search_fields = ( |
||||
'prospect__name', |
||||
'prospect__email', |
||||
'campaign__subject' |
||||
) |
||||
readonly_fields = ( |
||||
'tracking_id', 'sent', 'sent_at', |
||||
'opened', 'opened_at', |
||||
'clicked', 'clicked_at' |
||||
) |
||||
date_hierarchy = 'sent_at' |
||||
|
||||
def sent_status(self, obj): |
||||
return self._get_status_html(obj.sent, obj.sent_at) |
||||
sent_status.short_description = 'Sent' |
||||
sent_status.allow_tags = True |
||||
|
||||
def opened_status(self, obj): |
||||
return self._get_status_html(obj.opened, obj.opened_at) |
||||
opened_status.short_description = 'Opened' |
||||
opened_status.allow_tags = True |
||||
|
||||
def clicked_status(self, obj): |
||||
return self._get_status_html(obj.clicked, obj.clicked_at) |
||||
clicked_status.short_description = 'Clicked' |
||||
clicked_status.allow_tags = True |
||||
|
||||
def _get_status_html(self, status, date): |
||||
if status: |
||||
return format_html( |
||||
'<span style="color: green;">✓</span> {}', |
||||
date.strftime('%Y-%m-%d %H:%M') if date else '' |
||||
) |
||||
return format_html('<span style="color: red;">✗</span>') |
||||
@ -0,0 +1,5 @@ |
||||
from django.apps import AppConfig |
||||
|
||||
class CrmConfig(AppConfig): |
||||
default_auto_field = 'django.db.models.BigAutoField' |
||||
name = 'crm' |
||||
@ -0,0 +1,18 @@ |
||||
import django_filters |
||||
|
||||
from .models import Event, Status, Prospect |
||||
|
||||
class ProspectFilter(django_filters.FilterSet): |
||||
region = django_filters.CharFilter(lookup_expr='icontains') |
||||
events = django_filters.ModelMultipleChoiceFilter( |
||||
queryset=Event.objects.all(), |
||||
field_name='events', |
||||
) |
||||
statuses = django_filters.ModelMultipleChoiceFilter( |
||||
queryset=Status.objects.all(), |
||||
field_name='prospectstatus__status', |
||||
) |
||||
|
||||
class Meta: |
||||
model = Prospect |
||||
fields = ['region', 'events', 'statuses'] |
||||
@ -0,0 +1,40 @@ |
||||
from django import forms |
||||
from .models import Prospect, Event |
||||
import datetime |
||||
|
||||
class SmallTextArea(forms.Textarea): |
||||
def __init__(self, *args, **kwargs): |
||||
kwargs.setdefault('attrs', {}) |
||||
kwargs['attrs'].update({ |
||||
'rows': 2, |
||||
'cols': 100, |
||||
'style': 'height: 80px; width: 800px;' |
||||
}) |
||||
super().__init__(*args, **kwargs) |
||||
|
||||
class CSVImportForm(forms.Form): |
||||
csv_file = forms.FileField() |
||||
|
||||
class BulkEmailForm(forms.Form): |
||||
prospects = forms.ModelMultipleChoiceField( |
||||
queryset=Prospect.objects.all(), |
||||
widget=forms.CheckboxSelectMultiple |
||||
) |
||||
subject = forms.CharField(max_length=200) |
||||
content = forms.CharField(widget=forms.Textarea) |
||||
|
||||
class EventForm(forms.ModelForm): |
||||
prospects = forms.ModelMultipleChoiceField( |
||||
queryset=Prospect.objects.all(), |
||||
widget=forms.SelectMultiple(attrs={'class': 'select2'}), |
||||
required=False |
||||
) |
||||
description = forms.CharField(widget=SmallTextArea) |
||||
attachment_text = forms.CharField(widget=SmallTextArea) |
||||
|
||||
class Meta: |
||||
model = Event |
||||
fields = ['date', 'type', 'description', 'attachment_text', 'prospects', 'status'] |
||||
widgets = { |
||||
'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), |
||||
} |
||||
@ -0,0 +1,94 @@ |
||||
# Generated by Django 4.2.11 on 2024-12-08 15:10 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
import uuid |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
initial = True |
||||
|
||||
dependencies = [ |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='Prospect', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('email', models.EmailField(max_length=254, unique=True)), |
||||
('name', models.CharField(max_length=200)), |
||||
('region', models.CharField(max_length=100)), |
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
||||
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='Status', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('name', models.CharField(max_length=100, unique=True)), |
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='ProspectStatus', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
||||
('prospect', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.prospect')), |
||||
('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='crm.status')), |
||||
], |
||||
options={ |
||||
'ordering': ['-created_at'], |
||||
}, |
||||
), |
||||
migrations.CreateModel( |
||||
name='Event', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('date', models.DateTimeField()), |
||||
('type', models.CharField(choices=[('MAIL', 'Mailing List'), ('SMS', 'SMS Campaign'), ('PRESS', 'Press Release')], max_length=10)), |
||||
('description', models.TextField()), |
||||
('attachment_text', models.TextField(blank=True)), |
||||
('status', models.CharField(choices=[('PLANNED', 'Planned'), ('ACTIVE', 'Active'), ('COMPLETED', 'Completed')], max_length=20)), |
||||
('created_at', models.DateTimeField(auto_now_add=True)), |
||||
('prospects', models.ManyToManyField(related_name='events', to='crm.prospect')), |
||||
], |
||||
options={ |
||||
'ordering': ['-created_at'], |
||||
}, |
||||
), |
||||
migrations.CreateModel( |
||||
name='EmailCampaign', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('subject', models.CharField(max_length=200)), |
||||
('content', models.TextField()), |
||||
('sent_at', models.DateTimeField(blank=True, null=True)), |
||||
('event', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='crm.event')), |
||||
], |
||||
), |
||||
migrations.CreateModel( |
||||
name='EmailTracker', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('tracking_id', models.UUIDField(default=uuid.uuid4, editable=False)), |
||||
('sent', models.BooleanField(default=False)), |
||||
('sent_at', models.DateTimeField(blank=True, null=True)), |
||||
('opened', models.BooleanField(default=False)), |
||||
('opened_at', models.DateTimeField(blank=True, null=True)), |
||||
('clicked', models.BooleanField(default=False)), |
||||
('clicked_at', models.DateTimeField(blank=True, null=True)), |
||||
('error_message', models.TextField(blank=True)), |
||||
('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.emailcampaign')), |
||||
('prospect', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='crm.prospect')), |
||||
], |
||||
options={ |
||||
'unique_together': {('campaign', 'prospect')}, |
||||
}, |
||||
), |
||||
] |
||||
@ -0,0 +1,60 @@ |
||||
# Generated by Django 4.2.11 on 2024-12-08 20:58 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
import django.utils.timezone |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
('crm', '0001_initial'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterModelOptions( |
||||
name='event', |
||||
options={'ordering': ['-created_at'], 'permissions': [('manage_events', 'Can manage events'), ('view_events', 'Can view events')]}, |
||||
), |
||||
migrations.AlterModelOptions( |
||||
name='prospect', |
||||
options={'permissions': [('manage_prospects', 'Can manage prospects'), ('view_prospects', 'Can view prospects')]}, |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='created_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_events', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='modified_at', |
||||
field=models.DateTimeField(auto_now=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='event', |
||||
name='modified_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_events', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='created_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_prospects', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='modified_at', |
||||
field=models.DateTimeField(auto_now=True), |
||||
), |
||||
migrations.AddField( |
||||
model_name='prospect', |
||||
name='modified_by', |
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_prospects', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='event', |
||||
name='date', |
||||
field=models.DateTimeField(default=django.utils.timezone.now), |
||||
), |
||||
] |
||||
@ -0,0 +1,6 @@ |
||||
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin |
||||
from django.core.exceptions import PermissionDenied |
||||
|
||||
class CRMAccessMixin(LoginRequiredMixin, UserPassesTestMixin): |
||||
def test_func(self): |
||||
return self.request.user.groups.filter(name='CRM Manager').exists() |
||||
@ -0,0 +1,112 @@ |
||||
from django.db import models |
||||
from django.contrib.auth import get_user_model |
||||
from django.utils import timezone |
||||
import uuid |
||||
|
||||
User = get_user_model() |
||||
|
||||
class EventType(models.TextChoices): |
||||
MAILING = 'MAIL', 'Mailing List' |
||||
SMS = 'SMS', 'SMS Campaign' |
||||
PRESS = 'PRESS', 'Press Release' |
||||
|
||||
class Prospect(models.Model): |
||||
email = models.EmailField(unique=True) |
||||
name = models.CharField(max_length=200) |
||||
region = models.CharField(max_length=100) |
||||
users = models.ManyToManyField(get_user_model(), blank=True) |
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
created_by = models.ForeignKey( |
||||
User, |
||||
on_delete=models.SET_NULL, |
||||
null=True, |
||||
related_name='created_prospects' |
||||
) |
||||
modified_by = models.ForeignKey( |
||||
User, |
||||
on_delete=models.SET_NULL, |
||||
null=True, |
||||
related_name='modified_prospects' |
||||
) |
||||
modified_at = models.DateTimeField(auto_now=True) |
||||
|
||||
class Meta: |
||||
permissions = [ |
||||
("manage_prospects", "Can manage prospects"), |
||||
("view_prospects", "Can view prospects"), |
||||
] |
||||
|
||||
def __str__(self): |
||||
return f"{self.name} ({self.email})" |
||||
|
||||
class Status(models.Model): |
||||
name = models.CharField(max_length=100, unique=True) |
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
|
||||
def __str__(self): |
||||
return self.name |
||||
|
||||
class ProspectStatus(models.Model): |
||||
prospect = models.ForeignKey(Prospect, on_delete=models.CASCADE) |
||||
status = models.ForeignKey(Status, on_delete=models.PROTECT) |
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
|
||||
class Meta: |
||||
ordering = ['-created_at'] |
||||
|
||||
class Event(models.Model): |
||||
date = models.DateTimeField(default=timezone.now) |
||||
type = models.CharField(max_length=10, choices=EventType.choices) |
||||
description = models.TextField() |
||||
attachment_text = models.TextField(blank=True) |
||||
prospects = models.ManyToManyField(Prospect, related_name='events') |
||||
status = models.CharField(max_length=20, choices=[ |
||||
('PLANNED', 'Planned'), |
||||
('ACTIVE', 'Active'), |
||||
('COMPLETED', 'Completed'), |
||||
]) |
||||
created_at = models.DateTimeField(auto_now_add=True) |
||||
created_by = models.ForeignKey( |
||||
User, |
||||
on_delete=models.SET_NULL, |
||||
null=True, |
||||
related_name='created_events' |
||||
) |
||||
modified_by = models.ForeignKey( |
||||
User, |
||||
on_delete=models.SET_NULL, |
||||
null=True, |
||||
related_name='modified_events' |
||||
) |
||||
modified_at = models.DateTimeField(auto_now=True) |
||||
|
||||
class Meta: |
||||
ordering = ['-created_at'] |
||||
permissions = [ |
||||
("manage_events", "Can manage events"), |
||||
("view_events", "Can view events"), |
||||
] |
||||
|
||||
def __str__(self): |
||||
return f"{self.get_type_display()} - {self.date.date()}" |
||||
|
||||
class EmailCampaign(models.Model): |
||||
event = models.OneToOneField(Event, on_delete=models.CASCADE) |
||||
subject = models.CharField(max_length=200) |
||||
content = models.TextField() |
||||
sent_at = models.DateTimeField(null=True, blank=True) |
||||
|
||||
class EmailTracker(models.Model): |
||||
campaign = models.ForeignKey(EmailCampaign, on_delete=models.CASCADE) |
||||
prospect = models.ForeignKey(Prospect, on_delete=models.CASCADE) |
||||
tracking_id = models.UUIDField(default=uuid.uuid4, editable=False) |
||||
sent = models.BooleanField(default=False) |
||||
sent_at = models.DateTimeField(null=True, blank=True) |
||||
opened = models.BooleanField(default=False) |
||||
opened_at = models.DateTimeField(null=True, blank=True) |
||||
clicked = models.BooleanField(default=False) |
||||
clicked_at = models.DateTimeField(null=True, blank=True) |
||||
error_message = models.TextField(blank=True) |
||||
|
||||
class Meta: |
||||
unique_together = ['campaign', 'prospect'] |
||||
@ -0,0 +1,43 @@ |
||||
# services.py |
||||
from django.core.mail import send_mail, get_connection |
||||
from django.conf import settings |
||||
from django.template.loader import render_to_string |
||||
|
||||
def send_bulk_email(subject, content, prospects): |
||||
""" |
||||
Send bulk emails to prospects |
||||
Returns tuple of (success_count, error_count) |
||||
""" |
||||
success_count = 0 |
||||
error_count = 0 |
||||
|
||||
# Get email connection |
||||
connection = get_connection() |
||||
|
||||
# You might want to wrap this in try/except if you want to handle connection errors |
||||
connection.open() |
||||
|
||||
for prospect in prospects: |
||||
try: |
||||
# You could add basic personalization here |
||||
personalized_content = content.replace('{name}', prospect.name) |
||||
|
||||
send_mail( |
||||
subject=subject, |
||||
message=personalized_content, # Plain text version |
||||
html_message=personalized_content, # HTML version |
||||
from_email=settings.DEFAULT_FROM_EMAIL, |
||||
recipient_list=[prospect.email], |
||||
connection=connection, |
||||
fail_silently=False, |
||||
) |
||||
success_count += 1 |
||||
|
||||
except Exception as e: |
||||
error_count += 1 |
||||
# You might want to log the error here |
||||
print(f"Failed to send email to {prospect.email}: {str(e)}") |
||||
|
||||
connection.close() |
||||
|
||||
return success_count, error_count |
||||
@ -0,0 +1,10 @@ |
||||
document.addEventListener("DOMContentLoaded", function () { |
||||
const selectAll = document.getElementById("select-all"); |
||||
const prospectCheckboxes = document.getElementsByName("selected_prospects"); |
||||
|
||||
selectAll.addEventListener("change", function () { |
||||
prospectCheckboxes.forEach((checkbox) => { |
||||
checkbox.checked = selectAll.checked; |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,31 @@ |
||||
{% extends "crm/base.html" %} {% block content %} |
||||
<div class="container padding-bottom"> |
||||
<div class="grid-x padding-bottom"> |
||||
<div class="cell medium-6 large-6 my-block bubble"> |
||||
<h1 class="title">Add New Prospect</h1> |
||||
|
||||
<form method="post"> |
||||
{% csrf_token %} |
||||
<div class="form-group"> |
||||
<label for="name">Name:</label> |
||||
<input type="text" name="name" id="name" required /> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="email">Email:</label> |
||||
<input type="email" name="email" id="email" required /> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label for="region">Region:</label> |
||||
<input type="text" name="region" id="region" required /> |
||||
</div> |
||||
|
||||
<button type="submit" class="small-button margin-v20"> |
||||
Save Prospect |
||||
</button> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,71 @@ |
||||
<!DOCTYPE html> |
||||
<html> |
||||
{% load static %} |
||||
|
||||
<head> |
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||
<link |
||||
rel="stylesheet" |
||||
href="{% static 'tournaments/css/foundation.min.css' %}" |
||||
/> |
||||
<link rel="stylesheet" href="{% static 'tournaments/css/style.css' %}" /> |
||||
<link rel="stylesheet" href="{% static 'tournaments/css/basics.css' %}" /> |
||||
|
||||
<link |
||||
rel="icon" |
||||
type="image/png" |
||||
href="{% static 'tournaments/images/favicon.png' %}" |
||||
/> |
||||
|
||||
<title>{% block head_title %}Page Title{% endblock %} - Padel Club</title> |
||||
<!-- Matomo --> |
||||
<script> |
||||
var _paq = window._paq = window._paq || []; |
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ |
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]); |
||||
_paq.push(["setDoNotTrack", true]); |
||||
_paq.push(['trackPageView']); |
||||
_paq.push(['enableLinkTracking']); |
||||
(function() { |
||||
var u="//matomo.padelclub.app/"; |
||||
_paq.push(['setTrackerUrl', u+'matomo.php']); |
||||
_paq.push(['setSiteId', '1']); |
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; |
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); |
||||
})(); |
||||
</script> |
||||
<!-- End Matomo Code --> |
||||
|
||||
</head> |
||||
|
||||
<body class="wrapper"> |
||||
<header> |
||||
<div class="grid-x"> |
||||
<div class="medium-6 large-9 cell topblock my-block "> |
||||
<a href="{% url 'index' %}"> |
||||
<img |
||||
src="{% static 'tournaments/images/PadelClub_logo_512.png' %}" |
||||
class="logo inline" |
||||
/> |
||||
<div class="inline padding-left"> |
||||
<h1 class="club">{% block first_title %}Page Title{% endblock %}</h1> |
||||
<h1 class="event">{% block second_title %}Page Title{% endblock %}</h1> |
||||
</div> |
||||
</a> |
||||
</div> |
||||
{% block right_header %}{% endblock %} |
||||
</div> |
||||
</header> |
||||
|
||||
<main> |
||||
<!-- Content --> |
||||
{% block content %} |
||||
<!-- The content of child templates will be inserted here --> |
||||
{% endblock %} |
||||
</main> |
||||
|
||||
<footer/> |
||||
|
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,27 @@ |
||||
{% extends "crm/base.html" %} {% block content %} |
||||
<div class="container"> |
||||
<div class="grid-x padding-bottom"> |
||||
<div class="cell medium-6 large-6 my-block bubble"> |
||||
<h1 class="title"> |
||||
{% if form.instance.pk %}Edit{% else %}Add{% endif %} Event |
||||
</h1> |
||||
|
||||
<form method="post"> |
||||
{% csrf_token %} {{ form }} |
||||
|
||||
<div class="mt-3"> |
||||
<button type="submit" class="btn small-button"> |
||||
Save Event |
||||
</button> |
||||
<a |
||||
href="{% url 'crm:planned_events' %}" |
||||
class="btn btn-secondary" |
||||
>Cancel</a |
||||
> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{% endblock %} |
||||
@ -0,0 +1,16 @@ |
||||
<div class="bottom-border"> |
||||
<div class="table-row-3-colums"> |
||||
<div class="left-column"> |
||||
<div class="semibold">{{ event.get_type_display }}</div> |
||||
<div class="minor-info">{{ event.description|truncatechars:100 }}</div> |
||||
</div> |
||||
|
||||
<div class="right-column"> |
||||
<span>{{ event.date|date:"d/m/Y H:i" }}</span> |
||||
<a href="{% url 'crm:edit_event' event.id %}" class="small-button">Edit</a> |
||||
<!-- {% if event.status == 'PLANNED' %} |
||||
<a href="{% url 'crm:start_event' event.id %}" class="small-button">Start</a> |
||||
{% endif %} --> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
@ -0,0 +1,58 @@ |
||||
{% extends "crm/base.html" %} |
||||
{% load crm_tags %} |
||||
|
||||
|
||||
{% block content %} |
||||
|
||||
{% if request.user|is_crm_manager %} |
||||
|
||||
<div class="d-flex"> |
||||
<a href="{% url 'crm:add_event' %}" class="small-button margin-v20"> |
||||
Add Event |
||||
</a> |
||||
<a href="{% url 'crm:add_prospect' %}" class="small-button margin-v20 left-margin"> |
||||
Add Prospect |
||||
</a> |
||||
</div> |
||||
|
||||
<div class="container grid-x padding-bottom"> |
||||
<div class="cell medium-6 large-6 my-block bubble"> |
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4"> |
||||
<h1 class="title">Completed Events</h1> |
||||
</div> |
||||
|
||||
<div class="list-group"> |
||||
{% for event in completed_events %} |
||||
{% include "crm/event_row.html" with event=event %} |
||||
{% empty %} |
||||
<div class="list-group-item">No completed events.</div> |
||||
{% endfor %} |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
<div class="cell medium-6 large-6 my-block bubble"> |
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4"> |
||||
<h1 class="title">Planned Events</h1> |
||||
</div> |
||||
|
||||
<div class="list-group"> |
||||
{% for event in planned_events %} |
||||
{% include "crm/event_row.html" with event=event %} |
||||
{% empty %} |
||||
<div class="list-group-item">No planned events.</div> |
||||
{% endfor %} |
||||
</div> |
||||
|
||||
</div> |
||||
</div> |
||||
|
||||
{% else %} |
||||
|
||||
Not authorized |
||||
|
||||
{% endif %} |
||||
|
||||
{% endblock %} |
||||
@ -0,0 +1,57 @@ |
||||
{% extends "crm/base.html" %} |
||||
|
||||
{% block content %} |
||||
<div class="container bubble"> |
||||
<h2>Prospects</h2> |
||||
|
||||
<div class="card mb-4"> |
||||
<div class="card-body"> |
||||
<form method="get" class="row g-3"> |
||||
{{ filter.form }} |
||||
<div class="col-12"> |
||||
<button type="submit" class="btn btn-primary">Filter</button> |
||||
<a href="{% url 'crm:prospect-list' %}" class="btn btn-secondary">Clear</a> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<a href="{% url 'crm:prospect-import' %}" class="btn btn-success">Import CSV</a> |
||||
<a href="{% url 'crm:send-bulk-email' %}" class="btn btn-primary">Send Email</a> |
||||
</div> |
||||
|
||||
<div class="table-responsive"> |
||||
<table class="table table-striped"> |
||||
<thead> |
||||
<tr> |
||||
<th><input type="checkbox" id="select-all"></th> |
||||
<th>Name</th> |
||||
<th>Email</th> |
||||
<th>Region</th> |
||||
<th>Status</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in prospects %} |
||||
<tr> |
||||
<td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td> |
||||
<td>{{ prospect.name }}</td> |
||||
<td>{{ prospect.email }}</td> |
||||
<td>{{ prospect.region }}</td> |
||||
<td> |
||||
{% for status in prospect.prospectstatus_set.all %} |
||||
<span class="badge bg-primary">{{ status.status.name }}</span> |
||||
{% endfor %} |
||||
</td> |
||||
<td> |
||||
<button class="btn btn-sm btn-secondary">Edit</button> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,47 @@ |
||||
{% extends "crm/base.html" %} |
||||
|
||||
{% block content %} |
||||
<div class="container mt-4"> |
||||
<h2>Send Bulk Email</h2> |
||||
|
||||
<form method="post"> |
||||
{% csrf_token %} |
||||
|
||||
<div class="mb-3"> |
||||
<label class="form-label">{{ form.prospects.label }}</label> |
||||
{{ form.prospects }} |
||||
{% if form.prospects.errors %} |
||||
<div class="alert alert-danger"> |
||||
{{ form.prospects.errors }} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<label class="form-label">{{ form.subject.label }}</label> |
||||
{{ form.subject }} |
||||
{% if form.subject.errors %} |
||||
<div class="alert alert-danger"> |
||||
{{ form.subject.errors }} |
||||
</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<label class="form-label">{{ form.content.label }}</label> |
||||
{{ form.content }} |
||||
{% if form.content.errors %} |
||||
<div class="alert alert-danger"> |
||||
{{ form.content.errors }} |
||||
</div> |
||||
{% endif %} |
||||
<div class="form-text"> |
||||
You can use {name} to insert the prospect's name. |
||||
</div> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-primary">Send Email</button> |
||||
<a href="{% url 'crm:prospect-list' %}" class="btn btn-secondary">Cancel</a> |
||||
</form> |
||||
</div> |
||||
{% endblock %} |
||||
@ -0,0 +1,7 @@ |
||||
from django import template |
||||
|
||||
register = template.Library() |
||||
|
||||
@register.filter(name='is_crm_manager') |
||||
def is_crm_manager(user): |
||||
return user.groups.filter(name='CRM Manager').exists() |
||||
@ -0,0 +1,3 @@ |
||||
from django.test import TestCase |
||||
|
||||
# Create your tests here. |
||||
@ -0,0 +1,15 @@ |
||||
from django.urls import path |
||||
from . import views |
||||
|
||||
app_name = 'crm' |
||||
|
||||
urlpatterns = [ |
||||
path('', views.EventListView.as_view(), name='planned_events'),path('', views.EventListView.as_view(), name='planned-events'), |
||||
path('events/add/', views.EventCreateView.as_view(), name='add_event'), |
||||
path('events/<int:pk>/edit/', views.EditEventView.as_view(), name='edit_event'), |
||||
path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'), |
||||
path('prospects/', views.ProspectListView.as_view(), name='prospect-list'), |
||||
path('add-prospect/', views.add_prospect, name='add_prospect'), |
||||
path('prospects/import/', views.CSVImportView.as_view(), name='prospect-import'), |
||||
path('email/send/', views.SendBulkEmailView.as_view(), name='send-bulk-email'), |
||||
] |
||||
@ -0,0 +1,186 @@ |
||||
# views.py |
||||
from django.views.generic import FormView, ListView, DetailView, CreateView, UpdateView |
||||
from django.views.generic.edit import FormView, BaseUpdateView |
||||
from django.contrib.auth.mixins import LoginRequiredMixin |
||||
from django.contrib.auth.decorators import permission_required |
||||
from django.contrib import messages |
||||
from django.shortcuts import render, redirect, get_object_or_404 |
||||
from django.urls import reverse_lazy |
||||
from django.http import HttpResponse, HttpResponseRedirect |
||||
from django.views import View |
||||
from django.utils import timezone |
||||
from django.contrib.sites.shortcuts import get_current_site |
||||
from django.template.loader import render_to_string |
||||
from django.core.mail import send_mail |
||||
from django.conf import settings |
||||
|
||||
from .models import Event, Prospect, EmailTracker, EmailCampaign, EventType |
||||
from .filters import ProspectFilter |
||||
from .forms import CSVImportForm, BulkEmailForm, EventForm |
||||
|
||||
from .mixins import CRMAccessMixin |
||||
|
||||
import csv |
||||
from io import TextIOWrapper |
||||
from datetime import datetime |
||||
|
||||
@permission_required('crm.view_crm', raise_exception=True) |
||||
def add_prospect(request): |
||||
if request.method == 'POST': |
||||
name = request.POST.get('name') |
||||
email = request.POST.get('email') |
||||
region = request.POST.get('region') |
||||
|
||||
try: |
||||
prospect = Prospect.objects.create( |
||||
name=name, |
||||
email=email, |
||||
region=region, |
||||
created_by=request.user, |
||||
modified_by=request.user |
||||
) |
||||
messages.success(request, f'Prospect {name} has been added successfully!') |
||||
return redirect('crm:events') # or wherever you want to redirect after success |
||||
except Exception as e: |
||||
messages.error(request, f'Error adding prospect: {str(e)}') |
||||
|
||||
return render(request, 'crm/add_prospect.html') |
||||
|
||||
class EventCreateView(CRMAccessMixin, CreateView): |
||||
model = Event |
||||
form_class = EventForm |
||||
template_name = 'crm/event_form.html' |
||||
success_url = reverse_lazy('crm:planned_events') |
||||
|
||||
def form_valid(self, form): |
||||
form.instance.created_by = self.request.user |
||||
form.instance.modified_by = self.request.user |
||||
return super().form_valid(form) |
||||
|
||||
class EditEventView(CRMAccessMixin, UpdateView): |
||||
model = Event |
||||
form_class = EventForm |
||||
template_name = 'crm/event_form.html' |
||||
success_url = reverse_lazy('crm:planned_events') |
||||
|
||||
def form_valid(self, form): |
||||
form.instance.modified_by = self.request.user |
||||
response = super().form_valid(form) |
||||
messages.success(self.request, 'Event updated successfully!') |
||||
return response |
||||
|
||||
class StartEventView(CRMAccessMixin, BaseUpdateView): |
||||
model = Event |
||||
http_method_names = ['post', 'get'] |
||||
|
||||
def get(self, request, *args, **kwargs): |
||||
return self.post(request, *args, **kwargs) |
||||
|
||||
def post(self, request, *args, **kwargs): |
||||
event = get_object_or_404(Event, pk=kwargs['pk'], status='PLANNED') |
||||
event.status = 'ACTIVE' |
||||
event.save() |
||||
|
||||
if event.type == 'MAIL': |
||||
return HttpResponseRedirect( |
||||
reverse_lazy('crm:setup_email_campaign', kwargs={'event_id': event.id}) |
||||
) |
||||
elif event.type == 'SMS': |
||||
return HttpResponseRedirect( |
||||
reverse_lazy('crm:setup_sms_campaign', kwargs={'event_id': event.id}) |
||||
) |
||||
elif event.type == 'PRESS': |
||||
return HttpResponseRedirect( |
||||
reverse_lazy('crm:setup_press_release', kwargs={'event_id': event.id}) |
||||
) |
||||
|
||||
messages.success(request, 'Event started successfully!') |
||||
return HttpResponseRedirect(reverse_lazy('crm:planned_events')) |
||||
|
||||
class EventListView(CRMAccessMixin, ListView): |
||||
model = Event |
||||
template_name = 'crm/events.html' |
||||
context_object_name = 'events' # We won't use this since we're providing custom context |
||||
|
||||
def get_context_data(self, **kwargs): |
||||
context = super().get_context_data(**kwargs) |
||||
context['planned_events'] = Event.objects.filter( |
||||
status='PLANNED' |
||||
).order_by('date') |
||||
context['completed_events'] = Event.objects.filter( |
||||
status='COMPLETED' |
||||
).order_by('-date') |
||||
return context |
||||
|
||||
class ProspectListView(CRMAccessMixin, ListView): |
||||
model = Prospect |
||||
template_name = 'crm/prospect_list.html' |
||||
context_object_name = 'prospects' |
||||
filterset_class = ProspectFilter |
||||
|
||||
def get_context_data(self, **kwargs): |
||||
context = super().get_context_data(**kwargs) |
||||
context['filter'] = self.filterset_class( |
||||
self.request.GET, |
||||
queryset=self.get_queryset() |
||||
) |
||||
return context |
||||
|
||||
class CSVImportView(CRMAccessMixin, FormView): |
||||
template_name = 'crm/csv_import.html' |
||||
form_class = CSVImportForm |
||||
success_url = reverse_lazy('prospect-list') |
||||
|
||||
def form_valid(self, form): |
||||
csv_file = TextIOWrapper( |
||||
form.cleaned_data['csv_file'].file, |
||||
encoding='utf-8' |
||||
) |
||||
reader = csv.DictReader(csv_file) |
||||
|
||||
for row in reader: |
||||
Prospect.objects.create( |
||||
email=row['email'], |
||||
name=row.get('name', ''), |
||||
region=row.get('region', ''), |
||||
created_by=self.request.user, |
||||
modified_by=self.request.user |
||||
) |
||||
|
||||
return super().form_valid(form) |
||||
|
||||
class SendBulkEmailView(CRMAccessMixin, FormView): |
||||
template_name = 'crm/send_bulk_email.html' |
||||
form_class = BulkEmailForm |
||||
success_url = reverse_lazy('crm:prospect-list') |
||||
|
||||
def form_valid(self, form): |
||||
prospects = form.cleaned_data['prospects'] |
||||
subject = form.cleaned_data['subject'] |
||||
content = form.cleaned_data['content'] |
||||
|
||||
# Create Event for this email campaign |
||||
event = Event.objects.create( |
||||
date=datetime.now(), |
||||
type=EventType.MAILING, |
||||
description=f"Bulk email: {subject}", |
||||
status='COMPLETED', |
||||
created_by=self.request.user, |
||||
modified_by=self.request.user |
||||
) |
||||
event.prospects.set(prospects) |
||||
|
||||
# Send emails |
||||
success_count, error_count = send_bulk_email( |
||||
subject=subject, |
||||
content=content, |
||||
prospects=prospects |
||||
) |
||||
|
||||
# Show result message |
||||
messages.success( |
||||
self.request, |
||||
f"Sent {success_count} emails successfully. {error_count} failed." |
||||
) |
||||
|
||||
return super().form_valid(form) |
||||
Loading…
Reference in new issue