concerto: initial commit

This commit is contained in:
Thomas NOËL 2014-10-09 13:46:16 +02:00
parent 5ad5d3424c
commit a76c23c990
14 changed files with 524 additions and 0 deletions

View File

@ -0,0 +1,20 @@
Provides webservices to a "Concerto"-like database
icon-concerto.svg license
=========================
Creative Commons Attribution (CC BY 3.0) http://creativecommons.org/licenses/by/3.0/us/
Family designed by Ahmed Elzahra http://www.thenounproject.com/trochilidae
from the Noun Project http://www.thenounproject.com
original license.txt :
Thank you for using The Noun Project. This icon is licensed under Creative
Commons Attribution and must be attributed as:
Family by Ahmed Trochilidae from The Noun Project
If you have a Premium Account or have purchased a license for this icon, you
don't need to worry about attribution! We will share the profits from your
purchase with this icon's designer.

View File

View File

@ -0,0 +1,9 @@
from django.contrib import admin
from models import Concerto
class ConcertoAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
list_display = ('title', 'slug',)
admin.site.register(Concerto, ConcertoAdmin)

View File

@ -0,0 +1,14 @@
from django.utils.text import slugify
from django import forms
from .models import Concerto
class ConcertoForm(forms.ModelForm):
class Meta:
model = Concerto
exclude = ('slug', 'users')
def save(self, commit=True):
if not self.instance.slug:
self.instance.slug = slugify(self.instance.title)
return super(ConcertoForm, self).save(commit=commit)

View File

