├── .gitignore ├── .hgignore ├── .hgtags ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── build.sh ├── example_project ├── app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── app │ │ │ ├── base.html │ │ │ └── pay_with_robokassa.html │ │ └── robokassa │ │ │ ├── error.html │ │ │ ├── fail.html │ │ │ └── success.html │ ├── tests.py │ └── views.py ├── example_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py ├── robokassa ├── __init__.py ├── conf.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── signals.py ├── tests.py ├── urls.py └── views.py ├── runtests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .tox 2 | *.pyc 3 | __pycache__/ 4 | MANIFEST 5 | build/ 6 | dist/ 7 | .idea/ 8 | *.sqlite3 9 | *.bak 10 | *.egg-info/ 11 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | #IDE files 4 | .settings/* 5 | .project 6 | .pydevproject 7 | .cache/* 8 | nbproject/* 9 | .buildpath 10 | build.properties 11 | .idea/* 12 | 13 | #temp files 14 | *.pyc 15 | *.pyo 16 | *.orig 17 | *~ 18 | 19 | #misc files 20 | pip-log.txt 21 | 22 | #os files 23 | .DS_Store 24 | Thumbs.db 25 | 26 | #setup files 27 | build/ 28 | dist/ 29 | .build/ 30 | MANIFEST 31 | django_robokassa.egg-info 32 | .tox 33 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | d76fe387d6227fa7e5409f19d0c1818a8cfe579c 0.9.0 2 | 9f32acddc224e6e94c472805d9fee248c31c42d5 0.9.1 3 | 064e8588576a229fcbe22a8c507abfd2ee3850b9 0.9.2 4 | 0e224e5f37776c80d2fa7fda2c25fed303246bb8 0.9.3 5 | db88f5aeabab6de837326a6712b37e6a8986afc8 1.0 6 | af1b392e0048d7353f083ba73ef2d4c329fbf5f5 1.1 7 | 951f830faab4586eb83775ddceca86cef203943f 1.2 8 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 2 | История изменений 3 | ================= 4 | 5 | 1.4 (2018-04-26) 6 | ---------------- 7 | 8 | * Добавлена поддержка Python 3 (3.4, 3.5, 3.6, 3.7) 9 | * Добавлена поддержка последних версий Django (1.11, 2.0) 10 | * Добавлен тестовый проект example_project 11 | * Миграции заменены на стандартные (вместо South) 12 | * Код подчищен для большего соответствия PEP-8 13 | 14 | 1.3 (2016-03-07) 15 | ---------------- 16 | 17 | * исправлена работа в тестовом режиме (спасибо Шумихину Ивану); 18 | * настройка ROBOKASSA_TEST_FORM_TARGET позволяет указать свой тестовый сервер 19 | (спасибо https://github.com/superqwer). 20 | * репозиторий на bitbucket больше не поддерживается; 21 | * setup.py теперь использует setuptools. 22 | 23 | 1.2 (2013-12-24) 24 | ---------------- 25 | 26 | * Добавлена поддержка django 1.6 - спасибо Александру Симкину; 27 | * Python 2.5 больше не поддерживается. 28 | 29 | 1.1 (2013-04-12) 30 | ---------------- 31 | 32 | * На странице FailURL больше не проверяется подпись (т.к. Робокасса ее 33 | больше не передает) - спасибо @amureki; 34 | * улучшена справка - спасибо @bo858585. 35 | 36 | 1.0 (2012-03-24) 37 | ---------------- 38 | * Для работы теперь требуется django >= 1.3; 39 | * добавлена поддержка django 1.4; 40 | * все вьюхи возвращают теперь TemplateResponse; 41 | * миграции переведены на современную версию south; 42 | * запуск тестов через tox; 43 | * небольшие улучшения в README. 44 | 45 | 0.9.3 (2010-08-05) 46 | ------------------ 47 | Сообщения с ошибочной подписью не вызывают исключения. 48 | 49 | 0.9.2 (2010-06-23) 50 | ------------------ 51 | Добавлена поддержка django 1.2. 52 | 53 | 0.9.1 (2010-05-10) 54 | ------------------ 55 | Исправлена работа с дополнительными (пользовательскими) параметрами. 56 | 57 | 0.9.0 (2010-04-15) 58 | ------------------ 59 | Первая версия 60 | 61 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Mikhail Korobov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | recursive-include docs *.txt -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | django-robokassa3 3 | ================ 4 | 5 | *Данный репозиторий является форком более неподдерживаемой библиотеки за авторством https://github.com/kmike* 6 | *Добавлены поддержка современных версий Django (1.11, 2.0), поддержка Python 3 и тестовый проект.* 7 | 8 | **django-robokassa3** - это приложение для интеграции платежной системы ROBOKASSA в 9 | проекты на Django. 10 | 11 | До использования следует ознакомиться с официальной документацией 12 | ROBOKASSA (http://docs.robokassa.ru/). Приложение реализует 13 | протокол взаимодействия, описанный в этом документе. 14 | 15 | Установка 16 | ========= 17 | 18 | :: 19 | 20 | $ pip install django-robokassa3 21 | 22 | Потом следует добавить 'robokassa' в INSTALLED_APPS и выполнить :: 23 | 24 | $ python manage.py migrate 25 | 26 | Для работы требуется Django 1.11 или 2.0. Поддерживаются Python 2.7 и >=3.4. 27 | 28 | Настройка 29 | ========= 30 | 31 | В settings.py нужно указать следующие настройки: 32 | 33 | * ROBOKASSA_LOGIN - логин 34 | * ROBOKASSA_PASSWORD1 - пароль №1 35 | 36 | Необязательные параметры: 37 | 38 | * ROBOKASSA_PASSWORD2 - пароль №2. Его можно не указывать, если 39 | django-robokassa используется только для вывода формы платежа. 40 | Если django-robokassa используется для приема платежей, то этот 41 | параметр обязательный. 42 | 43 | * ROBOKASSA_USE_POST - используется ли метод POST при приеме результатов от 44 | ROBOKASSA. По умолчанию - True. Считается, что для Result URL, Success URL и 45 | Fail URL выбран один и тот же метод. 46 | 47 | * ROBOKASSA_STRICT_CHECK - использовать ли строгую проверку (требовать 48 | предварительного уведомления на ResultURL). По умолчанию - True. 49 | 50 | * ROBOKASSA_TEST_MODE - включен ли тестовый режим. По умолчанию False 51 | (т.е. включен боевой режим). 52 | 53 | * ROBOKASSA_EXTRA_PARAMS - список (list) названий дополнительных параметров, 54 | которые будут передаваться вместе с запросами. "Shp" к ним приписывать не 55 | нужно. 56 | 57 | * ROBOKASSA_TEST_FORM_TARGET - url робокассы для тестового режима. 58 | Настройка предназначена для случая, когда в распоряжении не имеется 59 | доступного в интернете домена (например разработка на localhost) 60 | и вместо сервера робокассы необходимо 61 | использовать свой. 62 | 63 | 64 | Использование 65 | ============= 66 | 67 | Форма для приема платежей 68 | ------------------------- 69 | 70 | Для того, чтобы упростить конструирование html-форм для отправки пользователей в 71 | Robokassa, в django-robokassa есть форма RobokassaForm. Она нужна 72 | для упрощения вывода информации в шаблонах, вычисления контрольной суммы и 73 | формирования параметров GET-запросов. 74 | 75 | Пример:: 76 | 77 | # views.py 78 | 79 | from django.shortcuts import get_object_or_404, render 80 | from django.contrib.auth.decorators import login_required 81 | 82 | from robokassa.forms import RobokassaForm 83 | 84 | @login_required 85 | def pay_with_robokassa(request, order_id): 86 | order = get_object_or_404(Order, pk=order_id) 87 | 88 | form = RobokassaForm(initial={ 89 | 'OutSum': order.total, 90 | 'InvId': order.id, 91 | 'Desc': order.name, 92 | 'Email': request.user.email, 93 | # 'IncCurrLabel': '', 94 | # 'Culture': 'ru' 95 | }) 96 | 97 | return render(request, 'pay_with_robokassa.html', {'form': form}) 98 | 99 | В initial все параметры необязательны. Детальную справку по параметрам 100 | лучше посмотреть в `документации `_ 101 | к Robokassa. Можно передавать в initial значения "пользовательских параметров", 102 | описанных в ROBOKASSA_EXTRA_PARAMS ('shp' к ним приписывать опять не нужно). 103 | 104 | Соответствующий шаблон:: 105 | 106 | {% extends 'base.html' %} 107 | 108 | {% block content %} 109 |
110 |

