gestion des filtres, des comXre, quelques optimisations
[restcoda.git] / rest.wsgi
CommitLineData
1795efcd
TN
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4import sys, traceback
5
a6627284
TN
6# configuration (codes d'accès à la base MS-SQL)
7sys.path.append('/home/thomas/public_html/')
8import rest_config
9
0a4c31d4 10# pour savoir quel objet interroger : Routes
1795efcd 11from routes import Mapper
a6627284 12from routes.middleware import RoutesMiddleware
81b8aba3
TN
13# et analyse des paramètres (POST et query_string) pour filtrage
14from paste.request import parse_formvars
1795efcd 15
a6627284 16# pour chercher les données sur MS-SQL
1795efcd 17from pymssql import connect
a6627284
TN
18
19# pour afficher le résultat : jinja
1795efcd
TN
20from jinja import Environment, FileSystemLoader
21from jinja.filters import stringfilter
0ae81148 22import jinja.exceptions
85a89d99
TN
23# on prepare un environnement jinja toujours disponible
24ejinja = Environment(loader=FileSystemLoader('/home/thomas/public_html/'))
1795efcd 25
0a4c31d4
TN
26# TODO systeme de cache : beaker
27# from beaker.middleware import CacheMiddleware
1795efcd 28
a6627284 29# formats de sortie autorisés, et content-type correspondant
d588d902
TN
30# formats = { 'xml': 'application/xml', 'html': 'text/html', 'txt': 'text/plain', 'json': 'application/json', 'rss': 'application/rss+xml' }
31formats = { 'xml': 'application/xml', 'html': 'text/html', 'txt': 'text/plain', 'json': 'text/plain', 'rss': 'application/rss+xml' }
1795efcd 32
0a4c31d4 33# les routes RESTful (cf http://routes.groovie.org/manual.html#restful-services)
1795efcd 34mapper = Mapper()
0a4c31d4
TN
35mapper.resource('demlog','demlog')
36mapper.resource('comlog','comlog')
37mapper.resource('demdep','demdep')
38mapper.resource('comdep','comdep')
39mapper.resource('dempub','dempub')
40mapper.resource('compub','compub')
0a4c31d4 41mapper.resource('comare','comare')
85a89d99
TN
42mapper.resource('comarei','comare:(impl)',controller='comare') # pour les comarexxx où xxx est un code d'implantation
43mapper.resource('comsre','comsre')
44mapper.resource('comsrei','comsre:(impl)',controller='comsre')
45mapper.resource('comxre','comxre') # comxre = comare + comsre
46mapper.resource('comxrei','comxre:(impl)',controller='comxre')
f6b772ed
TN
47mapper.resource('dem','dem')
48mapper.resource('com','com')
1795efcd 49
85a89d99 50
1795efcd 51class objetsql(object):
0a4c31d4 52 """objet de base : dispose d'un accès à MS-SQL (lire les données) et d'un accès à jinja (rendu des données)"""
1795efcd 53 def __init__(self, environ):
1795efcd 54 self.environ = environ
85a89d99
TN
55 parse_formvars(environ) # extraction des variables de query_string et POST
56 self.filters = environ['paste.parsed_formvars'][0].dict_of_lists() # puis traduction en dictionnaire {var1:[val1,val2], ...}
0ae81148 57 def template(self,template):
0ae81148 58 self.outputformat = self.environ['wsgiorg.routing_args'][1].get('format','xml')
85a89d99 59 return ejinja.get_template('%s.%s' % (template,self.outputformat))
0ae81148 60 def cursor(self):
85a89d99
TN
61 if not hasattr(self,'db_connect'):
62 self.db_connect = connect(host=rest_config.host,user=rest_config.user,password=rest_config.password,database=rest_config.database)
63 if not hasattr(self,'db_cursor'):
64 self.db_cursor = self.db_connect.cursor()
65 return self.db_cursor
66 def __del__(self):
67 # coupe la connexion à MSSQL à la mort de l'objet (normalement ça se fait tout seul mais j'en suis pas sûr sûr)
68 if hasattr(self,'db_connect'):
69 db_connect.close()
a6627284 70
1795efcd 71class document(objetsql):
0a4c31d4 72 """objet document CODA (demlog, comlog, demdep... ils ont tous le même format)"""
85a89d99
TN
73 accept_like_filters = [ 'code', 'demandeur', 'approbateur', 'code_service', 'statut', 'fournisseur' ]
74 def __init__(self, environ, code_document='%', prefix_template='document'):
1795efcd
TN
75 super(document, self).__init__(environ)
76 self.code_document = code_document
85a89d99
TN
77 self.prefix_template = prefix_template
78 # filtrage : on extrait la liste des filtres "acceptes" pour en faire des where
79 sqlwhere = []
80 for filter in self.accept_like_filters:
81 if filter in self.filters:
82 or_list = []
83 for v in self.filters[filter]:
84 or_list.append("%s like '%s'" % (filter,v))
85 sqlwhere.append('(' + ' or '.join(or_list) + ')')
86 # TODO : ajouter filtres min_*/max_*, avant/apres dans ce sqlwhere
87 self.sqlwhere = ''
88 for f in sqlwhere:
89 self.sqlwhere += ' and ' + f
90 # filtrage : valeur en cas de limite ("top n" en ODBC), 50 maxi
91 if 'limite' in self.filters:
92 self.sqllimit = min( int(self.filters['limite'][0]), 50 )
93 else:
94 self.sqllimit = 15
95 # TODO : ajouter filtres tri_asc/tri_desc
1795efcd 96
0ae81148
TN
97 def _get_index(self, code):
98 """renvoie une liste de documents"""
99 # connexion a la base de données
100 cursor = self.cursor()
101 # extraction des données
85a89d99 102 cursor.execute("select top %d * from auf_v_acces_demcom where (code like '%s') %s order by date_modif desc" % ( self.sqllimit, code, self.sqlwhere ) )
0ae81148 103 items = []
1795efcd 104 while 1:
0ae81148
TN
105 item = dict_fetchone( cursor )
106 if item == None: break
107 item['code_rest'] = coda2rest(item['code'])
108 items.append(item)
85a89d99
TN
109 index = {}
110 index['code'] = code
0ae81148
TN
111 index['documents'] = items
112 return index
113
114 def _get_details(self,code,id):
f2897cb3 115 """renvoie la liste des détails pour un document"""
1795efcd 116 details = []
0ae81148 117 cursor = self.cursor()
85a89d99 118 cursor.execute("select * from auf_v_acces_dtls_demcom where (code like '%s') and (numero = %d)" % (code, id))
1795efcd 119 while 1:
0ae81148 120 detail = dict_fetchone(cursor)
1795efcd
TN
121 if detail == None: break
122 details.append(detail)
0ae81148
TN
123 return details
124
125 def _get_document(self,code,id):
f2897cb3 126 """renvoie un document"""
0ae81148 127 cursor = self.cursor()
85a89d99 128 cursor.execute("select top 1 * from auf_v_acces_demcom where (code like '%s') and (numero = %d) %s" % (code, id, self.sqlwhere))
0ae81148
TN
129 document = dict_fetchone(cursor)
130 if document == None:
131 raise "document inexistant"
132 document['code_rest'] = coda2rest( document['code'] )
133 document['details'] = self._get_details( code,id )
134 return document
135
136 def index(self):
137 """renvoie une liste de documents formatée"""
85a89d99
TN
138 template = self.template('%s-index' % self.prefix_template) # préparation du modèle
139 index = self._get_index( self.code_document ) # extraction des documents concernés
140 output = template.render( index ) # formattage via le modèle
0ae81148
TN
141 return self.outputformat, output
142
143 def show(self):
144 """renvoie un document formaté"""
145 # return 'txt', '%s' % self.environ # pour debug
146 id = int(self.environ['wsgiorg.routing_args'][1]['id'])
85a89d99 147 template = self.template( self.prefix_template )
0ae81148
TN
148 document = self._get_document( self.code_document, id )
149 output = template.render( document )
1795efcd
TN
150 return self.outputformat, output
151
f6b772ed
TN
152class dem(document):
153 def __init__(self, environ):
154 super(dem, self).__init__(environ, code_document = 'DEM-%')
155
156class com(document):
157 def __init__(self, environ):
158 super(com, self).__init__(environ, code_document = 'COM-%')
159
1795efcd
TN
160class demlog(document):
161 def __init__(self, environ):
162 super(demlog, self).__init__(environ, code_document = 'DEM-LOG-AUF')
163
164class comlog(document):
165 def __init__(self, environ):
166 super(comlog, self).__init__(environ, code_document = 'COM-LOG-AUF')
167
0a4c31d4
TN
168class demdep(document):
169 def __init__(self, environ):
170 super(demdep, self).__init__(environ, code_document = 'DEM-DEP-AUF')
171
172class comdep(document):
173 def __init__(self, environ):
174 super(comdep, self).__init__(environ, code_document = 'COM-DEP-AUF')
175
176class dempub(document):
177 def __init__(self, environ):
178 super(dempub, self).__init__(environ, code_document = 'DEM-PUB-AUF')
179
180class compub(document):
181 def __init__(self, environ):
182 super(compub, self).__init__(environ, code_document = 'COM-PUB-AUF')
183
0a4c31d4
TN
184class comsre(document):
185 def __init__(self, environ):
85a89d99 186 # on distingue comsre et comsrexxx (avec xxx = implantation)
db1681c0
TN
187 impl = environ['wsgiorg.routing_args'][1].get('impl','%')
188 super(comsre, self).__init__(environ, code_document = 'COM-SRE-%s' % impl)
0a4c31d4
TN
189
190class comare(document):
191 def __init__(self, environ):
db1681c0
TN
192 impl = environ['wsgiorg.routing_args'][1].get('impl','%')
193 super(comare, self).__init__(environ, code_document = 'COM-ARE-%s' % impl)
0a4c31d4 194
85a89d99
TN
195class comxre(document):
196 def __init__(self, environ):
197 impl = environ['wsgiorg.routing_args'][1].get('impl','%')
198 super(comxre, self).__init__(environ, code_document = 'COM-%%RE-%s' % impl)
199
0ae81148
TN
200#
201# fin des objets accessibles
202#
203
a6627284 204def dispatcher(environ, start_response):
81b8aba3 205 """dispatch vers la bonne methode du bon objet, et retour WSGI"""
a6627284 206 results = environ['wsgiorg.routing_args'][1]
1795efcd
TN
207 try:
208 target_class = globals()[results['controller']]
0ae81148
TN
209 method = getattr(target_class,results['action'])
210 except:
211 start_response("404 Not Found", [('Content-type', 'text/html')])
212 return '<html><body><h2>404 objet ou action invalide</h2><pre>%s: %s\n%s</pre></body></html>' % ( sys.exc_info()[0] , sys.exc_info()[1] , traceback.format_exc())
213 try:
1795efcd
TN
214 type, output = method(target_class(environ))
215 start_response("200 OK", [('Content-type', formats[type])])
216 return output.encode('utf-8')
0ae81148
TN
217 except jinja.exceptions.TemplateNotFound, template:
218 start_response("415 Unsupported Media Type", [('Content-type', 'text/html')])
219 return '<html><body><h2>415 format non supporté (%s inexistant)</h2></body></html>' % template
1795efcd 220 except:
0ae81148
TN
221 start_response("500 INTERNAL ERROR", [('Content-type', 'text/html')])
222 return '<html><body><h2>500 erreur lors du traitement</h2><pre>%s: %s\n%s</pre></body></html>' % ( sys.exc_info()[0] , sys.exc_info()[1] , traceback.format_exc())
1795efcd 223
81b8aba3 224# application qui sera lancée par mod_wsgi : on route et on dispatche
a6627284 225application = RoutesMiddleware( dispatcher, mapper)
0ae81148 226# TODO : ajouter un middleware de cache (beaker, basé sur REQUEST_URI quand la methode est GET)
1795efcd 227
0a4c31d4
TN
228#
229# petits utilitaires
230#
1795efcd
TN
231def dict_fetchone(cursor):
232 """Renvoie le resultat d'un fetchone dans un dictionnaire"""
233 result = cursor.fetchone()
234 if result == None: return None
235 result_dict = {}
236 for i in range(len(result)):
237 if isinstance( result[i], str ):
238 result_dict[cursor.description[i][0]] = result[i].decode('iso-8859-1')
239 else:
240 result_dict[cursor.description[i][0]] = result[i]
241 return result_dict
242
a6627284 243import re
db1681c0 244p = re.compile('(dem|com)-(...)-(...)',re.IGNORECASE)
a6627284 245def coda2rest(value):
db1681c0
TN
246 """Traduit un nom CODA vers la base REST correspodante,
247 par exemple DEM-LOG-AUF en demlog ou COM-ARE-VN3 en comarevn3"""
a6627284
TN
248 m = p.search(value)
249 if m == None: return value
db1681c0
TN
250 if m.group(3).lower() == 'auf':
251 return m.group(1).lower() + m.group(2).lower()
252 else:
253 return m.group(1).lower() + m.group(2).lower() + m.group(3).lower()
a6627284 254