HarvestHub/GardenHub

View on GitHub
gardenhub/views.py

Summary

Maintainability
C
1 day
Test Coverage
from django.db.models import Q
from django.http import HttpResponseRedirect, JsonResponse
from django.contrib import messages
from django.contrib.auth import get_user_model, login
from django.contrib.auth.views import (
    LogoutView, PasswordResetView, PasswordResetConfirmView
)
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import ValidationError
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView, DeleteView
from django.views.generic.edit import (
    FormView, CreateView, UpdateView, DeletionMixin
)
from django.views.generic.base import TemplateView
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from sorl.thumbnail import get_thumbnail
from .models import Garden, Plot, Pick, Order
from .forms import (
    OrderForm,
    GardenForm,
    PlotForm,
    ActivateAccountForm,
    AccountSettingsForm
)
from .mixins import UserCanEditGardenMixin, UserCanEditPlotMixin


class LogoutView(LogoutView):
    """
    Logs out the user and redirects them to the login screen.
    """
    next_page = reverse_lazy('login')

    def get_next_page(self):
        # Notify the user that their logout was successful
        messages.add_message(
            self.request, messages.SUCCESS,
            "You've been successfully logged out."
        )
        return super().get_next_page()


class PasswordResetView(PasswordResetView):
    success_url = reverse_lazy('login')

    def form_valid(self, form):
        response = super().form_valid(form)
        email = form.cleaned_data['email']
        messages.add_message(
            self.request, messages.SUCCESS,
            "Please check your email {} to reset your password. It may take a "
            "few minutes to arrive.".format(email)
        )
        return response


class PasswordResetConfirmView(PasswordResetConfirmView):
    success_url = reverse_lazy('home')
    post_reset_login = True

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.add_message(
            self.request, messages.SUCCESS,
            "Welcome back! You have successfully changed your password."
        )
        return response


class HomePageView(LoginRequiredMixin, TemplateView):
    """
    Welcome screen with calls to action.
    """
    template_name = 'gardenhub/homepage.html'


class OrderListView(LoginRequiredMixin, ListView):
    """
    Manage orders page to view all upcoming orders.
    """
    context_object_name = 'orders'

    def get_queryset(self):
        return self.request.user.get_orders()


class OrderCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    """
    This is a form used to submit a new order. It's used by gardeners, garden
    managers, or anyone who has the ability to edit a plot.
    """
    model = Order
    form_class = OrderForm

    def get_form(self, *args, **kwargs):
        form = super().get_form(*args, **kwargs)

        # Constrain Plot choices
        # We have to do this here because we can access the Request
        form.fields['plot'].queryset = self.request.user.get_plots()

        # Prevent overlapping order dates
        # http://wiki.c2.com/?TestIfDateRangesOverlap
        if form.is_valid():
            data = form.cleaned_data

            overlapping_orders = Order.objects.filter(
                # On the same plot, and
                Q(plot__id=data['plot'].id) &
                (
                    # Start date occurs before the end date, and
                    Q(start_date__lte=data['end_date']) &
                    # End date occurs after the start date
                    Q(end_date__gte=data['start_date'])
                )
            ).active()  # Only active orders matter in this context

            if overlapping_orders.count() > 0:
                # Overlapping orders have been found!
                form.add_error(None, ValidationError(
                    "Dates overlap with another order on this plot. Please "
                    "cancel the other order(s) or select a different date "
                    "range.",
                    code='overlap'
                ))

        return form

    def get_form_kwargs(self):
        # Force Order values: canceled and requester
        order = Order(canceled=False, requester=self.request.user)
        kwargs = super().get_form_kwargs()
        kwargs.update({'instance': order})
        return kwargs

    def form_valid(self, form):
        response = super().form_valid(form)  # Sets self.object
        order = self.object
        # Notify pickers on this order's garden that there's a new order
        pickers = order.plot.garden.pickers.all()
        for picker in pickers:
            picker.email_user(
                subject="New order on plot {} in {}".format(
                    order.plot.title, order.plot.garden.title),
                message=render_to_string(
                    'gardenhub/email_picker_new_order.txt', {
                        'picker': picker,
                        'order': order
                    }
                )
            )
        # Friendly confirmation message
        messages.add_message(
            self.request, messages.SUCCESS,
            "Your order was successfully submitted!"
        )
        return response

    def test_func(self):
        # Ensure the user has the ability to edit at least 1 plot
        return self.request.user.is_gardener()


class PickCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    """
    Form enabling a picker to submit a Pick for a given plot.
    """
    model = Pick
    fields = ['crops', 'comment']
    success_url = reverse_lazy('home')

    def test_func(self):
        # Reject non-Pickers from this view
        return self.request.user.is_picker()

    def get_form_kwargs(self):
        # Set the pick's plot and requester
        plot = get_object_or_404(Plot, id=self.kwargs['plotId'])
        pick = Pick(plot=plot, picker=self.request.user)
        kwargs = super().get_form_kwargs()
        kwargs.update({'instance': pick})
        return kwargs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['plot'] = get_object_or_404(Plot, id=self.kwargs['plotId'])
        return context

    def form_valid(self, form):
        response = super().form_valid(form)  # Sets self.object
        pick = self.object
        # Notify Pick inquirers
        for inquirer in pick.inquirers():
            inquirer.email_user(
                subject="Plot {} in {} has been picked!".format(
                    pick.plot.title, pick.plot.garden.title),
                message=render_to_string(
                    'gardenhub/email_inquirer_new_pick.txt', {
                        'inquirer': inquirer,
                        'pick': pick
                    }
                )
            )
        # Friendly confirmation message
        messages.add_message(
            self.request, messages.SUCCESS,
            "Pick for {} was submitted!".format(pick.plot)
        )
        return response


class OrderDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
    """
    Review an individual order that's been submitted. Anyone who can edit the
    plot may view or cancel these orders.
    """
    model = Order

    def test_func(self):
        # Can the user manage this order?
        is_manager = self.request.user.can_edit_order(self.get_object())
        # Should the user pick this order?
        is_picker = self.request.user.is_order_picker(self.get_object())
        # Is the user a manager or picker of this order?
        return is_manager or is_picker


class OrderCancelView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Order
    success_url = reverse_lazy('order-list')
    template_name_suffix = '_confirm_cancel'

    def delete(self, request, *args, **kwargs):
        order = self.object = self.get_object()
        success_url = self.get_success_url()

        order.canceled = True
        order.canceled_timestamp = timezone.now()
        order.save()

        messages.add_message(
            self.request, messages.SUCCESS,
            "You've successfully canceled order #{}.".format(order.id)
        )

        return HttpResponseRedirect(success_url)

    def test_func(self):
        order = self.get_object()
        is_manager = self.request.user.can_edit_order(order)
        return is_manager and order.is_open()


class GardenListView(LoginRequiredMixin, ListView):
    """
    A list of all gardens the logged-in user can edit.
    """
    context_object_name = 'gardens'

    def get_queryset(self):
        return self.request.user.get_gardens()


class GardenDetailView(LoginRequiredMixin, UserCanEditGardenMixin, DetailView):
    """
    View a single garden.
    """
    model = Garden


class GardenUpdateView(LoginRequiredMixin, UserCanEditGardenMixin, UpdateView):
    """
    Edit form for an individual garden.
    """
    model = Garden
    form_class = GardenForm

    def form_valid(self, form):
        response = super().form_valid(form)
        # Get user objects from email addresses and invite everyone else
        managers = get_user_model().objects.get_or_invite_users(
            form.cleaned_data['manager_emails'],  # Submitted emails
            self.request  # The email template needs request data
        )
        self.object.managers.set(managers)
        messages.add_message(
            self.request, messages.SUCCESS,
            "{} has been successfully updated!".format(
                self.object.title)
        )
        self.object.save()  # FIXME: Prevent saving the object twice
        return response


class PlotListView(LoginRequiredMixin, ListView):
    """
    A list of all plots the logged-in user can edit.
    """
    context_object_name = 'plots'

    def get_queryset(self):
        return self.request.user.get_plots()


class PlotCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    model = Plot
    form_class = PlotForm
    success_url = reverse_lazy('plot-list')

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.add_message(
            self.request, messages.SUCCESS,
            "{} has been successfully created!".format(self.object)
        )
        return response

    def test_func(self):
        # Only garden managers can create plots
        return self.request.user.is_garden_manager()


class PlotUpdateView(LoginRequiredMixin, UserCanEditPlotMixin, UpdateView):
    """
    Edit form for an individual plot.
    """
    model = Plot
    form_class = PlotForm
    success_url = reverse_lazy('plot-list')

    def get_form(self, *args, **kwargs):
        form = super().get_form(*args, **kwargs)
        garden = self.object.garden
        garden_field = form.fields['garden']
        # Garden options should be all gardens the user can manage
        # plus the current Plot's garden
        garden_queryset = Garden.objects.filter(
            Q(id=garden.id) |
            Q(managers__id=self.request.user.id)
        ).distinct()
        # Constrain Garden choices
        # We have to do this here because we can access the Request
        garden_field.queryset = garden_queryset
        # Only managers of the plot's garden can change its garden and name
        if not self.request.user.can_edit_garden(garden):
            garden_field.disabled = True
            form.fields['title'].disabled = True
        return form

    def form_valid(self, form):
        response = super().form_valid(form)
        # Get user objects from email addresses and invite everyone else
        gardeners = get_user_model().objects.get_or_invite_users(
            form.cleaned_data['gardener_emails'],  # Submitted emails
            self.request  # The email template needs request data
        )
        self.object.gardeners.set(gardeners)
        self.object.save()  # FIXME: Prevent saving the object twice
        messages.add_message(
            self.request, messages.SUCCESS,
            "{} has been successfully updated!".format(self.object)
        )
        return response


