# pylint: disable=attribute-defined-outside-init
from django import http
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views import generic
from oscar.apps.customer.utils import get_password_reset_url
from oscar.core.compat import get_user_model
from oscar.core.loading import get_class, get_classes, get_model, get_profile_class
from oscar.core.utils import safe_referrer
from oscar.views.generic import PostActionMixin
from . import signals
PageTitleMixin, RegisterUserMixin = get_classes(
"customer.mixins", ["PageTitleMixin", "RegisterUserMixin"]
)
CustomerDispatcher = get_class("customer.utils", "CustomerDispatcher")
EmailAuthenticationForm, EmailUserCreationForm, OrderSearchForm = get_classes(
"customer.forms",
["EmailAuthenticationForm", "EmailUserCreationForm", "OrderSearchForm"],
)
ProfileForm, ConfirmPasswordForm = get_classes(
"customer.forms", ["ProfileForm", "ConfirmPasswordForm"]
)
UserAddressForm = get_class("address.forms", "UserAddressForm")
Order = get_model("order", "Order")
UserAddress = get_model("address", "UserAddress")
Email = get_model("communication", "Email")
User = get_user_model()
# =======
# Account
# =======
[docs]class AccountSummaryView(generic.RedirectView):
"""
View that exists for legacy reasons and customisability. It commonly gets
called when the user clicks on "Account" in the navbar.
Oscar defaults to just redirecting to the profile summary page (and
that redirect can be configured via OSCAR_ACCOUNT_REDIRECT_URL), but
it's also likely you want to display an 'account overview' page or
such like. The presence of this view allows just that, without
having to change a lot of templates.
"""
pattern_name = settings.OSCAR_ACCOUNTS_REDIRECT_URL
permanent = False
[docs]class AccountRegistrationView(RegisterUserMixin, generic.FormView):
form_class = EmailUserCreationForm
template_name = "oscar/customer/registration.html"
redirect_field_name = "next"
[docs] def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect(settings.LOGIN_REDIRECT_URL)
return super().get(request, *args, **kwargs)
def get_logged_in_redirect(self):
return reverse("customer:summary")
[docs] def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data(*args, **kwargs)
ctx["cancel_url"] = safe_referrer(self.request, "")
return ctx
[docs]class AccountAuthView(RegisterUserMixin, generic.TemplateView):
"""
This is actually a slightly odd double form view that allows a customer to
either login or register.
"""
template_name = "oscar/customer/login_registration.html"
login_prefix, registration_prefix = "login", "registration"
login_form_class = EmailAuthenticationForm
registration_form_class = EmailUserCreationForm
redirect_field_name = "next"
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect(settings.LOGIN_REDIRECT_URL)
return super().get(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data(*args, **kwargs)
if "login_form" not in kwargs:
ctx["login_form"] = self.get_login_form()
if "registration_form" not in kwargs:
ctx["registration_form"] = self.get_registration_form()
return ctx
def post(self, request, *args, **kwargs):
# Use the name of the submit button to determine which form to validate
if "login_submit" in request.POST:
return self.validate_login_form()
elif "registration_submit" in request.POST:
return self.validate_registration_form()
return http.HttpResponseBadRequest()
# LOGIN
def get_login_form(self, bind_data=False):
return self.login_form_class(**self.get_login_form_kwargs(bind_data))
def get_login_form_kwargs(self, bind_data=False):
kwargs = {}
kwargs["request"] = self.request
kwargs["host"] = self.request.get_host()
kwargs["prefix"] = self.login_prefix
kwargs["initial"] = {
"redirect_url": self.request.GET.get(self.redirect_field_name, ""),
}
if bind_data and self.request.method in ("POST", "PUT"):
kwargs.update(
{
"data": self.request.POST,
"files": self.request.FILES,
}
)
return kwargs
def validate_login_form(self):
form = self.get_login_form(bind_data=True)
if form.is_valid():
user = form.get_user()
# Grab a reference to the session ID before logging in
old_session_key = self.request.session.session_key
auth_login(self.request, form.get_user())
# Raise signal robustly (we don't want exceptions to crash the
# request handling). We use a custom signal as we want to track the
# session key before calling login (which cycles the session ID).
signals.user_logged_in.send_robust(
sender=self,
request=self.request,
user=user,
old_session_key=old_session_key,
)
msg = self.get_login_success_message(form)
if msg:
messages.success(self.request, msg)
return redirect(self.get_login_success_url(form))
ctx = self.get_context_data(login_form=form)
return self.render_to_response(ctx)
# pylint: disable=unused-argument
def get_login_success_message(self, form):
return _("Welcome back")
def get_login_success_url(self, form):
redirect_url = form.cleaned_data["redirect_url"]
if redirect_url:
return redirect_url
# Redirect staff members to dashboard as that's the most likely place
# they'll want to visit if they're logging in.
if self.request.user.is_staff:
return reverse("dashboard:index")
return settings.LOGIN_REDIRECT_URL
# REGISTRATION
def get_registration_form(self, bind_data=False):
return self.registration_form_class(
**self.get_registration_form_kwargs(bind_data)
)
def get_registration_form_kwargs(self, bind_data=False):
kwargs = {}
kwargs["host"] = self.request.get_host()
kwargs["prefix"] = self.registration_prefix
kwargs["initial"] = {
"redirect_url": self.request.GET.get(self.redirect_field_name, ""),
}
if bind_data and self.request.method in ("POST", "PUT"):
kwargs.update(
{
"data": self.request.POST,
"files": self.request.FILES,
}
)
return kwargs
def validate_registration_form(self):
form = self.get_registration_form(bind_data=True)
if form.is_valid():
self.register_user(form)
msg = self.get_registration_success_message(form)
messages.success(self.request, msg)
return redirect(self.get_registration_success_url(form))
ctx = self.get_context_data(registration_form=form)
return self.render_to_response(ctx)
# pylint: disable=unused-argument
def get_registration_success_message(self, form):
return _("Thanks for registering!")
def get_registration_success_url(self, form):
redirect_url = form.cleaned_data["redirect_url"]
if redirect_url:
return redirect_url
return settings.LOGIN_REDIRECT_URL
[docs]class LogoutView(generic.RedirectView):
url = settings.OSCAR_HOMEPAGE
permanent = False
def get(self, request, *args, **kwargs):
auth_logout(request)
response = super().get(request, *args, **kwargs)
for cookie in settings.OSCAR_COOKIES_DELETE_ON_LOGOUT:
response.delete_cookie(cookie)
return response
# =============
# Profile
# =============
[docs]class ProfileView(PageTitleMixin, generic.TemplateView):
template_name = "oscar/customer/profile/profile.html"
page_title = _("Profile")
active_tab = "profile"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["profile_fields"] = self.get_profile_fields(self.request.user)
return ctx
def get_profile_fields(self, user):
field_data = []
# Check for custom user model
for field_name in User._meta.additional_fields:
field_data.append(self.get_model_field_data(user, field_name))
# Check for profile class
profile_class = get_profile_class()
if profile_class:
try:
profile = profile_class.objects.get(user=user)
except ObjectDoesNotExist:
profile = profile_class(user=user)
field_names = [f.name for f in profile._meta.local_fields]
for field_name in field_names:
if field_name in ("user", "id"):
continue
field_data.append(self.get_model_field_data(profile, field_name))
return field_data
[docs] def get_model_field_data(self, model_class, field_name):
"""
Extract the verbose name and value for a model's field value
"""
field = model_class._meta.get_field(field_name)
if field.choices:
value = getattr(model_class, "get_%s_display" % field_name)()
else:
value = getattr(model_class, field_name)
return {
"name": getattr(field, "verbose_name"),
"value": value,
}
[docs]class ProfileUpdateView(PageTitleMixin, generic.FormView):
form_class = ProfileForm
template_name = "oscar/customer/profile/profile_form.html"
page_title = _("Edit Profile")
active_tab = "profile"
success_url = reverse_lazy("customer:profile-view")
def send_email_changed_email(self, old_user, new_email):
user = self.request.user
extra_context = {
"user": user,
"reset_url": get_password_reset_url(user),
"new_email": new_email,
"request": self.request,
}
CustomerDispatcher().send_email_changed_email_for_user(old_user, extra_context)
[docs]class ProfileDeleteView(PageTitleMixin, generic.FormView):
form_class = ConfirmPasswordForm
template_name = "oscar/customer/profile/profile_delete.html"
page_title = _("Delete profile")
active_tab = "profile"
success_url = settings.OSCAR_HOMEPAGE
[docs]class ChangePasswordView(PageTitleMixin, generic.FormView):
form_class = PasswordChangeForm
template_name = "oscar/customer/profile/change_password_form.html"
page_title = _("Change Password")
active_tab = "profile"
success_url = reverse_lazy("customer:profile-view")
def send_password_changed_email(self):
user = self.request.user
extra_context = {
"user": user,
"reset_url": get_password_reset_url(self.request.user),
"request": self.request,
}
CustomerDispatcher().send_password_changed_email_for_user(user, extra_context)
# =============
# Email history
# =============
[docs]class EmailHistoryView(PageTitleMixin, generic.ListView):
context_object_name = "emails"
template_name = "oscar/communication/email/email_list.html"
paginate_by = settings.OSCAR_EMAILS_PER_PAGE
page_title = _("Email History")
active_tab = "emails"
[docs] def get_queryset(self):
"""
Return Queryset of :py:class:`Email <oscar.apps.customer.abstract_models.AbstractEmail>`
instances, that has been sent to the currently authenticated user.
"""
return Email._default_manager.filter(user=self.request.user)
[docs]class EmailDetailView(PageTitleMixin, generic.DetailView):
"""Customer email"""
template_name = "oscar/communication/email/email_detail.html"
context_object_name = "email"
active_tab = "emails"
[docs] def get_object(self, queryset=None):
return get_object_or_404(
Email, user=self.request.user, id=self.kwargs["email_id"]
)
[docs] def get_page_title(self):
"""Append email subject to page title"""
return "%s: %s" % (_("Email"), self.object.subject)
# =============
# Order history
# =============
[docs]class OrderHistoryView(PageTitleMixin, generic.ListView):
"""
Customer order history
"""
context_object_name = "orders"
template_name = "oscar/customer/order/order_list.html"
paginate_by = settings.OSCAR_ORDERS_PER_PAGE
model = Order
form_class = OrderSearchForm
page_title = _("Order History")
active_tab = "orders"
def get(self, request, *args, **kwargs):
if "date_from" in request.GET:
self.form = self.form_class(self.request.GET)
if not self.form.is_valid():
self.object_list = self.get_queryset()
ctx = self.get_context_data(object_list=self.object_list)
return self.render_to_response(ctx)
data = self.form.cleaned_data
# If the user has just entered an order number, try and look it up
# and redirect immediately to the order detail page.
if data["order_number"] and not (data["date_to"] or data["date_from"]):
try:
order = Order.objects.get(
number=data["order_number"], user=self.request.user
)
except Order.DoesNotExist:
pass
else:
return redirect("customer:order", order_number=order.number)
else:
self.form = self.form_class()
return super().get(request, *args, **kwargs)
[docs] def get_queryset(self):
"""
Return Queryset of :py:class:`Order <oscar.apps.order.abstract_models.AbstractOrder>`
instances for the currently authenticated user.
"""
qs = self.model._default_manager.filter(user=self.request.user)
if self.form.is_bound and self.form.is_valid():
qs = qs.filter(**self.form.get_filters())
return qs
[docs] def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data(*args, **kwargs)
ctx["form"] = self.form
return ctx
[docs]class OrderDetailView(PageTitleMixin, PostActionMixin, generic.DetailView):
model = Order
active_tab = "orders"
[docs] def get_template_names(self):
return ["oscar/customer/order/order_detail.html"]
[docs] def get_page_title(self):
"""
Order number as page title
"""
return "%s #%s" % (_("Order"), self.object.number)
[docs] def get_object(self, queryset=None):
return get_object_or_404(
self.model, user=self.request.user, number=self.kwargs["order_number"]
)
[docs] def do_reorder(self, order):
"""
'Re-order' a previous order.
This puts the contents of the previous order into your basket
"""
# Collect lines to be added to the basket and any warnings for lines
# that are no longer available.
basket = self.request.basket
lines_to_add = []
warnings = []
for line in order.lines.all():
is_available, reason = line.is_available_to_reorder(
basket, self.request.strategy
)
if is_available:
lines_to_add.append(line)
else:
warnings.append(reason)
# Check whether the number of items in the basket won't exceed the
# maximum.
total_quantity = sum([line.quantity for line in lines_to_add])
is_quantity_allowed, reason = basket.is_quantity_allowed(total_quantity)
if not is_quantity_allowed:
messages.warning(self.request, reason)
self.response = redirect("customer:order-list")
return
# Add any warnings
for warning in warnings:
messages.warning(self.request, warning)
for line in lines_to_add:
options = []
for attribute in line.attributes.all():
if attribute.option:
options.append(
{"option": attribute.option, "value": attribute.value}
)
basket.add_product(line.product, line.quantity, options)
if len(lines_to_add) > 0:
self.response = redirect("basket:summary")
messages.info(
self.request,
_(
"All available lines from order %(number)s "
"have been added to your basket"
)
% {"number": order.number},
)
else:
self.response = redirect("customer:order-list")
messages.warning(
self.request,
_(
"It is not possible to re-order order %(number)s "
"as none of its lines are available to purchase"
)
% {"number": order.number},
)
[docs]class OrderLineView(PostActionMixin, generic.DetailView):
"""Customer order line"""
[docs] def get_object(self, queryset=None):
order = get_object_or_404(
Order, user=self.request.user, number=self.kwargs["order_number"]
)
return order.lines.get(id=self.kwargs["line_id"])
def do_reorder(self, line):
self.response = redirect("customer:order", self.kwargs["order_number"])
basket = self.request.basket
line_available_to_reorder, reason = line.is_available_to_reorder(
basket, self.request.strategy
)
if not line_available_to_reorder:
messages.warning(self.request, reason)
return
# We need to pass response to the get_or_create... method
# as a new basket might need to be created
self.response = redirect("basket:summary")
# Convert line attributes into basket options
options = []
for attribute in line.attributes.all():
if attribute.option:
options.append({"option": attribute.option, "value": attribute.value})
basket.add_product(line.product, line.quantity, options)
if line.quantity > 1:
msg = _(
"%(qty)d copies of '%(product)s' have been added to your basket"
) % {"qty": line.quantity, "product": line.product}
else:
msg = _("'%s' has been added to your basket") % line.product
messages.info(self.request, msg)
[docs]class AnonymousOrderDetailView(generic.DetailView):
model = Order
template_name = "oscar/customer/anon_order.html"
[docs] def get_object(self, queryset=None):
# Check URL hash matches that for order to prevent spoof attacks
order = get_object_or_404(
self.model, user=None, number=self.kwargs["order_number"]
)
if not order.check_verification_hash(self.kwargs["hash"]):
raise http.Http404()
return order
# ------------
# Address book
# ------------
[docs]class AddressListView(PageTitleMixin, generic.ListView):
"""Customer address book"""
context_object_name = "addresses"
template_name = "oscar/customer/address/address_list.html"
paginate_by = settings.OSCAR_ADDRESSES_PER_PAGE
active_tab = "addresses"
page_title = _("Address Book")
[docs] def get_queryset(self):
"""Return customer's addresses"""
return UserAddress._default_manager.filter(user=self.request.user)
[docs]class AddressCreateView(PageTitleMixin, generic.CreateView):
form_class = UserAddressForm
model = UserAddress
template_name = "oscar/customer/address/address_form.html"
active_tab = "addresses"
page_title = _("Add a new address")
success_url = reverse_lazy("customer:address-list")
[docs] def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["title"] = _("Add a new address")
return ctx
[docs] def get_success_url(self):
messages.success(self.request, _("Address '%s' created") % self.object.summary)
return super().get_success_url()
[docs]class AddressUpdateView(PageTitleMixin, generic.UpdateView):
form_class = UserAddressForm
model = UserAddress
template_name = "oscar/customer/address/address_form.html"
active_tab = "addresses"
page_title = _("Edit address")
success_url = reverse_lazy("customer:address-list")
[docs] def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["title"] = _("Edit address")
return ctx
[docs] def get_queryset(self):
return self.request.user.addresses.all()
[docs] def get_success_url(self):
messages.success(self.request, _("Address '%s' updated") % self.object.summary)
return super().get_success_url()
[docs]class AddressDeleteView(PageTitleMixin, generic.DeleteView):
model = UserAddress
template_name = "oscar/customer/address/address_delete.html"
page_title = _("Delete address?")
active_tab = "addresses"
context_object_name = "address"
success_url = reverse_lazy("customer:address-list")
[docs] def get_queryset(self):
return UserAddress._default_manager.filter(user=self.request.user)
[docs] def get_success_url(self):
messages.success(self.request, _("Address '%s' deleted") % self.object.summary)
return super().get_success_url()
[docs]class AddressChangeStatusView(generic.RedirectView):
"""
Sets an address as default_for_(billing|shipping)
"""
url = reverse_lazy("customer:address-list")
permanent = False
def get(self, request, *args, pk=None, action=None, **kwargs):
address = get_object_or_404(UserAddress, user=self.request.user, pk=pk)
# We don't want the user to set an address as the default shipping
# address, though they should be able to set it as their billing
# address.
if address.country.is_shipping_country:
setattr(address, "is_%s" % action, True)
elif action == "default_for_billing":
setattr(address, "is_default_for_billing", True)
else:
messages.error(request, _("We do not ship to this country"))
address.save()
return super().get(request, *args, **kwargs)