diff --git a/contact_us/__init__.py b/contact_us/__init__.py new file mode 100644 index 0000000..25dcd9b --- /dev/null +++ b/contact_us/__init__.py @@ -0,0 +1 @@ +default_app_config = 'contact_us.apps.ContactUsConfig' diff --git a/contact_us/admin.py b/contact_us/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/contact_us/apps.py b/contact_us/apps.py new file mode 100644 index 0000000..251eda5 --- /dev/null +++ b/contact_us/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class ContactUsConfig(AppConfig): + name = 'contact_us' + verbose_name = _("Contact requests") diff --git a/contact_us/context_processors.py b/contact_us/context_processors.py new file mode 100644 index 0000000..b9cc8bb --- /dev/null +++ b/contact_us/context_processors.py @@ -0,0 +1,5 @@ +from contact_us.forms import ContactUsForm + + +def contact_us_form(request): + return {'contact_us_form': ContactUsForm()} diff --git a/contact_us/forms.py b/contact_us/forms.py new file mode 100644 index 0000000..8e9391b --- /dev/null +++ b/contact_us/forms.py @@ -0,0 +1,85 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Div, HTML, Field, Submit +from django import forms +from django.conf import settings +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from ckeditor.widgets import CKEditorWidget + + +from .mixins import RequestNotifiable +from .models import ( + Request, + STATUS_NEW, STATUS_REJECTED, + STATUS_PROCESSED, STATUS_IN_PROCESSING +) + +# -------------------------------- Client-side forms -----------------------------------# + + +class ContactUsForm(RequestNotifiable, forms.ModelForm): + + field_template = 'bootstrap/forms/product_search.html' + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_action = reverse_lazy('contact_us:send') + self.helper.form_method = 'post' + self.helper.layout = Layout( + HTML(""" +
"""), + Field('name', css_class="left-menu__text", placeholder=_("Имя"), template=self.field_template), + Field('email', css_class="left-menu__text", placeholder=_("Email"), template=self.field_template), + Field('subject', css_class="left-menu__text", placeholder=_("Название продукта"), template=self.field_template), + Field('phone', css_class="left-menu__text", placeholder=_("Номер телефона"), template=self.field_template), + Submit('send', _("Отправить"), css_class="left-menu__btn") + ) + super(ContactUsForm, self).__init__(*args, **kwargs) + self.init_fields(self.fields) + + def init_fields(self, fields): + for field_name in fields: + fields[field_name].required = True + + def save(self, commit=True): + self.instance.status = STATUS_NEW + return super().save(commit) + + def send_email(self, request, files=()): + context = { + 'from_email': self.instance.type.author.email, + 'bcc': self.get_def_email(settings) if self.get_def_email(settings) is not None else (), + 'recipients': self.instance.type.author.email, + 'context': { + 'username': self.instance.name, + 'email': self.instance.email, + 'subject': self.instance.subject, + 'message': self.instance.message, + 'sent_at': self.instance.create_at, + 'attachments': files, + 'edit_link': request.scheme + "://" + request.get_host() + reverse_lazy( + 'admin:{}_{}_change'.format( + self.instance._meta.app_label, + self.instance._meta.object_name.lower()), + args=(self.instance.id,) + ).__str__(), + }, + 'files': [], + 'send_at_date': self.instance.create_at, + + } + self.send_request_notification( + context, + ) + + @property + def is_request_sent(self): + return self.notification_sent + + class Meta: + model = Request + fields = ('name', 'email', 'subject', 'phone') diff --git a/contact_us/mixins.py b/contact_us/mixins.py new file mode 100644 index 0000000..638ef89 --- /dev/null +++ b/contact_us/mixins.py @@ -0,0 +1,58 @@ +from django.template.loader import get_template +from django.utils.translation import ugettext_lazy as _ + + +class RequestNotifiable(object): + MAIL_CATEGORY = 'Request' + + SLUG_NOTIFICATION = 'notification' + SLUG_RESPONSE = 'response' + + def __init__(self, *args, **kwargs): + super(RequestNotifiable, self).__init__(*args, **kwargs) + + def get_mail_template(self, **kwargs): + return None + # return MailTemplate.objects.filter(**kwargs).first() + + def send_request_notification(self, context): + """ + :param context + :param template_path string: + :return None: + """ + notification = self.get_mail_template( + slug=self.SLUG_NOTIFICATION, + ) + + kwargs = { + 'subject': _('New contact us request'), + 'message': get_template('contact_us/contact_email_notification.html').template.source, + } + if not notification: + kwargs.update({ + 'name': 'Admin notification', + 'slug': self.SLUG_NOTIFICATION, + 'num_of_retries': 3, + 'is_html': True + }) + # MailTemplate.objects.create(**kwargs) + else: + notification.subject = kwargs.get('subject') + notification.message = kwargs.get('message') + notification.save() + + # send_db_mail( + # self.SLUG_NOTIFICATION, + # context.get('recipients'), + # context.get('subject'), + # context.get('context'), + # from_email=context.get('from_email', tuple()), + # bcc=context.get('bcc', tuple()), + # files=context.get('files', tuple()), + # send_at_date=context.get('send_at_date'), + # retry=True, + # retry_delay=300, + # max_retries=3, + # use_celery=celery_supported() + # ) diff --git a/contact_us/models.py b/contact_us/models.py new file mode 100644 index 0000000..f6a8cb2 --- /dev/null +++ b/contact_us/models.py @@ -0,0 +1,69 @@ +import os +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.validators import RegexValidator +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +# Create your models here. +from core.models import AbstractStatusModel, STATUS_DELETED + +# --------------------- REQUEST STATUS LIST -------------------- + +STATUS_NEW = 0 +from core.models import AbstractStatusModel, STATUS_NEW, STATUS_CHOICES, STATUS_ACTIVE, STATUS_DELETED + +# --------------------- REQUEST STATUS LIST -------------------- + +STATUS_IN_PROCESSING = 10 +STATUS_PROCESSED = 20 +STATUS_REJECTED = 40 + +REQUEST_STATUS_CHOICES = ( + STATUS_CHOICES[0], + (STATUS_IN_PROCESSING, _('Processing')), + (STATUS_PROCESSED, _('Processed')), + (STATUS_REJECTED, _('Rejected')), + STATUS_CHOICES[-1] +) + +REQUEST_DEFAULT_STATUS = STATUS_NEW + +# ----------------- REQUEST FILE STATUS LIST ------------------ + +FILE_REQUEST_STATUS_CHOICES = ( + (STATUS_ACTIVE, _('Active')), + (STATUS_DELETED, _('Deleted')), +) + +FILE_REQUEST_DEFAULT_STATUS = STATUS_ACTIVE + + +class Request(AbstractStatusModel): + name = models.CharField(_('Name'), max_length=255) + email = models.EmailField(_('Email')) + subject = models.CharField(_('Subject'), max_length=500) + message = models.TextField(blank=True, null=True) + phone_regex = RegexValidator(regex=r'^\((+7)|8)?\d{10}$', + message="Phone number must be entered in the format: '+99999999999'. Up to 12 digits allowed.") + phone = models.CharField(validators=[phone_regex], max_length=12, blank=True, null=True) + status = models.SmallIntegerField(_('Status'), default=REQUEST_DEFAULT_STATUS, choices=REQUEST_STATUS_CHOICES) + + @property + def is_status_processed(self): + return self.status == STATUS_PROCESSED + + @property + def is_status_rejected(self): + return self.status == STATUS_REJECTED + + @property + def is_status_processing(self): + return self.status == STATUS_IN_PROCESSING + + def __str__(self): + return self.subject + + class Meta: + verbose_name = _('Request') + verbose_name_plural = _('Requests') diff --git a/contact_us/templatetags/__init__.py b/contact_us/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contact_us/templatetags/custom_admin_modify.py b/contact_us/templatetags/custom_admin_modify.py new file mode 100644 index 0000000..5edda81 --- /dev/null +++ b/contact_us/templatetags/custom_admin_modify.py @@ -0,0 +1,8 @@ +from django.contrib.admin.templatetags.admin_modify import submit_row, register + + +@register.inclusion_tag('admin/contact_us/custom_submit_line.html', takes_context=True) +def custom_submit_row(context): + ctx = submit_row(context) + ctx['show_answer'] = context.get('show_answer',True) + return ctx diff --git a/contact_us/tests.py b/contact_us/tests.py new file mode 100644 index 0000000..0716457 --- /dev/null +++ b/contact_us/tests.py @@ -0,0 +1,22 @@ +from django.test import TestCase + +from core.tests import BehaviourTestCaseMixin + +from .models import Request + + +# Create your tests here. +class SendRequestWithTestCase(BehaviourTestCaseMixin, TestCase): + model = Request + + def create_instance(self, **kwargs): + return self.get_model() + + def send_form_without_attachments_by_db_mailer(self): + pass + + def send_form_with_attachments_by_db_mailer(self): + pass + + def save_attachments_locally(self): + pass diff --git a/contact_us/urls.py b/contact_us/urls.py new file mode 100644 index 0000000..e2e7f8a --- /dev/null +++ b/contact_us/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from .views import ContactUsFormView + +urlpatterns = [ + url(r'^send/$', ContactUsFormView.as_view(), name='send'), +] diff --git a/contact_us/views.py b/contact_us/views.py new file mode 100644 index 0000000..953eab4 --- /dev/null +++ b/contact_us/views.py @@ -0,0 +1,62 @@ +import logging + +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.db import transaction +from django.urls import reverse_lazy +from django.views.generic import FormView +from django.utils.translation import ugettext_lazy as _ + +from .forms import ContactUsForm + +logger = logging.getLogger(__name__) + + +# Create your views here. +class ContactUsFormView(SuccessMessageMixin, FormView): + template_name = None + form_class = ContactUsForm + http_method_names = ['post'] + success_message = _('Request has been sent successfully!') + error_message = _('Some errors occurred during sending the request. Check the input fields or try latter.') + + def get_initial(self): + initial = super().get_initial() + if self.request.method == "GET": + initial['type'] = RequestType.objects.order_by('order').first() + if self.request.user.is_authenticated(): + initial['name'] = self.request.user.profile.get_full_name(self.request, self.request.user.profile) + initial['email'] = self.request.user.email + return initial + + def form_valid(self, form): + try: + with transaction.atomic(): + if self.request.user.is_authenticated(): + form.instance.user = self.request.user + form.save() + files = [] + if len(self.request.FILES) > 0: + attachments = AttachmentFormSet(self.request.POST, self.request.FILES, form_kwargs={'request': form.instance}) + if attachments.is_valid(): + attachments.save() + files = attachments.get_file_absolute_urls(self.request) + form.send_email(self.request, files) + return super().form_valid(form) + except ValueError as ve: + logger.critical(ve) + messages.error(self.request, self.error_message, 'danger') + except Exception as e: + logger.critical(e) + messages.error(self.request, self.error_message, 'danger') + + return self.form_invalid(form) + + def form_invalid(self, form): + context = self.get_context_data() + if len(form.errors) > 0: + context['form_show_errors'] = True + return self.render_to_response(context) + + def get_success_url(self): + return super().get_success_url()