diff --git a/.gitignore b/.gitignore index b9ff4402..bd54a86c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ ref.css ref.css.map .coverage crm/dev_settings\.py +env \ No newline at end of file diff --git a/accounts/admin.py b/accounts/admin.py deleted file mode 100644 index 0224ad86..00000000 --- a/accounts/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin -from accounts.models import Account -# Register your models here. - - -admin.site.register(Account) diff --git a/accounts/forms.py b/accounts/forms.py index 419f71a5..fc5d4359 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,8 +1,8 @@ from django import forms from .models import Account from common.models import Comment, Attachments -from leads.models import * -from contacts.models import * +from leads.models import Lead +from contacts.models import Contact from django.db.models import Q @@ -37,10 +37,10 @@ class AccountForm(forms.ModelForm): self.fields["billing_country"].choices = [ ("", "--Country--"), ] + list(self.fields["billing_country"].choices)[1:] self.fields["lead"].queryset = Lead.objects.all( - ).exclude(status='dead') + ).exclude(status='closed') if request_user: self.fields["lead"].queryset = Lead.objects.filter( - Q(assigned_to__in=[request_user]) | Q(created_by=request_user)).exclude(status='dead') + 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)) diff --git a/accounts/models.py b/accounts/models.py index b0029903..bf43f95f 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -29,7 +29,9 @@ class Account(models.Model): email = models.EmailField() phone = PhoneNumberField(null=True) industry = models.CharField( - _("Industry Type"), max_length=255, choices=INDCHOICES, blank=True, null=True) + _("Industry Type"), + max_length=255, choices=INDCHOICES, + blank=True, null=True) # billing_address = models.ForeignKey( # Address, related_name='account_billing_address', on_delete=models.CASCADE, blank=True, null=True) # shipping_address = models.ForeignKey( @@ -49,14 +51,16 @@ class Account(models.Model): website = models.URLField(_("Website"), blank=True, null=True) description = models.TextField(blank=True, null=True) created_by = models.ForeignKey( - User, related_name='account_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='account_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) is_active = models.BooleanField(default=False) tags = models.ManyToManyField(Tags, blank=True) status = models.CharField( choices=ACCOUNT_STATUS_CHOICE, max_length=64, default='open') lead = models.ForeignKey( - 'leads.Lead', related_name="account_leads", on_delete=models.SET_NULL, null=True) + 'leads.Lead', related_name="account_leads", + on_delete=models.SET_NULL, null=True) contact_name = models.CharField(pgettext_lazy( "Name of Contact", "Contact Name"), max_length=120) contacts = models.ManyToManyField( diff --git a/accounts/templates/accounts.html b/accounts/templates/accounts.html index 016669eb..ec1e69aa 100644 --- a/accounts/templates/accounts.html +++ b/accounts/templates/accounts.html @@ -2,6 +2,7 @@ {% load staticfiles %} {% load paginate %} {% block extralinks %} +{% load thumbnail %} {% endblock %} {% block content %} @@ -89,6 +90,7 @@ ID Name + Created By City State Tags @@ -106,6 +108,15 @@ {{ 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 %} + {% else %} + 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 %} diff --git a/accounts/templates/create_account.html b/accounts/templates/create_account.html index eeb473cd..56ce7472 100644 --- a/accounts/templates/create_account.html +++ b/accounts/templates/create_account.html @@ -56,7 +56,12 @@
- {{ account_form.lead }} + {{ account_form.lead.errors }}
@@ -286,4 +291,4 @@ } }); -{% endblock js_block %} \ No newline at end of file +{% endblock js_block %} diff --git a/accounts/tests.py b/accounts/tests.py index 79f33722..67625c3f 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -7,18 +7,21 @@ from common.models import User, Comment, Attachments class AccountCreateTest(object): def setUp(self): self.user = User.objects.create( - first_name="uday", username='uday', email='u@mp.com', role='ADMIN') - self.user.set_password('uday2293') + first_name="mike", username='mike', email='u@mp.com', role='ADMIN') + self.user.set_password('mike2293') self.user.save() self.account = Account.objects.create( - name="Uday", email="udayteja@micropyramid.com", phone="8333855552", - billing_address_line="", billing_street="KPHB", billing_city="HYDERABAD", - billing_state="ANDHRA PRADESH", billing_postcode="500073", billing_country="IN", - website="www.uday.com", created_by=self.user, + name="mike", email="mike@micropyramid.com", phone="8333855552", + billing_address_line="", billing_street="KPHB", + billing_city="New York", + billing_state="usa", billing_postcode="500073", + billing_country="IN", + website="www.mike.com", created_by=self.user, industry="SOFTWARE", description="Yes.. Testing Done") self.case = Case.objects.create( - name="raghu", case_type="Problem", status="New", account=self.account, + name="raghu", case_type="Problem", + status="New", account=self.account, priority="Low", description="something", created_by=self.user, closed_on="2016-05-04") self.comment = Comment.objects.create( @@ -29,25 +32,34 @@ class AccountCreateTest(object): attachment='image.png', case=self.case, created_by=self.user, account=self.account ) - self.client.login(email='u@mp.com', password='uday2293') + self.client.login(email='u@mp.com', password='mike2293') class AccountsCreateTestCase(AccountCreateTest, TestCase): def test_account_create_url(self): response = self.client.get('/accounts/create/', { - 'name': "Uday", 'email': "udayteja@micropyramid.com", 'phone': "", - 'billing_address_line': "", 'billing_street': "KPHB", 'billing_city': "HYDERABAD", - 'billing_state': "ANDHRA PRADESH", 'billing_postcode': "500073", 'billing_country': "IN", - 'website': "www.uday.com", + 'name': "mike", 'email': "mike@micropyramid.com", + 'phone': "", + 'billing_address_line': "", + 'billing_street': "KPHB", + 'billing_city': "New York", + 'billing_state': "usa", + 'billing_postcode': "500073", + 'billing_country': "IN", + 'website': "www.mike.com", 'industry': "SOFTWARE", 'description': "Yes.. Testing Done"}) self.assertEqual(response.status_code, 200) def test_account_create_html(self): response = self.client.get('/accounts/create/', { - 'name': "Uday", 'email': "udayteja@micropyramid.com", 'phone': "", - 'billing_address_line': "", 'billing_street': "KPHB", 'billing_city': "HYDERABAD", - 'billing_state': "ANDHRA PRADESH", 'billing_postcode': "500073", 'billing_country': "IN", - 'website': "www.uday.com", + 'name': "mike", 'email': "mike@micropyramid.com", 'phone': "", + 'billing_address_line': "", + 'billing_street': "KPHB", + 'billing_city': "New York", + 'billing_state': "usa", + 'billing_postcode': "500073", + 'billing_country': "IN", + 'website': "www.mike.com", 'industry': "SOFTWARE", 'description': "Yes.. Testing Done"}) self.assertTemplateUsed(response, 'create_account.html') @@ -64,8 +76,10 @@ class AccountsListTestCase(AccountCreateTest, TestCase): self.account = Account.objects.all() data = {'name': 'name', 'city': 'city', 'billing_address_line': "billing_address_line", - 'billing_street': "billing_street", 'billing_city': "billing_city", - 'billing_state': "billing_state", 'billing_postcode': "billing_postcode", + 'billing_street': "billing_street", + 'billing_city': "billing_city", + 'billing_state': "billing_state", + 'billing_postcode': "billing_postcode", 'billing_country': "billing_country"} response = self.client.post('/accounts/list/', data) self.assertEqual(response.status_code, 200) @@ -101,22 +115,36 @@ class AccountsRemoveTestCase(AccountCreateTest, TestCase): class AccountsUpdateUrlTestCase(AccountCreateTest, TestCase): def test_accounts_update(self): - response = self.client.get('/accounts/' + str(self.account.id) + '/edit/', { - 'name': "Uday", 'email': "udayteja@micropyramid.com", 'phone': "8333855552", - 'billing_address_line': "", 'billing_street': "KPHB", 'billing_city': "HYDERABAD", - 'billing_state': "ANDHRA PRADESH", 'billing_postcode': "500073", 'billing_country': "IN", - 'website': "www.uday.com", - 'industry': "SOFTWARE", 'description': "Yes.. Testing Done"}) + response = self.client.get( + '/accounts/' + str(self.account.id) + '/edit/', { + 'name': "mike", + 'email': "mike@micropyramid.com", 'phone': "8333855552", + 'billing_address_line': "", + 'billing_street': "KPHB", + 'billing_city': "New York", + 'billing_state': "usa", + 'billing_postcode': "500073", + 'billing_country': "IN", + 'website': "www.mike.com", + 'industry': "SOFTWARE", + 'description': "Yes.. Testing Done"}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'create_account.html') def test_accounts_update_post(self): - response = self.client.post('/accounts/' + str(self.account.id) + '/edit/', { - 'name': "meghana", 'email': "meghana@micropyramid.com", 'phone': "9555333123", - 'billing_address_line': "", 'billing_street': "KPHB", 'billing_city': "HYDERABAD", - 'billing_state': "ANDHRA PRADESH", 'billing_postcode': "500073", 'billing_country': "IN", - 'website': "www.meghana.com", - 'industry': "SOFTWARE", 'description': "Yes.. Testing Done"}) + response = self.client.post( + '/accounts/' + str(self.account.id) + '/edit/', + { + 'name': "dolly", 'email': "dolly@micropyramid.com", + 'phone': "9555333123", + 'billing_address_line': "", + 'billing_street': "KPHB", + 'billing_city': "New York", + 'billing_state': "usa", + 'billing_postcode': "500073", + 'billing_country': "IN", + 'website': "www.dolly.com", + 'industry': "SOFTWARE", 'description': "Yes.. Testing Done"}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'create_account.html') @@ -134,9 +162,15 @@ class AccountsUpdateUrlTestCase(AccountCreateTest, TestCase): class AccountCreateEmptyFormTestCase(AccountCreateTest, TestCase): def test_account_creation_invalid_data(self): - data = {'name': "", 'email': "", 'phone': "", 'website': "", 'industry': "", 'description': "", - 'billing_address_line': "", 'billing_street': "KPHB", 'billing_city': "HYDERABAD", - 'billing_state': "ANDHRA PRADESH", 'billing_postcode': "500073", 'billing_country': "IN"} + data = {'name': "", 'email': "", 'phone': "", + 'website': "", 'industry': "", + 'description': "", + 'billing_address_line': "", + 'billing_street': "KPHB", + 'billing_city': "New York", + 'billing_state': "usa", + 'billing_postcode': "500073", + 'billing_country': "IN"} response = self.client.post('/accounts/create/', data) self.assertEqual(response.status_code, 200) @@ -190,5 +224,6 @@ class AttachmentTestCase(AccountCreateTest, TestCase): def test_attachment_delete(self): response = self.client.post( - '/accounts/attachment/remove/', {'attachment_id': self.attachment.id}) + '/accounts/attachment/remove/', + {'attachment_id': self.attachment.id}) self.assertEqual(response.status_code, 200) diff --git a/accounts/urls.py b/accounts/urls.py index 6d19dc1e..b47c7050 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -12,12 +12,15 @@ urlpatterns = [ path('create/', CreateAccountView.as_view(), name='new_account'), path('/view/', AccountDetailView.as_view(), name="view_account"), path('/edit/', AccountUpdateView.as_view(), name="edit_account"), - path('/delete/', AccountDeleteView.as_view(), name="remove_account"), + path('/delete/', AccountDeleteView.as_view(), + name="remove_account"), 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('comment/remove/', DeleteCommentView.as_view(), + name="remove_comment"), - path('attachment/add/', AddAttachmentView.as_view(), name="add_attachment"), + path('attachment/add/', AddAttachmentView.as_view(), + name="add_attachment"), path('attachment/remove/', DeleteAttachmentsView.as_view(), name="remove_attachment"), ] diff --git a/accounts/views.py b/accounts/views.py index 408b2337..433fecb0 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,16 +1,14 @@ -import json from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.sites.shortcuts import get_current_site -from django.core.mail import EmailMessage from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.template.loader import render_to_string +from django.shortcuts import get_object_or_404, redirect from django.views.generic import ( CreateView, UpdateView, DetailView, TemplateView, View, DeleteView) -from accounts.forms import AccountForm, AccountCommentForm, AccountAttachmentForm +from accounts.forms import AccountForm, AccountCommentForm, \ + AccountAttachmentForm from accounts.models import Account, Tags from common.models import User, Comment, Attachments -from common.utils import INDCHOICES, COUNTRIES, CURRENCY_CODES, CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE +from common.utils import INDCHOICES, COUNTRIES, \ + CURRENCY_CODES, CASE_TYPE, PRIORITY_CHOICE, STATUS_CHOICE from contacts.models import Contact from opportunity.models import Opportunity, STAGES, SOURCES from cases.models import Case @@ -91,7 +89,8 @@ class CreateAccountView(LoginRequiredMixin, CreateView): def dispatch(self, request, *args, **kwargs): self.users = User.objects.filter(is_active=True).order_by('email') - return super(CreateAccountView, self).dispatch(request, *args, **kwargs) + return super( + CreateAccountView, self).dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(CreateAccountView, self).get_form_kwargs() @@ -160,10 +159,12 @@ class CreateAccountView(LoginRequiredMixin, CreateView): context["industries"] = INDCHOICES context["countries"] = COUNTRIES context["contact_count"] = Contact.objects.count() - context["lead_count"] = Lead.objects.exclude(status='dead').count() + context["leads"] = Lead.objects.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( - Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude(status='dead').count() + Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude(status='closed').count() return context @@ -178,18 +179,17 @@ class AccountDetailView(LoginRequiredMixin, DetailView): if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: if self.request.user != account_record.created_by: raise PermissionDenied - if ( + + comment_permission = True if ( self.request.user == account_record.created_by or self.request.user.is_superuser or self.request.user.role == 'ADMIN' - ): - comment_permission = True - else: - comment_permission = False + ) else False context.update({ "comments": account_record.accounts_comments.all(), "attachments": account_record.account_attachment.all(), - "opportunity_list": Opportunity.objects.filter(account=account_record), + "opportunity_list": Opportunity.objects.filter( + account=account_record), "contacts": account_record.contacts.all(), "users": User.objects.filter(is_active=True).order_by('email'), "cases": Case.objects.filter(account=account_record), @@ -212,7 +212,8 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): def dispatch(self, request, *args, **kwargs): self.users = User.objects.filter(is_active=True).order_by('email') - return super(AccountUpdateView, self).dispatch(request, *args, **kwargs) + return super(AccountUpdateView, self).dispatch( + request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(AccountUpdateView, self).get_form_kwargs() @@ -233,9 +234,6 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): # Save Account account_object = form.save(commit=False) account_object.save() - - all_members_list = [] - account_object.tags.clear() if self.request.POST.get('tags', ''): tags = self.request.POST.get("tags") @@ -284,10 +282,12 @@ class AccountUpdateView(LoginRequiredMixin, UpdateView): context["industries"] = INDCHOICES context["countries"] = COUNTRIES context["contact_count"] = Contact.objects.count() - context["lead_count"] = Lead.objects.exclude(status='dead').count() + context["leads"] = Lead.objects.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( - Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude(status='dead').count() + Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)).exclude(status='closed').count() return context @@ -322,7 +322,8 @@ class AddCommentView(LoginRequiredMixin, CreateView): return self.form_valid(form) return self.form_invalid(form) - data = {'error': "You don't have permission to comment for this account."} + data = { + 'error': "You don't have permission to comment for this account."} return JsonResponse(data) def form_valid(self, form): @@ -392,7 +393,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): self.account = get_object_or_404( Account, id=request.POST.get('accountid')) if ( - request.user == self.account.created_by or request.user.is_superuser or + request.user == self.account.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -402,7 +404,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): return self.form_invalid(form) data = { - 'error': "You don't have permission to add attachment for this account."} + 'error': "You don't have permission to add attachment \ + for this account."} return JsonResponse(data) def form_valid(self, form): @@ -415,7 +418,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): "attachment_id": attachment.id, "attachment": attachment.file_name, "attachment_url": attachment.attachment.url, - "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), + "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 @@ -431,12 +435,14 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): 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 == 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."} + data = { + 'error': "You don't have permission to delete this attachment."} return JsonResponse(data) diff --git a/cases/admin.py b/cases/admin.py deleted file mode 100644 index 48695622..00000000 --- a/cases/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from cases.models import Case - - -admin.site.register(Case) diff --git a/cases/models.py b/cases/models.py index acc408db..12f4d15a 100644 --- a/cases/models.py +++ b/cases/models.py @@ -26,7 +26,8 @@ class Case(models.Model): assigned_to = models.ManyToManyField( User, related_name='case_assigned_users') created_by = models.ForeignKey( - User, related_name='case_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='case_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) is_active = models.BooleanField(default=False) @@ -39,30 +40,44 @@ class Case(models.Model): def get_meetings(self): content_type = ContentType.objects.get(app_label="cases", model="case") return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Meeting", status="Planned") + content_type=content_type, + object_id=self.id, + event_type="Meeting", + status="Planned") def get_completed_meetings(self): content_type = ContentType.objects.get(app_label="cases", model="case") return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Meeting").exclude(status="Planned") + content_type=content_type, + object_id=self.id, + event_type="Meeting").exclude(status="Planned") def get_tasks(self): content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter(content_type=content_type, object_id=self.id, event_type="Task", status="Planned") + return Event.objects.filter(content_type=content_type, + object_id=self.id, + event_type="Task", + status="Planned") def get_completed_tasks(self): content_type = ContentType.objects.get(app_label="cases", model="case") return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Task").exclude(status="Planned") + content_type=content_type, + object_id=self.id, + event_type="Task").exclude(status="Planned") def get_calls(self): content_type = ContentType.objects.get(app_label="cases", model="case") - return Event.objects.filter(content_type=content_type, object_id=self.id, event_type="Call", status="Planned") + return Event.objects.filter(content_type=content_type, + object_id=self.id, event_type="Call", + status="Planned") def get_completed_calls(self): content_type = ContentType.objects.get(app_label="cases", model="case") return Event.objects.filter( - content_type=content_type, object_id=self.id, event_type="Call").exclude(status="Planned") + content_type=content_type, + object_id=self.id, + event_type="Call").exclude(status="Planned") def get_assigned_user(self): return User.objects.get(id=self.assigned_to.id) diff --git a/cases/templates/cases.html b/cases/templates/cases.html index fe7ff318..3b12d2ed 100644 --- a/cases/templates/cases.html +++ b/cases/templates/cases.html @@ -123,7 +123,7 @@ {% if request.user == case.created_by or request.user.role == "ADMIN" or request.user.is_superuser %} {% if case.status != 'Closed' %} - + {% endif %} {% endif %} diff --git a/cases/tests.py b/cases/tests.py index c1a71d1a..55b3dd89 100644 --- a/cases/tests.py +++ b/cases/tests.py @@ -10,29 +10,29 @@ class CaseCreation(object): def setUp(self): self.address = Address.objects.create( street="6th phase", - city="hyderabad", + city="LosVegas", postcode="506344", - country='IN') + country='US') self.user = User.objects.create( - first_name="raghu", - username='raghu', + first_name="robert", + username='robert', email='r@mp.com', role="ADMIN") - self.user.set_password('raghu') + self.user.set_password('robert') self.user.save() - self.client.login(email='r@mp.com', password='raghu') + self.client.login(email='r@mp.com', password='robert') - self.client.login(email='r@mp.com', password='raghu') + self.client.login(email='r@mp.com', password='robert') self.account = Account.objects.create( name="account", email="account@gmail.com", phone="12345", billing_address_line="", billing_street="6th phase", - billing_city="hyderabad", + billing_city="LosVegas", billing_postcode="506344", - billing_country='IN', + billing_country='US', website="www.account.com", description="account", created_by=self.user) @@ -44,7 +44,7 @@ class CaseCreation(object): ) self.case = Case.objects.create( - name="raghu", case_type="Problem", status="New", + name="robert", case_type="Problem", status="New", account=self.account, priority="Low", description="something", created_by=self.user, closed_on="2016-05-04") @@ -158,7 +158,7 @@ class CaseFormTestCase(CaseCreation, TestCase): def test_case_creation_same_name(self): response = self.client.post('/cases/create/', - {'name': 'raghu', + {'name': 'robert', 'case_type': 'type', 'status': 'status', 'account': self.account, diff --git a/cases/urls.py b/cases/urls.py index 56feea89..d9bf95f1 100644 --- a/cases/urls.py +++ b/cases/urls.py @@ -2,7 +2,8 @@ from django.urls import path from cases.views import ( CasesListView, create_case, CaseDetailView, update_case, RemoveCaseView, CloseCaseView, select_contact, GetCasesView, AddCommentView, - UpdateCommentView, DeleteCommentView, AddAttachmentView, DeleteAttachmentsView) + UpdateCommentView, DeleteCommentView, + AddAttachmentView, DeleteAttachmentsView) app_name = 'cases' @@ -11,7 +12,8 @@ urlpatterns = [ path('create/', create_case, name='add_case'), path('/view/', CaseDetailView.as_view(), name="view_case"), path('/edit_case/', update_case, name="edit_case"), - path('/remove/', RemoveCaseView.as_view(), name="remove_case"), + path('/remove/', + RemoveCaseView.as_view(), name="remove_case"), path('close_case/', CloseCaseView.as_view(), name="close_case"), path('select_contacts/', select_contact, name="select_contacts"), @@ -19,9 +21,11 @@ urlpatterns = [ 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('comment/remove/', + DeleteCommentView.as_view(), name="remove_comment"), - path('attachment/add/', AddAttachmentView.as_view(), name="add_attachment"), + path('attachment/add/', + AddAttachmentView.as_view(), name="add_attachment"), path('attachment/remove/', DeleteAttachmentsView.as_view(), name="remove_attachment"), ] diff --git a/cases/views.py b/cases/views.py index ef194ab9..20de21ff 100644 --- a/cases/views.py +++ b/cases/views.py @@ -6,7 +6,8 @@ from django.db.models import Q from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string -from django.views.generic import CreateView, DetailView, ListView, TemplateView, View +from django.views.generic import (CreateView, DetailView, + ListView, TemplateView, View) from cases.models import Case from cases.forms import CaseForm, CaseCommentForm, CaseAttachmentForm @@ -101,13 +102,15 @@ def create_case(request): for assigned_to_user in assigned_to_list: user = get_object_or_404(User, pk=assigned_to_user) mail_subject = 'Assigned to case.' - message = render_to_string('assigned_to/cases_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'case': case - }) - email = EmailMessage(mail_subject, message, to=[user.email]) + message = render_to_string( + 'assigned_to/cases_assigned.html', { + 'user': user, + 'domain': current_site.domain, + 'protocol': request.scheme, + 'case': case + }) + email = EmailMessage( + mail_subject, message, to=[user.email]) email.content_subtype = "html" email.send() @@ -126,29 +129,26 @@ def create_case(request): success_url = reverse("cases:add_case") if request.POST.get('from_account'): from_account = request.POST.get('from_account') - success_url = reverse("accounts:view_account", pk=from_account) + success_url = reverse("accounts:view_account", kwargs={ + 'pk': from_account}) return JsonResponse({'error': False, "success_url": success_url}) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - - else: - context = {} - context["case_form"] = form - context["accounts"] = accounts - if request.GET.get('view_account'): - context['account'] = get_object_or_404( - Account, id=request.GET.get('view_account')) - context["contacts"] = contacts - context["users"] = users - context["case_types"] = CASE_TYPE - context["case_priority"] = PRIORITY_CHOICE - context["case_status"] = STATUS_CHOICE - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - context["contacts_list"] = [ - int(i) for i in request.POST.getlist('contacts', []) if i] - return render(request, template_name, context) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["case_form"] = form + context["accounts"] = accounts + if request.GET.get('view_account'): + context['account'] = get_object_or_404( + Account, id=request.GET.get('view_account')) + context["contacts"] = contacts + context["users"] = users + context["case_types"] = CASE_TYPE + context["case_priority"] = PRIORITY_CHOICE + context["case_status"] = STATUS_CHOICE + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] + context["contacts_list"] = [ + int(i) for i in request.POST.getlist('contacts', []) if i] + return render(request, template_name, context) class CaseDetailView(LoginRequiredMixin, DetailView): @@ -162,7 +162,8 @@ class CaseDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super(CaseDetailView, self).get_context_data(**kwargs) - user_assgn_list = [assigned_to.id for assigned_to in context['object'].assigned_to.all()] + user_assgn_list = [ + assigned_to.id for assigned_to in context['object'].assigned_to.all()] if self.request.user == context['object'].created_by: user_assgn_list.append(self.request.user.id) if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: @@ -183,7 +184,6 @@ class CaseDetailView(LoginRequiredMixin, DetailView): def update_case(request, pk): case_object = Case.objects.filter(pk=pk).first() - users = User.objects.filter(is_active=True).order_by('email') accounts = Account.objects.filter(status="open") contacts = Contact.objects.all() if request.user.role != "ADMIN" and not request.user.is_superuser: @@ -191,14 +191,17 @@ def update_case(request, pk): created_by=request.user) contacts = Contact.objects.filter( Q(assigned_to__in=[request.user]) | Q(created_by=request.user)) - kwargs_data = {"assigned_to": users, "account": accounts, "contacts": contacts} + kwargs_data = { + "assigned_to": User.objects.filter(is_active=True).order_by('email'), + "account": accounts, "contacts": contacts} form = CaseForm(instance=case_object, **kwargs_data) - template_name = "create_cases.html" if request.POST: - form = CaseForm(request.POST, request.FILES, instance=case_object, **kwargs_data) + form = CaseForm(request.POST, request.FILES, + instance=case_object, **kwargs_data) if form.is_valid(): - assigned_to_ids = case_object.assigned_to.all().values_list('id', flat=True) + assigned_to_ids = case_object.assigned_to.all().values_list( + 'id', flat=True) case_obj = form.save(commit=False) case_obj.contacts.clear() case_obj.save() @@ -210,18 +213,20 @@ def update_case(request, pk): assigned_form_users = form.cleaned_data.get( 'assigned_to').values_list('id', flat=True) all_members_list = list( - set(list(assigned_form_users)) - set(list(assigned_to_ids))) + set(list(assigned_form_users)) - + set(list(assigned_to_ids))) - if len(all_members_list): + 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 case.' - message = render_to_string('assigned_to/cases_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': request.scheme, - 'case': case_obj - }) + message = render_to_string( + 'assigned_to/cases_assigned.html', { + 'user': user, + 'domain': current_site.domain, + 'protocol': request.scheme, + 'case': case_obj + }) email = EmailMessage( mail_subject, message, to=[user.email]) email.content_subtype = "html" @@ -238,7 +243,8 @@ def update_case(request, pk): success_url = reverse("cases:list") if request.POST.get('from_account'): from_account = request.POST.get('from_account') - success_url = reverse("accounts:view_account", pk=from_account) + success_url = reverse("accounts:view_account", kwargs={ + 'pk': from_account}) if request.FILES.get('case_attachment'): attachment = Attachments() attachment.created_by = request.user @@ -249,37 +255,33 @@ def update_case(request, pk): attachment.save() return JsonResponse({'error': False, 'success_url': success_url}) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["case_obj"] = case_object + user_assgn_list = [ + assgined_to.id for assgined_to in context["case_obj"].assigned_to.all()] - else: - return JsonResponse({'error': True, 'errors': form.errors}) + if request.user == case_object.created_by: + user_assgn_list.append(request.user.id) + if request.user.role != "ADMIN" and not request.user.is_superuser: + if request.user.id not in user_assgn_list: + raise PermissionDenied + context["case_form"] = form + context["accounts"] = accounts + if request.GET.get('view_account'): + context['account'] = get_object_or_404( + Account, id=request.GET.get('view_account')) + context["contacts"] = contacts + context["users"] = kwargs_data['assigned_to'] + context["case_types"] = CASE_TYPE + context["case_priority"] = PRIORITY_CHOICE + context["case_status"] = STATUS_CHOICE + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] + context["contacts_list"] = [ + int(i) for i in request.POST.getlist('contacts', []) if i] - else: - context = {} - context["case_obj"] = case_object - user_assgn_list = [ - assgined_to.id for assgined_to in context["case_obj"].assigned_to.all()] - - if request.user == case_object.created_by: - user_assgn_list.append(request.user.id) - if request.user.role != "ADMIN" and not request.user.is_superuser: - if request.user.id not in user_assgn_list: - raise PermissionDenied - context["case_form"] = form - context["accounts"] = accounts - if request.GET.get('view_account'): - context['account'] = get_object_or_404( - Account, id=request.GET.get('view_account')) - context["contacts"] = contacts - context["users"] = users - context["case_types"] = CASE_TYPE - context["case_priority"] = PRIORITY_CHOICE - context["case_status"] = STATUS_CHOICE - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - context["contacts_list"] = [ - int(i) for i in request.POST.getlist('contacts', []) if i] - - return render(request, template_name, context) + return render(request, "create_cases.html", context) class RemoveCaseView(LoginRequiredMixin, View): @@ -291,18 +293,20 @@ class RemoveCaseView(LoginRequiredMixin, View): account = request.GET.get('view_account') return redirect("accounts:view_account", pk=account) if ( - self.request.user.role == "ADMIN" or self.request.user.is_superuser or + self.request.user.role == "ADMIN" or + self.request.user.is_superuser or self.request.user == self.object.created_by ): self.object.delete() return redirect("cases:list") - else: - raise PermissionDenied + raise PermissionDenied def post(self, request, *args, **kwargs): case_id = kwargs.get("case_id") self.object = get_object_or_404(Case, id=case_id) - if self.request.user.role == "ADMIN" or self.request.user.is_superuser or self.request.user == self.object.created_by: + if (self.request.user.role == "ADMIN" or + self.request.user.is_superuser or + self.request.user == self.object.created_by): self.object.delete() if request.is_ajax(): return JsonResponse({'error': False}) @@ -310,8 +314,7 @@ class RemoveCaseView(LoginRequiredMixin, View): Q(assigned_to__in=[request.user]) | Q(created_by=request.user)).distinct().count() data = {"case_id": case_id, "count": count} return JsonResponse(data) - else: - raise PermissionDenied + raise PermissionDenied class CloseCaseView(LoginRequiredMixin, View): @@ -320,15 +323,15 @@ class CloseCaseView(LoginRequiredMixin, View): case_id = request.POST.get("case_id") self.object = get_object_or_404(Case, id=case_id) if ( - self.request.user.role == "ADMIN" or self.request.user.is_superuser or + self.request.user.role == "ADMIN" or + self.request.user.is_superuser or self.request.user == self.object.created_by ): self.object.status = "Closed" self.object.save() data = {'status': "Closed", "cid": case_id} return JsonResponse(data) - else: - raise PermissionDenied + raise PermissionDenied def select_contact(request): @@ -358,7 +361,8 @@ class AddCommentView(LoginRequiredMixin, CreateView): self.case = get_object_or_404(Case, id=request.POST.get('caseid')) if ( request.user in self.case.assigned_to.all() or - request.user == self.case.created_by or request.user.is_superuser or + request.user == self.case.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -392,7 +396,8 @@ class UpdateCommentView(LoginRequiredMixin, View): self.comment_obj = get_object_or_404( Comment, id=request.POST.get("commentid")) if ( - request.user == self.comment_obj.commented_by or request.user.is_superuser or + request.user == self.comment_obj.commented_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = CaseCommentForm(request.POST, instance=self.comment_obj) @@ -422,7 +427,8 @@ class DeleteCommentView(LoginRequiredMixin, View): self.object = get_object_or_404( Comment, id=request.POST.get("comment_id")) if ( - request.user == self.object.commented_by or request.user.is_superuser or + request.user == self.object.commented_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): self.object.delete() @@ -443,7 +449,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): self.case = get_object_or_404(Case, id=request.POST.get('caseid')) if ( request.user in self.case.assigned_to.all() or - request.user == self.case.created_by or request.user.is_superuser or + request.user == self.case.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -453,7 +460,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): return self.form_invalid(form) data = {"error": True, - "errors": "You don't have permission to add attachment for this case."} + "errors": + "You don't have permission to add attachment for this case."} return JsonResponse(data) def form_valid(self, form): @@ -468,7 +476,9 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): "attachment_id": attachment.id, "attachment": attachment.file_name, "attachment_url": attachment.attachment.url, - "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), + "download_url": + reverse('common:download_attachment', + kwargs={'pk': attachment.id}), "created_on": attachment.created_on, "created_by": attachment.created_by.email, "message": "attachment Created", @@ -477,7 +487,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView): }) def form_invalid(self, form): - return JsonResponse({"error": True, "errors": form['attachment'].errors}) + return JsonResponse({"error": True, + "errors": form['attachment'].errors}) class DeleteAttachmentsView(LoginRequiredMixin, View): @@ -486,7 +497,8 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): 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 == self.object.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): self.object.delete() @@ -495,5 +507,6 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): return JsonResponse(data) data = {"error": True, - "errors": "You don't have permission to delete this attachment."} + "errors": + "You don't have permission to delete this attachment."} return JsonResponse(data) diff --git a/common/forms.py b/common/forms.py index 965dd6e9..10dc5705 100644 --- a/common/forms.py +++ b/common/forms.py @@ -104,13 +104,15 @@ class UserForm(forms.ModelForm): email = self.cleaned_data.get("email") if self.instance.id: if self.instance.email != email: - if not User.objects.filter(email=self.cleaned_data.get("email")).exists(): + if not User.objects.filter( + email=self.cleaned_data.get("email")).exists(): return self.cleaned_data.get("email") raise forms.ValidationError('Email already exists') else: return self.cleaned_data.get("email") else: - if not User.objects.filter(email=self.cleaned_data.get("email")).exists(): + if not User.objects.filter( + email=self.cleaned_data.get("email")).exists(): return self.cleaned_data.get("email") raise forms.ValidationError('User already exists with this email') @@ -173,7 +175,8 @@ class PasswordResetEmailForm(PasswordResetForm): def clean_email(self): email = self.cleaned_data.get('email') - if not User.objects.filter(email__iexact=email, is_active=True).exists(): + if not User.objects.filter(email__iexact=email, + is_active=True).exists(): raise forms.ValidationError("User doesn't exist with this Email") return email @@ -216,10 +219,9 @@ def find_urls(string): website_regex_port = "^https?://[A-Za-z0-9.-]+\.[A-Za-z]{2,63}:[0-9]{2,4}$" url = re.findall(website_regex, string) url_port = re.findall(website_regex_port, string) - if len(url) > 0 and url[0] != '': + if url and url[0] != '': return url - else: - return url_port + return url_port class APISettingsForm(forms.ModelForm): @@ -243,9 +245,11 @@ class APISettingsForm(forms.ModelForm): def clean_website(self): website = self.data.get('website') - if website and not (website.startswith('http://') or website.startswith('https://')): + if website and not (website.startswith('http://') or + website.startswith('https://')): raise forms.ValidationError("Please provide valid schema") if not len(find_urls(website)) > 0: raise forms.ValidationError( - "Please provide a valid URL with schema and without trailing slash - Example: http://google.com") + "Please provide a valid URL with schema and without trailing \ + slash - Example: http://google.com") return website diff --git a/common/models.py b/common/models.py index 10f77f7d..24e7c763 100644 --- a/common/models.py +++ b/common/models.py @@ -3,10 +3,13 @@ import os import time from django.db import models from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager +from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin, + UserManager) from common.templatetags.common_tags import ( - is_document_file_image, is_document_file_audio, is_document_file_video, is_document_file_pdf, - is_document_file_code, is_document_file_text, is_document_file_sheet, is_document_file_zip + is_document_file_image, is_document_file_audio, + is_document_file_video, is_document_file_pdf, + is_document_file_code, is_document_file_text, + is_document_file_sheet, is_document_file_zip ) from common.utils import COUNTRIES, ROLES @@ -51,7 +54,7 @@ class User(AbstractBaseUser, PermissionsMixin): full_name = self.email return full_name - def __unicode__(self): + def __str__(self): return self.email class Meta: @@ -113,15 +116,25 @@ class Comment(models.Model): commented_by = models.ForeignKey( User, on_delete=models.CASCADE, blank=True, null=True) account = models.ForeignKey( - 'accounts.Account', blank=True, null=True, related_name="accounts_comments", on_delete=models.CASCADE) - lead = models.ForeignKey('leads.Lead', blank=True, null=True, - related_name="leads", on_delete=models.CASCADE) + 'accounts.Account', blank=True, null=True, + related_name="accounts_comments", + on_delete=models.CASCADE) + lead = models.ForeignKey('leads.Lead', + blank=True, null=True, + related_name="leads", + on_delete=models.CASCADE) opportunity = models.ForeignKey( - 'opportunity.Opportunity', blank=True, null=True, related_name="opportunity_comments", on_delete=models.CASCADE) + 'opportunity.Opportunity', blank=True, + null=True, related_name="opportunity_comments", + on_delete=models.CASCADE) contact = models.ForeignKey( - 'contacts.Contact', blank=True, null=True, related_name="contact_comments", on_delete=models.CASCADE) + 'contacts.Contact', blank=True, + null=True, related_name="contact_comments", + on_delete=models.CASCADE) user = models.ForeignKey( - 'User', blank=True, null=True, related_name="user_comments", on_delete=models.CASCADE) + 'User', blank=True, null=True, + related_name="user_comments", + on_delete=models.CASCADE) def get_files(self): return Comment_Files.objects.filter(comment_id=self) @@ -142,22 +155,30 @@ class Comment_Files(models.Model): class Attachments(models.Model): created_by = models.ForeignKey( - User, related_name='attachment_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='attachment_created_by', + on_delete=models.SET_NULL, null=True) file_name = models.CharField(max_length=60) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) attachment = models.FileField( max_length=1001, upload_to='attachments/%Y/%m/') lead = models.ForeignKey( - 'leads.Lead', null=True, blank=True, related_name='lead_attachment', on_delete=models.CASCADE) + 'leads.Lead', null=True, + blank=True, related_name='lead_attachment', + on_delete=models.CASCADE) account = models.ForeignKey( - 'accounts.Account', null=True, blank=True, related_name='account_attachment', on_delete=models.CASCADE) + 'accounts.Account', null=True, blank=True, + related_name='account_attachment', on_delete=models.CASCADE) contact = models.ForeignKey( - 'contacts.Contact', on_delete=models.CASCADE, related_name='contact_attachment', blank=True, null=True) + 'contacts.Contact', on_delete=models.CASCADE, + related_name='contact_attachment', + blank=True, null=True) opportunity = models.ForeignKey( - 'opportunity.Opportunity', blank=True, null=True, on_delete=models.CASCADE, + 'opportunity.Opportunity', blank=True, + null=True, on_delete=models.CASCADE, related_name='opportunity_attachment') case = models.ForeignKey( - 'cases.Case', blank=True, null=True, on_delete=models.CASCADE, related_name='case_attachment') + 'cases.Case', blank=True, null=True, + on_delete=models.CASCADE, related_name='case_attachment') def file_type(self): name_ext_list = self.attachment.url.split(".") @@ -185,8 +206,7 @@ class Attachments(models.Model): def get_file_type_display(self): if self.attachment: return self.file_type()[1] - else: - return None + return None def document_path(self, filename): @@ -204,7 +224,8 @@ class Document(models.Model): title = models.CharField(max_length=1000, blank=True, null=True) document_file = models.FileField(upload_to=document_path, max_length=5000) created_by = models.ForeignKey( - User, related_name='document_uploaded', on_delete=models.SET_NULL, null=True) + User, related_name='document_uploaded', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(auto_now_add=True) status = models.CharField( choices=DOCUMENT_STATUS_CHOICE, max_length=64, default='active') @@ -249,7 +270,8 @@ class APISettings(models.Model): User, related_name='lead_assignee_users') tags = models.ManyToManyField('accounts.Tags', blank=True) created_by = models.ForeignKey( - User, related_name='settings_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='settings_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/common/templates/doc_list.html b/common/templates/doc_list.html index 98b7cbb6..4515aec4 100644 --- a/common/templates/doc_list.html +++ b/common/templates/doc_list.html @@ -183,4 +183,5 @@ $('#docs_filter').submit(); }); -{% endblock js_block %} \ No newline at end of file +{% endblock js_block %} + diff --git a/common/templates/settings/create.html b/common/templates/settings/create.html index 281583d8..65321aa5 100644 --- a/common/templates/settings/create.html +++ b/common/templates/settings/create.html @@ -148,3 +148,4 @@ {% endblock js_block %} + diff --git a/common/templates/settings/list.html b/common/templates/settings/list.html index ce5307ae..33dff112 100644 --- a/common/templates/settings/list.html +++ b/common/templates/settings/list.html @@ -85,4 +85,5 @@ window.location = $(this).attr('href') }); -{% endblock js_block %} \ No newline at end of file +{% endblock js_block %} + diff --git a/common/tests.py b/common/tests.py index 0b229a3e..e25ad61d 100644 --- a/common/tests.py +++ b/common/tests.py @@ -8,13 +8,29 @@ from common.forms import * class ObjectsCreation(object): def setUp(self): self.client = Client() - self.user = User.objects.create(first_name="admin", username='admin', email='admin@micropyramid.com', - is_staff=True, is_admin=True, is_superuser=True, is_active=True, role='ADMIN') + # self.user = User.objects.create(first_name="admin", + # username='admin', + # email='admin@micropyramid.com', + # is_staff=True, + # is_admin=True, + # is_superuser=True, is_active=True) + + self.user = User.objects.create(first_name="admin", + username='admin', + email='admin@micropyramid.com', + is_staff=True, + is_admin=True, + is_superuser=True, + is_active=True, role='ADMIN') + self.user.set_password('admin123') self.user.save() - self.user = User.objects.create(first_name="pavan", username='pavan', email='pavan@micropyramid.com', - is_staff=True, is_admin=True, is_superuser=True, is_active=False) - self.user.set_password('pavan123') + self.user = User.objects.create(first_name="paul", + username='paul', + email='paul@micropyramid.com', + is_staff=True, is_admin=True, + is_superuser=True, is_active=False) + self.user.set_password('paul123') self.user.save() # self.user user_login = self.client.login( @@ -92,7 +108,7 @@ class ForgotPasswordViewTestCase(ObjectsCreation, TestCase): class LoginViewTestCase(ObjectsCreation, TestCase): - # user2 = User.objects.create(first_name="pavan", username='pavan', email='pavan@micropyramid.com', + # user2 = User.objects.create(first_name="paul", username='paul', email='paul@micropyramid.com', # is_staff=True, is_admin=True, is_superuser=True, is_active=False) def test_login_post(self): self.client.logout() @@ -133,20 +149,20 @@ class LoginViewTestCase(ObjectsCreation, TestCase): class UserTestCase(ObjectsCreation, TestCase): def test_user_create_url(self): response = self.client.get('/users/create/', { - 'first_name': 'meghana', - 'last_name': "reddy", - 'username': 'meghana', - 'email': 'meghana@micropyramid.com', - 'password': 'meghana123'}) + 'first_name': 'micheal', + 'last_name': "clark", + 'username': 'micheal', + 'email': 'micheal@micropyramid.com', + 'password': 'micheal123'}) self.assertEqual(response.status_code, 200) def test_user_create_html(self): response = self.client.get('/users/create/', { - 'first_name': 'meghana', + 'first_name': 'micheal', 'last_name': "", - 'username': 'meghana', + 'username': 'micheal', 'email': '', - 'password': 'meghana123'}) + 'password': 'micheal123'}) self.assertTemplateUsed(response, 'create.html') @@ -158,7 +174,7 @@ class UserListTestCase(ObjectsCreation, TestCase): response = self.client.get('/users/list/') # get_img_url = self.users.filter() get_user = User.objects.get(email='admin@micropyramid.com') - self.assertEqual(get_user.email, get_user.__unicode__()) + self.assertEqual(get_user.email, get_user.__str__()) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'list.html') @@ -193,9 +209,11 @@ class UserUpdateTestCase(ObjectsCreation, TestCase): def test_accounts_update_post(self): response = self.client.post('/users/' + str(self.user.id) + '/edit/', - {'first_name': "meghana", 'user_name': 'meghana', + {'first_name': "micheal", + 'user_name': 'micheal', 'email': "abc@micropyramid", - 'role': "USER", 'is_superuser': False}) + 'role': "USER", + 'is_superuser': False}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'create.html') @@ -226,6 +244,14 @@ class ProfileViewTestCase(ObjectsCreation, TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + # def test_context_data(self): + # user_login = self.client.login( + # username='admin@micropyramid.com', password='admin123') + # url = "/profile/" + # response = self.client.get(url) + # print(response, "hello") + # self.assertContains(response,'admin@micropyramid.com') + class UserDetailView(ObjectsCreation, TestCase): def test_user_detail(self): @@ -246,11 +272,14 @@ class DocumentDetailView(ObjectsCreation, TestCase): class CreateCommentFile(TestCase): def test_invalid_user_form(self): - fields = ['email', 'first_name', 'last_name', - 'username', 'role', 'profile_pic'] - user1 = User.objects.create(username='teja', - first_name='teja', - last_name='reddy', + fields = ['email', + 'first_name', + 'last_name', + 'username', 'role', + 'profile_pic'] + user1 = User.objects.create(username='robert', + first_name='robert', + last_name='clark', email='tr@mp.com', role='USER', profile_pic="", @@ -261,7 +290,7 @@ class CreateCommentFile(TestCase): 'profile_pic': user1.profile_pic, 'password': user1.password} form = UserForm(data=data) - userr = User.objects.get(username='teja') + userr = User.objects.get(username='robert') # print(userr) self.assertEqual(len(userr.password), 3) self.assertFalse(form.is_valid()) diff --git a/common/urls.py b/common/urls.py index 17e3386b..ab6beadd 100644 --- a/common/urls.py +++ b/common/urls.py @@ -2,11 +2,16 @@ from django.contrib.auth import views as auth_views from django.urls import path from common.views import ( - HomeView, LoginView, ForgotPasswordView, LogoutView, ChangePasswordView, ProfileView, - UsersListView, CreateUserView, UpdateUserView, UserDetailView, UserDeleteView, PasswordResetView, - DocumentListView, document_create, document_update, DocumentDetailView, DocumentDeleteView, - download_document, change_user_status, download_attachment, add_comment, edit_comment, remove_comment, - api_settings, add_api_settings, view_api_settings, update_api_settings, delete_api_settings, + HomeView, LoginView, ForgotPasswordView, LogoutView, + ChangePasswordView, ProfileView, + UsersListView, CreateUserView, UpdateUserView, UserDetailView, + UserDeleteView, PasswordResetView, + DocumentListView, document_create, document_update, + DocumentDetailView, DocumentDeleteView, + download_document, change_user_status, download_attachment, + add_comment, edit_comment, remove_comment, + api_settings, add_api_settings, view_api_settings, + update_api_settings, delete_api_settings, change_passsword_by_admin, google_login ) from django.conf.urls.static import static @@ -19,9 +24,11 @@ app_name = 'common' urlpatterns = [ path('', HomeView.as_view(), name='home'), path('login/', LoginView.as_view(), name='login'), - path('forgot-password/', ForgotPasswordView.as_view(), name='forgot_password'), + path('forgot-password/', + ForgotPasswordView.as_view(), name='forgot_password'), path('logout/', LogoutView.as_view(), name='logout'), - path('change-password/', ChangePasswordView.as_view(), name='change_password'), + path('change-password/', + ChangePasswordView.as_view(), name='change_password'), path('profile/', ProfileView.as_view(), name='profile'), # User views @@ -29,14 +36,17 @@ urlpatterns = [ path('users/create/', CreateUserView.as_view(), name='create_user'), path('users//edit/', UpdateUserView.as_view(), name="edit_user"), path('users//view/', UserDetailView.as_view(), name='view_user'), - path('users//delete/', UserDeleteView.as_view(), name='remove_user'), + path('users//delete/', + UserDeleteView.as_view(), name='remove_user'), path( 'password-reset/', PasswordResetView.as_view(), name='password_reset'), path('password-reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), path( - 'reset///', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), + 'reset///', + auth_views.PasswordResetConfirmView.as_view(), + name='password_reset_confirm'), path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), @@ -44,7 +54,8 @@ urlpatterns = [ path('documents/list/', DocumentListView.as_view(), name='doc_list'), path('documents/create/', document_create, name='create_doc'), path('documents//edit/', document_update, name="edit_doc"), - path('documents//view/', DocumentDetailView.as_view(), name='view_doc'), + path('documents//view/', + DocumentDetailView.as_view(), name='view_doc'), path('documents//delete/', DocumentDeleteView.as_view(), name='remove_doc'), @@ -56,7 +67,8 @@ urlpatterns = [ path('attachments//download/', download_attachment, name='download_attachment'), - path('user/status//', change_user_status, name='change_user_status'), + path('user/status//', + change_user_status, name='change_user_status'), path('comment/add/', add_comment, name="add_comment"), path('comment//edit/', edit_comment, name="edit_comment"), @@ -65,7 +77,8 @@ urlpatterns = [ # settings path('api/settings/', api_settings, name="api_settings"), path('api/settings/add/', add_api_settings, name="add_api_settings"), - path('api/settings//', view_api_settings, name="view_api_settings"), + path('api/settings//', + view_api_settings, name="view_api_settings"), path('api/settings//update/', update_api_settings, name="update_api_settings"), path('api/settings//delete/', diff --git a/common/utils.py b/common/utils.py index 7eb33001..b161bb9f 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,3 +1,4 @@ +import pytz from django.utils.translation import ugettext_lazy as _ @@ -51,7 +52,7 @@ LEAD_STATUS = ( ('in process', 'In Process'), ('converted', 'Converted'), ('recycled', 'Recycled'), - ('dead', 'Closed') + ('closed', 'Closed') ) @@ -579,3 +580,11 @@ def get_client_ip(request): else: ip = request.META.get('REMOTE_ADDR') return ip + + +def convert_to_custom_timezone(custom_date, custom_timezone, to_utc=False): + user_time_zone = pytz.timezone(custom_timezone) + if to_utc: + custom_date = user_time_zone.localize(custom_date.replace(tzinfo=None)) + user_time_zone = pytz.UTC + return custom_date.astimezone(user_time_zone) diff --git a/common/views.py b/common/views.py index 6862f7bb..6c05338f 100644 --- a/common/views.py +++ b/common/views.py @@ -7,13 +7,20 @@ from django.db.models import Q from django.core.mail import EmailMessage from django.contrib.auth.hashers import check_password from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin -from django.http import HttpResponseRedirect, JsonResponse, HttpResponse, Http404 +from django.http import (HttpResponseRedirect, + JsonResponse, HttpResponse, + Http404) from django.shortcuts import render, redirect, get_object_or_404 from django.views.generic import ( CreateView, UpdateView, DetailView, TemplateView, View, DeleteView) -from common.models import User, Document, Attachments, Comment, APISettings, Google +from common.models import (User, Document, Attachments, + Comment, + APISettings, + Google) from common.forms import ( - UserForm, LoginForm, ChangePasswordForm, PasswordResetEmailForm, DocumentForm, UserCommentForm, + UserForm, LoginForm, + ChangePasswordForm, PasswordResetEmailForm, + DocumentForm, UserCommentForm, APISettingsForm ) from django.contrib.auth.views import PasswordResetView @@ -26,6 +33,8 @@ from accounts.models import Account, Tags from leads.models import Lead from django.template.loader import render_to_string from django.core.exceptions import PermissionDenied +import boto3 +import botocore def handler404(request, exception): @@ -45,7 +54,8 @@ class AdminRequiredMixin(AccessMixin): if not request.user.role == "ADMIN": if not request.user.is_superuser: return self.handle_no_permission() - return super(AdminRequiredMixin, self).dispatch(request, *args, **kwargs) + return super(AdminRequiredMixin, self).dispatch( + request, *args, **kwargs) class HomeView(LoginRequiredMixin, TemplateView): @@ -55,18 +65,21 @@ class HomeView(LoginRequiredMixin, TemplateView): context = super(HomeView, self).get_context_data(**kwargs) accounts = Account.objects.filter(status="open") contacts = Contact.objects.all() - leads = Lead.objects.exclude(status='converted' and 'dead') + leads = Lead.objects.exclude(status='converted') opportunities = Opportunity.objects.all() if self.request.user.role == "ADMIN" or self.request.user.is_superuser: pass else: accounts = accounts.filter(created_by=self.request.user.id) contacts = contacts.filter( - Q(assigned_to__id__in=[self.request.user.id]) | Q(created_by=self.request.user.id)) + Q(assigned_to__id__in=[self.request.user.id]) | + Q(created_by=self.request.user.id)) leads = leads.filter( - Q(assigned_to__id__in=[self.request.user.id]) | Q(created_by=self.request.user.id)).exclude(status='dead') + Q(assigned_to__id__in=[self.request.user.id]) | + Q(created_by=self.request.user.id)).exclude(status='closed') opportunities = opportunities.filter( - Q(assigned_to__id__in=[self.request.user.id]) | Q(created_by=self.request.user.id)) + Q(assigned_to__id__in=[self.request.user.id]) | + Q(created_by=self.request.user.id)) context["accounts"] = accounts context["contacts_count"] = contacts.count() @@ -88,7 +101,8 @@ class ChangePasswordView(LoginRequiredMixin, TemplateView): form = ChangePasswordForm(request.POST) if form.is_valid(): user = request.user - if not check_password(request.POST.get('CurrentPassword'), user.password): + if not check_password(request.POST.get('CurrentPassword'), + user.password): error = "Invalid old password" else: user.set_password(request.POST.get('Newpassword')) @@ -97,13 +111,16 @@ class ChangePasswordView(LoginRequiredMixin, TemplateView): return HttpResponseRedirect('/') else: errors = form.errors - return render(request, "change_password.html", {'error': error, 'errors': errors, 'change_password_form': form}) + return render(request, "change_password.html", + {'error': error, 'errors': errors, + 'change_password_form': form}) class ProfileView(LoginRequiredMixin, TemplateView): template_name = "profile.html" def get_context_data(self, **kwargs): + context = super(ProfileView, self).get_context_data(**kwargs) context["user_obj"] = self.request.user return context @@ -138,41 +155,41 @@ class LoginView(TemplateView): if user is not None: login(request, user) return HttpResponseRedirect('/') - else: - return render(request, "login.html", { - "ENABLE_GOOGLE_LOGIN": settings.ENABLE_GOOGLE_LOGIN, - "GP_CLIENT_SECRET": settings.GP_CLIENT_SECRET, - "GP_CLIENT_ID": settings.GP_CLIENT_ID, - "error": True, - "message": "Your username and password didn't match. Please try again." - }) - else: return render(request, "login.html", { "ENABLE_GOOGLE_LOGIN": settings.ENABLE_GOOGLE_LOGIN, "GP_CLIENT_SECRET": settings.GP_CLIENT_SECRET, "GP_CLIENT_ID": settings.GP_CLIENT_ID, "error": True, - "message": "Your Account is inactive. Please Contact Administrator" + "message": + "Your username and password didn't match. \ + Please try again." }) - else: return render(request, "login.html", { "ENABLE_GOOGLE_LOGIN": settings.ENABLE_GOOGLE_LOGIN, "GP_CLIENT_SECRET": settings.GP_CLIENT_SECRET, "GP_CLIENT_ID": settings.GP_CLIENT_ID, "error": True, - "message": "Your Account is not Found. Please Contact Administrator" + "message": + "Your Account is inactive. Please Contact Administrator" }) - - else: return render(request, "login.html", { "ENABLE_GOOGLE_LOGIN": settings.ENABLE_GOOGLE_LOGIN, "GP_CLIENT_SECRET": settings.GP_CLIENT_SECRET, "GP_CLIENT_ID": settings.GP_CLIENT_ID, - # "error": True, - # "message": "Your username and password didn't match. Please try again." - "form": form + "error": True, + "message": + "Your Account is not Found. Please Contact Administrator" }) + return render(request, "login.html", { + "ENABLE_GOOGLE_LOGIN": settings.ENABLE_GOOGLE_LOGIN, + "GP_CLIENT_SECRET": settings.GP_CLIENT_SECRET, + "GP_CLIENT_ID": settings.GP_CLIENT_ID, + # "error": True, + # "message": "Your username and password didn't match. Please try again." + "form": form + }) + class ForgotPasswordView(TemplateView): template_name = "forgot_password.html" @@ -264,7 +281,8 @@ class UserDetailView(AdminRequiredMixin, DetailView): users_data.append(assigned_dict) context.update({ "user_obj": user_obj, - "opportunity_list": Opportunity.objects.filter(assigned_to=user_obj.id), + "opportunity_list": + Opportunity.objects.filter(assigned_to=user_obj.id), "contacts": Contact.objects.filter(assigned_to=user_obj.id), "cases": Case.objects.filter(assigned_to=user_obj.id), # "accounts": Account.objects.filter(assigned_to=user_obj.id), @@ -282,14 +300,16 @@ class UpdateUserView(LoginRequiredMixin, UpdateView): def form_valid(self, form): user = form.save(commit=False) if self.request.is_ajax(): - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): if self.request.user.id != self.object.id: data = {'error_403': True, 'error': True} return JsonResponse(data) if user.role == "USER": user.is_superuser = False user.save() - if self.request.user.role == "ADMIN" and self.request.user.is_superuser: + if (self.request.user.role == "ADMIN" and + self.request.user.is_superuser): if self.request.is_ajax(): data = {'success_url': reverse_lazy( 'common:users_list'), 'error': False} @@ -349,17 +369,14 @@ def document_create(request): data = {'success_url': reverse_lazy( 'common:doc_list'), 'error': False} return JsonResponse(data) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - else: - context = {} - context["doc_form"] = form - context["users"] = users - context["sharedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - context["errors"] = form.errors - return render(request, template_name, context) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["doc_form"] = form + context["users"] = users + context["sharedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] + context["errors"] = form.errors + return render(request, template_name, context) class DocumentListView(LoginRequiredMixin, TemplateView): @@ -373,9 +390,12 @@ class DocumentListView(LoginRequiredMixin, TemplateView): queryset = queryset else: if self.request.user.documents(): - doc_ids = self.request.user.documents().values_list('id', flat=True) + doc_ids = self.request.user.documents().values_list('id', + flat=True) shared_ids = queryset.filter( - Q(status='active') & Q(shared_to__id__in=[self.request.user.id])).values_list('id', flat=True) + Q(status='active') & + Q(shared_to__id__in=[self.request.user.id])).values_list( + 'id', flat=True) queryset = queryset.filter( Q(id__in=doc_ids) | Q(id__in=shared_ids)) else: @@ -407,7 +427,8 @@ class DocumentListView(LoginRequiredMixin, TemplateView): search = False if ( - self.request.POST.get('doc_name') or self.request.POST.get('status') or + self.request.POST.get('doc_name') or + self.request.POST.get('status') or self.request.POST.get('shared_to') ): search = True @@ -439,7 +460,8 @@ def document_update(request, pk): document = Document.objects.filter(id=pk).first() if request.POST: - form = DocumentForm(request.POST, request.FILES, instance=document, users=users) + form = DocumentForm(request.POST, request.FILES, + instance=document, users=users) if form.is_valid(): doc = form.save(commit=False) doc.save() @@ -451,21 +473,17 @@ def document_update(request, pk): data = {'success_url': reverse_lazy( 'common:doc_list'), 'error': False} return JsonResponse(data) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - - else: - context = {} - context["doc_obj"] = document - context["doc_form"] = form - context["doc_file_name"] = context["doc_obj"].document_file.name.split( - "/")[-1] - context["users"] = users - context["sharedto_list"] = [ - int(i) for i in request.POST.getlist('shared_to', []) if i] - context["errors"] = form.errors - return render(request, template_name, context) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["doc_obj"] = document + context["doc_form"] = form + context["doc_file_name"] = context["doc_obj"].document_file.name.split( + "/")[-1] + context["users"] = users + context["sharedto_list"] = [ + int(i) for i in request.POST.getlist('shared_to', []) if i] + context["errors"] = form.errors + return render(request, template_name, context) class UpdateDocumentView(LoginRequiredMixin, UpdateView): @@ -479,7 +497,8 @@ class UpdateDocumentView(LoginRequiredMixin, UpdateView): raise PermissionDenied self.users = User.objects.filter(is_active=True).order_by('email') - return super(UpdateDocumentView, self).dispatch(request, *args, **kwargs) + return super(UpdateDocumentView, self).dispatch( + request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(UpdateDocumentView, self).get_form_kwargs() @@ -526,10 +545,12 @@ class DocumentDetailView(LoginRequiredMixin, DetailView): def dispatch(self, request, *args, **kwargs): if not request.user.role == 'ADMIN': - if (not request.user == Document.objects.get(id=kwargs['pk']).created_by): + if (not request.user == + Document.objects.get(id=kwargs['pk']).created_by): raise PermissionDenied - return super(DocumentDetailView, self).dispatch(request, *args, **kwargs) + return super(DocumentDetailView, self).dispatch( + request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(DocumentDetailView, self).get_context_data(**kwargs) @@ -548,29 +569,82 @@ def download_document(request, pk): if (not request.user == doc_obj.created_by and request.user not in doc_obj.shared_to.all()): raise PermissionDenied - path = doc_obj.document_file.path - file_path = os.path.join(settings.MEDIA_ROOT, path) - if os.path.exists(file_path): - with open(file_path, 'rb') as fh: - response = HttpResponse( - fh.read(), content_type="application/vnd.ms-excel") - response['Content-Disposition'] = 'inline; filename=' + \ - os.path.basename(file_path) + if settings.STORAGE_TYPE == "normal": + # print('no no no no') + path = doc_obj.document_file.path + file_path = os.path.join(settings.MEDIA_ROOT, path) + if os.path.exists(file_path): + with open(file_path, 'rb') as fh: + response = HttpResponse( + fh.read(), content_type="application/vnd.ms-excel") + response['Content-Disposition'] = 'inline; filename=' + \ + os.path.basename(file_path) + return response + else: + file_path = doc_obj.document_file + file_name = doc_obj.title + # print(file_path) + # print(file_name) + BUCKET_NAME = "django-crm-demo" + KEY = str(file_path) + s3 = boto3.resource('s3') + try: + s3.Bucket(BUCKET_NAME).download_file(KEY, file_name) + # print('got it') + with open(file_name, 'rb') as fh: + response = HttpResponse( + fh.read(), content_type="application/vnd.ms-excel") + response['Content-Disposition'] = 'inline; filename=' + \ + os.path.basename(file_name) + os.remove(file_name) return response + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == "404": + print("The object does not exist.") + else: + raise + + return path raise Http404 def download_attachment(request, pk): attachment_obj = Attachments.objects.filter(id=pk).last() - path = attachment_obj.attachment.path - file_path = os.path.join(settings.MEDIA_ROOT, path) - if os.path.exists(file_path): - with open(file_path, 'rb') as fh: - response = HttpResponse( - fh.read(), content_type="application/vnd.ms-excel") - response['Content-Disposition'] = 'inline; filename=' + \ - os.path.basename(file_path) - return response + if attachment_obj: + if settings.STORAGE_TYPE == "normal": + path = attachment_obj.attachment.path + file_path = os.path.join(settings.MEDIA_ROOT, path) + if os.path.exists(file_path): + with open(file_path, 'rb') as fh: + response = HttpResponse( + fh.read(), content_type="application/vnd.ms-excel") + response['Content-Disposition'] = 'inline; filename=' + \ + os.path.basename(file_path) + return response + else: + file_path = attachment_obj.attachment + file_name = attachment_obj.file_name + # print(file_path) + # print(file_name) + BUCKET_NAME = "django-crm-demo" + KEY = str(file_path) + s3 = boto3.resource('s3') + try: + s3.Bucket(BUCKET_NAME).download_file(KEY, file_name) + with open(file_name, 'rb') as fh: + response = HttpResponse( + fh.read(), content_type="application/vnd.ms-excel") + response['Content-Disposition'] = 'inline; filename=' + \ + os.path.basename(file_name) + os.remove(file_name) + return response + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == "404": + print("The object does not exist.") + else: + raise + # if file_path: + # print('yes tus pus') raise Http404 @@ -598,8 +672,7 @@ def add_comment(request): "commented_on": comment.commented_on, "commented_by": comment.commented_by.email }) - else: - return JsonResponse({"error": form.errors}) + return JsonResponse({"error": form.errors}) def edit_comment(request, pk): @@ -614,8 +687,7 @@ def edit_comment(request, pk): "comment_id": comment_obj.id, "comment": comment_obj.comment, }) - else: - return JsonResponse({"error": form['comment'].errors}) + return JsonResponse({"error": form['comment'].errors}) data = {'error': "You don't have permission to edit this comment."} return JsonResponse(data) @@ -667,12 +739,9 @@ def add_api_settings(request): success_url = reverse_lazy("common:add_api_settings") data = {'success_url': success_url, 'error': False} return JsonResponse(data) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - else: - data = {'form': form, "setting": api_settings, - 'users': users, 'assign_to_list': assign_to_list} + return JsonResponse({'error': True, 'errors': form.errors}) + data = {'form': form, "setting": api_settings, + 'users': users, 'assign_to_list': assign_to_list} return render(request, 'settings/create.html', data) @@ -714,14 +783,14 @@ def update_api_settings(request, pk): success_url = reverse_lazy("common:add_api_settings") data = {'success_url': success_url, 'error': False} return JsonResponse(data) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - else: - data = { - 'form': form, "setting": api_settings, 'users': users, 'assign_to_list': assign_to_list, - 'assigned_to_list': json.dumps([setting.id for setting in api_settings.lead_assigned_to.all() if setting]) - } + return JsonResponse({'error': True, 'errors': form.errors}) + data = { + 'form': form, "setting": api_settings, + 'users': users, 'assign_to_list': assign_to_list, + 'assigned_to_list': + json.dumps( + [setting.id for setting in api_settings.lead_assigned_to.all() if setting]) + } return render(request, 'settings/update.html', data) @@ -731,7 +800,8 @@ def delete_api_settings(request, pk): api_settings.delete() data = {"error": False, "response": "Successfully Deleted!"} else: - data = {"error": True, "response": "Object Not Found!"} + data = {"error": True, + "response": "Object Not Found!"} # return JsonResponse(data) return redirect('common:api_settings') @@ -743,15 +813,16 @@ def change_passsword_by_admin(request): user.set_password(request.POST.get("new_passwoord")) user.save() mail_subject = 'Crm Account Password Changed' - message = "

hello " + user.username + "


Your account password has been changed !

" + \ - "

New Password : " + \ + message = "

hello " + user.username +\ + "


Your account password has been changed !\ +

" \ + + "

New Password : " + \ request.POST.get("new_passwoord") + "

" email = EmailMessage(mail_subject, message, to=[user.email]) email.content_subtype = "html" email.send() return HttpResponseRedirect('/users/list/') - else: - raise PermissionDenied + raise PermissionDenied def google_login(request): @@ -759,7 +830,8 @@ def google_login(request): params = { 'grant_type': 'authorization_code', 'code': request.GET.get('code'), - 'redirect_uri': 'http://' + request.META['HTTP_HOST'] + reverse('common:google_login'), + 'redirect_uri': 'http://' + + request.META['HTTP_HOST'] + reverse('common:google_login'), 'client_id': settings.GP_CLIENT_ID, 'client_secret': settings.GP_CLIENT_SECRET } @@ -807,7 +879,7 @@ def google_login(request): role="USER" ) - google, created = Google.objects.get_or_create(user=user) + google, _ = Google.objects.get_or_create(user=user) google.user = user google.google_url = link google.verified_email = verified_email @@ -827,16 +899,14 @@ def google_login(request): if request.GET.get('state') != '1235dfghjkf123': return HttpResponseRedirect(request.GET.get('state')) - else: - return HttpResponseRedirect(reverse("common:home")) + return HttpResponseRedirect(reverse("common:home")) + if request.GET.get('next'): + next_url = request.GET.get('next') else: - if request.GET.get('next'): - next_url = request.GET.get('next') - else: - next_url = '1235dfghjkf123' - rty = "https://accounts.google.com/o/oauth2/auth?client_id=" + \ - settings.GP_CLIENT_ID + "&response_type=code" - rty += "&scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&redirect_uri=" + \ - 'http://' + request.META['HTTP_HOST'] + \ - reverse('common:google_login') + "&state=" + next_url - return HttpResponseRedirect(rty) + next_url = '1235dfghjkf123' + rty = "https://accounts.google.com/o/oauth2/auth?client_id=" + \ + settings.GP_CLIENT_ID + "&response_type=code" + rty += "&scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&redirect_uri=" \ + + 'http://' + request.META['HTTP_HOST'] + \ + reverse('common:google_login') + "&state=" + next_url + return HttpResponseRedirect(rty) diff --git a/contacts/forms.py b/contacts/forms.py index 68e3468b..50b6b9f5 100644 --- a/contacts/forms.py +++ b/contacts/forms.py @@ -24,7 +24,9 @@ class ContactForm(forms.ModelForm): class Meta: model = Contact fields = ( - 'assigned_to', 'first_name', 'last_name', 'email', 'phone', 'address', 'description' + 'assigned_to', 'first_name', + 'last_name', 'email', + 'phone', 'address', 'description' ) diff --git a/contacts/models.py b/contacts/models.py index a0e2bd7b..3b9307b5 100644 --- a/contacts/models.py +++ b/contacts/models.py @@ -11,12 +11,14 @@ class Contact(models.Model): email = models.EmailField(unique=True) phone = PhoneNumberField(null=True, unique=True) address = models.ForeignKey( - Address, related_name='adress_contacts', on_delete=models.CASCADE, blank=True, null=True) + Address, related_name='adress_contacts', + on_delete=models.CASCADE, blank=True, null=True) description = models.TextField(blank=True, null=True) assigned_to = models.ManyToManyField( User, related_name='contact_assigned_users') created_by = models.ForeignKey( - User, related_name='contact_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='contact_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) is_active = models.BooleanField(default=False) diff --git a/contacts/tests.py b/contacts/tests.py index c84db515..2bb1b43a 100644 --- a/contacts/tests.py +++ b/contacts/tests.py @@ -10,16 +10,16 @@ class ContactObjectsCreation(object): def setUp(self): self.client = Client() self.user = User.objects.create( - first_name="navaneetha", - username='navaneetha', + first_name="Nathan", + username='Nathan', email="n@mp.com", role="ADMIN") self.user.set_password('navi123') self.user.save() self.address = Address.objects.create( - street="kphb 5th phase", - city="hyd", - state="telanagana", + street="5th phase", + city="Orlando", + state="Florida", postcode=502279, country="AD") self.contact = Contact.objects.create( @@ -31,7 +31,7 @@ class ContactObjectsCreation(object): created_by=self.user) self.contact.assigned_to.add(self.user) self.case = Case.objects.create( - name="raghu", + name="lucy", case_type="Problem", status="New", priority="Low", @@ -52,6 +52,9 @@ class ContactObjectsCreation(object): class ContactObjectsCreation_Count(ContactObjectsCreation, TestCase): def test_contact_object_creation(self): c = Contact.objects.count() + con = Contact.objects.filter(id=self.contact.id) + # print(self.contact.first_name) + self.assertEqual(str(con.last()), self.contact.first_name) self.assertEqual(c, 1) def test_address_object_creation(self): @@ -76,7 +79,7 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): def test_contacts_create(self): response = self.client.post('/contacts/create/', { 'first_name': 'contact', - 'last_name': 'reddy', + 'last_name': 'george', 'email': 'meg@gmail.com', 'phone': '+917898901234', 'address': self.address.id, @@ -96,6 +99,11 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): response = self.client.get(reverse("contacts:list")) self.assertEqual(response.status_code, 200) + def test_contacts_delete_get(self): + response = self.client.get( + '/contacts/' + str(self.contact.id) + '/delete/') + self.assertEqual(response.status_code, 302) + def test_contacts_delete_location_checking(self): response = self.client.post( '/contacts/' + str(self.contact.id) + '/delete/') @@ -104,7 +112,7 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): def test_contacts_edit(self): response = self.client.post( '/contacts/' + str(self.contact.id) + '/edit/', { - 'name': 'priya', + 'name': 'Preston', 'email': 'contact@gmail.com', 'phone': '12345', 'pk': self.contact.id, @@ -114,7 +122,7 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): def test_contacts_edit_html(self): response = self.client.post( '/contacts/' + str(self.contact.id) + '/edit/', { - 'name': 'priya', + 'name': 'Preston', 'email': 'contact@gmail.com', 'phone': '12345', 'pk': self.contact.id, @@ -149,7 +157,8 @@ class ContactsListTestCase(ContactObjectsCreation, TestCase): def test_contacts_list_queryset(self): data = {'fist_name': 'contact', - 'city': "hyd", 'phone': '12345', 'email': "contact@gmail.com"} + 'city': "Orlando", 'phone': '12345', + 'email': "contact@gmail.com"} response = self.client.post('/contacts/list/', data) self.assertEqual(response.status_code, 200) @@ -162,16 +171,32 @@ class CommentTestCase(ContactObjectsCreation, TestCase): '/contacts/comment/add/', {'contactid': self.contact.id}) self.assertEqual(response.status_code, 200) + # def test_GetContactsView(self): + # response = self.client.get('/get/list/') + # # self.assertEqual(response.status_code, 200) + # self.assertIsNone(response.context['contacts']) + def test_comment_edit(self): response = self.client.post( '/contacts/comment/edit/', {'commentid': self.comment.id}) self.assertEqual(response.status_code, 200) + resp = self.client.post( + '/contacts/comment/edit/', + {'commentid': self.comment.id, 'comment': 'hello123'}) + self.assertEqual(resp.status_code, 200) def test_comment_delete(self): response = self.client.post( '/contacts/comment/remove/', {'comment_id': self.comment.id}) self.assertEqual(response.status_code, 200) + def test_form_valid(self): + response = self.client.post( + '/contacts/comment/add/', {'contactid': self.contact.id, + 'comment': 'hello'}) + # print(response , "response") + self.assertEqual(response.status_code, 200) + class AttachmentTestCase(ContactObjectsCreation, TestCase): def test_attachment_add(self): diff --git a/contacts/urls.py b/contacts/urls.py index da8c2839..13c2bfaf 100644 --- a/contacts/urls.py +++ b/contacts/urls.py @@ -1,7 +1,9 @@ from django.urls import path from contacts.views import ( - ContactsListView, CreateContactView, ContactDetailView, UpdateContactView, RemoveContactView, - GetContactsView, AddCommentView, UpdateCommentView, DeleteCommentView, AddAttachmentsView, DeleteAttachmentsView) + ContactsListView, CreateContactView, ContactDetailView, + UpdateContactView, RemoveContactView, + GetContactsView, AddCommentView, UpdateCommentView, + DeleteCommentView, AddAttachmentsView, DeleteAttachmentsView) app_name = 'contacts' @@ -11,15 +13,21 @@ urlpatterns = [ path('create/', CreateContactView.as_view(), name='add_contact'), path('/view/', ContactDetailView.as_view(), name="view_contact"), path('/edit/', UpdateContactView.as_view(), name="edit_contact"), - path('/delete/', RemoveContactView.as_view(), name="remove_contact"), + path('/delete/', + RemoveContactView.as_view(), + name="remove_contact"), path('get/list/', GetContactsView.as_view(), name="get_contacts"), 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('comment/remove/', + DeleteCommentView.as_view(), + name="remove_comment"), - path('attachment/add/', AddAttachmentsView.as_view(), name="add_attachment"), + path('attachment/add/', + AddAttachmentsView.as_view(), + name="add_attachment"), path('attachment/remove/', DeleteAttachmentsView.as_view(), name="remove_attachment"), ] diff --git a/contacts/views.py b/contacts/views.py index 0444528e..eb3f18a9 100644 --- a/contacts/views.py +++ b/contacts/views.py @@ -12,7 +12,8 @@ from common.models import User, Comment, Attachments from common.forms import BillingAddressForm from common.utils import COUNTRIES from contacts.models import Contact -from contacts.forms import ContactForm, ContactCommentForm, ContactAttachmentForm +from contacts.forms import (ContactForm, + ContactCommentForm, ContactAttachmentForm) from accounts.models import Account from django.core.exceptions import PermissionDenied from django.db.models import Q @@ -25,9 +26,11 @@ class ContactsListView(LoginRequiredMixin, TemplateView): def get_queryset(self): queryset = self.model.objects.all() - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): queryset = queryset.filter( - Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)) + Q(assigned_to__in=[self.request.user]) | + Q(created_by=self.request.user)) request_post = self.request.POST if request_post: @@ -58,8 +61,10 @@ class ContactsListView(LoginRequiredMixin, TemplateView): int(i) for i in self.request.POST.getlist('assigned_to', []) if i] search = False if ( - self.request.POST.get('first_name') or self.request.POST.get('city') or - self.request.POST.get('phone') or self.request.POST.get( + self.request.POST.get('first_name') or + self.request.POST.get('city') or + self.request.POST.get('phone') or + self.request.POST.get( 'email') or self.request.POST.get('assigned_to') ): search = True @@ -78,7 +83,8 @@ class CreateContactView(LoginRequiredMixin, CreateView): def dispatch(self, request, *args, **kwargs): self.users = User.objects.filter(is_active=True).order_by('email') - return super(CreateContactView, self).dispatch(request, *args, **kwargs) + return super(CreateContactView, self).dispatch( + request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(CreateContactView, self).get_form_kwargs() @@ -96,7 +102,8 @@ class CreateContactView(LoginRequiredMixin, CreateView): contact_obj.created_by = self.request.user contact_obj.save() if self.request.GET.get('view_account', None): - if Account.objects.filter(id=int(self.request.GET.get('view_account'))).exists(): + if Account.objects.filter( + id=int(self.request.GET.get('view_account'))).exists(): Account.objects.get(id=int(self.request.GET.get( 'view_account'))).contacts.add(contact_obj) return self.form_valid(form) @@ -113,12 +120,13 @@ class CreateContactView(LoginRequiredMixin, CreateView): for assigned_to_user in assigned_to_list: user = get_object_or_404(User, pk=assigned_to_user) mail_subject = 'Assigned to contact.' - message = render_to_string('assigned_to/contact_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': self.request.scheme, - 'contact': contact_obj - }) + message = render_to_string( + 'assigned_to/contact_assigned.html', { + 'user': user, + 'domain': current_site.domain, + 'protocol': self.request.scheme, + 'contact': contact_obj + }) email = EmailMessage(mail_subject, message, to=[user.email]) email.content_subtype = "html" email.send() @@ -176,10 +184,12 @@ class ContactDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super(ContactDetailView, self).get_context_data(**kwargs) - user_assgn_list = [assigned_to.id for assigned_to in context['object'].assigned_to.all()] + user_assgn_list = [ + assigned_to.id for assigned_to in context['object'].assigned_to.all()] if self.request.user == context['object'].created_by: user_assgn_list.append(self.request.user.id) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): if self.request.user.id not in user_assgn_list: raise PermissionDenied assigned_data = [] @@ -189,8 +199,10 @@ class ContactDetailView(LoginRequiredMixin, DetailView): assigned_dict['name'] = each.email assigned_data.append(assigned_dict) - context.update({"comments": context["contact_record"].contact_comments.all(), - 'attachments': context["contact_record"].contact_attachment.all(), + context.update({"comments": + context["contact_record"].contact_comments.all(), + 'attachments': + context["contact_record"].contact_attachment.all(), "assigned_data": json.dumps(assigned_data) }) return context @@ -203,7 +215,8 @@ class UpdateContactView(LoginRequiredMixin, UpdateView): def dispatch(self, request, *args, **kwargs): self.users = User.objects.filter(is_active=True).order_by('email') - return super(UpdateContactView, self).dispatch(request, *args, **kwargs) + return super(UpdateContactView, self).dispatch( + request, *args, **kwargs) def get_form_kwargs(self): kwargs = super(UpdateContactView, self).get_form_kwargs() @@ -224,7 +237,8 @@ class UpdateContactView(LoginRequiredMixin, UpdateView): return self.form_invalid(form) def form_valid(self, form): - assigned_to_ids = self.get_object().assigned_to.all().values_list('id', flat=True) + assigned_to_ids = self.get_object().assigned_to.all().values_list( + 'id', flat=True) contact_obj = form.save(commit=False) all_members_list = [] @@ -234,16 +248,17 @@ class UpdateContactView(LoginRequiredMixin, UpdateView): 'assigned_to').values_list('id', flat=True) all_members_list = list( set(list(assigned_form_users)) - set(list(assigned_to_ids))) - if len(all_members_list): + 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 contact.' - message = render_to_string('assigned_to/contact_assigned.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': self.request.scheme, - 'contact': contact_obj - }) + message = render_to_string( + 'assigned_to/contact_assigned.html', { + 'user': user, + 'domain': current_site.domain, + 'protocol': self.request.scheme, + 'contact': contact_obj + }) email = EmailMessage( mail_subject, message, to=[user.email]) email.content_subtype = "html" @@ -288,7 +303,8 @@ class UpdateContactView(LoginRequiredMixin, UpdateView): assigned_to.id for assigned_to in context["contact_obj"].assigned_to.all()] if self.request.user == context['contact_obj'].created_by: user_assgn_list.append(self.request.user.id) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): if self.request.user.id not in user_assgn_list: raise PermissionDenied context["address_obj"] = self.object.address @@ -317,7 +333,9 @@ class RemoveContactView(LoginRequiredMixin, View): def post(self, request, *args, **kwargs): contact_id = kwargs.get("pk") self.object = get_object_or_404(Contact, id=contact_id) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser and self.request.user != self.object.created_by: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser and + self.request.user != self.object.created_by): raise PermissionDenied else: if self.object.address_id: @@ -339,7 +357,8 @@ class AddCommentView(LoginRequiredMixin, CreateView): Contact, id=request.POST.get('contactid')) if ( request.user in self.contact.assigned_to.all() or - request.user == self.contact.created_by or request.user.is_superuser or + request.user == self.contact.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -430,7 +449,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): Contact, id=request.POST.get('contactid')) if ( request.user in self.contact.assigned_to.all() or - request.user == self.contact.created_by or request.user.is_superuser or + request.user == self.contact.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -453,7 +473,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): "attachment": attachment.file_name, "attachment_url": attachment.attachment.url, "created_on": attachment.created_on, - "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), + "download_url": reverse('common:download_attachment', + kwargs={'pk': attachment.id}), "attachment_display": attachment.get_file_type_display(), "created_by": attachment.created_by.email }) @@ -468,12 +489,14 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): 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 == self.object.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): self.object.delete() data = {"aid": request.POST.get("attachment_id")} return JsonResponse(data) - data = {'error': "You don't have permission to delete this attachment."} + data = {'error': + "You don't have permission to delete this attachment."} return JsonResponse(data) diff --git a/crm/settings.py b/crm/settings.py index bb270449..f91608ee 100644 --- a/crm/settings.py +++ b/crm/settings.py @@ -7,7 +7,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = 'mwx@&97%!$fx_*zgj(2ygi^(s=oh5j(cqb$=+-mkd9scbt!0v0' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True ALLOWED_HOSTS = ['*'] @@ -36,6 +36,7 @@ INSTALLED_APPS = [ 'sorl.thumbnail', 'phonenumber_field', 'storages', + 'marketing', ] MIDDLEWARE = [ diff --git a/crm/urls.py b/crm/urls.py index 95b2712f..02026995 100644 --- a/crm/urls.py +++ b/crm/urls.py @@ -11,7 +11,8 @@ urlpatterns = [ path('accounts/', include('accounts.urls', namespace="accounts")), path('leads/', include('leads.urls', namespace="leads")), path('contacts/', include('contacts.urls', namespace="contacts")), - path('opportunities/', include('opportunity.urls', namespace="opportunities")), + path('opportunities/', + include('opportunity.urls', namespace="opportunities")), path('cases/', include('cases.urls', namespace="cases")), path('emails/', include('emails.urls', namespace="emails")), # path('planner/', include('planner.urls', namespace="planner")), diff --git a/emails/models.py b/emails/models.py index fc38d00b..20832a2f 100644 --- a/emails/models.py +++ b/emails/models.py @@ -14,7 +14,7 @@ class Email(models.Model): status = models.CharField(max_length=200, default="sent") important = models.BooleanField(max_length=10, default=False) - def __unicode__(self): + def __str__(self): return self.subject class Meta: diff --git a/emails/tests.py b/emails/tests.py index f3dcc5f7..18714582 100644 --- a/emails/tests.py +++ b/emails/tests.py @@ -9,11 +9,15 @@ class UserCreation(TestCase): def setUp(self): self.client = Client() self.user = User.objects.create( - first_name="navaneetha", username='navaneetha', email="n@mp.com", role="ADMIN") + first_name="nicole", + username='nicole', + email="n@mp.com", role="ADMIN") self.user.set_password('navi123') self.user.save() self.email = Email.objects.create( - from_email="admin@micropyramid.com", to_email="meghana@micropyramid.com", subject="wish", message="haii", + from_email="admin@micropyramid.com", + to_email="nike@micropyramid.com", + subject="wish", message="haii", important=False) self.client.login(username='n@mp.com', password='navi123') @@ -24,7 +28,7 @@ class EmailSentEdit(UserCreation, TestCase): 'to_email': "xyz@mp.com", 'subject': "hello hi", 'message': 'simple mail'}) - print('yes') + # print('yes') self.assertTrue(form.is_valid()) def test_edit_form_invalid(self): @@ -32,7 +36,7 @@ class EmailSentEdit(UserCreation, TestCase): 'to_email': "", 'subject': "hello hi", 'message': 'simple mail'}) - print('yes2') + # print('yes2') self.assertFalse(form.is_valid()) @@ -44,6 +48,27 @@ class EmailListTestCase(UserCreation, TestCase): class EmailTestCase(UserCreation, TestCase): + def test_email_compose(self): + url = "/emails/compose/" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_email_trash_get(self): + url = "/emails/email_trash_delete/" + str(self.email.pk) + "/" + # print(url) + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + + def test_email_send_fail(self): + url = "/emails/compose/" + data = { + 'from_email': "abc@micropyramid.com", 'to_email': "", + 'subject': 'act', 'message': "Hello" + } + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) +# email_trash_delete/ + def test_email_send(self): url = "/emails/compose/" data = { @@ -55,7 +80,7 @@ class EmailTestCase(UserCreation, TestCase): # boo = Email.objects.get(important=True) # print('yes') self.assertFalse(get_email.important) - self.assertEqual(get_email.subject, get_email.__unicode__()) + self.assertEqual(get_email.subject, get_email.__str__()) self.assertEqual(response.status_code, 302) @@ -69,6 +94,58 @@ class EmailTestCase(UserCreation, TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) + def test_email_draft(self): + url = "/emails/email_draft/" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_email_draft_delete(self): + url = "/emails/email_draft_delete/" + str(self.email.pk) + "/" + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + + def test_email_delete(self): + url = "/emails/email_delete/" + str(self.email.pk) + "/" + response = self.client.get(url) + self.assertEqual(response.status_code, 302) + + def test_email_view(self): + url = "/emails/email_view/" + str(self.email.pk) + "/" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_email_sent_edit_get(self): + url = "/emails/email_sent_edit/" + str(self.email.pk) + "/" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + def test_email_sent_edit_post(self): + url = "/emails/email_sent_edit/" + str(self.email.pk) + "/" + data = { + 'from_email': "abc@micropyramid.com", 'to_email': "meg@gmail.com", + 'subject': 'act', 'message': "Hello" + } + data1 = { + 'from_email': "abc@micropyramid.com", 'to_email': "", + 'subject': 'act', 'message': "Hello" + } + response = self.client.post(url, data) + response1 = self.client.post(url, data1) + self.assertEqual(response.status_code, 302) + self.assertEqual(response1.status_code, 200) + + def test_email_imp_list(self): + url = "/emails/email_imp_list/" + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + + # def test_email_move_to_trash(self): + # url = "/emails/email_move_to_trash/" + str(self.email.pk) + "/" + # response = self.client.get(url) + # self.assertEqual(response.status_code, 302) + # response = self.client.post(reverse('302'), {}, HTTP_REFERER=url) + # def test_email_trash_del(self): # url = "/emails/trash_delete/"+str(self.email.pk)+"/$" # print(url) diff --git a/leads/forms.py b/leads/forms.py index 6bc3cee5..b9a154ce 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -17,7 +17,8 @@ class LeadForm(forms.ModelForm): self.fields['assigned_to'].required = False for key, value in self.fields.items(): if key == 'phone': - value.widget.attrs['placeholder'] = 'Enter phone number with country code' + value.widget.attrs['placeholder'] =\ + 'Enter phone number with country code' else: value.widget.attrs['placeholder'] = value.label @@ -46,9 +47,12 @@ class LeadForm(forms.ModelForm): class Meta: model = Lead - fields = ('assigned_to', 'first_name', 'last_name', 'account_name', 'title', - 'phone', 'email', 'status', 'source', 'website', 'description', - 'address_line', 'street', 'city', 'state', 'postcode', 'country' + fields = ('assigned_to', 'first_name', + 'last_name', 'account_name', 'title', + 'phone', 'email', 'status', 'source', + 'website', 'description', + 'address_line', 'street', + 'city', 'state', 'postcode', 'country' ) diff --git a/leads/migrations/0007_auto_20190306_1226.py b/leads/migrations/0007_auto_20190306_1226.py new file mode 100644 index 00000000..4885d66d --- /dev/null +++ b/leads/migrations/0007_auto_20190306_1226.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-03-06 06:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('leads', '0006_auto_20190218_1217'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('assigned', 'Assigned'), ('in process', 'In Process'), ('converted', 'Converted'), ('recycled', 'Recycled'), ('closed', 'Closed')], max_length=255, null=True, verbose_name='Status of Lead'), + ), + ] diff --git a/leads/models.py b/leads/models.py index a30002ed..0763756b 100644 --- a/leads/models.py +++ b/leads/models.py @@ -3,7 +3,8 @@ from django.utils.translation import pgettext_lazy from django.utils.translation import ugettext_lazy as _ from common.models import User -from common.utils import LEAD_STATUS, LEAD_SOURCE, COUNTRIES, return_complete_address +from common.utils import (LEAD_STATUS, LEAD_SOURCE, + COUNTRIES, return_complete_address) from phonenumber_field.modelfields import PhoneNumberField from accounts.models import Tags from contacts.models import Contact @@ -11,15 +12,19 @@ from contacts.models import Contact class Lead(models.Model): title = models.CharField( - pgettext_lazy("Treatment Pronouns for the customer", "Title"), max_length=64, blank=True, null=True) + pgettext_lazy("Treatment Pronouns for the customer", + "Title"), max_length=64, blank=True, null=True) first_name = models.CharField(("First name"), max_length=255) last_name = models.CharField(("Last name"), max_length=255) email = models.EmailField(null=True, blank=True) phone = PhoneNumberField(null=True, blank=True) status = models.CharField( - _("Status of Lead"), max_length=255, blank=True, null=True, choices=LEAD_STATUS) + _("Status of Lead"), + max_length=255, blank=True, + null=True, choices=LEAD_STATUS) source = models.CharField( - _("Source of Lead"), max_length=255, blank=True, null=True, choices=LEAD_SOURCE) + _("Source of Lead"), max_length=255, + blank=True, null=True, choices=LEAD_SOURCE) address_line = models.CharField( _("Address"), max_length=255, blank=True, null=True) street = models.CharField( @@ -37,9 +42,12 @@ class Lead(models.Model): User, related_name='lead_assigned_users') account_name = models.CharField(max_length=255, null=True, blank=True) opportunity_amount = models.DecimalField( - _("Opportunity Amount"), decimal_places=2, max_digits=12, blank=True, null=True) + _("Opportunity Amount"), + decimal_places=2, max_digits=12, + blank=True, null=True) created_by = models.ForeignKey( - User, related_name='lead_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='lead_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) is_active = models.BooleanField(default=False) enquery_type = models.CharField(max_length=255, blank=True, null=True) diff --git a/leads/tasks.py b/leads/tasks.py index 6e4f519b..b4d0a54a 100644 --- a/leads/tasks.py +++ b/leads/tasks.py @@ -13,7 +13,9 @@ def get_rendered_html(template_name, context={}): @task -def send_email(subject, html_content, text_content=None, from_email=None, recipients=[], attachments=[], bcc=[], cc=[]): +def send_email(subject, html_content, + text_content=None, from_email=None, + recipients=[], attachments=[], bcc=[], cc=[]): # send email to user with attachment if not from_email: from_email = settings.DEFAULT_FROM_EMAIL diff --git a/leads/templates/leads.html b/leads/templates/leads.html index 7dd527f8..fd892f5d 100644 --- a/leads/templates/leads.html +++ b/leads/templates/leads.html @@ -114,11 +114,12 @@ ID Name + Created By Source Status Assigned User - Tags - Created On + Tags + Created On Actions {% endif %} @@ -133,6 +134,15 @@ {{ forloop.counter }} {{ lead.first_name }} {{ lead.last_name }} + + {% if lead.created_by.profile_pic %} + {% thumbnail lead.created_by.profile_pic "40x40" crop="center" as im %} + + {% endthumbnail %} + {% else %} + Micro profile pic + {% endif %} + {{ lead.source }} {{ lead.get_status_display }} diff --git a/leads/tests.py b/leads/tests.py index 6b6d1977..06ef0200 100644 --- a/leads/tests.py +++ b/leads/tests.py @@ -3,6 +3,8 @@ from cases.models import Case from leads.models import Lead from common.models import User, Comment, Attachments from accounts.models import Account +from leads.tasks import * +from leads.forms import * class TestLeadModel(object): @@ -10,20 +12,20 @@ class TestLeadModel(object): self.client = Client() self.user = User.objects.create( - username='uday', email='u@mp.com', role="ADMIN") - self.user.set_password('uday2293') + username='jorge', email='j@mp.com', role="ADMIN") + self.user.set_password('jorge2293') self.user.save() - self.client.login(username='u@mp.com', password='uday2293') + self.client.login(username='j@mp.com', password='jorge2293') self.account = Account.objects.create(name="account", email="account@gmail.com", phone="12345", billing_address_line="", - billing_street="Gokul enclave colony", - billing_city="Hasthinapuram", - billing_state="Telangana", - billing_postcode="500079", + billing_street="Arcade enclave colony", + billing_city="NewTown", + billing_state="California", + billing_postcode="5009", billing_country="AD", website="account.com", industry="IT", @@ -31,14 +33,14 @@ class TestLeadModel(object): created_by=self.user) self.lead = Lead.objects.create(title="LeadCreation", - first_name="anjali", + first_name="Alisa", last_name="k", - email="anjalikotha1993@gmail.com", + email="Alisak1993@gmail.com", address_line="", - street="Gokul enclave colony", - city="Hasthinapuram", - state="Telangana", - postcode="500079", + street="Arcade enclave colony", + city="NewTown", + state="California", + postcode="5079", country="AD", website="www.gmail.com", status="assigned", @@ -47,25 +49,29 @@ class TestLeadModel(object): description="Iam an Lead", created_by=self.user) self.case = Case.objects.create( - name="raghu", case_type="Problem", status="New", account=self.account, + name="Rose", case_type="Problem", + status="New", + account=self.account, priority="Low", description="something", created_by=self.user, closed_on="2016-05-04") self.comment = Comment.objects.create( comment='testikd', case=self.case, commented_by=self.user) self.attachment = Attachments.objects.create( - attachment='image.png', case=self.case, created_by=self.user, account=self.account) + attachment='image.png', + case=self.case, created_by=self.user, + account=self.account) class LeadsPostrequestTestCase(TestLeadModel, TestCase): def test_valid_postrequesttestcase_date(self): - data = {'title': 'LeadCreation', 'first_name': "kotha", - 'last_name': "anjali", 'email': "anjalikotha1993@gmail.com", + data = {'title': 'LeadCreation', 'first_name': "micky", + 'last_name': "Alisa", 'email': "Alisamicky1993@gmail.com", 'account': self.account, 'address_line': "", - 'street': "Gokul enclave colony", - 'city': "Hasthinapuram", - 'state': "Telangana", - 'postcode': "500079", + 'street': "Arcade enclave colony", + 'city': "NewTown", + 'state': "California", + 'postcode': "579", 'country': "AD", 'website': "www.gmail.com", "status ": "assigned", "source": "Call", @@ -76,9 +82,10 @@ class LeadsPostrequestTestCase(TestLeadModel, TestCase): def test_leads_list(self): self.lead = Lead.objects.all() - get_name = Lead.objects.get(first_name='anjali') + get_name = Lead.objects.get(first_name='Alisa') # 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)) + self.assertEqual(get_name.first_name + + get_name.last_name, str(get_name)) response = self.client.get('/leads/list/') self.assertEqual(response.status_code, 200) @@ -91,14 +98,14 @@ class LeadsCreateUrlTestCase(TestLeadModel, TestCase): def test_leads_create_url(self): response = self.client.post('/leads/create/', { 'title': 'LeadCreation', - 'first_name': "kotha", - 'email': "anjalikotha1993@gmail.com", + 'first_name': "micky", + 'email': "Alisamicky1993@gmail.com", 'account': self.account, 'address_line': "", - 'street': "Gokul enclave colony", - 'city': "Hasthinapuram", - 'state': "Telangana", - 'postcode': "500079", + 'street': "Arcade enclave colony", + 'city': "NewTown", + 'state': "California", + 'postcode': "579", 'country': "AD", 'website': "www.gmail.com", "status": "assigned", @@ -110,11 +117,14 @@ class LeadsCreateUrlTestCase(TestLeadModel, TestCase): def test_leads_create_html(self): response = self.client.post('/leads/create/', { - 'title': 'LeadCreation', 'name': "kotha", 'email': "anjalikotha1993@gmail.com", 'account': self.account, - 'address_line': "", 'street': "Gokul enclave colony", 'city': "Hasthinapuram", - 'state': "Telangana", 'postcode': "500079", 'country': "AD", + 'title': 'LeadCreation', 'name': "micky", + 'email': "Alisamicky1993@gmail.com", 'account': self.account, + 'address_line': "", + 'street': "Arcade enclave colony", 'city': "NewTown", + 'state': "California", 'postcode': "579", 'country': "AD", 'website': "www.gmail.com", 'status': "assigned", - "source": "Call", 'opportunity_amount': "700", 'description': "Iam an Lead", 'created_by': self.user}) + "source": "Call", 'opportunity_amount': "700", + 'description': "Iam an Lead", 'created_by': self.user}) # self.assertTemplateUsed(response, 'create_lead.html') self.assertEqual(response.status_code, 200) @@ -129,14 +139,14 @@ class LeadsViewTestCase(TestLeadModel, TestCase): def test_leads_view(self): Lead.objects.create(title="LeadCreationbylead", - first_name="anjali", + first_name="Alisa", last_name="k", - email="srilathakotha1993@gmail.com", + email="Alisa@gmail.com", address_line="", - street="Gokul enclave colony", - city="Hasthinapuram", - state="Telangana", - postcode="500079", + street="Arcade enclave colony", + city="NewTown", + state="California", + postcode="579", country="AD", website="www.gmail.com", status='converted', @@ -158,8 +168,9 @@ class LeadListTestCase(TestLeadModel, TestCase): def test_leads_list_queryset(self): self.lead = Lead.objects.all() - data = {'fist_name': "meghana", 'last_name': "reddy", - 'city': "hyd", 'email': "contact@gmail.com", 'status': "Assigned"} + data = {'fist_name': "marphy", 'last_name': "racheal", + 'city': "hyd", 'email': "contact@gmail.com", + 'status': "Assigned"} response = self.client.post('/leads/list/', data) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'leads.html') @@ -189,21 +200,24 @@ class UpdateLeadTestCase(TestLeadModel, TestCase): def test_update_lead(self): url = '/leads/' + str(self.lead.id) + '/edit/' data = { - 'title': "Creation", 'first_name': "meghana", 'last_name': "k", + 'title': "Creation", 'first_name': "marphy", 'last_name': "k", 'email': "meg@gmail.com", 'account': self.account, - 'address_line': "", 'street': "Gokul enclave colony", 'city': "Hasthinapuram", - 'state': "Telangana", 'postcode': "500079", 'country': "AD", - 'phone': "+917894563452", 'website': "www.gmail.com", 'status': "assigned", - 'source': "", 'opportunity_amount': "700", 'description': "Iam an Lead", 'created_by': self.user} + 'address_line': "", + 'street': "Arcade enclave colony", 'city': "NewTown", + 'state': "California", + 'postcode': "579", 'country': "AD", + 'phone': "+917894563452", + 'website': "www.gmail.com", 'status': None, + 'source': "", 'opportunity_amount': "700", + 'description': "Iam an Lead", 'created_by': self.user} response = self.client.post(url, data) - # self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 200) class LeadDetailTestCase(TestLeadModel, TestCase): def test_lead_detail(self): url = '/leads/' + str(self.lead.id) + '/view/' - response = self.client.get(url) + response = self.client.get(url, {'status': ''}) self.assertEqual(response.status_code, 200) @@ -235,3 +249,23 @@ class AttachmentTestCase(TestLeadModel, TestCase): '/leads/attachment/remove/', {'attachment_id': self.attachment.id}) self.assertEqual(response.status_code, 200) + +class TestTemplates(TestLeadModel, TestCase): + def test_lead_list_view(self): + resp = self.client.post('/leads/list/', {'name': 'trevor', + 'tag': "123", + 'source': "call", + 'assigned_to': '1', + 'tab_status': 'Open'}) + self.assertEqual(resp.status_code, 200) + # print(resp.name) + # self.assertTrue(resp.name) + + # def test_lead_from_site(self): + # resp = self.client.get() + + +class TestConvertLeadView(TestLeadModel, TestCase): + def test_get_fun(self): + resp = self.client.get('/leads/' + str(self.lead.id) + '/convert/') + self.assertEqual(resp.status_code, 302) diff --git a/leads/urls.py b/leads/urls.py index 88198bc6..321b84a2 100644 --- a/leads/urls.py +++ b/leads/urls.py @@ -1,6 +1,7 @@ from django.urls import path from leads.views import ( - LeadListView, create_lead, LeadDetailView, update_lead, DeleteLeadView, convert_lead, + LeadListView, create_lead, LeadDetailView, + update_lead, DeleteLeadView, convert_lead, GetLeadsView, AddCommentView, UpdateCommentView, DeleteCommentView, AddAttachmentsView, DeleteAttachmentsView, create_lead_from_site ) @@ -24,9 +25,11 @@ urlpatterns = [ 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('comment/remove/', + DeleteCommentView.as_view(), name="remove_comment"), - path('attachment/add/', AddAttachmentsView.as_view(), name="add_attachment"), + path('attachment/add/', + AddAttachmentsView.as_view(), name="add_attachment"), path('attachment/remove/', DeleteAttachmentsView.as_view(), name="remove_attachment"), ] diff --git a/leads/views.py b/leads/views.py index 35121e5c..30105775 100644 --- a/leads/views.py +++ b/leads/views.py @@ -9,11 +9,11 @@ from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.urls import reverse from django.views.generic import ( - CreateView, UpdateView, DetailView, ListView, TemplateView, View) + CreateView, DetailView, ListView, TemplateView, View) from accounts.models import Account, Tags from common.models import User, Comment, Attachments, APISettings -from common.utils import LEAD_STATUS, LEAD_SOURCE, COUNTRIES, get_client_ip +from common.utils import LEAD_STATUS, LEAD_SOURCE, COUNTRIES from common import status from contacts.models import Contact from leads.models import Lead @@ -31,9 +31,11 @@ class LeadListView(LoginRequiredMixin, TemplateView): def get_queryset(self): queryset = self.model.objects.all().exclude(status='converted') - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): queryset = queryset.filter( - Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user)) + Q(assigned_to__in=[self.request.user]) | + Q(created_by=self.request.user)) request_post = self.request.POST if request_post: @@ -61,8 +63,8 @@ class LeadListView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super(LeadListView, self).get_context_data(**kwargs) # context["lead_obj"] = self.get_queryset() - open_leads = self.get_queryset().exclude(status='dead') - close_leads = self.get_queryset().filter(status='dead') + open_leads = self.get_queryset().exclude(status='closed') + close_leads = self.get_queryset().filter(status='closed') context["status"] = LEAD_STATUS context["open_leads"] = open_leads context["close_leads"] = close_leads @@ -73,14 +75,13 @@ class LeadListView(LoginRequiredMixin, TemplateView): context["assignedto_list"] = [ int(i) for i in self.request.POST.getlist('assigned_to', []) if i] - search = False - if ( + search = True if ( self.request.POST.get('name') or self.request.POST.get('city') or self.request.POST.get('email') or self.request.POST.get('tag') or - self.request.POST.get('status') or self.request.POST.get('source') or + self.request.POST.get('status') or + self.request.POST.get('source') or self.request.POST.get('assigned_to') - ): - search = True + ) else False context["search"] = search @@ -107,7 +108,6 @@ def create_lead(request): if request.POST: form = LeadForm(request.POST, request.FILES, assigned_to=users) if form.is_valid(): - lead_obj = form.save(commit=False) lead_obj.created_by = request.user lead_obj.save() @@ -128,12 +128,13 @@ def create_lead(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 - }) + 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" @@ -171,12 +172,13 @@ def create_lead(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 - }) + 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" @@ -187,22 +189,18 @@ def create_lead(request): if request.POST.get("savenewform"): success_url = reverse("leads:add_lead") return JsonResponse({'error': False, 'success_url': success_url}) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["lead_form"] = form + context["accounts"] = Account.objects.filter(status="open") + context["users"] = users + context["countries"] = COUNTRIES + context["status"] = LEAD_STATUS + context["source"] = LEAD_SOURCE + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] - else: - return JsonResponse({'error': True, 'errors': form.errors}) - - else: - context = {} - context["lead_form"] = form - context["accounts"] = Account.objects.filter(status="open") - context["users"] = users - context["countries"] = COUNTRIES - context["status"] = LEAD_STATUS - context["source"] = LEAD_SOURCE - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - - return render(request, template_name, context) + return render(request, template_name, context) class LeadDetailView(LoginRequiredMixin, DetailView): @@ -216,7 +214,8 @@ class LeadDetailView(LoginRequiredMixin, DetailView): assigned_to.id for assigned_to in context['object'].assigned_to.all()] if self.request.user == context['object'].created_by: user_assgn_list.append(self.request.user.id) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user.role != "ADMIN" and not + self.request.user.is_superuser): if self.request.user.id not in user_assgn_list: raise PermissionDenied comments = Comment.objects.filter( @@ -244,8 +243,10 @@ class LeadDetailView(LoginRequiredMixin, DetailView): assigned_data.append(assigned_dict) context.update({ - "attachments": attachments, "comments": comments, "status": LEAD_STATUS, "countries": COUNTRIES, - "reminder_form_set": reminder_form_set, "meetings": meetings, "calls": calls, + "attachments": attachments, "comments": comments, + "status": LEAD_STATUS, "countries": COUNTRIES, + "reminder_form_set": reminder_form_set, + "meetings": meetings, "calls": calls, "assigned_data": json.dumps(assigned_data)}) return context @@ -266,7 +267,8 @@ def update_lead(request, pk): if request.POST: form = LeadForm(request.POST, request.FILES, - instance=lead_record, initial=initial, assigned_to=users) + instance=lead_record, + initial=initial, assigned_to=users) if request.POST.get('status') == "converted": form.fields['account_name'].required = True @@ -275,7 +277,8 @@ def update_lead(request, pk): form.fields['account_name'].required = False form.fields['email'].required = False if form.is_valid(): - assigned_to_ids = lead_record.assigned_to.all().values_list('id', flat=True) + assigned_to_ids = lead_record.assigned_to.all().values_list( + 'id', flat=True) lead_obj = form.save(commit=False) lead_obj.save() lead_obj.tags.clear() @@ -298,17 +301,19 @@ def update_lead(request, pk): assigned_form_users = form.cleaned_data.get( 'assigned_to').values_list('id', flat=True) all_members_list = list( - set(list(assigned_form_users)) - set(list(assigned_to_ids))) - if len(all_members_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 - }) + 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" @@ -351,12 +356,13 @@ def update_lead(request, pk): 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 - }) + 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" @@ -368,32 +374,28 @@ def update_lead(request, pk): if status: success_url = reverse('accounts:list') return JsonResponse({'error': False, 'success_url': success_url}) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["lead_obj"] = lead_record + user_assgn_list = [ + assigned_to.id for assigned_to in lead_record.assigned_to.all()] + if request.user == lead_record.created_by: + user_assgn_list.append(request.user.id) + if request.user.role != "ADMIN" and not request.user.is_superuser: + if request.user.id not in user_assgn_list: + raise PermissionDenied - else: - return JsonResponse({'error': True, 'errors': form.errors}) + context["lead_form"] = form + context["accounts"] = Account.objects.filter(status="open") + context["users"] = users + context["countries"] = COUNTRIES + context["status"] = LEAD_STATUS + context["source"] = LEAD_SOURCE + context["error"] = error + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] - else: - context = {} - context["lead_obj"] = lead_record - user_assgn_list = [ - assigned_to.id for assigned_to in lead_record.assigned_to.all()] - if request.user == lead_record.created_by: - user_assgn_list.append(request.user.id) - if request.user.role != "ADMIN" and not request.user.is_superuser: - if request.user.id not in user_assgn_list: - raise PermissionDenied - - context["lead_form"] = form - context["accounts"] = Account.objects.filter(status="open") - context["users"] = users - context["countries"] = COUNTRIES - context["status"] = LEAD_STATUS - context["source"] = LEAD_SOURCE - context["error"] = error - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - - return render(request, template_name, context) + return render(request, template_name, context) class DeleteLeadView(LoginRequiredMixin, View): @@ -404,13 +406,13 @@ class DeleteLeadView(LoginRequiredMixin, View): def post(self, request, *args, **kwargs): self.object = get_object_or_404(Lead, id=kwargs.get("pk")) if ( - self.request.user.role == "ADMIN" or self.request.user.is_superuser or + self.request.user.role == "ADMIN" or + self.request.user.is_superuser or self.request.user == self.object.created_by ): self.object.delete() return redirect("leads:list") - else: - raise PermissionDenied + raise PermissionDenied def convert_lead(request, pk): @@ -435,7 +437,8 @@ def convert_lead(request, pk): account_object.contacts.add(*contacts_list) account_object.save() current_site = get_current_site(request) - for assigned_to_user in lead_obj.assigned_to.all().values_list('id', flat=True): + for assigned_to_user in lead_obj.assigned_to.all().values_list( + 'id', flat=True): user = get_object_or_404(User, pk=assigned_to_user) mail_subject = 'Assigned to account.' message = render_to_string('assigned_to/account_assigned.html', { @@ -450,7 +453,8 @@ def convert_lead(request, pk): return redirect("accounts:list") return HttpResponseRedirect( - reverse('leads:edit_lead', kwargs={'pk': lead_obj.id}) + '?status=converted') + reverse('leads:edit_lead', kwargs={ + 'pk': lead_obj.id}) + '?status=converted') class AddCommentView(LoginRequiredMixin, CreateView): @@ -463,7 +467,8 @@ class AddCommentView(LoginRequiredMixin, CreateView): self.lead = get_object_or_404(Lead, id=request.POST.get('leadid')) if ( request.user in self.lead.assigned_to.all() or - request.user == self.lead.created_by or request.user.is_superuser or + request.user == self.lead.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -537,11 +542,6 @@ class GetLeadsView(LoginRequiredMixin, ListView): context_object_name = "leads" template_name = "leads_list.html" - def get_context_data(self, **kwargs): - context = super(GetLeadsView, self).get_context_data(**kwargs) - context["leads"] = self.get_queryset() - return context - class AddAttachmentsView(LoginRequiredMixin, CreateView): model = Attachments @@ -553,7 +553,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): self.lead = get_object_or_404(Lead, id=request.POST.get('leadid')) if ( request.user in self.lead.assigned_to.all() or - request.user == self.lead.created_by or request.user.is_superuser or + request.user == self.lead.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -576,7 +577,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): "attachment_url": attachment.attachment.url, "created_on": attachment.created_on, "created_by": attachment.created_by.email, - "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), + "download_url": reverse( + 'common:download_attachment', kwargs={'pk': attachment.id}), "attachment_display": attachment.get_file_type_display() }) @@ -590,14 +592,16 @@ class DeleteAttachmentsView(LoginRequiredMixin, View): 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 == self.object.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): self.object.delete() data = {"aid": request.POST.get("attachment_id")} return JsonResponse(data) - data = {'error': "You don't have permission to delete this attachment."} + data = {'error': + "You don't have permission to delete this attachment."} return JsonResponse(data) @@ -606,21 +610,26 @@ def create_lead_from_site(request): # ip_addres = get_client_ip(request) # website_address = request.scheme + '://' + ip_addres api_key = request.POST.get('apikey') - # api_setting = APISettings.objects.filter(website=website_address, apikey=api_key).first() + # api_setting = APISettings.objects.filter( + # website=website_address, apikey=api_key).first() api_setting = APISettings.objects.filter(apikey=api_key).first() if not api_setting: return JsonResponse({ - 'error': True, 'message': "You don't have permission, please contact the admin!."}, + 'error': True, + 'message': + "You don't have permission, please contact the admin!."}, status=status.HTTP_400_BAD_REQUEST) - if api_setting and request.POST.get("email") and request.POST.get("full_name"): + if (api_setting and request.POST.get("email") and + request.POST.get("full_name")): # user = User.objects.filter(is_admin=True, is_active=True).first() user = api_setting.created_by lead = Lead.objects.create( title=request.POST.get("full_name"), status="assigned", source=api_setting.website, description=request.POST.get("message"), - email=request.POST.get("email"), phone=request.POST.get("phone"), + email=request.POST.get("email"), + phone=request.POST.get("phone"), is_active=True, created_by=user) lead.assigned_to.add(user) # Send Email to Assigned Users @@ -629,18 +638,19 @@ def create_lead_from_site(request): # Create Contact contact = Contact.objects.create( first_name=request.POST.get("full_name"), - email=request.POST.get("email"), phone=request.POST.get("phone"), + email=request.POST.get("email"), + phone=request.POST.get("phone"), description=request.POST.get("message"), created_by=user, is_active=True) contact.assigned_to.add(user) lead.contacts.add(contact) - return JsonResponse({'error': False, 'message': "Lead Created sucessfully."}, + return JsonResponse({'error': False, + 'message': "Lead Created sucessfully."}, status=status.HTTP_201_CREATED) return JsonResponse({'error': True, 'message': "In-valid data."}, status=status.HTTP_400_BAD_REQUEST) - else: - return JsonResponse({ - 'error': True, 'message': "In-valid request method."}, - status=status.HTTP_400_BAD_REQUEST) + return JsonResponse({ + 'error': True, 'message': "In-valid request method."}, + status=status.HTTP_400_BAD_REQUEST) diff --git a/marketing/forms.py b/marketing/forms.py new file mode 100644 index 00000000..0327bf8b --- /dev/null +++ b/marketing/forms.py @@ -0,0 +1,290 @@ +import csv +import re +import json +import openpyxl +from django import forms + +from common.models import User +from marketing.models import ( + ContactList, Contact, + EmailTemplate, Campaign, Tag +) + + +def csv_doc_validate(document): + temp_row = [] + reader = csv.reader((document.read().decode("utf-8")).splitlines()) + # csv_headers = ["first name", "last name", "email"] + csv_headers = ["first name", "email"] + # required_headers = ["first name", "last name", "email"] + required_headers = ["first name", "email"] + for y_index, row in enumerate(reader): + each = {} + if y_index == 0: + csv_headers = [header_name.lower() + for header_name in row if header_name] + missing_headers = set(required_headers) - \ + set([r.lower() for r in row]) + if missing_headers: + missing_headers_str = ', '.join(missing_headers) + message = 'Missing headers: %s' % (missing_headers_str) + return {"error": True, "message": message} + continue + elif not ''.join(str(x) for x in row): + continue + else: + for x_index, cell_value in enumerate(row): + try: + csv_headers[x_index] + except IndexError: + continue + if csv_headers[x_index] in required_headers: + if not cell_value: + message = 'Missing required value %s for row %s' % ( + csv_headers[x_index], y_index + 1) + return {"error": True, "message": message} + each[csv_headers[x_index]] = cell_value + temp_row.append(each) + return {"error": False, "validated_rows": temp_row} + + +def get_validated_rows(wb, sheet_name, validated_rows): + # headers = ["first name", "last name", "email"] + # required_headers = ["first name", "last name", "email"] + headers = ["first name", "email"] + required_headers = ["first name", "email"] + work_sheet = wb.get_sheet_by_name(name=sheet_name) + for y_index, row in enumerate(work_sheet.iter_rows()): + if y_index == 0: + missing_headers = set(required_headers) - \ + set([str(cell.value).lower() for cell in row]) + if missing_headers: + missing_headers_str = ', '.join(missing_headers) + message = "Missing headers: %s %s" % ( + missing_headers_str, sheet_name) + return {"error": True, "message": message} + continue + elif not ''.join(str(cell.value) for cell in row): + continue + else: + temp_row = [] + for x_index, cell in enumerate(row): + try: + headers[x_index] + except IndexError: + continue + if headers[x_index] in required_headers: + if not cell.value: + message = 'Missing required \ + value %s for row %s in sheet %s'\ + % (headers[x_index], y_index + 1, sheet_name) + return {"error": True, "message": message} + # elif x_index == 0: + # cell_value = is_valid_date_format(cell.value) + # if not cell_value: + # message = 'Invalid date format(format:yyyy-mm-dd) \ + # %s for row %s in sheet %s' % (headers[x_index], y_index + 1, sheet_name) + # return {"error": True, "message": message} + temp_row.append(cell.value) + if len(temp_row) >= len(required_headers): + validated_rows.append(temp_row) + return validated_rows + + +def xls_doc_validate(document): + wb = openpyxl.load_workbook(document) + sheets = wb.get_sheet_names() + validated_rows = [] + for sheet_name in sheets: + get_validated_rows(wb, sheet_name, validated_rows) + return {"error": False, "validated_rows": validated_rows} + + +def import_document_validator(document): + try: + # dialect = csv.Sniffer().sniff(document.read(1024).decode("ascii")) + document.seek(0, 0) + return csv_doc_validate(document) + except Exception as e: + print (e) + try: + return xls_doc_validate(document) + except Exception as e: + print (e) + return {"error": True, "message": "Not a valid CSV/XLS file"} + + +class ContactListForm(forms.ModelForm): + tags = forms.CharField(max_length=5000, required=False) + contacts_file = forms.FileField(required=False) + + class Meta: + model = ContactList + fields = ["name"] + + def __init__(self, *args, **kwargs): + super(ContactListForm, self).__init__(*args, **kwargs) + 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 + if self.data.get('contacts_file'): + self.fields['contacts_file'].widget.attrs.update({ + "accept": ".csv,.xls,.xlsx,.xlsm,.xlsb,.xml", + }) + + def clean_contacts_file(self): + document = self.cleaned_data.get("contacts_file") + if document: + data = import_document_validator(document) + if data.get("error"): + raise forms.ValidationError(data.get("message")) + else: + self.validated_rows = data.get("validated_rows") + 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: + instance_visible_to = [] + if self.instance and self.instance.id is not None: + instance_visible_to = list( + set(self.instance.visible_to.all().values_list( + 'email', flat=True))) + for each in visible_to_data: + visible_to = User.objects.filter(email=each) + if instance_visible_to: + visible_to = visible_to.exclude( + email__in=instance_visible_to) + if visible_to: + raise forms.ValidationError( + str(each) + ' User aleady existed') + return self.data['visible_to'] + raise forms.ValidationError("Select any of the users") + + +class ContactForm(forms.ModelForm): + contact_list = forms.CharField(max_length=5000) + + class Meta: + model = Contact + fields = ["name", "email", "contact_number"] + + 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) == []: + 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): + contacts_file = forms.FileField() + contact_list = forms.CharField(max_length=5000) + + def __init__(self, *args, **kwargs): + super(ContactsCSVUploadForm, self).__init__(*args, **kwargs) + self.fields['contacts_file'].widget.attrs.update({ + "accept": ".csv,.xls,.xlsx,.xlsm,.xlsb,.xml", + }) + + def clean_contacts_file(self): + document = self.cleaned_data.get("contacts_file") + data = import_document_validator(document) + if data.get("error"): + raise forms.ValidationError(data.get("message")) + else: + self.validated_rows = data.get("validated_rows") + return document + + 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 EmailTemplateForm(forms.ModelForm): + + class Meta: + model = EmailTemplate + fields = ['title', 'subject', 'html'] + + +class SendCampaignForm(forms.ModelForm): + schedule_later = forms.BooleanField(required=False) + reply_to_crm = forms.BooleanField(required=False) + timezone = forms.CharField(max_length=500, required=False) + schedule_date_time = forms.CharField(max_length=100, required=False) + reply_to_email = forms.EmailField(max_length=100, required=False) + 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) + + class Meta: + model = Campaign + fields = ['title', 'subject', 'html', 'email_template'] + + def __init__(self, *args, **kwargs): + super(SendCampaignForm, self).__init__(*args, **kwargs) + if self.data.get('schedule_later') and self.data['schedule_later'] == 'true': + self.fields['timezone'].required = True + self.fields['schedule_date_time'].required = True + if not self.data.get('reply_to_crm'): + self.fields['reply_to_email'].required = True + + 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 diff --git a/marketing/migrations/0001_initial.py b/marketing/migrations/0001_initial.py new file mode 100644 index 00000000..8ea614ec --- /dev/null +++ b/marketing/migrations/0001_initial.py @@ -0,0 +1,178 @@ +# Generated by Django 2.1.7 on 2019-02-25 13:21 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Campaign', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=5000)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('schedule_date_time', models.DateTimeField(blank=True, null=True)), + ('reply_to_email', models.EmailField(blank=True, max_length=254, null=True)), + ('subject', models.CharField(max_length=5000)), + ('html', models.TextField()), + ('html_processed', models.TextField(blank=True, default='')), + ('from_email', models.EmailField(blank=True, max_length=254, null=True)), + ('from_name', models.EmailField(blank=True, max_length=254, null=True)), + ('sent', models.IntegerField(blank=True, default='0')), + ('opens', models.IntegerField(blank=True, default='0')), + ('opens_unique', models.IntegerField(blank=True, default='0')), + ('bounced', models.IntegerField(default='0')), + ('status', models.CharField(choices=[('Scheduled', 'Scheduled'), ('Cancelled', 'Cancelled'), ('Sending', 'Sending'), ('Preparing', 'Preparing'), ('Sent', 'Sent')], default='Preparing', max_length=20)), + ], + ), + migrations.CreateModel( + name='CampaignLinkClick', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField()), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('user_agent', models.CharField(blank=True, max_length=2000, null=True)), + ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='marketing.Campaign')), + ], + ), + migrations.CreateModel( + name='CampaignLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('message_id', models.CharField(blank=True, max_length=1000, null=True)), + ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaign_contacts', to='marketing.Campaign')), + ], + ), + migrations.CreateModel( + name='CampaignOpen', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField()), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('user_agent', models.CharField(blank=True, max_length=2000, null=True)), + ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='marketing.Campaign')), + ], + ), + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('name', models.CharField(max_length=500)), + ('email', models.EmailField(max_length=254)), + ('contact_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 20 digits allowed.", regex='^\\+?1?\\d{9,15}$')])), + ('is_unsubscribed', models.BooleanField(default=False)), + ('is_bounced', models.BooleanField(default=False)), + ('company_name', models.CharField(blank=True, max_length=500, null=True)), + ('last_name', models.CharField(blank=True, max_length=500, null=True)), + ('city', models.CharField(blank=True, max_length=500, null=True)), + ('state', models.CharField(blank=True, max_length=500, null=True)), + ('contry', models.CharField(blank=True, max_length=500, null=True)), + ], + ), + migrations.CreateModel( + name='ContactList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('name', models.CharField(max_length=500)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_contactlist', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='EmailTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('title', models.CharField(max_length=5000)), + ('subject', models.CharField(max_length=5000)), + ('html', models.TextField()), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_emailtemplates', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Link', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('original', models.URLField(max_length=2100)), + ('clicks', models.IntegerField(default='0')), + ('unique', models.IntegerField(default='0')), + ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marketing_links', to='marketing.Campaign')), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=500)), + ('color', models.CharField(default='#999999', max_length=20, verbose_name='color')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_tags', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='contactlist', + name='tags', + field=models.ManyToManyField(to='marketing.Tag'), + ), + migrations.AddField( + model_name='contactlist', + name='visible_to', + field=models.ManyToManyField(related_name='contact_lists_visible_to', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='contact', + name='contact_list', + field=models.ManyToManyField(related_name='contacts', to='marketing.ContactList'), + ), + migrations.AddField( + model_name='contact', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_contacts_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='campaignopen', + name='contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='marketing.Contact'), + ), + migrations.AddField( + model_name='campaignlog', + name='contact', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_campaign_logs', to='marketing.Contact'), + ), + migrations.AddField( + model_name='campaignlinkclick', + name='contact', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='marketing.Contact'), + ), + migrations.AddField( + model_name='campaignlinkclick', + name='link', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='marketing.Link'), + ), + migrations.AddField( + model_name='campaign', + name='contact_lists', + field=models.ManyToManyField(related_name='campaigns', to='marketing.ContactList'), + ), + migrations.AddField( + model_name='campaign', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_campaigns_created_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='campaign', + name='email_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='marketing.EmailTemplate'), + ), + ] diff --git a/marketing/migrations/0002_auto_20190307_1227.py b/marketing/migrations/0002_auto_20190307_1227.py new file mode 100644 index 00000000..df674c66 --- /dev/null +++ b/marketing/migrations/0002_auto_20190307_1227.py @@ -0,0 +1,40 @@ +# Generated by Django 2.1.7 on 2019-03-07 06:57 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='campaign', + name='updated_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='contact', + name='updated_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='contactlist', + name='updated_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='emailtemplate', + name='updated_on', + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name='tag', + name='created_on', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/marketing/migrations/0003_failedcontact.py b/marketing/migrations/0003_failedcontact.py new file mode 100644 index 00000000..b9506794 --- /dev/null +++ b/marketing/migrations/0003_failedcontact.py @@ -0,0 +1,34 @@ +# Generated by Django 2.1.7 on 2019-03-07 12:20 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('marketing', '0002_auto_20190307_1227'), + ] + + operations = [ + migrations.CreateModel( + name='FailedContact', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('name', models.CharField(blank=True, max_length=500, null=True)), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('contact_number', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 20 digits allowed.", regex='^\\+?1?\\d{9,15}$')])), + ('company_name', models.CharField(blank=True, max_length=500, null=True)), + ('last_name', models.CharField(blank=True, max_length=500, null=True)), + ('city', models.CharField(blank=True, max_length=500, null=True)), + ('state', models.CharField(blank=True, max_length=500, null=True)), + ('contry', models.CharField(blank=True, max_length=500, null=True)), + ('contact_list', models.ManyToManyField(related_name='failed_contacts', to='marketing.ContactList')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='marketing_failed_contacts_created_by', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/marketing/models.py b/marketing/models.py index 3c915728..fea77fd0 100644 --- a/marketing/models.py +++ b/marketing/models.py @@ -1,228 +1,57 @@ -from datetime import datetime +from datetime import datetime, timedelta from django.db import models -# from django.utils.translation import ugettext_lazy as _ +from django.utils.timesince import timesince +from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator -# from django.db.models import Sum -# from django.template.defaultfilters import slugify +from django.db.models import Sum +from django.template.defaultfilters import slugify +from common.models import User class Tag(models.Model): name = models.CharField(max_length=500) - color = models.CharField(max_length=20, default="#999999", verbose_name=_("color")) - created_by = models.ForeignKey(User, related_name="marketing_tags", null=True, on_delete=models.SET_NULL) + color = models.CharField(max_length=20, + default="#999999", verbose_name=_("color")) + created_by = models.ForeignKey(User, + related_name="marketing_tags", + null=True, on_delete=models.SET_NULL) + created_on = models.DateTimeField(auto_now_add=True) @property def created_by_user(self): - return self.created_by.serialized_data() if self.created_by else None - - -def attachment_url(self, filename): - file_extension = filename.split('.')[-1] - return "%s/Documents/%s" % ( - self.company, "attachment_" + str(slugify(datetime.now())) + "." + file_extension) - # file_split = filename.split('.') - # file_extension = file_split[-1] - # path = "%s_%s_%s" % (file_split[0], self.profile.user.email, str(datetime.now())) - # return str(self.company.subdomain) + "/users/" + str(self.profile.user.email) + \ - # "/docs/" + slugify(path) + "." + file_extension - - -class Document(models.Model): - title = models.CharField(max_length=155) - description = models.CharField(max_length=2500, blank=True, null=True) - document_file = models.FileField(upload_to=attachment_url) - created_by = models.ForeignKey(User, related_name="created_marketing_documents", null=True, on_delete=models.SET_NULL) - created_on = models.DateTimeField(auto_now_add=True) - is_active = models.BooleanField(default=True) - - def __str__(self): - return self.title - - def is_document_file_image(self, ext): - image_ext_list = ['bmp', 'dds', 'gif', 'jpg', 'jpeg', 'png', 'psd', 'pspimage', - 'tga', 'thm', 'tif', 'tiff', 'yuv'] - return ext.lower() in image_ext_list - - def is_document_file_audio(self, ext): - audio_ext_list = ['aif', 'iff', 'm3u', 'm4a', 'mid', 'mp3', - 'mpa', 'wav', 'wma'] - return ext.lower() in audio_ext_list - - def is_document_file_video(self, ext): - video_ext_list = ['3g2', '3gp', 'asf', 'avi', 'flv', 'm4v', 'mov', - 'mp4', 'mpg', 'rm', 'srt', 'swf', 'vob', 'wmv'] - return ext.lower() in video_ext_list - - def is_document_file_pdf(self, ext): - pdf_ext_list = ['indd', 'pct', 'pdf'] - return ext.lower() in pdf_ext_list - - def is_document_file_code(self, ext): - code_ext_list = ['aspx', 'json', 'jsp', 'do', 'htm', 'html', 'ser', - 'php', 'jad', 'cfm', 'xml', 'js', 'pod', 'asp', - 'atomsvc', 'rdf', 'pou', 'jsf', 'abs', 'pl', 'asm', - 'srz', 'luac', 'cod', 'lib', 'arxml', 'bas', 'ejs', - 'fs', 'hbs', 's', 'ss', 'cms', 'pyc', 'vcxproj', - 'jse', 'smali', 'xla', 'lxk', 'pdb', 'src', 'cs', - 'ipb', 'ave', 'mst', 'vls', 'rcc', 'sax', 'scr', - 'dtd', 'axd', 'mrl', 'xsl', 'ino', 'spr', 'xsd', - 'cgi', 'isa', 'ws', 'rss', 'dvb', 'nupkg', 'xlm', - 'v4e', 'rss', 'prg', 'form', 'bat', 'mrc', 'asi', - 'jdp', 'fmb', 'graphml', 'gcode', 'aia', 'py', 'atp', - 'mzp', 'o', 'scs', 'mm', 'cpp', 'java', 'gypi', 'idb', - 'txml', 'c', 'vip', 'tra', 'rc', 'action', 'vlx', - 'asta', 'pyo', 'lua', 'gml', 'prl', 'rfs', 'cpb', - 'sh', 'rbf', 'gp', 'phtml', 'bp', 'scb', 'sln', 'vbp', - 'wbf', 'bdt', 'mac', 'rpy', 'eaf', 'mc', 'mwp', 'gnt', - 'h', 'swift', 'e', 'styl', 'cxx', 'as', 'liquid', - 'dep', 'fas', 'vbs', 'aps', 'vbe', 'lss', 'cmake', - 'resx', 'csb', 'dpk', 'pdml', 'txx', 'dbg', 'jsa', - 'sxs', 'sasf', 'pm', 'csx', 'r', 'wml', 'au3', 'stm', - 'cls', 'cc', 'ins', 'jsc', 'dwp', 'rpg', 'arb', 'bml', - 'inc', 'eld', 'sct', 'sm', 'wbt', 'csproj', 'tcz', - 'html5', 'gbl', 'cmd', 'dlg', 'tpl', 'rbt', 'xcp', - 'tpm', 'qry', 'mfa', 'ptx', 'lsp', 'pag', 'ebc', - 'php3', 'cob', 'csc', 'pyt', 'dwt', 'rb', 'wsdl', - 'lap', 'textile', 'sfx', 'x', 'a5r', 'dbp', 'pmp', - 'ipr', 'fwx', 'pbl', 'vbw', 'phl', 'cbl', 'pas', - 'mom', 'dbmdl', 'lol', 'wdl', 'ppam', 'plx', 'vb', - 'cgx', 'lst', 'lmp', 'vd', 'bcp', 'thtml', 'scpt', - 'isu', 'mrd', 'perl', 'dtx', 'f', 'wpk', 'ipf', 'ptl', - 'luca', 'hx', 'uvproj', 'qvs', 'vba', 'xjb', - 'appxupload', 'ti', 'svn-base', 'bsc', 'mak', - 'vcproj', 'dsd', 'ksh', 'pyw', 'bxml', 'mo', 'irc', - 'gcl', 'dbml', 'mlv', 'wsf', 'tcl', 'dqy', 'ssi', - 'pbxproj', 'bal', 'trt', 'sal', 'hkp', 'vbi', 'dob', - 'htc', 'p', 'ats', 'seam', 'loc', 'pli', 'rptproj', - 'pxml', 'pkb', 'dpr', 'scss', 'dsb', 'bb', 'vbproj', - 'ash', 'rml', 'nbk', 'nvi', 'lmv', 'mw', 'jl', 'dso', - 'cba', 'jks', 'ary', 'run', 'vps', 'clm', 'brml', - 'msha', 'mdp', 'tmh', 'rdf', 'jsx', 'sdl', 'ptxml', - 'fxl', 'wmw', 'dcr', 'bcc', 'cbp', 'bmo', 'bsv', - 'less', 'gss', 'ctl', 'rpyc', 'ascx', 'odc', 'wiki', - 'obr', 'l', 'axs', 'bpr', 'ppa', 'rpo', 'sqlproj', - 'smm', 'dsr', 'arq', 'din', 'jml', 'jsonp', 'ml', - 'rc2', 'myapp', 'cla', 'xme', 'obj', 'jsdtscope', - 'gyp', 'datasource', 'cp', 'rh', 'lpx', 'a2w', 'ctp', - 'ulp', 'nt', 'script', 'bxl', 'gs', 'xslt', 'mg', - 'pch', 'mhl', 'zpd', 'psm1', 'asz', 'm', 'jacl', - 'pym', 'rws', 'acu', 'ssq', 'wxs', 'coffee', 'ncb', - 'akt', 'pyx', 'zero', 'hs', 'mkb', 'tru', 'xul', - 'mfl', 'sca', 'sbr', 'master', 'opv', 'matlab', - 'sami', 'agc', 'slim', 'tea', 'pbl', 'm51', 'mec', - 'asc', 'gch', 'enml', 'ino', 'kst', 'jade', 'dfb', - 'ips', 'rgs', 'vbx', 'cspkg', 'ncx', 'brs', 'wfs', - 'ifp', 'nse', 'xtx', 'j', 'cx', 'ps1', 'nas', 'mk', - 'ccs', 'vrp', 'lnp', 'cml', 'c#', 'idl', 'exp', 'apb', - 'nsi', 'asmx', 'tdo', 'pjt', 'fdt', 's5d', 'mvba', - 'mf', 'odl', 'bzs', 'jardesc', 'tgml', 'moc', 'wxi', - 'cpz', 'fsx', 'jav', 'ocb', 'agi', 'tec', 'txl', - 'amw', 'mscr', 'dfd', 'dpd', 'pun', 'f95', 'vdproj', - 'xsc', 'diff', 'wxl', 'dgml', 'airi', 'kmt', 'ksc', - 'io', 'rbw', 'sas', 'vcp', 'resources', 'param', - 'cg', 'hlsl', 'vssscc', 'bgm', 'xn', 'targets', 'sl', - 'gsc', 'qs', 'owl', 'devpak', 'phps', 'hdf', 'pri', - 'nbin', 'xaml', 's4e', 'scm', 'tk', 'poc', 'uix', - 'clw', 'factorypath', 's43', 'awd', 'htr', 'php2', - 'classpath', 'pickle', 'rob', 'msil', 'ebx', 'tsq', - 'lml', 'f90', 'lds', 'vup', 'pbi', 'swt', 'vap', 'ig', - 'pdo', 'frt', 'fcg', 'c++', 'xcl', 'dfn', 'aar', - 'for', 're', 'twig', 'ebm', 'dhtml', 'hc', 'pro', - 'ahk', 'rule', 'bsh', 'jcs', 'zrx', 'wsdd', 'csp', - 'drc', 'appxsym'] - return ext.lower() in code_ext_list - - def is_document_file_text(self, ext): - text_ext_list = ['doc', 'docx', 'log', 'msg', 'odt', 'pages', 'rtf', - 'tex', 'txt', 'wpd', 'wps'] - return ext.lower() in text_ext_list - - def is_document_file_sheet(self, ext): - sheet_ext_list = ['csv', 'xls', 'xlsx', - 'xlsm', 'xlsb', 'xltx', 'xltm', 'xlt'] - return ext.lower() in sheet_ext_list - - def is_document_file_zip(self, ext): - ext_list = ['zip', '7Z', 'gz', 'rar', 'ZIPX', 'ACE', 'tar', ] - return ext.lower() in ext_list - - def file_type(self): - name_ext_list = self.document_file.url.split(".") - if (len(name_ext_list) > 1): - ext = name_ext_list[int(len(name_ext_list) - 1)] - if self.is_document_file_audio(ext): - return ("audio", "fa fa-file-audio-o") - elif self.is_document_file_video(ext): - return ("video", "fa fa-file-video-o") - elif self.is_document_file_image(ext): - return ("image", "fa fa-file-image-o") - elif self.is_document_file_pdf(ext): - return ("pdf", "fa fa-file-pdf-o") - elif self.is_document_file_code(ext): - return ("code", "fa fa-file-code-o") - elif self.is_document_file_text(ext): - return ("text", "fa fa-file-text") - elif self.is_document_file_sheet(ext): - return ("sheet", "fa fa-file-excel-o") - elif self.is_document_file_zip(ext): - return ("zip", "fa fa-file-archive-o") - return ("file", "fa fa-file-o") - return ("file", "fa fa-file-o") - - @property - def file_type_code(self): - return self.file_type() - - @property - def document_file_path(self): - return self.document_file.url if self.document_file else None - - @property - def document_file_name(self): - return self.document_file.name if self.document_file else None + return self.created_by if self.created_by else None class EmailTemplate(models.Model): - created_by = models.ForeignKey(User, related_name="marketing_emailtemplates", null=True, on_delete=models.SET_NULL) + created_by = models.ForeignKey( + User, related_name="marketing_emailtemplates", + null=True, on_delete=models.SET_NULL) created_on = models.DateTimeField(auto_now_add=True) + updated_on = models.DateTimeField(auto_now=True) title = models.CharField(max_length=5000) subject = models.CharField(max_length=5000) html = models.TextField() - class Meta: - unique_together = (("title", "company"),) - @property def created_by_user(self): - return self.created_by.serialized_data() if self.created_by else None + return self.created_by if self.created_by else None class ContactList(models.Model): - created_by = models.ForeignKey(User, related_name="marketing_contactlist", null=True, on_delete=models.SET_NULL) + created_by = models.ForeignKey( + User, related_name="marketing_contactlist", + null=True, on_delete=models.SET_NULL) created_on = models.DateTimeField(auto_now_add=True) + updated_on = models.DateTimeField(auto_now=True) name = models.CharField(max_length=500) tags = models.ManyToManyField(Tag) - is_public = models.BooleanField(default=False) - - class Meta: - unique_together = (("name", "company"),) + # is_public = models.BooleanField(default=False) + visible_to = models.ManyToManyField( + User, related_name="contact_lists_visible_to") @property def created_by_user(self): - return self.created_by.serialized_data() if self.created_by else None - - @property - def created_by_user_profile(self): - if self.created_by is not None: - profile = Profile.objects.filter( - user=self.created_by, company=self.company).last() - if profile is not None: - return Skinner().parse(profile, fields=[ - 'employee_name', 'userid', 'profile_pic_path', 'employee_id']) - else: - return self.created_by.serialized_data() - else: - return None + return self.created_by if self.created_by else None @property def created_on_format(self): @@ -230,16 +59,18 @@ class ContactList(models.Model): @property def created_on_since(self): - return created_since(self.created_on) + now = datetime.now() + difference = now.replace(tzinfo=None) - \ + self.created_on.replace(tzinfo=None) - @property - def created_by_company(self): - return self.company.serialized_data() if self.company else None + if difference <= timedelta(minutes=1): + return 'just now' + return '%(time)s ago' % { + 'time': timesince(self.created_on).split(', ')[0]} @property def tags_data(self): - return Skinner().parse( - self.tags.all(), fields=["id", "name", "color"])['data'] + return self.tags.all() @property def no_of_contacts(self): @@ -260,21 +91,27 @@ class ContactList(models.Model): @property def no_of_clicks(self): clicks = CampaignLog.objects.filter( - contact__contact_list__in=[self]).aggregate(Sum('no_of_clicks'))['no_of_clicks__sum'] + contact__contact_list__in=[self]).aggregate(Sum( + 'no_of_clicks'))['no_of_clicks__sum'] return clicks class Contact(models.Model): phone_regex = RegexValidator( regex=r'^\+?1?\d{9,15}$', - message="Phone number must be entered in the format: '+999999999'. Up to 20 digits allowed." + message="Phone number must be entered in the format: '+999999999'. \ + Up to 20 digits allowed." ) - created_by = models.ForeignKey(User, related_name="marketing_contacts_created_by", null=True, on_delete=models.SET_NULL) + created_by = models.ForeignKey( + User, related_name="marketing_contacts_created_by", + null=True, on_delete=models.SET_NULL) created_on = models.DateTimeField(auto_now_add=True) + updated_on = models.DateTimeField(auto_now=True) contact_list = models.ManyToManyField(ContactList, related_name="contacts") name = models.CharField(max_length=500) email = models.EmailField() - contact_number = models.CharField(validators=[phone_regex], max_length=20, blank=True, null=True) + contact_number = models.CharField( + validators=[phone_regex], max_length=20, blank=True, null=True) is_unsubscribed = models.BooleanField(default=False) is_bounced = models.BooleanField(default=False) company_name = models.CharField(max_length=500, null=True, blank=True) @@ -283,8 +120,34 @@ class Contact(models.Model): state = models.CharField(max_length=500, null=True, blank=True) contry = models.CharField(max_length=500, null=True, blank=True) - class Meta: - unique_together = (("company", "email"),) + def __str__(self): + return self.email + + +class FailedContact(models.Model): + phone_regex = RegexValidator( + regex=r'^\+?1?\d{9,15}$', + message="Phone number must be entered in the format: '+999999999'.\ + Up to 20 digits allowed." + ) + created_by = models.ForeignKey( + User, related_name="marketing_failed_contacts_created_by", null=True, + on_delete=models.SET_NULL) + created_on = models.DateTimeField(auto_now_add=True) + contact_list = models.ManyToManyField( + ContactList, related_name="failed_contacts") + name = models.CharField(max_length=500, null=True, blank=True) + email = models.EmailField(null=True, blank=True) + contact_number = models.CharField( + validators=[phone_regex], max_length=20, blank=True, null=True) + company_name = models.CharField(max_length=500, null=True, blank=True) + last_name = models.CharField(max_length=500, null=True, blank=True) + city = models.CharField(max_length=500, null=True, blank=True) + state = models.CharField(max_length=500, null=True, blank=True) + contry = models.CharField(max_length=500, null=True, blank=True) + + def __str__(self): + return self.email class Campaign(models.Model): @@ -297,10 +160,15 @@ class Campaign(models.Model): ) title = models.CharField(max_length=5000) - created_by = models.ForeignKey(User, related_name="marketing_campaigns_created_by", null=True, on_delete=models.SET_NULL) + created_by = models.ForeignKey( + User, related_name="marketing_campaigns_created_by", + null=True, on_delete=models.SET_NULL) created_on = models.DateTimeField(auto_now_add=True) - contact_lists = models.ManyToManyField(ContactList, related_name="campaigns") - email_template = models.ForeignKey(EmailTemplate, blank=True, null=True, on_delete=models.SET_NULL) + updated_on = models.DateTimeField(auto_now=True) + contact_lists = models.ManyToManyField( + ContactList, related_name="campaigns") + email_template = models.ForeignKey( + EmailTemplate, blank=True, null=True, on_delete=models.SET_NULL) schedule_date_time = models.DateTimeField(blank=True, null=True) reply_to_email = models.EmailField(blank=True, null=True) subject = models.CharField(max_length=5000) @@ -312,16 +180,39 @@ class Campaign(models.Model): opens = models.IntegerField(default='0', blank=True) opens_unique = models.IntegerField(default='0', blank=True) bounced = models.IntegerField(default='0') - status = models.CharField(default="Preparing", choices=STATUS_CHOICES, max_length=20) + status = models.CharField( + default="Preparing", choices=STATUS_CHOICES, max_length=20) @property def no_of_unsubscribers(self): - unsubscribers = self.campaign_contacts.filter(contact__is_unsubscribed=True).count() + unsubscribers = self.campaign_contacts.filter( + contact__is_unsubscribed=True).count() return unsubscribers + @property + def no_of_bounces(self): + bounces = self.campaign_contacts.filter( + contact__is_bounced=True).count() + return bounces + + @property + def no_of_clicks(self): + clicks = self.marketing_links.aggregate(Sum('clicks'))['clicks__sum'] + return clicks + + @property + def no_of_sent_emails(self): + contacts = self.campaign_contacts.count() + return contacts + + @property + def created_on_format(self): + return self.created_on.strftime('%b %d, %Y %I:%M %p') + class Link(models.Model): - campaign = models.ForeignKey(Campaign, related_name="marketing_links", on_delete=models.CASCADE) + campaign = models.ForeignKey( + Campaign, related_name="marketing_links", on_delete=models.CASCADE) original = models.URLField(max_length=2100) clicks = models.IntegerField(default='0') unique = models.IntegerField(default='0') @@ -329,18 +220,23 @@ class Link(models.Model): class CampaignLog(models.Model): created_on = models.DateTimeField(auto_now_add=True) - campaign = models.ForeignKey(Campaign, related_name='campaign_contacts', on_delete=models.CASCADE) - contact = models.ForeignKey(Contact, related_name="marketing_campaign_logs", null=True, on_delete=models.SET_NULL) + campaign = models.ForeignKey( + Campaign, related_name='campaign_contacts', on_delete=models.CASCADE) + contact = models.ForeignKey( + Contact, related_name="marketing_campaign_logs", + null=True, on_delete=models.SET_NULL) message_id = models.CharField(max_length=1000, null=True, blank=True) class CampaignLinkClick(models.Model): campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) - link = models.ForeignKey(Link, blank=True, null=True, on_delete=models.CASCADE) + link = models.ForeignKey( + Link, blank=True, null=True, on_delete=models.CASCADE) 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 = models.ForeignKey( + Contact, blank=True, null=True, on_delete=models.CASCADE) class CampaignOpen(models.Model): @@ -348,4 +244,5 @@ class CampaignOpen(models.Model): 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 = models.ForeignKey( + Contact, blank=True, null=True, on_delete=models.CASCADE) diff --git a/marketing/tasks.py b/marketing/tasks.py index 2e86cae1..8be0e0fc 100644 --- a/marketing/tasks.py +++ b/marketing/tasks.py @@ -1,4 +1,13 @@ +import datetime +import hashlib +import pytz +import requests from celery.task import task +from django.conf import settings +from django.core.mail import EmailMessage +from django.template import Template, Context +from common.utils import convert_to_custom_timezone +from marketing.models import Contact, ContactList, Campaign, CampaignLog @task @@ -14,3 +23,152 @@ def campaign_open(request): @task def campaign_click(request): pass + + +@task +def upload_csv_file(data, user, contact_lists): + for each in data: + contact = Contact.objects.filter(email=each['email']).first() + if not contact: + contact = Contact.objects.create( + email=each['email'], created_by_id=user, + name=each['first name']) + if each.get('company name', None): + contact.company_name = each['company name'] + if each.get('last name', None): + contact.last_name = each['last name'] + if each.get('city', None): + contact.city = each['city'] + if each.get("state", None): + contact.state = each['state'] + contact.save() + for contact_list in contact_lists: + 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): + msg = EmailMessage( + subject, + content, + from_email, + to_email, + bcc, + reply_to=reply_to, + ) + for attachment in attachments: + msg.attach(*attachment) + msg.content_subtype = "html" + res = msg.send() + print (res) + + +def get_campaign_message_id(campaign): + hash_ = hashlib.md5() + hash_.update( + str(str(campaign.id) + str(campaign.campaign.created_by.id)).encode('utf-8') + + str(datetime.datetime.now()).encode('utf-8') + ) + file_hash = hash_.hexdigest() + return file_hash + + +@task +def run_campaign(campaign): + try: + campaign = Campaign.objects.get(id=campaign) + subject = campaign.subject + # project_settings = Settings.objects.filter().first() + + contacts = [] + for each_list in campaign.contact_lists.all(): + contacts = Contact.objects.filter(contact_list__in=[each_list]) + # contacts = campaign.contacts.filter(Q(is_bounced=False) | Q(is_unsubscribed=False)).distinct() + default_html = campaign.html_processed + for each_contact in contacts: + html = default_html + campaign_log = CampaignLog.objects.create(contact=each_contact, + campaign=campaign) + if campaign.reply_to_email: + reply_to_email = campaign.reply_to_email + else: + message_id = get_campaign_message_id(campaign_log) + campaign_log.message_id = message_id + campaign_log.save() + domain_name = settings.SUBDOMAIN_NAME + '.' + project_settings.domain if project_settings else '' + # domain_name = 'micro.peeljobs.com' + if campaign.from_email is not None: + from_email = campaign.from_email + else: + from_email = campaign.created_by.email + reply_to_email = str(from_email) + ' <' + str(message_id + '@' + domain_name + '') + '>' + if not (each_contact.is_bounced or each_contact.is_unsubscribed): + # link = '' + domain_url = '%s%s' % (settings.SCHEME, settings.HOST_URL) + link = '' + 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} + + html = Template(html).render(Context(names_dict)) + mail_html = html + link + from_email = str(campaign.from_name) + "<" + str(campaign.from_email) + '>' + # text_content = re.sub(r'<(.*?)>', '', html) + # ReplyMail.objects.create(campaign_log=campaign_log, + # reply_from=campaign.created_by.email, + # text=text_content, + # sent_on=datetime.datetime.now()) + send_campaign_mail( + subject, mail_html, from_email, [each_contact.email], [], [reply_to_email], []) + except Exception as e: + print (e) + pass + + +@task +def run_all_campaigns(): + start_date = datetime.date.today() + campaigns = Campaign.objects.filter(schedule_date_time__date=start_date) + for each in campaigns: + run_campaign(each.id) + + +@task +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() + + 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() + + +@task +def send_scheduled_campaigns(): + from datetime import datetime + campaigns = Campaign.objects.filter(schedule_date_time__isnull=False) + for each in campaigns: + sent_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + sent_time = datetime.strptime(sent_time, '%Y-%m-%d %H:%M:%S') + local_tz = pytz.timezone('UTC') + schedule_date_time_hour = str(each.schedule_date_time.time()).replace(':00:00', '') + sent_time = local_tz.localize(sent_time) + sent_time = convert_to_custom_timezone(sent_time, each.timezone) + if ( + str(each.schedule_date_time.date()) == str(sent_time.date()) and + str(schedule_date_time_hour) == str(sent_time.hour) + ): + run_campaign(each) diff --git a/marketing/tests.py b/marketing/tests.py index 7ce503c2..d5df681b 100644 --- a/marketing/tests.py +++ b/marketing/tests.py @@ -1,3 +1,68 @@ from django.test import TestCase - +from django.test import Client +from common.models import User +# from marketing.models import Tag, Document, ContactList, EmailTemplate, Contact, Campaign, CampaignLinkClick, CampaignLog, Link, CampaignOpen +# from .models import * +from marketing.views import * # Create your tests here. + + +class TestMarketingModel(object): + def setUp(self): + self.client = Client() + + self.user = User.objects.create( + username='jorge', email='j@mp.com', role="ADMIN") + self.user.set_password('jorge2293') + self.user.save() + + self.client.login(username='j@mp.com', password='jorge2293') + + +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/' + 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) diff --git a/marketing/urls.py b/marketing/urls.py index 3b5951ed..86adfebd 100644 --- a/marketing/urls.py +++ b/marketing/urls.py @@ -1,8 +1,9 @@ from django.urls import path from .views import ( dashboard, contact_lists, contacts_list, contact_list_new, contacts_list_new, contact_list_detail, edit_contact, - email_template_list, email_template_new, email_template_edit, - email_template_detail, campaign_list, campaign_new, campaign_edit, campaign_details + 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 ) app_name = 'marketing' @@ -14,16 +15,20 @@ urlpatterns = [ 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('cl/list/edit//', edit_contact_list, name='edit_contact_list'), + path('cl/list/delete//', delete_contact_list, name='delete_contact_list'), path('cl/list/cnew/', contacts_list_new, name='contacts_list_new'), path('cl/list/detail/', contact_list_detail, name='contact_list_detail'), 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//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('cm/list/', campaign_list, name='campaign_list'), path('cm/new/', campaign_new, name='campaign_new'), - path('cm/edit/', campaign_edit, name='campaign_edit'), - path('cm/details/', campaign_details, name='campaign_details') + path('cm//edit/', campaign_edit, name='campaign_edit'), + path('cm//details/', campaign_details, name='campaign_details'), + path('cm//delete/', campaign_delete, name='campaign_delete') ] diff --git a/marketing/views.py b/marketing/views.py index 31aee40d..8d829d42 100644 --- a/marketing/views.py +++ b/marketing/views.py @@ -1,61 +1,418 @@ -from django.shortcuts import render +import json +import pytz +import lxml +import lxml.html +from lxml.cssselect import CSSSelector +from django.conf import Settings +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.db.models import Q +from django.http.response import (JsonResponse, + HttpResponse, HttpResponseRedirect) +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse_lazy, reverse +from common import status +from marketing.models import ( + Tag, ContactList, Contact, EmailTemplate, Campaign, CampaignLog, Link +) +from marketing.forms import ContactListForm, ContactForm, EmailTemplateForm, SendCampaignForm +from marketing.tasks import upload_csv_file, run_campaign +TIMEZONE_CHOICES = [(tz, tz) for tz in pytz.common_timezones] + + +def get_exact_match(query, m2m_field, ids): + # query = tasks_list.annotate(count=Count(m2m_field))\ + # .filter(count=len(ids)) + query = query + for _id in ids: + query = query.filter(**{m2m_field: _id}) + return query + + +@login_required(login_url='/login') def dashboard(request): return render(request, 'marketing/dashboard.html') +@login_required(login_url='/login') def contact_lists(request): - return render(request, 'marketing/lists/index.html') + tags = Tag.objects.all() + if (request.user.is_superuser): + queryset = ContactList.objects.all() + else: + queryset = ContactList.objects.filter(created_by=request.user) + if request.method == 'POST': + post_tags = request.POST.get('tags') + if request.POST.get('search'): + queryset = queryset.filter( + Q(name__icontains=request.POST['search']) | + Q(created_by__email__icontains=request.POST[ + 'search'])).distinct() + + if post_tags: + + filtered_list = json.loads(request.POST['tags']) + + 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} + return render(request, 'marketing/lists/index.html', data) +@login_required(login_url='/login') def contacts_list(request): - return render(request, 'marketing/lists/all.html') + contacts = Contact.objects.all() + data = {"contacts": contacts} + return render(request, 'marketing/lists/all.html', data) +@login_required(login_url='/login') def contact_list_new(request): - return render(request, 'marketing/lists/new.html') + data = {} + if request.method == "POST": + form = ContactListForm(request.POST, request.FILES) + if form.is_valid(): + instance = form.save(commit=False) + instance.created_by = request.user + instance.save() + 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) + if request.FILES.get('contacts_file'): + upload_csv_file.delay( + form.validated_rows, request.user.id, [instance.id]) + + return JsonResponse({'error': False, + 'data': form.data}, + status=status.HTTP_201_CREATED) + else: + # return JsonResponse({'error': True, 'errors': form.errors}, + # status=status.HTTP_400_BAD_REQUEST) + return JsonResponse( + {'error': True, 'errors': form.errors}, + status=status.HTTP_200_OK) + else: + return render(request, 'marketing/lists/new.html', data) +@login_required(login_url='/login') +def edit_contact_list(request, pk): + user = request.user + try: + contact_list = ContactList.objects.get(pk=pk) + except ContactList.DoesNotExist: + contact_list = ContactList.objects.none() + return JsonResponse({}, status=status.HTTP_404_NOT_FOUND) + + if request.method == 'GET': + # contact_lists = ContactList.objects.filter(company=request.company) + if (user.is_superuser): + contact_lists = ContactList.objects.all() + else: + contact_lists = ContactList.objects.filter(created_by=request.user) + + data = {'contact_list': contact_list, 'contact_lists': contact_lists} + return render(request, 'marketing/lists/new.html', data) + else: + form = ContactListForm( + request.POST, request.FILES, instance=contact_list) + if form.is_valid(): + instance = form.save() + instance.tags.clear() + 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) + if request.FILES.get('contacts_file'): + upload_csv_file.delay( + form.validated_rows, request.user.id, [instance.id]) + + return JsonResponse({'error': False, + 'data': form.data}, status=status.HTTP_200_OK) + return JsonResponse({'error': True, + 'errors': form.errors}, status=status.HTTP_200_OK) + + +@login_required(login_url='/login') +def view_contact_list(request, pk): + contact_list = get_object_or_404(ContactList, pk=pk) + contacts = Contact.objects.filter(contact_list__in=[contact_list]) + if request.POST and request.POST.get("search"): + contacts = contacts.filter( + Q(name__icontains=request.POST['search']) | + Q(email__icontains=request.POST['search'])) + + per_page = request.GET.get("per_page", 10) + paginator = Paginator(contacts.order_by('id'), per_page) + page = request.GET.get('page', 1) + try: + contacts = paginator.page(page) + except PageNotAnInteger: + contacts = paginator.page(1) + except EmptyPage: + contacts = paginator.page(paginator.num_pages) + + data = {'contact_list': contact_list, "contacts": contacts} + template_name = "marketing/lists/all.html" + return render(request, template_name, data) + + +@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') + return HttpResponseRedirect(redirect_to) + + +@login_required(login_url='/login') def contacts_list_new(request): - return render(request, 'marketing/lists/cnew.html') + if request.POST: + form = ContactForm(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + instance.created_by = request.user + instance.save() + for each in json.loads(request.POST['contact_list']): + instance.contact_list.add(ContactList.objects.get(id=each)) + # return JsonResponse(form.data, status=status.HTTP_201_CREATED) + return reverse('marketing:contact_list') + else: + data = {'error': True, 'errors': form.errors} + else: + if (request.user.is_superuser): + queryset = ContactList.objects.all() + else: + queryset = ContactList.objects.filter(created_by=request.user) + data = {"contact_list": queryset} + return render(request, 'marketing/lists/cnew.html', data) +@login_required(login_url='/login') def edit_contact(request): return render(request, 'marketing/lists/edit_contact.html') +@login_required(login_url='/login') def contact_list_detail(request): return render(request, 'marketing/lists/detail.html') +@login_required(login_url='/login') def email_template_list(request): - return render(request, 'marketing/email_template/index.html') + if (request.user.is_admin or request.user.is_superuser): + queryset = EmailTemplate.objects.all() + else: + queryset = EmailTemplate.objects.filter( + created_by=request.user) + 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'])) + + 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} + return render(request, 'marketing/email_template/index.html', data) +@login_required(login_url='/login') def email_template_new(request): - return render(request, 'marketing/email_template/new.html') + if request.POST: + form = EmailTemplateForm(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + instance.created_by = request.user + instance.save() + return JsonResponse({'error': False, 'data': form.data}, status=status.HTTP_201_CREATED) + else: + return JsonResponse({'error': True, 'errors': form.errors}, status=status.HTTP_200_OK) + else: + return render(request, 'marketing/email_template/new.html') -def email_template_edit(request): - return render(request, 'marketing/email_template/edit.html') +@login_required(login_url='/login') +def email_template_edit(request, pk): + email_template = get_object_or_404(EmailTemplate, pk=pk) + + if request.method == 'GET': + data = {'email_template': email_template} + return render(request, 'marketing/email_template/new.html', data) + else: + form = EmailTemplateForm(request.POST, instance=email_template) + if form.is_valid(): + instance = form.save(commit=False) + # instance.created_by = request.user + instance.save() + return JsonResponse({'error': False, 'data': form.data}, status=status.HTTP_201_CREATED) + return JsonResponse({'error': True, 'errors': form.errors}, status=status.HTTP_200_OK) -def email_template_detail(request): - return render(request, 'marketing/email_template/detail.html') +@login_required(login_url='/login') +def email_template_detail(request, pk): + queryset = get_object_or_404(EmailTemplate, id=pk) + data = {'email_template': queryset} + return render(request, 'marketing/email_template/details.html', data) +@login_required(login_url='/login') +def email_template_delete(request, pk): + try: + # EmailTemplate.objects.get(id=pk).delete() + redirect_to = reverse('marketing:email_template_list') + except ContactList.DoesNotExist: + redirect_to = reverse('marketing:email_template_list') + return HttpResponseRedirect(redirect_to) + + +@login_required(login_url='/login') def campaign_list(request): - return render(request, 'marketing/campaign/index.html') + campaigns = Campaign.objects.all() + 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} + return render(request, 'marketing/campaign/index.html', data) +@login_required(login_url='/login') def campaign_new(request): - return render(request, 'marketing/campaign/new.html') + if request.method == 'GET': + if request.user.is_admin or request.user.is_superuser: + email_templates = EmailTemplate.objects.all() + else: + email_templates = EmailTemplate.objects.filter( + created_by=request.user) + if request.user.is_admin or request.user.is_superuser: + contact_lists = ContactList.objects.all() + else: + contact_lists = ContactList.objects.filter( + created_by=request.user) + # if request.GET.get('contact_list'): + # contacts = Contact.objects.filter( + # contact_list__id__in=json.loads(request.GET.get('contact_list'))).distinct() + + data = { + 'contact_lists': contact_lists, 'email_templates': email_templates, "timezones": TIMEZONE_CHOICES} + # return JsonResponse(data, status=status.HTTP_200_OK) + return render(request, 'marketing/campaign/new.html', data) + else: + print ("request.POST", request.POST) + form = SendCampaignForm(request.POST) + if form.is_valid(): + instance = form.save(commit=False) + instance.created_by = request.user + if request.POST['from_email']: + instance.from_email = request.POST['from_email'] + if request.POST['from_name']: + instance.from_name = request.POST['from_name'] + if request.POST['reply_to_email']: + instance.reply_to_email = request.POST['reply_to_email'] + instance.save() + for each in json.loads(request.POST['contact_list']): + instance.contact_lists.add(ContactList.objects.get(id=each)) + # contacts = Contact.objects.filter(contact_list__in=json.loads(request.POST['contact_list'])).distinct() + # for each_contact in contacts: + # instance.contacts.add(each_contact) + + camp = instance + + html = camp.html + dom = lxml.html.document_fromstring(html) + selAnchor = CSSSelector('a') + links = selAnchor(dom) + llist = [] + # Extract links and unique links + for e in links: + llist.append(e.get('href')) + + # get uniaue linnk + links = set(llist) + + # Replace Links with new One + domain_url = '%s://%s' % (request.scheme, request.META['HTTP_HOST']) + if Link.objects.filter(campaign_id=camp.id): + Link.objects.filter(campaign_id=camp.id).delete() + for l in links: + link = Link.objects.create(campaign_id=camp.id, original=l) + html = html.replace( + 'href="' + l + '"', 'href="' + domain_url + '/a/' + str(link.id) + '/e/{{email_id}}"') + camp.html_processed = html + camp.sent_status = "scheduled" + camp.save() + + if request.POST['schedule_later'] == 'false': + run_campaign.delay(instance.id) + else: + schedule_date_time = request.POST.get('schedule_date_time', '') + # user_timezone = request.POST.get('timezone', '') + # if user_timezone and schedule_date_time: + # print ("schedule_date_time", schedule_date_time) + # print ("timezone", user_timezone) + # start_time_object = ddatetime.strptime(schedule_date_time, '%Y-%m-%d %H:%M:%S') + # schedule_date_time = convert_to_custom_timezone(start_time_object, user_timezone, to_utc=True) + # print ("After", schedule_date_time) + instance.schedule_date_time = schedule_date_time + # instance.timezone = request.POST['timezone'] + instance.save() + + return JsonResponse({'error': True, 'data': form.data}, status=status.HTTP_201_CREATED) + return JsonResponse({'error': True, 'errors': form.errors}, status=status.HTTP_200_OK) +@login_required(login_url='/login') def campaign_edit(request): return render(request, 'marketing/campaign/edit.html') +@login_required(login_url='/login') def campaign_details(request): return render(request, 'marketing/campaign/details.html') + + +@login_required(login_url='/login') +def campaign_delete(request): + return render(request, 'marketing/campaign/details.html') diff --git a/opportunity/models.py b/opportunity/models.py index 850829d3..5efe18a0 100644 --- a/opportunity/models.py +++ b/opportunity/models.py @@ -12,15 +12,19 @@ class Opportunity(models.Model): name = models.CharField(pgettext_lazy( "Name of Opportunity", "Name"), max_length=64) account = models.ForeignKey( - Account, related_name='opportunities', on_delete=models.CASCADE, blank=True, null=True) + Account, related_name='opportunities', + on_delete=models.CASCADE, blank=True, null=True) stage = models.CharField( - pgettext_lazy("Stage of Opportunity", "Stage"), max_length=64, choices=STAGES) + pgettext_lazy("Stage of Opportunity", + "Stage"), max_length=64, choices=STAGES) currency = models.CharField( max_length=3, choices=CURRENCY_CODES, blank=True, null=True) amount = models.DecimalField( - _("Opportunity Amount"), decimal_places=2, max_digits=12, blank=True, null=True) + _("Opportunity Amount"), + decimal_places=2, max_digits=12, blank=True, null=True) lead_source = models.CharField( - _("Source of Lead"), max_length=255, choices=SOURCES, blank=True, null=True) + _("Source of Lead"), max_length=255, + choices=SOURCES, blank=True, null=True) probability = models.IntegerField(default=0, blank=True, null=True) contacts = models.ManyToManyField(Contact) closed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) @@ -30,7 +34,8 @@ class Opportunity(models.Model): assigned_to = models.ManyToManyField( User, related_name='opportunity_assigned_to') created_by = models.ForeignKey( - User, related_name='opportunity_created_by', on_delete=models.SET_NULL, null=True) + User, related_name='opportunity_created_by', + on_delete=models.SET_NULL, null=True) created_on = models.DateTimeField(_("Created on"), auto_now_add=True) is_active = models.BooleanField(default=False) tags = models.ManyToManyField(Tags, blank=True) diff --git a/opportunity/tests.py b/opportunity/tests.py index 63f264a5..6ec6a51c 100644 --- a/opportunity/tests.py +++ b/opportunity/tests.py @@ -12,22 +12,24 @@ from cases.models import Case class OpportunityModel(object): def setUp(self): - self.user = User.objects.create(first_name="madhurima", username='madhurima', email="m@mp.com", role="ADMIN") + self.user = User.objects.create( + first_name="meghan", username='meghan', + email="m@mp.com", role="ADMIN") self.user.set_password('madhu123') self.user.save() self.address = Address.objects.create( - street="kphb", city="hyderabad", postcode="584", + street="kphb", city="canada", postcode="584", country='IN') self.client.login(email='m@mp.com', password='madhu123') self.account = Account.objects.create( - name="uday", email="uday@gmail.com", + name="john", email="john@gmail.com", phone="58964", billing_address_line="", - billing_street="kphb", billing_city="hyderabad", - billing_postcode="584", billing_country='IN', + billing_street="kphb", billing_city="canada", + billing_postcode="584", billing_country='US', website="hello.com", industry="sw", description="bgyyr", created_by=self.user) @@ -40,7 +42,7 @@ class OpportunityModel(object): created_by=self.user) self.opportunity = Opportunity.objects.create( - name="madhurima", amount="478", + name="meghan", amount="478", stage="negotiation/review", lead_source="Call", probability="58", closed_on="2016-05-04", description="hgfdxc", @@ -58,7 +60,7 @@ class OpportunityModel(object): class OpportunityCreateTestCase(OpportunityModel, TestCase): def test_opportunity_create(self): response = self.client.get('/opportunities/create/', { - 'name': "madhurima", 'amount': "478", + 'name': "meghan", 'amount': "478", 'stage': "NEGOTIATION/REVIEW", 'lead_source': "Call", 'probability': "58", 'closed_on': "2016-05-04", 'description': "hgfdxc"}) @@ -66,14 +68,14 @@ class OpportunityCreateTestCase(OpportunityModel, TestCase): def test_opportunity_create_post(self): url = '/opportunities/create/' - data = {'name': "meghana", 'amount': "500", 'stage': "CLOSED WON"} + data = {'name': "micky", 'amount': "500", 'stage': "CLOSED WON"} response = self.client.post(url, data) # self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 200) def test_opportunity_invalid(self): url = '/opportunities/create/' - data = {'name': "meghana", 'amount': "", 'stage': ""} + data = {'name': "micky", 'amount': "", 'stage': ""} response = self.client.post(url, data) self.assertEqual(response.status_code, 200) @@ -81,12 +83,15 @@ class OpportunityCreateTestCase(OpportunityModel, TestCase): class opportunityCreateTestCase(OpportunityModel, TestCase): def test_view_opportunity(self): - response = self.client.get('/opportunities/' + str(self.opportunity.id) + '/view/') + response = self.client.get( + '/opportunities/' + str(self.opportunity.id) + '/view/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['opportunity_record'].id, self.opportunity.id) + self.assertEqual( + response.context['opportunity_record'].id, self.opportunity.id) def test_del_opportunity_url(self): - response = self.client.get('/opportunities/' + str(self.opportunity.id) + '/delete/') + response = self.client.get( + '/opportunities/' + str(self.opportunity.id) + '/delete/') self.assertEqual(response['location'], '/opportunities/list/') def test_opportunity_delete(self): @@ -98,21 +103,25 @@ class opportunityCreateTestCase(OpportunityModel, TestCase): class EditOpportunityTestCase(OpportunityModel, TestCase): def test_edit_url(self): - response = self.client.get('/opportunities/' + str(self.opportunity.id) + '/edit/') + response = self.client.get( + '/opportunities/' + str(self.opportunity.id) + '/edit/') self.assertEqual(response.status_code, 200) def test_edit_opportunity(self): response = self.client.get('/opportunities/' + str(self.opportunity.id) + '/edit/', { - 'name': "madhurima", 'amount': "478", + 'name': "meghan", 'amount': "478", 'stage': "negotiation/review", 'lead_source': "Call", 'probability': "58", 'closed_on': "2016-05-04", 'description': "hgfdxc"}) - self.assertEqual(response.status_code, 200) + resp = self.client.post( '/opportunities/' + str(self.opportunity.id) + '/edit/', **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + self.assertEqual(resp.status_code, 200) + resp1 = self.client.post( '/opportunities/create/', **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + self.assertEqual(resp1.status_code, 200) def test_update_opportunity(self): url = '/opportunities/' + str(self.opportunity.id) + '/edit/' data = { - 'name': "madhurima", 'amount': "478", 'stage': "QUALIFICATION", + 'name': "meghan", 'amount': "478", 'stage': "QUALIFICATION", 'probability': "58", 'closed_on': "2016-05-04", 'description': "hgfdxc"} response = self.client.post(url, data) # self.assertEqual(response.status_code, 302) @@ -137,14 +146,18 @@ class OpportunityListView(OpportunityModel, TestCase): def test_opportunity_list_queryset(self): self.account = Account.objects.all() data = { - 'name': 'madhurima', 'stage': 'city', + 'name': 'meghan', 'stage': 'city', 'lead_source': 'Call', 'accounts': self.account} response = self.client.post('/opportunities/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)) get_contact = Contact.objects.get(last_name="s") + # get_account = Account.objects.get(name='john') # not done + # print(get_account.name,"wqieoruopwqeiruqrewiou") + # print(self.account.last()) self.assertEqual(get_contact.last_name, "s") + # self.assertEqual(get_account, self.account.last()) # not done # self.assertEqual(get_opp_val.lead_source,'call') self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'opportunity.html') @@ -188,3 +201,12 @@ class AttachmentTestCase(OpportunityModel, TestCase): # data = {'attachment_id':self.attachment.id} # response = self.client.post(url,data) # self.assertEqual(response.status_code, 200) + + +class TestGetOpportunitiesView(OpportunityModel, TestCase): + def test_get_page(self): + url = "/opportunities/get/list/" + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIsNotNone(resp.context['opportunities']) + diff --git a/opportunity/urls.py b/opportunity/urls.py index adf7c65c..fd1449d8 100644 --- a/opportunity/urls.py +++ b/opportunity/urls.py @@ -1,7 +1,10 @@ from django.urls import path from opportunity.views import ( - OpportunityListView, create_opportunity, OpportunityDetailView, update_opportunity, - DeleteOpportunityView, GetContactView, GetOpportunitiesView, AddCommentView, + OpportunityListView, create_opportunity, + OpportunityDetailView, update_opportunity, + DeleteOpportunityView, GetContactView, + GetOpportunitiesView, + AddCommentView, UpdateCommentView, DeleteCommentView, AddAttachmentsView) @@ -13,16 +16,19 @@ urlpatterns = [ path('create/', create_opportunity, name='save'), path('/view/', OpportunityDetailView.as_view(), name="opp_view"), path('/edit/', update_opportunity, name="opp_edit"), - path('/delete/', DeleteOpportunityView.as_view(), name="opp_remove"), + path('/delete/', + DeleteOpportunityView.as_view(), name="opp_remove"), path('contacts/', GetContactView.as_view(), name="contacts"), path('get/list/', GetOpportunitiesView.as_view(), name="get_opportunity"), 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('comment/remove/', + DeleteCommentView.as_view(), name="remove_comment"), - path('attachment/add/', AddAttachmentsView.as_view(), name="add_attachment"), + path('attachment/add/', + AddAttachmentsView.as_view(), name="add_attachment"), path('attachment/remove/', DeleteCommentView.as_view(), name="remove_attachment"), ] diff --git a/opportunity/views.py b/opportunity/views.py index 2fea5111..02a75aba 100644 --- a/opportunity/views.py +++ b/opportunity/views.py @@ -5,12 +5,14 @@ from django.core.mail import EmailMessage from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string -from django.views.generic import CreateView, UpdateView, DetailView, ListView, TemplateView, View +from django.views.generic import (CreateView, DetailView, + ListView, TemplateView, View) from accounts.models import Account, Tags from common.models import User, Comment, Attachments from common.utils import STAGES, SOURCES, CURRENCY_CODES from contacts.models import Contact -from opportunity.forms import OpportunityForm, OpportunityCommentForm, OpportunityAttachmentForm +from opportunity.forms import (OpportunityForm, OpportunityCommentForm, + OpportunityAttachmentForm) from opportunity.models import Opportunity from django.urls import reverse from django.db.models import Q @@ -23,10 +25,13 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): template_name = "opportunity.html" def get_queryset(self): - queryset = self.model.objects.all().prefetch_related("contacts", "account") - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + queryset = self.model.objects.all().prefetch_related( + "contacts", "account") + if self.request.user.role != "ADMIN" and not \ + self.request.user.is_superuser: queryset = queryset.filter( - Q(assigned_to__in=[self.request.user]) | Q(created_by=self.request.user.id)) + Q(assigned_to__in=[self.request.user]) | + Q(created_by=self.request.user.id)) request_post = self.request.POST if request_post: @@ -58,7 +63,8 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): search = False if ( self.request.POST.get('name') or self.request.POST.get('stage') or - self.request.POST.get('lead_source') or self.request.POST.get('account') or + self.request.POST.get('lead_source') or + self.request.POST.get('account') or self.request.POST.get('contacts') ): search = True @@ -72,9 +78,6 @@ class OpportunityListView(LoginRequiredMixin, TemplateView): def create_opportunity(request): - template_name = "create_opportunity.html" - - users = User.objects.filter(is_active=True).order_by('email') accounts = Account.objects.filter(status="open") contacts = Contact.objects.all() if request.user.role != "ADMIN" and not request.user.is_superuser: @@ -83,9 +86,9 @@ def create_opportunity(request): contacts = Contact.objects.filter( Q(assigned_to__in=[request.user]) | Q(created_by=request.user)) - kwargs_data = {"assigned_to": users, "account": accounts, "contacts": contacts} - form = OpportunityForm(**kwargs_data) - + kwargs_data = { + "assigned_to": User.objects.filter(is_active=True).order_by('email'), + "account": accounts, "contacts": contacts} if request.POST: form = OpportunityForm(request.POST, request.FILES, **kwargs_data) if form.is_valid(): @@ -102,13 +105,15 @@ def create_opportunity(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]) + 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', []): @@ -138,31 +143,29 @@ def create_opportunity(request): success_url = reverse("opportunities:save") if request.POST.get('from_account'): from_account = request.POST.get('from_account') - success_url = reverse("accounts:view_account", pk=from_account) + success_url = reverse("accounts:view_account", kwargs={ + 'pk': from_account}) + print(success_url) return JsonResponse({'error': False, 'success_url': success_url}) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["opportunity_form"] = OpportunityForm(**kwargs_data) - else: - return JsonResponse({'error': True, 'errors': form.errors}) + context["accounts"] = accounts + if request.GET.get('view_account'): + context['account'] = get_object_or_404( + Account, id=request.GET.get('view_account')) + context["contacts"] = contacts + context["users"] = kwargs_data['assigned_to'] + context["currencies"] = CURRENCY_CODES + context["stages"] = STAGES + context["sources"] = SOURCES + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] - else: - context = {} - context["opportunity_form"] = form - - context["accounts"] = accounts - if request.GET.get('view_account'): - context['account'] = get_object_or_404( - Account, id=request.GET.get('view_account')) - context["contacts"] = contacts - context["users"] = users - context["currencies"] = CURRENCY_CODES - context["stages"] = STAGES - context["sources"] = SOURCES - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - - context["contacts_list"] = [ - int(i) for i in request.POST.getlist('contacts', []) if i] - return render(request, template_name, context) + context["contacts_list"] = [ + int(i) for i in request.POST.getlist('contacts', []) if i] + return render(request, "create_opportunity.html", context) class OpportunityDetailView(LoginRequiredMixin, DetailView): @@ -177,10 +180,13 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super(OpportunityDetailView, self).get_context_data(**kwargs) - user_assgn_list = [assigned_to.id for assigned_to in context['object'].assigned_to.all()] + user_assgn_list = [ + assigned_to.id for assigned_to in + context['object'].assigned_to.all()] if self.request.user == context['object'].created_by: user_assgn_list.append(self.request.user.id) - if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if self.request.user.role != "ADMIN" and not \ + self.request.user.is_superuser: if self.request.user.id not in user_assgn_list: raise PermissionDenied assigned_data = [] @@ -192,15 +198,15 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView): comments = context["opportunity_record"].opportunity_comments.all() context.update({ - "comments": comments, 'attachments': context["opportunity_record"].opportunity_attachment.all(), + "comments": comments, + 'attachments': context[ + "opportunity_record"].opportunity_attachment.all(), "assigned_data": json.dumps(assigned_data)}) return context def update_opportunity(request, pk): - template_name = "create_opportunity.html" opportunity_object = Opportunity.objects.filter(pk=pk).first() - users = User.objects.filter(is_active=True).order_by('email') accounts = Account.objects.filter(status="open") contacts = Contact.objects.all() if request.user.role != "ADMIN" and not request.user.is_superuser: @@ -209,13 +215,18 @@ def update_opportunity(request, pk): contacts = Contact.objects.filter( Q(assigned_to__in=[request.user]) | Q(created_by=request.user)) - kwargs_data = {"assigned_to": users, "account": accounts, "contacts": contacts} + kwargs_data = { + "assigned_to": User.objects.filter(is_active=True).order_by('email'), + "account": accounts, "contacts": contacts} form = OpportunityForm(instance=opportunity_object, **kwargs_data) if request.POST: - form = form = OpportunityForm(request.POST, request.FILES, instance=opportunity_object, **kwargs_data) + form = form = OpportunityForm( + request.POST, request.FILES, + instance=opportunity_object, **kwargs_data) if form.is_valid(): - assigned_to_ids = opportunity_object.assigned_to.all().values_list('id', flat=True) + assigned_to_ids = opportunity_object.assigned_to.all().values_list( + 'id', flat=True) opportunity_obj = form.save(commit=False) if request.POST.get('stage') in ['CLOSED WON', 'CLOSED LOST']: opportunity_obj.closed_by = request.user @@ -228,17 +239,19 @@ def update_opportunity(request, pk): assigned_form_users = form.cleaned_data.get( 'assigned_to').values_list('id', flat=True) all_members_list = list( - set(list(assigned_form_users)) - set(list(assigned_to_ids))) - if len(all_members_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 - }) + 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" @@ -276,37 +289,35 @@ def update_opportunity(request, pk): success_url = reverse('opportunities:list') if request.POST.get('from_account'): from_account = request.POST.get('from_account') - success_url = reverse("accounts:view_account", pk=from_account) + success_url = reverse("accounts:view_account", kwargs={ + 'pk': from_account}) return JsonResponse({'error': False, 'success_url': success_url}) - - else: - return JsonResponse({'error': True, 'errors': form.errors}) - else: - context = {} - context["opportunity_obj"] = opportunity_object - user_assgn_list = [ - assigned_to.id for assigned_to in context["opportunity_obj"].assigned_to.all()] - if request.user == context['opportunity_obj'].created_by: - user_assgn_list.append(request.user.id) - if request.user.role != "ADMIN" and not request.user.is_superuser: - if request.user.id not in user_assgn_list: - raise PermissionDenied - context["opportunity_form"] = form - context["accounts"] = accounts - if request.GET.get('view_account'): - context['account'] = get_object_or_404( - Account, id=request.GET.get('view_account')) - context["contacts"] = contacts - context["users"] = users - context["currencies"] = CURRENCY_CODES - context["stages"] = STAGES - context["sources"] = SOURCES - context["assignedto_list"] = [ - int(i) for i in request.POST.getlist('assigned_to', []) if i] - context["contacts_list"] = [ - int(i) for i in request.POST.getlist('contacts', []) if i] - - return render(request, template_name, context) + return JsonResponse({'error': True, 'errors': form.errors}) + context = {} + context["opportunity_obj"] = opportunity_object + user_assgn_list = [ + assigned_to.id for assigned_to in + context["opportunity_obj"].assigned_to.all()] + if request.user == context['opportunity_obj'].created_by: + user_assgn_list.append(request.user.id) + if request.user.role != "ADMIN" and not request.user.is_superuser: + if request.user.id not in user_assgn_list: + raise PermissionDenied + context["opportunity_form"] = form + context["accounts"] = accounts + if request.GET.get('view_account'): + context['account'] = get_object_or_404( + Account, id=request.GET.get('view_account')) + context["contacts"] = contacts + context["users"] = kwargs_data['assigned_to'] + context["currencies"] = CURRENCY_CODES + context["stages"] = STAGES + context["sources"] = SOURCES + context["assignedto_list"] = [ + int(i) for i in request.POST.getlist('assigned_to', []) if i] + context["contacts_list"] = [ + int(i) for i in request.POST.getlist('contacts', []) if i] + return render(request, "create_opportunity.html", context) class DeleteOpportunityView(LoginRequiredMixin, View): @@ -316,7 +327,9 @@ class DeleteOpportunityView(LoginRequiredMixin, View): def post(self, request, *args, **kwargs): self.object = get_object_or_404(Opportunity, id=kwargs.get("pk")) - if self.request.user.role == "ADMIN" or self.request.user.is_superuser or self.request.user == self.object.created_by: + if (self.request.user.role == "ADMIN" or + self.request.user.is_superuser or + self.request.user == self.object.created_by): self.object.delete() if request.is_ajax(): return JsonResponse({'error': False}) @@ -326,8 +339,7 @@ class DeleteOpportunityView(LoginRequiredMixin, View): return redirect("accounts:view_account", pk=account) return redirect("opportunities:list") - else: - raise PermissionDenied + raise PermissionDenied class GetContactView(LoginRequiredMixin, View): @@ -339,7 +351,8 @@ class GetContactView(LoginRequiredMixin, View): contacts = account.contacts.all() else: contacts = Contact.objects.all() - data = {contact.pk: contact.first_name for contact in contacts.distinct()} + data = {contact.pk: + contact.first_name for contact in contacts.distinct()} return JsonResponse(data) @@ -354,7 +367,8 @@ class AddCommentView(LoginRequiredMixin, CreateView): Opportunity, id=request.POST.get('opportunityid')) if ( request.user in self.opportunity.assigned_to.all() or - request.user == self.opportunity.created_by or request.user.is_superuser or + request.user == self.opportunity.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -427,11 +441,6 @@ class GetOpportunitiesView(LoginRequiredMixin, ListView): context_object_name = "opportunities" template_name = "opportunities_list.html" - def get_context_data(self, **kwargs): - context = super(GetOpportunitiesView, self).get_context_data(**kwargs) - context["opportunities"] = self.get_queryset() - return context - class AddAttachmentsView(LoginRequiredMixin, CreateView): model = Attachments @@ -444,7 +453,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): Opportunity, id=request.POST.get('opportunityid')) if ( request.user in self.opportunity.assigned_to.all() or - request.user == self.opportunity.created_by or request.user.is_superuser or + request.user == self.opportunity.created_by or + request.user.is_superuser or request.user.role == 'ADMIN' ): form = self.get_form() @@ -467,7 +477,8 @@ class AddAttachmentsView(LoginRequiredMixin, CreateView): "attachment_url": attachment.attachment.url, "created_on": attachment.created_on, "created_by": attachment.created_by.email, - "download_url": reverse('common:download_attachment', kwargs={'pk': attachment.id}), + "download_url": reverse('common:download_attachment', + kwargs={'pk': attachment.id}), "attachment_display": attachment.get_file_type_display() }) @@ -480,10 +491,13 @@ 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'): + if (request.user == self.object.created_by or + request.user.is_superuser or + request.user.role == 'ADMIN'): self.object.delete() data = {"aid": request.POST.get("attachment_id")} return JsonResponse(data) - data = {'error': "You don't have permission to delete this attachment."} + data = { + 'error': "You don't have permission to delete this attachment."} return JsonResponse(data) diff --git a/planner/forms.py b/planner/forms.py index 0e7ef03c..9bfeb773 100644 --- a/planner/forms.py +++ b/planner/forms.py @@ -57,8 +57,10 @@ class ReminderForm(forms.ModelForm): model = Reminder fields = '__all__' - reminder_type = forms.ChoiceField(choices=(('Email', 'Email'), ('Popup', 'Popup')), - widget=forms.Select(attrs={'class': 'form-control input-sm'})) + reminder_type = forms.ChoiceField(choices=( + ('Email', 'Email'), ('Popup', 'Popup')), + widget=forms.Select(attrs={ + 'class': 'form-control input-sm'})) reminder_time = forms.ChoiceField(choices=(("0", "on time"), ("60", "1m before"), ("120", "2m before"), @@ -71,7 +73,9 @@ class ReminderForm(forms.ModelForm): ("10800", "3h before"), ("18000", "5h before"), ("86400", "1d before") - ), widget=forms.Select(attrs={'class': 'form-control input-sm'})) + ), + widget=forms.Select(attrs={ + 'class': 'form-control input-sm'})) # class MeetingEventForm(EventForm): diff --git a/planner/models.py b/planner/models.py index 3b7de05b..6dfe8284 100644 --- a/planner/models.py +++ b/planner/models.py @@ -13,8 +13,9 @@ from common.utils import EVENT_PARENT_TYPE, EVENT_STATUS class Reminder(models.Model): reminder_type = models.CharField(max_length=5, blank=True, null=True) - reminder_time = models.IntegerField(pgettext_lazy("time of the reminder to event in Seconds", "Reminder"), - blank=True, null=True) + reminder_time = models.IntegerField(pgettext_lazy( + "time of the reminder to event in Seconds", "Reminder"), + blank=True, null=True) def __str__(self): return self.reminder_type @@ -28,9 +29,13 @@ class Event(models.Model): name = models.CharField( pgettext_lazy("Name of the Event", "Event"), max_length=64) - event_type = models.CharField(_("Type of the event"), max_length=7) # call meeting task - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, blank=True, null=True, - limit_choices_to=limit, choices=EVENT_PARENT_TYPE) + event_type = models.CharField( + _("Type of the event"), max_length=7) # call meeting task + content_type = models.ForeignKey(ContentType, + on_delete=models.CASCADE, + blank=True, null=True, + limit_choices_to=limit, + choices=EVENT_PARENT_TYPE) object_id = models.PositiveIntegerField(blank=True, null=True) parent = GenericForeignKey('content_type', 'object_id') status = models.CharField( @@ -42,22 +47,28 @@ class Event(models.Model): start_date = models.DateField(default=None) close_date = models.DateField(default=None, null=True) - duration = models.IntegerField(pgettext_lazy("Duration of the Event in Seconds", "Durations"), default=None, - null=True) # not for tasks + duration = models.IntegerField(pgettext_lazy( + "Duration of the Event in Seconds", "Durations"), default=None, + null=True) # not for tasks reminders = models.ManyToManyField(Reminder, blank=True) priority = models.CharField(max_length=10, blank=True) # only for task updated_on = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey( - User, on_delete=models.SET_NULL, null=True, blank=True, related_name='updated_user') + User, on_delete=models.SET_NULL, null=True, + blank=True, related_name='updated_user') attendees_user = models.ManyToManyField(User, blank=True, related_name='attendees_user') attendees_contacts = models.ManyToManyField(Contact, blank=True, - related_name='attendees_contact') + related_name='attendees_contact' + ) attendees_leads = models.ManyToManyField(Lead, blank=True, related_name='attendees_lead') created_on = models.DateTimeField(_("Created on"), auto_now_add=True) - created_by = models.ForeignKey(User, related_name='event_created_by', on_delete=models.SET_NULL, null=True) - assigned_to = models.ManyToManyField(User, blank=True, related_name='event_assigned_users') + created_by = models.ForeignKey( + User, related_name='event_created_by', + on_delete=models.SET_NULL, null=True) + assigned_to = models.ManyToManyField( + User, blank=True, related_name='event_assigned_users') description = models.TextField(blank=True, null=True) is_active = models.BooleanField(default=False) diff --git a/planner/tests.py b/planner/tests.py index a4b8c395..51ae38d6 100644 --- a/planner/tests.py +++ b/planner/tests.py @@ -1,8 +1,8 @@ -import datetime -from django.utils import timezone -from django.test import TestCase -from planner.models import Event -from django.contrib.auth.models import User +# import datetime +# from django.utils import timezone +# from django.test import TestCase +# from planner.models import Event +# from django.contrib.auth.models import User # class EventObjects(object): diff --git a/requirements.txt b/requirements.txt index 3644dfab..381420ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,6 @@ billiard==3.5.0.5 celery==4.2.1 kombu==4.3.0 vine==1.2.0 - +openpyxl==2.6.0 +lxml==4.3.2 +cssselect==1.0.3 diff --git a/static/css/main.scss b/static/css/main.scss index 20849b55..57b3fb5c 100644 --- a/static/css/main.scss +++ b/static/css/main.scss @@ -1,16 +1,16 @@ @import "ref.scss"; *{ - font-family: 'Roboto', sans-serif; + font-family: 'Quicksand', sans-serif; } body { - font-family: 'Roboto', sans-serif; + font-family: 'Quicksand', sans-serif; padding: 0; font-weight: 500; margin: 0; - padding-top:60px; - font-size: 15px; + font-size: 14px; + background: #f7f7f9; @media(min-width:$large){ - font-size:18px; + font-size:17px; } @media(min-width: $mob_min) and (max-width: $smob_max){ padding-top: 50px; @@ -45,9 +45,9 @@ body { font-weight: 600; color: #333; span { - color: $color_cyan_aqua_approx; + color: $sky_color; &.create_head { - color: $color_screamin_green_approx; + color: $sky_color; } } } @@ -128,21 +128,21 @@ a{ /* inner_pages starts here */ -.main_body_bg_color{ +/*.main_body_bg_color{ background: #fff; -} +}*/ .primary_btn{ background: $login_btn_bg; color: #fff; border:none; - font-size: 0.85rem; - font-weight: 600; + font-size: 0.9rem; + font-weight: 500; text-transform: capitalize; letter-spacing: initial; display: inline-block; border-radius: 3px; - padding: 5px 10px; + padding: 2px 10px; line-height: 1.5rem; bordeR: none; cursor: pointer; @@ -162,11 +162,11 @@ a{ margin-right: 5px; } } -.seconadry_btn{ +.secondary_btn{ @extend .primary_btn; - background: $dark_sky; + background: $blue; &:hover{ - background: darken($dark_sky, 10%); + background: darken($blue, 10%); } } .tertiary_btn{ @@ -487,6 +487,9 @@ $color_2: #000; .table_container_row{ margin-top: 10px; .card{ + border: none; + border-radius: 0; + box-shadow: none; .card-title{ padding:5px 10px !important; background: #e5e5e5; @@ -523,20 +526,21 @@ $color_2: #000; margin-top:0px; } } - } - .card-body{ + .card-body{ padding:0px 0px; table{ + border: none; + margin-bottom: 0; thead{ tr{ th{ - color: #5b646d; - font-weight: 600; - /* text-transform: uppercase; */ + color: #454545; + font-weight: 500; font-size: 0.9rem; border: none; text-align: left; padding:6px; + border-bottom: 1px solid darken(#f7f7f9, 2%); } } } @@ -549,6 +553,7 @@ $color_2: #000; text-align: left; font-weight: 500; padding:7px 5px; + border-bottom: 1px solid darken(#f7f7f9, 2%); @media(min-width: $large){ font-size:1rem; } @@ -570,25 +575,12 @@ $color_2: #000; } } &.actions{ - .btn{ - border-radius: 2px; - font-size: 0.85rem; - padding:3px 5px; - font-weight: 600; - color: #fff; - margin-right:2px; - svg{ - display: inline-block; - font-size: 0.85rem; - margin-right: 0px; - } - &:hover{ - text-decoration: none; - } - } - .action{ - svg{ - margin-right: 4px; + a{ + &.box{ + @extend .box; + line-height: 1.5rem; + width: 1.65rem; + height: 1.65rem; } } } @@ -597,6 +589,8 @@ $color_2: #000; } } } + } + /* mail_menu starts here */ .mail_menu{ .panel{ @@ -992,14 +986,12 @@ padding:0px !important; font-size: 0.83rem; } pre{ - font-family: 'Muli', sans-serif; margin-bottom: 0.5rem; a{ } } #comments_div{ - font-family: 'Muli', sans-serif; .list-group-item{ margin-bottom: 0.75rem; border: none; @@ -1137,71 +1129,129 @@ pre{ } /* marketing_pages */ + .side_menu{ - background: #404f88; - position: fixed; - top: 50px; - left: 0; - width: 250px; - height: 100%; - border: none; - .list-group-item{ + background: #fff; + position: fixed; + left: 0; + width: 250px; + top: 0; + height: 100%; + border: none; + .logo_block{ + a{ display: block; - border: none; - background: none; + padding:0.75rem 1rem; + color: #000; + font-size: 1.35rem; + font-weight: 800; + .green_color{ + color: $green; + } + } + } + .profile_block{ + margin-top: 0.5rem; + ul{ + margin: 0; padding: 0; - margin-bottom: 1rem; - .item{ - display: block; - width: 100%; - h6{ - &.heading{ - font-size: 0.75rem; - letter-spacing: 0.02rem; - text-transform: uppercase; - color: #d5ddf9; - background: #526199; - font-weight: 600; - border: none; - line-height: 2rem; - padding-left: 1rem; - svg{ - display: inline-block; - margin-right: 5px; + padding-left: 1rem; + li{ + list-style: none; + display: inline-block; + margin-right: 3px; + a{ + &.profile_pic{ + img{ + width: 2rem; + height: 2rem; + border-radius: 3px; } } + &.box{ + @extend .box; + } } - ul{ + } + } + } + .menu_block{ + .main_menu{ + margin-top: 1.5rem; + padding: 0; + padding-left: 1rem; + li{ + list-style: none; + a{ + font-size: 0.83rem; + font-size: 400; + transition: 0.6s all ease-out; + svg{ + display: inline-block; + margin-right: 0.5rem; + font-size: 1.1rem; + } + } + &.parent_menu{ margin: 0; padding: 0; - li{ - list-style: none; - a{ - font-size: 0.85rem; - color: #fff; - font-weight: 600; - line-height: 1.6rem; - display: block; - padding: 0.2rem 0; - padding-left: 0.7rem; - &:hover{ - background: #ed7354; - } - &.active{ - background: #ed7354; - } - span{ - background: #00a1ff; - border-radius: 3px; - display: inline-block; - font-size: 0.8rem; - padding: 0.1rem 0.4rem; - line-height: 1.3rem; - margin-right: 0.5rem; - } - svg{ - display: inline-block; - margin-right: 0.4rem; + margin-bottom: 0.5rem; + list-style: none; + a{ + display: inline-block; + color: $text_color; + padding-top: 0.35rem; + padding-bottom: 0.35rem; + transition: 0.6s all ease-out; + &.red{ + color: $red; + } + &.green{ + color: $green; + } + &.purple{ + color: $purple; + } + } + .submenu{ + padding-left: 1.6rem; + margin-top: 0.3rem; + position: relative; + &::before{ + content: ""; + height: 100%; + background: lighten($text_color, 40%); + width: 1px; + position: absolute; + left: 0.5rem; + } + li{ + a{ + transition: 0.6s all ease-out; + &.yellow{ + svg{ + color: $dark_yellow; + } + &:hover{ + color: $dark_yellow; + } + } + &.d_green{ + svg{ + color: $d_green; + } + &:hover{ + color: $d_green ; + } + } + &.sky_color{ + svg{ + color: $sky_color; + } + &:hover{ + color: $sky_color; + } + } } } } @@ -1209,13 +1259,21 @@ pre{ } } } +} +/* side_menu */ .marketing_pages{ margin-left: 250px; margin-bottom: 1rem; + margin-top: 2rem; } .badge{ - font-size: 85%; + width: auto !important; + height: auto !important; + font-size: 100%; + border-radius: 10rem; + display: inline-block; + padding: .05rem 1rem; } .a_color{ color: $text_color !important; @@ -1498,3 +1556,31 @@ pre{ font-size: 12px; color: #a7a7a7a7; } + +.actions { + .edit, .delete { + svg { + path { + fill: white; + } + } + } +} + +.btn-danger{ + background-color: #ffc107; + border-color: #ffc107; + color: #fff!important; +} + +.btn-success{ + color: #fff!important; + background-color: #099cbe; + border-color: #099cbe; +} + +.btn-info{ + color: #fff!important; + background: #28a745; + border-color: #35a745; +} diff --git a/static/css/mention.css b/static/css/mention.css new file mode 100644 index 00000000..3ba9a63d --- /dev/null +++ b/static/css/mention.css @@ -0,0 +1,67 @@ +.mention-wrapper { + position: relative; + display: block; + width: 100%; + height: 100%; } + +.mention-wrapper textarea { + color: rgba(0, 0, 0, 0.1); } + +.mention-display { + color: rgba(0, 0, 0, 0.9); + position: absolute; + display: inline-block; + box-sizing: border-box; + pointer-events: none; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + border-radius: 4px; + word-break: break-word; + -moz-appearance: textfield-multiline; + -webkit-appearance: textarea; + font: medium -moz-fixed; + font: -webkit-small-control; + border: 3px solid transparent; + padding: 10px; + font-size: 16px; } + +.mention-options { + position: absolute; + pointer-events: none; + opacity: 0; + color: rgba(0, 0, 0, 0.9); + width: 100%; + border: 3px solid transparent; + box-sizing: border-box; } + +.mention-options.show { + pointer-events: initial; + opacity: 1; + z-index: 9999; + border: 2px solid #ddd; + background: #ddd; } + +.mention-options .mention-option { + padding: 5px 10px; + background: #FFF; + position: relative; + cursor: pointer; + display: none; } + +.mention-options .mention-option:hover:after, .mention-options .mention-option.active:after { + content: ''; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.1); + pointer-events: none; + cursor: pointer; } + +.mention-options .mention-option.show { + display: block; } + +/*# sourceMappingURL=mention.css.map */ diff --git a/static/css/mention.css.map b/static/css/mention.css.map new file mode 100644 index 00000000..e6e84f59 --- /dev/null +++ b/static/css/mention.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,gBAAiB;EACf,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGd,yBAA0B;EAAE,KAAK,EAAE,kBAAkB;;AAErD,gBAAiB;EACf,KAAK,EAAE,kBAAkB;EACzB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,YAAY;EACrB,UAAU,EAAE,UAAU;EACtB,cAAc,EAAE,IAAI;EACpB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,aAAa,EAAE,GAAG;EAClB,UAAU,EAAE,UAAU;EACtB,eAAe,EAAE,mBAAmB;EACpC,kBAAkB,EAAE,QAAQ;EAC5B,IAAI,EAAE,iBAAiB;EACvB,IAAI,EAAE,qBAAqB;EAC3B,MAAM,EAAE,qBAAqB;EAC7B,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,IAAI;;AAGjB,gBAAiB;EACf,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,CAAC;EACV,KAAK,EAAE,kBAAkB;EACzB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,qBAAqB;EAC7B,UAAU,EAAE,UAAU;;AAGxB,qBAAsB;EACpB,cAAc,EAAE,OAAO;EACvB,OAAO,EAAE,CAAC;EACV,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,cAAc;EACtB,UAAU,EAAE,IAAI;;AAIlB,gCAAiC;EAC/B,OAAO,EAAE,QAAQ;EACjB,UAAU,EAAE,IAAI;EAChB,QAAQ,EAAE,QAAQ;EAClB,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,IAAI;;AAGf,2FAA4F;EAC1F,OAAO,EAAE,EAAE;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,kBAAkB;EAC9B,cAAc,EAAE,IAAI;EACpB,MAAM,EAAE,OAAO;;AAGjB,qCAAsC;EAAE,OAAO,EAAE,KAAK", +"sources": ["mention.scss"], +"names": [], +"file": "mention.css" +} \ No newline at end of file diff --git a/static/css/ref.scss b/static/css/ref.scss index a632223d..ce066fad 100644 --- a/static/css/ref.scss +++ b/static/css/ref.scss @@ -12,11 +12,17 @@ $color_blue_dianne_approx: #273e48; $color_cerise_red_approx: #e92064; $login_btn_bg:#dc3545; $info_color:#138496; -$green:#41B871; $dark_sky:#099CBE; $dark_yellow:#FEA124; $inner_page_bg:#CDCDCD; -$green: #5cca64; +$green: #7ae847; +$d_green: #06ae90; +$red:#fb1e07; +$sky_color:#099CBE; +$green: #7ae847; +$blue: #1095fe; +$purple: #9414c9; + @@ -38,6 +44,13 @@ $mob_min:240px; .card{ border:1px solid #ccc; } + +.heading{ + margin-bottom: 1rem; + span{ + font-size: 1.35rem; + } +} .buttons_row{ /* margin-top: 10px; margin-bottom: 10px;*/ @@ -98,3 +111,68 @@ select{ } } } + + +.box{ + background: red; + display: inline-block; + width: 2rem; + height: 2rem; + line-height: 1.6rem; + border-radius: 3px; + text-align: center; + padding-top: 0.1rem; + transition: 0.6s all ease-out; + &.sky_blue{ + background: lighten($sky_color, 55%); + color: $sky_color; + &:hover{ + background: $sky_color; + } + } + &.red{ + background: lighten($red, 45%); + color: $red; + &:hover{ + background: $red; + } + } + &.green{ + background: lighten($d_green, 55%); + color: $d_green; + &:hover{ + background: $d_green; + } + } + &.l_green{ + background: lighten($green, 55%); + color: $green; + &:hover{ + background: $green; + } + } + &.purple{ + background: lighten($purple, 50%); + color: $purple; + &:hover{ + background: $purple; + } + } + &.yellow{ + background: lighten($dark_yellow, 50%); + color: $dark_yellow; + &:hover{ + background: $dark_yellow; + } + } + &.dark_grey{ + background: lighten(#6c757d, 45%); + color: #6c757d; + &:hover{ + background: #6c757d; + } + } + &:hover{ + color: #fff; + } +} \ No newline at end of file diff --git a/static/files/contacts_sample.csv b/static/files/contacts_sample.csv new file mode 100644 index 00000000..e0c9214a --- /dev/null +++ b/static/files/contacts_sample.csv @@ -0,0 +1,4 @@ +company name,email,first name,last name,city,state +micropyramid,admin@micropyramid.com,Admin,MP,Hyderabad,Telangana +micropyramid,hr@micropyramid.com,HR,MP,Hyderabad,Telangana +micropyramid,contactus@micropyramid.com,Contact US,MP,Hyderabad,Telangana diff --git a/static/images/busy.gif b/static/images/busy.gif new file mode 100644 index 00000000..c480dd0e Binary files /dev/null and b/static/images/busy.gif differ diff --git a/templates/base.html b/templates/base.html index 4096a73f..1374323b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,9 +9,10 @@ Django CRM - + + {% compress css %} @@ -72,7 +73,7 @@ -
+
{% block breadcrumb %} {% endblock %} diff --git a/templates/marketing/base.html b/templates/marketing/base.html index 6118bbb0..c56c4740 100644 --- a/templates/marketing/base.html +++ b/templates/marketing/base.html @@ -4,26 +4,64 @@ {% endblock %} +{% block header %} + +{% endblock header %} + {% block content %} -
- + --> {% endblock %} diff --git a/templates/marketing/campaign/details.html b/templates/marketing/campaign/details.html index 857f9491..bbc17464 100644 --- a/templates/marketing/campaign/details.html +++ b/templates/marketing/campaign/details.html @@ -60,7 +60,6 @@
-
@@ -188,7 +187,7 @@
- +
@@ -273,7 +272,7 @@ - +
@@ -358,7 +357,7 @@ - +
@@ -443,7 +442,7 @@ - +
@@ -530,7 +529,7 @@ - +
@@ -538,7 +537,7 @@ - + {% endblock %} diff --git a/templates/marketing/campaign/index.html b/templates/marketing/campaign/index.html index 4590656f..e7f00630 100644 --- a/templates/marketing/campaign/index.html +++ b/templates/marketing/campaign/index.html @@ -2,145 +2,170 @@ {% load staticfiles %} {% load paginate %} {% block content %} -{{ block.super }} +{{ block.super }}
-
-
-
-
-
-
- Campaigns - 54 - Add New Campaign -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CampaignReceipentsSentUnique OpensUnique ClicksActions
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
Primary Sample scheduled newsletter 5478 - Tue, Nov 22, 2016, 12:00AM - - 20.6% 125 Opened - - 15.5% 78 Clicked - - - - -
-
-
-
-
-
-
+
+
+
+
+
+
+ Campaigns - {{ campaigns_list|length }} + Add New Campaign +
+
+ + + + + + + + + + + + + {% for campaign in campaigns_list %} + + + + + + + + + {% empty %} + + + + {% endfor %} + + +
CampaignReceipentsSentUnique OpensUnique ClicksActions
Primary {{ campaign.title }} 5478 + {{ campaign.created_on }} + + 20.6% 125 Opened + + 15.5% 78 Clicked + + + +
No records found!
+
+
+
+
+
+
{% endblock %} \ No newline at end of file diff --git a/templates/marketing/campaign/new.html b/templates/marketing/campaign/new.html index 2bc8137b..b699aeb8 100644 --- a/templates/marketing/campaign/new.html +++ b/templates/marketing/campaign/new.html @@ -2,198 +2,284 @@ {% load staticfiles %} {% load paginate %} {% block content %} -{{ block.super }} +{{ block.super }}
-
-
-
-
-
-
-
-
- {% if contact_obj %}Edit{% else %}Create{% endif %} Email Template -
- -
-
-
- - -
-
-
-
- - - Set a title for this campaign? -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- - -
-
- -
- Optionally append a query string to all links in your email newsletter. A good use case is Google Analytics tracking. Don't include '?' in your query string. -
- -
-
- - - Allowed file types: jpeg, jpg, gif, png, pdf, zip -
-
- - -
-
- - -

-
- - -
-
-
-
- - -
-
- -
-
- Use the following tags in your subject, plain text or HTML code and they'll automatically be formatted when your campaign is sent. For web version and unsubscribe tags, you can style them with inline CSS. -
-
-
-
-
Essential tags (HTML only)
-

The following tags can only be used on the HTML version

-
    -
  • - Webversion link: - View web version -
  • -
  • - Unsubscribe link: - Unsubscribe here -
  • -
- -
Essential tags
-

The following tags can only be used on the HTML version

-
    -
  • - Webversion link: - View web version -
  • -
  • - Unsubscribe link: - Unsubscribe here -
  • -
- -
- - -
-
Personalization tags
-

The following tags can be used on both Plain text or HTML version

-
    -
  • - Name: - [Name,fallback=] -
  • -
  • - Email: - [Email] -
  • -
  • - Two digit day of the month (eg. 01 to 31): - [currentdaynumber] -
  • -
- -
Essential tags
-

The following tags can only be used on the HTML version

-
    -
  • - Name: - [Name,fallback=] -
  • -
  • - Email: - [Email] -
  • -
  • - Two digit day of the month (eg. 01 to 31): - [currentdaynumber] -
  • -
- -
- -
-
-
-
-
-
- -
-
-
-
- -
-
+
+
+
+
+
+
+
+
+ Create Campaign +
+ +
+
+
+ + + Set a title for this campaign? + +
+
+
+
+ + + +
+
+
+
+ + + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+
+ + +
+
+ + + +
+
+ +
+ Optionally append a query string to all links in your email newsletter. A good use case is Google Analytics tracking. Don't include '?' in your query string. +
+ +
+
+ + + Allowed file types: jpeg, jpg, gif, png, pdf, zip + +
+
+ + +
+
+ + +

+
+ + Cancel +
+
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+ +
+
+ Use the following tags in your subject, plain text or HTML code and they'll automatically be formatted when your campaign is sent. For web version and unsubscribe tags, you can style them with inline CSS. + + +
+
+
+
+
Essential tags (HTML only)
+

The following tags can only be used on the HTML version

+
    +
  • + Webversion link: + + View web version + +
  • +
  • + Unsubscribe link: + + Unsubscribe here + +
  • +
+
Essential tags
+

The following tags can only be used on the HTML version

+
    +
  • + Webversion link: + + View web version + +
  • +
  • + Unsubscribe link: + + Unsubscribe here + +
  • +
+
+
+
Personalization tags
+

The following tags can be used on both Plain text or HTML version

+
    +
  • + Name: + [Name,fallback=] +
  • +
  • + Email: + [Email] +
  • +
  • + Two digit day of the month (eg. 01 to 31): + [currentdaynumber] +
  • +
+
Essential tags
+

The following tags can only be used on the HTML version

+
    +
  • + Name: + [Name,fallback=] +
  • +
  • + Email: + [Email] +
  • +
  • + Two digit day of the month (eg. 01 to 31): + [currentdaynumber] +
  • +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
{% endblock %} {% block js_block %} - + + {% endblock js_block %} \ No newline at end of file diff --git a/templates/marketing/dashboard.html b/templates/marketing/dashboard.html index 8e737091..e656fd32 100644 --- a/templates/marketing/dashboard.html +++ b/templates/marketing/dashboard.html @@ -1,4 +1,4 @@ -w{% extends 'marketing/base.html' %} +{% extends 'marketing/base.html' %} {% load staticfiles %} {% load paginate %} {% block content %} @@ -7,7 +7,7 @@ w{% extends 'marketing/base.html' %}
Dashboard page will come soon -
+
{% endblock %} \ No newline at end of file diff --git a/templates/marketing/email_template/details.html b/templates/marketing/email_template/details.html index e69de29b..f28ea634 100644 --- a/templates/marketing/email_template/details.html +++ b/templates/marketing/email_template/details.html @@ -0,0 +1,20 @@ +{% extends 'marketing/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% block content %} +{{ block.super }} +
+
+
+

{{email_template.title}}

+

{{email_template.subject}}

+ {{email_template.html|safe}} +
+
+
+{% endblock %} + +{% block js_block %} + +{% endblock js_block %} \ No newline at end of file diff --git a/templates/marketing/email_template/index.html b/templates/marketing/email_template/index.html index b1b2913b..b8d88e38 100644 --- a/templates/marketing/email_template/index.html +++ b/templates/marketing/email_template/index.html @@ -2,7 +2,7 @@ {% load staticfiles %} {% load paginate %} {% block content %} -{{ block.super }} +{{ block.super }}
@@ -10,8 +10,8 @@
- Email Templates - 15 - Add New Email Template + Email Templates - {{email_templates|length}} + Add New Email Template
@@ -24,85 +24,92 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% for email_template in email_templates %} + + + + + + + {% empty %} + + + + {% endfor %} +
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
Sample scheduled Email template View Template - - - -
{{ email_template.title }} View Template + + +
No Records Found!
@@ -110,7 +117,7 @@
-
+
{% endblock %} @@ -119,6 +126,6 @@ {% endblock js_block %} \ No newline at end of file diff --git a/templates/marketing/email_template/new.html b/templates/marketing/email_template/new.html index b2bc4674..e7a491ef 100644 --- a/templates/marketing/email_template/new.html +++ b/templates/marketing/email_template/new.html @@ -1,48 +1,64 @@ {% extends 'marketing/base.html' %} {% load staticfiles %} -{% load paginate %} {% block content %} -{{ block.super }} -
-
-
-
-
-
-
-
-
- {% if contact_obj %}Edit{% else %}Create{% endif %} Email Template -
- -
-
-
- -
-
-
-
- -
-
-
-
-
- -
-
- -
-
-
-
- -
+ {{ block.super }} +
+
+
+
+
+
+
+
+
+ {% if email_template %}Edit{% else %}Create{% endif %} Email Template +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
{% if email_template %}{{ email_template.html|safe }}{% endif %}
+
+ + +
+ +
+ + Cancel +
+
+ +
+
+
+
+ +
+
-
{% endblock %} {% block js_block %} + -{% endblock js_block %} \ No newline at end of file +{% endblock js_block %} diff --git a/templates/marketing/lists/all.html b/templates/marketing/lists/all.html index 96082b2f..60cbb99a 100644 --- a/templates/marketing/lists/all.html +++ b/templates/marketing/lists/all.html @@ -5,15 +5,17 @@ {{ block.super }}
+
+
+ All Contacts - 22 + Add New Contact List +
+
-
- Contacts List - 61 - Add New List -
-
+
@@ -27,105 +29,174 @@ - + - + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/marketing/lists/index.html b/templates/marketing/lists/index.html index 55d8c436..40d0ef3d 100644 --- a/templates/marketing/lists/index.html +++ b/templates/marketing/lists/index.html @@ -10,7 +10,7 @@
- Contact List - 61 + Contact List - {{ contact_lists|length }} Add New Contact
@@ -21,108 +21,133 @@
- + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {% for contact_list in contact_lists %} + + + + + + + + + {% empty %} + + {% endfor %} +
21 Micropyramid 23 - 15% + 15% - 20% + 20% - - - - + + + +
2MicropyramidPeeljobs 23 - 15% + 70% - 20% + 20% - - - - + + + +
2Micropyramid3Study Signal 23 - 15% + 70% - 20% + 20% - - - - + + + +
2Micropyramid4Django Crm 23 - 15% + 70% - 20% + 20% - - - - + + + +
2Micropyramid5Refactored + 23 - 15% + 70% - 20% + 20% - - - - + + + +
2Micropyramid6Refactored 23 - 15% + 70% - 20% + 20% - - - - + + + + +
7Artbnkr23 + 70% + + 20% + + + + + +
8Now Finance125 + 45% + + 65% + + + + + +
9Goshoppi154 + 5% + + 54% + + + + + +
10Techond23 + 15% + + 20% + + + + +
Name Email Last ActvityStatusTags Actions
12Micropyramidadminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Subscribed - - - - -
12Peeljobsadminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Unsubscribed - - - - -
12Study Signaladminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Bounced - - - - -
12Radiostringadminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Marked - - - - -
12Dummycompanyadminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Spam - - - - -
12Dummycompanyadminmp@gmail.com - Tue, Nov 22, 2016, 12:00AM - - Unconfirmed - - - - -
{{ forloop.counter }}{{ contact_list.name }}{{ contact_list.created_by }} + {{ contact_list.updated_on }} + + {% for tag in contact_list.tags.all %} + {{tag.name}} + {% empty %} + No Tags! + {% endfor %} + + + + +
No records found!
diff --git a/templates/marketing/lists/new.html b/templates/marketing/lists/new.html index eb3b20b1..15c52d7b 100644 --- a/templates/marketing/lists/new.html +++ b/templates/marketing/lists/new.html @@ -1,61 +1,115 @@ {% extends 'marketing/base.html' %} {% load staticfiles %} {% load paginate %} +{% block extralinks %} + +{% endblock extralinks %} {% block content %} -{{ block.super }} +{{ block.super }}
-
-
-
-
-
-
- {% if contact_obj %}Edit{% else %}Create{% endif %} Contact List -
-
- + +
+
+
+
+
+ {% if contact_list %}Edit{% else %}Create{% endif %} Contact List +
+
- + +
-
- + +
- -
- - {% if request.GET.view_account %} - - {% endif %} - - -
-
- Save - - Cancel +
+ +
+ +
+ +
-
-
-
-
-
-
- -
- - +
+ +
+
+
+ + Cancel +
+
+
+
+
+
+
+ +
-{% endblock %} \ No newline at end of file +{% endblock %} +{% block js_block %} + + + +{% endblock js_block %} \ No newline at end of file diff --git a/templates/root.html b/templates/root.html index 2fbf1746..13ae48a3 100644 --- a/templates/root.html +++ b/templates/root.html @@ -25,6 +25,7 @@
+ {% block header %}
{% endif %} - + + {% endblock %} -
+
{% block breadcrumb %} {% endblock %} @@ -81,6 +83,7 @@ + {% block js_block %}{% endblock js_block %} diff --git a/templates/sales/base.html b/templates/sales/base.html index edee6262..5798ea94 100644 --- a/templates/sales/base.html +++ b/templates/sales/base.html @@ -1,13 +1,13 @@ {% extends 'root.html' %} {% block main_menu %} - + {% endblock %} \ No newline at end of file