def account_activate_view(request, token):
    """
    When a new user is invited, an email call to action will send them to this
    view so they can fill out their profile and activate their account.
    """
    user = get_object_or_404(get_user_model(), activation_token=token)

    # The user is already active, this token isn't needed.
    if user.is_active:
        user.activation_token = None
        user.save()
        return HttpResponseRedirect(reverse_lazy('home'))

    # Form has been submitted
    if request.method == 'POST':
        form = ActivateAccountForm(request.POST)

        if form.is_valid() and form.cleaned_data['password1'] == form.cleaned_data['password2']:  # noqa
            user.first_name = form.cleaned_data['first_name']
            user.last_name = form.cleaned_data['last_name']
            user.phone_number = form.cleaned_data['phone_number']
            user.is_active = True
            user.set_password(form.cleaned_data['password1'])
            user.save()
            login(request, user)
            messages.add_message(
                request, messages.SUCCESS,
                "Welcome to GardenHub! You've successfully activated "
                "your account."
            )
            return HttpResponseRedirect(reverse_lazy('home'))

    else:
        form = ActivateAccountForm()

    return render(request, 'gardenhub/account_activate.html', {
        'form': form
    })


class AccountView(LoginRequiredMixin, TemplateView):
    """
    Profile edit screen for the logged-in user.
    """
    template_name = 'gardenhub/account_detail.html'


class AccountSettingsView(LoginRequiredMixin, FormView):
    """
    Account settings screen for the logged-in user.
    """
    template_name = 'gardenhub/account_settings.html'
    form_class = AccountSettingsForm
    success_url = reverse_lazy('account-settings')

    def form_valid(self, form):
        user = self.request.user
        data = form.cleaned_data

        # Set user's name
        user.first_name = data['first_name']
        user.last_name = data['last_name']

        # Set phone number
        if data['phone_number']:
            user.phone_number = data['phone_number']

        # Set photo
        if data['photo']:
            user.photo = data['photo']

        # Set password if it's entered
        pass1 = data['new_password1']
        pass2 = data['new_password2']
        oldpass = data['password']

        has_passwords = oldpass and pass1 and pass2

        # Only applicable if the user entered data into the password fields
        if has_passwords:
            passwords_match = pass1 == pass2
            valid_oldpass = user.check_password(oldpass)

            if valid_oldpass and passwords_match:
                user.set_password(pass1)
                messages.add_message(
                    self.request, messages.SUCCESS,
                    "Account successfully updated."
                )
            elif not valid_oldpass:
                messages.add_message(
                    self.request, messages.ERROR,
                    "You entered the wrong password."
                )
            else:
                messages.add_message(
                    self.request, messages.ERROR,
                    "The given passwords do not match."
                )
        # If the user did *not* enter passwords...
        else:
            messages.add_message(
                self.request, messages.SUCCESS, "Profile successfully updated."
            )

        # Save user object
        user.save()

        return super().form_valid(form)


class AccountRemoveView(LoginRequiredMixin, DeletionMixin, TemplateView):
    """
    Remove the logged-in user's GardenHub account.
    """
    template_name = 'gardenhub/account_confirm_remove.html'
    success_url = reverse_lazy('logout')

    def delete(self, request, *args, **kwargs):
        user = self.object = request.user
        success_url = self.get_success_url()

        user.is_active = False
        user.save()

        messages.add_message(
            self.request, messages.SUCCESS,
            "You've successfully removed your account."
        )

        return HttpResponseRedirect(success_url)


class ApiCrops(LoginRequiredMixin, UserCanEditPlotMixin, DetailView):
    """
    Return JSON about crops.
    """
    model = Plot

    def get(self, request, *args, **kwargs):
        super().get(self, request, *args, **kwargs)
        try:
            crops = self.object.crops.all()
            return JsonResponse({
                "crops": [{
                    "id": crop.id,
                    "title": crop.title,
                    "image": get_thumbnail(
                        crop.image, '125x125', crop='center').url
                } for crop in crops]})

        except Exception:
            return JsonResponse({})