diff --git a/accounts/tests.py b/accounts/tests.py index 67625c3f..2e9fd09b 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,23 +1,36 @@ from django.test import TestCase from cases.models import Case -from accounts.models import Account -from common.models import User, Comment, Attachments +from accounts.models import Account, Tags +from common.models import User, Comment, Attachments, Address +from django.urls import reverse +from leads.models import Lead +from contacts.models import Contact +from django.core.files.uploadedfile import SimpleUploadedFile class AccountCreateTest(object): + def setUp(self): self.user = User.objects.create( first_name="mike", username='mike', email='u@mp.com', role='ADMIN') self.user.set_password('mike2293') self.user.save() + self.user1 = User.objects.create( + first_name="mp", + username='mp', + email='mp@micropyramid.com', + role="USER") + self.user1.set_password('mp') + self.user1.save() + self.account = Account.objects.create( 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, + website="www.mike.com", created_by=self.user, status="open", industry="SOFTWARE", description="Yes.. Testing Done") self.case = Case.objects.create( name="raghu", case_type="Problem", @@ -33,9 +46,40 @@ class AccountCreateTest(object): created_by=self.user, account=self.account ) self.client.login(email='u@mp.com', password='mike2293') + self.lead = Lead.objects.create(title="LeadCreation", + first_name="Alisa", + last_name="k", + email="Alisak1993@gmail.com", + address_line="", + street="Arcade enclave colony", + city="NewTown", + state="California", + postcode="5079", + country="AD", + website="www.gmail.com", + status="assigned", + source="Call", + opportunity_amount="700", + description="Iam an Lead", + created_by=self.user) + self.lead.assigned_to.add(self.user) + self.address = Address.objects.create( + street="5th phase", + city="Orlando", + state="Florida", + postcode=502279, country="AD") + + self.contact = Contact.objects.create( + first_name="contact", + email="contact@gmail.com", + phone="12345", + address=self.address, + description="contact", + created_by=self.user) class AccountsCreateTestCase(AccountCreateTest, TestCase): + def test_account_create_url(self): response = self.client.get('/accounts/create/', { 'name': "mike", 'email': "mike@micropyramid.com", @@ -87,12 +131,14 @@ class AccountsListTestCase(AccountCreateTest, TestCase): class AccountsCountTestCase(AccountCreateTest, TestCase): + def test_accounts_list_count(self): count = Account.objects.all().count() self.assertEqual(count, 1) class AccountsViewTestCase(AccountCreateTest, TestCase): + def test_accounts_view(self): self.accounts = Account.objects.all() response = self.client.get( @@ -102,6 +148,7 @@ class AccountsViewTestCase(AccountCreateTest, TestCase): class AccountsRemoveTestCase(AccountCreateTest, TestCase): + def test_accounts_remove(self): response = self.client.get( '/accounts/' + str(self.account.id) + '/delete/') @@ -114,6 +161,7 @@ class AccountsRemoveTestCase(AccountCreateTest, TestCase): class AccountsUpdateUrlTestCase(AccountCreateTest, TestCase): + def test_accounts_update(self): response = self.client.get( '/accounts/' + str(self.account.id) + '/edit/', { @@ -187,14 +235,25 @@ class AccountModelTest(AccountCreateTest, TestCase): class CommentTestCase(AccountCreateTest, TestCase): + def test_comment_add(self): response = self.client.post( - '/accounts/comment/add/', {'accountid': self.account.id}) + '/accounts/comment/add/', {'accountid': self.account.id, + 'comment': 'comment'}) + self.assertEqual(response.status_code, 200) + + def test_comment_create(self): + + response = self.client.post( + '/accounts/comment/add/', {'accountid': self.account.id, + 'comment': 'comment'}) self.assertEqual(response.status_code, 200) def test_comment_edit(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( - '/accounts/comment/edit/', {'commentid': self.comment.id}) + '/accounts/comment/edit/', {'commentid': self.comment.id, + 'comment': 'comment'}) self.assertEqual(response.status_code, 200) # def test_comment_valid(self): @@ -210,16 +269,27 @@ class CommentTestCase(AccountCreateTest, TestCase): '/accounts/comment/remove/', {'comment_id': self.comment.id}) self.assertEqual(response.status_code, 200) + def test_comment_deletion(self): + self.client.login(email='mp@micropyramid.com', password='mp') + response = self.client.post( + '/accounts/comment/remove/', {'comment_id': self.comment.id}) + self.assertEqual(response.status_code, 200) + class AttachmentTestCase(AccountCreateTest, TestCase): + def test_attachment_add(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/accounts/attachment/add/', {'accountid': self.account.id}) self.assertEqual(response.status_code, 200) def test_attachment_valid(self): + upload_file = open('static/images/user.png', 'rb') response = self.client.post( - '/accounts/attachment/add/', {'accountid': self.account.id}) + '/accounts/attachment/add/', {'accountid': self.account.id, + 'attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())}) self.assertEqual(response.status_code, 200) def test_attachment_delete(self): @@ -227,3 +297,52 @@ class AttachmentTestCase(AccountCreateTest, TestCase): '/accounts/attachment/remove/', {'attachment_id': self.attachment.id}) self.assertEqual(response.status_code, 200) + + def test_attachment_deletion(self): + self.client.login(email='mp@micropyramid.com', password='mp') + response = self.client.post( + '/accounts/attachment/remove/', + {'attachment_id': self.attachment.id}) + self.assertEqual(response.status_code, 200) + + +class TagCreateTest(object): + + def setUp(self): + self.tag = Tags.objects.create( + name="tag", slug="tag1") + + +class TagModelTest(TagCreateTest, TestCase): + + def test_string_representation(self): + tag = Tags(name="tag", slug="tag1") + self.assertEqual(str(self.tag.name), tag.name) + + +class TestCreateLeadPostView(AccountCreateTest, TestCase): + + def test_create_lead_post_status(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post(reverse( + 'accounts:new_account'), {"name": "mike", + "email": "mike@micropyramid.com", + "phone": "+91-833-385-5552", + "billing_address_line": "sddsv", + "billing_street": "KPHB", + "billing_city": "New York", + "billing_state": "usa", + "billing_postcode": "500073", + "billing_country": "IN", + "website": "www.mike.com", + "created_by": self.user, + "status": "open", + "industry": "SOFTWARE", + "description": "Yes.. Testing Done", + "lead": str(self.lead.id), + 'contacts': str(self.contact.id), + 'tags': 'tag1', + 'account_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + }) + self.assertEqual(response.status_code, 302) diff --git a/cases/tests.py b/cases/tests.py index 55b3dd89..d4ffb3cf 100644 --- a/cases/tests.py +++ b/cases/tests.py @@ -4,9 +4,12 @@ from contacts.models import Contact from accounts.models import Account from common.models import Address, Comment, Attachments from common.models import User +from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile class CaseCreation(object): + def setUp(self): self.address = Address.objects.create( street="6th phase", @@ -21,6 +24,13 @@ class CaseCreation(object): role="ADMIN") self.user.set_password('robert') self.user.save() + self.user1 = User.objects.create( + first_name="mp", + username='mp', + email='mp@micropyramid.com', + role="USER") + self.user1.set_password('mp') + self.user1.save() self.client.login(email='r@mp.com', password='robert') self.client.login(email='r@mp.com', password='robert') @@ -56,9 +66,12 @@ class CaseCreation(object): attachment='image.png', case=self.case, created_by=self.user, account=self.account ) + self.content_type = ContentType.objects.create( + app_label="cases1", model="case") class CaseViewTestCase(CaseCreation, TestCase): + def test_list_cases(self): self.cases = Case.objects.all() response = self.client.get('/cases/list/') @@ -78,12 +91,16 @@ class CaseViewTestCase(CaseCreation, TestCase): class CaseCreationUrlTestCase(CaseCreation, TestCase): + def test_create_cases(self): + upload_file = open('static/images/user.png', 'rb') response = self.client.post('/cases/create/', { - 'name': 'new case', 'case_type': 'Problem', - 'status': 'New', - 'account': self.account, 'contacts': [self.contacts.id], - 'priority': "Low", 'description': "something" + 'name': 'new case', 'closed_on': '2019-03-14', + 'status': 'New', 'priority': "Low", + 'created_by': self.user, "assigned_to": str(self.user.id), + 'contacts': str(self.contacts.id), + 'case_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) }) self.assertEqual(response.status_code, 200) @@ -99,8 +116,13 @@ class CaseCreationUrlTestCase(CaseCreation, TestCase): # self.assertTemplateUsed(response, 'create_cases.html') self.assertEqual(response.status_code, 200) + def test_create_case_get_request(self): + response = self.client.get('/cases/create/') + self.assertEqual(response.status_code, 200) + class CaseShowTestCase(CaseCreation, TestCase): + def test_show_case(self): response = self.client.get('/cases/' + str(self.case.id) + '/view/') self.assertEqual(response.status_code, 200) @@ -116,6 +138,7 @@ class CaseShowTestCase(CaseCreation, TestCase): class CaseRemoveTestCase(CaseCreation, TestCase): + def test_case_deletion_show_case(self): response = self.client.get('/cases/' + str(self.case.id) + '/remove/') self.assertEqual(response['location'], '/cases/list/') @@ -128,15 +151,22 @@ class CaseRemoveTestCase(CaseCreation, TestCase): class CaseUpdateTestCase(CaseCreation, TestCase): + def test_update_case_view(self): response = self.client.get( '/cases/' + str(self.case.id) + '/edit_case/') self.assertEqual(response.status_code, 200) def test_case_update(self): - response = self.client.post( - '/cases/' + str(self.case.id) + '/edit_case/', - {'hiddenval': self.case.id}) + upload_file = open('static/images/user.png', 'rb') + response = self.client.post('/cases/' + str(self.case.id) + '/edit_case/', { + 'name': 'new case', 'closed_on': '2019-03-14', + 'status': 'New', 'priority': "Low", + 'created_by': self.user, "assigned_to": str(self.user.id), + 'contacts': str(self.contacts.id), + 'case_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + }) self.assertEqual(response.status_code, 200) def test_case_update_html(self): @@ -185,10 +215,17 @@ class CaseFormTestCase(CaseCreation, TestCase): self.assertEqual(response.status_code, 200) def test_comment_add(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/cases/comment/add/', {'caseid': self.case.id}) self.assertEqual(response.status_code, 200) + def test_comment_form_valid(self): + response = self.client.post( + '/cases/comment/add/', {'caseid': self.case.id, 'comment': 'comment'}) + self.assertEqual(response.status_code, 200) + + def test_comment_edit(self): response = self.client.post( '/cases/comment/edit/', {'commentid': self.comment.id}) @@ -201,7 +238,18 @@ class CaseFormTestCase(CaseCreation, TestCase): class AttachmentTestCase(CaseCreation, TestCase): + def test_attachment_add(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post( + '/cases/attachment/add/', {'caseid': self.case.id, + 'attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + }) + self.assertEqual(response.status_code, 200) + + def test_attachment_creation(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/cases/attachment/add/', {'caseid': self.case.id}) self.assertEqual(response.status_code, 200) @@ -211,8 +259,15 @@ class AttachmentTestCase(CaseCreation, TestCase): '/cases/attachment/remove/', {'attachment_id': self.attachment.id}) self.assertEqual(response.status_code, 200) + def test_attachment_deletion(self): + self.client.login(email='mp@micropyramid.com', password='mp') + response = self.client.post( + '/cases/attachment/remove/', {'attachment_id': self.attachment.id}) + self.assertEqual(response.status_code, 200) + class SelectViewTestCase(CaseCreation, TestCase): + def test_select_contact(self): response = self.client.get('/cases/select_contacts/') self.assertEqual(response.status_code, 200) diff --git a/common/tests.py b/common/tests.py index e25ad61d..45845d33 100644 --- a/common/tests.py +++ b/common/tests.py @@ -1,11 +1,13 @@ from django.test import TestCase from django.test import Client - +from django.urls import reverse from common.models import User, Document from common.forms import * +from django.core.files.uploadedfile import SimpleUploadedFile class ObjectsCreation(object): + def setUp(self): self.client = Client() # self.user = User.objects.create(first_name="admin", @@ -33,13 +35,19 @@ class ObjectsCreation(object): self.user.set_password('paul123') self.user.save() # self.user - user_login = self.client.login( + self.client.login( username='admin@micropyramid.com', password='admin123') self.document = Document.objects.create( title="abc", document_file="1.png", created_by=self.user) + self.comment = Comment.objects.create( + comment='testikd', user=self.user, + commented_by=self.user + ) + class TestHomePage(ObjectsCreation, TestCase): + def test_home_page(self): response = self.client.get('/') self.assertEqual(response.status_code, 200) @@ -80,6 +88,7 @@ class UserCreateTestCase(ObjectsCreation, TestCase): class PasswordChangeTestCase(ObjectsCreation, TestCase): + def test_password_change(self): self.client.login(username="admin@micropyramid.com", password="admin123") @@ -98,6 +107,14 @@ class PasswordChangeTestCase(ObjectsCreation, TestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 200) + def change_passsword_by_admin(self): + print(self.user, "madhu", self.user.role) + response = self.client.post(reverse('common:change_passsword_by_admin'), { + 'useer_id': self.user.id, + 'new_passwoord': "admin123" + }) + self.assertEqual(response.status_code, 302) + class ForgotPasswordViewTestCase(ObjectsCreation, TestCase): @@ -109,7 +126,8 @@ class ForgotPasswordViewTestCase(ObjectsCreation, TestCase): class LoginViewTestCase(ObjectsCreation, TestCase): # 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) + # is_staff=True, is_admin=True, is_superuser=True, is_active=False) + def test_login_post(self): self.client.logout() data = {"email": "admin@micropyramid.com", "password": "admin123"} @@ -147,6 +165,7 @@ class LoginViewTestCase(ObjectsCreation, TestCase): class UserTestCase(ObjectsCreation, TestCase): + def test_user_create_url(self): response = self.client.get('/users/create/', { 'first_name': 'micheal', @@ -188,6 +207,7 @@ class UserListTestCase(ObjectsCreation, TestCase): class UserRemoveTestCase(ObjectsCreation, TestCase): + def test_users_remove(self): response = self.client.get('/users/' + str(self.user.id) + '/delete/') self.assertEqual(response['location'], '/users/list/') @@ -199,6 +219,7 @@ class UserRemoveTestCase(ObjectsCreation, TestCase): class UserUpdateTestCase(ObjectsCreation, TestCase): + def test_users_update(self): response = self.client.get('/users/' + str(self.user.id) + '/edit/', { 'first_name': "admin", @@ -239,6 +260,7 @@ class UserUpdateTestCase(ObjectsCreation, TestCase): class ProfileViewTestCase(ObjectsCreation, TestCase): + def test_profile_view(self): url = "/profile/" response = self.client.get(url) @@ -254,6 +276,7 @@ class ProfileViewTestCase(ObjectsCreation, TestCase): class UserDetailView(ObjectsCreation, TestCase): + def test_user_detail(self): url = "/users/" + str(self.user.id) + "/view/" response = self.client.get(url) @@ -261,6 +284,7 @@ class UserDetailView(ObjectsCreation, TestCase): class DocumentDetailView(ObjectsCreation, TestCase): + def test_document_detail(self): url = "/documents/" + str(self.document.id) + "/view/" repsonse = self.client.get(url) @@ -271,12 +295,8 @@ 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='robert', first_name='robert', last_name='clark', @@ -295,3 +315,81 @@ class CreateCommentFile(TestCase): self.assertEqual(len(userr.password), 3) self.assertFalse(form.is_valid()) # self.assertTrue(form.) + + +class CommentTestCase(ObjectsCreation, TestCase): + + def test_comment_add(self): + response = self.client.post( + '/comment/add/', {'userid': self.user.id}) + self.assertEqual(response.status_code, 200) + + def test_comment_edit(self): + response = self.client.post(reverse('common:edit_comment', + kwargs={'pk': self.comment.id})) + self.assertEqual(response.status_code, 200) + resp = self.client.post(reverse('common:edit_comment', + kwargs={'pk': self.comment.id}), {'comment': 'hello123'}) + self.assertEqual(resp.status_code, 200) + + def test_comment_delete(self): + response = self.client.post( + '/comment/remove/', {'comment_id': self.comment.id}) + self.assertEqual(response.status_code, 200) + + def test_form_valid(self): + response = self.client.post( + '/comment/add/', {'userid': self.user.id, + 'comment': 'hello'}) + self.assertEqual(response.status_code, 200) + + +class DocumentTestCase(ObjectsCreation, TestCase): + + def test_document_add(self): + response = self.client.post( + '/documents/create/') + self.assertEqual(response.status_code, 200) + + def test_document_create(self): + response = self.client.post( + '/documents/create/') + self.assertEqual(response.status_code, 200) + + def test_document_edit(self): + upload_file = open('static/images/user.png', 'rb') + data = {'title': "doc", + 'created_by': self.user, + 'document_file': SimpleUploadedFile( + upload_file.name, upload_file.read()), + 'status': 'active', + 'shared_to': str(self.user.id)} + response = self.client.get(reverse('common:edit_doc', + kwargs={'pk': self.document.id}), data) + self.assertEqual(response.status_code, 200) + + def test_document_valid(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post( + '/documents/create/', {'title': "doc", + 'created_by': self.user, + 'document_file': SimpleUploadedFile( + upload_file.name, upload_file.read()), + 'status': 'active', + 'shared_to': str(self.user.id)}) + self.assertEqual(response.status_code, 200) + + def test_document_delete(self): + response = self.client.get(reverse('common:remove_doc', + kwargs={'pk': self.document.id})) + self.assertEqual(response.status_code, 302) + + def test_document_list_view(self): + response = self.client.get(reverse('common:doc_list')) + self.assertEqual(response.status_code, 200) + response = self.client.post(reverse('common:doc_list'), { + 'doc_name': 'doc', + 'status': 'active', + "shared_to": str(self.user.id) + }) + self.assertEqual(response.status_code, 200) diff --git a/contacts/tests.py b/contacts/tests.py index 2bb1b43a..c601a50e 100644 --- a/contacts/tests.py +++ b/contacts/tests.py @@ -4,9 +4,13 @@ from common.models import Address, User, Comment, Attachments from cases.models import Case from django.test import Client from django.urls import reverse +from django.core.files.uploadedfile import SimpleUploadedFile +from contacts.forms import ContactAttachmentForm +from accounts.models import Account class ContactObjectsCreation(object): + def setUp(self): self.client = Client() self.user = User.objects.create( @@ -50,6 +54,7 @@ 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) @@ -77,6 +82,7 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): self.assertTemplateUsed(response, 'contacts.html') def test_contacts_create(self): + upload_file = open('static/images/user.png', 'rb') response = self.client.post('/contacts/create/', { 'first_name': 'contact', 'last_name': 'george', @@ -84,7 +90,45 @@ class ContactViewsTestCase(ContactObjectsCreation, TestCase): 'phone': '+917898901234', 'address': self.address.id, 'description': 'contact', - 'created_by': self.user}) + 'created_by': self.user, + 'assigned_to': str(self.user.id), + 'contact_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + }) + self.assertEqual(response.status_code, 302) + + def test_contact_create(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post('/contacts/create/', { + 'first_name': 'contact', + 'last_name': 'george', + 'email': 'meg@gmail.com', + 'phone': '+917898901234', + 'address': self.address.id, + 'description': 'contact', + 'created_by': self.user, + 'assigned_to': str(self.user.id), + 'contact_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + }) + self.assertEqual(response.status_code, 302) + + def test_update_contact(self): + upload_file = open('static/images/user.png', 'rb') + url = '/contacts/' + str(self.contact.id) + '/edit/' + data = { + 'first_name': 'contact', + 'last_name': 'george', + 'email': 'meg@gmail.com', + 'phone': '+917898901234', + 'address': self.address.id, + 'description': 'contact', + 'created_by': self.user, + 'assigned_to': str(self.user.id), + 'contact_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()) + } + response = self.client.post(url, data) self.assertEqual(response.status_code, 302) def test_contacts_create_html(self): @@ -166,6 +210,7 @@ class ContactsListTestCase(ContactObjectsCreation, TestCase): class CommentTestCase(ContactObjectsCreation, TestCase): + def test_comment_add(self): response = self.client.post( '/contacts/comment/add/', {'contactid': self.contact.id}) @@ -199,14 +244,18 @@ class CommentTestCase(ContactObjectsCreation, TestCase): class AttachmentTestCase(ContactObjectsCreation, TestCase): + def test_attachment_add(self): response = self.client.post( '/contacts/attachment/add/', {'contactid': self.contact.id}) self.assertEqual(response.status_code, 200) def test_attachment_valid(self): + upload_file = open('static/images/user.png', 'rb') response = self.client.post( - '/contacts/attachment/add/', {'contactid': self.contact.id}) + '/contacts/attachment/add/', {'contactid': self.contact.id, + 'attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())}) self.assertEqual(response.status_code, 200) def test_attachment_delete(self): diff --git a/crm/settings.py b/crm/settings.py index f91608ee..e9046b62 100644 --- a/crm/settings.py +++ b/crm/settings.py @@ -1,4 +1,5 @@ import os +from celery.schedules import crontab # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -107,7 +108,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'Asia/Calcutta' +TIME_ZONE = 'Asia/Kolkata' USE_I18N = True @@ -219,6 +220,21 @@ DEFAULT_FROM_EMAIL = 'no-reply@django-crm.micropyramid.com' CELERY_BROKER_URL = 'redis://localhost:6379' CELERY_RESULT_BACKEND = 'redis://localhost:6379' +CELERY_BEAT_SCHEDULE = { + "runs-campaign-for-every-thiry-minutes": { + "task": "marketing.tasks.run_all_campaigns", + "schedule": crontab(minute=30, hour='*') + }, + "runs-campaign-for-every-five-minutes": { + "task": "marketing.tasks.list_all_bounces_unsubscribes", + "schedule": crontab(minute='*/5') + }, + "runs-scheduled-campaigns-for-every-one-hour": { + "task": "marketing.tasks.send_scheduled_campaigns", + "schedule": crontab(hour='*/1') + }, +} + MAIL_SENDER = 'AMAZON' INACTIVE_MAIL_SENDER = 'MANDRILL' @@ -236,6 +252,8 @@ MANDRILL_API_KEY = os.getenv('MANDRILL_API_KEY', '') ADMIN_EMAIL = "admin@micropyramid.com" +URL_FOR_LINKS = "http://djangocrm.com" + try: from .dev_settings import * except ImportError: diff --git a/crm/urls.py b/crm/urls.py index 02026995..c0a054ba 100644 --- a/crm/urls.py +++ b/crm/urls.py @@ -1,3 +1,5 @@ +from django.conf import settings +from django.conf.urls.static import static from django.contrib.auth import views from django.urls import include, path from common.views import handler404, handler500 @@ -20,5 +22,9 @@ urlpatterns = [ ] +if settings.DEBUG: + urlpatterns = urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + handler404 = handler404 handler500 = handler500 diff --git a/leads/forms.py b/leads/forms.py index b9a154ce..9ab63ef8 100644 --- a/leads/forms.py +++ b/leads/forms.py @@ -15,6 +15,9 @@ class LeadForm(forms.ModelForm): self.fields['email'].required = True self.fields['assigned_to'].queryset = assigned_users self.fields['assigned_to'].required = False + self.fields['first_name'].required = False + self.fields['last_name'].required = False + self.fields['title'].required = True for key, value in self.fields.items(): if key == 'phone': value.widget.attrs['placeholder'] =\ diff --git a/leads/migrations/0008_auto_20190315_1503.py b/leads/migrations/0008_auto_20190315_1503.py new file mode 100644 index 00000000..7e195a7c --- /dev/null +++ b/leads/migrations/0008_auto_20190315_1503.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.7 on 2019-03-15 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('leads', '0007_auto_20190306_1226'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='first_name', + field=models.CharField(max_length=255, null=True, verbose_name='First name'), + ), + migrations.AlterField( + model_name='lead', + name='last_name', + field=models.CharField(max_length=255, null=True, verbose_name='Last name'), + ), + migrations.AlterField( + model_name='lead', + name='title', + field=models.CharField(default='title', max_length=64, verbose_name='Title'), + preserve_default=False, + ), + ] diff --git a/leads/models.py b/leads/models.py index 0763756b..4acfd971 100644 --- a/leads/models.py +++ b/leads/models.py @@ -13,9 +13,9 @@ 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) - first_name = models.CharField(("First name"), max_length=255) - last_name = models.CharField(("Last name"), max_length=255) + "Title"), max_length=64) + first_name = models.CharField(("First name"), null=True, max_length=255) + last_name = models.CharField(("Last name"), null=True, max_length=255) email = models.EmailField(null=True, blank=True) phone = PhoneNumberField(null=True, blank=True) status = models.CharField( diff --git a/leads/templates/leads.html b/leads/templates/leads.html index fd892f5d..a0153845 100644 --- a/leads/templates/leads.html +++ b/leads/templates/leads.html @@ -113,7 +113,7 @@ {% if open_leads|length > 0 %} ID - Name + Title Created By Source Status @@ -133,7 +133,8 @@ {% for lead in open_leads %} {{ forloop.counter }} - {{ lead.first_name }} {{ lead.last_name }} + + {{ lead.title }} {% if lead.created_by.profile_pic %} {% thumbnail lead.created_by.profile_pic "40x40" crop="center" as im %} @@ -207,7 +208,7 @@ {% if close_leads|length > 0 %} ID - Name + Title Source Status Assigned User @@ -226,7 +227,8 @@ {% for lead in close_leads %} {{ forloop.counter }} - {{ lead.first_name }} {{ lead.last_name }} + + {{ lead.title }} {{ lead.source }} {{ lead.get_status_display }} diff --git a/leads/templates/view_leads.html b/leads/templates/view_leads.html index 343f9aa7..e2e08c3c 100644 --- a/leads/templates/view_leads.html +++ b/leads/templates/view_leads.html @@ -37,11 +37,12 @@
-
{{ lead_record.first_name }} {{ lead_record.last_name }}
+
{% if lead_record.first_name %} + {{ lead_record.first_name }}{% endif %} {% if lead_record.last_name %} + {{ lead_record.last_name }}{% endif %}
-
{% if lead_record.account_name %} diff --git a/leads/tests.py b/leads/tests.py index 06ef0200..27410df4 100644 --- a/leads/tests.py +++ b/leads/tests.py @@ -1,13 +1,16 @@ from django.test import TestCase, Client from cases.models import Case from leads.models import Lead -from common.models import User, Comment, Attachments +from common.models import User, Comment, Attachments, APISettings from accounts.models import Account from leads.tasks import * from leads.forms import * +from django.urls import reverse +from django.core.files.uploadedfile import SimpleUploadedFile class TestLeadModel(object): + def setUp(self): self.client = Client() @@ -16,6 +19,14 @@ class TestLeadModel(object): self.user.set_password('jorge2293') self.user.save() + self.user1 = User.objects.create( + first_name="mp", + username='mp', + email='mp@micropyramid.com', + role="USER") + self.user1.set_password('mp') + self.user1.save() + self.client.login(username='j@mp.com', password='jorge2293') self.account = Account.objects.create(name="account", @@ -36,7 +47,7 @@ class TestLeadModel(object): first_name="Alisa", last_name="k", email="Alisak1993@gmail.com", - address_line="", + address_line="hyd", street="Arcade enclave colony", city="NewTown", state="California", @@ -47,7 +58,10 @@ class TestLeadModel(object): source="Call", opportunity_amount="700", description="Iam an Lead", - created_by=self.user) + created_by=self.user, + account_name="account", + phone="+91-123-456-7890") + self.lead.assigned_to.add(self.user) self.case = Case.objects.create( name="Rose", case_type="Problem", status="New", @@ -60,9 +74,12 @@ class TestLeadModel(object): attachment='image.png', case=self.case, created_by=self.user, account=self.account) + self.api_seetings = APISettings.objects.create( + title="api", apikey="api", created_by=self.user) class LeadsPostrequestTestCase(TestLeadModel, TestCase): + def test_valid_postrequesttestcase_date(self): data = {'title': 'LeadCreation', 'first_name': "micky", 'last_name': "Alisa", 'email': "Alisamicky1993@gmail.com", @@ -95,6 +112,7 @@ class LeadsPostrequestTestCase(TestLeadModel, TestCase): class LeadsCreateUrlTestCase(TestLeadModel, TestCase): + def test_leads_create_url(self): response = self.client.post('/leads/create/', { 'title': 'LeadCreation', @@ -128,8 +146,21 @@ class LeadsCreateUrlTestCase(TestLeadModel, TestCase): # self.assertTemplateUsed(response, 'create_lead.html') self.assertEqual(response.status_code, 200) + def test_leads_create_with_status_converted(self): + response = self.client.post('/leads/create/', { + 'title': 'LeadCreation', 'name': "micky", + 'email': "Alisamicky1993@gmail.com", 'account': self.account, + 'address_line': "", 'account_name': 'account1', + 'street': "Arcade enclave colony", 'city': "NewTown", + 'state': "California", 'postcode': "579", 'country': "AD", + 'website': "www.gmail.com", 'status': "converted", + "source": "Call", 'opportunity_amount': "700", + 'description': "Iam an Lead", 'created_by': self.user}) + self.assertEqual(response.status_code, 200) + class LeadsEditUrlTestCase(TestLeadModel, TestCase): + def test_leads_editurl(self): response = self.client.get('/leads/' + str(self.lead.id) + '/edit/') self.assertEqual(response.status_code, 200) @@ -177,6 +208,7 @@ class LeadListTestCase(TestLeadModel, TestCase): class GetLeadsViewTestCase(TestLeadModel, TestCase): + def test_get_lead(self): url = '/leads/get/list/' response = self.client.get(url) @@ -215,6 +247,7 @@ class UpdateLeadTestCase(TestLeadModel, TestCase): class LeadDetailTestCase(TestLeadModel, TestCase): + def test_lead_detail(self): url = '/leads/' + str(self.lead.id) + '/view/' response = self.client.get(url, {'status': ''}) @@ -222,12 +255,25 @@ class LeadDetailTestCase(TestLeadModel, TestCase): class CommentTestCase(TestLeadModel, TestCase): + def test_comment_add(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/leads/comment/add/', {'leadid': self.lead.id}) self.assertEqual(response.status_code, 200) + def test_comment_create(self): + response = self.client.post( + '/leads/comment/add/', {'leadid': self.lead.id, 'comment': "comment"}) + self.assertEqual(response.status_code, 200) + def test_comment_edit(self): + response = self.client.post( + '/leads/comment/edit/', {'commentid': self.comment.id, 'comment': "comment"}) + self.assertEqual(response.status_code, 200) + + def test_comment_update(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/leads/comment/edit/', {'commentid': self.comment.id}) self.assertEqual(response.status_code, 200) @@ -237,9 +283,17 @@ class CommentTestCase(TestLeadModel, TestCase): '/leads/comment/remove/', {'comment_id': self.comment.id}) self.assertEqual(response.status_code, 200) + def test_comment_deletion(self): + self.client.login(email='mp@micropyramid.com', password='mp') + response = self.client.post( + '/leads/comment/remove/', {'comment_id': self.comment.id}) + self.assertEqual(response.status_code, 200) + class AttachmentTestCase(TestLeadModel, TestCase): + def test_attachment_add(self): + self.client.login(email='mp@micropyramid.com', password='mp') response = self.client.post( '/leads/attachment/add/', {'leadid': self.lead.id}) self.assertEqual(response.status_code, 200) @@ -249,8 +303,23 @@ class AttachmentTestCase(TestLeadModel, TestCase): '/leads/attachment/remove/', {'attachment_id': self.attachment.id}) self.assertEqual(response.status_code, 200) + def test_attachment_deletion(self): + self.client.login(email='mp@micropyramid.com', password='mp') + response = self.client.post( + '/leads/attachment/remove/', {'attachment_id': self.attachment.id}) + self.assertEqual(response.status_code, 200) + + def test_attachment_valid(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post( + '/leads/attachment/add/', {'leadid': self.lead.id, + 'attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())}) + 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", @@ -266,6 +335,83 @@ class TestTemplates(TestLeadModel, TestCase): 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) + + +class TestCreateLeadPostView(TestLeadModel, TestCase): + + def test_create_lead_post_status(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post(reverse( + 'leads:add_lead'), {'first_name': 'm', + 'last_name': 's', + "created_by": self.user, + 'status': 'converted', + 'description': 'wgrgre', + 'website': 'www.mike.com', + 'phone': '+91-123-456-7890', + 'email': 'a@gmail.com', + 'account_name': 'account', + 'address_line': self.lead.address_line, + 'street': self.lead.street, + 'city': self.lead.city, + 'state': self.lead.state, + 'postcode': self.lead.postcode, + 'country': self.lead.country, + 'lead_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()), + 'assigned_to': str(self.user.id), + 'tags': 'tag' + }) + self.assertEqual(response.status_code, 200) + + def test_create_lead_get_request(self): + response = self.client.get(reverse('leads:add_lead')) + self.assertEqual(response.status_code, 200) + + def test_update_lead_post_status(self): + upload_file = open('static/images/user.png', 'rb') + response = self.client.post('/leads/' + str(self.lead.id) + '/edit/', + {'first_name': 'm', + 'last_name': 's', + "created_by": self.user, + 'status': 'converted', + 'description': 'wgrgre', + 'website': 'www.mike.com', + 'phone': '+91-123-456-7890', + 'email': 'a@gmail.com', + 'account_name': 'account', + 'address_line': self.lead.address_line, + 'street': self.lead.street, + 'city': self.lead.city, + 'state': self.lead.state, + 'postcode': self.lead.postcode, + 'country': self.lead.country, + 'lead_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read()), + 'assigned_to': str(self.user.id), + 'tags': 'tag' + }) + self.assertEqual(response.status_code, 200) + + def test_lead_convert(self): + response = self.client.get('/leads/' + str(self.lead.id) + '/convert/') + self.assertEqual(response.status_code, 302) + + +class TestLeadDetailView(TestLeadModel, TestCase): + + def test_lead_detail_view(self): + response = self.client.get( + reverse('leads:view_lead', kwargs={'pk': self.lead.id})) + self.assertEqual(response.status_code, 200) + + +class TestLeadFromSite(TestLeadModel, TestCase): + + def create_lead_from_site(self): + response = self.client.post('/leads/create/from-site/', {'apikey': self.api_seetings.apikey}) + self.assertEqual(response.status_code, 200) diff --git a/marketing/forms.py b/marketing/forms.py index 0327bf8b..bc9e0762 100644 --- a/marketing/forms.py +++ b/marketing/forms.py @@ -10,9 +10,12 @@ from marketing.models import ( EmailTemplate, Campaign, Tag ) +email_regex = '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$' + def csv_doc_validate(document): temp_row = [] + invalid_row = [] reader = csv.reader((document.read().decode("utf-8")).splitlines()) # csv_headers = ["first name", "last name", "email"] csv_headers = ["first name", "email"] @@ -20,6 +23,7 @@ def csv_doc_validate(document): required_headers = ["first name", "email"] for y_index, row in enumerate(reader): each = {} + invalid_each = {} if y_index == 0: csv_headers = [header_name.lower() for header_name in row if header_name] @@ -40,15 +44,23 @@ def csv_doc_validate(document): 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} + # message = 'Missing required value %s for row %s' % ( + # csv_headers[x_index], y_index + 1) + # return {"error": True, "message": message} + invalid_each[csv_headers[x_index]] = cell_value + else: + if csv_headers[x_index] == "email": + if re.match(email_regex, cell_value) is None: + invalid_each[csv_headers[x_index]] = cell_value each[csv_headers[x_index]] = cell_value - temp_row.append(each) - return {"error": False, "validated_rows": temp_row} + if invalid_each: + invalid_row.append(each) + else: + temp_row.append(each) + return {"error": False, "validated_rows": temp_row, "invalid_rows": invalid_row} -def get_validated_rows(wb, sheet_name, validated_rows): +def get_validated_rows(wb, sheet_name, validated_rows, invalid_rows): # headers = ["first name", "last name", "email"] # required_headers = ["first name", "last name", "email"] headers = ["first name", "email"] @@ -68,6 +80,7 @@ def get_validated_rows(wb, sheet_name, validated_rows): continue else: temp_row = [] + invalid_row = [] for x_index, cell in enumerate(row): try: headers[x_index] @@ -75,29 +88,28 @@ def get_validated_rows(wb, sheet_name, validated_rows): 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} + # message = 'Missing required \ + # value %s for row %s in sheet %s'\ + # % (headers[x_index], y_index + 1, sheet_name) + # return {"error": True, "message": message} + invalid_row.append(headers[x_index]) temp_row.append(cell.value) - if len(temp_row) >= len(required_headers): - validated_rows.append(temp_row) - return validated_rows + if len(invalid_row) > 0: + invalid_rows.append(temp_row) + else: + if len(temp_row) >= len(required_headers): + validated_rows.append(temp_row) + return validated_rows, invalid_rows def xls_doc_validate(document): wb = openpyxl.load_workbook(document) sheets = wb.get_sheet_names() validated_rows = [] + invalid_rows = [] for sheet_name in sheets: - get_validated_rows(wb, sheet_name, validated_rows) - return {"error": False, "validated_rows": validated_rows} + validated_rows, invalid_rows = get_validated_rows(wb, sheet_name, validated_rows, invalid_rows) + return {"error": False, "validated_rows": validated_rows, "invalid_rows": invalid_rows} def import_document_validator(document): @@ -141,7 +153,8 @@ class ContactListForm(forms.ModelForm): if data.get("error"): raise forms.ValidationError(data.get("message")) else: - self.validated_rows = data.get("validated_rows") + self.validated_rows = data.get("validated_rows", []) + self.invalid_rows = data.get("invalid_rows", []) return document def clean_tags(self): @@ -265,7 +278,7 @@ class SendCampaignForm(forms.ModelForm): class Meta: model = Campaign - fields = ['title', 'subject', 'html', 'email_template'] + fields = ['title', 'subject', 'html', 'email_template', 'attachment'] def __init__(self, *args, **kwargs): super(SendCampaignForm, self).__init__(*args, **kwargs) @@ -278,11 +291,11 @@ class SendCampaignForm(forms.ModelForm): 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) == []: + contact_list == []: raise forms.ValidationError( "Please choose any of the Contact List") else: - for each in json.loads(contact_list): + for each in contact_list: if not ContactList.objects.filter(id=each).first(): raise forms.ValidationError( "Please choose a valid Contact List") diff --git a/marketing/migrations/0004_auto_20190315_1443.py b/marketing/migrations/0004_auto_20190315_1443.py new file mode 100644 index 00000000..afdbea8b --- /dev/null +++ b/marketing/migrations/0004_auto_20190315_1443.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.7 on 2019-03-15 09:13 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0003_failedcontact'), + ] + + operations = [ + migrations.AlterField( + model_name='contact', + name='contact_number', + field=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}$')]), + ), + migrations.AlterField( + model_name='failedcontact', + name='contact_number', + field=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}$')]), + ), + ] diff --git a/marketing/migrations/0005_campaign_timezone.py b/marketing/migrations/0005_campaign_timezone.py new file mode 100644 index 00000000..31de46df --- /dev/null +++ b/marketing/migrations/0005_campaign_timezone.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.7 on 2019-03-18 07:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0004_auto_20190315_1443'), + ] + + operations = [ + migrations.AddField( + model_name='campaign', + name='timezone', + field=models.CharField(default='UTC', max_length=100), + ), + ] diff --git a/marketing/migrations/0006_campaign_attachment.py b/marketing/migrations/0006_campaign_attachment.py new file mode 100644 index 00000000..d8cad5c9 --- /dev/null +++ b/marketing/migrations/0006_campaign_attachment.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.7 on 2019-03-19 10:29 + +from django.db import migrations, models +import marketing.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('marketing', '0005_campaign_timezone'), + ] + + operations = [ + migrations.AddField( + model_name='campaign', + name='attachment', + field=models.FileField(blank=True, max_length=1000, null=True, upload_to=marketing.models.get_campaign_attachment_path), + ), + ] diff --git a/marketing/models.py b/marketing/models.py index fea77fd0..87ed96eb 100644 --- a/marketing/models.py +++ b/marketing/models.py @@ -1,11 +1,14 @@ +import os from datetime import datetime, timedelta from django.db import models from django.utils.timesince import timesince from django.utils.translation import ugettext_lazy as _ from django.core.validators import RegexValidator +from django.dispatch import receiver from django.db.models import Sum from django.template.defaultfilters import slugify from common.models import User +from common.utils import convert_to_custom_timezone class Tag(models.Model): @@ -150,6 +153,13 @@ class FailedContact(models.Model): return self.email +def get_campaign_attachment_path(self, filename): + file_split = filename.split('.') + file_extension = file_split[-1] + path = "%s_%s" % (file_split[0], str(datetime.now())) + return "campaigns/attachment/" + slugify(path) + "." + file_extension + + class Campaign(models.Model): STATUS_CHOICES = ( ('Scheduled', 'Scheduled'), @@ -170,6 +180,7 @@ class Campaign(models.Model): email_template = models.ForeignKey( EmailTemplate, blank=True, null=True, on_delete=models.SET_NULL) schedule_date_time = models.DateTimeField(blank=True, null=True) + timezone = models.CharField(max_length=100, default='UTC') reply_to_email = models.EmailField(blank=True, null=True) subject = models.CharField(max_length=5000) html = models.TextField() @@ -182,6 +193,8 @@ class Campaign(models.Model): bounced = models.IntegerField(default='0') status = models.CharField( default="Preparing", choices=STATUS_CHOICES, max_length=20) + attachment = models.FileField( + max_length=1000, upload_to=get_campaign_attachment_path, blank=True, null=True) @property def no_of_unsubscribers(self): @@ -209,6 +222,27 @@ class Campaign(models.Model): def created_on_format(self): return self.created_on.strftime('%b %d, %Y %I:%M %p') + @property + def sent_on_format(self): + if self.schedule_date_time: + c_schedule_date_time = convert_to_custom_timezone(self.schedule_date_time, self.timezone) + return c_schedule_date_time.strftime('%b %d, %Y %I:%M %p') + else: + c_created_on = convert_to_custom_timezone(self.created_on, self.timezone) + return c_created_on.strftime('%b %d, %Y %I:%M %p') + + +@receiver(models.signals.pre_delete, sender=Campaign) +def comment_attachments_delete(sender, instance, **kwargs): + attachment = instance.attachment + if attachment: + try: + if os.path.isfile(attachment.path): + os.remove(attachment.path) + except Exception: + return False + return True + class Link(models.Model): campaign = models.ForeignKey( diff --git a/marketing/tasks.py b/marketing/tasks.py index 8be0e0fc..7341e1ab 100644 --- a/marketing/tasks.py +++ b/marketing/tasks.py @@ -2,12 +2,13 @@ import datetime import hashlib import pytz import requests +from mimetypes import MimeTypes from celery.task import task from django.conf import settings from django.core.mail import EmailMessage from django.template import Template, Context from common.utils import convert_to_custom_timezone -from marketing.models import Contact, ContactList, Campaign, CampaignLog +from marketing.models import Contact, FailedContact, ContactList, Campaign, CampaignLog @task @@ -26,7 +27,7 @@ def campaign_click(request): @task -def upload_csv_file(data, user, contact_lists): +def upload_csv_file(data, invalid_data, user, contact_lists): for each in data: contact = Contact.objects.filter(email=each['email']).first() if not contact: @@ -45,6 +46,24 @@ def upload_csv_file(data, user, contact_lists): for contact_list in contact_lists: contact.contact_list.add(ContactList.objects.get(id=int(contact_list))) + for each in invalid_data: + contact = FailedContact.objects.filter(email=each['email']).first() + if not contact: + contact = FailedContact.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( @@ -76,13 +95,18 @@ def get_campaign_message_id(campaign): def run_campaign(campaign): try: campaign = Campaign.objects.get(id=campaign) + attachments = [] + if campaign.attachment: + file_path = campaign.attachment.path + file_name = file_path.split("/")[-1] + content = open(file_path, 'rb').read() + mime = MimeTypes() + mime_type = mime.guess_type(file_path) + attachments.append((file_name, content, mime_type[0])) 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() + contacts = Contact.objects.filter( + contact_list__in=[each_list for each_list in campaign.contact_lists.all()]) default_html = campaign.html_processed for each_contact in contacts: html = default_html @@ -94,18 +118,15 @@ def run_campaign(campaign): 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' + domain_name = 'django-crm.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 '', @@ -117,13 +138,9 @@ def run_campaign(campaign): 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()) + to_email = [each_contact.email] send_campaign_mail( - subject, mail_html, from_email, [each_contact.email], [], [reply_to_email], []) + subject, mail_html, from_email, to_email, [], [reply_to_email], attachments) except Exception as e: print (e) pass @@ -161,14 +178,16 @@ 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', '') + schedule_date_time = each.schedule_date_time + + sent_time = datetime.now().strftime('%Y-%m-%d %H:%M') + sent_time = datetime.strptime(sent_time, '%Y-%m-%d %H:%M') + local_tz = pytz.timezone(settings.TIME_ZONE) sent_time = local_tz.localize(sent_time) - sent_time = convert_to_custom_timezone(sent_time, each.timezone) + sent_time = convert_to_custom_timezone(sent_time, each.timezone, to_utc=True) + if ( str(each.schedule_date_time.date()) == str(sent_time.date()) and - str(schedule_date_time_hour) == str(sent_time.hour) + str(schedule_date_time.hour) == str(sent_time.hour) ): run_campaign(each) diff --git a/marketing/tests.py b/marketing/tests.py index d5df681b..1db06eb1 100644 --- a/marketing/tests.py +++ b/marketing/tests.py @@ -27,7 +27,7 @@ class TestTemplates(TestMarketingModel, TestCase): url4 = '/m/cl/lists/' url5 = '/m/cl/list/new/' url6 = '/m/cl/list/cnew/' - url7 = '/m/cl/list/detail/' + # url7 = '/m/cl/list/detail/' url8 = '/m/et/list/' url9 = '/m/et/new/' # url10 = '/m/et/edit/' @@ -42,7 +42,7 @@ class TestTemplates(TestMarketingModel, TestCase): resp4 = self.client.get(url4) resp5 = self.client.get(url5) resp6 = self.client.get(url6) - resp7 = self.client.get(url7) + # resp7 = self.client.get(url7) resp8 = self.client.get(url8) resp9 = self.client.get(url9) # resp10 = self.client.get(url10) @@ -57,7 +57,7 @@ class TestTemplates(TestMarketingModel, TestCase): 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(resp7.status_code, 200) self.assertEqual(resp8.status_code, 200) self.assertEqual(resp9.status_code, 200) # self.assertEqual(resp10.status_code, 200) diff --git a/marketing/urls.py b/marketing/urls.py index 86adfebd..8da6f9e0 100644 --- a/marketing/urls.py +++ b/marketing/urls.py @@ -3,7 +3,8 @@ 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_delete, email_template_detail, campaign_list, campaign_new, campaign_edit, campaign_details, campaign_delete, - edit_contact_list, delete_contact_list + edit_contact_list, delete_contact_list, failed_contact_list_detail, failed_contact_list_download_delete, + campaign_link_click, campaign_open ) app_name = 'marketing' @@ -15,10 +16,13 @@ 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('cl/list//detail/', contact_list_detail, name='contact_list_detail'), + path('cl/list//failed/', failed_contact_list_detail, name='failed_contact_list_detail'), + path('cl/list//failed/download/', + failed_contact_list_download_delete, name='failed_contact_list_download_delete'), + path('cl/list//edit/', edit_contact_list, name='edit_contact_list'), + path('cl/list//delete/', delete_contact_list, name='delete_contact_list'), path('et/list/', email_template_list, name='email_template_list'), path('et/new/', email_template_new, name='email_template_new'), @@ -30,5 +34,7 @@ urlpatterns = [ 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//delete/', campaign_delete, name='campaign_delete') + path('cm//delete/', campaign_delete, name='campaign_delete'), + path('cm/link//e//', campaign_link_click, name='campaign_link_click'), + path('cm/track-email//contact//', campaign_open, name='campaign_open'), ] diff --git a/marketing/views.py b/marketing/views.py index 8d829d42..95295d78 100644 --- a/marketing/views.py +++ b/marketing/views.py @@ -1,19 +1,23 @@ +import csv import json import pytz import lxml import lxml.html from lxml.cssselect import CSSSelector -from django.conf import Settings +from datetime import datetime as ddatetime +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.shortcuts import render, get_object_or_404, redirect from django.urls import reverse_lazy, reverse from common import status +from common.utils import convert_to_custom_timezone from marketing.models import ( - Tag, ContactList, Contact, EmailTemplate, Campaign, CampaignLog, Link + Tag, ContactList, Contact, EmailTemplate, Campaign, CampaignLog, Link, CampaignLinkClick, + CampaignOpen ) from marketing.forms import ContactListForm, ContactForm, EmailTemplateForm, SendCampaignForm from marketing.tasks import upload_csv_file, run_campaign @@ -97,7 +101,7 @@ def contact_list_new(request): instance.tags.add(tag) if request.FILES.get('contacts_file'): upload_csv_file.delay( - form.validated_rows, request.user.id, [instance.id]) + form.validated_rows, form.invalid_rows, request.user.id, [instance.id]) return JsonResponse({'error': False, 'data': form.data}, @@ -144,7 +148,7 @@ def edit_contact_list(request, pk): instance.tags.add(tag) if request.FILES.get('contacts_file'): upload_csv_file.delay( - form.validated_rows, request.user.id, [instance.id]) + form.validated_rows, form.invalid_rows, request.user.id, [instance.id]) return JsonResponse({'error': False, 'data': form.data}, status=status.HTTP_200_OK) @@ -215,8 +219,38 @@ def edit_contact(request): @login_required(login_url='/login') -def contact_list_detail(request): - return render(request, 'marketing/lists/detail.html') +def contact_list_detail(request, pk): + contact_list = get_object_or_404(ContactList, pk=pk) + contacts_list = contact_list.contacts.all() + data = {'contact_list': contact_list, "contacts_list": contacts_list} + return render(request, 'marketing/lists/detail.html', data) + + +@login_required(login_url='/login') +def failed_contact_list_detail(request, pk): + contact_list = get_object_or_404(ContactList, pk=pk) + failed_contacts_list = contact_list.failed_contacts.all() + data = {'contact_list': contact_list, "failed_contacts_list": failed_contacts_list} + return render(request, 'marketing/lists/failed_detail.html', data) + + +@login_required(login_url='/login') +def failed_contact_list_download_delete(request, pk): + contact_list = get_object_or_404(ContactList, pk=pk) + failed_contacts_list = contact_list.failed_contacts.all() + if failed_contacts_list.count() > 0: + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="failed_contacts.csv"' + writer = csv.writer(response) + writer.writerow(["company name", "email", "first name", "last name", "city", "state"]) + + for contact in failed_contacts_list: + writer.writerow([ + contact.company_name, contact.email, contact.name, contact.last_name, contact.city, contact.state]) + failed_contacts_list.delete() + return response + else: + return HttpResponseRedirect(reverse('marketing:contact_lists', kwargs={"pk": pk})) @login_required(login_url='/login') @@ -336,23 +370,25 @@ def campaign_new(request): # contact_list__id__in=json.loads(request.GET.get('contact_list'))).distinct() data = { - 'contact_lists': contact_lists, 'email_templates': email_templates, "timezones": TIMEZONE_CHOICES} + 'contact_lists': contact_lists, 'email_templates': email_templates, + "timezones": TIMEZONE_CHOICES, "settings_timezone": settings.TIME_ZONE} # 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) + form = SendCampaignForm(request.POST, request.FILES) if form.is_valid(): instance = form.save(commit=False) instance.created_by = request.user - if request.POST['from_email']: + if request.POST.get('from_email'): instance.from_email = request.POST['from_email'] - if request.POST['from_name']: + if request.POST.get('from_name'): instance.from_name = request.POST['from_name'] - if request.POST['reply_to_email']: + if request.POST.get('reply_to_email'): instance.reply_to_email = request.POST['reply_to_email'] + # if request.FILES.get('attachment'): + # instance.attachment = request.FILES.get('attachment') instance.save() - for each in json.loads(request.POST['contact_list']): + for each in request.POST.getlist('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: @@ -373,33 +409,31 @@ def campaign_new(request): links = set(llist) # Replace Links with new One - domain_url = '%s://%s' % (request.scheme, request.META['HTTP_HOST']) + # domain_url = '%s://%s' % (request.scheme, request.META['HTTP_HOST']) + domain_url = settings.URL_FOR_LINKS 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}}"') + 'href="' + l + '"', 'href="' + domain_url + '/m/cm/link/' + 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: + if request.POST.get('schedule_later', None) == 'true': 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) + user_timezone = request.POST.get('timezone', '') + if user_timezone and schedule_date_time: + start_time_object = ddatetime.strptime(schedule_date_time, '%Y-%m-%d %H:%M') + schedule_date_time = convert_to_custom_timezone(start_time_object, user_timezone, to_utc=True) instance.schedule_date_time = schedule_date_time - # instance.timezone = request.POST['timezone'] - instance.save() + instance.timezone = user_timezone + instance.save() + else: + run_campaign.delay(instance.id) - return JsonResponse({'error': True, 'data': form.data}, status=status.HTTP_201_CREATED) + return JsonResponse({'error': False, 'data': form.data}, status=status.HTTP_201_CREATED) return JsonResponse({'error': True, 'errors': form.errors}, status=status.HTTP_200_OK) @@ -409,10 +443,186 @@ def campaign_edit(request): @login_required(login_url='/login') -def campaign_details(request): - return render(request, 'marketing/campaign/details.html') +def campaign_details(request, pk): + try: + campaign = Campaign.objects.get(pk=pk) + except Campaign.DoesNotExist: + return redirect(reverse('marketing:campaign_list')) + + contact_lists = campaign.contact_lists.all() + if (request.user.is_admin or request.user.is_superuser): + contact_lists = contact_lists + else: + contact_lists = contact_lists.filter( + is_public=True, created_by=request.user) + + contacts = Contact.objects.filter(contact_list__in=contact_lists) + contact_list_ids = campaign.contact_lists.all().values_list('id', flat=True) + + contacts = Contact.objects.filter(contact_list__id__in=contact_list_ids) + + all_contacts = contacts + bounced_contacts = contacts.filter(is_bounced=True) + unsubscribe_contacts = contacts.filter( + is_unsubscribed=True) + read_contacts = campaign.marketing_links.filter(Q(clicks__gt=0)) + # read_contacts = + sent_contacts = contacts.exclude( + Q(is_bounced=True) | Q(is_unsubscribed=True)) + if request.POST: + contacts = contacts.filter(Q(name__icontains=request.POST['search'])) + + tab = request.GET.get('tab', 'contacts') + per_page = request.GET.get("per_page", 10) + page = request.GET.get('page', 1) + + contacts_paginator = Paginator(contacts.order_by('id'), per_page) + if (tab == "contacts"): + page = page + else: + page = 1 + try: + contacts = contacts_paginator.page(page) + except PageNotAnInteger: + contacts = contacts_paginator.page(1) + except EmptyPage: + contacts = contacts_paginator.page(contacts_paginator.num_pages) + + unsubscribe_contacts_paginator = Paginator(unsubscribe_contacts.order_by('id'), per_page) + if (tab == "unsubscribe_contacts"): + page = page + else: + page = 1 + + try: + unsubscribe_contacts = unsubscribe_contacts_paginator.page(page) + except PageNotAnInteger: + unsubscribe_contacts = unsubscribe_contacts_paginator.page(1) + except EmptyPage: + unsubscribe_contacts = unsubscribe_contacts_paginator.page(unsubscribe_contacts_paginator.num_pages) + + bounced_contacts_paginator = Paginator(bounced_contacts.order_by('id'), per_page) + if (tab == "bounced_contacts"): + page = page + else: + page = 1 + + try: + bounced_contacts = bounced_contacts_paginator.page(page) + except PageNotAnInteger: + bounced_contacts = bounced_contacts_paginator.page(1) + except EmptyPage: + bounced_contacts = bounced_contacts_paginator.page(bounced_contacts_paginator.num_pages) + + sent_contacts_paginator = Paginator(sent_contacts.order_by('id'), per_page) + if (tab == "sent_contacts"): + page = page + else: + page = 1 + + try: + sent_contacts = sent_contacts_paginator.page(page) + except PageNotAnInteger: + sent_contacts = sent_contacts_paginator.page(1) + except EmptyPage: + sent_contacts = sent_contacts_paginator.page(sent_contacts_paginator.num_pages) + + all_contacts_paginator = Paginator(all_contacts.order_by('id'), per_page) + if (tab == "all_contacts"): + page = page + else: + page = 1 + + try: + all_contacts = all_contacts_paginator.page(page) + except PageNotAnInteger: + all_contacts = all_contacts_paginator.page(1) + except EmptyPage: + all_contacts = all_contacts_paginator.page(all_contacts_paginator.num_pages) + + read_contacts_paginator = Paginator(read_contacts.order_by('id'), per_page) + if (tab == "read_contacts"): + page = page + else: + page = 1 + + try: + read_contacts = read_contacts_paginator.page(page) + except PageNotAnInteger: + read_contacts = read_contacts_paginator.page(1) + except EmptyPage: + read_contacts = read_contacts_paginator.page(read_contacts_paginator.num_pages) + + data = {'contact_lists': contact_lists, "campaign": campaign, + "contacts": contacts, 'unsubscribe_contacts': unsubscribe_contacts, + 'bounced_contacts': bounced_contacts, 'read_contacts': read_contacts, + 'sent_contacts': sent_contacts, 'all_contacts': all_contacts, 'tab': tab} + + return render(request, 'marketing/campaign/details.html', data) @login_required(login_url='/login') -def campaign_delete(request): - return render(request, 'marketing/campaign/details.html') +def campaign_delete(request, pk): + try: + campaign = Campaign.objects.get(id=pk) + campaign.delete() + redirect_url = reverse('marketing:campaign_list') + except Campaign.DoesNotExist: + redirect_url = reverse('marketing:campaign_list') + return redirect(redirect_url) + + +def campaign_link_click(request, link_id, email_id): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + + if x_forwarded_for: + ipaddress = x_forwarded_for.split(',')[-1].strip() + else: + ipaddress = request.META.get('REMOTE_ADDR') + + link = Link.objects.filter(id=link_id).first() + contact = Contact.objects.filter(id=email_id).first() + if link and contact: + campaign_link_click = CampaignLinkClick.objects.filter( + link=link, campaign=link.campaign, contact=contact).first() + if not campaign_link_click: + campaign_link_click = CampaignLinkClick.objects.create( + link=link, campaign=link.campaign, contact=contact, ip_address=ipaddress) + link.unique += 1 + else: + campaign_link_click.ip_address = ipaddress + link.clicks += 1 + link.save() + campaign_link_click.save() + url = link.original + else: + url = settings.URL_FOR_LINKS + return redirect(url) + + +def campaign_open(request, campaign_log_id, email_id): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + + if x_forwarded_for: + ipaddress = x_forwarded_for.split(',')[-1].strip() + else: + ipaddress = request.META.get('REMOTE_ADDR') + + campaign_log = CampaignLog.objects.filter(id=campaign_log_id).first() + contact = Contact.objects.filter(id=email_id).first() + image_data = settings.STATIC_URL + 'images/' + image_data = open(settings.STATIC_ROOT + '/images/company.png', 'rb').read() + if campaign_log and contact: + campaign_open = CampaignOpen.objects.filter( + campaign=campaign_log.campaign, contact=contact).first() + if not campaign_open: + campaign_open = CampaignOpen.objects.create( + campaign=campaign_log.campaign, contact=contact, ip_address=ipaddress) + campaign_log.campaign.opens_unique += 1 + else: + campaign_open.ip_address = ipaddress + campaign_log.campaign.opens += 1 + campaign_log.campaign.status = "Sent" + campaign_log.campaign.save() + campaign_open.save() + return HttpResponse(image_data, content_type="image/png") diff --git a/opportunity/tests.py b/opportunity/tests.py index 6ec6a51c..325ae2cf 100644 --- a/opportunity/tests.py +++ b/opportunity/tests.py @@ -4,12 +4,14 @@ from accounts.models import Account from contacts.models import Contact from common.models import User, Comment, Attachments, Address from cases.models import Case +from django.core.files.uploadedfile import SimpleUploadedFile # Create your tests here. class OpportunityModel(object): + def setUp(self): self.user = User.objects.create( @@ -47,6 +49,7 @@ class OpportunityModel(object): closed_on="2016-05-04", description="hgfdxc", created_by=self.user) + self.opportunity.assigned_to.add(self.user) self.case = Case.objects.create( name="raghu", case_type="Problem", status="New", account=self.account, priority="Low", description="something", @@ -58,6 +61,7 @@ class OpportunityModel(object): class OpportunityCreateTestCase(OpportunityModel, TestCase): + def test_opportunity_create(self): response = self.client.get('/opportunities/create/', { 'name': "meghan", 'amount': "478", @@ -67,8 +71,14 @@ class OpportunityCreateTestCase(OpportunityModel, TestCase): self.assertEqual(response.status_code, 200) def test_opportunity_create_post(self): + upload_file = open('static/images/user.png', 'rb') url = '/opportunities/create/' - data = {'name': "micky", 'amount': "500", 'stage': "CLOSED WON"} + data = {'name': "micky", 'amount': "500", 'stage': "CLOSED WON", + 'assigned_to': str(self.user.id), + 'contacts': str(self.contacts.id), + 'tags': 'tag', + 'oppurtunity_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())} response = self.client.post(url, data) # self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 200) @@ -113,16 +123,24 @@ class EditOpportunityTestCase(OpportunityModel, TestCase): 'stage': "negotiation/review", 'lead_source': "Call", 'probability': "58", 'closed_on': "2016-05-04", 'description': "hgfdxc"}) - resp = self.client.post( '/opportunities/' + str(self.opportunity.id) + '/edit/', **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + 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'}) + resp1 = self.client.post( + '/opportunities/create/', **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) self.assertEqual(resp1.status_code, 200) def test_update_opportunity(self): + upload_file = open('static/images/user.png', 'rb') url = '/opportunities/' + str(self.opportunity.id) + '/edit/' data = { 'name': "meghan", 'amount': "478", 'stage': "QUALIFICATION", - 'probability': "58", 'closed_on': "2016-05-04", 'description': "hgfdxc"} + 'probability': "58", 'closed_on': "2016-05-04", + 'description': "hgfdxc", + 'tags': 'tag', 'assigned_to': str(self.user.id), + 'contacts': str(self.contacts.id), + 'oppurtunity_attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())} response = self.client.post(url, data) # self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 200) @@ -137,6 +155,7 @@ class EditOpportunityTestCase(OpportunityModel, TestCase): class OpportunityListView(OpportunityModel, TestCase): + def test_opportunity_list(self): self.opportunity = Opportunity.objects.all() response = self.client.get('/opportunities/list/') @@ -164,6 +183,7 @@ class OpportunityListView(OpportunityModel, TestCase): class ContactGetViewTestCase(OpportunityModel, TestCase): + def test_get_contact(self): url = '/opportunities/contacts/' response = self.client.get(url) @@ -173,6 +193,7 @@ class ContactGetViewTestCase(OpportunityModel, TestCase): class CommentTestCase(OpportunityModel, TestCase): + def test_comment_add(self): response = self.client.post( '/opportunities/comment/add/', {'opportunityid': self.opportunity.id}) @@ -188,11 +209,21 @@ class CommentTestCase(OpportunityModel, TestCase): '/opportunities/comment/remove/', {'comment_id': self.comment.id}) self.assertEqual(response.status_code, 200) + def test_comment_form_valid(self): + response = self.client.post( + '/opportunities/comment/add/', {'opportunityid': self.opportunity.id, + 'comment': 'hello'}) + self.assertEqual(response.status_code, 200) + class AttachmentTestCase(OpportunityModel, TestCase): + def test_attachment_add(self): + upload_file = open('static/images/user.png', 'rb') url = "/opportunities/attachment/add/" - data = {'opportunityid': self.opportunity.id} + data = {'opportunityid': self.opportunity.id, + 'attachment': SimpleUploadedFile( + upload_file.name, upload_file.read())} response = self.client.post(url, data) self.assertEqual(response.status_code, 200) @@ -204,9 +235,9 @@ class AttachmentTestCase(OpportunityModel, TestCase): 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/static/images/company.png b/static/images/company.png new file mode 100644 index 00000000..fb2d9349 Binary files /dev/null and b/static/images/company.png differ diff --git a/templates/marketing/campaign/details.html b/templates/marketing/campaign/details.html index bbc17464..44eaa309 100644 --- a/templates/marketing/campaign/details.html +++ b/templates/marketing/campaign/details.html @@ -2,561 +2,545 @@ {% load staticfiles %} {% load paginate %} {% block content %} -{{ block.super }} +{{ block.super }}
-
-
-
-
-
-
-
-
- Campaign report -
- Sent on Tue, Nov 22, 2016, 12:00AM to 64211 Subscribers -
-
- -
-
-
Title: Campaign title
- -
- To: List: Campaign Users List: Dummy Users List: Example Users List: Template Users List: Successful Users -
-
-
- -
-
-
- 21.16% Opened 1356 unique / opened 1788 times -
-
-
-
- 216 Not Opened -
-
-
-
- 21.16% Clicked a link 120 unique clicks -
-
-
-
- 21.6% UnSubscribed 5 unsubscribed -
-
-
-
- 0.16% Bounced 3 bounced -
-
-
-
- 6% marked as spam 0 marked as spam -
-
-
-
-
-
-
-
-
- Last Activity - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Link(url)UniqueTotalExport
http://sendy.co/a/w/3c1223 - -
http://sendy.co/a/w/3c1223 - -
http://sendy.co/a/w/3c1223 - -
http://sendy.co/a/w/3c1223 - -
-
-
-
-
-
- -
-
-
-
-
-
-
- Last 5 Opened - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameEmailListStatus
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Subscribed -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Unsubscribed -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Unsubscribed -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Unsubscribed -
-
-
-
-
-
-
- -
-
-
-
-
-
- Last 5 Unsubscribed - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list - UnSubscribed - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - UnSubscribed - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - UnSubscribed - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - UnSubscribed - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - UnSubscribed - - Mar 26, 2017, 04:26PM -
-
-
-
-
-
-
- -
-
-
-
-
-
- Last 5 Bounced - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Bounced - - Mar 26, 2017, 04:26PM -
-
-
-
-
-
-
- -
-
-
-
-
-
- Last 5 Marked as Spam - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Spam - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Spam - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Spam - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Spam - - Mar 26, 2017, 04:26PM -
bcfr cvpx bcfrcvpx@gmail.combcfr users list - Spam - - Mar 26, 2017, 04:26PM -
-
-
-
-
-
-
- -
-
-
-
-
-
- All Countries -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CountryOpensExport
USA20
India10
Russia75
Australia220
Newzeland85
Europe52
USA20
India10
Russia75
Australia220
Newzeland85
Europe52
-
-
-
-
-
-
- -
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Campaign report +
+ Sent on {{ campaign.sent_on_format }} to 64211 Subscribers +
+
+
+
+
Title: {{ campaign.title }}
+ +
+ To: {% for contact_list in campaign.contact_lists.all %}List: {{ contact_list.name }}{% endfor %} +
+
+
+
+
+
+ 21.16% Opened 1356 unique / opened 1788 times +
+
+
+
+ 216 Not Opened +
+
+
+
+ 21.16% Clicked a link 120 unique clicks +
+
+
+
+ 21.6% UnSubscribed 5 unsubscribed +
+
+
+
+ 0.16% Bounced 3 bounced +
+
+
+
+ 6% marked as spam 0 marked as spam +
+
+
+ {% with campaign.marketing_links.all as campaign_marketing_links %} + {% if campaign_marketing_links|length > 0 %} +
+
+
+
+
+
+ Last Activity + +
+
+ + + + + + + + + + + {% if per_page %} + {% paginate per_page campaign_marketing_links %} + {% else%} + {% paginate 10 campaign_marketing_links %} + {% endif %} + {% for link in campaign_marketing_links %} + + + + + + + {% endfor %} + +
Link(url)UniqueTotalExport
{{ link.original }}{{ link.unique }}{{ link.clicks }} + +
+
+
+ +
+
+
+
+ {% endif %} + {% endwith %} +
+
+
+
+
+
+ Last 5 Opened + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailListStatus
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Subscribed +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Unsubscribed +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Unsubscribed +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Unsubscribed +
+
+
+
+
+
+
+
+
+
+
+
+
+ Last 5 Unsubscribed + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list + UnSubscribed + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + UnSubscribed + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + UnSubscribed + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + UnSubscribed + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + UnSubscribed + + Mar 26, 2017, 04:26PM +
+
+
+
+
+
+
+
+
+
+
+
+
+ Last 5 Bounced + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Bounced + + Mar 26, 2017, 04:26PM +
+
+
+
+
+
+
+
+
+
+
+
+
+ Last 5 Marked as Spam + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailListStatusDate
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Spam + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Spam + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Spam + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Spam + + Mar 26, 2017, 04:26PM +
bcfr cvpx bcfrcvpx@gmail.combcfr users list + Spam + + Mar 26, 2017, 04:26PM +
+
+
+
+
+
+
+
+
+
+
+
+
+ All Countries +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CountryOpensExport
USA20
India10
Russia75
Australia220
Newzeland85
Europe52
USA20
India10
Russia75
Australia220
Newzeland85
Europe52
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %} {% block js_block %} -{% endblock js_block %} \ No newline at end of file +{% endblock js_block %} diff --git a/templates/marketing/campaign/index.html b/templates/marketing/campaign/index.html index e7f00630..b4ceb6b7 100644 --- a/templates/marketing/campaign/index.html +++ b/templates/marketing/campaign/index.html @@ -31,7 +31,7 @@ Primary {{ campaign.title }} 5478 - {{ campaign.created_on }} + {{ campaign.sent_on_format }} 20.6% 125 Opened @@ -40,7 +40,10 @@ 15.5% 78 Clicked - + + @@ -49,116 +52,6 @@ No records found! {% endfor %} -
@@ -168,4 +61,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/marketing/campaign/new.html b/templates/marketing/campaign/new.html index b699aeb8..b4ceb6b7 100644 --- a/templates/marketing/campaign/new.html +++ b/templates/marketing/campaign/new.html @@ -6,280 +6,59 @@
-
-
-
-
-
-
- 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] -
  • -
-
-
-
-
-
-
-
-
- -
+
+
+
+
+ Campaigns - {{ campaigns_list|length }} + Add New Campaign +
+
+ + + + + + + + + + + + + {% for campaign in campaigns_list %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
CampaignReceipentsSentUnique OpensUnique ClicksActions
Primary {{ campaign.title }} 5478 + {{ campaign.sent_on_format }} + + 20.6% 125 Opened + + 15.5% 78 Clicked + + + + +
No records found!
- +
{% endblock %} -{% block js_block %} - - -{% endblock js_block %} \ No newline at end of file diff --git a/templates/marketing/lists/detail.html b/templates/marketing/lists/detail.html index e69de29b..64be352d 100644 --- a/templates/marketing/lists/detail.html +++ b/templates/marketing/lists/detail.html @@ -0,0 +1,55 @@ +{% extends 'marketing/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% block content %} +{{ block.super }} +
+
+
+
+
+
+
+ {{ contact_list.name }} - {{ contacts_list|length }} + {% if contact_list.failed_contacts.all|length > 0 %} + Failed Contacts + {% endif %} + Add New Contact +
+
+ + + + + + + + + + + {% for contact in contacts_list %} + + + + + + + {% empty %} + + + + {% endfor %} + +
IDNameEmailActions
{{ forloop.counter }}{{ contact.name }}{{ contact.email }} + + + +
No records found!
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/marketing/lists/failed_detail.html b/templates/marketing/lists/failed_detail.html new file mode 100644 index 00000000..352d5f21 --- /dev/null +++ b/templates/marketing/lists/failed_detail.html @@ -0,0 +1,55 @@ +{% extends 'marketing/base.html' %} +{% load staticfiles %} +{% load paginate %} +{% block content %} +{{ block.super }} +
+
+
+
+
+
+
+ {{ contact_list.name }} - Failed: {{ failed_contacts_list|length }} + {% if contact_list.contacts.all|length > 0 %} + Contacts + {% endif %} + Download & Delete +
+
+ + + + + + + + + + + {% for contact in failed_contacts_list %} + + + + + + + {% empty %} + + + + {% endfor %} + +
IDNameEmailActions
{{ forloop.counter }}{{ contact.name }}{{ contact.email }} + + + +
No records found!
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/marketing/lists/index.html b/templates/marketing/lists/index.html index 40d0ef3d..1054170d 100644 --- a/templates/marketing/lists/index.html +++ b/templates/marketing/lists/index.html @@ -29,7 +29,7 @@ {% for contact_list in contact_lists %} {{ forloop.counter }} - {{ contact_list.name }} + {{ contact_list.name }} {{ contact_list.created_by }} {{ contact_list.updated_on }} @@ -157,4 +157,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/marketing/lists/new.html b/templates/marketing/lists/new.html index 15c52d7b..55e32b37 100644 --- a/templates/marketing/lists/new.html +++ b/templates/marketing/lists/new.html @@ -2,95 +2,94 @@ {% load staticfiles %} {% load paginate %} {% block extralinks %} - + {% endblock extralinks %} {% block content %} -{{ block.super }} -
-
- -
-
+ {{ block.super }} +
+
+
+
+
+
+
+
+
+ {% if contact_list %}Edit{% else %}Create{% endif %} Contact List +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+ + Cancel +
+
+
+
+
+
+
+
+
+
+
{% endblock %} {% block js_block %} - - - + + -{% endblock js_block %} \ No newline at end of file + $(function() { + $('#tags_1').tagsInput({ width: 'auto' }); + }); + +{% endblock js_block %}