From 880431f3576e74f9ab54eeaed1c0f5125ed835f4 Mon Sep 17 00:00:00 2001 From: adeelehsan Date: Tue, 18 Sep 2018 16:46:09 +0500 Subject: [PATCH] EasyPay Integrated. - Mobile wallet is working -Over the counter(OTC) voucher is creation is working --- pycon_web/apps/__init__.py | 0 pycon_web/apps/mixin.py | 9 + pycon_web/apps/payments/__init__.py | 0 pycon_web/apps/payments/admin.py | 15 ++ pycon_web/apps/payments/api/__init__.py | 0 pycon_web/apps/payments/api/v1/__init__.py | 0 .../apps/payments/api/v1/easypay_helper.py | 167 ++++++++++++++++++ pycon_web/apps/payments/api/v1/serializers.py | 17 ++ pycon_web/apps/payments/api/v1/urls.py | 7 + pycon_web/apps/payments/api/v1/utils.py | 72 ++++++++ pycon_web/apps/payments/api/v1/views.py | 34 ++++ .../apps/payments/migrations/0001_initial.py | 64 +++++++ .../apps/payments/migrations/__init__.py | 0 pycon_web/apps/payments/models.py | 108 +++++++++++ pycon_web/apps/payments/urls.py | 7 + pycon_web/apps/urls.py | 9 + pycon_web/settings.py | 35 +++- pycon_web/urls.py | 3 + requirements.txt | 4 +- 19 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 pycon_web/apps/__init__.py create mode 100644 pycon_web/apps/mixin.py create mode 100644 pycon_web/apps/payments/__init__.py create mode 100644 pycon_web/apps/payments/admin.py create mode 100644 pycon_web/apps/payments/api/__init__.py create mode 100644 pycon_web/apps/payments/api/v1/__init__.py create mode 100644 pycon_web/apps/payments/api/v1/easypay_helper.py create mode 100644 pycon_web/apps/payments/api/v1/serializers.py create mode 100644 pycon_web/apps/payments/api/v1/urls.py create mode 100644 pycon_web/apps/payments/api/v1/utils.py create mode 100644 pycon_web/apps/payments/api/v1/views.py create mode 100644 pycon_web/apps/payments/migrations/0001_initial.py create mode 100644 pycon_web/apps/payments/migrations/__init__.py create mode 100644 pycon_web/apps/payments/models.py create mode 100644 pycon_web/apps/payments/urls.py create mode 100644 pycon_web/apps/urls.py diff --git a/pycon_web/apps/__init__.py b/pycon_web/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycon_web/apps/mixin.py b/pycon_web/apps/mixin.py new file mode 100644 index 0000000..bea443f --- /dev/null +++ b/pycon_web/apps/mixin.py @@ -0,0 +1,9 @@ +from django.db import models + + +class CreateUpdateMixin(models.Model): + created_at = models.DateTimeField(auto_now_add=True, editable=False) + last_updated_at = models.DateTimeField(auto_now=True, editable=False) + + class Meta: + abstract = True \ No newline at end of file diff --git a/pycon_web/apps/payments/__init__.py b/pycon_web/apps/payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycon_web/apps/payments/admin.py b/pycon_web/apps/payments/admin.py new file mode 100644 index 0000000..f1a3c3a --- /dev/null +++ b/pycon_web/apps/payments/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from .models import PaymentMethod, EasyPayPayment + + +class PaymentMethodAdmin(admin.ModelAdmin): + list_display = ['id', 'code', 'name'] + + +class EasyPayPaymentAdmin(admin.ModelAdmin): + list_display = ['id', 'payment_method'] + + +admin.site.register(PaymentMethod, PaymentMethodAdmin) +admin.site.register(EasyPayPayment, EasyPayPaymentAdmin) diff --git a/pycon_web/apps/payments/api/__init__.py b/pycon_web/apps/payments/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycon_web/apps/payments/api/v1/__init__.py b/pycon_web/apps/payments/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycon_web/apps/payments/api/v1/easypay_helper.py b/pycon_web/apps/payments/api/v1/easypay_helper.py new file mode 100644 index 0000000..cd102d2 --- /dev/null +++ b/pycon_web/apps/payments/api/v1/easypay_helper.py @@ -0,0 +1,167 @@ +import logging +from enum import Enum + +import zeep +from requests import Session +from zeep.transports import Transport + + +logger = logging.getLogger(__name__) + + +class EasyPayException(Exception): + def __init__(self, message): + super(EasyPayException, self).__init__(message) + + +class EasyPayIPNTransactionStatus(Enum): + SUCCESSFUL = 'PAID' + PENDING = 'PENDING' + FAILED = 'FAILED' + EXPIRED = 'EXPIRED' + + def is_successful(self): + return self == EasyPayIPNTransactionStatus.SUCCESSFUL + + def easypay_payment_status(self): + from pycon_web.apps.payments.models import TRANSACTION_STATUS_SUCCESS, TRANSACTION_STATUS_FAILURE, \ + TRANSACTION_STATUS_PENDING + if self == EasyPayIPNTransactionStatus.SUCCESSFUL: + return TRANSACTION_STATUS_SUCCESS + elif self == EasyPayIPNTransactionStatus.PENDING: + return TRANSACTION_STATUS_PENDING + elif self == EasyPayIPNTransactionStatus.FAILED: + return TRANSACTION_STATUS_FAILURE + elif self == EasyPayIPNTransactionStatus.EXPIRED: + return TRANSACTION_STATUS_FAILURE + + +def easypay_default_wsdl_client(wsdl_url): + session = Session() + session.verify = False + transport = Transport(session=session) + client = zeep.Client(wsdl=wsdl_url, transport=transport, strict=False) + return client + + +class EasyPayAPIHelper: + def __init__(self, easypay_payment_method): + wsdl_url = easypay_payment_method.get_value_or_none('WSDL_URL') + + self.username = easypay_payment_method.get_value_or_none('MERCHANT_USERNAME') + self.password = easypay_payment_method.get_value_or_none('MERCHANT_PASSWORD') + self.storeId = easypay_payment_method.get_value_or_none('STORE_ID') + self.binding_name = easypay_payment_method.get_value_or_none('SOAP_BINDING_NAME') + self.soap_endpoint = easypay_payment_method.get_value_or_none('SOAP_ENDPOINT') + + self.transaction_id = None + self.transaction_datetime = None + self.transaction_expiry_datetime = None + self.easy_pay_voucher_code = None + + self.client = easypay_default_wsdl_client(wsdl_url) + + def initiate_transaction(self, amount, easypay_transaction_type, mobile_number, email_address=None): + initiateTransactionHackService = self.client.create_service(self.binding_name, self.soap_endpoint) + + order_id = "Pycon-212" # setting it as constant because order functionality is missing + params_dict = { + 'username': self.username, + 'password': self.password, + 'orderId': order_id, + 'storeId': self.storeId, + 'transactionAmount': str(amount), + 'transactionType': easypay_transaction_type.api_representation(), + 'msisdn': mobile_number, + 'mobileAccountNo': mobile_number, + 'emailAddress': email_address + } + + logger.info('Sending request to EasyPay. Params: {}'.format(params_dict)) + + result = initiateTransactionHackService.initiateTransaction(**params_dict) + + """ + Sample Response + + + + + 0000 + Pycon-2 + 2824 + 592342 + 2017-08-18T15:47:25.937+05:00 + 2017-08-23T15:47:25.922+05:00 + + + + """ + + logger.info('Response from EasyPay : {}'.format(str(result))) + + self.easy_pay_voucher_code = result['paymentToken'] + self.transaction_id = result['transactionId'] + self.transaction_datetime = result['transactionDateTime'] + self.transaction_expiry_datetime = result['paymentTokenExiryDateTime'] + + status_code = EasyPayStatusCode(result['responseCode']) + + if status_code.is_successful(): + return self.easy_pay_voucher_code, self.transaction_id, self.transaction_expiry_datetime + + logger.error(result['responseCode']) + raise EasyPayException(status_code.user_facing_message()) + + +class EasyPayStatusCode(Enum): + PAYMENT_SUCCESSFUL = '0000' + SYSTEM_ERROR = '0001' + REQUIRED_FIELD_MISSING = '0002' + INVALID_ORDER_ID = '0003' + MERCHANT_DOES_NOT_EXIST = '0004' + MERCHANT_NOT_ACTIVE = '0005' + STORE_DOES_NOT_EXIST = '0006' + STORE_INACTIVE = '0007' + + def user_facing_message(self): + if self == EasyPayStatusCode.PAYMENT_SUCCESSFUL: + return 'Payment Successful.' + elif self == EasyPayStatusCode.SYSTEM_ERROR: + return 'System Error.' + elif self == EasyPayStatusCode.REQUIRED_FIELD_MISSING: + return 'A required field is missing in the request.Please make sure your ' \ + 'mobile number is correct' + elif self == EasyPayStatusCode.INVALID_ORDER_ID: + return 'Invalid Order ID.' + elif self == EasyPayStatusCode.MERCHANT_DOES_NOT_EXIST: + return 'Merchant does not exist.' + elif self == EasyPayStatusCode.MERCHANT_NOT_ACTIVE: + return 'Merchant not active.' + elif self == EasyPayStatusCode.STORE_DOES_NOT_EXIST: + return 'Store does not exist.' + elif self == EasyPayStatusCode.STORE_INACTIVE: + return 'Store Inactive.' + + def is_successful(self): + return self == EasyPayStatusCode.PAYMENT_SUCCESSFUL + + +class EasyPayPaymentMethod(Enum): + OTC = 0 + MA = 1 + + def api_representation(self): + if self == EasyPayPaymentMethod.OTC: + return 'OTC' + elif self == EasyPayPaymentMethod.MA: + return 'MA' + + def label(self): + if self == EasyPayPaymentMethod.OTC: + return 'Over the Counter' + elif self == EasyPayPaymentMethod.MA: + return 'Mobile Account' + + def api_success_code(self): + return EasyPayStatusCode.PAYMENT_SUCCESSFUL diff --git a/pycon_web/apps/payments/api/v1/serializers.py b/pycon_web/apps/payments/api/v1/serializers.py new file mode 100644 index 0000000..db9016e --- /dev/null +++ b/pycon_web/apps/payments/api/v1/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from pycon_web.apps.payments.models import PaymentMethod + + +class MakePaymentRequestSerializer(serializers.Serializer): + payment_method = serializers.SlugRelatedField(slug_field='code', + queryset=PaymentMethod.objects.all()) + mobile_number = serializers.CharField(required=True) + transaction_amount = serializers.IntegerField(required=True) + email = serializers.CharField(required=False, allow_null=True, allow_blank=True) + + def validate_mobile_number(self, mobile_number): + if len(mobile_number) != 11: + raise serializers.ValidationError("Invalid Mobile Number") + + return mobile_number diff --git a/pycon_web/apps/payments/api/v1/urls.py b/pycon_web/apps/payments/api/v1/urls.py new file mode 100644 index 0000000..f4463a3 --- /dev/null +++ b/pycon_web/apps/payments/api/v1/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from pycon_web.apps.payments.api.v1.views import MakePayment + +urlpatterns = [ + url(r'^make_payment/?$', MakePayment.as_view(), name='make_payment') +] \ No newline at end of file diff --git a/pycon_web/apps/payments/api/v1/utils.py b/pycon_web/apps/payments/api/v1/utils.py new file mode 100644 index 0000000..1c7e543 --- /dev/null +++ b/pycon_web/apps/payments/api/v1/utils.py @@ -0,0 +1,72 @@ +import logging + +from rest_framework import status +from rest_framework.response import Response + +logger = logging.getLogger(__name__) + + +DEFAULT_SUCCESS_MESSAGE = 'Operation successful.' +DEFAULT_FAILURE_MESSAGE = 'Something went wrong.' + + +def attempt_transaction(payment, email): + transaction_error_message = "Error while attempting transaction" + try: + logger.info('Attempting transactions') + success, message = payment.make_payment(email) + + except Exception as e: + logger.error(str(e)) + + response = failure_response(failure_message=transaction_error_message, + data=get_payment_messages(payment)) + return response + + if success: + + response = success_response( + {'payment_method': get_payment_messages(payment)}, message + ) + else: + response = failure_response( + failure_message=message, + data=get_payment_messages(payment) + ) + + return response + + +def success_response(data=None, success_message=DEFAULT_SUCCESS_MESSAGE, + status_code=status.HTTP_200_OK): + + response_data = { + 'success': True, + 'message': success_message, + 'data': data + } + return Response(response_data, status=status_code) + + +def failure_response(failure_message=DEFAULT_FAILURE_MESSAGE, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=None): + response_data = { + 'status_code': status_code, + 'success': False, + 'message': failure_message, + 'data': data + } + return Response(response_data, status=status_code) + + +def get_payment_messages(attrs): + payment_method = attrs.payment_method + + if payment_method.code == 'EASYPAY_MA': + payment_method_message = "Easypaisa mobile account" + elif payment_method.code == 'EASYPAY_OTC': + payment_method_message = "Easypaisa shop" + else: + payment_method_message = str(attrs.payment_method.code) + + return payment_method_message \ No newline at end of file diff --git a/pycon_web/apps/payments/api/v1/views.py b/pycon_web/apps/payments/api/v1/views.py new file mode 100644 index 0000000..4141d44 --- /dev/null +++ b/pycon_web/apps/payments/api/v1/views.py @@ -0,0 +1,34 @@ +from rest_framework.exceptions import ValidationError +from rest_framework.views import APIView + +from pycon_web.apps.payments.api.v1.serializers import MakePaymentRequestSerializer +from pycon_web.apps.payments.api.v1.utils import attempt_transaction, failure_response +from pycon_web.apps.payments.models import EasyPayPayment + + +class MakePayment(APIView): + + def post(self, request): + try: + request_serializer = MakePaymentRequestSerializer(data=request.data) + request_serializer.is_valid(raise_exception=True) + request_data = request_serializer.validated_data + + payment_method = request_data['payment_method'] + mobile_number = request_data['mobile_number'] + if not mobile_number: + raise Exception("Mobile number not found.") + + transaction_amount = request_data['transaction_amount'] + email = request_data.get('email', "pycon_arbisoft@gmail.com") + + if payment_method.code in ['EASYPAY_MA', 'EASYPAY_OTC']: + payment = EasyPayPayment.create(transaction_amount, mobile_number, + payment_method) + + response = attempt_transaction(payment, email) + + return response + + except ValidationError as ve: + return failure_response(failure_message=ve, status_code=400) diff --git a/pycon_web/apps/payments/migrations/0001_initial.py b/pycon_web/apps/payments/migrations/0001_initial.py new file mode 100644 index 0000000..814bd2a --- /dev/null +++ b/pycon_web/apps/payments/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-09-17 12:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_updated_at', models.DateTimeField(auto_now=True)), + ('amount', models.FloatField(default=0)), + ('description', models.TextField(blank=True, null=True)), + ('mobile_number', models.CharField(blank=True, max_length=20, null=True)), + ('transaction_status', models.CharField(choices=[(b'success', b'Transaction successful.'), (b'pending', b'Transaction in progress.'), (b'fail', b'Transaction failed.'), (b'initial', b'Transaction initialized.')], default=b'initial', max_length=25)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=100)), + ('code', models.CharField(max_length=100, unique=True)), + ('merchant_config', models.TextField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EasyPayPayment', + fields=[ + ('payment_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='payments.Payment')), + ('payment_token', models.CharField(blank=True, help_text=b'EasyPay generated token in case of OTC transactions', max_length=50, null=True)), + ('transaction_id', models.CharField(blank=True, help_text=b'EasyPay generated Order ID (only for Mobile Account)', max_length=50, null=True)), + ('transaction_expiry_datetime', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'abstract': False, + }, + bases=('payments.payment',), + ), + migrations.AddField( + model_name='payment', + name='payment_method', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='payments.PaymentMethod'), + ), + ] diff --git a/pycon_web/apps/payments/migrations/__init__.py b/pycon_web/apps/payments/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pycon_web/apps/payments/models.py b/pycon_web/apps/payments/models.py new file mode 100644 index 0000000..a2a13a1 --- /dev/null +++ b/pycon_web/apps/payments/models.py @@ -0,0 +1,108 @@ +import json +import logging + +from django.db import models + +from pycon_web.apps.mixin import CreateUpdateMixin +from pycon_web.apps.payments.api.v1.easypay_helper import EasyPayPaymentMethod, EasyPayException, \ + EasyPayAPIHelper + +logger = logging.getLogger(__name__) + + +TRANSACTION_STATUS_INITIAL = 'initial' +TRANSACTION_STATUS_PENDING = 'pending' +TRANSACTION_STATUS_SUCCESS = 'success' +TRANSACTION_STATUS_FAILURE = 'fail' + +TRANSACTION_STATUS_CHOICES = ( + (TRANSACTION_STATUS_SUCCESS, 'Transaction successful.'), + (TRANSACTION_STATUS_PENDING, 'Transaction in progress.'), + (TRANSACTION_STATUS_FAILURE, 'Transaction failed.'), + (TRANSACTION_STATUS_INITIAL, 'Transaction initialized.'), +) + + +class PaymentMethod(CreateUpdateMixin): + name = models.CharField(max_length=100) + code = models.CharField(max_length=100, unique=True) + merchant_config = models.TextField() + + def __str__(self): + return '{name}'.format(name=self.name) + + def get_value_or_none(self, key): + try: + configuration = json.loads(self.merchant_config) + return configuration.get(key, None) + except: + logger.exception('Exception while parsing Easypay Config') + return None + + +class Payment(CreateUpdateMixin): + ''' + This is an abstract model for payments + It contains all basic information which are possibly required in order to add a payment + Every payment option should inherit from this model as dine for EasyPay and Jazzcash + ''' + amount = models.FloatField(default=0) + description = models.TextField(blank=True, null=True) + mobile_number = models.CharField(max_length=20, null=True, blank=True) + + transaction_status = models.CharField(max_length=25, choices=TRANSACTION_STATUS_CHOICES, + default=TRANSACTION_STATUS_INITIAL) + + payment_method = models.ForeignKey(to=PaymentMethod) + + +class EasyPayPayment(Payment): + payment_token = models.CharField(max_length=50, null=True, blank=True, + help_text='EasyPay generated token in case of OTC transactions') + transaction_id = models.CharField(max_length=50, null=True, blank=True, + help_text='EasyPay generated Order ID (only for Mobile Account)') + transaction_expiry_datetime = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return '{amount} - {mobile_no} - {transaction_type}'.format(amount=self.amount, + mobile_no=self.mobile_number, + transaction_type=self.payment_method.name) + + @classmethod + def create(cls, transaction_amount, mobile_number, payment_method): + payment_object = cls(amount=transaction_amount, mobile_number=mobile_number, + payment_method=payment_method) + + payment_object.save() + return payment_object + + def make_payment(self, buyer_email): + if self.transaction_status in [TRANSACTION_STATUS_SUCCESS, TRANSACTION_STATUS_FAILURE]: + raise EasyPayException('Payment already processed for this instance') + try: + easy_pay_api_helper = EasyPayAPIHelper(self.payment_method) + self.transaction_status = TRANSACTION_STATUS_INITIAL + easy_pay_payment_method = EasyPayPaymentMethod[self.payment_method.name] + transaction_type = easy_pay_payment_method.value + self.payment_token, self.transaction_id, self.transaction_expiry_datetime = easy_pay_api_helper \ + .initiate_transaction( + amount=self.amount, + easypay_transaction_type=easy_pay_payment_method, + mobile_number=self.mobile_number, + email_address=buyer_email + ) + if transaction_type == EasyPayPaymentMethod.MA.value: + self.transaction_status = TRANSACTION_STATUS_SUCCESS + message = 'Success making payment via EasyPay.' + else: + message = 'Success generating voucher via EasyPay OTC.' + self.transaction_status = TRANSACTION_STATUS_PENDING + + success = True + except EasyPayException as epe: + self.transaction_status = TRANSACTION_STATUS_FAILURE + logger.exception('Exception with EasyPay PG') + success, message = False, str(epe) + + self.save() + return success, message \ No newline at end of file diff --git a/pycon_web/apps/payments/urls.py b/pycon_web/apps/payments/urls.py new file mode 100644 index 0000000..6d5f7db --- /dev/null +++ b/pycon_web/apps/payments/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url, include + +from pycon_web.apps.payments.api.v1.urls import urlpatterns as v1 + +urlpatterns = [ + url(r'^pycon_payments/', include(v1)), +] diff --git a/pycon_web/apps/urls.py b/pycon_web/apps/urls.py new file mode 100644 index 0000000..d695acc --- /dev/null +++ b/pycon_web/apps/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url, include + +from pycon_web.apps.payments.urls import urlpatterns as payment_urls + +v1_url_regex_pattern = r'^v1/' + +urlpatterns = [ + url(v1_url_regex_pattern, include(payment_urls)) +] diff --git a/pycon_web/settings.py b/pycon_web/settings.py index 94a741f..6f9e962 100644 --- a/pycon_web/settings.py +++ b/pycon_web/settings.py @@ -191,6 +191,37 @@ # Package/module name to import the root urlpatterns from for the project. ROOT_URLCONF = "%s.urls" % PROJECT_APP +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(asctime)s %(message)s' + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + } + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, + 'zeep.transports': { + 'level': 'DEBUG', + 'propagate': True, + 'handlers': ['console'], + }, + } +} TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -245,7 +276,9 @@ "mezzanine.forms", "mezzanine.galleries", "mezzanine.twitter", - "captcha" + "captcha", + "pycon_web.apps", + "pycon_web.apps.payments" # "mezzanine.accounts", # "mezzanine.mobile", ) diff --git a/pycon_web/urls.py b/pycon_web/urls.py index 1600982..cf0877a 100644 --- a/pycon_web/urls.py +++ b/pycon_web/urls.py @@ -9,6 +9,8 @@ from mezzanine.core.views import direct_to_template from mezzanine.conf import settings +from pycon_web.apps.urls import urlpatterns as apps_urls + admin.autodiscover() @@ -99,6 +101,7 @@ # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls")) + url(r'^api/', include(apps_urls)), ] # Adds ``STATIC_URL`` to the context of error pages, so that error diff --git a/requirements.txt b/requirements.txt index a4aa0f9..9234024 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ Mezzanine==4.2.3 psycopg2 future fabric -django-recaptcha==1.3.1 \ No newline at end of file +django-recaptcha==1.3.1 +zeep==2.3.0 +djangorestframework \ No newline at end of file