Compare commits

...

74 Commits

Author SHA1 Message Date
Frédéric Péters 43820628d0 debian: conflicts with standalone lingo package 2015-12-25 21:22:34 +01:00
Frédéric Péters 90171fd0ef lingo: import and adapt tests from standalone module 2015-12-25 21:22:33 +01:00
Frédéric Péters 14657cb7e2 lingo: insert into global MANIFEST.in 2015-12-25 21:22:33 +01:00
Frédéric Péters 6cb1097b75 lingo: update module references to match after-merge location 2015-12-25 21:18:19 +01:00
Frédéric Péters b3f68df240 lingo: import README 2015-12-25 21:18:19 +01:00
Serghei Mihai 006fa51b4a lingo: round amount computed from query string (#9174) 2015-12-25 21:18:19 +01:00
Serghei Mihai b130f0298a lingo: include query string in amount computation (#9174) 2015-12-25 21:18:19 +01:00
Serghei Mihai a2fe92d59d lingo: add transaction id and date to invoice payment notification (#9280)
Send data through POST
2015-12-25 21:18:19 +01:00
Serghei Mihai 1b6b570981 lingo: handle callback calls with POST method (#9360) 2015-12-25 21:18:19 +01:00
Serghei Mihai a33f6d4200 lingo: compute item amount from payload and its optional extra attribute (#9174) 2015-12-25 21:18:19 +01:00
Frédéric Péters 5bbd0193cf lingo: add a new manager page to list recent transactions (#9252) 2015-12-25 21:18:19 +01:00
Frédéric Péters e177ce4946 lingo: mention service name within the link 2015-12-25 21:18:19 +01:00
Serghei Mihai 3dddbdc142 lingo: store items total amount in transaction (#9259) 2015-12-25 21:18:19 +01:00
Frédéric Péters e5cfb178e4 lingo: use <h2> titles in cells (#9041) 2015-12-25 21:18:18 +01:00
Frédéric Péters 928a9b3647 lingo: use i18n in payment_form template (#9046) 2015-12-25 21:18:18 +01:00
Frédéric Péters 1a08a07c7b lingo: protect "basket link cell" against anonymous ajax calls (#9045) 2015-12-25 21:18:18 +01:00
Serghei Mihai d551a21475 lingo: add payzen backend (#9025) 2015-12-25 21:18:18 +01:00
Serghei Mihai 43630cfdec lingo: url typo fix 2015-12-25 21:18:18 +01:00
Frédéric Péters 546a937dff lingo: update migrations for new restricted_to_unlogged_users field (#8641) 2015-12-25 21:18:18 +01:00
Serghei Mihai b5d6c6b4cb lingo: notify remote invoices payment only if available in transaction (#8644) 2015-12-25 21:18:18 +01:00
Serghei Mihai e9102a2b20 lingo: distinguish machine and human payment return urls (#8350)
Tests added
2015-12-25 21:18:18 +01:00
Benjamin Dauvergne 017dfb0789 lingo: fix breadcrumb (fixes #8645) 2015-12-25 21:18:18 +01:00
Serghei Mihai b644de0cad lingo: fix payment link display (#8511) 2015-12-25 21:18:18 +01:00
Serghei Mihai 403cc51741 lingo: display payment link only for online payable invoices (#8511) 2015-12-25 21:18:18 +01:00
Serghei Mihai 96be8c4f6d lingo: update french localizations 2015-12-25 21:18:18 +01:00
Serghei Mihai 44ace86186 lingo: define minimal payment amount per regie (#8511) 2015-12-25 21:18:18 +01:00
Serghei Mihai 39926ba4f5 lingo: display invoice payment limit date (#8422) 2015-12-25 21:18:18 +01:00
Serghei Mihai df0a09f23d lingo: 'view and pay' link if invoice online payable (#8422) 2015-12-25 21:18:18 +01:00
Serghei Mihai 3ffcc1c941 lingo: update item view template (#8352) 2015-12-25 21:18:18 +01:00
Serghei Mihai 0e1fb7dc68 lingo: update french localizations (#8317) 2015-12-25 21:18:18 +01:00
Serghei Mihai 721c813513 lingo: fix typo in item template 2015-12-25 21:18:18 +01:00
Serghei Mihai 993f576622 lingo: check invoice online payment attribute 2015-12-25 21:18:18 +01:00
Serghei Mihai dc636e1926 lingo: remove rows styles of items table (#8210) 2015-12-25 21:18:18 +01:00
Serghei Mihai 2c4c7bce33 lingo: return empty items list if user not logged in (#8309) 2015-12-25 21:18:17 +01:00
Serghei Mihai 4272039a3f lingo: transaction handles remote items 2015-12-25 21:18:17 +01:00
Serghei Mihai 82a7b81f03 lingo: item details view (#8221) 2015-12-25 21:18:17 +01:00
Serghei Mihai 9a63364d5c lingo: item pdf download view (#8220) 2015-12-25 21:18:17 +01:00
Serghei Mihai d078408b57 lingo: active and history items cells (#7994) 2015-12-25 21:18:17 +01:00
Serghei Mihai c0945978ad lingo: provide user email payment backend (#8038) 2015-12-25 21:18:17 +01:00
Serghei Mihai 38b2e40194 lingo: add paybox payment service (#8032) 2015-12-25 21:18:17 +01:00
Frédéric Péters 654982d778 lingo: allow passing multiple amounts (#7471) 2015-12-25 21:18:17 +01:00
Frédéric Péters 8188b36d38 lingo: lookup user from NameID using new mellon object 2015-12-25 21:18:17 +01:00
Frédéric Péters 48938c98f5 lingo: add missing basket link template 2015-12-25 21:18:17 +01:00
Frédéric Péters 20ec4fa1ff lingo: add a cell to be a short link to the basket 2015-12-25 21:18:17 +01:00
Frédéric Péters b8e1e2e03d lingo: fix testing environment where no user is available 2015-12-25 21:18:17 +01:00
Frédéric Péters 0a12ffdb67 lingo: declare new URLs and actions via django.apps (#6979) 2015-12-25 21:18:17 +01:00
Frédéric Péters 16692b692b lingo: don't display cells to anonymous users 2015-12-25 21:18:17 +01:00
Frédéric Péters 9c29693dcd lingo: track transaction status, handle cancellations (#6999) 2015-12-25 21:18:17 +01:00
Frédéric Péters 81dc29d045 lingo: declare success url for regie management views 2015-12-25 21:18:17 +01:00
Frédéric Péters 11725f31da lingo: register manager page into combo (#6702) 2015-12-25 21:18:17 +01:00
Frédéric Péters 7378aeaf88 lingo: add basic management of regies (#6702) 2015-12-25 21:18:17 +01:00
Frédéric Péters ec8b79d98e lingo: add French translations 2015-12-25 21:18:17 +01:00
Frédéric Péters f7fca2f99a lingo: hide combo cell types if no regie are defined 2015-12-25 21:18:16 +01:00
Frédéric Péters eda81a82f8 lingo: remove unused imports 2015-12-25 21:18:16 +01:00
Frédéric Péters 839fe0686a lingo: add a "recent transactions" combo cell 2015-12-25 21:18:16 +01:00
Frédéric Péters ad80027ad5 lingo: use timezone.now() instead of datetime.now() 2015-12-25 21:18:16 +01:00
Frédéric Péters 3a19ec0e01 lingo: notify wcs synchronously for now, with a short timeout 2015-12-25 21:18:16 +01:00
Frédéric Péters a1812cd48b lingo: notify wcs of paid items 2015-12-25 21:18:16 +01:00
Frédéric Péters a857abb8d8 lingo: handle posting form and receiving callbacks 2015-12-25 21:18:16 +01:00
Frédéric Péters 6cd32e39b5 lingo: declare ingenico (ogone) service 2015-12-25 21:18:16 +01:00
Frédéric Péters 1d42ebc729 lingo: send request to payment service 2015-12-25 21:18:16 +01:00
Frédéric Péters e11ef16656 lingo: add initial migration 2015-12-25 21:18:16 +01:00
Frédéric Péters 98100a5d20 lingo: turn basket into a form 2015-12-25 21:18:16 +01:00
Frédéric Péters d13d62dd73 lingo: fill basket item with details from request 2015-12-25 21:18:16 +01:00
Frédéric Péters fa45b9b6ab lingo: add i18n to basket template 2015-12-25 21:18:16 +01:00
Frédéric Péters 6bed69145a lingo: mark itself as irrelevant if the basket is empty 2015-12-25 21:18:16 +01:00
Frédéric Péters 0932cbcd6a lingo: add "my basket" combo cell type (#6290) 2015-12-25 21:18:16 +01:00
Frédéric Péters 6c6c5c8635 lingo: add api to add items to basket, with fake values for now 2015-12-25 21:18:16 +01:00
Frédéric Péters 2e510a06e2 lingo: add basketitem model 2015-12-25 21:18:16 +01:00
Frédéric Péters e24b7bac2b lingo: add api view to get list of regies 2015-12-25 21:18:16 +01:00
Frédéric Péters ba24ce9bc8 lingo: add simple model for regies 2015-12-25 21:18:16 +01:00
Frédéric Péters 81a940367d lingo: don't use migrations system for the moment 2015-12-25 21:18:15 +01:00
Frédéric Péters 29c9abc5a2 lingo: hook into combo plugin system 2015-12-25 21:18:15 +01:00
Frédéric Péters b5ceeea21c lingo: start as a standalone module
Lingo was started as a standalone module and the commits where later crafted
onto the combo repository.
2015-12-25 21:18:08 +01:00
40 changed files with 2177 additions and 0 deletions

View File

@ -1,5 +1,6 @@
# locales
recursive-include combo/locale *.po *.mo
recursive-include combo/apps/lingo/locale *.po *.mo
# static
recursive-include combo/apps/dataviz/static *.css *.js *.ico *.gif *.png *.jpg
@ -10,6 +11,7 @@ recursive-include data/themes *.css *.js *.gif *.png *.jpg *.jpeg *.html
# templates
recursive-include combo/apps/dataviz/templates *.html
recursive-include combo/apps/momo/templates *.html
recursive-include combo/apps/lingo/templates *.html
recursive-include combo/apps/wcs/templates *.html
recursive-include combo/apps/family/templates *.html
recursive-include combo/manager/templates *.html

45
combo/apps/lingo/README Normal file
View File

@ -0,0 +1,45 @@
Lingo
=====
Lingo is an extension module for the combo content management system, adding
basket and payment features.
License
-------
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/>.
API
---
Items amount can be added to basket through API by posting to
*/api/lingo/add-basket-item* endpoint a json payload containing *amount* and/or
*"extra": {"amount": ...}* attribute or passing *amount* in the query string
For example:
{"display_name": "<item display name>",
"url": "http://<item url>",
"amount": "42.42",
"extra": {"amount": "10.42", ...},
...
}
or
/api/lingo/add-basket-item?amount=10
The "amount" attribute should be float or decimal or a list of
floats/decimals. For example:
{"amount": [14.12], "extra": {"amount": ["10.42", "5", "10"], ... }, ...}

View File

@ -0,0 +1,37 @@
# 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/>.
import django.apps
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
class Plugin(object):
def get_apps(self):
return [__name__]
class AppConfig(django.apps.AppConfig):
name = 'combo.apps.lingo'
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_extra_manager_actions(self):
return [{'href': reverse('lingo-manager-homepage'),
'text': _('Online Payment')}]
default_app_config = 'combo.apps.lingo.AppConfig'

25
combo/apps/lingo/admin.py Normal file
View File

@ -0,0 +1,25 @@
# 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 django.contrib import admin
from .models import Regie
class RegieAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('label',)}
admin.site.register(Regie, RegieAdmin)

View File

@ -0,0 +1,284 @@
# Linfo French Translation.
# Copyright (C) 2015 Entr'ouvert
# This file is distributed under the same license as the Lingo package.
# Frederic Peters <fpeters@entrouvert.com>, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: lingo 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-05 15:37+0000\n"
"PO-Revision-Date: 2015-03-10 11:05+0100\n"
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: lingo/__init__.py:35 lingo/templates/lingo/manager_base.html:10
msgid "Online Payment"
msgstr "Paiement en ligne"
#: lingo/models.py:43
msgid "Dummy (for tests)"
msgstr "Factice (pour tests)"
#: lingo/models.py:46
msgid "SP+ (Caisse d'epargne)"
msgstr "SP+ (Caisse d'épargne)"
#: lingo/models.py:47
msgid "Ingenico (formerly Ogone)"
msgstr "Ingenico (précédemment Ogone)"
#: lingo/models.py:48
msgid "Paybox"
msgstr ""
#: lingo/models.py:65 lingo/templates/lingo/combo/items.html:9
msgid "Label"
msgstr "Libellé"
#: lingo/models.py:67
msgid "Description"
msgstr "Description"
#: lingo/models.py:68
msgid "Payment Service"
msgstr "Service de paiement"
#: lingo/models.py:71
msgid "Payment Service Options"
msgstr "Options du service de paiement"
#: lingo/models.py:72
msgid "Webservice URL to retrieve remote items"
msgstr "Webservice de récuperation des factures distantes"
#: lingo/models.py:74
msgid "Minimal payment amount"
msgstr "Montant minimum de paiement"
#: lingo/models.py:81 lingo/models.py:306
msgid "Regie"
msgstr "Régie"
#: lingo/models.py:161
msgid "Subject"
msgstr "Objet"
#: lingo/models.py:162
msgid "Source URL"
msgstr "URL de source"
#: lingo/models.py:163
msgid "Details"
msgstr "Détails"
#: lingo/models.py:164 lingo/templates/lingo/combo/items.html:12
msgid "Amount"
msgstr "Montant(TTC)"
#: lingo/models.py:216
msgid "Running"
msgstr "En cours"
#: lingo/models.py:217
msgid "Paid"
msgstr "Payé"
#: lingo/models.py:218
msgid "Cancelled"
msgstr "Annulé"
#: lingo/models.py:219
msgid "Unknown"
msgstr "Inconnu"
#: lingo/models.py:226 lingo/templates/lingo/combo/basket.html:3
msgid "Basket"
msgstr "Panier"
#: lingo/models.py:253 lingo/templates/lingo/combo/recent_transactions.html:2
msgid "Recent Transactions"
msgstr "Transactions récentes"
#: lingo/models.py:282
msgid "Basket Link"
msgstr "Lien vers le Panier"
#: lingo/models.py:307
msgid "Title"
msgstr "Titre"
#: lingo/models.py:327
msgid "All"
msgstr "Tout"
#: lingo/models.py:357
msgid "Items History Cell"
msgstr "Cellule Historique des factures"
#: lingo/models.py:370
msgid "Active Items Cell"
msgstr "Cellule Factures actives"
#: lingo/templates/lingo/combo/basket.html:10
msgid "Total:"
msgstr "Total :"
#: lingo/templates/lingo/combo/basket.html:12
msgid "Pay selected items"
msgstr "Payer les éléments sélectionnés"
#: lingo/templates/lingo/combo/basket_link.html:3
msgid "Basket:"
msgstr "Panier :"
#: lingo/templates/lingo/combo/basket_link.html:3
msgid "items"
msgstr "factures"
#: lingo/templates/lingo/combo/item.html:5
#, python-format
msgid ""
"\n"
" Item nr. %(number)s\n"
" "
msgstr ""
"\n"
"Facture nr. %(number)s\n"
" "
#: lingo/templates/lingo/combo/item.html:12
#, python-format
msgid ""
"\n"
" Label: %(label)s\n"
" "
msgstr ""
"\n"
"Libellé : %(label)s\n"
" "
#: lingo/templates/lingo/combo/item.html:17
#, python-format
msgid ""
"\n"
" Total amount: %(amount)s€\n"
" "
msgstr ""
"\n"
"Montant total : %(amount)s€\n"
" "
#: lingo/templates/lingo/combo/item.html:23
#, python-format
msgid ""
"\n"
" Amount to pay: %(amount)s€\n"
" "
msgstr ""
"\n"
"Montant à régler : %(amount)s€\n"
" "
#: lingo/templates/lingo/combo/item.html:28
msgid "Issued on:"
msgstr "Émise le :"
#: lingo/templates/lingo/combo/item.html:30
msgid "Payed on:"
msgstr "Payée le :"
#: lingo/templates/lingo/combo/item.html:36
msgid "Pay"
msgstr "Payer"
#: lingo/templates/lingo/combo/items.html:8
msgid "Number"
msgstr "Numéro"
#: lingo/templates/lingo/combo/items.html:10
msgid "Issue date"
msgstr "Date d'émission"
#: lingo/templates/lingo/combo/items.html:11
msgid "Payment limit date"
msgstr "Date limite de paiement"
#: lingo/templates/lingo/combo/items.html:23
#, python-format
msgid ""
"\n"
" %(amount)s€\n"
" "
msgstr ""
#: lingo/templates/lingo/combo/items.html:28
msgid "View"
msgstr "Voir"
#: lingo/templates/lingo/combo/items.html:29
msgid "and pay"
msgstr "et payer"
#: lingo/templates/lingo/combo/items.html:33
msgid "Download"
msgstr "Télécharger"
#: lingo/templates/lingo/combo/items.html:41
msgid "No items yet"
msgstr "Aucune facture"
#: lingo/templates/lingo/manager_base.html:5
#: lingo/templates/lingo/regie_list.html:5
msgid "Regies"
msgstr "Régies"
#: lingo/templates/lingo/regie_confirm_delete.html:11
msgid "Are you sure you want to delete this?"
msgstr ""
#: lingo/templates/lingo/regie_confirm_delete.html:13
msgid "Confirm Deletion"
msgstr ""
#: lingo/templates/lingo/regie_confirm_delete.html:14
#: lingo/templates/lingo/regie_form.html:19
msgid "Cancel"
msgstr "Annuler"
#: lingo/templates/lingo/regie_form.html:6
msgid "Edit Regie"
msgstr "Editer une Régie"
#: lingo/templates/lingo/regie_form.html:8
msgid "New Regie"
msgstr "Nouvelle Régie"
#: lingo/templates/lingo/regie_form.html:18
msgid "Save"
msgstr "Sauvegarder"
#: lingo/templates/lingo/regie_form.html:21
msgid "Delete"
msgstr "Supprimer"
#: lingo/templates/lingo/regie_list.html:6
msgid "New"
msgstr "Nouvelle"
#: lingo/templates/lingo/regie_list.html:21
msgid ""
"\n"
" This site doesn't have any regie yet. Click on the \"New\" button in the "
"top\n"
" right of the page to add a first one.\n"
" "
msgstr ""
"\n"
"Ce site n'a pas encore de régie définie.Cliquez sur \"Nouvelle\" en haut à "
"droite pour en ajouter une.\n"
" "

View File

@ -0,0 +1,54 @@
# 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 django.core.urlresolvers import reverse_lazy
from django.http import HttpResponseRedirect
from django.views.generic import (CreateView, UpdateView, ListView,
DeleteView, TemplateView)
import eopayment
from .models import Regie, Transaction
class RegieListView(ListView):
model = Regie
class RegieCreateView(CreateView):
model = Regie
success_url = reverse_lazy('lingo-manager-regie-list')
class RegieUpdateView(UpdateView):
model = Regie
success_url = reverse_lazy('lingo-manager-regie-list')
class RegieDeleteView(DeleteView):
model = Regie
success_url = reverse_lazy('lingo-manager-regie-list')
class TransactionListView(ListView):
model = Transaction
paginate_by = 10
def get_queryset(self):
return Transaction.objects.filter(status=eopayment.PAID).order_by('-start_date')
class ManagerHomeView(TemplateView):
template_name = 'lingo/manager_home.html'

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import jsonfield.fields
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('data', '0005_auto_20150226_0903'),
]
operations = [
migrations.CreateModel(
name='BasketItem',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('subject', models.CharField(max_length=64, verbose_name='Subject')),
('source_url', models.URLField(verbose_name='Source URL')),
('details', models.TextField(verbose_name='Details', blank=True)),
('amount', models.DecimalField(verbose_name='Amount', max_digits=8, decimal_places=2)),
('creation_date', models.DateTimeField(auto_now_add=True)),
('cancellation_date', models.DateTimeField(null=True)),
('payment_date', models.DateTimeField(null=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LingoBasketCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Basket',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Regie',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('label', models.CharField(max_length=64, verbose_name='Label')),
('slug', models.SlugField(unique=True)),
('description', models.TextField(verbose_name='Description')),
('service', models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)")])),
('service_options', jsonfield.fields.JSONField(default=dict, verbose_name='Payment Service Options', blank=True)),
],
options={
'verbose_name': 'Regie',
},
bases=(models.Model,),
),
migrations.AddField(
model_name='basketitem',
name='regie',
field=models.ForeignKey(to='lingo.Regie'),
preserve_default=True,
),
migrations.AddField(
model_name='basketitem',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('lingo', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('start_date', models.DateTimeField(auto_now_add=True)),
('end_date', models.DateTimeField(null=True)),
('bank_data', jsonfield.fields.JSONField(default=dict, blank=True)),
('order_id', models.CharField(max_length=200)),
('items', models.ManyToManyField(to='lingo.BasketItem', blank=True)),
],
options={
},
bases=(models.Model,),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0002_transaction'),
]
operations = [
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)')]),
preserve_default=True,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0003_auto_20150306_1047'),
]
operations = [
migrations.AddField(
model_name='basketitem',
name='notification_date',
field=models.DateTimeField(null=True),
preserve_default=True,
),
]

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('data', '0005_auto_20150226_0903'),
('lingo', '0004_basketitem_notification_date'),
]
operations = [
migrations.CreateModel(
name='LingoRecentTransactionsCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Recent Transactions',
},
bases=(models.Model,),
),
migrations.AddField(
model_name='transaction',
name='user',
field=models.ForeignKey(default=0, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0005_auto_20150307_1242'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='status',
field=models.IntegerField(null=True),
preserve_default=True,
),
]

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
('data', '0005_auto_20150226_0903'),
('lingo', '0006_transaction_status'),
]
operations = [
migrations.CreateModel(
name='LingoBasketLinkCell',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Basket Link',
},
bases=(models.Model,),
),
]

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import ckeditor.fields
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
('data', '0010_feedcell'),
('lingo', '0007_lingobasketlinkcell'),
]
operations = [
migrations.CreateModel(
name='ActiveItems',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
('title', ckeditor.fields.RichTextField(verbose_name='Title', blank=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Active Items Cell',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ItemsHistory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('placeholder', models.CharField(max_length=20)),
('order', models.PositiveIntegerField()),
('slug', models.SlugField(verbose_name='Slug', blank=True)),
('public', models.BooleanField(default=True, verbose_name='Public')),
('regie', models.CharField(max_length=50, verbose_name='Regie', blank=True)),
('title', ckeditor.fields.RichTextField(verbose_name='Title', blank=True)),
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
('page', models.ForeignKey(to='data.Page')),
],
options={
'verbose_name': 'Items History Cell',
},
bases=(models.Model,),
),
migrations.AddField(
model_name='regie',
name='webservice_url',
field=models.URLField(verbose_name='Webservice URL to retrieve remote items', blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox')]),
preserve_default=True,
),
]

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('lingo', '0008_auto_20150908_1538'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='regie',
field=models.ForeignKey(to='lingo.Regie', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='transaction',
name='remote_items',
field=models.CharField(default='', max_length=512),
preserve_default=False,
),
migrations.AlterField(
model_name='transaction',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True),
preserve_default=True,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0009_auto_20150917_1456'),
]
operations = [
migrations.AddField(
model_name='regie',
name='payment_min_amount',
field=models.DecimalField(default=0, verbose_name='Minimal payment amount', max_digits=7, decimal_places=2),
preserve_default=True,
),
]

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0010_regie_payment_min_amount'),
]
operations = [
migrations.AddField(
model_name='activeitems',
name='restricted_to_unlogged',
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
preserve_default=True,
),
migrations.AddField(
model_name='itemshistory',
name='restricted_to_unlogged',
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
preserve_default=True,
),
migrations.AddField(
model_name='lingobasketcell',
name='restricted_to_unlogged',
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
preserve_default=True,
),
migrations.AddField(
model_name='lingobasketlinkcell',
name='restricted_to_unlogged',
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
preserve_default=True,
),
migrations.AddField(
model_name='lingorecenttransactionscell',
name='restricted_to_unlogged',
field=models.BooleanField(default=False, verbose_name='Restrict to unlogged users'),
preserve_default=True,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0011_auto_20151029_1535'),
]
operations = [
migrations.AlterField(
model_name='regie',
name='service',
field=models.CharField(max_length=64, verbose_name='Payment Service', choices=[(b'dummy', 'Dummy (for tests)'), (b'systempayv2', b'systempay (Banque Populaire)'), (b'sips', b'SIPS'), (b'spplus', "SP+ (Caisse d'epargne)"), (b'ogone', 'Ingenico (formerly Ogone)'), (b'paybox', 'Paybox'), (b'payzen', 'PayZen')]),
preserve_default=True,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('lingo', '0012_auto_20151130_1047'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='amount',
field=models.DecimalField(default=0, max_digits=7, decimal_places=2),
preserve_default=True,
),
]

View File

383
combo/apps/lingo/models.py Normal file
View File

@ -0,0 +1,383 @@
# 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/>.
import datetime
import json
import requests
import urllib
from dateutil import parser
from decimal import Decimal
import eopayment
from jsonfield import JSONField
from django import template
from django.conf import settings
from django.db import models
from django.forms import models as model_forms, Select
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.core.exceptions import PermissionDenied
from ckeditor.fields import RichTextField
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from combo.utils import NothingInCacheException, sign_url
SERVICES = [
(eopayment.DUMMY, _('Dummy (for tests)')),
(eopayment.SYSTEMPAY, 'systempay (Banque Populaire)'),
(eopayment.SIPS, 'SIPS'),
(eopayment.SPPLUS, _('SP+ (Caisse d\'epargne)')),
(eopayment.OGONE, _('Ingenico (formerly Ogone)')),
(eopayment.PAYBOX, _('Paybox')),
(eopayment.PAYZEN, _('PayZen')),
]
def build_remote_item(data, regie):
return RemoteItem(id=data.get('id'), regie=regie,
creation_date=data['created'],
payment_limit_date=data['pay_limit_date'],
display_id=data['display_id'],
total_amount=data.get('total_amount'),
amount=data.get('amount'),
subject=data.get('label'),
has_pdf=data.get('has_pdf'),
online_payment=data.get('online_payment'))
class Regie(models.Model):
label = models.CharField(verbose_name=_('Label'), max_length=64)
slug = models.SlugField(unique=True)
description = models.TextField(verbose_name=_('Description'))
service = models.CharField(verbose_name=_('Payment Service'),
max_length=64, choices=SERVICES)
service_options = JSONField(blank=True,
verbose_name=_('Payment Service Options'))
webservice_url = models.URLField(_('Webservice URL to retrieve remote items'),
blank=True)
payment_min_amount = models.DecimalField(_('Minimal payment amount'),
max_digits=7, decimal_places=2, default=0)
def is_remote(self):
return self.webservice_url != ''
class Meta:
verbose_name = _('Regie')
def natural_key(self):
return (self.slug,)
def __unicode__(self):
return self.label
def get_past_items(self, context):
"""
returns past items
"""
return self.get_items(context, past=True)
def get_items(self, context, past=False):
"""
returns current or past items
"""
if not self.is_remote():
payed = not past
return self.basketitem_set.filter(payment_date__isnull=payed,
user=context.get('user'))
if context.get('user'):
if context.get('request') and hasattr(context['request'], 'session') and \
context['request'].session.get('mellon_session'):
mellon = context.get('request').session['mellon_session']
url = self.webservice_url + '/invoices/'
if past:
url += 'history'
items = requests.get(self.signed_url(context['request'], url,
NameID=mellon['name_id_content'])).json()
if items.get('data'):
return [build_remote_item(item, self) for item in items.get('data')]
return []
return []
def download_item(self, request, item_id):
"""
downloads item's file
"""
if self.is_remote():
if hasattr(request, 'session') and request.session.get('mellon_session'):
mellon = request.session.get('mellon_session')
url = self.webservice_url + '/invoice/%s/pdf' % item_id
return requests.get(self.signed_url(request, url,
NameID=mellon['name_id_content']))
raise PermissionDenied
def get_item(self, request, item):
if not self.is_remote():
return self.basketitem_set.get(pk=item)
if hasattr(request, 'session') and request.session.get('mellon_session'):
mellon = request.session.get('mellon_session')
url = self.webservice_url + '/invoice/%s/' % item
item = requests.get(self.signed_url(request, url,
NameID=mellon['name_id_content'])).json()
return build_remote_item(item.get('data'), self)
return {}
def pay_item(self, request, item, transaction_id, transaction_date):
url = self.webservice_url + '/invoice/pay/'
data = {'invoice_id': item, 'transaction_id': transaction_id,
'transaction_date': transaction_date.strftime('%Y-%m-%dT%H:%M:%S')}
headers = {'content-type': 'application/json'}
return requests.post(self.signed_url(request, url),
data=json.dumps(data), headers=headers).json()
def as_api_dict(self):
return {'slug': self.slug,
'label': self.label,
'description': self.description}
def signed_url(self, request, url, **params):
orig = request.get_host()
url += '?orig=' + orig +'&' + urllib.urlencode(params)
signature_key = settings.LINGO_SIGNATURE_KEY
return sign_url(url, key=signature_key)
class BasketItem(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
regie = models.ForeignKey(Regie)
subject = models.CharField(verbose_name=_('Subject'), max_length=64)
source_url = models.URLField(_('Source URL'))
details = models.TextField(verbose_name=_('Details'), blank=True)
amount = models.DecimalField(verbose_name=_('Amount'),
decimal_places=2, max_digits=8)
creation_date = models.DateTimeField(auto_now_add=True)
cancellation_date = models.DateTimeField(null=True)
payment_date = models.DateTimeField(null=True)
notification_date = models.DateTimeField(null=True)
def notify(self):
# TODO: sign with real values
url = self.source_url + 'jump/trigger/paid?email=trigger@localhost&orig=combo'
url = sign_url(url, key='xxx')
message = {'result': 'ok'}
r = requests.post(url, data=json.dumps(message), timeout=3)
self.notification_date = timezone.now()
self.save()
class RemoteItem(object):
def __init__(self, id, regie, creation_date, payment_limit_date,
total_amount, amount, display_id, subject, has_pdf,
online_payment):
self.id = id
self.regie = regie
self.creation_date = parser.parse(creation_date)
self.payment_limit_date = parser.parse(payment_limit_date)
self.total_amount = Decimal(total_amount)
self.amount = Decimal(amount)
self.display_id = display_id
self.subject = subject
self.has_pdf = has_pdf
self.online_payment = online_payment
class Transaction(models.Model):
regie = models.ForeignKey(Regie, null=True)
items = models.ManyToManyField(BasketItem, blank=True)
remote_items = models.CharField(max_length=512)
start_date = models.DateTimeField(auto_now_add=True)
end_date = models.DateTimeField(null=True)
bank_data = JSONField(blank=True)
order_id = models.CharField(max_length=200)
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
status = models.IntegerField(null=True)
amount = models.DecimalField(default=0, max_digits=7, decimal_places=2)
def is_remote(self):
return self.remote_items != ''
def is_paid(self):
return self.status == eopayment.PAID
def get_status_label(self):
return {
0: _('Running'),
eopayment.PAID: _('Paid'),
eopayment.CANCELLED: _('Cancelled'),
}.get(self.status) or _('Unknown')
@register_cell_class
class LingoBasketCell(CellBase):
class Meta:
verbose_name = _('Basket')
@classmethod
def is_enabled(cls):
return Regie.objects.count() > 0
def is_relevant(self, context):
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
return False
items = BasketItem.objects.filter(
user=context['request'].user, payment_date__isnull=True
).exclude(cancellation_date__isnull=False)
return len(items) > 0
def render(self, context):
basket_template = template.loader.get_template('lingo/combo/basket.html')
context['items'] = BasketItem.objects.filter(
user=context['request'].user, payment_date__isnull=True
).exclude(cancellation_date__isnull=False)
context['total'] = sum([x.amount for x in context['items']])
return basket_template.render(context)
@register_cell_class
class LingoRecentTransactionsCell(CellBase):
class Meta:
verbose_name = _('Recent Transactions')
@classmethod
def is_enabled(cls):
return Regie.objects.count() > 0
def is_relevant(self, context):
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
return False
transactions = Transaction.objects.filter(
user=context['request'].user,
start_date__gte=timezone.now()-datetime.timedelta(days=7))
return len(transactions) > 0
def render(self, context):
recent_transactions_template = template.loader.get_template(
'lingo/combo/recent_transactions.html')
context['transactions'] = Transaction.objects.filter(
user=context['request'].user,
start_date__gte=timezone.now()-datetime.timedelta(days=7)
).order_by('-start_date')
return recent_transactions_template.render(context)
@register_cell_class
class LingoBasketLinkCell(CellBase):
user_dependant = True
class Meta:
verbose_name = _('Basket Link')
@classmethod
def is_enabled(cls):
return Regie.objects.count() > 0
def is_relevant(self, context):
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
return False
items = BasketItem.objects.filter(
user=context['request'].user, payment_date__isnull=True
).exclude(cancellation_date__isnull=False)
return len(items) > 0
def render(self, context):
if not (getattr(context['request'], 'user', None) and context['request'].user.is_authenticated()):
return ''
basket_template = template.loader.get_template('lingo/combo/basket_link.html')
context['items'] = BasketItem.objects.filter(
user=context['request'].user, payment_date__isnull=True
).exclude(cancellation_date__isnull=False)
context['total'] = sum([x.amount for x in context['items']])
return basket_template.render(context)
class Items(CellBase):
regie = models.CharField(_('Regie'), max_length=50, blank=True)
title = RichTextField(_('Title'), blank=True)
user_dependant = True
template_name = 'lingo/combo/items.html'
class Meta:
abstract = True
class Media:
js = ('xstatic/jquery-ui.min.js', 'js/gadjo.js',)
css = {'all': ('xstatic/themes/smoothness/jquery-ui.min.css', )}
@classmethod
def is_enabled(cls):
return Regie.objects.count() > 0
def get_default_form_class(self):
if Regie.objects.count() == 1:
return None
regies = [('', _('All'))]
regies.extend([(r.slug, r.label) for r in Regie.objects.all()])
return model_forms.modelform_factory(self.__class__,
fields=['regie', 'title'],
widgets={'regie': Select(choices=regies)})
def get_regies(self):
if self.regie:
return [Regie.objects.get(slug=self.regie)]
return Regie.objects.all()
def get_cell_extra_context(self):
ctx = {'title': self.title}
items = self.get_items()
# sort items by creation date
items.sort(key=lambda i: i.creation_date, reverse=True)
ctx.update({'items': items})
return ctx
def render(self, context):
self.context = context
if not context.get('synchronous'):
raise NothingInCacheException()
return super(Items, self).render(context)
@register_cell_class
class ItemsHistory(Items):
class Meta:
verbose_name = _('Items History Cell')
def get_items(self):
items = []
for r in self.get_regies():
items.extend(r.get_past_items(self.context))
return items
@register_cell_class
class ActiveItems(Items):
class Meta:
verbose_name = _('Active Items Cell')
def get_items(self):
items = []
for r in self.get_regies():
items.extend(r.get_items(self.context))
return items

View File

@ -0,0 +1,14 @@
{% load i18n %}
{% if total %}
<h2>{% trans "Basket" %}</h2>
<form action="{% url 'lingo-pay' %}" method="POST">
{% csrf_token %}
<ul>
{% for item in items %}
<li><label><input type="checkbox" name="item" value="{{ item.id }}" checked/> {{ item.subject }}: {{ item.amount }} €</label> (<a href="{{ item.source_url}}">voir</a>)</li>
{% endfor %}
<li><strong>{% trans "Total:" %}</strong> {{ total }} €</li>
</ul>
<input type="submit" value="{% trans "Pay selected items" %}"/>
</form>
{% endif %}

View File

@ -0,0 +1,5 @@
{% load i18n %}
{% if total %}
<p><a href="{{site_base}}">{% trans 'Basket:' %} {{items|length}} {% trans 'items' %} ({{ total }} €)</a></p>
</form>
{% endif %}

View File

@ -0,0 +1,41 @@
{% load i18n %}
<div id="content">
<div id="appbar">
<h2>
{% blocktrans with number=item.display_id %}
Item nr. {{ number }}
{% endblocktrans %}
</h2>
<form action="{% url 'lingo-pay' %}" method="POST">
<div id="item">
<h4 class="label">
{% blocktrans with label=item.subject %}
Label: {{ label }}
{% endblocktrans %}
</h4>
<div class="total_amount">
{% blocktrans with amount=item.total_amount %}
Total amount: {{ amount }}€
{% endblocktrans %}
</div>
{% if item.amount %}
<div class="amount">
{% blocktrans with amount=item.amount %}
Amount to pay: {{ amount }}€
{% endblocktrans %}
</div>
{% endif %}
<div class="issued">{% trans "Issued on:" %} {{ item.creation_date|date:"SHORT_DATE_FORMAT" }}</div>
{% if item.payment_date %}
<div class="paid">{% trans "Payed on:" %} {{ item.payment_date|date:"SHORT_DATE_FORMAT" }}</div>
{% endif %}
{% if item.online_payment and item.amount >= regie.payment_min_amount %}
{% csrf_token %}
<input type="hidden" name="regie" value="{{ regie.pk }}" />
<input type="hidden" name="item" value="{{ item.id }}" />
<button>{% trans "Pay" %}</button>
{% endif %}
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,42 @@
{% load i18n %}
<h3>{{ title|safe }}</h3>
{% if items %}
<table id="items">
<thead>
<tr>
<td>{% trans "Number" %}</td>
<td>{% trans "Label" %}</td>
<td>{% trans "Issue date" %}</td>
<td>{% trans "Payment limit date" %}</td>
<td>{% trans "Amount" %}</td>
<td></td>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.display_id }}</td>
<td>{{ item.subject }}</td>
<td>{{ item.creation_date|date:"SHORT_DATE_FORMAT" }}</td>
<td>{{ item.payment_limit_date|date:"SHORT_DATE_FORMAT" }}</td>
<td>{% blocktrans with amount=item.total_amount %}
{{ amount }}€
{% endblocktrans %}
</td>
<td>
<a href="{% url 'view-item' regie_id=item.regie.pk item_id=item.id %}" rel="popup" class="icon-view">{% trans "View" %}
{% if item.online_payment and item.amount >= item.regie.payment_min_amount %}{% trans "and pay" %}{% endif %}
</a>
{% if item.has_pdf %} /
<a href="{% url 'download-item-pdf' regie_id=item.regie.pk item_id=item.id %}" class="icon-pdf">
{% trans "Download" %}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{% trans "No items yet" %}
{% endif %}

View File

@ -0,0 +1,18 @@
{% load i18n %}
<h2>{% trans "Recent Transactions" %}</h2>
{% csrf_token %}
<ul>
{% for transaction in transactions %}
<li>{{ transaction.start_date|date:"d E o H:i"|lower }}
{% if transaction.is_paid %}
<ul>
{% for item in transaction.items.all %}
<li>{{ item.subject }}: {{ item.amount }} €</label> (<a href="{{ item.source_url}}">voir</a>)</li>
{% endfor %}
</ul>
{% else %}
{{ transaction.get_status_label }}
{% endif %}
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,11 @@
{% extends "combo/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans 'Online Payment' %}</h2>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-homepage' %}">{% trans 'Online Payment' %}</a>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "lingo/manager_base.html" %}
{% load i18n %}
{% block content %}
<ul class="apps">
<li><a class="button" href="{% url 'lingo-manager-transactions-list' %}">{% trans 'Transactions' %}</a></li>
<li><a class="button" href="{% url 'lingo-manager-regie-list' %}">{% trans 'Regies' %}</a></li>
</ul>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% load i18n %}
<html>
<head>
</head>
<body onload="document.forms[0].submit()">
<p>
{% trans "Please wait while your browser is being redirected to the payment website..." %}
</p>
<form action="{{ form.url }}" method="{{ form.method }}" style="display: none">
{% for field in form.fields %}
<input type="{{ field.type }}" name="{{ field.name }}" value="{{ field.value }}"/>
{% endfor %}
<input type="submit">
</form>
</body>
</html>

View File

@ -0,0 +1,17 @@
{% extends "combo/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{{ view.model.get_verbose_name }}</h2>
{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{% blocktrans %}Are you sure you want to delete this?{% endblocktrans %}
<div class="buttons">
<button>{% trans 'Confirm Deletion' %}</button>
<a class="cancel" href="{% url 'lingo-manager-homepage' %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "lingo/regie_list.html" %}
{% load i18n %}
{% block appbar %}
{% if object.id %}
<h2>{% trans "Edit Regie" %}</h2>
{% else %}
<h2>{% trans "New Regie" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button>{% trans "Save" %}</button>
<a class="cancel" href="{% url 'lingo-manager-homepage' %}">{% trans 'Cancel' %}</a>
{% if object.id %}
<a class="delete" rel="popup" href="{% url 'lingo-manager-regie-delete' pk=object.id %}">{% trans 'Delete' %}</a>
{% endif %}
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "lingo/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans 'Regies' %}</h2>
<a rel="popup" href="{% url 'lingo-manager-regie-add' %}">{% trans 'New' %}</a>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-regie-list' %}">{% trans 'Regies' %}</a>
{% endblock %}
{% block content %}
{% if object_list %}
<div class="objects-list">
{% for regie in object_list %}
<div>
<a href="{% url 'lingo-manager-regie-edit' pk=regie.id %}">{{ regie.label }} <span>({{regie.service}})</span></a>
</div>
{% endfor %}
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This site doesn't have any regie yet. Click on the "New" button in the top
right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,74 @@
{% extends "lingo/manager_base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans 'Transactions' %}</h2>
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'lingo-manager-transactions-list' %}">{% trans 'Transactions' %}</a>
{% endblock %}
{% block content %}
{% if object_list %}
<table class="main">
<thead>
<tr>
<th>{% trans 'Transaction Identifier' %}</th>
<th>{% trans 'Date' %}</th>
<th>{% trans 'User' %}</th>
<th>{% trans 'Amount' %}</th>
<th colspan="2">{% trans 'Items' %}</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td rowspan="{{object.items.all.count}}">{{object.order_id}}</td>
<td rowspan="{{object.items.all.count}}">{{object.start_date}}</td>
<td rowspan="{{object.items.all.count}}">{{object.user.first_name}} {{object.user.last_name}}</td>
<td rowspan="{{object.items.all.count}}">{{object.amount}} €</td>
{% for item in object.items.all %}
{% if not forloop.first %}<tr>{% endif %}
<td style="font-size: smaller">{{item.subject}}</td> <td style="font-size: smaller" class="price">{{item.amount}} €</td></tr>
{% endfor %}
{% if not object.items.all %}</tr>{% endif %}
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
<p class="paginator">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">&lt;&lt;</a>
{% else %}
<span>&lt;&lt;</span>
{% endif %}
&nbsp;
<span class="current">
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
&nbsp;
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">&gt;&gt;</a>
{% else %}
<span>&gt;&gt;</span>
{% endif %}
</div>
{% endif %}
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This site doesn't have any transaction yet.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}

50
combo/apps/lingo/urls.py Normal file
View File

@ -0,0 +1,50 @@
# 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 django.conf.urls import patterns, url, include
from combo.urls_utils import decorated_includes, manager_required
from .views import (RegiesApiView, AddBasketItemApiView, PayView, CallbackView,
ReturnView, ItemDownloadView, ItemView)
from .manager_views import (RegieListView, RegieCreateView, RegieUpdateView,
RegieDeleteView, TransactionListView, ManagerHomeView)
lingo_manager_urls = patterns('lingo.manager_views',
url('^$', ManagerHomeView.as_view(), name='lingo-manager-homepage'),
url('^transactions/$', TransactionListView.as_view(), name='lingo-manager-transactions-list'),
url('^regies/$', RegieListView.as_view(), name='lingo-manager-regie-list'),
url('^regies/add/$', RegieCreateView.as_view(), name='lingo-manager-regie-add'),
url('^regies/(?P<pk>\w+)/edit$', RegieUpdateView.as_view(),
name='lingo-manager-regie-edit'),
url('^regies/(?P<pk>\w+)/delete$', RegieDeleteView.as_view(),
name='lingo-manager-regie-delete'),
)
urlpatterns = patterns('',
url('^api/lingo/regies$', RegiesApiView.as_view(), name='api-regies'),
url('^api/lingo/add-basket-item$', AddBasketItemApiView.as_view(),
name='api-add-basket-item'),
url(r'^lingo/pay$', PayView.as_view(), name='lingo-pay'),
url(r'^lingo/callback/(?P<regie_pk>\w+)/$', CallbackView.as_view(), name='lingo-callback'),
url(r'^lingo/return/(?P<regie_pk>\w+)/$', ReturnView.as_view(), name='lingo-return'),
url(r'^manage/lingo/', decorated_includes(manager_required,
include(lingo_manager_urls))),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_id>[\w,-]+)/pdf$',
ItemDownloadView.as_view(), name='download-item-pdf'),
url(r'^lingo/item/(?P<regie_id>[\w,-]+)/(?P<item_id>[\w,-]+)/$',
ItemView.as_view(), name='view-item'),
)

265
combo/apps/lingo/views.py Normal file
View File

@ -0,0 +1,265 @@
# 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
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponseForbidden
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, ListView, TemplateView
import eopayment
try:
from mellon.models import UserSAMLIdentifier
except ImportError:
UserSAMLIdentifier = None
from .models import Regie, BasketItem, Transaction
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):
# XXX: check request signature
request_body = json.loads(self.request.body)
extra = request_body.get('extra', {})
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]
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'}))
return response
class PayView(View):
def post(self, request, *args, **kwargs):
regie_id = request.POST.get('regie')
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'))
# XXX: check all items are going to the same regie
regie = items[0].regie
remote_items = ''
transaction = Transaction()
if request.user.is_authenticated():
transaction.user = request.user
else:
transaction.user = None
transaction.save()
transaction.regie = regie
transaction.items = items
transaction.remote_items = remote_items
transaction.status = 0
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()
if total_amount < regie.payment_min_amount:
return HttpResponseForbidden()
payment = eopayment.Payment(regie.service, regie.service_options)
return_url = request.build_absolute_uri(
reverse('lingo-callback', kwargs={'regie_pk': regie.id}))
(order_id, kind, data) = payment.request(total_amount,
email=request.user.email,
next_url=return_url)
transaction.order_id = order_id
transaction.save()
# 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})
raise NotImplementedError()
class CallbackView(View):
def handle_callback(self, request, backend_response, **kwargs):
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
payment = eopayment.Payment(regie.service, regie.service_options)
payment_response = payment.response(backend_response)
if not payment_response.result == eopayment.CANCELLED:
# cancellation are not signed...
assert payment_response.signed is True
transaction = Transaction.objects.get(order_id=payment_response.order_id)
transaction.status = payment_response.result
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()
except:
# ignore errors, it will 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):
def get(self, request, *args, **kwargs):
regie = Regie.objects.get(id=kwargs.get('regie_pk'))
payment = eopayment.Payment(regie.service, regie.service_options)
try:
payment_response = payment.response(request.environ['QUERY_STRING'])
except:
# if eopayment can't get response from query string redirect to
# homepage
return HttpResponseRedirect('/')
transaction = Transaction.objects.get(order_id=payment_response.order_id)
if transaction.items:
# redirect to first transaction local item view
return HttpResponseRedirect(reverse('view-item',
kwargs={'regie_id': regie.pk,
'item_id': transaction.items[0].id}))
if transaction.remote_items:
# redirect to first transaction remote item view
item = transaction.remote_items.split(',')[0]
return HttpResponseRedirect(reverse('view-item',
kwargs={'regie_id': regie.pk,
'item_id': item.id}))
class ItemDownloadView(View):
http_method_names = [u'get']
def get(self, request, *args, **kwargs):
regie = Regie.objects.get(pk=kwargs['regie_id'])
data = regie.download_item(request, kwargs['item_id'])
r = HttpResponse(data, content_type='application/pdf')
r['Content-Disposition'] = 'attachment; filename="%(item_id)s.pdf"' % kwargs
return r
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}

View File

@ -64,6 +64,7 @@ INSTALLED_APPS = (
'combo.apps.publik',
'combo.apps.family',
'combo.apps.dataviz',
'combo.apps.lingo',
'xstatic.pkg.chartnew_js',
)

1
debian/control vendored
View File

@ -16,6 +16,7 @@ Depends: ${misc:Depends}, ${python:Depends},
python-django-cmsplugin-blurp,
python-xstatic-chartnew-js
Recommends: python-django-mellon
Conflicts: python-lingo
Description: Portal Management System (Python module)
Package: combo

70
tests/test_lingo_cells.py Normal file
View File

@ -0,0 +1,70 @@
import pytest
from django.contrib.auth.models import User
from django.test.client import RequestFactory
from django.template import Context
from django.utils import timezone
from combo.data.models import Page
from combo.apps.lingo.models import Regie, BasketItem, Transaction
from combo.apps.lingo.models import LingoBasketCell, LingoRecentTransactionsCell
pytestmark = pytest.mark.django_db
@pytest.fixture
def user():
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
user = User.objects.create_user('admin', email=None, password='admin')
return user
@pytest.fixture
def regie():
try:
regie = Regie.objects.get(slug='test')
except Regie.DoesNotExist:
regie = Regie()
regie.label = 'Test'
regie.slug = 'test'
regie.description = 'test'
regie.save()
return regie
def test_cell_disabled():
Regie.objects.all().delete()
assert LingoBasketCell.is_enabled() is False
assert LingoRecentTransactionsCell.is_enabled() is False
def test_cell_enabled(regie):
assert LingoBasketCell.is_enabled() is True
assert LingoRecentTransactionsCell.is_enabled() is True
def test_basket_cell(regie, user):
page = Page(title='xxx', slug='test_basket_cell', template_name='standard')
page.save()
cell = LingoBasketCell(page=page, placeholder='content', order=0)
context = Context({'request': RequestFactory().get('/')})
context['request'].user = None
assert cell.is_relevant(context) is False
context['request'].user = user
assert cell.is_relevant(context) is False
item = BasketItem()
item.user = user
item.regie = regie
item.subject = 'foo'
item.source_url = 'http://example.net'
item.amount = 12345
item.save()
assert cell.is_relevant(context) is True
item.cancellation_date = timezone.now()
item.save()
assert cell.is_relevant(context) is False
item.cancellation_date = None
item.save()
content = cell.render(context)
assert '12345' in content

View File

@ -0,0 +1,68 @@
from django.contrib.auth.models import User
from django.core.wsgi import get_wsgi_application
from webtest import TestApp
import pytest
from combo.apps.lingo.models import Regie
pytestmark = pytest.mark.django_db
@pytest.fixture
def admin_user():
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
user = User.objects.create_superuser('admin', email=None, password='admin')
return user
def login(app, username='admin', password='admin'):
login_page = app.get('/login/')
login_form = login_page.forms[0]
login_form['username'] = username
login_form['password'] = password
resp = login_form.submit()
assert resp.status_int == 302
return app
def test_access(admin_user):
app = login(TestApp(get_wsgi_application()))
resp = app.get('/manage/', status=200)
assert '/manage/lingo/' in resp.body
def test_add_regie(admin_user):
Regie.objects.all().delete()
app = login(TestApp(get_wsgi_application()))
resp = app.get('/manage/lingo/regies/', status=200)
resp = resp.click('New')
resp.forms[0]['label'] = 'Test'
resp.forms[0]['slug'] = 'test'
resp.forms[0]['description'] = 'description'
resp.forms[0]['service'] = 'dummy'
resp = resp.forms[0].submit()
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
assert Regie.objects.count() == 1
regie = Regie.objects.all()[0]
assert regie.label == 'Test'
def test_edit_regie(admin_user):
test_add_regie(admin_user)
app = login(TestApp(get_wsgi_application()))
resp = app.get('/manage/lingo/regies/', status=200)
resp = resp.click('Test')
resp.forms[0]['description'] = 'other description'
resp = resp.forms[0].submit()
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
assert Regie.objects.count() == 1
regie = Regie.objects.all()[0]
assert regie.description == 'other description'
def test_delete_regie(admin_user):
test_add_regie(admin_user)
app = login(TestApp(get_wsgi_application()))
resp = app.get('/manage/lingo/regies/', status=200)
resp = resp.click('Test')
resp = resp.click('Delete')
assert 'Are you sure you want to delete this?' in resp.body
resp = resp.forms[0].submit()
assert resp.location == 'http://localhost:80/manage/lingo/regies/'
assert Regie.objects.count() == 0

147
tests/test_lingo_payment.py Normal file
View File

@ -0,0 +1,147 @@
import pytest
from datetime import datetime, timedelta
import urlparse
import urllib
from decimal import Decimal
import json
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.wsgi import get_wsgi_application
from webtest import TestApp
from django.test import Client
from combo.apps.lingo.models import Regie, BasketItem, Transaction, RemoteItem
pytestmark = pytest.mark.django_db
client = Client()
@pytest.fixture
def regie():
try:
regie = Regie.objects.get(slug='test')
except Regie.DoesNotExist:
regie = Regie()
regie.label = 'Test'
regie.slug = 'test'
regie.description = 'test'
regie.payment_min_amount = Decimal(4.5)
regie.service = 'dummy'
regie.service_options = {'siret': '1234'}
regie.save()
return regie
@pytest.fixture
def user():
try:
user = User.objects.get(username='admin')
except User.DoesNotExist:
user = User.objects.create_user('admin', email=None, password='admin')
return user
def login(username='admin', password='admin'):
resp = client.post('/login/', {'username': username, 'password': password})
assert resp.status_code == 302
def test_payment_min_amount(regie, user):
items = {'item1': {'amount': '1.5', 'source_url': '/item/1'},
'item2': {'amount': '2.4', 'source_url': '/item/2'}
}
b_items = []
for subject, details in items.iteritems():
b_item = BasketItem.objects.create(user=user, regie=regie,
subject=subject, **details)
b_items.append(b_item.pk)
login()
resp = client.post(reverse('lingo-pay'), {'item': b_items, 'regie': regie.pk})
assert resp.status_code == 403
def test_successfull_items_payment(regie, user):
items = {'item1': {'amount': '10.5', 'source_url': '/item/1'},
'item2': {'amount': '42', 'source_url': '/item/2'},
'item3': {'amount': '100', 'source_url': '/item/3'},
'item4': {'amount': '354', 'source_url': '/item/4'}
}
b_items = []
for subject, details in items.iteritems():
b_item = BasketItem.objects.create(user=user, regie=regie,
subject=subject, **details)
b_items.append(b_item.pk)
login()
resp = client.post(reverse('lingo-pay'), {'item': b_items, 'regie': regie.pk})
assert resp.status_code == 302
location = resp.get('location')
assert 'dummy-payment' in location
parsed = urlparse.urlparse(location)
# get return_url and transaction id from location
qs = urlparse.parse_qs(parsed.query)
args = {'transaction_id': qs['transaction_id'][0], 'signed': True,
'ok': True, 'reason': 'Paid'}
# simulate backend callback call
resp = client.get(qs['return_url'][0], args)
assert resp.status_code == 200
def test_add_amount_to_basket(regie, user):
user_email = 'foo@example.com'
User.objects.get_or_create(email=user_email)
amount = 42
data = {'amount': amount, 'display_name': 'test amount',
'url': 'http://example.com'}
url = '%s?email=%s' % (reverse('api-add-basket-item'), user_email)
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content) == {'result': 'success'}
assert BasketItem.objects.filter(amount=amount).exists()
data['extra'] = {'amount': '22.22'}
resp = client.post(url, json.dumps(data), content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content) == {'result': 'success'}
assert BasketItem.objects.filter(amount=Decimal('64.22')).exists()
data['amount'] = [amount]
data['extra'] = {'amount': ['22.22', '12']}
resp = client.post('%s&amount=5' % url, json.dumps(data),
content_type='application/json')
assert resp.status_code == 200
assert json.loads(resp.content) == {'result': 'success'}
assert BasketItem.objects.filter(amount=Decimal('81.22')).exists()
def test_payment_callback(regie, user):
item = BasketItem.objects.create(user=user, regie=regie,
subject='test_item', amount='10.5',
source_url='/testitem')
login()
resp = client.post(reverse('lingo-pay'), {'item': [item.pk],
'regie': regie.pk})
assert resp.status_code == 302
location = resp.get('location')
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
# call callback with GET
get_resp = client.get(qs['return_url'][0], data)
assert get_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3
resp = client.post(reverse('lingo-pay'), {'item': [item.pk],
'regie': regie.pk})
assert resp.status_code == 302
location = resp.get('location')
parsed = urlparse.urlparse(location)
qs = urlparse.parse_qs(parsed.query)
transaction_id = qs['transaction_id'][0]
data = {'transaction_id': transaction_id, 'signed': True,
'amount': qs['amount'][0], 'ok': True}
# call callback with POST
post_resp = client.post(qs['return_url'][0], urllib.urlencode(data),
content_type='text/html')
assert post_resp.status_code == 200
assert Transaction.objects.get(order_id=transaction_id).status == 3