eviltnan/freeturn

View on GitHub
crm/wagtail_admin/project.py

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
import logging
from datetime import timedelta

from django.conf.urls import url
from django.contrib.admin.utils import quote
from django.forms import CharField, HiddenInput
from django.shortcuts import redirect, get_object_or_404
from django.template import Template, Context
from django.template.defaultfilters import pluralize
from django.urls import reverse
from django.utils import timezone
from django_filters.fields import ModelChoiceField
from django_fsm import TransitionNotAllowed
from google.api_core.exceptions import GoogleAPIError
from instance_selector.widgets import InstanceSelectorWidget
from wagtail.admin import messages
from wagtail.admin.forms import WagtailAdminModelForm
from wagtail.admin.rich_text import get_rich_text_editor_widget
from wagtail.admin.search import SearchArea
from wagtail.contrib.modeladmin.helpers import AdminURLHelper, ButtonHelper
from wagtail.contrib.modeladmin.mixins import ThumbnailMixin
from wagtail.contrib.modeladmin.options import ModelAdmin
from wagtail.contrib.modeladmin.views import CreateView, InspectView, ModelFormView, \
    InstanceSpecificView
from wagtail.tests.utils.form_data import rich_text

from crm import gmail_utils
from crm.models import City, CV, MessageTemplate
from crm.models.project import Project

logger = logging.getLogger(__file__)


class ProjectURLHelper(AdminURLHelper):
    def get_action_url_pattern(self, action):
        if action == 'state':
            return r'^{}/{}/{}/(?P<instance_pk>[-\w]+)/(?P<action>[-\w]+)/$'.format(
                self.opts.app_label, self.opts.model_name, action
            )
        pattern = super().get_action_url_pattern(action)
        return pattern


class StateTransitionForm(WagtailAdminModelForm):
    template = ModelChoiceField(queryset=MessageTemplate.objects.all(),
                                widget=InstanceSelectorWidget(model=MessageTemplate))

    text = CharField(widget=get_rich_text_editor_widget(),
                     help_text="Change template text in 'Settings' > 'Message templates'",
                     initial='Write your message here...')
    cv = ModelChoiceField(queryset=CV.objects.all(),
                          label='CV',
                          widget=InstanceSelectorWidget(model=CV),
                          required=False)

    def __init__(self, **kwargs):
        # start here
        # add cv attachment checkbox
        project = kwargs['instance']
        data = kwargs.get('data', {})
        action = kwargs.pop('action')
        kwargs['initial']['template'] = project.get_message_template(action)
        template_pk = data.get('template')
        if template_pk:
            self.message_template = get_object_or_404(MessageTemplate, pk=data.get('template'))
            kwargs['data'] = data.copy()
            kwargs['data']['text'] = kwargs['data'].get('text') or rich_text(
                Template(self.message_template.text).render(Context({'project': project}))
            )
            if self.message_template.attach_cv:
                if 'cv' not in kwargs['data']:
                    kwargs['data']['cv'] = project.cvs.first()
        else:
            self.message_template = None
        super().__init__(**kwargs)

        if template_pk:
            self.fields['template'].widget = HiddenInput()
        else:
            self.fields.pop('text')
            self.fields.pop('cv')
            self.next = True

    class Meta:
        model = Project
        fields = ['template', 'text', 'cv']


class StateTransitionView(ModelFormView, InstanceSpecificView):
    action = None
    form_class = StateTransitionForm
    template_name = 'state.html'

    def __init__(self, **kwargs):
        self.action = kwargs.pop('action')
        self.page_title = f'{self.action.capitalize()} {Project._meta.verbose_name}: write your message'
        super().__init__(**kwargs)

    def get_context_data(self, form=None, **kwargs):
        context = super().get_context_data(form, **kwargs)
        if not self.request.user.social_auth.filter(provider='google-oauth2').exists():
            messages.error(self.request,
                           "Message won't be sent, because no google social auth connection is configured. "
                           "Go to 'AccountSetting'->'More actions' -> 'Google Login'.")
        if self.instance.manager is None:
            messages.error(self.request,
                           "Project doesn't have a manager, messages can't be sent",
                           buttons=[
                               messages.button(text='EDIT', url=reverse('crm_project_modeladmin_edit',
                                                                        kwargs={'instance_pk': self.instance.pk}))
                           ])
        transitions = list(Project._meta.get_field('state').get_all_transitions(Project))
        context['transition'] = next(transition for transition in transitions if transition.name == self.action)
        return context

    def get_form_kwargs(self):
        return {'action': self.action, **super().get_form_kwargs()}

    def get_form_class(self):
        return StateTransitionForm

    def get_success_message(self, instance):
        return "{model_name} '{instance}' now in state {instance.state}".format(
            model_name=self.verbose_name.capitalize(), instance=instance
        )

    def send_mail(self, data):
        from_user = self.request.user
        project_message = self.instance.messages.first()
        to_email = (project_message.reply_to if project_message else None) or self.instance.manager.email
        text = data['text']
        cv = data['cv']

        try:
            gmail_utils.send_email(
                from_user=from_user,
                to_email=to_email,
                rich_text=text,
                cv=cv,
                project_message=project_message
            )
        except GoogleAPIError as ex:
            logger.error(f"Can't send messages: {ex}")
            messages.error(self.request, f"Can't send messages: {ex}")
            return
        except gmail_utils.NoSocialAuth:
            return
        messages.success(self.request,
                         f'Message sent to {to_email}')

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if self.request.POST.get('next'):
            return self.render_to_response(self.get_context_data(form=form))
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form):
        method = getattr(form.instance, self.action)

        try:
            method()
            if self.request.POST.get('change_state') != 'change_state':
                self.send_mail(data=form.cleaned_data)
        except TransitionNotAllowed:
            return self.form_invalid(form)
        return super().form_valid(form)


