Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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