{{ form.as_p }}

111 |

112 |
113 | {% endblock %} 114 | 115 | Форма выведется в виде набора скрытых input-тегов. 116 | 117 | У формы есть атрибут target, содержащий URL, по которому форму следует 118 | отправлять. В тестовом режиме это будет тестовый URL, в боевом - боевой. 119 | 120 | Обратите внимание, {% csrf_token %} в форме не нужен (и более того, добавлять 121 | его к форме небезопасно), т.к. форма ведет на внешний сайт - сайт робокассы. 122 | 123 | Вместо отправки формы можно сформировать GET-запрос. У формы есть 124 | метод get_redirect_url, который возвращает нужный адрес со всеми параметрами. 125 | Редирект на этот адрес равносилен отправке формы методом GET. 126 | 127 | django-robokassa не включает в себя модели "Покупка" (``Order`` в примере), 128 | т.к. эта модель будет отличаться от сайта к сайту. Обработку смены статусов 129 | покупок следует осуществлять в обработчиках сигналов. 130 | 131 | 132 | Получение результатов платежей 133 | ------------------------------ 134 | В Robokassa есть несколько методов определения результата платежа: 135 | 136 | 1. При переходе на страницы Success и Fail гарантируется, что платеж 137 | соответственно прошел и не прошел 138 | 139 | 2. При успешном или неудачном платеже Robokassa отправляет POST или GET запрос 140 | на Result URL. 141 | 142 | 3. Можно запрашивать статус платежа через XML-сервис. 143 | 144 | В django-robokassa на данный момент поддерживаются методы 1 и 2 и их совмещение 145 | (дополнительная проверка, что при переходе на Success URL уже было уведомление 146 | на Result URL при использовании опции ROBOKASSA_STRICT_CHECK = True). 147 | 148 | В целях безопасности лучше всегда использовать строгую проверку 149 | (с подтверждением через Result URL). Ее механизм: 150 | 151 | 1. После оплаты robokassa.ru отправляет "фоновый" запрос на ResultURL. 152 | 153 | 2. Внутри view, связанного с ResultURL, происходит проверка содержащейся в 154 | запросе md5-подписи через ROBOKASSA_PASSWORD2 (это второй пароль, который не 155 | передается по сети и известен только отправителю и получателю). 156 | ROBOKASSA_PASSWORD2 нужен для подтверждения того, что запрос был послан 157 | именно с robokassa.ru. 158 | 159 | 3. Если запрос правильный, то view шлет сигнал 160 | ``robokassa.signals.result_received``. Чтоб производить 161 | манипуляции внутри сайта (например, начислять средства согласно 162 | пришедшему запросу или менять статус заказа), нужно добавить 163 | соответствующий обработчик этого сигнала. 164 | 165 | 4. Если все в порядке, то view, связанный с Result URL, 166 | отдает robokassa.ru ответ вида ``OK``, 167 | где ```` - уникальный id текущей операции. 168 | Этот ответ необходим для того, чтобы robokassa.ru получила 169 | подтверждение того, что все необходимые действия произведены. 170 | 171 | 5. Если robokassa.ru получает этот ответ, то пользователь перенаправляется 172 | на Success URL. На этой страничке обычно лучше вывести сообщение 173 | об успешном прохождении платежа/оплаты. Если ответ view, связанной 174 | с Result URL, не соответвтует ожидаемому, то пользователь перенаправляется 175 | не на Success URL, а на Fail URL; там ему хорошо бы показать 176 | сообщение о произошедшей ошибке. 177 | 178 | 179 | Сигналы 180 | ------- 181 | 182 | Обработку смены статусов покупок следует осуществлять в обработчиках сигналов. 183 | 184 | * ``robokassa.signals.result_received`` - шлется при получении уведомления от 185 | Robokassa. Получение этого сигнала означает, что оплата была успешной. 186 | В качестве sender передается экземпляр модели SuccessNotification, у 187 | которой есть атрибуты InvId и OutSum. 188 | 189 | * ``robokassa.signals.success_page_visited`` - шлется при переходе пользователя 190 | на страницу успешной оплаты. Этот сигнал следует использовать вместо 191 | result_received, если не используется строгая проверка 192 | (ROBOKASSA_STRICT_CHECK=False) 193 | 194 | * ``robokassa.signals.fail_page_visited`` - шлется при переходе пользователя 195 | на страницу ошибки оплаты. Получение этого сигнала означает, что оплата 196 | не была произведена. В обработчике следует осуществлять разблокирвку товара 197 | на складе и т.д. 198 | 199 | Все сигналы получают параметры InvId (номер заказа), OutSum (сумма оплаты) и 200 | extra (словарь с дополнительными параметрами, описанными в 201 | ROBOKASSA_EXTRA_PARAMS). 202 | 203 | Пример:: 204 | 205 | from robokassa.signals import result_received 206 | from my_app.models import Order 207 | 208 | def payment_received(sender, **kwargs): 209 | order = Order.objects.get(id=kwargs['InvId']) 210 | order.status = 'paid' 211 | order.paid_sum = kwargs['OutSum'] 212 | order.extra_param = kwargs['extra']['my_param'] 213 | order.save() 214 | 215 | result_received.connect(payment_received) 216 | 217 | 218 | 219 | urls.py 220 | ------- 221 | 222 | Для настройки Result URL, Success URL и Fail URL можно подключить 223 | модуль robokassa.urls:: 224 | 225 | urlpatterns = patterns('', 226 | #... 227 | url(r'^robokassa/', include('robokassa.urls')), 228 | #... 229 | ) 230 | 231 | Адреса, которые нужно указывать в панели robokassa, в этом случае будут иметь вид 232 | 233 | * Result URL: ``http://yoursite.ru/robokassa/result/`` 234 | * Success URL: ``http://yoursite.ru/robokassa/success/`` 235 | * Fail URL: ``http://yoursite.ru/robokassa/fail/`` 236 | 237 | 238 | Шаблоны 239 | ------- 240 | 241 | * ``robokassa/success.html`` - показывается в случае успешной оплаты. В 242 | контексте есть переменная form типа ``SuccessRedirectForm``, InvId 243 | и OutSum с параметрами заказа, а также все дополнительные параметры, описанные 244 | в ROBOKASSA_EXTRA_PARAMS. 245 | 246 | * ``robokassa/fail.html`` - показывается в случае неуспешной оплаты. В 247 | контексте есть переменная form типа ``FailRedirectForm``, InvId 248 | и OutSum с параметрами заказа, а также все дополнительные параметры, описанные 249 | в ROBOKASSA_EXTRA_PARAMS. 250 | 251 | * ``robokassa/error.html`` - показывается при ошибочном запросе к странице 252 | "успех" или "неудача" (например, при ошибке в контрольной сумме). В контексте 253 | есть переменная form класса ``FailRedirectForm`` или ``SuccessRedirectForm``. 254 | 255 | Разработка 256 | ========== 257 | 258 | Оригинальная более неподдерживаемая версия библиотеки: https://github.com/kmike/django-robokassa 259 | Разработка ведётся на GitHub: https://github.com/mpyrev/django-robokassa 260 | 261 | Пожелания, идеи, баг-репорты и тд. пишите в трекер: https://github.com/mpyrev/django-robokassa/issues 262 | 263 | Лицензия - MIT. 264 | 265 | Тестирование 266 | ------------ 267 | 268 | Для запуска тестов установите `tox `_, склонируйте репозиторий 269 | и выполните команду 270 | 271 | :: 272 | 273 | $ tox 274 | 275 | из корня репозитория. 276 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python setup.py bdist 3 | python setup.py bdist_wheel --universal 4 | -------------------------------------------------------------------------------- /example_project/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpyrev/django-robokassa/f619ca61a205fd8eacadf82e51a55c68c2603449/example_project/app/__init__.py -------------------------------------------------------------------------------- /example_project/app/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /example_project/app/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class AppConfig(AppConfig): 8 | name = 'app' 9 | -------------------------------------------------------------------------------- /example_project/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-04-26 13:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Order', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=255)), 21 | ('total', models.DecimalField(decimal_places=2, max_digits=15)), 22 | ('status', models.CharField(blank=True, max_length=255, null=True)), 23 | ('paid_sum', models.DecimalField(blank=True, decimal_places=2, max_digits=15, null=True)), 24 | ('extra_param', models.CharField(blank=True, max_length=255, null=True)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /example_project/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpyrev/django-robokassa/f619ca61a205fd8eacadf82e51a55c68c2603449/example_project/app/migrations/__init__.py -------------------------------------------------------------------------------- /example_project/app/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | from django.dispatch import receiver 6 | from robokassa.signals import result_received 7 | 8 | 9 | class Order(models.Model): 10 | name = models.CharField(max_length=255) 11 | total = models.DecimalField(max_digits=15, decimal_places=2) 12 | status = models.CharField(max_length=255, blank=True, null=True) 13 | paid_sum = models.DecimalField(max_digits=15, decimal_places=2, blank=True, null=True) 14 | extra_param = models.CharField(max_length=255, blank=True, null=True) 15 | 16 | 17 | @receiver(result_received) 18 | def payment_received(sender, **kwargs): 19 | order = Order.objects.get(pk=kwargs['InvId']) 20 | order.status = 'paid' 21 | order.paid_sum = kwargs['OutSum'] 22 | order.extra_param = kwargs['extra']['my_param'] 23 | order.save() 24 | -------------------------------------------------------------------------------- /example_project/app/templates/app/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | {% block content %}{% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /example_project/app/templates/app/pay_with_robokassa.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% block content %} 4 |
5 |

{{ form.as_p }}

6 |

7 |
8 | {% endblock %} -------------------------------------------------------------------------------- /example_project/app/templates/robokassa/error.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% block title %} 4 | Ошибка на стороне сервера 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Ошибка на стороне сервера

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example_project/app/templates/robokassa/fail.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% block title %} 4 | Неудача 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Неудача

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example_project/app/templates/robokassa/success.html: -------------------------------------------------------------------------------- 1 | {% extends 'app/base.html' %} 2 | 3 | {% block title %} 4 | Успех 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

