Changeset 9130

Show
Ignore:
Timestamp:
06/20/08 16:59:10 (5 months ago)
Author:
matthias
Message:

* Reorganised module directory structure
* Added basic support for layouts
* Refecatored some of the modules (split them up in seperate modules and functions, introduce private functions, cleanup experimental code)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • sandbox/aida/modules/aida/controller.js

    r9129 r9130  
    44var logger = logging.getLogger(__name__); 
    55 
    6 importModule('helma.skin'); 
    7 importModule('templating.este', 'este'); 
    8 importModule('templating.ejs', 'ejs'); 
    96importModule('javascript.prototype'); 
    10 importModule('routing'); 
    11 importFromModule("filters", "*"); 
     7 
     8importModule("controller.routing", "routing"); 
     9importFromModule("controller.filters", "*"); 
     10importFromModule("controller.helpers", "*"); 
     11importFromModule("controller.templates", "*"); 
     12importFromModule("controller.actions", "*"); 
     13importFromModule("controller.layout", "*"); 
    1214 
    1315importFromModule("config.environments.development.development", "config"); 
    1416 
    1517this.context = {}; 
    16 this.helpers = []; 
    1718 
    1819function getControllerInstance(name, req, res, session) { 
     
    5455   controller.actionName = req.route.action; 
    5556    
    56    var content = ""; 
     57   controller.content = ""; 
    5758   if (!controller.beforeFiltersPass()) return; 
    58    content = controller.callAction(req.route.handler, req, res, session); 
    59    content = controller.applyAfterFilters(content, req, res, session); 
    60    res.write(content); 
     59   controller.callAction(req.route.handler); 
     60   controller.applyAfterFilters(); 
     61   controller.renderLayout(); 
    6162} 
    6263 
    63  
    64 function callAction(handler, req, res, session) { 
    65    res.push(); 
    66 //   try { 
    67       handler.call(this); 
    68 /*   } catch(e) { 
    69       res.write(e.rhinoException); 
    70    } */ 
    71    if (!res.calledRender) this.render(); 
    72    content = res.pop(); 
    73    return content; 
    74 } 
    75  
    76 /** 
    77  * Returns the handler function (action), that matches the given route. 
    78  * This function will be called by routing.handleRequest. 
    79  * <p>When a controller object processes a request, it looks for the corresponding instance method  
    80  * for the incoming action. The method can be defined as a property of the actions object within the  
    81  * controller, or as a method, which name ends with _action. It will look up the method in the following  
    82  * order. 
    83  * <ol> 
    84  *  <li>        actions.{format}.{method}.{action} 
    85  *  <li>        actions.{format}.{action} 
    86  *  <li>        actions.{action} 
    87  *  <li>        {action}_{method}_{format}_action 
    88  *  <li>        {action}_{action}_action 
    89  *  <li>        {action}_{method}_action 
    90  *  <li>        {action}_action 
    91  * </ol> 
    92  * <p>If it finds one, that method is invoked. If no method can be called, the controller looks for  
    93  * a template named after the current controller and action. If found, this template is rendered directly.  
    94  * If not, it will try to find a corresponding method for the action “notfound”, and that method is called.  
    95  * Because aida.controller, which is the parent of all Action Controllers defines a “notfound_action”,  
    96  * the controller always returns a result. 
    97  * <p>In contrast to Rails, not all public methods in a controller may be invoked as an action method.  
    98  * Instead you have to add “_action” to the method name, or make it a property of the actions object within  
    99  * the controller. Because of this fact there is no need for a method “hide_action”. 
    100  * <pre class=javascript> 
    101  * importModule(“aida.controller”, “Controller”); 
    102  * this.__proto__ = Controller; 
    103  *  
    104  * // will handle /controller/index 
    105  * function index_action() { 
    106  *       render(); 
    107  * } 
    108  *  
    109  * // will handle POST /controller/index 
    110  * function index_post_action() { 
    111  *   // handle post data 
    112  *   res.redirect("/" + this.getShortName()); // back to index 
    113  * } 
    114  * 
    115  * // will handle /controller/index.rss 
    116  * function index_rss_action() { 
    117  *   // print this content as rss 
    118  * } 
    119  *  
    120  * this.actions.json = { 
    121  *    // will handle /controller/index.json 
    122  *    index : function() { 
    123  *      // index as json 
    124  *    }, 
    125  *    // will handle POST /controller/index.json 
    126  *    index.post : function() { 
    127  *      // handle post data (submited as json)     
    128  *    } 
    129  * } 
    130  *  
    131  * </pre> 
    132  *  
    133  * 
    134  * @param {object} [route]    If route isn't passed to the function, we will fallback to req.route 
    135  * @config {string} action      Action name, that should be called 
    136  * @config {string} [format]    Format name, matching an incoming format (xml, html, json, ...) 
    137  * @config {string} [method]    HTTP-Moethod (post, get, delete, ...) 
    138  * @return {function}         Returns the matching handler function 
    139  * @see routing.handleRequest 
    140  */ 
    141 function getAction(route) { 
    142    var h; 
    143    var route = route || req.route; 
    144    logger.info("route.action:" + route.action); 
    145    var action = (route.action || "").toLowerCase().replace(".", "_").underscore(); 
    146    var method = (route.method || "").toLowerCase(); 
    147    var format = (route.format || "").toLowerCase(); 
    148     
    149    h = ( 
    150       this.actions && this.actions[format] && this.actions[format][method] && this.actions[format][method][action] || 
    151       this.actions && this.actions[format] && this.actions[format][action] || 
    152       this.actions && this.actions[action] 
    153    ) 
    154    if (!h) h = (    
    155       this[action + "_" + method + "_" + format + "_action"] || 
    156       this[action + "_" + method + "_action"] ||          
    157       this[action + "_" + format + "_action"] || 
    158       this[action + "_action"] 
    159    ) 
    160    if (!h && this.getTemplateSource().exists) { 
    161       h = function() { 
    162          render.call(this); 
    163       } 
    164    } 
    165    if (!h) h = ( 
    166       (action != "notfound") ? this.getAction({action:"notfound", format:format, method:method}) : null 
    167    ) 
    168    return h; 
    169 } 
    170  
    171 /** 
    172  * Writes content to the response. 
    173  *  
    174  * <p>Depending on the options you pass to the function it will do the following 
    175  * 
    176  * <dl>  
    177  * <dt>render()</dt><dd> 
    178  *  With no parameter, the render method renders the default template  
    179  *  for the current controller and action. The following code will render the  
    180  *  template <span class=path>app/views/blog/index.skin</span> 
    181  *  <pre class=javascript> 
    182  * importModule(“aida.controller”, “Controller”); 
    183  * this.__proto__ = Controller; 
    184  *  
    185  * function index() { 
    186  *      render(); 
    187  * } 
    188  *  </pre> 
    189  * 
    190  *  <p>render() may just be called once per request. If you don't call render() in your 
    191  *  action method, Controller, or to be more specific - handleRequest, will 
    192  *  perform it without any parameters. 
    193  *  <p>If you don't specify a handler method for the action, Action Controller will try 
    194  *  to find the corresponding template (skin) and call it. If it can't find a method nor  
    195  *  a template for the matching route (controller/action) it will call the action "notfound" 
    196  *  for the controller. 
    197  * </dd>  
    198  *  
    199  * <dt>render({text:<string>})</dt><dd> 
    200  *  <p>Sends the given string to the response.  
    201  *  No template interpretation or HTML escaping is performed. 
    202  *  <pre class=javascript> 
    203  * function index_action() { 
    204  *          render({text: "Hello there!"}); 
    205  * } 
    206  *  </pre> 
    207  * </dd> 
    208  * 
    209  * <dt>render({template:<string>, type:<skin|extension>, context:<object>})</dt><dd> 
    210  *  <p>Interprets string as the source to a template of the given type, rendering the  
    211  *  results back to the client. If the :locals hash is given, the contents are used  
    212  *  to set the values of local variables in the template.  
    213  *  <p>The following code adds method_missing to a controller if the application is  
    214  *  running in development mode. If the controller is called with an invalid action,  
    215  *  this renders an inline template to display the action’s name and a formatted version  
    216  *  of the request parameters. 
    217  *  <code class=javascript> 
    218  * function welcome_action() { 
    219  *        render({ 
    220  *     template:  
    221  *       '&lt;h2&gt;Hello &lt;% name %>!&lt;/h2&gt; \ 
    222  *        &lt;p&gt;Welcome, and have a nice day!&lt;/p&gt;', 
    223  *     context : { name : "Matthias" } 
    224  *   }); 
    225  * } 
    226  *  </code>  
    227  *  <p>Note: You have to end each line of the multi line string with a backslash, 
    228  *  otherwise you will get an “unterminated string literal” error from Rhino. 
    229  * </dd> 
    230  * 
    231  * <dt>render({action:<string>})</dt><dd> 
    232  *  <p>Renders the template for a given action in this controller. 
    233  *  <pre class=javascript> 
    234  * function display_cart_action() { 
    235  *        if (cart.isEmpty()) {  
    236  *     render({action:"index"}); 
    237  *   } else { 
    238  *     // ... 
    239  *   } 
    240  * } 
    241  *  </pre> 
    242  *  Note that calling render({action:...}) does not call the action method;  
    243  *  it simply displays the template. If the template needs instance variables,  
    244  *  these must be set up by the method that calls the render. 
    245  * </dd> 
    246  * </dl> 
    247  *  
    248  * @param {object} options Options for defining the output. See description. 
    249  */ 
    250 function render(options) { 
    251    if (!res.contentType) res.contentType = routing.Formats.getMimeType(req.route.format); 
    252    if (!options) options = {}; 
    253    if (res.calledRender) throw new DoubleRenderError(); 
    254    res.calledRender = true; 
    255    var action = options.action || req.route.action; 
    256    var context = {}; 
    257    for (var name in this.helpers) { 
    258       if (this.helpers[name]._namespace) { 
    259          context[this.helpers[name]._namespace] = this.helpers[name]; 
    260       } else { 
    261          context = Object.extend(context, this.helpers[name]);          
    262       } 
    263    } 
    264    context = Object.extend( 
    265       context, 
    266       { 
    267          request : req.data, 
    268          controller : this, 
    269          flash : req.flash, 
    270          headers : req.headers,  
    271          logger : logger,  
    272          params : req.data,  
    273          request : req,  
    274          response : res, 
    275          session : session    
    276       }, 
    277       this.context || {} 
    278    ); 
    279    if (options.context) { 
    280       context = Object.extend(context, options.context); 
    281    } 
    282     
    283    if (typeof options === "string" && options != "") { 
    284       res.writeln(options); 
    285    } else if (options && options.inline) { 
    286       var options = Object.extend({ 
    287          type : "skin", 
    288          locals : {} 
    289       }, options); 
    290       if (options.type === "skin") { 
    291          /* 
    292          var skin = []; 
    293          helma.skin.parseSkin(options.inline, function(part) { 
    294             skin.push(part); 
    295          }); 
    296          FIXME: can't create inline skin on the fly 
    297          */ 
    298          var skin = new helma.skin.Skin(options.inline); 
    299          skin.render(options.locals); 
    300       } 
    301    } else { 
    302       var type = this.determineTemplateType({action:action, format:req.route.format}); 
    303       if (type === "est") { 
    304          var resource = this.getTemplateSource({action:action, type:type}); 
    305          new este.TemplateEngine(resource.getContent()).evaluate(res, context); 
    306       } else if (type === "ejs") { 
    307          var resource = this.getTemplateSource({action:action, type:type}); 
    308          var result = new ejs.EJS(resource.getContent()).render(context); 
    309          res.write(result); 
    310       } else if (type === "skin") { 
    311          helma.skin.render(this.getTemplatePath({action:action, type:type}), context); 
    312       } else if (type) { 
    313          helma.skin.render(this.getTemplatePath({action:action, type:type}), context); 
    314       } else { 
    315          res.writeln("Couldn't find " + this.getTemplateSource({action:action, type:type || "skin"})); 
    316       } 
    317    } 
    318 } 
    31964 
    32065/** 
     
    32671} 
    32772 
    328 function determineTemplateType(options) { 
    329    var types = ["est", "ejs", "skin", options.format]; 
    330    for (var i=0; i<types.length; i++) { 
    331       if (this.getTemplateSource(Object.extend(options, {type:types[i]})).exists()) { 
    332          return types[i]; 
    333       } 
    334    } 
    335    return; 
    336 } 
    337  
    338 function getTemplateSource(options) { 
    339    return this.getResource(this.getTemplatePath(options)); 
    340 } 
    341  
    342 function getTemplatePath(options) { 
    343    logger.info(uneval(options)) 
    344    var options = Object.extend({ 
    345       action : "index", 
    346       controller : this.getShortName(), 
    347       format : "html", 
    348       type : "skin" 
    349    }, options || {}); 
    350    var result = (config.templateRoot || "app/views") + '/' + options.controller + '/' + options.action + '.' + options.format + ((options.type != options.format) ? ('.' + options.type) : ''); 
    351    return result; 
    352 } 
    353  
    354 function foo(name, value) { 
    355    this[name] = value; 
    356 } 
    35773 
    35874function getShortName() { 
     
    36177} 
    36278 
    363  
    364 var actions = { 
    365    notfound : function() { 
    366       res.writeln("notfound") 
    367    } 
    368 } 
    369  
    370 function notfound_action() { 
    371    res.writeln("notfound") 
    372 } 
    373  
    374 function _extends(proto) { 
    375    this.actions.__proto__ = proto.actions; 
    376 } 
    37779 
    37880// ERROR Objects 
     
    38385   } 
    38486} 
    385  
    386 function importHelpers(module, namespace) { 
    387    loadHelpers(this, module, namespace); 
    388 } 
    389  
    390  
    391 importFromModule("helma.file", "File"); 
    392 /** 
    393  * Loads and mount helpers from a file. 
    394  * Loads the helper file located at <tt>app/helpers/$name_helpers.js</tt> and 
    395  * adds it to the module. All loaded helpers will be available in the view. 
    396  * 
    397  * @param {object} module        controller module, where the helpers should be mounted 
    398  * @param {string} name          name of the helpers (file) 
    399  * @param {string} [namespace]   optional namespace, for the helpers which will be used in the view 
    400  * @return {object} helper module 
    401  */ 
    402 function loadHelpers(module, name, namespace) { 
    403    // FIXME: 
    404    var helpersDir = new File(APP_DIR + "/app/helpers");    
    405    var helpersPattern = /^([a-z][a-z_\-0-9]*_helpers).js$/i; 
    406    var helpers; 
    407    if (!module.helpers) module.helpers = {}; 
    408     
    409    // load helpers 
    410    var helperFile = new File(helpersDir, name + "_helpers.js"); 
    411    if (helperFile.exists()) { 
    412       importModule("app.helpers." + name + "_helpers"); 
    413       helpers = app.helpers[name + "_helpers"]; 
    414       if (namespace) helpers._namespace = namespace; 
    415       module.helpers[name] = helpers;       
    416       shell.writeln("loaded helpers for " + module.__name__ + " from " + helperFile);       
    417    } 
    418     
    419    return helpers; 
    420 }