@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve">
<g id="Layer_1_1_">
</g>
<path d="M64.63,56.39c-0.84,0.79-1.47,1.32-1.47,1.61v26.5c0,1.45-1.18,2.62-2.62,2.62h-5.55c-0.91,0-1.79-0.4-2.391-0.93 c-0.479-0.41-0.77-0.91-0.77-1.36v-3c0-0.39-0.13-0.76-0.34-1.061C51.16,80.3,50.61,80,49.99,80s-1.16,0.3-1.49,0.77 c-0.21,0.301-0.34,0.671-0.34,1.061v3c0,1.02-0.62,2.29-2.19,2.29h-7.43c-1.45,0-2.63-1.17-2.63-2.62V58 c0-1.26-11.15-7.13-0.26-18.9c1.32-1.44,2.98-2.96,5.02-4.58c0.48-0.37-0.06-0.61,0.99,0c0.62,0.37,1.46,0.76,2.53,1.09 c1.1,0.34,2.46,0.61,4.1,0.72c0.47,0.03,0.97,0.05,1.49,0.05c0.51,0,0.98-0.02,1.43-0.05h0.07c0.319-0.02,0.62-0.05,0.92-0.09 c2.68-0.29,4.45-1.04,5.53-1.71c1.02-0.61,0.489-0.38,0.989,0c0.49,0.37,0.95,0.74,1.391,1.1c1.449,1.17,2.68,2.3,3.72,3.38 c1.439,1.5,2.5,2.9,3.25,4.21C70.95,49.89,66.98,54.17,64.63,56.39z"/>
<path d="M62.161,94.438c0,1.062-2.1,1.188-4.688,1.188l0,0c-2.59,0-4.688-0.216-4.688-1.188v-0.125c0-2.588,2.099-4.688,4.688-4.688 l0,0c2.588,0,4.688,2.1,4.688,4.688V94.438z"/>
<path d="M46.8,94.438c0,1.062-2.099,1.188-4.688,1.188l0,0c-2.589,0-4.688-0.216-4.688-1.188v-0.125 c0-2.588,2.099-4.688,4.688-4.688l0,0c2.589,0,4.688,2.1,4.688,4.688V94.438z"/>
<circle cx="49.546" cy="19.25" r="14.125"/>
<g id="Layer_1_2_">
</g>
<g id="Layer_1_3_">
</g>
<path d="M30.057,57.234v2.038h-19.7v-3.113c0-1.067-10.896-6.646,4.617-18.179c0,0,1.906,2.212,7.1,2.212 c5.212,0,6.935-2.212,6.935-2.212c0.725,0.516,1.39,1.023,2.002,1.513C19.546,50.598,30.057,56.09,30.057,57.234z"/>
<path d="M10.356,61.481h19.7v10.942H25.93c-1.207,0-2.335-0.944-2.335-1.697v-2.211c0-0.744-0.612-1.356-1.364-1.356 c-0.743,0-1.347,0.612-1.347,1.356v2.211c0,0.753-0.464,1.697-1.618,1.697h-6.969c-1.067,0-1.941-0.866-1.941-1.933V61.481z"/>
<path d="M30.345,82.979v0.096c0,0.787-1.547,0.874-3.462,0.874c-0.953,0-1.828-0.044-2.448-0.166 c-0.63-0.131-1.015-0.35-1.015-0.708v-0.096c0-1.907,1.548-3.463,3.463-3.463c0.883,0,1.696,0.332,2.299,0.874 c0.053,0.044,0.105,0.096,0.149,0.14c0.49,0.49,0.831,1.119,0.953,1.828C30.328,82.558,30.345,82.769,30.345,82.979z"/>
<path d="M19.001,83.072c0,0.784-1.551,0.878-3.462,0.878l0,0c-1.913,0-3.462-0.16-3.462-0.878v-0.093 c0-1.911,1.55-3.462,3.462-3.462l0,0c1.911,0,3.462,1.551,3.462,3.462V83.072z"/>
<path d="M30.359,23.425H13.623c1.741-2.796,4.842-4.661,8.372-4.661S28.619,20.629,30.359,23.425z"/>
<path d="M31.835,28.605c0,5.436-4.413,9.84-9.84,9.84c-5.436,0-9.84-4.405-9.84-9.84c0-1.114,0.19-2.194,0.528-3.192h18.616 C31.646,26.411,31.835,27.491,31.835,28.605z"/>
<path d="M60.59,77.42l2.57-8.189v9.359C61.74,78.27,60.59,77.86,60.59,77.42z"/>
<path d="M95.35,77.42c0,1.07-6.76,1.93-7.81,1.93h-5.56c-0.62,0-2.08-0.85-3.5-0.85c-1.311,0-2.58,0.85-3.131,0.85H68.41 c-0.09,0-0.23-0.01-0.41-0.02V58c0-1.2,10.95-6.63,2.07-17.41c0.319-0.26,0.649-0.51,1-0.77c0,0,1.899,2.21,7.08,2.21 c5.189,0,6.899-2.21,6.899-2.21c16.061,11.49,4.41,17.78,4.41,18.84L95.35,77.42z"/>
<path d="M87.257,84.732c0,0.781-1.545,0.873-3.449,0.873l0,0c-1.904,0-3.447-0.158-3.447-0.873v-0.092 c0-1.904,1.543-3.449,3.447-3.449l0,0c1.904,0,3.449,1.545,3.449,3.449V84.732z"/>
<path d="M75.956,84.732c0,0.781-1.544,0.873-3.449,0.873l0,0c-1.902,0-3.446-0.158-3.446-0.873v-0.092 c0-1.904,1.544-3.449,3.446-3.449l0,0c1.905,0,3.449,1.545,3.449,3.449V84.732z"/>
<circle cx="77.976" cy="29.425" r="10.39"/>
<path d="M74.399,26.262c-0.646,0.646-1.695,0.646-2.343,0l-5.67-5.673c-0.646-0.646-0.646-1.695,0-2.341l0,0 c0.646-0.646,1.693-0.646,2.342,0l5.671,5.673C75.045,24.567,75.047,25.616,74.399,26.262L74.399,26.262z"/>
<path d="M81.708,26.262c0.646,0.646,1.693,0.646,2.342,0l5.672-5.673c0.646-0.646,0.646-1.695,0-2.341l0,0 c-0.646-0.646-1.694-0.646-2.342,0l-5.672,5.673C81.062,24.567,81.062,25.616,81.708,26.262L81.708,26.262z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
'''
Application emulating Concerto's webservices.
'''
try:
import psycopg2
import psycopg2.extras
except ImportError:
psycopg2 = None
import hashlib
import hmac
import base64
from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.dateformat import format as date_format
from django.utils.dateformat import time_format
from django.utils.translation import ugettext_lazy as _
from passerelle.datasources.models import BaseDataSource
class Concerto(BaseDataSource):
invoice_hash_secret = models.CharField(max_length=80)
pg_database = models.CharField(max_length=64)
pg_user = models.CharField(max_length=64, null=True, blank=True)
pg_password = models.CharField(max_length=64, null=True, blank=True)
pg_host = models.CharField(max_length=64, null=True, blank=True)
pg_port = models.CharField(max_length=64, null=True, blank=True)
category = _('Business Process Connectors')
class Meta:
verbose_name = _(u'Concerto™')
def __init__(self, *args, **kwargs):
super(Concerto, self).__init__(*args, **kwargs)
self.pgconn = None
def get_absolute_url(self):
return reverse('concerto-view', kwargs={'slug': self.slug})
@classmethod
def get_add_url(cls):
return reverse('concerto-add')
def get_edit_url(self):
return reverse('concerto-edit', kwargs={'slug': self.slug})
def get_delete_url(self):
return reverse('concerto-delete', kwargs={'slug': self.slug})
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name
@classmethod
def get_icon_class(cls):
return 'concerto'
def get_connection(self, new=False):
if psycopg2 is None:
raise Http404
if new and self.pgconn is not None:
self.pgconn.close()
self.pgconn = None
if self.pgconn is None:
args = {'database': self.pg_database}
if self.pg_user:
args['user'] = self.pg_user
args['password'] = self.pg_password
if self.pg_host:
args['host'] = self.pg_host
if self.pg_port:
args['port'] = self.pg_port
self.pgconn = psycopg2.connect(**args)
return self.pgconn
def execute_query(self, query, **kwargs):
conn = self.get_connection()
cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
cursor.execute(query, kwargs)
return cursor
def get_hash(self, *args):
hash = base64.b64encode(hmac.HMAC(
key=str(self.invoice_hash_secret),
msg='#'.join(map(lambda s: str(s), args)),
digestmod=hashlib.sha256).digest(),
altchars='AB')
return hash[:8]
def family_link(self, family_id, secret):
query = 'SELECT COUNT(id) FROM data_famille \
WHERE id = %(family_id)s \
AND code_secret = %(secret)s'
cursor = self.execute_query(query, family_id=family_id, secret=secret)
exists = cursor.fetchone()[0] != 0
query = 'SELECT link_date FROM data_liaisonnameidfamille \
WHERE famille_id = %(family_id)s'
cursor = self.execute_query(query, family_id=family_id)
links = [link[0] for link in cursor.fetchall()]
return {'exists': exists, 'links': links}
def family_data(self, nameid, data_filter=None):
queries = {
'families': "SELECT f.* FROM data_famille AS f \
INNER JOIN data_liaisonnameidfamille AS lp \
ON f.id = lp.famille_id \
WHERE lp.name_id = %(nameid)s",
'kids': "SELECT e.* FROM data_enfant AS e \
INNER JOIN data_liaisonnameidfamille as lp \
ON e.famille_id = lp.famille_id \
WHERE lp.name_id = %(nameid)s",
'parents': "SELECT f.id as famille_id, p.* FROM data_personne AS p \
INNER JOIN data_liaisonparentfamille as lp \
ON lp.personne_id = p.id \
INNER JOIN data_famille AS f \
ON f.id = lp.famille_id \
INNER JOIN data_liaisonnameidfamille as lnf \
ON lnf.famille_id = f.id \
WHERE lnf.name_id = %(nameid)s"
}
data = {}
if data_filter and queries.has_key(data_filter):
queries = {data_filter: queries[data_filter]}
for group, query in queries.iteritems():
cursor = self.execute_query(queries[group], nameid=nameid)
data[group] = [getattr(self, 'format_family_%s' % group)(row) for row in cursor.fetchall()]
return data
def format_family_parents(self, row):
data = dict(row)
data.update({'text': '{nom} {prenom}'.format(**row)})
return data
def format_family_kids(self, row):
data = dict(row)
data.update({'text': '{nom} {prenom} - {date_naissance}'.format(**row)})
return data
def format_family_families(self, row):
data = dict(row)
data.update({'text': u'Dossier numéro {id}'.format(**row)})
return data
def link_to_family(self, nameid, family_id):
queries = (
'DELETE FROM data_liaisonnameidfamille \
WHERE name_id=%(nameid)s',
'INSERT INTO data_liaisonnameidfamille(name_id, famille_id, link_date) \
VALUES (%(nameid)s, %(famille)s, CURRENT_TIMESTAMP)'
)
for query in queries:
cursor = self.execute_query(query, nameid=nameid, famille=family_id)
def unlink_from_family(self, nameid):
query = 'DELETE FROM data_liaisonnameidfamille \
WHERE name_id = %(nameid)s \
RETURNING *'
cursor = self.execute_query(query, nameid=nameid)
return cursor.fetchone()[0]
def get_invoices(self, query, nameid):
invoices = []
for row in self.execute_query(query, nameid=nameid).fetchall():
data = dict(row)
hash = self.get_hash(data['id'], data['date_generation'], data['montant'])
data.update({'hash': hash})
invoices.append(data)
return invoices
def family_invoices(self, nameid, status):
queries = {'all': 'SELECT invoice.*, invoice.montant as amount, \
invoice.date_generation as creation_date, \
(DATE(invoice.date_limite_paie) < DATE(NOW())) as expired, \
invoice.date_limite_paie as expiration_date, \
(invoice.paye OR invoice.prelevement_automatique OR \
invoice.statut_tipi IS NOT DISTINCT FROM \'PAID\' OR \
invoice.solde = 0) as paid, \
DATE(invoice.date_reglement) as paid_date \
FROM data_facture AS invoice \
INNER JOIN data_famille AS family \
ON invoice.famille_id = family.id \
INNER JOIN data_liaisonnameidfamille AS lp \
ON family.id = lp.famille_id \
WHERE lp.name_id = %(nameid)s \
AND invoice.active = \'true\' \
ORDER BY invoice.date_generation DESC',
'unpaid': 'SELECT invoice.*, \
(DATE(invoice.date_limite_paie) < DATE(NOW())) as expired, \
(invoice.paye OR invoice.prelevement_automatique \
OR invoice.statut_tipi \
IS NOT DISTINCT FROM \'PAID\' \
OR invoice.solde = 0) as paid \
FROM data_facture AS invoice \
INNER JOIN data_famille AS family \
ON invoice.famille_id = family.id \
INNER JOIN data_liaisonnameidfamille AS lp \
ON family.id = lp.famille_id \
WHERE lp.name_id = %(nameid)s \
AND invoice.paye = \'false\' \
AND invoice.active = \'true\' \
AND invoice.solde <> 0 \
AND invoice.statut_tipi IS DISTINCT FROM \'PAID\' \
AND DATE(invoice.date_generation) <= DATE(now()) \
AND DATE(invoice.date_limite_paie) >= DATE(NOW()) \
ORDER BY invoice.date_generation DESC'
}
return self.get_invoices(queries[status], nameid)
def get_invoice(self, invoice_id, invoice_hash):
query = 'SELECT data_facture.id as id, \
data_facture.solde as amount, \
data_facture.date_generation as creation_date, \
(DATE(data_facture.date_limite_paie) < DATE(NOW())) as expired, \
data_facture.date_limite_paie as expiration_date, \
(data_facture.paye OR \
data_facture.prelevement_automatique OR \
data_facture.statut_tipi IS NOT DISTINCT FROM \'PAID\' OR \
data_facture.solde = 0) as paid, \
data_facture.date_reponse_tipi as paid_date, \
data_facture.date_passage_perception, \
data_facture.prelevement_automatique, \
data_facture.id as refdet, \
data_facture.statut_tipi, \
\'REGIE ORLEANS\' as objet, \
data_facture.montant as total_amount \
FROM data_facture \
WHERE data_facture.id = %(invoice_id)s \
AND data_facture.active = \'true\''
r = self.execute_query(query, invoice_id=invoice_id).fetchone()
if invoice_hash == self.get_hash(r['id'], r['creation_date'],
r['total_amount']):
return dict(r)
def update_invoice(self, invoice_id, invoice_hash, status):
get_query = 'SELECT id, date_generation, montant FROM \
data_facture WHERE id = %(invoice_id)s'
update_query = 'UPDATE data_facture \
SET date_reponse_tipi=now(), statut_tipi=%(status) \
WHERE id = %(invoice_id)s \
RETURNING id, paye'
invoice = self.execute_query(get_query, invoice_id=invoice_id).fetchone()
if invoice:
hash = self.get_hash(invoice['id'], invoice['date_generation'],
invoice['montant'])
if hash == invoice_hash:
return self.execute_query(query, invoice_id=invoice_id,
status=status).fetchone()

