Changeset 531 for Dev


Ignore:
Timestamp:
03/27/14 14:44:36 (11 years ago)
Author:
hendrikvanantwerpen
Message:
  • Return to using truly ISO formatted dates, including milliseconds.
  • Also set constraint on surveyrun dates when value is initially set.
  • Separate runs & results from surveys and questions.
  • Moved date & email format to schema itself.
Location:
Dev/trunk
Files:
4 added
14 edited

Legend:

Unmodified
Added
Removed
  • Dev/trunk/Gruntfile.js

    r522 r531  
    7474                       ,'heroku-config'
    7575                       ,'http:db-push-local-to-cloud']);
     76    grunt.registerTask('cloudant-url',
     77                       "Print the Cloudant URL to the console.",
     78                       ['path-check:heroku'
     79                       ,'heroku-config'
     80                       ,'print:herokuConfig.CLOUDANT_URL:CLOUDANT_URL'
     81                       ]);
    7682    grunt.registerTask('#',
    7783                       "---\nTASKS BELOW ARE INTERNAL AND SHOULD NOT USUALLY BE CALLED FROM THE COMMAND-LINE\n---",
     
    304310    grunt.loadTasks('./grunt-tasks');
    305311
     312    grunt.registerTask('print',"Print a variable.",
     313                       function(varname,name) {
     314                            grunt.log.write(grunt.template.process((name||varname)+"=<%= "+varname+" %>"));
     315                       });
     316
    306317    // UTIL FUNCTIONS
    307318
  • Dev/trunk/src/client/qed-client/model/classes/_Class.js

    r525 r531  
    6464        },
    6565        _formatDate: function(date) {
    66             return stamp.toISOString(date,{zulu:true,milliseconds:false});
     66            return stamp.toISOString(date,{zulu:true,milliseconds:true});
    6767        },
    6868        _sanitize: function(obj) {
  • Dev/trunk/src/client/qed-client/model/widgets/SurveyRunWidget.js

    r513 r531  
    1111            var endDateBox = this.endDateBox;
    1212            this.own(this.startDateBox.on('change', function(value){
    13                 endDateBox.constraints.min  = value;
     13                endDateBox.constraints.min = value;
    1414            }));
    1515        },
     
    2424                value.respondentCanDeleteOwnResponse ? ["on"] : [];
    2525            this.inherited(arguments);
     26            this.endDateBox.constraints.min =
     27                this.startDateBox.get('value');
    2628        }
    2729    });
  • Dev/trunk/src/client/qed-client/pages/surveys.js

    r529 r531  
    1818            this.draftsTab.set('title','Drafts (<span class="qedLoading"></span>)');
    1919            this.publishedTab.set('title','Published (<span class="qedLoading"></span>)');
    20             this.runsTab.set('title','Runs (<span class="qedLoading"></span>)');
    2120            this.refresh();
    2221        },
     
    5655        _onRunSurvey:function(survey){
    5756            var surveyRun = surveyRuns.create();
    58             surveyRun.title = 'Run of "' + survey.title + '" of '+(new Date().toString());
     57            surveyRun.title = 'Run of "'+survey.title+'" of '+
     58                              (new Date().toString());
    5959            surveyRun.survey = survey;
    6060            surveyRuns.save(surveyRun)
    6161            .then(lang.hitch(this,function(surveyRun){
    62                 this._onRunDetails(surveyRun);
    63             }),lang.hitch(this,function(err){
    64                 this.notify(err.error,'error');
    65             }));
    66         },
    67         _onRunDetails: function(surveyRun) {
    68             Router.go(surveyRuns.getObjectPath(surveyRun));
    69         },
    70         _onRunDelete: function(surveyRun) {
    71             if ( !confirm("Are you sure you want to delete this survey run?") ) {
    72                 return;
    73             }
    74             surveyRuns.remove(surveyRun)
    75             .then(lang.hitch(this,function(){
    76                 this.notify("SurveyRun successfully deleted.");
    77                 this.refreshRuns();
     62                Router.go(surveyRuns.getObjectPath(
     63                    surveyRuns.getId(surveyRun)));
    7864            }),lang.hitch(this,function(err){
    7965                this.notify(err.error,'error');
     
    8369            this.refreshDrafts();
    8470            this.refreshPublished();
    85             this.refreshRuns();
    8671        },
    8772        refreshDrafts: function() {
    88             this.draftsContainer.set('content','');
     73            this.draftsContainer.set('content','Loading draft surveys <span class="qedLoading"></span>');
    8974            when(surveys.query({drafts:true}), lang.hitch(this,function(surveys) {
    9075                this.draftsTab.set('title','Drafts ('+surveys.length+')');
     76                this.draftsContainer.set('content','');
    9177                array.forEach(surveys,function(survey){
    9278                    var w = new LineWithActionsWidget({
     
    127113        },
    128114        refreshPublished: function() {
    129             this.publishedContainer.set('content','');
     115            this.publishedContainer.set('content','Loading published surveys <span class="qedLoading"></span>');
    130116            when(surveys.query({published:true}), lang.hitch(this, function(surveys) {
     117                this.publishedContainer.set('content','');
    131118                this.publishedTab.set('title','Published ('+surveys.length+')');
    132119                array.forEach(surveys,function(survey){
     
    152139                },this);
    153140            }));
    154         },
    155         refreshRuns: function() {
    156             this.runsContainer.set('content','');
    157             when(surveyRuns.query(), lang.hitch(this,function(surveyRuns){
    158                 this.runsTab.set('title','Runs ('+surveyRuns.length+')');
    159                 array.forEach(surveyRuns,function(surveyRun){
    160                     var w = new LineWithActionsWidget({
    161                         title: surveyRun.title || "",
    162                         actions:[{
    163                             callback: lang.hitch(this,'_onRunDetails',surveyRun),
    164                             properties: {
    165                                 label: 'Details',
    166                                 tooltip: 'Show details for this run',
    167                                 icon: 'Edit'
    168                             }
    169                         },{
    170                             callback: lang.hitch(this,'_onRunDelete',surveyRun),
    171                             properties: {
    172                                 label: 'Delete',
    173                                 tooltip: 'Delete this run',
    174                                 icon: 'Delete'
    175                             }
    176                         }]
    177                     });
    178                     this.runsContainer.addChild(w);
    179                 },this);
    180             }));
    181141        }
    182142    });
  • Dev/trunk/src/client/qed-client/pages/templates/surveys.html

    r529 r531  
    3434        </div>
    3535
    36         <div data-dojo-type="dijit/layout/BorderContainer"
    37              title="Runs"
    38              data-dojo-attach-point="runsTab">
    39             <div data-dojo-type="dijit/layout/ContentPane"
    40                  data-dojo-props="region: 'center'"
    41                  data-dojo-attach-point="runsContainer">
    42             </div>
    43             <div data-dojo-type="dijit/layout/ContentPane"
    44                  data-dojo-props="region: 'bottom'"
    45                  style="height: 40px;">
    46             </div>
    47         </div>
    4836    </div>
    4937
  • Dev/trunk/src/client/qed-client/routes.js

    r490 r531  
    11define([
    22    "./model/classes/questions",
     3    "./model/classes/sessions",
    34    "./model/classes/surveyRuns",
    45    "./model/classes/surveys",
     
    910    "./pages/survey",
    1011    "./pages/surveyRun",
     12    "./pages/surveyRuns",
    1113    "./pages/surveys"
    12 ], function(questionsClass, surveyRunsClass, surveysClass, index, previewSurvey, question, questions, survey, surveyRun, surveys) {
     14], function(questionsClass, sessionsClass, surveyRunsClass, surveysClass, index, previewSurvey, question, questions, survey, surveyRun, surveyRuns, surveys) {
    1315
    1416    return [
     
    1921        { path: surveysClass.getCollectionPath(), constructor: surveys },
    2022        { path: surveysClass.getObjectPath(':objectId'), constructor: survey },
     23        { path: surveyRunsClass.getCollectionPath(), constructor: surveyRuns },
    2124        { path: surveyRunsClass.getObjectPath(':objectId'), constructor: surveyRun },
    22         //{ path: "/sessions", constructor: sessions },
    23         //{ path: "/session/:sessionId", constructor: session },
     25        //{ path: sessionsClass.getObjectPath(''), constructor: sessions },
     26        //{ path: sessionsClass.getObjectPath(':objectId'), constructor: session },
    2427        { path: surveysClass.getPreviewPath(':surveyId'), constructor: previewSurvey }
    2528    ];
  • Dev/trunk/src/client/qed-client/ui/templates/MainMenu.html

    r466 r531  
    11<div class="mainMenu">
    22    <div  data-dojo-type="dijit/MenuBar">
    3         <div class="rftMainMenuButton" data-dojo-type="./MenuBarLink" data-dojo-props="path:'/sessions'">Sessions</div>
    4         <div class="rftMainMenuButton" data-dojo-type="dijit/PopupMenuBarItem">
     3        <div class="rftMainMenuButton"
     4             data-dojo-type="./MenuBarLink" data-dojo-props="path:'/sessions'">Sessions</div>
     5        <div class="rftMainMenuButton"
     6             data-dojo-type="dijit/PopupMenuBarItem">
    57            <span>Content</span>
    68            <div data-dojo-type="dijit/DropDownMenu">
    7                 <div data-dojo-type="./MenuLink" class="blue bgColorHover" data-dojo-props="path:'/surveys', iconClass:'rftIcon rftIconSurvey'">Surveys</div>
    8                 <div data-dojo-type="./MenuLink" class="orange bgColorHover" data-dojo-props="path:'/questions', iconClass:'rftIcon rftIconQuestion'">Questions</div>
    9                 <div data-dojo-type="./MenuLink" class="purple bgColorHover" data-dojo-props="path:'/applications', iconClass: 'rftIcon rftIconApplication'">Applications</div>
    10                 <div data-dojo-type="./MenuLink" class="red bgColorHover" data-dojo-props="path:'/dashboards', iconClass: 'rftIcon rftIconDashboard'">Dashboards</div>
     9                <div data-dojo-type="./MenuLink"
     10                     class="blue bgColorHover"
     11                     data-dojo-props="path:'/surveys', iconClass:'rftIcon rftIconSurvey'">
     12                  Surveys</div>
     13                <div data-dojo-type="./MenuLink"
     14                     class="orange bgColorHover"
     15                     data-dojo-props="path:'/questions', iconClass:'rftIcon rftIconQuestion'">
     16                  Questions</div>
    1117            </div>
    1218        </div>
    13         <div class="rftMainMenuButton" data-dojo-type="./MenuBarLink" data-dojo-props="path:'/results'">Results</div>
    14         <div class="rftMainMenuButton" data-dojo-type="./SessionMenu"></div>
     19        <div class="rftMainMenuButton"
     20             data-dojo-type="./MenuBarLink" data-dojo-props="path:'/surveyRuns'">
     21          Results</div>
     22        <div class="rftMainMenuButton"
     23             data-dojo-type="./SessionMenu"></div>
    1524    </div>
    1625</div>
  • Dev/trunk/src/server/api/responses.js

    r527 r531  
    1313
    1414    var getResponsesBySurveyRunId = exports.getResponsesBySurveyRunId = function(surveyRunId) {
    15         var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(surveyRunId);
    16         return couch.get(url)
     15        return couch.get('_design/responses/_view/by_surveyrun',
     16                         {query:{key:surveyRunId,reduce:false,include_docs:true}})
    1717        .handle({
    1818            '-1': _.identity,
    19             200: util.handleRowValues,
     19            200: util.handleRowDocs,
    2020            default: util.handleUnknownResponse
    2121        });
  • Dev/trunk/src/server/api/surveyRuns.js

    r527 r531  
    1515    app.get('/',
    1616        util.ensureMIME(util.JSON_MIME),
    17         util.makeDocsGet('SurveyRun'));
     17        function(req,res) {
     18            var qs = {reduce:false,include_docs:true};
     19            var now = new Date().toISOString();
     20            var hr;
     21            if ( 'current' in req.query ) {
     22                // we make the assumption that there will be fewer
     23                // runs with no or a future enddate than with a
     24                // startdate in the past, i.e. we assume fewer future
     25                // surveys are planned than past surveys are run.
     26                hr = couch.get('_design/surveyRuns/_view/by_end_date',{
     27                    query: {startkey:now,reduce:false,include_docs:true}
     28                }).handle({
     29                    '-1': _.identity,
     30                    200: function(result) {
     31                        return _.filter(
     32                            util.handleRowDocs(result),
     33                            function(doc){
     34                                return !doc.startDate ||
     35                                       doc.startDate <= now;
     36                            });
     37                    },
     38                    default: util.handleUnknownResponse
     39                });
     40            } else if ( 'future' in req.query ) {
     41                hr = couch.get('_design/surveyRuns/_view/by_start_date',{
     42                    query: {startkey:now,reduce:false,include_docs:true}
     43                }).handle({
     44                    '-1': _.identity,
     45                    200: util.handleRowDocs,
     46                    default: util.handleUnknownResponse
     47                });
     48            } else if ( 'past' in req.query ) {
     49                hr = couch.get('_design/surveyRuns/_view/by_end_date',{
     50                    query: {endkey:now,reduce:false,include_docs:true}
     51                }).handle({
     52                    '-1': _.identity,
     53                    200: util.handleRowDocs,
     54                    default: util.handleUnknownResponse
     55                });
     56            } else {
     57                hr = util.getDocumentsOfType('SurveyRun');
     58            }
     59            hr.handle({'-1': util.handleException})
     60            .handle(res.send.bind(res));
     61        });
    1862    app.post('/',
    1963        util.ensureMIME(util.JSON_MIME),
  • Dev/trunk/src/server/api/util.js

    r527 r531  
    8181
    8282    var getDocumentsOfType = exports.getDocumentsOfType = function(type) {
    83         var url = '_design/default/_view/by_type?key='+JSON.stringify(type);
    84         return couch.get(url)
     83        return couch.get('_design/default/_view/by_type',
     84                         {query:{reduce:false,include_docs:true,key:type}})
    8585        .handle({
    8686            '-1': _.identity,
    87             200: handleRowValues,
     87            200: handleRowDocs,
    8888            404: function() { return {error: "Cannot find collection of type "+type}; },
    8989            default: handleUnknownResponse
  • Dev/trunk/src/server/config/couchdb-design-docs.js

    r525 r531  
    3030        _id: "schemaInfo",
    3131        version: "4",
    32         viewsVersion: "1"
     32        viewsVersion: "2"
    3333    },
    3434
     
    4949            by_type: {
    5050                map: function(doc){
    51                     emit(doc.type, doc);
    52                 }
     51                    emit(doc.type, 1);
     52                },
     53                reduce: function(keys,values){ return sum(values); }
    5354            },
    5455            typeless: {
     
    104105                    emit(doc.topic||"(default)",1);
    105106                },
    106                 reduce: function(key, values, rereduce) { return sum(values); }
     107                reduce: function(key, values ) { return sum(values); }
    107108            },
    108109            all_variables: {
     
    137138                map: function(doc){
    138139                    if ( doc.type !== 'Question' ) { return; }
    139                     emit(doc.code,doc);
    140                 }
     140                    emit(doc.code,1);
     141                },
     142                reduce: function(key, values) { return sum(values); }
    141143            },
    142144            published_by_code: {
    143145                map: function(doc){
    144                     if ( doc.type !== 'Question' || !doc.publicationDate ) { return; }
    145                     emit(doc.code,doc);
    146                 }
     146                    if ( doc.type !== 'Question' ||
     147                         !doc.publicationDate ) { return; }
     148                    emit(doc.code,1);
     149                },
     150                reduce: function(key, values) { return sum(values); }
    147151            },
    148152            lib: {
     
    159163            drafts: {
    160164                map: function(doc){
    161                     if ( doc.type !== 'Survey' || doc.publicationDate ) { return; }
     165                    if ( doc.type !== 'Survey' ||
     166                         doc.publicationDate ) { return; }
    162167                    emit(doc._id,doc);
    163168                }
     
    165170            published: {
    166171                map: function(doc){
    167                     if ( doc.type !== 'Survey' || !doc.publicationDate ) { return; }
     172                    if ( doc.type !== 'Survey' ||
     173                         !doc.publicationDate ) { return; }
    168174                    emit(doc._id,doc);
    169175                }
     
    176182        language: "javascript",
    177183        views: {
    178             by_dates: {
     184            by_start_date: {
    179185                map: function(doc){
    180186                    if ( doc.type !== 'SurveyRun' ) { return; }
    181                     var startDate = doc.startDate || "";
    182                     var endDate = doc.endDate || {};
    183                     emit([startDate,endDate,doc.liveName||null],doc);
    184                 }
     187                    emit(doc.startDate||null,1);
     188                },
     189                reduce: function(keys,values){ return sum(values); }
     190            },
     191            by_end_date: {
     192                map: function(doc){
     193                    if ( doc.type !== 'SurveyRun' ) { return; }
     194                    emit(doc.endDate||{},1);
     195                },
     196                reduce: function(keys,values){ return sum(values); }
    185197            }
    186198        }
     
    194206                map: function(doc){
    195207                    if ( doc.type !== 'Response' ) { return; }
    196                     emit(doc.surveyRunId, doc);
    197                 }
     208                    emit(doc.surveyRunId, 1);
     209                },
     210                reduce: function(keys,values){ return sum(values); }
    198211            }
    199212        }
  • Dev/trunk/src/server/config/couchdb-schema.json

    r525 r531  
    1010  "definitions": {
    1111    "nonEmptyString": { "type": "string", "minLength": 1 },
    12     "codeString": { "type": "string", "pattern": "^[A-Za-z0-9]+$" },
    13     "subcodeString": { "type": "string", "pattern": "^[A-Za-z0-9]*$" },
     12    "code": { "type": "string", "pattern": "^[A-Za-z0-9]+$" },
     13    "subcode": { "type": "string", "pattern": "^[A-Za-z0-9]*$" },
     14    "datetime": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"},
     15    "html5Email": {"type": "string", "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"},
    1416    "schemaInfo": {
    1517      "type": "object",
     
    4042          "_rev": { "$ref": "#/definitions/nonEmptyString" },
    4143          "categories": { "type": "array", "items": { "$ref": "#/definitions/nonEmptyString" } },
    42           "code": { "$ref": "#/definitions/codeString" },
     44          "code": { "$ref": "#/definitions/code" },
    4345          "content": { "type": "array", "items": { "$ref": "#/definitions/content/any" } },
    4446          "description": { "$ref": "#/definitions/nonEmptyString" },
    45           "publicationDate": { "type": "string", "format": "datetime" },
     47          "publicationDate": { "$ref": "#/definitions/datetime" },
    4648          "title": { "$ref": "#/definitions/nonEmptyString" },
    4749          "topic": { "$ref": "#/definitions/nonEmptyString" }
     
    5759          "_rev": { "$ref": "#/definitions/nonEmptyString" },
    5860          "description": { "$ref": "#/definitions/nonEmptyString" },
    59           "publicationDate": { "type": "string", "format": "datetime" },
     61          "publicationDate": { "$ref": "#/definitions/datetime" },
    6062          "questions": { "type": "array", "items": { "$ref": "#/definitions/docs/Question" } },
    6163          "title": { "$ref": "#/definitions/nonEmptyString" }
     
    7173          "_rev": { "$ref": "#/definitions/nonEmptyString" },
    7274          "description": { "$ref": "#/definitions/nonEmptyString" },
    73           "endDate": { "type": "string", "format": "datetime" },
    74           "liveName": { "$ref": "#/definitions/nonEmptyString" },
     75          "endDate": { "$ref": "#/definitions/datetime" },
    7576          "mode": { "type": "string", "enum": [ "open", "closed" ] },
    7677          "respondentCanDeleteOwnResponse": { "type": "boolean" },
    7778          "secret": { "$ref": "#/definitions/nonEmptyString" },
    78           "startDate": { "type": "string", "format": "datetime" },
     79          "startDate": { "$ref": "#/definitions/datetime" },
    7980          "survey": { "$ref": "#/definitions/docs/Survey" },
    8081          "title": { "$ref": "#/definitions/nonEmptyString" }
     
    9697              "additionalProperties": false
    9798          },
    98           "email": { "type": "string", "format": "email" },
    99           "publicationDate": { "type": "string", "format": "datetime" },
     99          "publicationDate": { "$ref": "#/definitions/datetime" },
    100100          "secret": { "$ref": "#/definitions/nonEmptyString" },
    101101          "surveyRunId": { "$ref": "#/definitions/nonEmptyString" }
     
    149149        "properties": {
    150150          "type": { "type": "string", "pattern": "^StringInput$" },
    151           "subcode": { "$ref": "#/definitions/subcodeString" },
     151          "subcode": { "$ref": "#/definitions/subcode" },
    152152          "text": { "$ref": "#/definitions/nonEmptyString" }
    153153        },
     
    160160          "type": { "type": "string", "pattern": "^TextInput$" },
    161161          "maxLength": { "type": "integer" },
    162           "subcode": { "$ref": "#/definitions/subcodeString" },
     162          "subcode": { "$ref": "#/definitions/subcode" },
    163163          "text": { "$ref": "#/definitions/nonEmptyString" }
    164164        },
     
    173173          "max": { "type": "integer" },
    174174          "places": { "type": "integer" },
    175           "subcode": { "$ref": "#/definitions/subcodeString" },
     175          "subcode": { "$ref": "#/definitions/subcode" },
    176176          "text": { "$ref": "#/definitions/nonEmptyString" }
    177177        },
     
    193193              "minLabel": { "$ref": "#/definitions/nonEmptyString" },
    194194              "maxLabel": { "$ref": "#/definitions/nonEmptyString" },
    195               "subcode": { "$ref": "#/definitions/subcodeString" },
     195              "subcode": { "$ref": "#/definitions/subcode" },
    196196              "text": { "$ref": "#/definitions/nonEmptyString" }
    197197            },
     
    219219              "type": "object",
    220220              "properties": {
    221                   "subcode": { "$ref": "#/definitions/subcodeString" }
     221                  "subcode": { "$ref": "#/definitions/subcode" }
    222222              },
    223223              "required": ["subcode"],
    224224              "additionalProperties": false
    225225          },
    226           "subcode": { "$ref": "#/definitions/subcodeString" }
     226          "subcode": { "$ref": "#/definitions/subcode" }
    227227        },
    228228        "required":["type","items","subcode"],
     
    236236              "type": "object",
    237237              "properties": {
    238                 "subcode": { "$ref": "#/definitions/subcodeString" },
     238                "subcode": { "$ref": "#/definitions/subcode" },
    239239                "text": { "$ref": "#/definitions/nonEmptyString" }
    240240              },
     
    245245              "type": "object",
    246246              "properties": {
    247                 "subcode": { "$ref": "#/definitions/subcodeString" }
     247                "subcode": { "$ref": "#/definitions/subcode" }
    248248              },
    249249              "required": ["subcode"],
  • Dev/trunk/src/server/util/http-result.js

    r527 r531  
    6363                if ( status in fOrObj ) {
    6464                    return fOrObj[status](result);
     65                } else if ( status >= 200 && status < 300 &&
     66                            'success' in fOrObj ) {
     67                    return fOrObj.success(status,result);
     68                } else if ( !(status >= 200 && status < 300) &&
     69                            'failure' in fOrObj ) {
     70                    return fOrObj.failure(status,result);
    6571                } else if ( 'default' in fOrObj ) {
    6672                    return fOrObj['default'](status,result);
  • Dev/trunk/src/server/util/validator.js

    r493 r531  
    11var tv4 = require('tv4');
    2 
    3 // from: http://www.w3.org/TR/html5/forms.html#valid-e-mail-address
    4 var html5EmailRe = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
    5 var datetimeRe = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z/;
    6 
    7 tv4.addFormat({
    8     email: function(data){
    9         if ( typeof data === "string" && html5EmailRe.test(data) ) {
    10             return null;
    11         } else {
    12             return "Probably an invalid email address.";
    13         }
    14     },
    15     datetime: function(data){
    16         if ( typeof data === "string" && datetimeRe.test(data) ) {
    17             return null;
    18         } else {
    19             return "Invalid timestamp.";
    20         }
    21     }
    22 });
    232
    243module.exports = function() {
Note: See TracChangeset for help on using the changeset viewer.