diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c49fe0a..74aaf2bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ deploy_mp: - pip install -r /home/dj_crm/dj_crm/requirements.txt - python /home/dj_crm/dj_crm/manage.py migrate --noinput - sudo /etc/init.d/uwsgi restart + - sudo /usr/bin/supervisorctl restart all only: - master tags: @@ -35,6 +36,7 @@ deploy_live: - pip install -r /home/dj_crm/dj_crm/requirements.txt - python /home/dj_crm/dj_crm/manage.py migrate --noinput - sudo /etc/init.d/uwsgi restart + - sudo /usr/bin/supervisorctl restart all only: - master tags: diff --git a/Dockerfile b/Dockerfile index aa455553..1b897595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ COPY . /app/ RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ apt update && \ - apt install -y git ruby-dev nodejs postgresql-client && \ + apt install -y git ruby-dev nodejs postgresql-client redis-server wkhtmltopdf && \ apt clean && \ gem install compass sass && \ npm -g install less && \ diff --git a/README.rst b/README.rst index 488d40d6..dbea41a1 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,28 @@ Demo credentials for Django CRM: * **Password:** admin +Installation +============ + +* Install the dependencies + + .. code-block:: python + + pip install -r requirements.txt + python manage.py makemigrations + python manage.py createsuperuser + python manage.py runserver + + This will install all the required dependencies for django-crm. + After this download and install the System Requirements. + + +System Requirements +=================== + +- wkhtmltopdf (https://wkhtmltopdf.org/downloads.html) +- sass (https://www.npmjs.com/package/sass) or (https://rubygems.org/gems/sass) + Community ========= diff --git a/accounts/forms.py b/accounts/forms.py index fc5d4359..2815fbf9 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,6 +1,6 @@ from django import forms from .models import Account -from common.models import Comment, Attachments +from common.models import Comment, Attachments, User from leads.models import Lead from contacts.models import Contact from django.db.models import Q @@ -36,14 +36,18 @@ class AccountForm(forms.ModelForm): 'placeholder': 'Postcode'}) self.fields["billing_country"].choices = [ ("", "--Country--"), ] + list(self.fields["billing_country"].choices)[1:] - self.fields["lead"].queryset = Lead.objects.all( - ).exclude(status='closed') - if request_user: + # self.fields["lead"].queryset = Lead.objects.all( + # ).exclude(status='closed') + if request_user.role == 'ADMIN': + self.fields["lead"].queryset = Lead.objects.filter().exclude(status='closed') + self.fields["contacts"].queryset = Contact.objects.filter() + else: self.fields["lead"].queryset = Lead.objects.filter( Q(assigned_to__in=[request_user]) | Q(created_by=request_user)).exclude(status='closed') self.fields["contacts"].queryset = Contact.objects.filter( Q(assigned_to__in=[request_user]) | Q(created_by=request_user)) + self.fields['assigned_to'].required = False if account_view: self.fields['billing_address_line'].required = True self.fields['billing_street'].required = True @@ -55,7 +59,7 @@ class AccountForm(forms.ModelForm): class Meta: model = Account fields = ('name', 'phone', 'email', 'website', 'industry', - 'description', 'status', + 'description', 'status', 'assigned_to', 'billing_address_line', 'billing_street', 'billing_city', 'billing_state', 'billing_postcode', 'billing_country', 'lead', 'contacts') @@ -75,3 +79,15 @@ class AccountAttachmentForm(forms.ModelForm): class Meta: model = Attachments fields = ('attachment', 'account') + + +class EmailForm(forms.Form): + recipients = forms.CharField(max_length=500) + message_subject = forms.CharField(max_length=500) + message_body = forms.CharField(widget=forms.Textarea) + + def __init__(self, *args, **kwargs): + super(EmailForm, self).__init__(*args, **kwargs) + self.fields['message_subject'].widget.attrs['class'] = 'form-control' + self.fields['message_subject'].widget.attrs['required'] = True + self.fields['message_subject'].widget.attrs['placeholder'] = 'Email Subject' diff --git a/accounts/migrations/0007_email.py b/accounts/migrations/0007_email.py new file mode 100644 index 00000000..26f28b3d --- /dev/null +++ b/accounts/migrations/0007_email.py @@ -0,0 +1,26 @@ +# Generated by Django 2.1.7 on 2019-04-30 08:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contacts', '0003_merge_20190214_1427'), + ('accounts', '0006_auto_20190212_1708'), + ] + + operations = [ + migrations.CreateModel( + name='Email', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sent_at', models.DateTimeField(auto_now_add=True)), + ('message_subject', models.TextField(null=True)), + ('message_body', models.TextField(null=True)), + ('recipient', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recieved_email', to='contacts.Contact')), + ('sender', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_email', to='accounts.Account')), + ], + ), + ] diff --git a/accounts/migrations/0008_account_assigned_to.py b/accounts/migrations/0008_account_assigned_to.py new file mode 100644 index 00000000..5ed47fcf --- /dev/null +++ b/accounts/migrations/0008_account_assigned_to.py @@ -0,0 +1,20 @@ +# Generated by Django 2.1.7 on 2019-05-17 09:14 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0007_email'), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='assigned_to', + field=models.ManyToManyField(related_name='account_assigned_users', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index bf43f95f..4529fa09 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -6,6 +6,7 @@ from common.models import User from common.utils import INDCHOICES, COUNTRIES from phonenumber_field.modelfields import PhoneNumberField from django.utils.text import slugify +from contacts.models import Contact class Tags(models.Model): @@ -65,6 +66,8 @@ class Account(models.Model): "Name of Contact", "Contact Name"), max_length=120) contacts = models.ManyToManyField( 'contacts.Contact', related_name="account_contacts") + assigned_to = models.ManyToManyField( + User, related_name='account_assigned_users') def __str__(self): return self.name @@ -102,3 +105,23 @@ class Account(models.Model): else: address += self.get_billing_country_display() return address + + +class Email(models.Model): + sender = models.ForeignKey( + Account, related_name='sent_email', on_delete=models.SET_NULL, null=True) + + recipient = models.ForeignKey( + Contact, related_name='recieved_email', on_delete=models.SET_NULL, null=True) + + sent_at = models.DateTimeField(auto_now_add=True) + message_subject = models.TextField(null=True) + message_body = models.TextField(null=True) + + def __str__(self): + return self.message_body + + def save(self, *args, **kwargs): + # prevent user from sending messages to themselves + if self.sender.email != self.recipient.email: + super(Email, self).save(*args, **kwargs) diff --git a/accounts/tasks.py b/accounts/tasks.py new file mode 100644 index 00000000..faeac063 --- /dev/null +++ b/accounts/tasks.py @@ -0,0 +1,28 @@ +from celery.task import task +from django.conf import settings +from django.core.mail import EmailMessage +from django.template.loader import render_to_string +from accounts.models import Account, Email +from contacts.models import Contact + + +@task +def send_email(subject, html_content, from_email=None, + recipients=[]): + account = Account.objects.filter(id=from_email).first() + if account: + contacts = recipients + for contact in contacts: + contact_obj = Contact.objects.filter(email=contact).first() + if contact_obj: + Email.objects.create(sender=account, + recipient=contact_obj,message_subject=subject, + message_body=html_content) + msg = EmailMessage( + subject, + html_content, + from_email=account.email, + to=[contact_obj.email, ] + ) + msg.content_subtype = "html" + msg.send() \ No newline at end of file diff --git a/accounts/templates/accounts.html b/accounts/templates/accounts.html index ec1e69aa..0ca7b419 100644 --- a/accounts/templates/accounts.html +++ b/accounts/templates/accounts.html @@ -11,7 +11,8 @@
- Add New Account + + Add New Account
@@ -25,23 +26,27 @@
- +
- +
- + {% for tag in tags %} - + {% endfor%}
@@ -67,10 +72,12 @@
@@ -78,9 +85,10 @@
- Open Accounts - {% if show_pageitems %} {% show_pageitems %}{% else %}{{open_accounts|length}}{% endif %} + Open Accounts - {% if show_pageitems %} + {% show_pageitems %}{% else %}{{open_accounts|length}}{% endif %} - +
@@ -106,19 +114,25 @@ {% endif %} {% for each_account in open_accounts %} - {{ forloop.counter }} - {% if each_account.name %}{{ each_account.name }}{% endif %} + {{ forloop.counter }} + {% if each_account.name %}{{ each_account.name }}{% endif %} + {% if each_account.created_by.profile_pic %} - {% thumbnail each_account.created_by.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} + {% thumbnail each_account.created_by.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} {% else %} - Micro profile pic + Micro profile pic {% endif %} - {% if each_account.billing_city %}{{ each_account.billing_city }} {% else %} Not specified {% endif %} - {% if each_account.billing_state %}{{ each_account.billing_state }} {% else %} Not specified {% endif %} + {% if each_account.billing_city %}{{ each_account.billing_city }} {% else %} Not specified + {% endif %} + {% if each_account.billing_state %}{{ each_account.billing_state }} {% else %} Not + specified {% endif %} {% with tags=each_account.tags.all %} {% if tags %} @@ -132,8 +146,10 @@ {# View #} - - + + {% endfor %} @@ -153,10 +169,11 @@
- Closed Accounts - {% if show_pageitems %} {% show_pageitems %}{% else %}{{close_accounts|length}}{% endif %} + Closed Accounts - {% if show_pageitems %} + {% show_pageitems %}{% else %}{{close_accounts|length}}{% endif %} - - + +
@@ -179,11 +196,15 @@ {% paginate 10 close_accounts %} {% endif %} {% for each_account in close_accounts %} - + - - - + + + {% endfor %} @@ -228,48 +251,72 @@ {% block js_block %} @@ -196,11 +226,26 @@ // $(document).ready(function() { // $('.assigned_users').select2(); // }); - $(document).ready(function() { + $(document).ready(function () { $("#id_contacts").select2(); + $("#id_assigned_to").select2() + $("#id_assigned_to").change(function(){ + $("#id_assigned_to").attr('name', 'assigned_to') + }) + // $("#id_assigned_to") + // var data_assigned_to = [] + // data_assigned_to = $('#id_assigned_to').val() + // $('#id_assigned_to_').val($('#id_assigned_to').val()) + // console.log($('#id_assigned_to').val()) + // console.log($('#id_assigned_to_').val()) + // $('#id_assigned_to').change(function(){ + // data_assigned_to = [] + // data_assigned_to.push($(this).val()) + // $('#id_assigned_to_hidden').val(data_assigned_to) + // }) }); - $("#copy_billing_btn").click(function(){ + $("#copy_billing_btn").click(function () { var address_line = $("#id_address_line").val(); var street = $("#id_street").val(); var city = $("#id_city").val(); @@ -208,52 +253,50 @@ var postcode = $("#id_postcode").val(); var country = $("#id_country").val(); }); - - $(".savenew").on('click', function(e){ + + $(".savenew").on('click', function (e) { e.preventDefault() $("#save_new_form").val('true') $('#formid').submit() }); - + function onAddTag(tag) { alert("Added a tag: " + tag); } function onRemoveTag(tag) { alert("Removed a tag: " + tag); } - - function onChangeTag(input,tag) { + + function onChangeTag(input, tag) { alert("Changed a tag: " + tag); } - - $(function() { - - $('#tags_1').tagsInput({width:'auto'}); + + $(function () { + + $('#tags_1').tagsInput({ width: 'auto' }); $('#tags_2').tagsInput({ width: 'auto', - onChange: function(elem, elem_tags) - { - var languages = ['php','ruby','javascript']; - $('.tag', elem_tags).each(function() - { - if($(this).text().search(new RegExp('\\b(' + languages.join('|') + ')\\b')) >= 0) + onChange: function (elem, elem_tags) { + var languages = ['php', 'ruby', 'javascript']; + $('.tag', elem_tags).each(function () { + if ($(this).text().search(new RegExp('\\b(' + languages.join('|') + ')\\b')) >= 0) $(this).css('background-color', 'yellow'); }); } }); $('#tags_3').tagsInput({ width: 'auto', - + //autocomplete_url:'test/fake_plaintext_endpoint.html' //jquery.autocomplete (not jquery ui) - autocomplete_url:'test/fake_json_endpoint.html' // jquery ui autocomplete requires a json endpoint + autocomplete_url: 'test/fake_json_endpoint.html' // jquery ui autocomplete requires a json endpoint }); - - - // Uncomment this line to see the callback functions in action - // $('input.tags').tagsInput({onAddTag:onAddTag,onRemoveTag:onRemoveTag,onChange: onChangeTag}); - - // Uncomment this line to see an input with no interface for adding new tags. - // $('input.tags').tagsInput({interactive:false}); + + + // Uncomment this line to see the callback functions in action + // $('input.tags').tagsInput({onAddTag:onAddTag,onRemoveTag:onRemoveTag,onChange: onChangeTag}); + + // Uncomment this line to see an input with no interface for adding new tags. + // $('input.tags').tagsInput({interactive:false}); }); function remove_attachment(x) { const con = confirm("Do you want to Delete it for Sure!?"); @@ -273,22 +316,29 @@ $('form#formid').ajaxForm({ - type:'POST', - dataType:'json', + type: 'POST', + dataType: 'json', url: ".", - data:$('#formid').serialize(), - success: function(data) { - if(data.error){ + data: $('#formid').serialize(), + success: function (data) { + if (data.error) { $('.error').html('') for (var key in data.errors) { - $('#id__'+key).html("

" + data.errors[key][0] + "

"); + $('#id__' + key).html("

" + data.errors[key][0] + "

"); }; $('#submit_btn').removeAttr('disabled') } - else{ - window.location = data.success_url; - } + else { + window.location = data.success_url; + } } }); + + // $('#call_save').click(function (e) { + // e.preventDefault() + // console.log($('#formid').serialize()) + // console.log($('#formid').serializeArray()) + + // }) -{% endblock js_block %} +{% endblock js_block %} \ No newline at end of file diff --git a/accounts/templates/create_mail_accounts.html b/accounts/templates/create_mail_accounts.html new file mode 100644 index 00000000..0780b1f8 --- /dev/null +++ b/accounts/templates/create_mail_accounts.html @@ -0,0 +1,178 @@ +{% extends 'sales/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% block extralinks %} +{% load thumbnail %} + +{% endblock %} +{% block content %} + +
+ +
+
+
+
+
+
+
+
+ Send Mail +
+ +
+
+
+ + + + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+ + +
+ +
+ + Cancel +
+
+ +
+
+
+
+ +
+
+
+{% endblock %} +{% block js_block %} + + + +{% endblock js_block %} \ No newline at end of file diff --git a/accounts/templates/view_account.html b/accounts/templates/view_account.html index 6c9d09b4..5178e4eb 100644 --- a/accounts/templates/view_account.html +++ b/accounts/templates/view_account.html @@ -1,10 +1,21 @@ {% extends 'sales/base.html' %} {% load staticfiles %} +{% load thumbnail %} {% block breadcrumb %} + +{% block extralinks %} + + +{% endblock extralinks %} + @@ -12,19 +23,19 @@ {% block content %}
-
-
+
+
Overview - + @@ -34,7 +45,7 @@
{% if account_record.name %}
-
+
@@ -74,12 +85,12 @@
{% endif %} - {% if account_record.contacts %} + {% if account_record.contacts.all %}
{% for contact in account_record.contacts.all %} - + {% endfor %}
@@ -104,11 +115,24 @@
{% endif %} +
+ {% if account_record.assigned_to.all %} +
+ +
+ {% for user in account_record.assigned_to.all %} +
{{ user }}
+ {% endfor %} +
+
+ {% endif %} +
{% if account_record.description %}
- +
{% endif %} @@ -127,7 +151,8 @@
- Created by {{ account_record.created_by }} created on {{ account_record.created_on }} + Created by {{ account_record.created_by }} created on + {{ account_record.created_on }}
@@ -141,10 +166,13 @@
-
+
+ +

- +
@@ -155,11 +183,19 @@ {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == attachment.created_by %}
- +
{% endif %}
+ {% if attachment.attachment %} + {% if 'image' in attachment.file_type %} + {% thumbnail attachment.attachment "80x80" crop="center" as im %} + + {% endthumbnail %} + {% endif %} + {% endif %}
  {{attachment.file_name}} Download
@@ -183,11 +219,15 @@ {% if comment_permission %}
- -
+ +
+ +

- +
@@ -198,7 +238,8 @@
  • {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == comment.commented_by %}
    - +
  • -
    {{ forloop.counter }}{% if each_account.name %}{{ each_account.name }}{% endif %}{% if each_account.billing_city %}{{ each_account.billing_city }} {% else %} Not specified {% endif %}{% if each_account.billing_state %}{{ each_account.billing_state }} {% else %} Not specified {% endif %}{% if each_account.name %}{{ each_account.name }}{% endif %} + {% if each_account.billing_city %}{{ each_account.billing_city }} {% else %} Not specified + {% endif %}{% if each_account.billing_state %}{{ each_account.billing_state }} {% else %} Not + specified {% endif %} {% with tags=each_account.tags.all %} {% if tags %} @@ -196,8 +217,10 @@ {% endwith %} - - + +
    @@ -280,9 +331,11 @@ {% endfor %} @@ -305,7 +358,8 @@
    Opportunities - +
    {{ contact.phone }} {{ contact.email }} - View - Edit - Remove + View + Edit + Remove
    @@ -327,10 +381,13 @@ - {% endfor %} @@ -353,7 +410,8 @@
    Cases - +
    {{opportunity.name}} {{opportunity.stage}} {{opportunity.probability}} {% if opportunity.probability %} % {% endif %} - View - Edit - Remove + + View + Edit + Remove
    @@ -375,10 +433,12 @@ - {% endfor %} @@ -394,38 +454,100 @@ + +
    +
    +
    +
    +
    + Tasks + +
    +
    +
    {{ case.name }} {{ case.status }} {{ case.priority }} - View - Edit - Remove + + View + Edit + Remove
    + {% if tasks|length > 0 %} + + + + + + + + + + {% endif %} + + {% for task in tasks %} + + + + + + + + {% endfor %} + +
    S.NoTitleStatusPriorityActions
    {{ forloop.counter }}{{task.title}}{{task.status}}{{task.priority}} {% if task.probability %} % {% endif %} + View + Edit + Remove +
    +
    + {% ifequal tasks|length 0 %} +
    No tasks Records Found
    + {% endifequal %} +
    +
    +
    +
    +
    +
    +
    + +
    + Processing File... +
    +
    {% endblock content %} {% block js_block %} + + + - {% endblock js_block %} \ No newline at end of file diff --git a/accounts/tests.py b/accounts/tests.py index ea3c4149..cfd3e53c 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -112,7 +112,7 @@ class AccountsListTestCase(AccountCreateTest, TestCase): def test_accounts_list(self): self.accounts = Account.objects.all() - response = self.client.get('/accounts/list/') + response = self.client.get(reverse('accounts:list')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'accounts.html') @@ -125,7 +125,7 @@ class AccountsListTestCase(AccountCreateTest, TestCase): 'billing_state': "billing_state", 'billing_postcode': "billing_postcode", 'billing_country': "billing_country"} - response = self.client.post('/accounts/list/', data) + response = self.client.post(reverse('accounts:list'), data) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'accounts.html') @@ -152,7 +152,7 @@ class AccountsRemoveTestCase(AccountCreateTest, TestCase): def test_accounts_remove(self): response = self.client.get( '/accounts/' + str(self.account.id) + '/delete/') - self.assertEqual(response['location'], '/accounts/list/') + self.assertEqual(response['location'], '/accounts/') # def test_accounts_remove_status(self): # Account.objects.filter(id=self.account.id).delete() diff --git a/accounts/urls.py b/accounts/urls.py index b47c7050..d1b2f759 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -2,13 +2,13 @@ from django.urls import path from accounts.views import ( AccountsListView, CreateAccountView, AccountDetailView, AccountUpdateView, AccountDeleteView, AddCommentView, UpdateCommentView, DeleteCommentView, - AddAttachmentView, DeleteAttachmentsView + AddAttachmentView, DeleteAttachmentsView, create_mail ) app_name = 'accounts' urlpatterns = [ - path('list/', AccountsListView.as_view(), name='list'), + path('', AccountsListView.as_view(), name='list'), path('create/', CreateAccountView.as_view(), name='new_account'), path('/view/', AccountDetailView.as_view(), name="view_account"), path('/edit/', AccountUpdateView.as_view(), name="edit_account"), @@ -23,4 +23,6 @@ urlpatterns = [ name="add_attachment"), path('attachment/remove/', DeleteAttachmentsView.as_view(), name="remove_attachment"), + path('create-mail//', create_mail, + name="create_mail"), ] diff --git a/accounts/views.py b/accounts/views.py index 433fecb0..1c15e225 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,11 +1,11 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import ( - CreateView, UpdateView, DetailView, TemplateView, View, DeleteView) + CreateView, UpdateView, DetailView, TemplateView, View, DeleteView, FormView) from accounts.forms import AccountForm, AccountCommentForm, \ - AccountAttachmentForm -from accounts.models import Account, Tags + AccountAttachmentForm, EmailForm +from accounts.models import Account, Tags, Email from common.models import User, Comment, Attachments from common.utils import INDCHOICES, COUNTRIES, \ CURRENCY_CODES, CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE @@ -16,6 +16,10 @@ from django.urls import reverse_lazy, reverse from leads.models import Lead from django.core.exceptions import PermissionDenied from django.db.models import Q +from django.contrib.auth.decorators import login_required +from accounts.tasks import send_email +from common.tasks import send_email_user_mentions +from django.contrib.sites.shortcuts import get_current_site class AccountsListView(LoginRequiredMixin, TemplateView): @@ -28,6 +32,9 @@ class AccountsListView(LoginRequiredMixin, TemplateView): if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: queryset = queryset.filter(created_by=self.request.user.id) + if self.request.GET.get('tag', None): + queryset = queryset.filter(tags__in = self.request.GET.getlist('tag')) + request_post = self.request.POST if request_post: if request_post.get('name'): @@ -40,9 +47,9 @@ class AccountsListView(LoginRequiredMixin, TemplateView): queryset = queryset.filter( industry__icontains=request_post.get('industry')) if request_post.get('tag'): - queryset = queryset.filter(tags__in=request_post.get('tag')) + queryset = queryset.filter(tags__in=request_post.getlist('tag')) - return queryset + return queryset.distinct() def get_context_data(self, **kwargs): context = super(AccountsListView, self).get_context_data(**kwargs) @@ -57,6 +64,12 @@ class AccountsListView(LoginRequiredMixin, TemplateView): context["per_page"] = self.request.POST.get('per_page') tag_ids = list(set(Account.objects.values_list('tags', flat=True))) context["tags"] = Tags.objects.filter(id__in=tag_ids) + if self.request.POST.get('tag', None): + context["request_tags"] = self.request.POST.getlist('tag') + elif self.request.GET.get('tag', None): + context["request_tags"] = self.request.GET.getlist('tag') + else: + context["request_tags"] = None search = False if ( @@ -88,15 +101,21 @@ class CreateAccountView(LoginRequiredMixin, CreateView): template_name = "create_account.html" def dispatch(self, request, *args, **kwargs): - self.users = User.objects.filter(is_active=True).order_by('email') + if self.request.user.role == 'ADMIN' or self.request.user.is_superuser: + self.users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + self.users = [] + else: + self.users = User.objects.filter(role='ADMIN').order_by('email') return super( CreateAccountView, self).dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(CreateAccountView, self).get_form_kwargs() kwargs.update({"account": True}) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: - kwargs.update({"request_user": self.request.user}) + kwargs.update({"request_user": self.request.user}) + # if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + # kwargs.update({"request_user": self.request.user}) return kwargs def post(self, request, *args, **kwargs): @@ -125,6 +144,8 @@ class CreateAccountView(LoginRequiredMixin, CreateView): account_object.tags.add(tag) if self.request.POST.getlist('contacts', []): account_object.contacts.add(*self.request.POST.getlist('contacts')) + if self.request.POST.getlist('assigned_to', []): + account_object.assigned_to.add(*self.request.POST.getlist('assigned_to')) if self.request.FILES.get('account_attachment'): attachment = Attachments() attachment.created_by = self.request.user @@ -159,8 +180,13 @@ class CreateAccountView(LoginRequiredMixin, CreateView): context["industries"] = INDCHOICES context["countries"] = COUNTRIES context["contact_count"] = Contact.objects.count() - context["leads"] = Lead.objects.exclude( - status__in=['converted', 'closed']) + if self.request.user.role == 'ADMIN': + context["leads"] = Lead.objects.exclude( + status__in=['converted', 'closed']) + else: + context["leads"] = Lead.objects.filter( + Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude( + status__in=['converted', 'closed']) context["lead_count"] = context["leads"].count() if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: context["lead_count"] = Lead.objects.filter( @@ -185,6 +211,13 @@ class AccountDetailView(LoginRequiredMixin, DetailView): self.request.user.is_superuser or self.request.user.role == 'ADMIN' ) else False + if self.request.user.is_superuser or self.request.user.role == 'ADMIN': + users_mention = list(User.objects.all().values('username')) + elif self.request.user != account_record.created_by: + users_mention = [{'username': account_record.created_by.username}] + else: + users_mention = [] + context.update({ "comments": account_record.accounts_comments.all(), "attachments": account_record.account_attachment.all(), @@ -201,6 +234,8 @@ class AccountDetailView(LoginRequiredMixin, DetailView): "case_priority": PRIORITY_CHOICE, "case_status": STATUS_CHOICE, 'comment_permission': comment_permission, + 'tasks':account_record.accounts_tasks.all(), + 'users_mention': users_mention, }) return context @@ -211,15 +246,21 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): template_name = "create_account.html" def dispatch(self, request, *args, **kwargs): - self.users = User.objects.filter(is_active=True).order_by('email') + if self.request.user.role == 'ADMIN' or self.request.user.is_superuser: + self.users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + self.users = [] + else: + self.users = User.objects.filter(role='ADMIN').order_by('email') return super(AccountUpdateView, self).dispatch( request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(AccountUpdateView, self).get_form_kwargs() kwargs.update({"account": True}) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: - kwargs.update({"request_user": self.request.user}) + kwargs.update({"request_user": self.request.user}) + # if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + # kwargs.update({"request_user": self.request.user}) return kwargs def post(self, request, *args, **kwargs): @@ -248,6 +289,11 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): if self.request.POST.getlist('contacts', []): account_object.contacts.clear() account_object.contacts.add(*self.request.POST.getlist('contacts')) + if self.request.POST.getlist('assigned_to', []): + account_object.assigned_to.clear() + account_object.assigned_to.add(*self.request.POST.getlist('assigned_to')) + else: + account_object.assigned_to.clear() if self.request.FILES.get('account_attachment'): attachment = Attachments() attachment.created_by = self.request.user @@ -282,8 +328,13 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): context["industries"] = INDCHOICES context["countries"] = COUNTRIES context["contact_count"] = Contact.objects.count() - context["leads"] = Lead.objects.exclude( - status__in=['converted', 'closed']) + if self.request.user.role == 'ADMIN': + context["leads"] = Lead.objects.exclude( + status__in=['converted', 'closed']) + else: + context["leads"] = Lead.objects.filter( + Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude( + status__in=['converted', 'closed']) context["lead_count"] = context["leads"].count() if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: context["lead_count"] = Lead.objects.filter( @@ -331,6 +382,11 @@ class AddCommentView(LoginRequiredMixin, CreateView): comment.commented_by = self.request.user comment.account = self.account comment.save() + comment_id = comment.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'accounts', domain=current_site.domain, + protocol=self.request.scheme) + return JsonResponse({ "comment_id": comment.id, "comment": comment.comment, "commented_on": comment.commented_on, @@ -360,6 +416,10 @@ class UpdateCommentView(LoginRequiredMixin, View): def form_valid(self, form): self.comment_obj.comment = form.cleaned_data.get("comment") self.comment_obj.save(update_fields=["comment"]) + comment_id = self.comment_obj.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'accounts', domain=current_site.domain, + protocol=self.request.scheme) return JsonResponse({ "comment_id": self.comment_obj.id, "comment": self.comment_obj.comment, @@ -422,7 +482,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): kwargs={'pk': attachment.id}), "attachment_display": attachment.get_file_type_display(), "created_on": attachment.created_on, - "created_by": attachment.created_by.email + "created_by": attachment.created_by.email, + "file_type": attachment.file_type() }) def form_invalid(self, form): @@ -446,3 +507,23 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): data = { 'error': "You don't have permission to delete this attachment."} return JsonResponse(data) + +@login_required +def create_mail(request, account_id): + + if request.method == 'GET': + account = get_object_or_404(Account, pk=account_id) + contacts_list = list(account.contacts.all().values('email')) + email_form = EmailForm() + return render(request, 'create_mail_accounts.html', {'account_id': account_id, + 'contacts_list': contacts_list, 'email_form': email_form}) + + if request.method == 'POST': + form = EmailForm(request.POST) + if form.is_valid(): + send_email.delay(form.data.get('message_subject'), form.data.get('message_body'), + from_email=account_id, recipients=form.data.get('recipients').split(',')) + + return JsonResponse({'error': False}) + else: + return JsonResponse({'error': True, 'errors': form.errors}) diff --git a/cases/forms.py b/cases/forms.py index 076dcdb3..8c4861ff 100644 --- a/cases/forms.py +++ b/cases/forms.py @@ -14,10 +14,11 @@ class CaseForm(forms.ModelForm): field.widget.attrs = {"class": "form-control"} self.fields['description'].widget.attrs.update({ 'rows': '6'}) - self.fields['assigned_to'].queryset = assigned_users + if assigned_users: + self.fields['assigned_to'].queryset = assigned_users + self.fields['assigned_to'].required = False self.fields['account'].queryset = case_accounts self.fields['contacts'].queryset = case_contacts - self.fields['assigned_to'].required = False self.fields['contacts'].required = False for key, value in self.fields.items(): value.widget.attrs['placeholder'] = value.label diff --git a/cases/tasks.py b/cases/tasks.py new file mode 100644 index 00000000..df560af1 --- /dev/null +++ b/cases/tasks.py @@ -0,0 +1,36 @@ +from celery.task import task +from django.conf import settings +from django.core.mail import EmailMultiAlternatives, EmailMessage +from django.db.models import Q +from django.shortcuts import reverse +from django.template.loader import render_to_string + +from accounts.models import User +from cases.models import Case + + +@task +def send_email_to_assigned_user(recipients, case_id, domain='demo.django-crm.io', protocol='http'): + """ Send Mail To Users When they are assigned to a case """ + case = Case.objects.get(id=case_id) + for user in recipients: + recipients_list = [] + user = User.objects.filter(id=user).first() + if user: + recipients_list.append(user.email) + context = {} + context["url"] = protocol + '://' + domain + \ + reverse('cases:view_case', args=(case.id,)) + context["user"] = user + context["case"] = case + subject = 'Assigned to case.' + html_content = render_to_string( + 'assigned_to/cases_assigned.html', context=context) + + msg = EmailMessage( + subject, + html_content, + to=recipients_list + ) + msg.content_subtype = "html" + msg.send() diff --git a/cases/templates/view_case.html b/cases/templates/view_case.html index e8b72bcf..b6eae98c 100644 --- a/cases/templates/view_case.html +++ b/cases/templates/view_case.html @@ -1,6 +1,15 @@ {% extends 'sales/base.html' %} {% load staticfiles %} +{% load thumbnail %} {% block content %} +{% block extralinks %} + + +{% endblock extralinks %}
    + +{% endblock content %} +{% block js_block %} + + + +{% endblock js_block %} \ No newline at end of file diff --git a/invoices/templates/invoices_list.html b/invoices/templates/invoices_list.html new file mode 100644 index 00000000..b6e039b3 --- /dev/null +++ b/invoices/templates/invoices_list.html @@ -0,0 +1,369 @@ +{% extends 'sales/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% load thumbnail %} +{% block extralinks %} + +{% endblock %} +{% block content %} + +
    + + + +
    +
    +
    +
    +
    +
    +
    Filters
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + +
    +
    + + Clear +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + Invoices - {% if show_pageitems %} + {% show_pageitems %}{% else %} + {{ invoices|length }}{% endif %} + + + +
    +
    + + + {% if invoices|length > 0 %} + + + + + + + + + + + + {% endif %} + + + {% if per_page %} + {% paginate per_page invoices %} + {% else %} + {% paginate 10 invoices %} + {% endif %} + {% for invoice in invoices %} + + + + + + + + + + + + {% endfor %} + +
    IDInvoice TitleInvoice NumberEmailStatusAssigned ToCreated byCreated OnActions
    {{ forloop.counter }} + {{ invoice.invoice_title }} {{ invoice.invoice_number }} {{ invoice.email }} {{ invoice.status }} + {% with users=invoice.assigned_to.all %} + {% for user in users %} + {% if user.profile_pic %} + {% thumbnail user.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} + {% else %} + + {% endif %} + {% empty %} + None + {% endfor %} + {% endwith %} + + {% if invoice.created_by.profile_pic %} + {% thumbnail invoice.created_by.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} + {% else %} + Micro profile pic + {% endif %} + {{ invoice.created_on|date:'d-m-Y' }} + + {% if request.user == invoice.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} + + {% if invoice.is_draft %} + + {% endif %} + {% if invoice.is_sent == True %} + + + + {% endif %} + {% if invoice.is_resent %} + + + {% endif %} + {% if invoice.is_paid_or_cancelled == True %} + + {% endif %} + {% endif %} +
    +
    + {%ifequal invoices|length 0%} +
    No Invoices Found
    + {%endifequal%} +
    + {% show_pages %} +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +{% endblock %} +{% block js_block %} + + +{% endblock js_block %} \ No newline at end of file diff --git a/invoices/tests.py b/invoices/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/invoices/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/invoices/urls.py b/invoices/urls.py new file mode 100644 index 00000000..c08d359b --- /dev/null +++ b/invoices/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from invoices.views import * + +app_name = 'invoices' + + +urlpatterns = [ + path('', invoices_list, name='invoices_list'), + path('create/', invoices_create, name='invoices_create'), + path('detail//', invoice_details, name='invoice_details'), + path('edit//', invoice_edit, name='invoice_edit'), + path('delete//', invoice_delete, name='invoice_delete'), + path('download//', invoice_download, name='invoice_download'), + path('send-mail//', invoice_send_mail, name='invoice_send_mail'), + path('cancelled-mail//', invoice_change_status_cancelled, name='invoice_change_status_cancelled'), + path('paid-mail//', invoice_change_status_paid, name='invoice_change_status_paid'), + + path('comment/add/', AddCommentView.as_view(), name="add_comment"), + path('comment/edit/', UpdateCommentView.as_view(), name="edit_comment"), + path('comment/remove/', DeleteCommentView.as_view(), name="remove_comment"), + + path('attachment/add/', AddAttachmentView.as_view(), name="add_attachment"), + path('attachment/remove/', DeleteAttachmentsView.as_view(), + name="remove_attachment"), + +] diff --git a/invoices/views.py b/invoices/views.py new file mode 100644 index 00000000..4c2e1a2c --- /dev/null +++ b/invoices/views.py @@ -0,0 +1,415 @@ +import io +import os + +import pdfkit +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import PermissionDenied +from django.db.models import Q +from django.http.response import HttpResponse, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render, reverse +from django.template.loader import render_to_string +from django.views.generic import (CreateView, DeleteView, DetailView, FormView, + TemplateView, UpdateView, View) + +from common.models import Address, Attachments, Comment, User +from common.tasks import send_email_user_mentions +from invoices.forms import (InvoiceAddressForm, InvoiceAttachmentForm, + InvoiceCommentForm, InvoiceForm) +from invoices.models import Invoice +from invoices.tasks import send_email, send_invoice_email, send_invoice_email_cancel + + +@login_required +def invoices_list(request): + + if request.user.role == 'ADMIN' or request.user.is_superuser: + users = User.objects.all() + elif request.user.google.all(): + users = User.objects.none() + elif request.user.role == 'USER': + users = User.objects.filter(role='ADMIN') + + status = ( + ('Draft', 'Draft'), + ('Sent', 'Sent'), + ('Paid', 'Paid'), + ('Pending', 'Pending'), + ('Cancel', 'Cancel'), + ) + + if request.method == 'GET': + context = {} + if request.user.role == 'ADMIN' or request.user.is_superuser: + invoices = Invoice.objects.all().distinct() + else: + invoices = Invoice.objects.filter( + Q(created_by=request.user) | Q(assigned_to=request.user)).distinct() + context['invoices'] = invoices.order_by('id') + context['status'] = status + context['users'] = users + return render(request, 'invoices_list.html', context) + + if request.method == 'POST': + context = {} + context['status'] = status + context['users'] = users + invoices = Invoice.objects.filter() + if request.user.role == 'ADMIN' or request.user.is_superuser: + invoices = invoices + else: + invoices = invoices.filter( + Q(created_by=request.user) | Q(assigned_to=request.user)).distinct() + + if request.POST.get('invoice_title_number', None): + invoices = invoices.filter( + Q(invoice_title__icontains=request.POST.get('invoice_title_number')) | + Q(invoice_number__icontains=request.POST.get('invoice_title_number'))) + + if request.POST.get('created_by', None): + invoices = invoices.filter( + created_by__id=request.POST.get('created_by')) + + if request.POST.getlist('assigned_to', None): + invoices = invoices.filter( + assigned_to__in=request.POST.getlist('assigned_to')) + context['assigned_to'] = request.POST.getlist('assigned_to') + + if request.POST.get('status', None): + invoices = invoices.filter(status=request.POST.get('status')) + + if request.POST.get('total_amount', None): + invoices = invoices.filter( + total_amount=request.POST.get('total_amount')) + + context['invoices'] = invoices.distinct().order_by('id') + return render(request, 'invoices_list.html', context) + + +@login_required +def invoices_create(request): + if request.method == 'GET': + context = {} + context["form"] = InvoiceForm(request_user=request.user) + # "prefix" use case in form, both from address and to address use the + # same model and same model form, so the name attribute in the form will + # be same for the both forms and the address will be same for both + # shipping and to address, so use prefix parameter to distinguish the + # forms, don't remove this comment + context["from_address_form"] = InvoiceAddressForm( + prefix='from') + context["to_address_form"] = InvoiceAddressForm(prefix='to') + + return render(request, 'invoices_create.html', context) + + if request.method == 'POST': + form = InvoiceForm(request.POST, request_user=request.user) + from_address_form = InvoiceAddressForm( + request.POST, prefix='from') + to_address_form = InvoiceAddressForm(request.POST, prefix='to') + if form.is_valid() and from_address_form.is_valid() and to_address_form.is_valid(): + from_address_obj = from_address_form.save() + to_address_obj = to_address_form.save() + invoice_obj = form.save(commit=False) + invoice_obj.created_by = request.user + invoice_obj.from_address = from_address_obj + invoice_obj.to_address = to_address_obj + invoice_obj.save() + form.save_m2m() + kwargs = {'domain': request.get_host(), 'protocol': request.scheme} + send_email.delay(invoice_obj.id, **kwargs) + return JsonResponse({'error': False, 'success_url': reverse('invoices:invoices_list')}) + else: + return JsonResponse({'error': True, 'errors': form.errors, + 'from_address_errors': from_address_form.errors, + 'to_address_errors': to_address_form.errors}) + + +@login_required +def invoice_details(request, invoice_id): + invoice = get_object_or_404(Invoice.objects.select_related( + 'from_address', 'to_address'), pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user or request.user in invoice.assigned_to.all()): + raise PermissionDenied + + if request.method == 'GET': + context = {} + context['invoice'] = invoice + context['attachments'] = invoice.invoice_attachment.all() + context['comments'] = invoice.invoice_comments.all() + if request.user.is_superuser or request.user.role == 'ADMIN': + context['users_mention'] = list( + User.objects.all().values('username')) + elif request.user != invoice.created_by: + context['users_mention'] = [ + {'username': invoice.created_by.username}] + else: + context['users_mention'] = list( + invoice.assigned_to.all().values('username')) + return render(request, 'invoices_detail.html', context) + + +@login_required +def invoice_edit(request, invoice_id): + invoice_obj = get_object_or_404(Invoice, pk=invoice_id) + + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice_obj.created_by == request.user or request.user in invoice_obj.assigned_to.all()): + raise PermissionDenied + + if request.method == 'GET': + context = {} + context['invoice_obj'] = invoice_obj + context['form'] = InvoiceForm( + instance=invoice_obj, request_user=request.user) + context['from_address_form'] = InvoiceAddressForm(prefix='from', + instance=invoice_obj.from_address) + context['to_address_form'] = InvoiceAddressForm(prefix='to', + instance=invoice_obj.to_address) + return render(request, 'invoices_create.html', context) + + if request.method == 'POST': + form = InvoiceForm(request.POST, instance=invoice_obj, + request_user=request.user) + from_address_form = InvoiceAddressForm( + request.POST, instance=invoice_obj.from_address, prefix='from') + to_address_form = InvoiceAddressForm( + request.POST, instance=invoice_obj.to_address, prefix='to') + if form.is_valid() and from_address_form.is_valid() and to_address_form.is_valid(): + from_address_obj = from_address_form.save() + to_address_obj = to_address_form.save() + invoice_obj = form.save(commit=False) + invoice_obj.created_by = request.user + invoice_obj.from_address = from_address_obj + invoice_obj.to_address = to_address_obj + invoice_obj.save() + form.save_m2m() + kwargs = {'domain': request.get_host(), 'protocol': request.scheme} + send_email.delay(invoice_obj.id, **kwargs) + return JsonResponse({'error': False, 'success_url': reverse('invoices:invoices_list')}) + else: + return JsonResponse({'error': True, 'errors': form.errors, + 'from_address_errors': from_address_form.errors, + 'to_address_errors': to_address_form.errors}) + + +@login_required +def invoice_delete(request, invoice_id): + invoice = get_object_or_404(Invoice, pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user): + raise PermissionDenied + + if request.method == 'GET': + invoice.delete() + return redirect('invoices:invoices_list') + + +@login_required +def invoice_send_mail(request, invoice_id): + invoice = get_object_or_404(Invoice, pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user): + raise PermissionDenied + + if request.method == 'GET': + Invoice.objects.filter(id=invoice_id).update( + is_email_sent=True, status='Sent') + kwargs = {'domain': request.get_host(), 'protocol': request.scheme} + send_invoice_email.delay(invoice_id, **kwargs) + return redirect('invoices:invoices_list') + + +@login_required +def invoice_change_status_paid(request, invoice_id): + invoice = get_object_or_404(Invoice, pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user): + raise PermissionDenied + + if request.method == 'GET': + Invoice.objects.filter(id=invoice_id).update(status='Paid') + kwargs = {'domain': request.get_host(), 'protocol': request.scheme} + # send_invoice_email.delay(invoice_id, **kwargs) + return redirect('invoices:invoices_list') + +@login_required +def invoice_change_status_cancelled(request, invoice_id): + invoice = get_object_or_404(Invoice, pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user): + raise PermissionDenied + + if request.method == 'GET': + Invoice.objects.filter(id=invoice_id).update(status='Cancelled') + kwargs = {'domain': request.get_host(), 'protocol': request.scheme} + send_invoice_email_cancel.delay(invoice_id, **kwargs) + return redirect('invoices:invoices_list') + +@login_required +def invoice_download(request, invoice_id): + invoice = get_object_or_404(Invoice.objects.select_related( + 'from_address', 'to_address'), pk=invoice_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or invoice.created_by == request.user or request.user in invoice.assigned_to.all()): + raise PermissionDenied + + if request.method == 'GET': + context = {} + context['invoice'] = invoice + html = render_to_string('invoice_download_pdf.html', context=context) + pdfkit.from_string(html, 'out.pdf', options={'encoding': "UTF-8"}) + pdf = open("out.pdf", 'rb') + response = HttpResponse(pdf.read(), content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename=Invoice.pdf' + pdf.close() + os.remove("out.pdf") + return response + + +class AddCommentView(LoginRequiredMixin, CreateView): + model = Comment + form_class = InvoiceCommentForm + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + self.object = None + self.invoice = get_object_or_404( + Invoice, id=request.POST.get('invoice_id')) + if ( + request.user == self.invoice.created_by or request.user.is_superuser or + request.user.role == 'ADMIN' + ): + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + return self.form_invalid(form) + + data = { + 'error': "You don't have permission to comment for this account."} + return JsonResponse(data) + + def form_valid(self, form): + comment = form.save(commit=False) + comment.commented_by = self.request.user + comment.invoice = self.invoice + comment.save() + comment_id = comment.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'invoices', domain=current_site.domain, + protocol=self.request.scheme) + return JsonResponse({ + "comment_id": comment.id, "comment": comment.comment, + "commented_on": comment.commented_on, + "commented_by": comment.commented_by.email + }) + + def form_invalid(self, form): + return JsonResponse({"error": form['comment'].errors}) + + +class UpdateCommentView(LoginRequiredMixin, View): + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + self.comment_obj = get_object_or_404( + Comment, id=request.POST.get("commentid")) + if request.user == self.comment_obj.commented_by: + form = InvoiceCommentForm(request.POST, instance=self.comment_obj) + if form.is_valid(): + return self.form_valid(form) + + return self.form_invalid(form) + + data = {'error': "You don't have permission to edit this comment."} + return JsonResponse(data) + + def form_valid(self, form): + self.comment_obj.comment = form.cleaned_data.get("comment") + self.comment_obj.save(update_fields=["comment"]) + comment_id = self.comment_obj.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'invoices', domain=current_site.domain, + protocol=self.request.scheme) + return JsonResponse({ + "comment_id": self.comment_obj.id, + "comment": self.comment_obj.comment, + }) + + def form_invalid(self, form): + return JsonResponse({"error": form['comment'].errors}) + + +class DeleteCommentView(LoginRequiredMixin, View): + + def post(self, request, *args, **kwargs): + self.object = get_object_or_404( + Comment, id=request.POST.get("comment_id")) + if request.user == self.object.commented_by: + self.object.delete() + data = {"cid": request.POST.get("comment_id")} + return JsonResponse(data) + + data = {'error': "You don't have permission to delete this comment."} + return JsonResponse(data) + + +class AddAttachmentView(LoginRequiredMixin, CreateView): + model = Attachments + form_class = InvoiceAttachmentForm + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + self.object = None + self.invoice = get_object_or_404( + Invoice, id=request.POST.get('invoice_id')) + if ( + request.user == self.invoice.created_by or + request.user.is_superuser or + request.user.role == 'ADMIN' + ): + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + + return self.form_invalid(form) + + data = { + 'error': "You don't have permission to add attachment \ + for this account."} + return JsonResponse(data) + + def form_valid(self, form): + attachment = form.save(commit=False) + attachment.created_by = self.request.user + attachment.file_name = attachment.attachment.name + attachment.invoice = self.invoice + attachment.save() + return JsonResponse({ + "attachment_id": attachment.id, + "attachment": attachment.file_name, + "attachment_url": attachment.attachment.url, + "download_url": reverse('common:download_attachment', + kwargs={'pk': attachment.id}), + "attachment_display": attachment.get_file_type_display(), + "created_on": attachment.created_on, + "created_by": attachment.created_by.email, + "file_type": attachment.file_type() + }) + + def form_invalid(self, form): + return JsonResponse({"error": form['attachment'].errors}) + + +class DeleteAttachmentsView(LoginRequiredMixin, View): + + def post(self, request, *args, **kwargs): + self.object = get_object_or_404( + Attachments, id=request.POST.get("attachment_id")) + if ( + request.user == self.object.created_by or + request.user.is_superuser or + request.user.role == 'ADMIN' + ): + self.object.delete() + data = {"acd": request.POST.get("attachment_id")} + return JsonResponse(data) + + data = { + 'error': "You don't have permission to delete this attachment."} + return JsonResponse(data) diff --git a/leads/forms.py b/leads/forms.py index 9ab63ef8..9465c5f9 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -13,11 +13,12 @@ class LeadForm(forms.ModelForm): if self.data.get('status') == 'converted': self.fields['account_name'].required = True self.fields['email'].required = True - self.fields['assigned_to'].queryset = assigned_users - self.fields['assigned_to'].required = False self.fields['first_name'].required = False self.fields['last_name'].required = False self.fields['title'].required = True + if assigned_users: + self.fields['assigned_to'].queryset = assigned_users + self.fields['assigned_to'].required = False for key, value in self.fields.items(): if key == 'phone': value.widget.attrs['placeholder'] =\ diff --git a/leads/migrations/0009_lead_created_from_site.py b/leads/migrations/0009_lead_created_from_site.py new file mode 100644 index 00000000..970b4f89 --- /dev/null +++ b/leads/migrations/0009_lead_created_from_site.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-05-29 06:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('leads', '0008_auto_20190315_1503'), + ] + + operations = [ + migrations.AddField( + model_name='lead', + name='created_from_site', + field=models.BooleanField(default=False), + ), + ] diff --git a/leads/models.py b/leads/models.py index 4acfd971..e7aa7822 100644 --- a/leads/models.py +++ b/leads/models.py @@ -53,12 +53,16 @@ class Lead(models.Model): enquery_type = models.CharField(max_length=255, blank=True, null=True) tags = models.ManyToManyField(Tags, blank=True) contacts = models.ManyToManyField(Contact, related_name="lead_contacts") + created_from_site = models.BooleanField(default=False) class Meta: ordering = ['-created_on'] def __str__(self): - return self.first_name + self.last_name + if self.first_name and self.last_name: + return self.first_name + self.last_name + else: + return self.title def get_complete_address(self): return return_complete_address(self) diff --git a/leads/tasks.py b/leads/tasks.py index b4d0a54a..11222ede 100644 --- a/leads/tasks.py +++ b/leads/tasks.py @@ -1,8 +1,10 @@ from celery.task import task from django.conf import settings +from django.core.mail import EmailMultiAlternatives, EmailMessage from django.db.models import Q -from django.core.mail import EmailMultiAlternatives +from django.shortcuts import reverse from django.template.loader import render_to_string + from accounts.models import User from leads.models import Lead @@ -59,3 +61,31 @@ def send_lead_assigned_emails(lead_id, new_assigned_to_list, site_address): mail_kwargs["html_content"] = html_content mail_kwargs["recipients"] = [user.email] send_email.delay(**mail_kwargs) + + + +@task +def send_email_to_assigned_user(recipients, lead_id, domain='demo.django-crm.io', protocol='http'): + """ Send Mail To Users When they are assigned to a lead """ + lead = Lead.objects.get(id=lead_id) + for user in recipients: + recipients_list = [] + user = User.objects.filter(id=user).first() + if user: + recipients_list.append(user.email) + context = {} + context["url"] = protocol + '://' + domain + \ + reverse('leads:view_lead', args=(lead.id,)) + context["user"] = user + context["lead"] = lead + subject = 'Assigned to lead.' + html_content = render_to_string( + 'assigned_to/leads_assigned.html', context=context) + + msg = EmailMessage( + subject, + html_content, + to=recipients_list + ) + msg.content_subtype = "html" + msg.send() diff --git a/leads/templates/leads.html b/leads/templates/leads.html index a0153845..53a4fdc3 100644 --- a/leads/templates/leads.html +++ b/leads/templates/leads.html @@ -11,7 +11,8 @@ @@ -26,16 +27,19 @@
    - +
    - {% for each_source in source %} - + {% endfor %}
    @@ -44,19 +48,22 @@
    - {% for each_status in status %} - + {% endfor%}
    @@ -64,10 +71,11 @@
    - + {% for tag in tags %} - + {% endfor%}
    @@ -91,10 +99,12 @@
    @@ -102,9 +112,10 @@
    - Open Leads - {% if show_pageitems %} {% show_pageitems %}{% else %} {{ open_leads|length }}{% endif %} + Open Leads - {% if show_pageitems %} {% show_pageitems %}{% else %} + {{ open_leads|length }}{% endif %} - +
    @@ -117,7 +128,7 @@ Created By Source Status - Assigned User + Assigned To Tags Created On Actions @@ -134,14 +145,16 @@ {{ forloop.counter }} - {{ lead.title }} + {{ lead.title }} {% if lead.created_by.profile_pic %} - {% thumbnail lead.created_by.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} + {% thumbnail lead.created_by.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} {% else %} - Micro profile pic + Micro profile pic {% endif %} {{ lead.source }} @@ -150,18 +163,19 @@ {% with lead_users=lead.assigned_to.all %} {% for user in lead_users %} {% if user.profile_pic %} - {% thumbnail user.profile_pic "40x40" crop="center" as im %} - - {% endthumbnail %} + {% thumbnail user.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} {% else %} - + {% endif %} {% empty %} - Not Assigned to Any + None {% endfor %} {% endwith %} - + {% with tags=lead.tags.all %} {% if tags %} {% for tag in tags %} @@ -173,10 +187,12 @@ {% endwith %} {{ lead.created_on }} - - + + {% if request.user == lead.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} - + {% endif %} @@ -197,9 +213,10 @@
    - Closed Leads - {% if show_pageitems %} {% show_pageitems %}{% else %} {{ close_leads|length }}{% endif %} + Closed Leads - {% if show_pageitems %} {% show_pageitems %}{% else %} + {{ close_leads|length }}{% endif %} - +
    @@ -228,7 +245,7 @@ {{ forloop.counter }} - {{ lead.title }} + {{ lead.title }} {{ lead.source }} {{ lead.get_status_display }} @@ -236,11 +253,11 @@ {% for user in lead_users %} {{ user.email }}
    {% empty %} - Not Assigned to Any + None {% endfor %} {% endwith %} - + {% with tags=lead.tags.all %} {% if tags %} {% for tag in tags %} @@ -252,10 +269,12 @@ {% endwith %} {{ lead.created_on }} - - + + {% if request.user == lead.created_by %} - + {% endif %} @@ -281,43 +300,44 @@ {% block js_block %} - - + + - - -{% endblock js_block %} + + +{% endblock js_block %} \ No newline at end of file diff --git a/leads/tests.py b/leads/tests.py index 2eea301e..b48cd689 100644 --- a/leads/tests.py +++ b/leads/tests.py @@ -148,11 +148,11 @@ class LeadsPostrequestTestCase(TestLeadModel, TestCase): # print(get_name.first_name+get_name.last_name,str(get_name)) self.assertEqual(get_name.first_name + get_name.last_name, str(get_name)) - response = self.client.get('/leads/list/') + response = self.client.get(reverse('leads:list')) self.assertEqual(response.status_code, 200) def test_leads_list_html(self): - response = self.client.get('/leads/list/') + response = self.client.get(reverse('leads:list')) self.assertTemplateUsed(response, 'leads.html') @@ -239,7 +239,7 @@ class LeadListTestCase(TestLeadModel, TestCase): def test_leads_list(self): self.lead = Lead.objects.all() - response = self.client.get('/leads/list/') + response = self.client.get(reverse('leads:list')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'leads.html') @@ -248,7 +248,7 @@ class LeadListTestCase(TestLeadModel, TestCase): data = {'fist_name': "marphy", 'last_name': "racheal", 'city': "hyd", 'email': "contact@gmail.com", 'status': "Assigned"} - response = self.client.post('/leads/list/', data) + response = self.client.post(reverse('leads:list'), data) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'leads.html') @@ -265,7 +265,7 @@ class LeadsRemoveTestCase(TestLeadModel, TestCase): def test_leads_remove(self): response = self.client.get('/leads/' + str(self.lead.id) + '/delete/') - self.assertEqual(response['location'], '/leads/list/') + self.assertEqual(response['location'], '/leads/') def test_leads_remove_status(self): self.client.login(username='mp@micropyramid.com', password='mp') @@ -372,7 +372,7 @@ class AttachmentTestCase(TestLeadModel, TestCase): class TestTemplates(TestLeadModel, TestCase): def test_lead_list_view(self): - resp = self.client.post('/leads/list/', {'name': 'trevor', + resp = self.client.post(reverse('leads:list'), {'name': 'trevor', 'tag': "123", 'source': "call", 'assigned_to': '1', @@ -483,7 +483,7 @@ class TestLeadListView(TestCase): self.client.login(username='g@mp.com', password='jorge2293') def test_lead_list_view(self): - response = self.client.get('/leads/list/') + response = self.client.get(reverse('leads:list')) self.assertEqual(response.status_code, 200) # response = self.client.post('/leads/create/',{}) diff --git a/leads/urls.py b/leads/urls.py index 321b84a2..347c1f76 100644 --- a/leads/urls.py +++ b/leads/urls.py @@ -11,7 +11,7 @@ app_name = 'leads' urlpatterns = [ - path('list/', LeadListView.as_view(), name='list'), + path('', LeadListView.as_view(), name='list'), path('create/', create_lead, name='add_lead'), # create_lead_from_site path('create/from-site/', create_lead_from_site, diff --git a/leads/views.py b/leads/views.py index 30105775..1988f41f 100644 --- a/leads/views.py +++ b/leads/views.py @@ -22,7 +22,8 @@ from planner.models import Event, Reminder from planner.forms import ReminderForm from leads.tasks import send_lead_assigned_emails from django.core.exceptions import PermissionDenied - +from common.tasks import send_email_user_mentions +from leads.tasks import send_email_to_assigned_user class LeadListView(LoginRequiredMixin, TemplateView): model = Lead @@ -52,13 +53,13 @@ class LeadListView(LoginRequiredMixin, TemplateView): if request_post.get('status'): queryset = queryset.filter(status=request_post.get('status')) if request_post.get('tag'): - queryset = queryset.filter(tags__in=request_post.get('tag')) + queryset = queryset.filter(tags__in=request_post.getlist('tag')) if request_post.get('source'): queryset = queryset.filter(source=request_post.get('source')) if request_post.getlist('assigned_to'): queryset = queryset.filter( assigned_to__id__in=request_post.getlist('assigned_to')) - return queryset + return queryset.distinct() def get_context_data(self, **kwargs): context = super(LeadListView, self).get_context_data(**kwargs) @@ -74,6 +75,7 @@ class LeadListView(LoginRequiredMixin, TemplateView): is_active=True).order_by('email') context["assignedto_list"] = [ int(i) for i in self.request.POST.getlist('assigned_to', []) if i] + context["request_tags"] = self.request.POST.getlist('tag') search = True if ( self.request.POST.get('name') or self.request.POST.get('city') or @@ -102,7 +104,13 @@ class LeadListView(LoginRequiredMixin, TemplateView): def create_lead(request): template_name = "create_lead.html" - users = User.objects.filter(is_active=True).order_by('email') + users = [] + if request.user.role == 'ADMIN' or request.user.is_superuser: + users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + users = [] + else: + users = User.objects.filter(role='ADMIN').order_by('email') form = LeadForm(assigned_to=users) if request.POST: @@ -125,20 +133,23 @@ def create_lead(request): lead_obj.assigned_to.add(*request.POST.getlist('assigned_to')) assigned_to_list = request.POST.getlist('assigned_to') current_site = get_current_site(request) - for assigned_to_user in assigned_to_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to lead.' - message = render_to_string( - 'assigned_to/leads_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'lead': lead_obj - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + recipients = assigned_to_list + send_email_to_assigned_user.delay(recipients, lead_obj.id, domain=current_site.domain, + protocol=request.scheme) + # for assigned_to_user in assigned_to_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to lead.' + # message = render_to_string( + # 'assigned_to/leads_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'lead': lead_obj + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() if request.FILES.get('lead_attachment'): attachment = Attachments() @@ -169,20 +180,23 @@ def create_lead(request): # account_object.assigned_to.add(*request.POST.getlist('assigned_to')) assigned_to_list = request.POST.getlist('assigned_to') current_site = get_current_site(request) - for assigned_to_user in assigned_to_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to account.' - message = render_to_string( - 'assigned_to/account_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'account': account_object - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + recipients = assigned_to_list + send_email_to_assigned_user.delay(recipients, lead_obj.id, domain=current_site.domain, + protocol=request.scheme) + # for assigned_to_user in assigned_to_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to account.' + # message = render_to_string( + # 'assigned_to/account_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'account': account_object + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() account_object.save() success_url = reverse('leads:list') @@ -242,11 +256,19 @@ class LeadDetailView(LoginRequiredMixin, DetailView): assigned_dict['name'] = each.email assigned_data.append(assigned_dict) + if self.request.user.is_superuser or self.request.user.role == 'ADMIN': + users_mention = list(User.objects.all().values('username')) + elif self.request.user != context['object'].created_by: + users_mention = [{'username': context['object'].created_by.username}] + else: + users_mention = list(context['object'].assigned_to.all().values('username')) + context.update({ "attachments": attachments, "comments": comments, "status": LEAD_STATUS, "countries": COUNTRIES, "reminder_form_set": reminder_form_set, "meetings": meetings, "calls": calls, + "users_mention": users_mention, "assigned_data": json.dumps(assigned_data)}) return context @@ -254,7 +276,13 @@ class LeadDetailView(LoginRequiredMixin, DetailView): def update_lead(request, pk): lead_record = Lead.objects.filter(pk=pk).first() template_name = "create_lead.html" - users = User.objects.filter(is_active=True).order_by('email') + users = [] + if request.user.role == 'ADMIN' or request.user.is_superuser: + users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + users = [] + else: + users = User.objects.filter(role='ADMIN').order_by('email') status = request.GET.get('status', None) initial = {} if status and status == "converted": @@ -303,21 +331,25 @@ def update_lead(request, pk): all_members_list = list( set(list(assigned_form_users)) - set(list(assigned_to_ids))) - if all_members_list: - for assigned_to_user in all_members_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to lead.' - message = render_to_string( - 'assigned_to/leads_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'lead': lead_obj - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + current_site = get_current_site(request) + recipients = all_members_list + send_email_to_assigned_user.delay(recipients, lead_obj.id, domain=current_site.domain, + protocol=request.scheme) + # if all_members_list: + # for assigned_to_user in all_members_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to lead.' + # message = render_to_string( + # 'assigned_to/leads_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'lead': lead_obj + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() lead_obj.assigned_to.clear() lead_obj.assigned_to.add(*request.POST.getlist('assigned_to')) @@ -353,21 +385,28 @@ def update_lead(request, pk): # account_object.assigned_to.add(*request.POST.getlist('assigned_to')) assigned_to_list = request.POST.getlist('assigned_to') current_site = get_current_site(request) - for assigned_to_user in assigned_to_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to account.' - message = render_to_string( - 'assigned_to/account_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'account': account_object - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + recipients = assigned_to_list + send_email_to_assigned_user.delay(recipients, lead_obj.id, domain=current_site.domain, + protocol=request.scheme) + # current_site = get_current_site(request) + # for assigned_to_user in assigned_to_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to account.' + # message = render_to_string( + # 'assigned_to/account_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'account': account_object + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() + for comment in lead_obj.leads_comments.all(): + comment.account = account_object + comment.save() account_object.save() status = request.GET.get('status', None) success_url = reverse('leads:list') @@ -435,7 +474,7 @@ def convert_lead(request, pk): ) contacts_list = lead_obj.contacts.all().values_list('id', flat=True) account_object.contacts.add(*contacts_list) - account_object.save() + account_obj = account_object.save() current_site = get_current_site(request) for assigned_to_user in lead_obj.assigned_to.all().values_list( 'id', flat=True): @@ -485,6 +524,10 @@ class AddCommentView(LoginRequiredMixin, CreateView): comment.commented_by = self.request.user comment.lead = self.lead comment.save() + comment_id = comment.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'leads', domain=current_site.domain, + protocol=self.request.scheme) return JsonResponse({ "comment_id": comment.id, "comment": comment.comment, "commented_on": comment.commented_on, @@ -514,6 +557,10 @@ class UpdateCommentView(LoginRequiredMixin, View): def form_valid(self, form): self.comment_obj.comment = form.cleaned_data.get("comment") self.comment_obj.save(update_fields=["comment"]) + comment_id = self.comment_obj.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'leads', domain=current_site.domain, + protocol=self.request.scheme) return JsonResponse({ "commentid": self.comment_obj.id, "comment": self.comment_obj.comment, @@ -579,7 +626,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): "created_by": attachment.created_by.email, "download_url": reverse( 'common:download_attachment', kwargs={'pk': attachment.id}), - "attachment_display": attachment.get_file_type_display() + "attachment_display": attachment.get_file_type_display(), + "file_type": attachment.file_type() }) def form_invalid(self, form): diff --git a/marketing/forms.py b/marketing/forms.py index bc9e0762..5b252ede 100644 --- a/marketing/forms.py +++ b/marketing/forms.py @@ -1,4 +1,5 @@ import csv +import datetime import re import json import openpyxl @@ -108,7 +109,8 @@ def xls_doc_validate(document): validated_rows = [] invalid_rows = [] for sheet_name in sheets: - validated_rows, invalid_rows = get_validated_rows(wb, sheet_name, validated_rows, invalid_rows) + validated_rows, invalid_rows = get_validated_rows( + wb, sheet_name, validated_rows, invalid_rows) return {"error": False, "validated_rows": validated_rows, "invalid_rows": invalid_rows} @@ -118,11 +120,11 @@ def import_document_validator(document): document.seek(0, 0) return csv_doc_validate(document) except Exception as e: - print (e) + print(e) try: return xls_doc_validate(document) except Exception as e: - print (e) + print(e) return {"error": True, "message": "Not a valid CSV/XLS file"} @@ -139,8 +141,9 @@ class ContactListForm(forms.ModelForm): self.fields['contacts_file'].widget.attrs.update({ "accept": ".csv,.xls,.xlsx,.xlsm,.xlsb,.xml", }) - if self.instance is None: - self.fields['contacts_file'].required = True + self.fields['contacts_file'].required = True + # if self.instance is None: + # self.fields['contacts_file'].required = True if self.data.get('contacts_file'): self.fields['contacts_file'].widget.attrs.update({ "accept": ".csv,.xls,.xlsx,.xlsm,.xlsb,.xml", @@ -155,31 +158,10 @@ class ContactListForm(forms.ModelForm): else: self.validated_rows = data.get("validated_rows", []) self.invalid_rows = data.get("invalid_rows", []) + if self.invalid_rows: + raise forms.ValidationError('Uploaded file is not valid') return document - def clean_tags(self): - tags_data = self.data['tags'].split(",") if self.data['tags'] else [] - if len(tags_data) > 0: - instance_tags = [] - if self.instance and self.instance.id is not None: - instance_tags = list( - set(self.instance.tags.all().values_list( - 'name', flat=True))) - for each in tags_data: - tags = Tag.objects.filter(name__iexact=each) - if instance_tags: - tags = tags.exclude(name__in=instance_tags) - if tags: - raise forms.ValidationError( - str(each) + ' Tag aleady existed with this name') - if bool(re.search(r"[~\!_.@#\$%\^&\*\(\)\+{}\":;'/\[\]]", - each)): - raise forms.ValidationError( - "Tags Should not contain special \ - charcters except hyphens") - return self.data['tags'] - raise forms.ValidationError("Enter Any tags") - def clean_visible_to(self): visible_to_data = json.loads(self.data['visible_to']) if len(visible_to_data) > 0: @@ -203,26 +185,45 @@ class ContactListForm(forms.ModelForm): class ContactForm(forms.ModelForm): contact_list = forms.CharField(max_length=5000) + def __init__(self, *args, **kwargs): + request_user = kwargs.pop('request_user', None) + self.obj_instance = kwargs.get('instance', None) + super(ContactForm, self).__init__(*args, **kwargs) + for field in self.fields.values(): + field.widget.attrs = {"class": "form-control"} + + self.fields['name'].required = True + self.fields['last_name'].required = True + self.fields['city'].required = True + self.fields['state'].required = True + self.fields['email'].required = True + self.fields['contact_list'].required = False + class Meta: model = Contact - fields = ["name", "email", "contact_number"] + fields = ["name", "email", "contact_number", + "last_name", "city", "state"] - def __init__(self, *args, **kwargs): - super(ContactForm, self).__init__(*args, **kwargs) - - def clean_contact_list(self): - contact_list = self.cleaned_data.get("contact_list") - if not contact_list or contact_list == '[]' or\ - json.loads(contact_list) == []: + def clean_email(self): + email = self.cleaned_data.get('email') + if Contact.objects.filter(email=email).exclude(id=self.instance.id).exists(): raise forms.ValidationError( - "Please choose any of the Contact List") - else: - for each in json.loads(contact_list): - if not ContactList.objects.filter(id=each).first(): - raise forms.ValidationError( - "Please choose a valid Contact List") + 'Contact with this Email already exists') + return email - return contact_list + # def clean_contact_list(self): + # contact_list = self.cleaned_data.get("contact_list") + # if not contact_list or contact_list == '[]' or\ + # json.loads(contact_list) == []: + # raise forms.ValidationError( + # "Please choose any of the Contact List") + # else: + # for each in json.loads(contact_list): + # if not ContactList.objects.filter(id=each).first(): + # raise forms.ValidationError( + # "Please choose a valid Contact List") + + # return contact_list class ContactsCSVUploadForm(forms.Form): @@ -234,6 +235,7 @@ class ContactsCSVUploadForm(forms.Form): self.fields['contacts_file'].widget.attrs.update({ "accept": ".csv,.xls,.xlsx,.xlsm,.xlsb,.xml", }) + self.fields['contacts_file'].required = True def clean_contacts_file(self): document = self.cleaned_data.get("contacts_file") @@ -275,12 +277,14 @@ class SendCampaignForm(forms.ModelForm): from_email = forms.EmailField(max_length=100, required=True) from_name = forms.CharField(max_length=100, required=True) contact_list = forms.CharField(max_length=5000, required=True) + tags = forms.CharField(max_length=5000, required=False) class Meta: model = Campaign - fields = ['title', 'subject', 'html', 'email_template', 'attachment'] + fields = ['title', 'subject', 'html', 'email_template', 'attachment', ] def __init__(self, *args, **kwargs): + self.contacts_list = kwargs.pop('contacts_list') super(SendCampaignForm, self).__init__(*args, **kwargs) if self.data.get('schedule_later') and self.data['schedule_later'] == 'true': self.fields['timezone'].required = True @@ -288,16 +292,31 @@ class SendCampaignForm(forms.ModelForm): if not self.data.get('reply_to_crm'): self.fields['reply_to_email'].required = True + def clean_schedule_date_time(self): + schedule_date_time = self.cleaned_data.get('schedule_date_time') + if self.cleaned_data.get('schedule_later') == 'true': + schedule_date_time = datetime.datetime.strptime( + schedule_date_time, '%Y-%m-%d %H:%M') + if schedule_date_time < datetime.datetime.now(): + raise forms.ValidationError( + 'Schedule Date Time should be greater than current time') + return schedule_date_time + def clean_contact_list(self): - contact_list = self.cleaned_data.get("contact_list") + contact_list = self.contacts_list if not contact_list or contact_list == '[]' or \ contact_list == []: raise forms.ValidationError( "Please choose any of the Contact List") else: for each in contact_list: - if not ContactList.objects.filter(id=each).first(): + contacts_list_obj = ContactList.objects.filter(id=each).first() + if not contacts_list_obj: raise forms.ValidationError( "Please choose a valid Contact List") - return contact_list + if contacts_list_obj.contacts.count() < 1: + raise forms.ValidationError( + 'The contact list "{}" does not have any contacts in it .'.format(contacts_list_obj.name)) + + return contact_list \ No newline at end of file diff --git a/marketing/migrations/0007_auto_20190611_1226.py b/marketing/migrations/0007_auto_20190611_1226.py new file mode 100644 index 00000000..871436d1 --- /dev/null +++ b/marketing/migrations/0007_auto_20190611_1226.py @@ -0,0 +1,27 @@ +# Generated by Django 2.1.7 on 2019-06-11 06:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0006_campaign_attachment'), + ] + + operations = [ + migrations.CreateModel( + name='CampaignCompleted', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_completed', models.BooleanField(default=False)), + ('campaign', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='campaign_is_completed', to='marketing.Campaign')), + ], + ), + migrations.AlterField( + model_name='campaignopen', + name='contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contact_campaign', to='marketing.Contact'), + ), + ] diff --git a/marketing/migrations/0008_auto_20190612_1905.py b/marketing/migrations/0008_auto_20190612_1905.py new file mode 100644 index 00000000..da0c38a0 --- /dev/null +++ b/marketing/migrations/0008_auto_20190612_1905.py @@ -0,0 +1,45 @@ +# Generated by Django 2.1.7 on 2019-06-12 13:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0007_auto_20190611_1226'), + ] + + operations = [ + migrations.AlterModelOptions( + name='campaign', + options={'ordering': ('created_on',)}, + ), + migrations.AlterModelOptions( + name='contact', + options={'ordering': ['id']}, + ), + migrations.AlterModelOptions( + name='contactlist', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='emailtemplate', + options={'ordering': ['id']}, + ), + migrations.AddField( + model_name='campaign', + name='tags', + field=models.ManyToManyField(to='marketing.Tag'), + ), + migrations.AlterField( + model_name='campaignopen', + name='campaign', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaign_open', to='marketing.Campaign'), + ), + migrations.AlterField( + model_name='campaignopen', + name='contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contact_campaign_open', to='marketing.Contact'), + ), + ] diff --git a/marketing/models.py b/marketing/models.py index 87ed96eb..b63ec2b4 100644 --- a/marketing/models.py +++ b/marketing/models.py @@ -35,6 +35,9 @@ class EmailTemplate(models.Model): subject = models.CharField(max_length=5000) html = models.TextField() + class Meta: + ordering = ['id', ] + @property def created_by_user(self): return self.created_by if self.created_by else None @@ -52,6 +55,9 @@ class ContactList(models.Model): visible_to = models.ManyToManyField( User, related_name="contact_lists_visible_to") + class Meta: + ordering = ('id',) + @property def created_by_user(self): return self.created_by if self.created_by else None @@ -126,6 +132,9 @@ class Contact(models.Model): def __str__(self): return self.email + class Meta: + ordering = ['id', ] + class FailedContact(models.Model): phone_regex = RegexValidator( @@ -191,11 +200,15 @@ class Campaign(models.Model): opens = models.IntegerField(default='0', blank=True) opens_unique = models.IntegerField(default='0', blank=True) bounced = models.IntegerField(default='0') + tags = models.ManyToManyField(Tag) status = models.CharField( default="Preparing", choices=STATUS_CHOICES, max_length=20) attachment = models.FileField( max_length=1000, upload_to=get_campaign_attachment_path, blank=True, null=True) + class Meta: + ordering = ('-created_on', ) + @property def no_of_unsubscribers(self): unsubscribers = self.campaign_contacts.filter( @@ -225,12 +238,28 @@ class Campaign(models.Model): @property def sent_on_format(self): if self.schedule_date_time: - c_schedule_date_time = convert_to_custom_timezone(self.schedule_date_time, self.timezone) + c_schedule_date_time = convert_to_custom_timezone( + self.schedule_date_time, self.timezone) return c_schedule_date_time.strftime('%b %d, %Y %I:%M %p') else: - c_created_on = convert_to_custom_timezone(self.created_on, self.timezone) + c_created_on = convert_to_custom_timezone( + self.created_on, self.timezone) return c_created_on.strftime('%b %d, %Y %I:%M %p') + @property + def get_all_emails_count(self): + return self.contact_lists.exclude(contacts__email=None).values_list('contacts__email').count() + + @property + def get_all_email_bounces_count(self): + return self.contact_lists.filter(contacts__is_bounced=True + ).exclude(contacts__email=None).values_list('contacts__email').count() + + @property + def get_all_emails_unsubscribed_count(self): + return self.contact_lists.filter(contacts__is_unsubscribed=True + ).exclude(contacts__email=None).values_list('contacts__email').count() + @receiver(models.signals.pre_delete, sender=Campaign) def comment_attachments_delete(sender, instance, **kwargs): @@ -274,9 +303,19 @@ class CampaignLinkClick(models.Model): class CampaignOpen(models.Model): - campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) + campaign = models.ForeignKey( + Campaign, on_delete=models.CASCADE, related_name='campaign_open') ip_address = models.GenericIPAddressField() created_on = models.DateTimeField(auto_now_add=True) user_agent = models.CharField(max_length=2000, blank=True, null=True) contact = models.ForeignKey( - Contact, blank=True, null=True, on_delete=models.CASCADE) + Contact, blank=True, null=True, on_delete=models.CASCADE, related_name='contact_campaign_open') + + +class CampaignCompleted(models.Model): + """ This Model Is Used To Check If The Scheduled Later Emails Have Been Sent + related name : campaign_is_completed + """ + campaign = models.OneToOneField( + Campaign, on_delete=models.CASCADE, related_name='campaign_is_completed') + is_completed = models.BooleanField(default=False) diff --git a/marketing/tasks.py b/marketing/tasks.py index 7341e1ab..6b89ccd4 100644 --- a/marketing/tasks.py +++ b/marketing/tasks.py @@ -1,14 +1,18 @@ import datetime import hashlib +from mimetypes import MimeTypes + import pytz import requests -from mimetypes import MimeTypes from celery.task import task from django.conf import settings from django.core.mail import EmailMessage -from django.template import Template, Context +from django.shortcuts import reverse +from django.template import Context, Template + from common.utils import convert_to_custom_timezone -from marketing.models import Contact, FailedContact, ContactList, Campaign, CampaignLog +from marketing.models import (Campaign, CampaignLog, Contact, ContactList, + FailedContact, CampaignCompleted) @task @@ -44,7 +48,8 @@ def upload_csv_file(data, invalid_data, user, contact_lists): contact.state = each['state'] contact.save() for contact_list in contact_lists: - contact.contact_list.add(ContactList.objects.get(id=int(contact_list))) + contact.contact_list.add( + ContactList.objects.get(id=int(contact_list))) for each in invalid_data: contact = FailedContact.objects.filter(email=each['email']).first() @@ -62,7 +67,8 @@ def upload_csv_file(data, invalid_data, user, contact_lists): contact.state = each['state'] contact.save() for contact_list in contact_lists: - contact.contact_list.add(ContactList.objects.get(id=int(contact_list))) + contact.contact_list.add( + ContactList.objects.get(id=int(contact_list))) def send_campaign_mail(subject, content, from_email, to_email, bcc, reply_to, attachments): @@ -78,7 +84,7 @@ def send_campaign_mail(subject, content, from_email, to_email, bcc, reply_to, at msg.attach(*attachment) msg.content_subtype = "html" res = msg.send() - print (res) + print(res) def get_campaign_message_id(campaign): @@ -92,7 +98,7 @@ def get_campaign_message_id(campaign): @task -def run_campaign(campaign): +def run_campaign(campaign, domain='demo.django-crm.io', protocol='https'): try: campaign = Campaign.objects.get(id=campaign) attachments = [] @@ -123,26 +129,42 @@ def run_campaign(campaign): from_email = campaign.from_email else: from_email = campaign.created_by.email - reply_to_email = str(from_email) + ' <' + str(message_id + '@' + domain_name + '') + '>' + reply_to_email = str(from_email) + ' <' + \ + str(message_id + '@' + domain_name + '') + '>' if not (each_contact.is_bounced or each_contact.is_unsubscribed): - domain_url = settings.URL_FOR_LINKS - link = '' + # domain_url = settings.URL_FOR_LINKS + domain_url = protocol + '://' + domain + img_src_url = domain_url + reverse('marketing:campaign_open', kwargs={ + 'campaign_log_id': campaign_log.id, 'email_id': each_contact.id}) + # images can only be accessed over https + link = 'company_logo'.format( + img_src_url=img_src_url) + # link = 'company_logo' + + unsubscribe_from_campaign_url = reverse( + 'marketing:unsubscribe_from_campaign', kwargs={'contact_id': each_contact.id, }) + unsubscribe_from_campaign_html = "

    Unsubscribe".format( + domain_url + unsubscribe_from_campaign_url) names_dict = {'company_name': each_contact.company_name if each_contact.company_name else '', 'last_name': each_contact.last_name if each_contact.last_name else '', 'city': each_contact.city if each_contact.city else '', 'state': each_contact.state if each_contact.state else '', 'first_name': each_contact.name, - 'email': each_contact.email, 'email_id': each_contact.id} + 'email': each_contact.email, 'email_id': each_contact.id, + 'unsubscribe_from_campaign_url': unsubscribe_from_campaign_url} html = Template(html).render(Context(names_dict)) - mail_html = html + link - from_email = str(campaign.from_name) + "<" + str(campaign.from_email) + '>' + mail_html = html + link + unsubscribe_from_campaign_html + from_email = str(campaign.from_name) + "<" + \ + str(campaign.from_email) + '>' to_email = [each_contact.email] send_campaign_mail( subject, mail_html, from_email, to_email, [], [reply_to_email], attachments) except Exception as e: - print (e) + print(e) pass @@ -159,18 +181,20 @@ def list_all_bounces_unsubscribes(): bounces = requests.get('https://api.sendgrid.com/api/bounces.get.json?api_user=' + settings.EMAIL_HOST_USER + '&api_key=' + settings.EMAIL_HOST_PASSWORD) for each in bounces.json(): - contact = Contact.objects.filter(email=each['email']).first() - if contact: - contact.is_bounced = True - contact.save() + if type(each) == dict: + contact = Contact.objects.filter(email=each.get('email')).first() + if contact: + contact.is_bounced = True + contact.save() bounces = requests.get('https://api.sendgrid.com/api/unsubscribes.get.json?api_user=' + settings.EMAIL_HOST_USER + '&api_key=' + settings.EMAIL_HOST_PASSWORD) for each in bounces.json(): - contact = Contact.objects.filter(email=each['email']).first() - if contact: - contact.is_unsubscribed = True - contact.save() + if type(each) == dict: + contact = Contact.objects.filter(email=each.get('email')).first() + if contact: + contact.is_unsubscribed = True + contact.save() @task @@ -178,16 +202,23 @@ def send_scheduled_campaigns(): from datetime import datetime campaigns = Campaign.objects.filter(schedule_date_time__isnull=False) for each in campaigns: - schedule_date_time = each.schedule_date_time + completed = CampaignCompleted.objects.filter( + is_completed=True).values_list('campaign_id', flat=True) - sent_time = datetime.now().strftime('%Y-%m-%d %H:%M') - sent_time = datetime.strptime(sent_time, '%Y-%m-%d %H:%M') - local_tz = pytz.timezone(settings.TIME_ZONE) - sent_time = local_tz.localize(sent_time) - sent_time = convert_to_custom_timezone(sent_time, each.timezone, to_utc=True) + if each.id not in completed: + schedule_date_time = each.schedule_date_time - if ( - str(each.schedule_date_time.date()) == str(sent_time.date()) and - str(schedule_date_time.hour) == str(sent_time.hour) - ): - run_campaign(each) + sent_time = datetime.now().strftime('%Y-%m-%d %H:%M') + sent_time = datetime.strptime(sent_time, '%Y-%m-%d %H:%M') + local_tz = pytz.timezone(settings.TIME_ZONE) + sent_time = local_tz.localize(sent_time) + sent_time = convert_to_custom_timezone( + sent_time, each.timezone, to_utc=True) + + if ( + str(each.schedule_date_time.date()) == str(sent_time.date()) and + str(schedule_date_time.hour) == str(sent_time.hour) + ): + run_campaign.delay(each.id) + CampaignCompleted.objects.create( + campaign=each, is_completed=True) diff --git a/marketing/templates/contact_detail.html b/marketing/templates/contact_detail.html new file mode 100644 index 00000000..7db02519 --- /dev/null +++ b/marketing/templates/contact_detail.html @@ -0,0 +1,114 @@ +{% extends 'marketing/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% block content %} +{{ block.super }} +
    +
    +
    +
    +
    +
    +
    +
    +
    + Contact Detail +
    +
    +
    +
    +
    + +
    + {{contact_obj.email}} +
    +
    +
    +
    + +
    + {{contact_obj.name}} +
    +
    +
    +
    + +
    + {{contact_obj.last_name}} +
    +
    +
    +
    + +
    + {{contact_obj.city}} +
    +
    +
    +
    + +
    + {{contact_obj.state}} +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block js_block %} + + + + + +{% endblock js_block %} \ No newline at end of file diff --git a/marketing/templates/html_message_campaign.html b/marketing/templates/html_message_campaign.html new file mode 100644 index 00000000..ab89d7eb --- /dev/null +++ b/marketing/templates/html_message_campaign.html @@ -0,0 +1,14 @@ +{% block body %} + +{% block content %} +{{html|safe}} + +{% endblock content %} + + +{% block unsubscribe_link %} + Unsubscribe +{% endblock unsubscribe_link %} + + +{% endblock body %} \ No newline at end of file diff --git a/marketing/tests.py b/marketing/tests.py index d1e3edeb..af74bbd4 100644 --- a/marketing/tests.py +++ b/marketing/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase from django.test import Client from common.models import User +from django.shortcuts import reverse import os # from marketing.models import Tag, Document, ContactList, EmailTemplate, Contact, Campaign, CampaignLinkClick, CampaignLog, Link, CampaignOpen # from .models import * @@ -22,51 +23,30 @@ class TestMarketingModel(object): class TestTemplates(TestMarketingModel, TestCase): def test_templates(self): - url1 = '/m/' - url2 = '/m/cl/all/' - url3 = '/m/cl/edit_contact/' - url4 = '/m/cl/lists/' - url5 = '/m/cl/list/new/' - url6 = '/m/cl/list/cnew/' - # url7 = '/m/cl/list/detail/' - url8 = '/m/et/list/' - url9 = '/m/et/new/' - # url10 = '/m/et/edit/' - # url11 = '/m/et/detail/' - url12 = '/m/cm/list/' - url13 = '/m/cm/new/' - # url14 = '/m/cm/edit/' - # url15 = '/m/cm/details/' + url1 = reverse('marketing:dashboard') + url2 = reverse('marketing:contact_lists') + url4 = reverse('marketing:contacts_list') + url5 = reverse('marketing:contact_list_new') + url8 = reverse('marketing:email_template_list') + url9 = reverse('marketing:email_template_new') + url12 = reverse('marketing:campaign_list') + url13 = reverse('marketing:campaign_new') resp1 = self.client.get(url1) resp2 = self.client.get(url2) - resp3 = self.client.get(url3) resp4 = self.client.get(url4) resp5 = self.client.get(url5) - resp6 = self.client.get(url6) - # resp7 = self.client.get(url7) resp8 = self.client.get(url8) resp9 = self.client.get(url9) - # resp10 = self.client.get(url10) - # resp11 = self.client.get(url11) resp12 = self.client.get(url12) resp13 = self.client.get(url13) - # resp14 = self.client.get(url14) - # resp15 = self.client.get(url15) self.assertEqual(resp1.status_code, 200) self.assertEqual(resp2.status_code, 200) - self.assertEqual(resp3.status_code, 200) self.assertEqual(resp4.status_code, 200) self.assertEqual(resp5.status_code, 200) - self.assertEqual(resp6.status_code, 200) - # self.assertEqual(resp7.status_code, 200) self.assertEqual(resp8.status_code, 200) self.assertEqual(resp9.status_code, 200) - # self.assertEqual(resp10.status_code, 200) - # self.assertEqual(resp11.status_code, 200) self.assertEqual(resp12.status_code, 200) self.assertEqual(resp13.status_code, 200) - # self.assertEqual(resp14.status_code, 200) - # self.assertEqual(resp15.status_code, 200) @@ -84,7 +64,7 @@ class TestCreateContacts(TestMarketingModel, TestCase): fp.writelines(data) with open('marketing/test_file.csv') as fp: - response = self.client.post('/m/cl/list/cnew/', + response = self.client.post(reverse('marketing:contact_list_new'), {'name': 'sample_test', 'attachment': fp}) self.assertEqual(response.status_code, 200) @@ -99,13 +79,13 @@ class TestViewContactList(TestMarketingModel, TestCase): # self.assertTemplateUsed(response, 'marketing/lists/detail.html') def test_contact_list_pagination(self): - response = self.client.get('/m/cl/all/?page=1') + response = self.client.get(reverse('marketing:contact_lists') + '?page=1') self.assertEqual(response.status_code, 200) - response = self.client.get('/m/cl/all/?page=asdf') + response = self.client.get(reverse('marketing:contact_lists') + '?page=asdf') self.assertEqual(response.status_code, 200) - response = self.client.get('/m/cl/all/?page=') + response = self.client.get(reverse('marketing:contact_lists') + '?page=') self.assertEqual(response.status_code, 200) @@ -113,13 +93,13 @@ class TestEmailTemplateList(TestMarketingModel, TestCase): def test_email_template_list_pagination(self): - response = self.client.get('/m/et/list/?page=1') + response = self.client.get(reverse('marketing:email_template_list') + '?page=1') self.assertEqual(response.status_code, 200) - response = self.client.get('/m/et/list/?page=asdf') + response = self.client.get(reverse('marketing:email_template_list') + '?page=asdf') self.assertEqual(response.status_code, 200) - response = self.client.get('/m/et/list/?page=') + response = self.client.get(reverse('marketing:email_template_list') + '?page=') self.assertEqual(response.status_code, 200) diff --git a/marketing/urls.py b/marketing/urls.py index 8da6f9e0..9679fa7a 100644 --- a/marketing/urls.py +++ b/marketing/urls.py @@ -4,7 +4,8 @@ from .views import ( email_template_list, email_template_new, email_template_edit, email_template_delete, email_template_detail, campaign_list, campaign_new, campaign_edit, campaign_details, campaign_delete, edit_contact_list, delete_contact_list, failed_contact_list_detail, failed_contact_list_download_delete, - campaign_link_click, campaign_open + campaign_link_click, campaign_open, demo_file_download, delete_contact, unsubscribe_from_campaign, contact_detail, + edit_failed_contact, delete_failed_contact, download_contacts_for_campaign, create_campaign_from_template ) app_name = 'marketing' @@ -12,29 +13,37 @@ app_name = 'marketing' urlpatterns = [ path('', dashboard, name='dashboard'), - path('cl/all/', contact_lists, name='contact_lists'), - path('cl/edit_contact/', edit_contact, name='edit_contact'), - path('cl/lists/', contacts_list, name='contacts_list'), - path('cl/list/new/', contact_list_new, name='contact_list_new'), + path('contact-list/', contact_lists, name='contact_lists'), + path('contacts//edit/', edit_contact, name='edit_contact'), + path('contacts//delete/', delete_contact, name='delete_contact'), + path('contacts/', contacts_list, name='contacts_list'), + path('contact-list/create/', contact_list_new, name='contact_list_new'), path('cl/list/cnew/', contacts_list_new, name='contacts_list_new'), - path('cl/list//detail/', contact_list_detail, name='contact_list_detail'), - path('cl/list//failed/', failed_contact_list_detail, name='failed_contact_list_detail'), - path('cl/list//failed/download/', + path('contact-list//detail/', contact_list_detail, name='contact_list_detail'), + path('contact-list//failed/', failed_contact_list_detail, name='failed_contact_list_detail'), + path('contact-list//failed/edit/', edit_failed_contact, name='edit_failed_contact'), + path('contact-list//failed/delete/', delete_failed_contact, name='delete_failed_contact'), + path('contact-list//failed/download/', failed_contact_list_download_delete, name='failed_contact_list_download_delete'), - path('cl/list//edit/', edit_contact_list, name='edit_contact_list'), + path('contact-list//edit/', edit_contact_list, name='edit_contact_list'), path('cl/list//delete/', delete_contact_list, name='delete_contact_list'), - path('et/list/', email_template_list, name='email_template_list'), - path('et/new/', email_template_new, name='email_template_new'), - path('et//edit/', email_template_edit, name='email_template_edit'), - path('et//detail/', email_template_detail, name='email_template_detail'), - path('et//delete/', email_template_delete, name='email_template_delete'), + path('email-templates/', email_template_list, name='email_template_list'), + path('email-templates/create/', email_template_new, name='email_template_new'), + path('email-templates//edit/', email_template_edit, name='email_template_edit'), + path('email-templates//detail/', email_template_detail, name='email_template_detail'), + path('email-templates//delete/', email_template_delete, name='email_template_delete'), - path('cm/list/', campaign_list, name='campaign_list'), - path('cm/new/', campaign_new, name='campaign_new'), + path('campaigns/', campaign_list, name='campaign_list'), + path('campaigns/create/', campaign_new, name='campaign_new'), path('cm//edit/', campaign_edit, name='campaign_edit'), - path('cm//details/', campaign_details, name='campaign_details'), - path('cm//delete/', campaign_delete, name='campaign_delete'), + path('campaigns//details/', campaign_details, name='campaign_details'), + path('campaigns//delete/', campaign_delete, name='campaign_delete'), path('cm/link//e//', campaign_link_click, name='campaign_link_click'), path('cm/track-email//contact//', campaign_open, name='campaign_open'), + path('demo-file-download-for-contacts-list/', demo_file_download, name='demo_file_download'), + path('unsubscribe-from-campaign//', unsubscribe_from_campaign, name="unsubscribe_from_campaign"), + path('contacts//view/', contact_detail, name="contact_detail"), + path('download-contacts-for-campaign//', download_contacts_for_campaign, name="download_contacts_for_campaign"), + path('create-campaign-from-template//', create_campaign_from_template, name="create_campaign_from_template"), ] diff --git a/marketing/views.py b/marketing/views.py index 494fc906..17ed3bbe 100644 --- a/marketing/views.py +++ b/marketing/views.py @@ -1,27 +1,30 @@ import csv import json -import pytz +from datetime import datetime as ddatetime + import lxml import lxml.html -from lxml.cssselect import CSSSelector -from datetime import datetime as ddatetime +import pytz from django.conf import settings from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.core.exceptions import PermissionDenied +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Q -from django.http.response import (JsonResponse, - HttpResponse, HttpResponseRedirect) -from django.shortcuts import render, get_object_or_404, redirect -from django.urls import reverse_lazy, reverse +from django.http.response import (HttpResponse, HttpResponseRedirect, + JsonResponse) +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse, reverse_lazy +from lxml.cssselect import CSSSelector + from common import status from common.utils import convert_to_custom_timezone -from marketing.models import ( - Tag, ContactList, Contact, EmailTemplate, Campaign, CampaignLog, Link, CampaignLinkClick, - CampaignOpen -) -from marketing.forms import ContactListForm, ContactForm, EmailTemplateForm, SendCampaignForm -from marketing.tasks import upload_csv_file, run_campaign - +from common.models import User +from marketing.forms import (ContactForm, ContactListForm, EmailTemplateForm, + SendCampaignForm) +from marketing.models import (Campaign, CampaignLinkClick, CampaignLog, + CampaignOpen, Contact, ContactList, + EmailTemplate, Link, Tag, FailedContact) +from marketing.tasks import run_campaign, upload_csv_file TIMEZONE_CHOICES = [(tz, tz) for tz in pytz.common_timezones] @@ -37,51 +40,85 @@ def get_exact_match(query, m2m_field, ids): @login_required(login_url='/login') def dashboard(request): - return render(request, 'marketing/dashboard.html') + if request.user.role == 'ADMIN' or request.user.is_superuser: + email_templates = EmailTemplate.objects.all() + contacts = Contact.objects.all() + campaign = Campaign.objects.all() + contacts_list = ContactList.objects.all() + else: + email_templates = EmailTemplate.objects.filter(created_by=request.user) + contacts = Contact.objects.filter(created_by=request.user) + campaign = Campaign.objects.filter(created_by=request.user) + contacts_list = ContactList.objects.filter(created_by=request.user) + + context = { + 'email_templates': email_templates, + 'contacts': contacts, + 'campaigns': campaign, + 'contacts_list': contacts_list + } + return render(request, 'marketing/dashboard.html', context) @login_required(login_url='/login') def contact_lists(request): tags = Tag.objects.all() + users = User.objects.all() if (request.user.role == "ADMIN"): queryset = ContactList.objects.all() else: - queryset = ContactList.objects.filter(created_by=request.user) + queryset = ContactList.objects.filter( + Q(created_by=request.user) | Q(visible_to=request.user)) + # users = User.objects.none() + if request.GET.get('tag'): + queryset = queryset.filter(tags=request.GET.get('tag')) if request.method == 'POST': - post_tags = request.POST.get('tags') - if request.POST.get('search'): + post_tags = request.POST.getlist('tag') + + if request.POST.get('contact_list_name'): queryset = queryset.filter( - Q(name__icontains=request.POST['search']) | - Q(created_by__email__icontains=request.POST[ - 'search'])).distinct() + name__icontains=request.POST.get('contact_list_name')) - if post_tags: + if request.POST.get('created_by'): + queryset = queryset.filter( + created_by=request.POST.get('created_by')) - filtered_list = json.loads(request.POST['tags']) + if request.POST.get('tag'): + queryset = queryset.filter(tags__id__in=post_tags) + request_tags = request.POST.getlist('tag') - if len(filtered_list) > 1: - queryset = get_exact_match(queryset, 'tags', filtered_list) - else: - queryset = queryset.filter(tags__id__in=filtered_list) - - per_page = request.GET.get("per_page", 10) - paginator = Paginator(queryset, per_page) - page = request.GET.get('page', 1) - try: - contact_lists = paginator.page(page) - except PageNotAnInteger: - contact_lists = paginator.page(1) - except EmptyPage: - contact_lists = paginator.page(paginator.num_pages) - data = {'contact_lists': contact_lists, 'tags': tags} + data = {'contact_lists': queryset, 'tags': tags, 'users': users, + 'request_tags': request.POST.getlist('tag'), } return render(request, 'marketing/lists/index.html', data) @login_required(login_url='/login') def contacts_list(request): + users = User.objects.all() + if (request.user.role == "ADMIN"): + contacts = Contact.objects.all() + else: + contacts = Contact.objects.filter(created_by=request.user) + # users = User.objects.none() contacts = Contact.objects.all() - data = {"contacts": contacts} - return render(request, 'marketing/lists/all.html', data) + if request.method == 'GET': + context = {'contacts': contacts, 'users': users} + return render(request, 'marketing/lists/all.html', context) + + if request.method == 'POST': + data = request.POST + if data.get('contact_name'): + contacts = contacts.filter( + name__icontains=data.get('contact_name')) + + if data.get('contact_email'): + contacts = contacts.filter(email=data.get('contact_email')) + + if data.get('created_by'): + contacts = contacts.filter(created_by=data.get('created_by')) + + context = {'contacts': contacts, 'users': User.objects.all()} + return render(request, 'marketing/lists/all.html', context) @login_required(login_url='/login') @@ -182,11 +219,12 @@ def view_contact_list(request, pk): @login_required(login_url='/login') def delete_contact_list(request, pk): - try: - # ContactList.objects.get(pk=pk).delete() - redirect_to = reverse('marketing:contact_lists') - except ContactList.DoesNotExist: - redirect_to = reverse('marketing:contact_lists') + contact_list_obj = get_object_or_404(ContactList, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_list_obj.created_by == request.user): + raise PermissionDenied + + contact_list_obj.delete() + redirect_to = reverse('marketing:contact_lists') return HttpResponseRedirect(redirect_to) @@ -214,14 +252,47 @@ def contacts_list_new(request): @login_required(login_url='/login') -def edit_contact(request): - return render(request, 'marketing/lists/edit_contact.html') +def edit_contact(request, pk): + contact_obj = get_object_or_404(Contact, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_obj.created_by == request.user): + raise PermissionDenied + if request.method == 'GET': + form = ContactForm(instance=contact_obj) + return render(request, 'marketing/lists/edit_contact.html', {'form': form}) + + if request.method == 'POST': + form = ContactForm(request.POST, instance=contact_obj) + if form.is_valid(): + contact = form.save(commit=False) + contact.save() + form.save_m2m() + return JsonResponse({'error': False, 'success_url': reverse('marketing:contacts_list')}) + else: + return JsonResponse({'error': True, 'errors': form.errors, }) + + +@login_required(login_url='/login') +def delete_contact(request, pk): + contact_obj = get_object_or_404(Contact, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_obj.created_by == request.user): + raise PermissionDenied + contact_obj.delete() + return redirect('marketing:contacts_list') @login_required(login_url='/login') def contact_list_detail(request, pk): contact_list = get_object_or_404(ContactList, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_list.created_by == request.user): + raise PermissionDenied contacts_list = contact_list.contacts.all() + if request.POST: + if request.POST.get('name'): + contacts_list = contacts_list.filter(name__icontains=request.POST.get('name')) + if request.POST.get('email'): + contacts_list = contacts_list.filter(email=request.POST.get('email')) + if request.POST.get('company_name'): + contacts_list = contacts_list.filter(company_name=request.POST.get('company_name')) data = {'contact_list': contact_list, "contacts_list": contacts_list} return render(request, 'marketing/lists/detail.html', data) @@ -230,7 +301,8 @@ def contact_list_detail(request, pk): def failed_contact_list_detail(request, pk): contact_list = get_object_or_404(ContactList, pk=pk) failed_contacts_list = contact_list.failed_contacts.all() - data = {'contact_list': contact_list, "failed_contacts_list": failed_contacts_list} + data = {'contact_list': contact_list, + "failed_contacts_list": failed_contacts_list} return render(request, 'marketing/lists/failed_detail.html', data) @@ -242,7 +314,8 @@ def failed_contact_list_download_delete(request, pk): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="failed_contacts.csv"' writer = csv.writer(response) - writer.writerow(["company name", "email", "first name", "last name", "city", "state"]) + writer.writerow(["company name", "email", "first name", + "last name", "city", "state"]) for contact in failed_contacts_list: writer.writerow([ @@ -255,27 +328,23 @@ def failed_contact_list_download_delete(request, pk): @login_required(login_url='/login') def email_template_list(request): + users = User.objects.all() if (request.user.is_admin or request.user.is_superuser): queryset = EmailTemplate.objects.all() else: queryset = EmailTemplate.objects.filter( created_by=request.user) + # users = User.objects.none() if request.method == 'POST': - queryset = queryset.filter( - Q(title__icontains=request.POST['search']) | - Q(subject__icontains=request.POST['search']) | - Q(created_by__email__icontains=request.POST['search'])) + if request.POST.get('template_name'): + queryset = queryset.filter( + title__icontains=request.POST.get('template_name')) - per_page = request.GET.get("per_page", 10) - paginator = Paginator(queryset.distinct(), per_page) - page = request.GET.get('page') - try: - email_templates = paginator.page(page) - except PageNotAnInteger: - email_templates = paginator.page(1) - except EmptyPage: - email_templates = paginator.page(paginator.num_pages) - data = {'email_templates': email_templates} + if request.POST.get('created_by'): + queryset = queryset.filter( + created_by=request.POST.get('created_by')) + + data = {'email_templates': queryset, 'users': users} return render(request, 'marketing/email_template/index.html', data) @@ -321,7 +390,7 @@ def email_template_detail(request, pk): @login_required(login_url='/login') def email_template_delete(request, pk): try: - # EmailTemplate.objects.get(id=pk).delete() + EmailTemplate.objects.get(id=pk).delete() redirect_to = reverse('marketing:email_template_list') except ContactList.DoesNotExist: redirect_to = reverse('marketing:email_template_list') @@ -330,25 +399,24 @@ def email_template_delete(request, pk): @login_required(login_url='/login') def campaign_list(request): - campaigns = Campaign.objects.all() + users = User.objects.all() + if (request.user.role == "ADMIN"): + queryset = Campaign.objects.all() + else: + queryset = Campaign.objects.all().filter(created_by=request.user) + # users = User.objects.none() + if request.GET.get('tag'): + queryset = queryset.filter(tags=request.GET.get('tag')) if request.method == 'POST': - campaigns = campaigns.filter( - Q(title__icontains=request.POST['search']) | - Q(created_by__email__icontains=request.POST['search']) - ) - if request.POST['scheduled_on']: - campaigns = campaigns.filter( - schedule_date_time__date__lte=request.POST['scheduled_on']) - per_page = request.GET.get("per_page", 10) # Show 15 contacts per page - paginator = Paginator(campaigns.distinct().order_by('-created_on'), per_page) - page = request.GET.get('page') - try: - campaigns = paginator.page(page) - except PageNotAnInteger: - campaigns = paginator.page(1) - except EmptyPage: - campaigns = paginator.page(paginator.num_pages) - data = {'campaigns_list': campaigns} + if request.POST.get('campaign_name'): + queryset = queryset.filter( + title__icontains=request.POST.get('campaign_name')) + + if request.POST.get('created_by'): + queryset = queryset.filter( + created_by=request.POST.get('created_by')) + + data = {'campaigns_list': queryset, 'users': users} return render(request, 'marketing/campaign/index.html', data) @@ -375,7 +443,8 @@ def campaign_new(request): # return JsonResponse(data, status=status.HTTP_200_OK) return render(request, 'marketing/campaign/new.html', data) else: - form = SendCampaignForm(request.POST, request.FILES) + form = SendCampaignForm(request.POST, request.FILES, + contacts_list=request.POST.getlist('contact_list')) if form.is_valid(): instance = form.save(commit=False) instance.created_by = request.user @@ -393,6 +462,12 @@ def campaign_new(request): # contacts = Contact.objects.filter(contact_list__in=json.loads(request.POST['contact_list'])).distinct() # for each_contact in contacts: # instance.contacts.add(each_contact) + tags = request.POST['tags'].split( + ',') if request.POST['tags'] else [] + for each in tags: + tag, _ = Tag.objects.get_or_create( + name=each, created_by=request.user) + instance.tags.add(tag) camp = instance @@ -425,13 +500,16 @@ def campaign_new(request): schedule_date_time = request.POST.get('schedule_date_time', '') user_timezone = request.POST.get('timezone', '') if user_timezone and schedule_date_time: - start_time_object = ddatetime.strptime(schedule_date_time, '%Y-%m-%d %H:%M') - schedule_date_time = convert_to_custom_timezone(start_time_object, user_timezone, to_utc=True) + start_time_object = ddatetime.strptime( + schedule_date_time, '%Y-%m-%d %H:%M') + schedule_date_time = convert_to_custom_timezone( + start_time_object, user_timezone, to_utc=True) instance.schedule_date_time = schedule_date_time instance.timezone = user_timezone instance.save() else: - run_campaign.delay(instance.id) + run_campaign.delay( + instance.id, domain=request.get_host(), protocol=request.scheme) return JsonResponse({'error': False, 'data': form.data}, status=status.HTTP_201_CREATED) return JsonResponse({'error': True, 'errors': form.errors}, status=status.HTTP_200_OK) @@ -463,11 +541,13 @@ def campaign_details(request, pk): contacts = Contact.objects.filter(contact_list__id__in=contact_list_ids) all_contacts = contacts - bounced_contacts = contacts.filter(is_bounced=True) + bounced_contacts = contacts.filter(is_bounced=True).distinct() unsubscribe_contacts = contacts.filter( - is_unsubscribed=True) - read_contacts = campaign.marketing_links.filter(Q(clicks__gt=0)) - # read_contacts = + is_unsubscribed=True).distinct() + # read_contacts = campaign.marketing_links.filter(Q(clicks__gt=0)).distinct() + contact_ids = CampaignOpen.objects.filter( + campaign=campaign).values_list('contact_id', flat=True) + read_contacts = Contact.objects.filter(id__in=contact_ids) sent_contacts = contacts.exclude( Q(is_bounced=True) | Q(is_unsubscribed=True)) if request.POST: @@ -489,7 +569,8 @@ def campaign_details(request, pk): except EmptyPage: contacts = contacts_paginator.page(contacts_paginator.num_pages) - unsubscribe_contacts_paginator = Paginator(unsubscribe_contacts.order_by('id'), per_page) + unsubscribe_contacts_paginator = Paginator( + unsubscribe_contacts.order_by('id'), per_page) if (tab == "unsubscribe_contacts"): page = page else: @@ -500,9 +581,11 @@ def campaign_details(request, pk): except PageNotAnInteger: unsubscribe_contacts = unsubscribe_contacts_paginator.page(1) except EmptyPage: - unsubscribe_contacts = unsubscribe_contacts_paginator.page(unsubscribe_contacts_paginator.num_pages) + unsubscribe_contacts = unsubscribe_contacts_paginator.page( + unsubscribe_contacts_paginator.num_pages) - bounced_contacts_paginator = Paginator(bounced_contacts.order_by('id'), per_page) + bounced_contacts_paginator = Paginator( + bounced_contacts.order_by('id'), per_page) if (tab == "bounced_contacts"): page = page else: @@ -513,7 +596,8 @@ def campaign_details(request, pk): except PageNotAnInteger: bounced_contacts = bounced_contacts_paginator.page(1) except EmptyPage: - bounced_contacts = bounced_contacts_paginator.page(bounced_contacts_paginator.num_pages) + bounced_contacts = bounced_contacts_paginator.page( + bounced_contacts_paginator.num_pages) sent_contacts_paginator = Paginator(sent_contacts.order_by('id'), per_page) if (tab == "sent_contacts"): @@ -526,7 +610,8 @@ def campaign_details(request, pk): except PageNotAnInteger: sent_contacts = sent_contacts_paginator.page(1) except EmptyPage: - sent_contacts = sent_contacts_paginator.page(sent_contacts_paginator.num_pages) + sent_contacts = sent_contacts_paginator.page( + sent_contacts_paginator.num_pages) all_contacts_paginator = Paginator(all_contacts.order_by('id'), per_page) if (tab == "all_contacts"): @@ -539,7 +624,8 @@ def campaign_details(request, pk): except PageNotAnInteger: all_contacts = all_contacts_paginator.page(1) except EmptyPage: - all_contacts = all_contacts_paginator.page(all_contacts_paginator.num_pages) + all_contacts = all_contacts_paginator.page( + all_contacts_paginator.num_pages) read_contacts_paginator = Paginator(read_contacts.order_by('id'), per_page) if (tab == "read_contacts"): @@ -552,7 +638,8 @@ def campaign_details(request, pk): except PageNotAnInteger: read_contacts = read_contacts_paginator.page(1) except EmptyPage: - read_contacts = read_contacts_paginator.page(read_contacts_paginator.num_pages) + read_contacts = read_contacts_paginator.page( + read_contacts_paginator.num_pages) data = {'contact_lists': contact_lists, "campaign": campaign, "contacts": contacts, 'unsubscribe_contacts': unsubscribe_contacts, @@ -611,8 +698,7 @@ def campaign_open(request, campaign_log_id, email_id): campaign_log = CampaignLog.objects.filter(id=campaign_log_id).first() contact = Contact.objects.filter(id=email_id).first() - image_data = settings.STATIC_URL + 'images/' - image_data = open(settings.STATIC_ROOT + '/images/company.png', 'rb').read() + image_data = open('static/images/company.png', 'rb').read() if campaign_log and contact: campaign_open = CampaignOpen.objects.filter( campaign=campaign_log.campaign, contact=contact).first() @@ -627,3 +713,111 @@ def campaign_open(request, campaign_log_id, email_id): campaign_log.campaign.save() campaign_open.save() return HttpResponse(image_data, content_type="image/png") + + +def demo_file_download(request): + sample_data = [ + 'company name,email,first name,last name,city,state\n', + 'company_name_1,user1@email.com,first_name,last_name,Hyderabad,Telangana\n', + 'company_name_2,user2@email.com,first_name,last_name,Hyderabad,Telangana\n', + 'company_name_3,user3@email.com,first_name,last_name,Hyderabad,Telangana\n', + 'company_name_4,user4@email.com,first_name,last_name,Hyderabad,Telangana\n' + ] + response = HttpResponse( + sample_data, content_type='text/plain') + response['Content-Disposition'] = 'attachment; filename={}'.format( + 'sample_data.csv') + return response + + +def unsubscribe_from_campaign(request, contact_id): + contact_obj = get_object_or_404(Contact, pk=contact_id) + contact_obj.is_unsubscribed = True + contact_obj.save() + return HttpResponseRedirect('/') + + +@login_required +def contact_detail(request, contact_id): + contact_obj = get_object_or_404(Contact, pk=contact_id) + + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_obj.created_by == request.user): + raise PermissionDenied + if request.method == 'GET': + return render(request, 'contact_detail.html', {'contact_obj': contact_obj}) + + +@login_required(login_url='/login') +def edit_failed_contact(request, pk): + contact_obj = get_object_or_404(FailedContact, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_obj.created_by == request.user): + raise PermissionDenied + if request.method == 'GET': + form = ContactForm(instance=contact_obj) + return render(request, 'marketing/lists/edit_contact.html', {'form': form}) + + if request.method == 'POST': + form = ContactForm(request.POST, instance=contact_obj) + if form.is_valid(): + contact = form.save(commit=False) + contact.save() + form.save_m2m() + return JsonResponse({'error': False, 'success_url': reverse('marketing:contacts_list')}) + else: + return JsonResponse({'error': True, 'errors': form.errors, }) + + +@login_required(login_url='/login') +def delete_failed_contact(request, pk): + contact_obj = get_object_or_404(FailedContact, pk=pk) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or contact_obj.created_by == request.user): + raise PermissionDenied + if request.method == 'GET': + contact_obj.delete() + return redirect('marketing:contacts_list') + + +@login_required +def download_contacts_for_campaign(request, compaign_id): + campaign_obj = get_object_or_404(Campaign, pk=compaign_id) + if not (request.user.role == 'ADMIN' or request.user.is_superuser or campaign_obj.created_by == request.user): + raise PermissionDenied + if request.method == 'GET': + if request.GET.get('is_bounced') == 'true': + contact_ids = campaign_obj.contact_lists.filter(contacts__is_bounced=True).values_list( + 'contacts__id', flat=True) + contacts = Contact.objects.filter(id__in=contact_ids).values( + 'company_name', 'email', 'name', 'last_name', 'city', 'state') + + if request.GET.get('is_unsubscribed') == 'true': + contact_ids = campaign_obj.contact_lists.filter(contacts__is_unsubscribed=True).values_list( + 'contacts__id', flat=True) + contacts = Contact.objects.filter(id__in=contact_ids).values( + 'company_name', 'email', 'name', 'last_name', 'city', 'state') + + if request.GET.get('is_opened') == 'true': + contact_ids = CampaignOpen.objects.filter( + campaign=campaign_obj).values_list('contact_id', flat=True) + contacts = Contact.objects.filter(id__in=contact_ids).values( + 'company_name', 'email', 'name', 'last_name', 'city', 'state') + + data = [ + 'company name,email,first name,last name,city,state\n', + ] + for contact in contacts: + data.append((', '.join(contact.values()) + '\n')) + response = HttpResponse( + data, content_type='text/plain') + response['Content-Disposition'] = 'attachment; filename={}'.format( + 'data_downloads.csv') + return response + + +@login_required +def create_campaign_from_template(request, template_id): + email_template_obj = get_object_or_404(EmailTemplate, pk=template_id) + if request.method == 'GET': + url_location = reverse('marketing:campaign_new') + url_query_params = '?email_template={}'.format(template_id) + url = url_location + url_query_params + return HttpResponseRedirect(url) diff --git a/opportunity/forms.py b/opportunity/forms.py index 5a9e1d60..c8ef7c0d 100644 --- a/opportunity/forms.py +++ b/opportunity/forms.py @@ -15,10 +15,11 @@ class OpportunityForm(forms.ModelForm): field.widget.attrs = {"class": "form-control"} self.fields['description'].widget.attrs.update({ 'rows': '8'}) - self.fields['assigned_to'].queryset = assigned_users + if assigned_users: + self.fields['assigned_to'].queryset = assigned_users + self.fields['assigned_to'].required = False self.fields['account'].queryset = opp_accounts self.fields['contacts'].queryset = opp_contacts - self.fields['assigned_to'].required = False self.fields['contacts'].required = False for key, value in self.fields.items(): value.widget.attrs['placeholder'] = value.label diff --git a/opportunity/tasks.py b/opportunity/tasks.py new file mode 100644 index 00000000..d3fb01d3 --- /dev/null +++ b/opportunity/tasks.py @@ -0,0 +1,36 @@ +from celery.task import task +from django.conf import settings +from django.core.mail import EmailMultiAlternatives, EmailMessage +from django.db.models import Q +from django.shortcuts import reverse +from django.template.loader import render_to_string + +from accounts.models import User +from opportunity.models import Opportunity + + +@task +def send_email_to_assigned_user(recipients, opportunity_id, domain='demo.django-crm.io', protocol='http'): + """ Send Mail To Users When they are assigned to a opportunity """ + opportunity = Opportunity.objects.get(id=opportunity_id) + for user in recipients: + recipients_list = [] + user = User.objects.filter(id=user).first() + if user: + recipients_list.append(user.email) + context = {} + context["url"] = protocol + '://' + domain + \ + reverse('opportunity:opp_view', args=(opportunity.id,)) + context["user"] = user + context["opportunity"] = opportunity + subject = 'Assigned to opportunity.' + html_content = render_to_string( + 'assigned_to/opportunity_assigned.html', context=context) + + msg = EmailMessage( + subject, + html_content, + to=recipients_list + ) + msg.content_subtype = "html" + msg.send() diff --git a/opportunity/templates/opportunity.html b/opportunity/templates/opportunity.html index 98fc252c..0253cbe3 100644 --- a/opportunity/templates/opportunity.html +++ b/opportunity/templates/opportunity.html @@ -1,13 +1,17 @@ {% extends "sales/base.html" %} {% load staticfiles %} {% load paginate %} +{% block extralinks %} + +{% endblock %} {% block content %}
    @@ -15,83 +19,103 @@
    -
    -
    -
    Filters
    -
    -
    -
    - - + +
    +
    Filters
    +
    +
    +
    + + +
    -
    -
    -
    - - +
    +
    + + +
    -
    -
    -
    - - +
    +
    + + +
    -
    -
    -
    - - +
    +
    + + +
    -
    -
    -
    - - Clear +
    +
    + + +
    +
    +
    +
    + + Clear +
    -
    - +
    -
    +
    - +
    -
    - Opportunities - {% if show_pageitems %} {% show_pageitems %}{% else %} {{ opportunity_list|length }}{% endif %} - - - - -
    -
    - - - {% if opportunity_list|length > 0 %} +
    + Opportunities - {% if show_pageitems %} {% show_pageitems %}{% else %} + {{ opportunity_list|length }}{% endif %} + + + + +
    +
    +
    + + {% if opportunity_list|length > 0 %} - + @@ -99,48 +123,48 @@ - {% endif %} - - - {% if per_page %} + {% endif %} + + + {% if per_page %} {% paginate per_page opportunity_list %} - {% else %} + {% else %} {% paginate 10 opportunity_list %} - {% endif %} - {% for opportunity in opportunity_list %} - - - - - - + + + + + - - - - - - {% endfor %} - -
    S.No NameAssigned UserAssigned To Tags Account Lead Source Actions
    {{ forloop.counter }}{{ opportunity.name }} - {% with opportunity_users=opportunity.assigned_to.all %} - {% if opportunity_users %} - {% for user in opportunity_users %} - {{ user.email }}
    - {% endfor %} - {% else %} - Not Assigned to Any - {% endif %} - {% endwith %} -
    - {% with tags=opportunity.tags.all %} - {% if tags %} - {% for tag in tags %} - {{ tag.name }} - {% endfor %} - {% else %} - No Tags - {% endif %} - {% endwith %} - - {% if opportunity.account %} + {% endif %} + {% for opportunity in opportunity_list %} +
    {{ forloop.counter }}{{ opportunity.name }} + {% with opportunity_users=opportunity.assigned_to.all %} + {% if opportunity_users %} + {% for user in opportunity_users %} + {{ user.email }}
    + {% endfor %} + {% else %} + None + {% endif %} + {% endwith %} +
    + {% with tags=opportunity.tags.all %} + {% if tags %} + {% for tag in tags %} + {{ tag.name }} + {% endfor %} + {% else %} + No Tags + {% endif %} + {% endwith %} + + {% if opportunity.account %} {{ opportunity.account }} - {% else %} + {% else %} No Related Account - {% endif %} - {% if opportunity.stage %}{{ opportunity.stage }}{% else %}Not Specified {% endif %}{% if opportunity.lead_source %}{{ opportunity.lead_source }}{% else %}Not Specified{% endif %} - - - {% if request.user == opportunity.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} - - {% endif %} -
    + {% if opportunity.stage %}{{ opportunity.stage }}{% else %}Not Specified {% endif %} + {% if opportunity.lead_source %}{{ opportunity.lead_source }}{% else %}Not Specified{% endif %} + + + + + {% if request.user == opportunity.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} + + {% endif %} + + + {% endfor %} + +
    {% ifequal opportunity_list|length 0 %} -
    No Opportunity Records Found
    +
    No Opportunity Records Found
    {% endifequal %}
    {% show_pages %} @@ -178,25 +205,26 @@
    - +
    {% endblock %} {% block js_block %} -{% endblock js_block %} +{% endblock js_block %} \ No newline at end of file diff --git a/opportunity/templates/view_opportunity.html b/opportunity/templates/view_opportunity.html index 9a7008c7..1f763c0a 100644 --- a/opportunity/templates/view_opportunity.html +++ b/opportunity/templates/view_opportunity.html @@ -1,6 +1,15 @@ {% extends 'sales/base.html' %} {% load staticfiles %} +{% load thumbnail %} {% block breadcrumb %} +{% block extralinks %} + + +{% endblock extralinks %}
    - {% endif %} + {% endif %}
    - Created by {{ opportunity_record.created_by }} created on {{ opportunity_record.created_on }} + Created by {{ opportunity_record.created_by }} created on + {{ opportunity_record.created_on }}
    @@ -140,10 +158,13 @@
    -
    +
    + +

    - +
    @@ -154,17 +175,25 @@ {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == attachment.created_by %}
    - +
    {% endif %}
    + {% if attachment.attachment %} + {% if 'image' in attachment.file_type %} + {% thumbnail attachment.attachment "80x80" crop="center" as im %} + + {% endthumbnail %} + {% endif %} + {% endif %}
      {{attachment.file_name}} Download
    -
     Uploaded by : {{ attachment.created_by }}
    +
     Uploaded by : {{ attachment.created_by }}
    {{ attachment.created_on|date:'m/d/Y,  h:i A' }}
    @@ -183,11 +212,15 @@
    - -
    + +
    + +

    - +
    @@ -198,7 +231,8 @@ {% if request.user.is_superuser or request.user.role == 'ADMIN' or request.user == comment.commented_by %}
    - +
    - @@ -257,26 +302,28 @@ {% block js_block %} - - + + + {% endblock js_block %} \ No newline at end of file diff --git a/opportunity/tests.py b/opportunity/tests.py index 5ad10263..1c837a7c 100644 --- a/opportunity/tests.py +++ b/opportunity/tests.py @@ -116,11 +116,11 @@ class opportunityCreateTestCase(OpportunityModel, TestCase): def test_del_opportunity_url(self): response = self.client.get( '/opportunities/' + str(self.opportunity.id) + '/delete/') - self.assertEqual(response['location'], '/opportunities/list/') + self.assertEqual(response['location'], '/opportunities/') def test_opportunity_delete(self): Opportunity.objects.filter(id=self.account.id).delete() - response = self.client.get('/opportunities/list/') + response = self.client.get(reverse('opportunity:list')) self.assertEqual(response.status_code, 200) @@ -172,7 +172,7 @@ class OpportunityListView(OpportunityModel, TestCase): def test_opportunity_list(self): self.opportunity = Opportunity.objects.all() - response = self.client.get('/opportunities/list/') + response = self.client.get(reverse('opportunity:list')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'opportunity.html') @@ -181,7 +181,7 @@ class OpportunityListView(OpportunityModel, TestCase): data = { 'name': 'meghan', 'stage': 'city', 'lead_source': 'Call', 'accounts': self.account} - response = self.client.post('/opportunities/list/', data) + response = self.client.post(reverse('opportunity:list'), data) get_opp_val = Opportunity.objects.get(lead_source='Call') self.assertEqual(get_opp_val.lead_source, 'Call') self.assertEqual(get_opp_val.name, str(get_opp_val)) diff --git a/opportunity/urls.py b/opportunity/urls.py index de3931b4..83940957 100644 --- a/opportunity/urls.py +++ b/opportunity/urls.py @@ -12,7 +12,7 @@ app_name = 'opportunity' urlpatterns = [ - path('list/', OpportunityListView.as_view(), name='list'), + path('', OpportunityListView.as_view(), name='list'), path('create/', create_opportunity, name='save'), path('/view/', OpportunityDetailView.as_view(), name="opp_view"), path('/edit/', update_opportunity, name="opp_edit"), diff --git a/opportunity/views.py b/opportunity/views.py index 02a75aba..ece5fc10 100644 --- a/opportunity/views.py +++ b/opportunity/views.py @@ -17,6 +17,8 @@ from opportunity.models import Opportunity from django.urls import reverse from django.db.models import Q from django.core.exceptions import PermissionDenied +from common.tasks import send_email_user_mentions +from opportunity.tasks import send_email_to_assigned_user class OpportunityListView(LoginRequiredMixin, TemplateView): @@ -33,6 +35,9 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user.id)) + if self.request.GET.get('tag', None): + queryset = queryset.filter(tags__in = self.request.GET.getlist('tag')) + request_post = self.request.POST if request_post: if request_post.get('name'): @@ -49,7 +54,9 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): if request_post.get('contacts'): queryset = queryset.filter( contacts=request_post.get('contacts')) - return queryset + if request_post.get('tag'): + queryset = queryset.filter(tags__in=request_post.getlist('tag')) + return queryset.distinct() def get_context_data(self, **kwargs): context = super(OpportunityListView, self).get_context_data(**kwargs) @@ -59,7 +66,14 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): context["stages"] = STAGES context["sources"] = SOURCES context["per_page"] = self.request.POST.get('per_page') - + tag_ids = list(set(Opportunity.objects.values_list('tags', flat=True))) + context["tags"] = Tags.objects.filter(id__in=tag_ids) + if self.request.POST.get('tag', None): + context["request_tags"] = self.request.POST.getlist('tag') + elif self.request.GET.get('tag', None): + context["request_tags"] = self.request.GET.getlist('tag') + else: + context["request_tags"] = None search = False if ( self.request.POST.get('name') or self.request.POST.get('stage') or @@ -85,9 +99,15 @@ def create_opportunity(request): created_by=request.user) contacts = Contact.objects.filter( Q(assigned_to__in=[request.user]) | Q(created_by=request.user)) - + users = [] + if request.user.role == 'ADMIN' or request.user.is_superuser: + users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + users = [] + else: + users = User.objects.filter(role='ADMIN').order_by('email') kwargs_data = { - "assigned_to": User.objects.filter(is_active=True).order_by('email'), + "assigned_to": users, "account": accounts, "contacts": contacts} if request.POST: form = OpportunityForm(request.POST, request.FILES, **kwargs_data) @@ -102,20 +122,23 @@ def create_opportunity(request): *request.POST.getlist('assigned_to')) assigned_to_list = request.POST.getlist('assigned_to') current_site = get_current_site(request) - for assigned_to_user in assigned_to_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to opportunity.' - message = render_to_string( - 'assigned_to/opportunity_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'opportunity': opportunity_obj - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + recipients = assigned_to_list + send_email_to_assigned_user.delay(recipients, opportunity_obj.id, domain=current_site.domain, + protocol=request.scheme) + # for assigned_to_user in assigned_to_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to opportunity.' + # message = render_to_string( + # 'assigned_to/opportunity_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'opportunity': opportunity_obj + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() if request.POST.getlist('contacts', []): opportunity_obj.contacts.add( *request.POST.getlist('contacts')) @@ -197,10 +220,19 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView): assigned_data.append(assigned_dict) comments = context["opportunity_record"].opportunity_comments.all() + + if self.request.user.is_superuser or self.request.user.role == 'ADMIN': + users_mention = list(User.objects.all().values('username')) + elif self.request.user != context['object'].created_by: + users_mention = [{'username': context['object'].created_by.username}] + else: + users_mention = list(context['object'].assigned_to.all().values('username')) + context.update({ "comments": comments, 'attachments': context[ "opportunity_record"].opportunity_attachment.all(), + "users_mention": users_mention, "assigned_data": json.dumps(assigned_data)}) return context @@ -214,9 +246,15 @@ def update_opportunity(request, pk): created_by=request.user) contacts = Contact.objects.filter( Q(assigned_to__in=[request.user]) | Q(created_by=request.user)) - + users = [] + if request.user.role == 'ADMIN' or request.user.is_superuser: + users = User.objects.filter(is_active=True).order_by('email') + elif request.user.google.all(): + users = [] + else: + users = User.objects.filter(role='ADMIN').order_by('email') kwargs_data = { - "assigned_to": User.objects.filter(is_active=True).order_by('email'), + "assigned_to": users, "account": accounts, "contacts": contacts} form = OpportunityForm(instance=opportunity_object, **kwargs_data) @@ -241,21 +279,25 @@ def update_opportunity(request, pk): all_members_list = list( set(list(assigned_form_users)) - set(list(assigned_to_ids))) - if all_members_list: - for assigned_to_user in all_members_list: - user = get_object_or_404(User, pk=assigned_to_user) - mail_subject = 'Assigned to opportunity.' - message = render_to_string( - 'assigned_to/opportunity_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'opportunity': opportunity_obj - }) - email = EmailMessage( - mail_subject, message, to=[user.email]) - email.content_subtype = "html" - email.send() + current_site = get_current_site(request) + recipients = all_members_list + send_email_to_assigned_user.delay(recipients, opportunity_obj.id, domain=current_site.domain, + protocol=request.scheme) + # if all_members_list: + # for assigned_to_user in all_members_list: + # user = get_object_or_404(User, pk=assigned_to_user) + # mail_subject = 'Assigned to opportunity.' + # message = render_to_string( + # 'assigned_to/opportunity_assigned.html', { + # 'user': user, + # 'domain': current_site.domain, + # 'protocol': request.scheme, + # 'opportunity': opportunity_obj + # }) + # email = EmailMessage( + # mail_subject, message, to=[user.email]) + # email.content_subtype = "html" + # email.send() opportunity_obj.assigned_to.clear() opportunity_obj.assigned_to.add( @@ -384,6 +426,10 @@ class AddCommentView(LoginRequiredMixin, CreateView): comment.commented_by = self.request.user comment.opportunity = self.opportunity comment.save() + comment_id = comment.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'opportunity', domain=current_site.domain, + protocol=self.request.scheme) return JsonResponse({ "comment_id": comment.id, "comment": comment.comment, "commented_on": comment.commented_on, @@ -413,6 +459,10 @@ class UpdateCommentView(LoginRequiredMixin, View): def form_valid(self, form): self.comment_obj.comment = form.cleaned_data.get("comment") self.comment_obj.save(update_fields=["comment"]) + comment_id = self.comment_obj.id + current_site = get_current_site(self.request) + send_email_user_mentions.delay(comment_id, 'opportunity', domain=current_site.domain, + protocol=self.request.scheme) return JsonResponse({ "commentid": self.comment_obj.id, "comment": self.comment_obj.comment, @@ -479,7 +529,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): "created_by": attachment.created_by.email, "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), - "attachment_display": attachment.get_file_type_display() + "attachment_display": attachment.get_file_type_display(), + "file_type": attachment.file_type() }) def form_invalid(self, form): diff --git a/requirements.txt b/requirements.txt index 381420ee..a128e721 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.1.7 +Django==2.2.2 boto==2.49.0 boto3==1.9.94 django-simple-pagination==1.3 @@ -8,6 +8,7 @@ psycopg2-binary==2.7.7 redis django-compressor==2.2 coverage +pdfkit==0.6.1 pytest==4.1.1 pytest-cov==2.6.1 pytest-django==3.4.5 diff --git a/static/css/bootstrap-suggest.css b/static/css/bootstrap-suggest.css new file mode 100644 index 00000000..e6263359 --- /dev/null +++ b/static/css/bootstrap-suggest.css @@ -0,0 +1,27 @@ +/*! + * bootstra-suggest - v2.0.1 (https://github.com/lodev09/bootstrap-suggest#readme) + * Copyright 2013-2019 Jovanni Lo (lodev09@gmail.com) + * Licensed under MIT (https://github.com/lodev09/bootstrap-suggest/blob/master/LICENSE) + */ + +.suggest { + /* position: relative; */ + top: 7px; + z-index: 30; + text-align: left; +} + +.suggest > .dropdown-menu { + margin-top: 15px; + position: absolute; +} + +.suggest > .dropdown-menu > a.dropdown-item { + border-top: 1px solid #eeeeee; + padding: 5px 10px; +} + +.suggest > .dropdown-menu > a.dropdown-item:first-child { + border-top: 0; +} + diff --git a/static/css/main.scss b/static/css/main.scss index 57b3fb5c..6c583f50 100644 --- a/static/css/main.scss +++ b/static/css/main.scss @@ -535,7 +535,7 @@ $color_2: #000; tr{ th{ color: #454545; - font-weight: 500; + font-weight: 900; font-size: 0.9rem; border: none; text-align: left; diff --git a/static/js/ajaxForm.js b/static/js/ajaxForm.js new file mode 100644 index 00000000..c67fc332 --- /dev/null +++ b/static/js/ajaxForm.js @@ -0,0 +1,1277 @@ +/*! + * jQuery Form Plugin + * version: 3.51.0-2014.06.20 + * Requires jQuery v1.5 or later + * Copyright (c) 2014 M. Alsup + * Examples and documentation at: http://malsup.com/jquery/form/ + * Project repository: https://github.com/malsup/form + * Dual licensed under the MIT and GPL licenses. + * https://github.com/malsup/form#copyright-and-license + */ +/*global ActiveXObject */ + +// AMD support +(function (factory) { + "use strict"; + if (typeof define === 'function' && define.amd) { + // using AMD; register as anon module + define(['jquery'], factory); + } else { + // no AMD; invoke directly + factory( (typeof(jQuery) != 'undefined') ? jQuery : window.Zepto ); + } +} + +(function($) { +"use strict"; + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are mutually exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').on('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the + form does not have to exist when you invoke ajaxForm: + + $('#myForm').ajaxForm({ + delegation: true, + target: '#output' + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * Feature detection + */ +var feature = {}; +feature.fileapi = $("").get(0).files !== undefined; +feature.formdata = window.FormData !== undefined; + +var hasProp = !!$.fn.prop; + +// attr2 uses prop when it can but checks the return type for +// an expected string. this accounts for the case where a form +// contains inputs with names like "action" or "method"; in those +// cases "prop" returns the element +$.fn.attr2 = function() { + if ( ! hasProp ) { + return this.attr.apply(this, arguments); + } + var val = this.prop.apply(this, arguments); + if ( ( val && val.jquery ) || typeof val === 'string' ) { + return val; + } + return this.attr.apply(this, arguments); +}; + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + /*jshint scripturl:true */ + + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + var method, action, url, $form = this; + + if (typeof options == 'function') { + options = { success: options }; + } + else if ( options === undefined ) { + options = {}; + } + + method = options.type || this.attr2('method'); + action = options.url || this.attr2('action'); + + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + + options = $.extend(true, { + url: url, + success: $.ajaxSettings.success, + type: method || $.ajaxSettings.type, + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var traditional = options.traditional; + if ( traditional === undefined ) { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements); + if (options.data) { + options.extraData = options.data; + qx = $.param(options.data, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a, traditional); + if (qx) { + q = ( q ? (q + '&' + qx) : qx ); + } + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + if (options.resetForm) { + callbacks.push(function() { $form.resetForm(); }); + } + if (options.clearForm) { + callbacks.push(function() { $form.clearForm(options.includeHidden); }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + var fn = options.replaceTarget ? 'replaceWith' : 'html'; + $(options.target)[fn](data).each(oldSuccess, arguments); + }); + } + else if (options.success) { + callbacks.push(options.success); + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this ; // jQuery 1.4+ supports scope context + for (var i=0, max=callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + if (options.error) { + var oldError = options.error; + options.error = function(xhr, status, error) { + var context = options.context || this; + oldError.apply(context, [xhr, status, error, $form]); + }; + } + + if (options.complete) { + var oldComplete = options.complete; + options.complete = function(xhr, status) { + var context = options.context || this; + oldComplete.apply(context, [xhr, status, $form]); + }; + } + + // are there files to upload? + + // [value] (issue #113), also see comment: + // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219 + var fileInputs = $('input[type=file]:enabled', this).filter(function() { return $(this).val() !== ''; }); + + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + var fileAPI = feature.fileapi && feature.formdata; + log("fileAPI :" + fileAPI); + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + + var jqxhr; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + jqxhr = fileUploadIframe(a); + }); + } + else { + jqxhr = fileUploadIframe(a); + } + } + else if ((hasFileInputs || multipart) && fileAPI) { + jqxhr = fileUploadXhr(a); + } + else { + jqxhr = $.ajax(options); + } + + $form.removeData('jqxhr').data('jqxhr', jqxhr); + + // clear element array + for (var k=0; k < elements.length; k++) { + elements[k] = null; + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + // utility fn for deep serialization + function deepSerialize(extraData){ + var serialized = $.param(extraData, options.traditional).split('&'); + var len = serialized.length; + var result = []; + var i, part; + for (i=0; i < len; i++) { + // #252; undo param space replacement + serialized[i] = serialized[i].replace(/\+/g,' '); + part = serialized[i].split('='); + // #278; use array instead of object storage, favoring array serializations + result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]); + } + return result; + } + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i=0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + var serializedData = deepSerialize(options.extraData); + for (i=0; i < serializedData.length; i++) { + if (serializedData[i]) { + formdata.append(serializedData[i][0], serializedData[i][1]); + } + } + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType: false, + processData: false, + cache: false, + type: method || 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = $.ajaxSettings.xhr(); + if (xhr.upload) { + xhr.upload.addEventListener('progress', function(event) { + var percent = 0; + var position = event.loaded || event.position; /*event.position is deprecated*/ + var total = event.total; + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + options.uploadProgress(event, position, total, percent); + }, false); + } + return xhr; + }; + } + + s.data = null; + var beforeSend = s.beforeSend; + s.beforeSend = function(xhr, o) { + //Send FormData() provided by user + if (options.formData) { + o.data = options.formData; + } + else { + o.data = formdata; + } + if(beforeSend) { + beforeSend.call(this, xhr, o); + } + }; + return $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var deferred = $.Deferred(); + + // #341 + deferred.abort = function(status) { + xhr.abort(status); + }; + + if (a) { + // ensure that every serialized input is still enabled + for (i=0; i < elements.length; i++) { + el = $(elements[i]); + if ( hasProp ) { + el.prop('disabled', false); + } + else { + el.removeAttr('disabled'); + } + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + (new Date().getTime()); + if (s.iframeTarget) { + $io = $(s.iframeTarget); + n = $io.attr2('name'); + if (!n) { + $io.attr2('name', id); + } + else { + id = n; + } + } + else { + $io = $('