Успех

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example_project/app/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /example_project/app/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.shortcuts import get_object_or_404, render 5 | from django.contrib.auth.decorators import login_required 6 | from robokassa.forms import RobokassaForm 7 | 8 | from .models import Order 9 | 10 | 11 | @login_required 12 | def pay_with_robokassa(request, order_id): 13 | order = get_object_or_404(Order, pk=order_id) 14 | 15 | form = RobokassaForm(initial={ 16 | 'OutSum': order.total, 17 | 'InvId': order.id, 18 | 'Desc': order.name, 19 | 'Email': request.user.email, 20 | # 'IncCurrLabel': '', 21 | # 'Culture': 'ru' 22 | }) 23 | 24 | return render(request, 'app/pay_with_robokassa.html', {'form': form}) 25 | -------------------------------------------------------------------------------- /example_project/example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpyrev/django-robokassa/f619ca61a205fd8eacadf82e51a55c68c2603449/example_project/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/example_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'hl@couow8+w8sbk@)+#i!@*h^073p7z=@k7gjipu6v7h17y8az' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'robokassa', 42 | 'app', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'example_project.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'example_project.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | 125 | ROBOKASSA_LOGIN = 'test_login' 126 | ROBOKASSA_PASSWORD1 = 'test_password' 127 | ROBOKASSA_PASSWORD2 = 'test_password2' 128 | ROBOKASSA_EXTRA_PARAMS = ['param1', 'param2'] 129 | -------------------------------------------------------------------------------- /example_project/example_project/urls.py: -------------------------------------------------------------------------------- 1 | """example_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | from app import views 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', admin.site.urls), 23 | url(r'^robokassa/', include('robokassa.urls')), 24 | url(r'^pay/(?P\d+)/$', views.pay_with_robokassa), 25 | ] 26 | -------------------------------------------------------------------------------- /example_project/example_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Add our app to PYTHONPATH 6 | sys.path.append(os.path.join(os.path.dirname(__file__), '../')) 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError: 13 | # The above import may fail for some other reason. Ensure that the 14 | # issue is really that Django is missing to avoid masking other 15 | # exceptions on Python 2. 16 | try: 17 | import django 18 | except ImportError: 19 | raise ImportError( 20 | "Couldn't import Django. Are you sure it's installed and " 21 | "available on your PYTHONPATH environment variable? Did you " 22 | "forget to activate a virtual environment?" 23 | ) 24 | raise 25 | execute_from_command_line(sys.argv) 26 | -------------------------------------------------------------------------------- /robokassa/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpyrev/django-robokassa/f619ca61a205fd8eacadf82e51a55c68c2603449/robokassa/__init__.py -------------------------------------------------------------------------------- /robokassa/conf.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | 6 | # обязательные параметры - реквизиты магазина 7 | LOGIN = settings.ROBOKASSA_LOGIN 8 | PASSWORD1 = settings.ROBOKASSA_PASSWORD1 9 | PASSWORD2 = getattr(settings, 'ROBOKASSA_PASSWORD2', None) 10 | 11 | # использовать ли метод POST при приеме результатов 12 | USE_POST = getattr(settings, 'ROBOKASSA_USE_POST', True) 13 | 14 | # требовать предварительного уведомления на ResultURL 15 | STRICT_CHECK = getattr(settings, 'ROBOKASSA_STRICT_CHECK', True) 16 | 17 | # тестовый режим 18 | TEST_MODE = getattr(settings, 'ROBOKASSA_TEST_MODE', False) 19 | 20 | # url, по которому будет идти отправка форм 21 | FORM_TARGET = 'https://merchant.roboxchange.com/Index.aspx' 22 | 23 | if TEST_MODE: 24 | FORM_TARGET = getattr( 25 | settings, 26 | 'ROBOKASSA_TEST_FORM_TARGET', 27 | 'https://auth.robokassa.ru/Merchant/Index.aspx' 28 | ) 29 | 30 | # список пользовательских параметров ("shp" к ним приписывать не нужно) 31 | EXTRA_PARAMS = sorted(getattr(settings, 'ROBOKASSA_EXTRA_PARAMS', [])) 32 | -------------------------------------------------------------------------------- /robokassa/forms.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from hashlib import md5 5 | from six.moves.urllib.parse import urlencode 6 | 7 | import six 8 | from django import forms 9 | 10 | from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2, TEST_MODE, STRICT_CHECK, FORM_TARGET, EXTRA_PARAMS 11 | from robokassa.models import SuccessNotification 12 | 13 | 14 | class BaseRobokassaForm(forms.Form): 15 | def __init__(self, *args, **kwargs): 16 | super(BaseRobokassaForm, self).__init__(*args, **kwargs) 17 | # создаем дополнительные поля 18 | for key in EXTRA_PARAMS: 19 | self.fields['shp'+key] = forms.CharField(required=False) 20 | if 'initial' in kwargs: 21 | self.fields['shp'+key].initial = kwargs['initial'].get(key, 'None') 22 | 23 | def _append_extra_part(self, standard_part, value_func): 24 | extra_part = ":".join(["%s=%s" % ('shp'+key, value_func('shp' + key)) for key in EXTRA_PARAMS]) 25 | if extra_part: 26 | return ':'.join([standard_part, extra_part]) 27 | return standard_part 28 | 29 | def extra_params(self): 30 | extra = {} 31 | for param in EXTRA_PARAMS: 32 | if ('shp'+param) in self.cleaned_data: 33 | extra[param] = self.cleaned_data['shp'+param] 34 | return extra 35 | 36 | def _get_signature(self): 37 | return md5(self._get_signature_string().encode('utf-8')).hexdigest().upper() 38 | 39 | def _get_signature_string(self): 40 | raise NotImplementedError 41 | 42 | 43 | class RobokassaForm(BaseRobokassaForm): 44 | # login магазина в обменном пункте 45 | MrchLogin = forms.CharField(max_length=20, initial=LOGIN) 46 | 47 | # требуемая к получению сумма 48 | OutSum = forms.DecimalField(min_value=0, max_digits=20, decimal_places=2, required=False) 49 | 50 | # номер счета в магазине (должен быть уникальным для магазина) 51 | InvId = forms.IntegerField(min_value=0, required=False) 52 | 53 | # описание покупки 54 | Desc = forms.CharField(max_length=100, required=False) 55 | 56 | # контрольная сумма MD5 57 | SignatureValue = forms.CharField(max_length=32) 58 | 59 | # предлагаемая валюта платежа 60 | IncCurrLabel = forms.CharField(max_length=10, required=False) 61 | 62 | # e-mail пользователя 63 | Email = forms.CharField(max_length=100, required=False) 64 | 65 | # язык общения с клиентом (en или ru) 66 | Culture = forms.CharField(max_length=10, required=False) 67 | 68 | # Параметр с URL'ом, на который форма должны быть отправлена. 69 | # Может пригодиться для использования в шаблоне. 70 | target = FORM_TARGET 71 | 72 | def __init__(self, *args, **kwargs): 73 | 74 | super(RobokassaForm, self).__init__(*args, **kwargs) 75 | 76 | if TEST_MODE is True: 77 | self.fields['isTest'] = forms.BooleanField(required=False) 78 | self.fields['isTest'].initial = 1 79 | 80 | # скрытый виджет по умолчанию 81 | for field in self.fields: 82 | self.fields[field].widget = forms.HiddenInput() 83 | 84 | self.fields['SignatureValue'].initial = self._get_signature() 85 | 86 | def get_redirect_url(self): 87 | """ Получить URL с GET-параметрами, соответствующими значениям полей в 88 | форме. Редирект на адрес, возвращаемый этим методом, эквивалентен 89 | ручной отправке формы методом GET. 90 | """ 91 | def _initial(name, field): 92 | val = self.initial.get(name, field.initial) 93 | if not val: 94 | return val 95 | return six.text_type(val).encode('1251') 96 | 97 | fields = [(name, _initial(name, field)) 98 | for name, field in list(self.fields.items()) 99 | if _initial(name, field) 100 | ] 101 | params = urlencode(fields) 102 | return self.target+'?'+params 103 | 104 | def _get_signature_string(self): 105 | def _val(name): 106 | value = self.initial[name] if name in self.initial else self.fields[name].initial 107 | if value is None: 108 | return '' 109 | return six.text_type(value) 110 | standard_part = ':'.join([_val('MrchLogin'), _val('OutSum'), _val('InvId'), PASSWORD1]) 111 | return self._append_extra_part(standard_part, _val) 112 | 113 | 114 | class ResultURLForm(BaseRobokassaForm): 115 | """Форма для приема результатов и проверки контрольной суммы""" 116 | OutSum = forms.CharField(max_length=15) 117 | InvId = forms.IntegerField(min_value=0) 118 | SignatureValue = forms.CharField(max_length=32) 119 | 120 | def clean(self): 121 | try: 122 | signature = self.cleaned_data['SignatureValue'].upper() 123 | if signature != self._get_signature(): 124 | raise forms.ValidationError('Ошибка в контрольной сумме') 125 | except KeyError: 126 | raise forms.ValidationError('Пришли не все необходимые параметры') 127 | 128 | return self.cleaned_data 129 | 130 | def _get_signature_string(self): 131 | _val = lambda name: six.text_type(self.cleaned_data[name]) 132 | standard_part = ':'.join([_val('OutSum'), _val('InvId'), PASSWORD2]) 133 | return self._append_extra_part(standard_part, _val) 134 | 135 | 136 | class _RedirectPageForm(ResultURLForm): 137 | """Форма для проверки контрольной суммы на странице Success""" 138 | 139 | Culture = forms.CharField(max_length=10) 140 | 141 | def _get_signature_string(self): 142 | _val = lambda name: six.text_type(self.cleaned_data[name]) 143 | standard_part = ':'.join([_val('OutSum'), _val('InvId'), PASSWORD1]) 144 | return self._append_extra_part(standard_part, _val) 145 | 146 | 147 | class SuccessRedirectForm(_RedirectPageForm): 148 | """Форма для обработки страницы Success с дополнительной защитой. Она 149 | проверяет, что ROBOKASSA предварительно уведомила систему о платеже, 150 | отправив запрос на ResultURL.""" 151 | 152 | def clean(self): 153 | data = super(SuccessRedirectForm, self).clean() 154 | if STRICT_CHECK: 155 | if not SuccessNotification.objects.filter(InvId=data['InvId']): 156 | raise forms.ValidationError('От ROBOKASSA не было предварительного уведомления') 157 | return data 158 | 159 | 160 | class FailRedirectForm(BaseRobokassaForm): 161 | """Форма приема результатов для перенаправления на страницу Fail""" 162 | OutSum = forms.CharField(max_length=15) 163 | InvId = forms.IntegerField(min_value=0) 164 | Culture = forms.CharField(max_length=10) 165 | -------------------------------------------------------------------------------- /robokassa/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-04-26 12:33 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='SuccessNotification', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('InvId', models.IntegerField(db_index=True, verbose_name='\u041d\u043e\u043c\u0435\u0440 \u0437\u0430\u043a\u0430\u0437\u0430')), 21 | ('OutSum', models.CharField(max_length=15, verbose_name='\u0421\u0443\u043c\u043c\u0430')), 22 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='\u0414\u0430\u0442\u0430 \u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f')), 23 | ], 24 | options={ 25 | 'verbose_name': '\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e\u0431 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u043c \u043f\u043b\u0430\u0442\u0435\u0436\u0435', 26 | 'verbose_name_plural': '\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e\u0431 \u0443\u0441\u043f\u0435\u0448\u043d\u044b\u0445 \u043f\u043b\u0430\u0442\u0435\u0436\u0430\u0445 (ROBOKASSA)', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /robokassa/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpyrev/django-robokassa/f619ca61a205fd8eacadf82e51a55c68c2603449/robokassa/migrations/__init__.py -------------------------------------------------------------------------------- /robokassa/models.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | from django.utils.six import python_2_unicode_compatible 6 | 7 | 8 | @python_2_unicode_compatible 9 | class SuccessNotification(models.Model): 10 | InvId = models.IntegerField('Номер заказа', db_index=True) 11 | OutSum = models.CharField('Сумма', max_length=15) 12 | 13 | created_at = models.DateTimeField('Дата и время получения уведомления', auto_now_add=True) 14 | 15 | class Meta: 16 | verbose_name = 'Уведомление об успешном платеже' 17 | verbose_name_plural = 'Уведомления об успешных платежах (ROBOKASSA)' 18 | 19 | def __str__(self): 20 | return '#{}: {} ({})'.format(self.InvId, self.OutSum, self.created_at) 21 | -------------------------------------------------------------------------------- /robokassa/signals.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.dispatch import Signal 5 | 6 | 7 | result_received = Signal(providing_args=["InvId", "OutSum"]) 8 | success_page_visited = Signal(providing_args=["InvId", "OutSum"]) 9 | fail_page_visited = Signal(providing_args=["InvId", "OutSum"]) 10 | -------------------------------------------------------------------------------- /robokassa/tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | from django.test import TestCase as DjangoTestCase 6 | from robokassa.forms import RobokassaForm, ResultURLForm 7 | from robokassa.conf import LOGIN, PASSWORD1, PASSWORD2 8 | 9 | 10 | class RobokassaFormTest(TestCase): 11 | 12 | def setUp(self): 13 | self.form = RobokassaForm(initial={ 14 | 'OutSum': 100.00, 15 | 'InvId': 58, 16 | 'Desc' : 'Холодильник "Бирюса"', 17 | 'Email' : 'vasia@example.com' 18 | }) 19 | 20 | def testSignature(self): 21 | self.assertEqual(self.form._get_signature_string(), 22 | '%s:100.0:58:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1)) 23 | self.assertEqual(len(self.form.fields['SignatureValue'].initial), 32) 24 | 25 | def testSignatureMissingParams(self): 26 | form = RobokassaForm(initial = {'InvId': 5}) 27 | self.assertEqual(form._get_signature_string(), 28 | '%s::5:%s:shpparam1=None:shpparam2=None' % (LOGIN, PASSWORD1)) 29 | 30 | def testRedirectUrl(self): 31 | url = "https://merchant.roboxchange.com/Index.aspx?MrchLogin=test_login&OutSum=100.0&InvId=58&Desc=%D5%EE%EB%EE%E4%E8%EB%FC%ED%E8%EA+%22%C1%E8%F0%FE%F1%E0%22&SignatureValue=0EC23BE40003640B35EC07F6615FFB57&Email=vasia%40example.com&shpparam1=None&shpparam2=None" 32 | self.assertEqual(self.form.get_redirect_url(), url) 33 | 34 | 35 | class RobokassaFormExtraTest(TestCase): 36 | def testExtra(self): 37 | form = RobokassaForm(initial={ 38 | 'InvId': 58, 39 | 'OutSum': 100, 40 | 'param1': 'value1', 41 | 'param2': 'value2' 42 | }) 43 | self.assertEqual(form._get_signature_string(), 44 | '%s:100:58:%s:shpparam1=value1:shpparam2=value2' % (LOGIN, PASSWORD1)) 45 | 46 | 47 | class ResultURLTest(DjangoTestCase): 48 | 49 | def testFormExtra(self): 50 | form = ResultURLForm({ 51 | 'OutSum': '100', 52 | 'InvId': '58', 53 | 'SignatureValue': 'B2111A06F6B7A1E090D38367BF7032D9', 54 | 'shpparam1': 'Vasia', 55 | 'shpparam2': 'None', 56 | }) 57 | self.assertTrue(form.is_valid()) 58 | self.assertEqual(form._get_signature_string(), 59 | '100:58:%s:shpparam1=Vasia:shpparam2=None' % (PASSWORD2)) 60 | self.assertEqual(form.extra_params(), {'param1': 'Vasia', 'param2': 'None'}) 61 | 62 | 63 | def testFormValid(self): 64 | 65 | self.assertTrue(ResultURLForm({ 66 | 'OutSum': '100', 67 | 'InvId': '58', 68 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', 69 | 'shpparam1': 'None', 70 | 'shpparam2': 'None', 71 | }).is_valid()) 72 | 73 | self.assertFalse(ResultURLForm({ 74 | 'OutSum': '101', 75 | 'InvId': '58', 76 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', 77 | 'shpparam1': 'None', 78 | 'shpparam2': 'None', 79 | }).is_valid()) 80 | 81 | self.assertFalse(ResultURLForm({ 82 | 'OutSum': '100', 83 | 'InvId': '58', 84 | 'SignatureValue': '877D3BAF8381F70E56638C3BC82580C5', 85 | 'shpparam1': 'Vasia', 86 | 'shpparam2': 'None', 87 | }).is_valid()) 88 | 89 | def testEmptyFormValid(self): 90 | self.assertFalse(ResultURLForm().is_valid()) 91 | -------------------------------------------------------------------------------- /robokassa/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.conf.urls import url 5 | 6 | from . import views 7 | 8 | app_name = 'robokassa' 9 | 10 | urlpatterns = [ 11 | url(r'^result/$', views.receive_result, name='result'), 12 | url(r'^success/$', views.success, name='success'), 13 | url(r'^fail/$', views.fail, name='fail'), 14 | ] 15 | -------------------------------------------------------------------------------- /robokassa/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.http import HttpResponse 5 | from django.template.response import TemplateResponse 6 | from django.views.decorators.csrf import csrf_exempt 7 | 8 | from robokassa.conf import USE_POST 9 | from robokassa.forms import ResultURLForm, SuccessRedirectForm, FailRedirectForm 10 | from robokassa.models import SuccessNotification 11 | from robokassa.signals import result_received, success_page_visited, fail_page_visited 12 | 13 | 14 | @csrf_exempt 15 | def receive_result(request): 16 | """Обработчик для ResultURL.""" 17 | data = request.POST if USE_POST else request.GET 18 | form = ResultURLForm(data) 19 | if form.is_valid(): 20 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum'] 21 | 22 | # сохраняем данные об успешном уведомлении в базе, чтобы 23 | # можно было выполнить дополнительную проверку на странице успешного 24 | # заказа 25 | notification = SuccessNotification.objects.create(InvId=inv_id, OutSum=out_sum) 26 | 27 | # дополнительные действия с заказом (например, смену его статуса) можно 28 | # осуществить в обработчике сигнала robokassa.signals.result_received 29 | result_received.send(sender=notification, InvId=inv_id, OutSum=out_sum, 30 | extra=form.extra_params()) 31 | 32 | return HttpResponse('OK%s' % inv_id) 33 | return HttpResponse('error: bad signature') 34 | 35 | 36 | @csrf_exempt 37 | def success(request, template_name='robokassa/success.html', extra_context=None, 38 | error_template_name='robokassa/error.html'): 39 | """Обработчик для SuccessURL""" 40 | 41 | data = request.POST if USE_POST else request.GET 42 | form = SuccessRedirectForm(data) 43 | if form.is_valid(): 44 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum'] 45 | 46 | # в случае, когда не используется строгая проверка, действия с заказом 47 | # можно осуществлять в обработчике сигнала robokassa.signals.success_page_visited 48 | success_page_visited.send(sender=form, InvId=inv_id, OutSum=out_sum, 49 | extra=form.extra_params()) 50 | 51 | context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form} 52 | context.update(form.extra_params()) 53 | context.update(extra_context or {}) 54 | return TemplateResponse(request, template_name, context) 55 | 56 | return TemplateResponse(request, error_template_name, {'form': form}) 57 | 58 | 59 | @csrf_exempt 60 | def fail(request, template_name='robokassa/fail.html', extra_context=None, 61 | error_template_name='robokassa/error.html'): 62 | """Обработчик для FailURL""" 63 | 64 | data = request.POST if USE_POST else request.GET 65 | form = FailRedirectForm(data) 66 | if form.is_valid(): 67 | inv_id, out_sum = form.cleaned_data['InvId'], form.cleaned_data['OutSum'] 68 | 69 | # дополнительные действия с заказом (например, смену его статуса для 70 | # разблокировки товара на складе) можно осуществить в обработчике 71 | # сигнала robokassa.signals.fail_page_visited 72 | fail_page_visited.send(sender=form, InvId=inv_id, OutSum=out_sum, 73 | extra=form.extra_params()) 74 | 75 | context = {'InvId': inv_id, 'OutSum': out_sum, 'form': form} 76 | context.update(form.extra_params()) 77 | context.update(extra_context or {}) 78 | return TemplateResponse(request, template_name, context) 79 | 80 | return TemplateResponse(request, error_template_name, {'form': form}) 81 | 82 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | 5 | import django 6 | from django.conf import settings 7 | from django.core.management import call_command 8 | 9 | settings.configure( 10 | INSTALLED_APPS=('robokassa',), 11 | DATABASE_ENGINE='sqlite3', 12 | DATABASES={ 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | }, 16 | }, 17 | 18 | ROBOKASSA_LOGIN='test_login', 19 | ROBOKASSA_PASSWORD1='test_password', 20 | ROBOKASSA_PASSWORD2='test_password2', 21 | ROBOKASSA_EXTRA_PARAMS=['param1', 'param2'], 22 | ) 23 | 24 | django.setup() 25 | 26 | if __name__ == "__main__": 27 | call_command('test', 'robokassa') 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | from __future__ import unicode_literals 4 | 5 | from setuptools import setup 6 | 7 | 8 | setup( 9 | name='django-robokassa3', 10 | version='1.4', 11 | author='Mikhail Pyrev', 12 | author_email='mikhail.pyrev@gmail.com', 13 | 14 | packages=['robokassa', 'robokassa.migrations'], 15 | 16 | url='https://github.com/mpyrev/django-robokassa', 17 | license='MIT License', 18 | description='Приложение для интеграции платежной системы ROBOKASSA в проекты на Django.', 19 | long_description=open('README.rst').read() + "\n\n" + open('CHANGES.rst').read(), 20 | 21 | install_requires=[ 22 | 'Django>=1.11', 23 | 'six', 24 | ], 25 | 26 | classifiers=( 27 | 'Environment :: Web Environment', 28 | 'Framework :: Django', 29 | 'Framework :: Django :: 1.11', 30 | 'Framework :: Django :: 2.0', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Topic :: Software Development :: Libraries :: Python Modules', 42 | 'Natural Language :: Russian', 43 | ), 44 | ) 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,pypy,dj13,dj15,dj16 3 | 4 | [testenv] 5 | deps= 6 | django >= 1.4, < 1.5 7 | south 8 | 9 | commands= 10 | python runtests.py 11 | 12 | [testenv:dj13] 13 | deps = 14 | django >= 1.3, < 1.4 15 | south 16 | 17 | [testenv:dj15] 18 | deps = 19 | django >= 1.5, < 1.6 20 | south 21 | 22 | [testenv:dj16] 23 | deps = 24 | django >= 1.6, < 1.7 25 | south 26 | --------------------------------------------------------------------------------