class ProjectButtonHelper(ButtonHelper):
    def state_buttons(self, obj, pk):
        available_transitions = obj.get_available_state_transitions()
        buttons = []
        small = not isinstance(self.view, InspectView)
        for transition in available_transitions:
            action = transition.method.__name__
            buttons.append(
                {
                    'url': self.url_helper.get_action_url('state', quote(pk), action),
                    'label': action.capitalize(),
                    'classname': self.finalise_classname(
                        ['button-small' if small else 'button'] +
                        transition.custom.get('classes', [])
                    ),
                    'title': transition.custom['help'].capitalize(),
                }
            )
        return buttons

    def get_buttons_for_obj(self, obj, *args, **kwargs):
        btns = super().get_buttons_for_obj(obj, *args, **kwargs)
        usr = self.request.user
        ph = self.permission_helper
        pk = getattr(obj, self.opts.pk.attname)

        if ph.user_can_edit_obj(usr, obj):
            btns += self.state_buttons(obj, pk)
        return btns


class CreateProjectView(CreateView):
    def form_valid(self, form):
        instance = form.save()
        messages.success(
            self.request, self.get_success_message(instance),
            buttons=self.get_success_message_buttons(instance)
        )

        messages.info(
            self.request,
            'Now you can create CV for this project'
        )

        cv_create_url = f"{reverse('crm_cv_modeladmin_create')}?for_project={instance.pk}"
        return redirect(cv_create_url)

    def get_initial(self):
        next_month_first_day = (timezone.now() + timedelta(days=30)).replace(day=1)
        return {
            'start_date': next_month_first_day,
            'end_date': (next_month_first_day + timedelta(days=90)).replace(day=1),
            'location': City.most_popular()
        }


class ProjectAdmin(ThumbnailMixin, ModelAdmin):
    model = Project
    menu_icon = 'fa-product-hunt'
    menu_label = 'Projects'

    list_display = ('admin_thumb', 'name', 'manager', 'location', 'state', 'last_activity')
    list_per_page = 5
    list_select_related = ['manager', 'location']
    list_filter = ['state', 'modified']
    search_fields = ('project_page__title', 'manager__company__name', 'name', 'company__name',
                     'manager__first_name', 'manager__last_name')
    button_helper_class = ProjectButtonHelper
    url_helper_class = ProjectURLHelper
    ordering = ('-modified',)
    inspect_view_enabled = True
    inspect_view_fields = [
        'state', 'company', 'location',
        'original_description', 'original_url', 'notes',
        'start_date', 'end_date', 'duration', 'daily_rate', 'working_days',
        'budget', 'vat', 'invoice_amount', 'income_tax', 'nett_income',
        'project_page', 'logo'
    ]
    inspect_template_name = 'project_inspect.html'
    thumb_image_field_name = 'logo'
    thumb_default = '/static/img/default_project.png'
    list_display_add_buttons = 'name'
    create_view_class = CreateProjectView

    def last_activity(self, instance):
        days = (timezone.now() - instance.modified).days
        return f'{days} day{pluralize(days)} ago'

    def state_view(self, request, instance_pk, action):
        kwargs = {'model_admin': self, 'instance_pk': instance_pk, 'action': action}
        return StateTransitionView.as_view(**kwargs)(request)

    def get_admin_urls_for_registration(self):
        urls = super().get_admin_urls_for_registration()
        route = url(self.url_helper.get_action_url_pattern('state'),
                    self.state_view,
                    name=self.url_helper.get_action_url_name('state'))
        urls = urls + (route,)
        return urls

    def get_extra_attrs_for_field_col(self, obj, field_name):
        if field_name == 'state':
            return {
                'style': f'color: {obj.state_color};text-transform: uppercase;'
            }
        return {}


class ProjectSearchArea(SearchArea):
    def __init__(self):
        super().__init__(
            'Projects', reverse('crm_project_modeladmin_index'),
            name='projects',
            classnames='icon icon-fa-product-hunt',
            order=101)