combo/combo/apps/lingo/views.py

437 lines
17 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# lingo - basket and payment system
# Copyright (C) 2015 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal, ROUND_HALF_UP
import json
import logging
import requests
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.http import HttpResponseForbidden, Http404
from django.template.response import TemplateResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View, DetailView, ListView, TemplateView
from django.conf import settings
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from django.db.transaction import atomic
from django.utils.encoding import smart_text
2015-03-05 17:02:52 +01:00
import eopayment
from combo.utils import check_query
try:
from mellon.models import UserSAMLIdentifier
except ImportError:
UserSAMLIdentifier = None
from .models import Regie, BasketItem, Transaction, LingoBasketCell
def get_eopayment_object(request, regie):
options = regie.service_options
options.update({
'automatic_return_url': request.build_absolute_uri(
reverse('lingo-callback', kwargs={'regie_pk': regie.id})),
'normal_return_url': request.build_absolute_uri(
reverse('lingo-return', kwargs={'regie_pk': regie.id})),
})
return eopayment.Payment(regie.service, options)
def get_basket_url():
return LingoBasketCell.objects.all()[0].page.get_online_url()
class RegiesApiView(ListView):
model = Regie
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
data = {'data': [x.as_api_dict() for x in self.get_queryset()]}
json_str = json.dumps(data)
if 'jsonpCallback' in request.GET:
json_str = '%s(%s);' % (request.GET['jsonpCallback'], json_str)
response.write(json_str)
return response
class AddBasketItemApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(AddBasketItemApiView, self).dispatch(*args, **kwargs)
def get_amount(self, amount):
if isinstance(amount, list):
d = Decimal(sum([Decimal(a) for a in amount]))
else:
d = Decimal(amount)
return d.quantize(Decimal('0.01'), ROUND_HALF_UP)
def post(self, request, *args, **kwargs):
key = getattr(settings, 'LINGO_API_SIGN_KEY', '12345')
if not check_query(request.META['QUERY_STRING'], key):
return HttpResponseForbidden()
request_body = json.loads(self.request.body)
extra = request_body.get('extra', {})
if not 'amount' in request.GET and not 'amount' in request_body and \
not 'amount' in extra:
raise Exception('missing amount parameter')
item = BasketItem(amount=0)
item.amount = self.get_amount(request.GET.getlist('amount'))
if request_body.get('amount'):
item.amount += self.get_amount(request_body['amount'])
if extra.get('amount'):
item.amount += self.get_amount(extra['amount'])
try:
if request.GET.get('NameId'):
if UserSAMLIdentifier is None:
raise Exception('missing mellon?')
try:
user = UserSAMLIdentifier.objects.get(name_id=request.GET.get('NameId')).user
except UserSAMLIdentifier.DoesNotExist:
raise Exception('unknown name id')
elif request.GET.get('email'):
user = User.objects.get(email=request.GET.get('email'))
else:
raise Exception('no user specified')
except User.DoesNotExist:
raise Exception('unknown user')
item.user = user
if request.GET.get('regie_id'):
item.regie = Regie.objects.get(id=request.GET.get('regie_id'))
else:
# if there's no regie specified, use the first one we get from the
# database...
item.regie = Regie.objects.all()[0]
if request.GET.get('cancellable') == 'no':
item.user_cancellable = False
item.subject = request_body.get('display_name')
item.source_url = request_body.get('url')
item.save()
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'result': 'success', 'id': str(item.id)}))
return response
class RemoveBasketItemApiView(View):
http_method_names = ['post', 'options']
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(RemoveBasketItemApiView, self).dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
key = getattr(settings, 'LINGO_API_SIGN_KEY', '12345')
if not check_query(request.META['QUERY_STRING'], key):
return HttpResponseForbidden()
request_body = json.loads(self.request.body)
if not 'basket_item_id' in request_body:
raise Exception('missing basket_item_id parameter')
try:
if request.GET.get('NameId'):
if UserSAMLIdentifier is None:
raise Exception('missing mellon?')
try:
user = UserSAMLIdentifier.objects.get(name_id=request.GET.get('NameId')).user
except UserSAMLIdentifier.DoesNotExist:
raise Exception('unknown name id')
elif request.GET.get('email'):
user = User.objects.get(email=request.GET.get('email'))
else:
raise Exception('no user specified')
except User.DoesNotExist:
raise Exception('unknown user')
item = BasketItem.objects.get(id=request_body.get('basket_item_id'),
user=user, cancellation_date__isnull=True)
item.notify_cancellation(skip_notification=bool(request_body.get('skip_notification')))
response = HttpResponse(content_type='application/json')
response.write(json.dumps({'result': 'success'}))
return response
2015-03-05 17:02:52 +01:00
2015-03-05 17:02:52 +01:00
class PayView(View):
@atomic
2015-03-05 17:02:52 +01:00
def post(self, request, *args, **kwargs):
regie_id = request.POST.get('regie')
next_url = request.POST.get('next_url')
if not request.POST.getlist('item'):
messages.warning(request, _('You have to choose at least one item to pay'))
return HttpResponseRedirect(next_url)
if regie_id:
regie = Regie.objects.get(pk=regie_id)
if regie.is_remote():
items = []
remote_items_data = []
# get all items data from regie webservice
for item in request.POST.getlist('item'):
remote_items_data.append(regie.get_item(request, item))
remote_items = ','.join([x.id for x in remote_items_data])
else:
items = BasketItem.objects.filter(id__in=request.POST.getlist('item'), regie=regie)
remote_items = ''
else:
items = BasketItem.objects.filter(id__in=request.POST.getlist('item'))
regie_id = items[0].regie_id
for item in items:
if item.regie_id != regie_id:
messages.error(request, _(u'Invalid grouping for basket items.'))
return HttpResponseRedirect(next_url)
regie = items[0].regie
remote_items = ''
2015-03-05 17:02:52 +01:00
transaction = Transaction()
if request.user.is_authenticated():
transaction.user = request.user
else:
transaction.user = None
2015-03-05 17:02:52 +01:00
transaction.save()
transaction.regie = regie
2015-03-05 17:02:52 +01:00
transaction.items = items
transaction.remote_items = remote_items
transaction.status = 0
2015-03-05 17:02:52 +01:00
if remote_items:
total_amount = sum([x.amount for x in remote_items_data])
else:
total_amount = sum([x.amount for x in items])
transaction.amount = total_amount
transaction.save()
2015-03-05 17:02:52 +01:00
if total_amount < regie.payment_min_amount:
messages.warning(request, _(u'Minimal payment amount is %s €.') % regie.payment_min_amount)
return HttpResponseRedirect(next_url)
payment = get_eopayment_object(request, regie)
(order_id, kind, data) = payment.request(total_amount, email=request.user.email)
logger = logging.getLogger(__name__)
logger.info(u'emitted payment request with id %s', smart_text(order_id), extra={
'eopayment_order_id': smart_text(order_id), 'eopayment_data': repr(data)})
2015-03-05 17:02:52 +01:00
transaction.order_id = order_id
transaction.save()
if next_url:
# store the next url in session in order to be able to redirect to
# it if payment is canceled
request.session.setdefault('lingo_next_url',
{})[transaction.order_id] = request.build_absolute_uri(next_url)
2015-03-05 17:02:52 +01:00
# XXX: mark basket items as being processed (?)
if kind == eopayment.URL:
return HttpResponseRedirect(data)
elif kind == eopayment.FORM:
return TemplateResponse(request, 'lingo/payment_form.html', {'form': data})
2015-03-05 17:02:52 +01:00
raise NotImplementedError()
class CallbackView(View):
def handle_callback(self, request, backend_response, **kwargs):
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
payment = get_eopayment_object(request, regie)
logger = logging.getLogger(__name__)
try:
payment_response = payment.response(backend_response)
except eopayment.ResponseError as e:
logger.error(u'failed to process payment response: %s', e,
extra={'eopayment_raw_response': repr(backend_response)})
return HttpResponseBadRequest()
extra_info = {
'eopayment_order_id': smart_text(payment_response.order_id),
'eopayment_response': repr(payment_response),
}
for k, v in payment_response.bank_data.iteritems():
extra_info['eopayment_bank_data_' + k] = smart_text(v)
if not payment_response.result == eopayment.CANCELLED:
# cancellation are not signed...
logger.warning(u'received unsigned payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
assert payment_response.signed is True
try:
transaction = Transaction.objects.get(order_id=payment_response.order_id)
except Transaction.DoesNotExist:
logger.warning(u'received unknown payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
raise Http404
else:
extra_info['lingo_transaction_id'] = transaction.pk
if transaction.user:
# let hobo logger filter handle the extraction of user's infos
extra_info['user'] = transaction.user
logger.warning(u'received unknown payment response with id %s',
smart_text(payment_response.order_id), extra=extra_info)
transaction.status = payment_response.result
transaction.bank_transaction_id = payment_response.transaction_id
transaction.bank_data = payment_response.bank_data
transaction.end_date = timezone.now()
transaction.save()
# check if transaction belongs to right regie
assert transaction.regie == regie
if payment_response.result != eopayment.PAID:
return HttpResponse()
for item in transaction.items.all():
item.payment_date = transaction.end_date
item.save()
try:
item.notify_payment()
except RuntimeError:
# ignore errors, it should be retried later on if it fails
pass
if transaction.remote_items:
for item in transaction.remote_items.split(','):
regie.pay_item(request, item, transaction.order_id,
transaction.end_date)
return HttpResponse()
def get(self, request, *args, **kwargs):
return self.handle_callback(request, request.environ['QUERY_STRING'], **kwargs)
def post(self, request, *args, **kwargs):
return self.handle_callback(request, request.body, **kwargs)
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(CallbackView, self).dispatch(*args, **kwargs)
class ReturnView(View):
@csrf_exempt
def dispatch(self, *args, **kwargs):
return super(ReturnView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
return self.handle_return(request, request.environ['QUERY_STRING'], **kwargs)
def post(self, request, *args, **kwargs):
return self.handle_return(request, request.body, **kwargs)
def handle_return(self, request, backend_response, **kwargs):
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
payment = get_eopayment_object(request, regie)
try:
payment_response = payment.response(backend_response)
except eopayment.ResponseError as e:
# if eopayment can't get response from query string redirect to
# homepage
logging.error('failed to process payment response (%r)', e)
messages.error(request, _('We are sorry but the payment service '
'failed to provide a correct answer.'))
return HttpResponseRedirect(get_basket_url())
if payment_response.result == eopayment.PAID:
messages.info(request, regie.get_text_on_success())
transaction = Transaction.objects.get(order_id=payment_response.order_id)
if request.session.get('lingo_next_url'):
redirect_url = request.session['lingo_next_url'].get(transaction.order_id)
if redirect_url:
return HttpResponseRedirect(redirect_url)
# return to basket page if there are still items to pay
remaining_basket_items = BasketItem.objects.filter(user=self.request.user,
payment_date__isnull=True, cancellation_date__isnull=False).count()
if remaining_basket_items:
return HttpResponseRedirect(get_basket_url())
return HttpResponseRedirect('/')
2015-09-03 16:03:44 +02:00
class ItemDownloadView(View):
http_method_names = [u'get']
def get(self, request, *args, **kwargs):
regie = Regie.objects.get(pk=kwargs['regie_id'])
try:
data = regie.download_item(request, kwargs['item_id'])
except PermissionDenied:
return HttpResponseForbidden()
if data.status_code != 200:
logging.error('failed to retrieve invoice (%r)', data.status_code)
messages.error(request, _('We are sorry but an error occured when retrieving the invoice.'))
if self.request.META.get('HTTP_REFERER'):
return HttpResponseRedirect(self.request.META.get('HTTP_REFERER'))
return HttpResponseRedirect('/')
2015-09-03 16:03:44 +02:00
r = HttpResponse(data, content_type='application/pdf')
r['Content-Disposition'] = 'attachment; filename="%(item_id)s.pdf"' % kwargs
return r
2015-09-04 10:41:57 +02:00
class ItemView(TemplateView):
http_method_names = [u'get']
template_name = 'lingo/combo/item.html'
def get_context_data(self, **kwargs):
regie = Regie.objects.get(pk=kwargs['regie_id'])
item = regie.get_item(self.request, kwargs['item_id'])
return {'item': item, 'regie': regie}
class CancelItemView(DetailView):
model = BasketItem
template_name = 'lingo/combo/cancel-item.html'
def get_context_data(self, **kwargs):
context = super(CancelItemView, self).get_context_data(**kwargs)
context['basket_url'] = get_basket_url()
return context
def get_queryset(self):
return BasketItem.objects.filter(user=self.request.user,
payment_date__isnull=True,
cancellation_date__isnull=True)
def post(self, request, *args, **kwargs):
if not self.get_object().user_cancellable:
messages.error(request, _('This item cannot be removed'))
return HttpResponseRedirect(get_basket_url())
try:
self.get_object().notify_cancellation()
except requests.exceptions.HTTPError:
messages.error(request, _('An error occured when removing the item.'))
return HttpResponseRedirect(get_basket_url())