You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

views.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. from django.shortcuts import get_object_or_404
  2. from django.views.generic import (TemplateView, ListView, DetailView,
  3. CreateView, UpdateView, DeleteView,
  4. FormView, RedirectView)
  5. from django.core.urlresolvers import reverse, reverse_lazy
  6. from django.core.exceptions import ObjectDoesNotExist
  7. from django.http import HttpResponseRedirect, Http404
  8. from django.contrib import messages
  9. from django.utils.translation import ugettext_lazy as _
  10. from django.contrib.auth import logout as auth_logout, login as auth_login
  11. from django.contrib.auth.forms import PasswordChangeForm
  12. from django.contrib.sites.models import get_current_site
  13. from django.conf import settings
  14. from oscar.core.loading import get_model
  15. from oscar.views.generic import PostActionMixin
  16. from oscar.apps.customer.utils import get_password_reset_url
  17. from oscar.core.loading import get_class, get_profile_class, get_classes
  18. from oscar.core.compat import get_user_model
  19. PageTitleMixin, RegisterUserMixin = get_classes(
  20. 'customer.mixins', ['PageTitleMixin', 'RegisterUserMixin'])
  21. Dispatcher = get_class('customer.utils', 'Dispatcher')
  22. EmailAuthenticationForm, EmailUserCreationForm, OrderSearchForm = get_classes(
  23. 'customer.forms', ['EmailAuthenticationForm', 'EmailUserCreationForm',
  24. 'OrderSearchForm'])
  25. ProfileForm, ConfirmPasswordForm = get_classes(
  26. 'customer.forms', ['ProfileForm', 'ConfirmPasswordForm'])
  27. UserAddressForm = get_class('address.forms', 'UserAddressForm')
  28. user_registered = get_class('customer.signals', 'user_registered')
  29. Order = get_model('order', 'Order')
  30. Line = get_model('basket', 'Line')
  31. Basket = get_model('basket', 'Basket')
  32. UserAddress = get_model('address', 'UserAddress')
  33. Email = get_model('customer', 'Email')
  34. UserAddress = get_model('address', 'UserAddress')
  35. ProductAlert = get_model('customer', 'ProductAlert')
  36. CommunicationEventType = get_model('customer', 'CommunicationEventType')
  37. User = get_user_model()
  38. # =======
  39. # Account
  40. # =======
  41. class AccountSummaryView(RedirectView):
  42. """
  43. View that exists for legacy reasons and customisability. It commonly gets
  44. called when the user clicks on "Account" in the navbar, and can be
  45. overriden to determine to what sub-page the user is directed without
  46. having to change a lot of templates.
  47. """
  48. url = reverse_lazy(settings.OSCAR_ACCOUNTS_REDIRECT_URL)
  49. class AccountRegistrationView(RegisterUserMixin, FormView):
  50. form_class = EmailUserCreationForm
  51. template_name = 'customer/registration.html'
  52. redirect_field_name = 'next'
  53. def get(self, request, *args, **kwargs):
  54. if request.user.is_authenticated():
  55. return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
  56. return super(AccountRegistrationView, self).get(
  57. request, *args, **kwargs)
  58. def get_logged_in_redirect(self):
  59. return reverse('customer:summary')
  60. def get_form_kwargs(self):
  61. kwargs = super(AccountRegistrationView, self).get_form_kwargs()
  62. kwargs['initial'] = {
  63. 'email': self.request.GET.get('email', ''),
  64. 'redirect_url': self.request.GET.get(self.redirect_field_name, '')
  65. }
  66. kwargs['host'] = self.request.get_host()
  67. return kwargs
  68. def get_context_data(self, *args, **kwargs):
  69. ctx = super(AccountRegistrationView, self).get_context_data(
  70. *args, **kwargs)
  71. ctx['cancel_url'] = self.request.META.get('HTTP_REFERER', None)
  72. return ctx
  73. def form_valid(self, form):
  74. self.register_user(form)
  75. return HttpResponseRedirect(
  76. form.cleaned_data['redirect_url'])
  77. class AccountAuthView(RegisterUserMixin, TemplateView):
  78. """
  79. This is actually a slightly odd double form view
  80. """
  81. template_name = 'customer/login_registration.html'
  82. login_prefix, registration_prefix = 'login', 'registration'
  83. login_form_class = EmailAuthenticationForm
  84. registration_form_class = EmailUserCreationForm
  85. redirect_field_name = 'next'
  86. def get(self, request, *args, **kwargs):
  87. if request.user.is_authenticated():
  88. return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
  89. return super(AccountAuthView, self).get(
  90. request, *args, **kwargs)
  91. def get_context_data(self, *args, **kwargs):
  92. ctx = super(AccountAuthView, self).get_context_data(*args, **kwargs)
  93. ctx.update(kwargs)
  94. # Don't pass request as we don't want to trigger validation of BOTH
  95. # forms.
  96. if 'login_form' not in kwargs:
  97. ctx['login_form'] = self.get_login_form()
  98. if 'registration_form' not in kwargs:
  99. ctx['registration_form'] = self.get_registration_form()
  100. return ctx
  101. def get_login_form(self, request=None):
  102. return self.login_form_class(**self.get_login_form_kwargs(request))
  103. def get_login_form_kwargs(self, request=None):
  104. kwargs = {}
  105. kwargs['host'] = self.request.get_host()
  106. kwargs['prefix'] = self.login_prefix
  107. kwargs['initial'] = {
  108. 'redirect_url': self.request.GET.get(self.redirect_field_name, ''),
  109. }
  110. if request and request.method in ('POST', 'PUT'):
  111. kwargs.update({
  112. 'data': request.POST,
  113. 'files': request.FILES,
  114. })
  115. return kwargs
  116. def get_registration_form(self, request=None):
  117. return self.registration_form_class(
  118. **self.get_registration_form_kwargs(request))
  119. def get_registration_form_kwargs(self, request=None):
  120. kwargs = {}
  121. kwargs['host'] = self.request.get_host()
  122. kwargs['prefix'] = self.registration_prefix
  123. kwargs['initial'] = {
  124. 'redirect_url': self.request.GET.get(self.redirect_field_name, ''),
  125. }
  126. if request and request.method in ('POST', 'PUT'):
  127. kwargs.update({
  128. 'data': request.POST,
  129. 'files': request.FILES,
  130. })
  131. return kwargs
  132. def post(self, request, *args, **kwargs):
  133. # Use the name of the submit button to determine which form to validate
  134. if u'login_submit' in request.POST:
  135. return self.validate_login_form()
  136. elif u'registration_submit' in request.POST:
  137. return self.validate_registration_form()
  138. return self.get(request)
  139. def validate_login_form(self):
  140. form = self.get_login_form(self.request)
  141. if form.is_valid():
  142. auth_login(self.request, form.get_user())
  143. return HttpResponseRedirect(form.cleaned_data['redirect_url'])
  144. ctx = self.get_context_data(login_form=form)
  145. return self.render_to_response(ctx)
  146. def validate_registration_form(self):
  147. form = self.get_registration_form(self.request)
  148. if form.is_valid():
  149. self.register_user(form)
  150. return HttpResponseRedirect(form.cleaned_data['redirect_url'])
  151. ctx = self.get_context_data(registration_form=form)
  152. return self.render_to_response(ctx)
  153. class LogoutView(RedirectView):
  154. url = settings.OSCAR_HOMEPAGE
  155. permanent = False
  156. def get(self, request, *args, **kwargs):
  157. auth_logout(request)
  158. response = super(LogoutView, self).get(request, *args, **kwargs)
  159. for cookie in settings.OSCAR_COOKIES_DELETE_ON_LOGOUT:
  160. response.delete_cookie(cookie)
  161. return response
  162. # =============
  163. # Profile
  164. # =============
  165. class ProfileView(PageTitleMixin, TemplateView):
  166. template_name = 'customer/profile/profile.html'
  167. page_title = _('Profile')
  168. active_tab = 'profile'
  169. def get_context_data(self, **kwargs):
  170. ctx = super(ProfileView, self).get_context_data(**kwargs)
  171. ctx['profile_fields'] = self.get_profile_fields(self.request.user)
  172. return ctx
  173. def get_profile_fields(self, user):
  174. field_data = []
  175. # Check for custom user model
  176. for field_name in User._meta.additional_fields:
  177. field_data.append(
  178. self.get_model_field_data(user, field_name))
  179. # Check for profile class
  180. profile_class = get_profile_class()
  181. if profile_class:
  182. try:
  183. profile = profile_class.objects.get(user=user)
  184. except ObjectDoesNotExist:
  185. profile = profile_class(user=user)
  186. field_names = [f.name for f in profile._meta.local_fields]
  187. for field_name in field_names:
  188. if field_name in ('user', 'id'):
  189. continue
  190. field_data.append(
  191. self.get_model_field_data(profile, field_name))
  192. return field_data
  193. def get_model_field_data(self, model_class, field_name):
  194. """
  195. Extract the verbose name and value for a model's field value
  196. """
  197. field = model_class._meta.get_field(field_name)
  198. if field.choices:
  199. value = getattr(model_class, 'get_%s_display' % field_name)()
  200. else:
  201. value = getattr(model_class, field_name)
  202. return {
  203. 'name': getattr(field, 'verbose_name'),
  204. 'value': value,
  205. }
  206. class ProfileUpdateView(PageTitleMixin, FormView):
  207. form_class = ProfileForm
  208. template_name = 'customer/profile/profile_form.html'
  209. communication_type_code = 'EMAIL_CHANGED'
  210. page_title = _('Edit Profile')
  211. active_tab = 'profile'
  212. def get_form_kwargs(self):
  213. kwargs = super(ProfileUpdateView, self).get_form_kwargs()
  214. kwargs['user'] = self.request.user
  215. return kwargs
  216. def form_valid(self, form):
  217. # Grab current user instance before we save form. We may need this to
  218. # send a warning email if the email address is changed.
  219. try:
  220. old_user = User.objects.get(id=self.request.user.id)
  221. except User.DoesNotExist:
  222. old_user = None
  223. form.save()
  224. # We have to look up the email address from the form's
  225. # cleaned data because the object created by form.save() can
  226. # either be a user or profile depending on AUTH_PROFILE_MODULE
  227. new_email = form.cleaned_data['email']
  228. if old_user and new_email != old_user.email:
  229. # Email address has changed - send a confirmation email to the old
  230. # address including a password reset link in case this is a
  231. # suspicious change.
  232. ctx = {
  233. 'user': self.request.user,
  234. 'site': get_current_site(self.request),
  235. 'reset_url': get_password_reset_url(old_user),
  236. 'new_email': new_email,
  237. }
  238. msgs = CommunicationEventType.objects.get_and_render(
  239. code=self.communication_type_code, context=ctx)
  240. Dispatcher().dispatch_user_messages(old_user, msgs)
  241. messages.success(self.request, _("Profile updated"))
  242. return HttpResponseRedirect(self.get_success_url())
  243. def get_success_url(self):
  244. return reverse('customer:profile-view')
  245. class ProfileDeleteView(PageTitleMixin, FormView):
  246. form_class = ConfirmPasswordForm
  247. template_name = 'customer/profile/profile_delete.html'
  248. page_title = _('Delete profile')
  249. active_tab = 'profile'
  250. success_url = settings.OSCAR_HOMEPAGE
  251. def get_form_kwargs(self):
  252. kwargs = super(ProfileDeleteView, self).get_form_kwargs()
  253. kwargs['user'] = self.request.user
  254. return kwargs
  255. def form_valid(self, form):
  256. self.request.user.delete()
  257. messages.success(
  258. self.request,
  259. _("Your profile has now been deleted. Thanks for using the site."))
  260. return HttpResponseRedirect(self.get_success_url())
  261. class ChangePasswordView(PageTitleMixin, FormView):
  262. form_class = PasswordChangeForm
  263. template_name = 'customer/profile/change_password_form.html'
  264. communication_type_code = 'PASSWORD_CHANGED'
  265. page_title = _('Change Password')
  266. active_tab = 'profile'
  267. def get_form_kwargs(self):
  268. kwargs = super(ChangePasswordView, self).get_form_kwargs()
  269. kwargs['user'] = self.request.user
  270. return kwargs
  271. def form_valid(self, form):
  272. form.save()
  273. messages.success(self.request, _("Password updated"))
  274. ctx = {
  275. 'user': self.request.user,
  276. 'site': get_current_site(self.request),
  277. 'reset_url': get_password_reset_url(self.request.user),
  278. }
  279. msgs = CommunicationEventType.objects.get_and_render(
  280. code=self.communication_type_code, context=ctx)
  281. Dispatcher().dispatch_user_messages(self.request.user, msgs)
  282. return HttpResponseRedirect(self.get_success_url())
  283. def get_success_url(self):
  284. return reverse('customer:profile-view')
  285. # =============
  286. # Email history
  287. # =============
  288. class EmailHistoryView(PageTitleMixin, ListView):
  289. context_object_name = "emails"
  290. template_name = 'customer/email/email_list.html'
  291. paginate_by = 20
  292. page_title = _('Email History')
  293. active_tab = 'emails'
  294. def get_queryset(self):
  295. return Email._default_manager.filter(user=self.request.user)
  296. class EmailDetailView(PageTitleMixin, DetailView):
  297. """Customer email"""
  298. template_name = "customer/email/email_detail.html"
  299. context_object_name = 'email'
  300. active_tab = 'emails'
  301. def get_object(self, queryset=None):
  302. """Return an order object or 404"""
  303. return get_object_or_404(Email, user=self.request.user,
  304. id=self.kwargs['email_id'])
  305. def get_page_title(self):
  306. """Append email subject to page title"""
  307. return u'%s: %s' % (_('Email'), self.object.subject)
  308. # =============
  309. # Order history
  310. # =============
  311. class OrderHistoryView(PageTitleMixin, ListView):
  312. """
  313. Customer order history
  314. """
  315. context_object_name = "orders"
  316. template_name = 'customer/order/order_list.html'
  317. paginate_by = 20
  318. model = Order
  319. form_class = OrderSearchForm
  320. page_title = _('Order History')
  321. active_tab = 'orders'
  322. def get(self, request, *args, **kwargs):
  323. if 'date_from' in request.GET:
  324. self.form = self.form_class(self.request.GET)
  325. if not self.form.is_valid():
  326. self.object_list = self.get_queryset()
  327. ctx = self.get_context_data(object_list=self.object_list)
  328. return self.render_to_response(ctx)
  329. data = self.form.cleaned_data
  330. # If the user has just entered an order number, try and look it up
  331. # and redirect immediately to the order detail page.
  332. if data['order_number'] and not (data['date_to'] or
  333. data['date_from']):
  334. try:
  335. order = Order.objects.get(
  336. number=data['order_number'], user=self.request.user)
  337. except Order.DoesNotExist:
  338. pass
  339. else:
  340. return HttpResponseRedirect(
  341. reverse('customer:order',
  342. kwargs={'order_number': order.number}))
  343. else:
  344. self.form = self.form_class()
  345. return super(OrderHistoryView, self).get(request, *args, **kwargs)
  346. def get_queryset(self):
  347. qs = self.model._default_manager.filter(user=self.request.user)
  348. if self.form.is_bound and self.form.is_valid():
  349. qs = qs.filter(**self.form.get_filters())
  350. return qs
  351. def get_context_data(self, *args, **kwargs):
  352. ctx = super(OrderHistoryView, self).get_context_data(*args, **kwargs)
  353. ctx['form'] = self.form
  354. return ctx
  355. class OrderDetailView(PageTitleMixin, PostActionMixin, DetailView):
  356. model = Order
  357. active_tab = 'orders'
  358. def get_template_names(self):
  359. return ["customer/order/order_detail.html"]
  360. def get_page_title(self):
  361. """
  362. Order number as page title
  363. """
  364. return u'%s #%s' % (_('Order'), self.object.number)
  365. def get_object(self, queryset=None):
  366. return get_object_or_404(self.model, user=self.request.user,
  367. number=self.kwargs['order_number'])
  368. def do_reorder(self, order): # noqa (too complex (10))
  369. """
  370. 'Re-order' a previous order.
  371. This puts the contents of the previous order into your basket
  372. """
  373. # Collect lines to be added to the basket and any warnings for lines
  374. # that are no longer available.
  375. basket = self.request.basket
  376. lines_to_add = []
  377. warnings = []
  378. for line in order.lines.all():
  379. is_available, reason = line.is_available_to_reorder(
  380. basket, self.request.strategy)
  381. if is_available:
  382. lines_to_add.append(line)
  383. else:
  384. warnings.append(reason)
  385. # Check whether the number of items in the basket won't exceed the
  386. # maximum.
  387. total_quantity = sum([line.quantity for line in lines_to_add])
  388. is_quantity_allowed, reason = basket.is_quantity_allowed(
  389. total_quantity)
  390. if not is_quantity_allowed:
  391. messages.warning(self.request, reason)
  392. self.response = HttpResponseRedirect(
  393. reverse('customer:order-list'))
  394. return
  395. # Add any warnings
  396. for warning in warnings:
  397. messages.warning(self.request, warning)
  398. for line in lines_to_add:
  399. options = []
  400. for attribute in line.attributes.all():
  401. if attribute.option:
  402. options.append({
  403. 'option': attribute.option,
  404. 'value': attribute.value})
  405. basket.add_product(line.product, line.quantity, options)
  406. if len(lines_to_add) > 0:
  407. self.response = HttpResponseRedirect(reverse('basket:summary'))
  408. messages.info(
  409. self.request,
  410. _("All available lines from order %(number)s "
  411. "have been added to your basket") % {'number': order.number})
  412. else:
  413. self.response = HttpResponseRedirect(
  414. reverse('customer:order-list'))
  415. messages.warning(
  416. self.request,
  417. _("It is not possible to re-order order %(number)s "
  418. "as none of its lines are available to purchase") %
  419. {'number': order.number})
  420. class OrderLineView(PostActionMixin, DetailView):
  421. """Customer order line"""
  422. def get_object(self, queryset=None):
  423. """Return an order object or 404"""
  424. order = get_object_or_404(Order, user=self.request.user,
  425. number=self.kwargs['order_number'])
  426. return order.lines.get(id=self.kwargs['line_id'])
  427. def do_reorder(self, line):
  428. self.response = HttpResponseRedirect(
  429. reverse('customer:order',
  430. args=(int(self.kwargs['order_number']),)))
  431. basket = self.request.basket
  432. line_available_to_reorder, reason = line.is_available_to_reorder(
  433. basket, self.request.strategy)
  434. if not line_available_to_reorder:
  435. messages.warning(self.request, reason)
  436. return
  437. # We need to pass response to the get_or_create... method
  438. # as a new basket might need to be created
  439. self.response = HttpResponseRedirect(reverse('basket:summary'))
  440. # Convert line attributes into basket options
  441. options = []
  442. for attribute in line.attributes.all():
  443. if attribute.option:
  444. options.append({'option': attribute.option,
  445. 'value': attribute.value})
  446. basket.add_product(line.product, line.quantity, options)
  447. if line.quantity > 1:
  448. msg = _("%(qty)d copies of '%(product)s' have been added to your"
  449. " basket") % {
  450. 'qty': line.quantity, 'product': line.product}
  451. else:
  452. msg = _("'%s' has been added to your basket") % line.product
  453. messages.info(self.request, msg)
  454. class AnonymousOrderDetailView(DetailView):
  455. model = Order
  456. template_name = "customer/anon_order.html"
  457. def get_object(self, queryset=None):
  458. # Check URL hash matches that for order to prevent spoof attacks
  459. order = get_object_or_404(self.model, user=None,
  460. number=self.kwargs['order_number'])
  461. if self.kwargs['hash'] != order.verification_hash():
  462. raise Http404()
  463. return order
  464. # ------------
  465. # Address book
  466. # ------------
  467. class AddressListView(PageTitleMixin, ListView):
  468. """Customer address book"""
  469. context_object_name = "addresses"
  470. template_name = 'customer/address/address_list.html'
  471. paginate_by = 40
  472. active_tab = 'addresses'
  473. page_title = _('Address Book')
  474. def get_queryset(self):
  475. """Return customer's addresses"""
  476. return UserAddress._default_manager.filter(user=self.request.user)
  477. class AddressCreateView(PageTitleMixin, CreateView):
  478. form_class = UserAddressForm
  479. model = UserAddress
  480. template_name = 'customer/address/address_form.html'
  481. active_tab = 'addresses'
  482. page_title = _('Add a new address')
  483. def get_form_kwargs(self):
  484. kwargs = super(AddressCreateView, self).get_form_kwargs()
  485. kwargs['user'] = self.request.user
  486. return kwargs
  487. def get_context_data(self, **kwargs):
  488. ctx = super(AddressCreateView, self).get_context_data(**kwargs)
  489. ctx['title'] = _('Add a new address')
  490. return ctx
  491. def get_success_url(self):
  492. messages.success(self.request,
  493. _("Address '%s' created") % self.object.summary)
  494. return reverse('customer:address-list')
  495. class AddressUpdateView(PageTitleMixin, UpdateView):
  496. form_class = UserAddressForm
  497. model = UserAddress
  498. template_name = 'customer/address/address_form.html'
  499. active_tab = 'addresses'
  500. page_title = _('Edit address')
  501. def get_form_kwargs(self):
  502. kwargs = super(AddressUpdateView, self).get_form_kwargs()
  503. kwargs['user'] = self.request.user
  504. return kwargs
  505. def get_context_data(self, **kwargs):
  506. ctx = super(AddressUpdateView, self).get_context_data(**kwargs)
  507. ctx['title'] = _('Edit address')
  508. return ctx
  509. def get_queryset(self):
  510. return self.request.user.addresses.all()
  511. def get_success_url(self):
  512. messages.success(self.request,
  513. _("Address '%s' updated") % self.object.summary)
  514. return reverse('customer:address-list')
  515. class AddressDeleteView(PageTitleMixin, DeleteView):
  516. model = UserAddress
  517. template_name = "customer/address/address_delete.html"
  518. page_title = _('Delete address?')
  519. active_tab = 'addresses'
  520. context_object_name = 'address'
  521. def get_queryset(self):
  522. return UserAddress._default_manager.filter(user=self.request.user)
  523. def get_success_url(self):
  524. messages.success(self.request,
  525. _("Address '%s' deleted") % self.object.summary)
  526. return reverse('customer:address-list')
  527. class AddressChangeStatusView(RedirectView):
  528. """
  529. Sets an address as default_for_(billing|shipping)
  530. """
  531. url = reverse_lazy('customer:address-list')
  532. def get(self, request, pk=None, action=None, *args, **kwargs):
  533. address = get_object_or_404(UserAddress, user=self.request.user,
  534. pk=pk)
  535. # We don't want the user to set an address as the default shipping
  536. # address, though they should be able to set it as their billing
  537. # address.
  538. if address.country.is_shipping_country:
  539. setattr(address, 'is_%s' % action, True)
  540. elif action == 'default_for_billing':
  541. setattr(address, 'is_default_for_billing', True)
  542. else:
  543. messages.error(request, _('We do not ship to this country'))
  544. address.save()
  545. return super(AddressChangeStatusView, self).get(
  546. request, *args, **kwargs)