1 #!/usr/bin/env python 2 3 """ 4 Resources for use with WebStack. 5 6 Copyright (C) 2005, 2006, 2007, 2008 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU Lesser General Public License as published by the Free 10 Software Foundation; either version 3 of the License, or (at your option) any 11 later version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 16 details. 17 18 You should have received a copy of the GNU Lesser General Public License along 19 with this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 import WebStack.Generic 23 import XSLForms.Fields 24 import XSLForms.Prepare 25 import XSLForms.Output 26 import XSLForms.Utils 27 import XSLForms.Resources.Common 28 from XSLTools import XSLOutput 29 import os 30 31 class XSLFormsResource(XSLForms.Resources.Common.CommonResource): 32 33 """ 34 A generic XSLForms resource for use with WebStack. 35 36 When overriding this class, define the following attributes appropriately: 37 38 * template_resources - a dictionary mapping output identifiers to 39 (template_filename, output_filename) tuples, 40 indicating the template and stylesheet filenames 41 to be employed 42 43 * in_page_resources - a dictionary mapping fragment identifiers to 44 (output_identifier, output_filename, 45 node_identifier) tuples, indicating the output 46 identifier for which the fragment applies, the 47 stylesheet filename to be employed, along with 48 the node identifier used in the original 49 template and output documents to mark a region 50 of those documents as the fragment to be updated 51 upon "in-page" requests 52 53 * init_resources - a dictionary mapping initialiser/input 54 identifiers to (template_filename, 55 input_filename) tuples, indicating the template 56 and initialiser/input stylesheet filenames to be 57 employed 58 59 * transform_resources - a dictionary mapping transform identifiers to 60 lists of stylesheet filenames for use with the 61 transformation methods 62 63 * document_resources - a dictionary mapping document identifiers to 64 single filenames for use as source documents or 65 as references with the transformation methods 66 67 * resource_dir - the absolute path of the directory in which 68 stylesheet resources are to reside 69 70 All filenames shall be simple leafnames for files residing in the resource's 71 special resource directory 'resource_dir'. 72 73 The following attributes may also be specified: 74 75 * path_encoding - the assumed encoding of characters in request 76 paths 77 78 * encoding - the assumed encoding of characters in request 79 bodies 80 81 To provide actual functionality to resources, either override the 82 'respond_to_form' method and write the code for obtaining input, 83 initialising documents, creating output, and so on in that method, or 84 provide implementations for the following methods: 85 86 * select_activity - sets the activity name which will be used by the 87 default implementations of the other methods 88 89 * create_document - creates or obtains a document for the resource's 90 activity (need not be overridden) 91 92 * respond_to_input - application logic relying on any input from the 93 request, including submitted document 94 information 95 96 * init_document - initialises the document according to the 97 'init_resources' attribute described above (need 98 not be overridden) 99 100 * respond_to_document - application logic relying on any information 101 from the initialised document 102 103 * create_output - creates and sends final output to the user (need 104 not be overridden) 105 """ 106 107 #path_encoding = "utf-8" 108 #encoding = "utf-8" 109 110 template_resources = {} 111 in_page_resources = {} 112 init_resources = {} 113 transform_resources = {} 114 115 def clean_parameters(self, parameters): 116 117 """ 118 Workaround stray zero value characters from Konqueror in XMLHttpRequest 119 communications. 120 """ 121 122 for name, values in parameters.items(): 123 new_values = [] 124 for value in values: 125 if isinstance(value, (str, unicode)) and value.endswith("\x00"): 126 new_values.append(value[:-1]) 127 else: 128 new_values.append(value) 129 parameters[name] = new_values 130 131 def prepare_output(self, output_identifier): 132 133 """ 134 Prepare the output stylesheets using the given 'output_identifier' to 135 indicate which templates and stylesheets are to be employed in the 136 production of output from the resource. 137 138 The 'output_identifier' is used as a key to the 'template_resources' 139 dictionary attribute. 140 141 Return the full path to the output stylesheet for use with 'send_output' 142 or 'get_result'. 143 """ 144 145 template_path, output_path = prepare_output(self, output_identifier) 146 return output_path 147 148 def prepare_fragment(self, fragment_identifier): 149 150 """ 151 Prepare the output stylesheets for the given 'fragment_identifier', 152 indicating which templates and stylesheets are to be employed in the 153 production of output from the resource. 154 155 The 'fragment_identifier' is used as a key to the 'in_page_resources' 156 dictionary attribute which in turn obtains an 'output_identifier', which 157 is used as a key to the 'template_resources' dictionary attribute. 158 159 Return the full path to the output stylesheet for use with 'send_output' 160 or 'get_result'. 161 """ 162 163 template_path, fragment_path = prepare_fragment(self, fragment_identifier) 164 return fragment_path 165 166 def prepare_parameters(self, parameters): 167 168 """ 169 Prepare the stylesheet parameters from the given request 'parameters'. 170 This is most useful when preparing fragments for in-page update output. 171 """ 172 173 element_path = parameters.get("element-path", [""])[0] 174 if element_path: 175 return {"element-path" : element_path} 176 else: 177 return {} 178 179 def send_output(self, trans, stylesheet_filenames, document, stylesheet_parameters=None, 180 stylesheet_expressions=None, references=None): 181 182 """ 183 Send the output from the resource to the user employing the transaction 184 'trans', stylesheets having the given 'stylesheet_filenames', the 185 'document' upon which the output will be based, the optional parameters 186 as defined in the 'stylesheet_parameters' dictionary, the optional 187 expressions are defined in the 'stylesheet_expressions' dictionary, and 188 the optional 'references' to external documents. 189 """ 190 191 # Sanity check for the filenames list. 192 193 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 194 raise ValueError, stylesheet_filenames 195 196 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 197 expressions=stylesheet_expressions, references=references) 198 proc.send_output(trans.get_response_stream(), trans.get_response_stream_encoding(), 199 document) 200 201 def get_result(self, stylesheet_filenames, document, stylesheet_parameters=None, 202 stylesheet_expressions=None, references=None): 203 204 """ 205 Get the result of applying a transformation using stylesheets with the 206 given 'stylesheet_filenames', the 'document' upon which the result will 207 be based, the optional parameters as defined in the 208 'stylesheet_parameters' dictionary, the optional parameters as defined 209 in the 'stylesheet_parameters' dictionary and the optional 'references' 210 to external documents. 211 """ 212 213 # Sanity check for the filenames list. 214 215 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 216 raise ValueError, stylesheet_filenames 217 218 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 219 expressions=stylesheet_expressions, references=references) 220 return proc.get_result(document) 221 222 def prepare_initialiser(self, input_identifier, init_enumerations=1): 223 224 """ 225 Prepare an initialiser/input transformation using the given 226 'input_identifier'. The optional 'init_enumerations' (defaulting to 227 true) may be used to indicate whether enumerations are to be initialised 228 from external documents. 229 230 Return the full path to the input stylesheet for use with 'send_output' 231 or 'get_result'. 232 """ 233 234 template_path, input_path = prepare_initialiser(self, input_identifier, init_enumerations) 235 return input_path 236 237 def prepare_transform(self, transform_identifier): 238 239 """ 240 Prepare a transformation using the given 'transform_identifier'. 241 242 Return a list of full paths to the output stylesheets for use with 243 'send_output' or 'get_result'. 244 """ 245 246 filenames = self.transform_resources[transform_identifier] 247 248 # Sanity check for the filenames list. 249 250 if isinstance(filenames, str) or isinstance(filenames, unicode): 251 raise ValueError, filenames 252 253 paths = [] 254 for filename in filenames: 255 paths.append(os.path.abspath(os.path.join(self.resource_dir, filename))) 256 return paths 257 258 def _get_in_page_resource(self, trans): 259 260 """ 261 Return the in-page resource being referred to in the given transaction 262 'trans'. 263 """ 264 265 if hasattr(self, "path_encoding"): 266 return trans.get_path_info(self.path_encoding).split("/")[-1] 267 else: 268 return trans.get_path_info().split("/")[-1] 269 270 def get_in_page_resource(self, trans): 271 272 """ 273 Return the in-page resource being referred to in the given transaction 274 'trans' or None if no valid in-page resource is being referenced. 275 """ 276 277 name = self._get_in_page_resource(trans) 278 if self.in_page_resources.has_key(name): 279 return name 280 else: 281 return None 282 283 def respond(self, trans): 284 285 """ 286 Respond to the request described by the given transaction 'trans'. 287 """ 288 289 # Only obtain field information according to the stated method. 290 291 content_type = trans.get_content_type() 292 method = trans.get_request_method() 293 in_page_resource = self.get_in_page_resource(trans) 294 295 # Handle typical request methods, processing request information. 296 297 if method == "GET": 298 299 # Get the fields from the request path (URL). 300 301 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 302 parameters = trans.get_fields_from_path() 303 form.set_parameters(parameters) 304 305 elif method == "POST" and content_type.media_type in ( 306 "application/x-www-form-urlencoded", "multipart/form-data"): 307 308 # Get the fields from the request body. 309 310 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 311 if hasattr(self, "encoding"): 312 parameters = trans.get_fields_from_body(self.encoding) 313 else: 314 parameters = trans.get_fields_from_body() 315 316 # NOTE: Konqueror workaround. 317 self.clean_parameters(parameters) 318 319 form.set_parameters(parameters) 320 321 else: 322 323 # Initialise empty container. 324 325 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 326 327 # Call an overridden method with the processed request information. 328 329 self.respond_to_form(trans, form) 330 331 def respond_to_form(self, trans, form): 332 333 """ 334 Respond to the request described by the given transaction 'trans', using 335 the given 'form' object to conveniently retrieve field (request 336 parameter) information and structured form information (as DOM-style XML 337 documents). 338 """ 339 340 self.select_activity(trans, form) 341 self.create_document(trans, form) 342 self.respond_to_input(trans, form) 343 self.init_document(trans, form) 344 self.respond_to_document(trans, form) 345 self.create_output(trans, form) 346 raise WebStack.Generic.EndOfResponse 347 348 # Modular methods for responding to requests. 349 350 def select_activity(self, trans, form): 351 352 """ 353 Using the given transaction 'trans' and 'form' information, select the 354 activity being performed and set the 'current_activity' attribute in the 355 transaction. 356 """ 357 358 pass 359 360 def create_document(self, trans, form): 361 362 """ 363 Using the given transaction 'trans' and 'form' information, create the 364 document involved in the current activity and set the 'current_document' 365 attribute in the transaction. 366 367 Return whether a new document was created. 368 """ 369 370 documents = form.get_documents() 371 activity = form.get_activity() 372 373 if documents.has_key(activity): 374 form.set_document(documents[activity]) 375 return 0 376 else: 377 form.new_document(activity) 378 form.new_documents.add(activity) 379 return 1 380 381 def respond_to_input(self, trans, form): 382 383 """ 384 Using the given transaction 'trans' and 'form' information, perform the 385 parts of the current activity which rely on the information supplied in 386 the current document. 387 """ 388 389 pass 390 391 def init_document(self, trans, form, stylesheet_parameters=None, 392 stylesheet_expressions=None, references=None): 393 394 """ 395 Using the given transaction 'trans' and 'form' information, initialise 396 the current document. 397 """ 398 399 activity = form.get_activity() 400 401 # Transform, adding enumerations/ranges. 402 403 if self.init_resources.has_key(activity): 404 init_xsl = self.prepare_initialiser(activity) 405 form.set_document( 406 self.get_result( 407 [init_xsl], form.get_document(), stylesheet_parameters, 408 stylesheet_expressions, references 409 ) 410 ) 411 412 def respond_to_document(self, trans, form): 413 414 """ 415 Using the given transaction 'trans' and 'form' information, perform the 416 parts of the current activity which rely on a populated version of the 417 current document. 418 """ 419 420 pass 421 422 def create_output(self, trans, form, content_type=None, 423 stylesheet_parameters=None, stylesheet_expressions=None, references=None): 424 425 """ 426 Using the given transaction 'trans' and 'form' information, create the 427 output for the current activity using the previously set attributes in 428 the transaction. 429 """ 430 431 attributes = trans.get_attributes() 432 in_page_resource = self.get_in_page_resource(trans) 433 parameters = form.get_parameters() 434 435 # Start the response. 436 437 if attributes.has_key("encoding"): 438 encoding = attributes["encoding"] # NOTE: Potentially redundant. 439 elif hasattr(self, "encoding"): 440 encoding = self.encoding 441 else: 442 encoding = trans.default_charset 443 444 content_type = content_type or WebStack.Generic.ContentType("application/xhtml+xml", encoding) 445 trans.set_content_type(content_type) 446 447 # Ensure that an output stylesheet exists. 448 449 stylesheet_parameters = stylesheet_parameters or {} 450 451 if in_page_resource: 452 trans_xsl = self.prepare_fragment(in_page_resource) 453 stylesheet_parameters.update(self.prepare_parameters(parameters)) 454 else: 455 trans_xsl = self.prepare_output(form.get_activity()) 456 457 # Complete the response. 458 459 self.send_output(trans, [trans_xsl], form.get_document(), 460 stylesheet_parameters, stylesheet_expressions, references) 461 462 # General helper methods. 463 464 def add_elements(self, positions, *element_names): 465 466 """ 467 At the given 'positions', typically obtained as "selectors", add the 468 hierarchy of elements given in the 'element_names' parameters. 469 """ 470 471 XSLForms.Utils.add_elements(positions, *element_names) 472 473 def remove_elements(self, positions): 474 475 """ 476 Remove elements at the given 'positions', typically obtained as 477 "selectors". 478 """ 479 480 XSLForms.Utils.remove_elements(positions) 481 482 def prepare_output(self, output_identifier): 483 484 """ 485 Prepare the output stylesheet for the resource class or object 'self' 486 corresponding to the given 'output_identifier'. Return the template path 487 and the output stylesheet path in a 2-tuple. 488 """ 489 490 template_filename, output_filename = self.template_resources[output_identifier] 491 output_path = os.path.abspath(os.path.join(self.resource_dir, output_filename)) 492 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 493 XSLForms.Prepare.ensure_stylesheet(template_path, output_path) 494 return template_path, output_path 495 496 def prepare_fragment(self, fragment_identifier): 497 498 """ 499 Prepare the output stylesheet for the resource class or object 'self' 500 corresponding to the given 'fragment_identifier'. Return the template path 501 and the output stylesheet path in a 2-tuple. 502 """ 503 504 output_identifier, fragment_filename, node_identifier = self.in_page_resources[fragment_identifier] 505 fragment_path = os.path.abspath(os.path.join(self.resource_dir, fragment_filename)) 506 template_filename, output_filename = self.template_resources[output_identifier] 507 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 508 XSLForms.Prepare.ensure_stylesheet_fragment(template_path, fragment_path, node_identifier) 509 return template_path, fragment_path 510 511 def prepare_initialiser(self, input_identifier, init_enumerations): 512 513 """ 514 Prepare the initialising stylesheet for the resource class or object 'self' 515 corresponding to the given 'input_identifier' and 'init_enumerations' flag. 516 Return the template path and the initialising stylesheet path in a 2-tuple. 517 """ 518 519 template_filename, input_filename = self.init_resources[input_identifier] 520 input_path = os.path.abspath(os.path.join(self.resource_dir, input_filename)) 521 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 522 XSLForms.Prepare.ensure_input_stylesheet(template_path, input_path, init_enumerations) 523 return template_path, input_path 524 525 def prepare_resources(cls): 526 527 "Prepare the resources associated with the class 'cls'." 528 529 for output_identifier in cls.template_resources.keys(): 530 prepare_output(cls, output_identifier) 531 for fragment_identifier in cls.in_page_resources.keys(): 532 prepare_fragment(cls, fragment_identifier) 533 534 # NOTE: Using init_enumerations=1 here. 535 536 for input_identifier in cls.init_resources.keys(): 537 prepare_initialiser(cls, input_identifier, 1) 538 539 # Convenience methods for specifying resources. 540 541 def split(filename): 542 543 """ 544 Return a tuple containing the directory and filename without extension for 545 'filename'. 546 """ 547 548 d, leafname = os.path.split(filename) 549 name, ext = os.path.splitext(leafname) 550 return d, name 551 552 def output(template_filename): 553 554 """ 555 Return a tuple containing the 'template_filename' and a suitable output 556 stylesheet filename. 557 """ 558 559 d, name = split(template_filename) 560 output_name = name.replace("_template", "_output") + os.path.extsep + "xsl" 561 return (template_filename, os.path.join(d, output_name)) 562 563 def input(template_filename): 564 565 """ 566 Return a tuple containing the 'template_filename' and a suitable output 567 stylesheet filename. 568 """ 569 570 d, name = split(template_filename) 571 input_name = name.replace("_template", "_input") + os.path.extsep + "xsl" 572 return (template_filename, os.path.join(d, input_name)) 573 574 def resources(filename, d="Resources"): 575 576 """ 577 Return the resource directory for the given 'filename', using the optional 578 directory name 'd' to indicate the directory relative to the directory of 579 'filename' (or the default directory name, indicating that the directory 580 called "Resources" - a sibling of 'filename' - is the resource directory). 581 582 It is envisaged that callers provide the value of the __file__ special 583 variable to get the resource directory relative to a particular module. 584 """ 585 586 return os.path.join(os.path.split(filename)[0], d) 587 588 # vim: tabstop=4 expandtab shiftwidth=4