View File

@ -0,0 +1,64 @@
{% extends "gdc/base.html" %}
{% load i18n passerelle %}
{% block appbar %}
<h2>Concerto - {{ object.title }}</h2>
{% if perms.concerto.change_concerto %}
<a rel="popup" class="button" href="{% url 'concerto-edit' slug=object.slug %}">{% trans 'edit' %}</a>
{% endif %}
{% if perms.concerto.delete_concerto %}
<a rel="popup" class="button" href="{% url 'concerto-delete' slug=object.slug %}">{% trans 'delete' %}</a>
{% endif %}
{% endblock %}
{% block content %}
<div>
<h3>{% trans 'Endpoints' %}</h3>
<ul>
<li>{% trans "Check user's family link:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familylink</i>/&lt;family_id&gt;</i>/<i>&lt;secret&gt;</i></a>
</li>
<li>{% trans "Link to a family:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>linktofamily/&lt;nameid&gt;</i>/&lt;family_id&gt;</i></a>
</li>
<li>{% trans "Unlink from a family:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>unlinkfromfamily/&lt;nameid&gt;</i></a>
</li>
<li>{% trans "NameId's families:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familydata/&lt;nameid&gt;?filter=families</i></a>
</li>
<li>{% trans "Family's parents:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familydata/&lt;nameid&gt;?filter=parents</i></a>
</li>
<li>{% trans "Family's kids:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familydata/&lt;nameid&gt;?filter=kids</i></a>
</li>
<li>{% trans "Family's all invoices:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familyallinvoices/&lt;nameid&gt;</i></a>
<li>{% trans "Family's unpaid invoices:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>familyallinvoices/&lt;nameid&gt;?status=unpaid</i></a>
<li>{% trans "Invoice view:" %}
<a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>invoice/&lt;invoice_id&gt;/&lt;invoice_hash&gt;</i></a>
</li>
<li>{% trans "Invoice update:" %}
POST on <a>{{ site_base_uri }}{% url 'concerto-view' slug=object.slug %}<i>invoice/&lt;invoice_id&gt;/&lt;invoice_hash&gt;</i></a> with the 'status' in the payload
</li>
</ul>
</div>
{% if perms.base.view_accessright %}
<div>
<h3>{% trans "Security" %}</h3>
<p>
{% trans 'Accessing is limited to the following API users:' %}
</p>
{% access_rights_table resource=object permission='can_use_connector' %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
from django.views.decorators.csrf import csrf_exempt
from django.conf.urls import patterns, url
from django.contrib.auth.decorators import login_required
from views import *
urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', ConcertoDetailView.as_view(), name='concerto-view'),
url(r'^(?P<slug>[\w,-]+)/familylink/(?P<family_id>\w+)/(?P<secret>.+)$',
FamilyLink.as_view(), name='concerto-familylink'),
url(r'^(?P<slug>[\w,-]+)/familydata/(?P<nameid>.+)$',
FamilyData.as_view(), name='concerto-familydata'),
url(r'^(?P<slug>[\w,-]+)/linktofamily/(?P<nameid>.+)/(?P<family_id>\d+)$',
LinkToFamily.as_view(), name='concerto-linktofamily'),
url(r'^(?P<slug>[\w,-]+)/unlinkfromfamily/(?P<nameid>.+)$',
UnlinkFromFamily.as_view(), name='concerto-unlinkfromfamily'),
url(r'^(?P<slug>[\w,-]+)/famildata/(?P<nameid>.+)$',
FamilyData.as_view(), name='concerto-familydata'),
url(r'^(?P<slug>[\w,-]+)/familyallinvoices/(?P<nameid>.+)$',
FamilyInvoices.as_view(), name='concerto-familyallinvoices'),
url(r'^(?P<slug>[\w,-]+)/invoice/(?P<invoice_id>\w+)/(?P<invoice_hash>\w+)$',
InvoiceView.as_view(), name='concerto-invoiceview'),
)
management_urlpatterns = patterns('',
url(r'^add$', ConcertoCreateView.as_view(), name='concerto-add'),
url(r'^(?P<slug>[\w,-]+)/edit$', ConcertoUpdateView.as_view(), name='concerto-edit'),
url(r'^(?P<slug>[\w,-]+)/delete$', ConcertoDeleteView.as_view(), name='concerto-delete'),
)

View File

@ -0,0 +1,96 @@
import json
from django.core.urlresolvers import reverse
from django.http import Http404
from django.shortcuts import redirect
from django.views.generic.base import View, RedirectView
from django.views.generic.detail import SingleObjectMixin, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from passerelle.utils import to_json
from passerelle import utils
from passerelle.base.views import ResourceView
from .models import Concerto
from .forms import ConcertoForm
class ConcertoDetailView(DetailView):
model = Concerto
class ConcertoCreateView(CreateView):
model = Concerto
form_class = ConcertoForm
template_name = 'passerelle/manage/service_form.html'
class ConcertoUpdateView(UpdateView):
model = Concerto
form_class = ConcertoForm
template_name = 'passerelle/manage/service_form.html'
class ConcertoDeleteView(DeleteView):
model = Concerto
template_name = 'passerelle/manage/service_confirm_delete.html'
def get_success_url(self):
return reverse('manage-home')
class FamilyLink(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, family_id, secret, *args, **kwargs):
return self.get_object().family_link(family_id, secret)
class FamilyData(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, nameid):
data_filter = request.GET.get('filter', None)
return self.get_object().family_data(nameid, data_filter)
class LinkToFamily(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, nameid, family_id):
return self.get_object().link_to_family(nameid, family_id)
class UnlinkFromFamily(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, nameid):
return self.get_object().unlink_from_family(nameid)
class FamilyInvoices(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, nameid):
status = request.GET.get('status', 'all')
return self.get_object().family_invoices(nameid, status)
class InvoiceView(View, SingleObjectMixin):
model = Concerto
@utils.protected_api('can_use_connector')
@to_json('api')
def get(self, request, slug, invoice_id, invoice_hash):
return self.get_object().get_invoice(invoice_id, invoice_hash)
@utils.protected_api('can_use_connector')
@to_json('api')
def post(self, request, slug, invoice_id, invoice_hash):
invoice_status = request.POST.get('status')
return self.get_object().update_invoice(invoice_id, invoice_hash, invoice_status)

View File

@ -88,6 +88,7 @@ INSTALLED_APPS = (
'ovh',
'mobyt',
'pastell',
'concerto',
'gadjo',
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -23,3 +23,6 @@ li.clock a:hover { background-image: url(icons/icon-clock-hover.png); }
li.pastell a { background-image: url(icons/icon-pastell.png); }
li.pastell a:hover { background-image: url(icons/icon-pastell-hover.png); }
li.concerto a { background-image: url(icons/icon-concerto.png); }
li.concerto a:hover { background-image: url(icons/icon-concerto-hover.png); }

View File

@ -17,6 +17,7 @@ import mobyt.urls
import ovh.urls
import oxyd.urls
import pastell.urls
import concerto.urls
admin.autodiscover()
@ -64,6 +65,10 @@ urlpatterns = patterns('',
url(r'^manage/pastell/',
decorated_includes(login_required, include(pastell.urls.management_urlpatterns))),
url(r'^concerto/', include(concerto.urls.urlpatterns)),
url(r'^manage/concerto/',
decorated_includes(login_required, include(concerto.urls.management_urlpatterns))),
)
# activate URL for installed apps only