jipe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,877 @@
1
+ // Jester version 1.5
2
+ // Released October 25th, 2007
3
+
4
+ // Compatible, tested with Prototype 1.6.0.2
5
+
6
+ // Copyright 2007, thoughtbot, inc.
7
+ // Released under the MIT License.
8
+
9
+ Jester = {}
10
+ Jester.Resource = function(){};
11
+
12
+ // Doing it this way forces the validation of the syntax but gives flexibility enough to rename the new class.
13
+ Jester.Constructor = function(model){
14
+ return (function CONSTRUCTOR() {
15
+ this.klass = CONSTRUCTOR;
16
+ this.initialize.apply(this, arguments);
17
+ this.after_initialization.apply(this, arguments);
18
+ }).toString().replace(/CONSTRUCTOR/g, model);
19
+ }
20
+
21
+ // universal Jester callback holder for remote JSON loading
22
+ var jesterCallback = null;
23
+
24
+ Object.extend(Jester.Resource, {
25
+ model: function(model, options)
26
+ {
27
+ var new_model = null;
28
+ new_model = eval(model + " = " + Jester.Constructor(model));
29
+ new_model.prototype = new Jester.Resource();
30
+ Object.extend(new_model, Jester.Resource);
31
+
32
+ // We delay instantiating XML.ObjTree() so that it can be listed at the end of this file instead of the beginning
33
+ if (!Jester.Tree) {
34
+ Jester.Tree = new XML.ObjTree();
35
+ Jester.Tree.attr_prefix = "@";
36
+ }
37
+ if (!options) options = {};
38
+
39
+ var default_options = {
40
+ format: "xml",
41
+ singular: model.underscore(),
42
+ name: model,
43
+ defaultParams: {}
44
+ }
45
+ options = Object.extend(default_options, options);
46
+ options.format = options.format.toLowerCase();
47
+ options.plural = options.singular.pluralize(options.plural);
48
+ options.singular_xml = options.singular.replace(/_/g, "-");
49
+ options.plural_xml = options.plural.replace(/_/g, "-");
50
+ options.remote = false;
51
+
52
+ // Establish prefix
53
+ var default_prefix = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port : "");
54
+ if (options.prefix && options.prefix.match(/^https?:/))
55
+ options.remote = true;
56
+
57
+ if (!options.prefix)
58
+ options.prefix = default_prefix;
59
+
60
+ if (!options.prefix.match(/^(https?|file):/))
61
+ options.prefix = default_prefix + (options.prefix.match(/^\//) ? "" : "/") + options.prefix;
62
+
63
+ options.prefix = options.prefix.replace(/\b\/+$/,"");
64
+
65
+ // Establish custom URLs
66
+ options.urls = Object.extend(this._default_urls(options), options.urls);
67
+
68
+ // Assign options to model
69
+ new_model.name = model;
70
+ new_model.options = options;
71
+ for(var opt in options)
72
+ new_model["_" + opt] = options[opt];
73
+
74
+ // Establish custom URL helpers
75
+ for (var url in options.urls)
76
+ eval('new_model._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
77
+
78
+ if (options.checkNew)
79
+ this.buildAttributes(new_model, options);
80
+
81
+ if (window)
82
+ window[model] = new_model;
83
+
84
+ return new_model;
85
+ },
86
+
87
+ buildAttributes: function(model, options) {
88
+ model = model || this;
89
+ var async = options.asynchronous;
90
+
91
+ if (async == null)
92
+ async = true;
93
+
94
+ var buildWork = bind(model, function(doc) {
95
+ if (this._format == "json")
96
+ this._attributes = this._attributesFromJSON(doc);
97
+ else
98
+ this._attributes = this._attributesFromTree(doc[this._singular_xml]);
99
+ });
100
+ model.requestAndParse(options.format, buildWork, model._new_url(), {asynchronous: async});
101
+ },
102
+
103
+ loadRemoteJSON : function(url, callback, user_callback) {
104
+ // tack on user_callback if there is one, and only if it's really a function
105
+ if (typeof(user_callback) == "function")
106
+ jesterCallback = function(doc) {user_callback(callback(doc));}
107
+ else
108
+ jesterCallback = callback;
109
+
110
+ var script = document.createElement("script");
111
+ script.type = "text/javascript";
112
+
113
+ if (url.indexOf("?") == -1)
114
+ url += "?";
115
+ else
116
+ url += "&";
117
+ url += "callback=jesterCallback";
118
+ script.src = url;
119
+
120
+ document.firstChild.appendChild(script);
121
+ },
122
+
123
+ requestAndParse : function(format, callback, url, options, user_callback, remote) {
124
+ if (remote && format == "json" && user_callback)
125
+ return this.loadRemoteJSON(url, callback, user_callback)
126
+
127
+ parse_and_callback = null;
128
+ if (format.toLowerCase() == "json") {
129
+ parse_and_callback = function(transport) {
130
+ if (transport.status == 500) return callback(null);
131
+ eval("var attributes = " + transport.responseText); // hashes need this kind of eval
132
+ return callback(attributes);
133
+ }
134
+ } else {
135
+ parse_and_callback = function(transport) {
136
+ if (transport.status == 500) return callback(null);
137
+ return callback(Jester.Tree.parseXML(transport.responseText));
138
+ }
139
+ }
140
+
141
+ // most parse requests are going to be a GET
142
+ if (!(options.postBody || options.parameters || options.postbody || options.method == "post")) {
143
+ options.method = "get";
144
+ }
145
+
146
+ return this.request(parse_and_callback, url, options, user_callback);
147
+ },
148
+
149
+ // Helper to aid in handling either async or synchronous requests
150
+ request : function(callback, url, options, user_callback) {
151
+ if (user_callback) {
152
+ options.asynchronous = true;
153
+ // if an options hash was given instead of a callback
154
+ if (typeof(user_callback) == "object") {
155
+ for (var x in user_callback)
156
+ options[x] = user_callback[x];
157
+ user_callback = options.onComplete;
158
+ }
159
+ }
160
+ else
161
+ user_callback = function(arg){return arg;}
162
+
163
+ if (options.asynchronous) {
164
+ options.onComplete = function(transport, json) {user_callback(callback(transport), json);}
165
+ return new Ajax.Request(url, options).transport;
166
+ }
167
+ else
168
+ {
169
+ options.asynchronous = false; // Make sure it's set, to avoid being overridden.
170
+ return callback(new Ajax.Request(url, options).transport);
171
+ }
172
+ },
173
+
174
+ find : function(id, params, callback) {
175
+ // allow a params hash to be omitted and a callback function given directly
176
+ if (!callback && typeof(params) == "function") {
177
+ callback = params;
178
+ params = null;
179
+ }
180
+
181
+ var findAllWork = bind(this, function(doc) {
182
+ if (!doc) return null;
183
+
184
+ var collection = this._loadCollection(doc);
185
+
186
+ if (!collection) return null;
187
+
188
+ // This is better than requiring the controller to support a "limit" parameter
189
+ if (id == "first")
190
+ return collection[0];
191
+
192
+ return collection;
193
+ });
194
+
195
+ var findOneWork = bind(this, function(doc) {
196
+ if (!doc) return null;
197
+
198
+ var base = this._loadSingle(doc);
199
+
200
+ // if there were no properties, it was probably not actually loaded
201
+ if (!base || base._properties.length == 0) return null;
202
+
203
+ // even if the ID didn't come back, we obviously knew the ID to search with, so set it
204
+ if (!base._properties.include("id")) base._setAttribute("id", parseInt(id))
205
+
206
+ return base;
207
+ });
208
+
209
+ if (id == "first" || id == "all") {
210
+ var url = this._list_url(params);
211
+ return this.requestAndParse(this._format, findAllWork, url, {}, callback, this._remote);
212
+ }
213
+ else {
214
+ if (isNaN(parseInt(id))) return null;
215
+ if (!params) params = {};
216
+ params.id = id;
217
+
218
+ var url = this._show_url(params);
219
+ return this.requestAndParse(this._format, findOneWork, url, {}, callback, this._remote);
220
+ }
221
+ },
222
+
223
+ build : function(attributes) {
224
+ return new this(attributes);
225
+ },
226
+
227
+ create : function(attributes, params, callback) {
228
+ // allow a params hash to be omitted and a callback function given directly
229
+ if (!callback && typeof(params) == "function") {
230
+ callback = params;
231
+ params = null;
232
+ }
233
+
234
+ var base = new this(attributes);
235
+
236
+ createWork = bind(this, function(saved) {
237
+ return callback(base);
238
+ });
239
+
240
+ if (callback) {
241
+ return base.save(createWork);
242
+ }
243
+ else {
244
+ base.save();
245
+ return base;
246
+ }
247
+ },
248
+
249
+ // Destroys a REST object. Can be used as follows:
250
+ // object.destroy() - when called on an instance of a model, destroys that instance
251
+ // Model.destroy(1) - destroys the Model object with ID 1
252
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
253
+ //
254
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
255
+ destroy : function(params, callback) {
256
+ if (typeof(params) == "function") {
257
+ callback = params;
258
+ params = null;
259
+ }
260
+ if (typeof(params) == "number") {
261
+ params = {id: params};
262
+ }
263
+ params.id = params.id || this.id;
264
+ if (!params.id) return false;
265
+
266
+ var destroyWork = bind(this, function(transport) {
267
+ if (transport.status == 200) {
268
+ if (!params.id || this.id == params.id)
269
+ this.id = null;
270
+ return this;
271
+ }
272
+ else
273
+ return false;
274
+ });
275
+
276
+ return this.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
277
+ },
278
+
279
+ _interpolate: function(string, params) {
280
+ if (!params) return string;
281
+
282
+ var result = string;
283
+ params.each(function(pair) {
284
+ var re = new RegExp(":" + pair.key, "g");
285
+ if (result.match(re)) {
286
+ result = result.replace(re, pair.value);
287
+ params.unset(pair.key);
288
+ }
289
+ });
290
+ return result;
291
+ },
292
+
293
+ _url_for : function(action, params) {
294
+ if (!this._urls[action]) return "";
295
+ // if an integer is sent, it's assumed just the ID is a parameter
296
+ if (typeof(params) == "number") params = {id: params}
297
+
298
+ params = Object.extend(Object.clone(this._defaultParams), params);
299
+
300
+ if (params) params = $H(params);
301
+
302
+ var url = this._interpolate(this._prefix + this._urls[action], params)
303
+ return url + (params && params.any() ? "?" + params.toQueryString() : "");
304
+ },
305
+
306
+ _default_urls : function(options) {
307
+ urls = {
308
+ 'show' : "/" + options.plural + "/:id." + options.format,
309
+ 'list' : "/" + options.plural + "." + options.format,
310
+ 'new' : "/" + options.plural + "/new." + options.format
311
+ }
312
+ urls.create = urls.list;
313
+ urls.destroy = urls.update = urls.show;
314
+
315
+ return urls;
316
+ },
317
+
318
+ // Converts a JSON hash returns from ActiveRecord::Base#to_json into a hash of attribute values
319
+ // Does not handle associations, as AR's #to_json doesn't either
320
+ // Also, JSON doesn't include room to store types, so little auto-transforming is done here (just on 'id')
321
+ _attributesFromJSON : function(json) {
322
+ if (!json || json.constructor != Object) return false;
323
+ if (json.attributes) json = json.attributes;
324
+
325
+ var attributes = {};
326
+ var i = 0;
327
+ for (var attr in json) {
328
+ var value = json[attr];
329
+ if (attr == "id")
330
+ value = parseInt(value);
331
+ else if (attr.match(/(created_at|created_on|updated_at|updated_on)/)) {
332
+ var date = Date.parse(value);
333
+ if (date && !isNaN(date)) value = date;
334
+ }
335
+ attributes[attr] = value;
336
+ i += 1;
337
+ }
338
+ if (i == 0) return false; // empty hashes should just return false
339
+
340
+ return attributes;
341
+ },
342
+
343
+ // Converts the XML tree returned from a single object into a hash of attribute values
344
+ _attributesFromTree : function(elements) {
345
+ var attributes = {}
346
+ for (var attr in elements) {
347
+ // pull out the value
348
+ var value = elements[attr];
349
+ if (elements[attr] && elements[attr]["@type"]) {
350
+ if (elements[attr]["#text"])
351
+ value = elements[attr]["#text"];
352
+ else
353
+ value = undefined;
354
+ }
355
+
356
+ // handle empty value (pass it through)
357
+ if (!value) {}
358
+
359
+ // handle scalars
360
+ else if (typeof(value) == "string") {
361
+ // perform any useful type transformations
362
+ if (elements[attr]["@type"] == "integer") {
363
+ var num = parseInt(value);
364
+ if (!isNaN(num)) value = num;
365
+ }
366
+ else if (elements[attr]["@type"] == "boolean")
367
+ value = (value == "true");
368
+ else if (elements[attr]["@type"] == "datetime") {
369
+ var date = Date.parse(value);
370
+ if (!isNaN(date)) value = date;
371
+ }
372
+ }
373
+ // handle arrays (associations)
374
+ else {
375
+ var relation = value; // rename for clarity in the context of an association
376
+
377
+ // first, detect if it's has_one/belongs_to, or has_many
378
+ var i = 0;
379
+ var singular = null;
380
+ var has_many = false;
381
+ for (var val in relation) {
382
+ if (i == 0)
383
+ singular = val;
384
+ i += 1;
385
+ }
386
+
387
+ // has_many
388
+ if (relation[singular] && typeof(relation[singular]) == "object" && i == 1) {
389
+ var value = [];
390
+ var plural = attr;
391
+ var name = singular.camelize().capitalize();
392
+
393
+ // force array
394
+ if (!(elements[plural][singular].length > 0))
395
+ elements[plural][singular] = [elements[plural][singular]];
396
+
397
+ elements[plural][singular].each( bind(this, function(single) {
398
+ // if the association hasn't been modeled, do a default modeling here
399
+ // hosted object's prefix and format are inherited, singular and plural are set
400
+ // from the XML
401
+ if (eval("typeof(" + name + ")") == "undefined") {
402
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, plural: plural, format: this._format});
403
+ }
404
+ var base = eval(name + ".build(this._attributesFromTree(single))");
405
+ value.push(base);
406
+ }));
407
+ }
408
+ // has_one or belongs_to
409
+ else {
410
+ singular = attr;
411
+ var name = singular.capitalize();
412
+
413
+ // if the association hasn't been modeled, do a default modeling here
414
+ // hosted object's prefix and format are inherited, singular is set from the XML
415
+ if (eval("typeof(" + name + ")") == "undefined") {
416
+ Jester.Resource.model(name, {prefix: this._prefix, singular: singular, format: this._format});
417
+ }
418
+ value = eval(name + ".build(this._attributesFromTree(value))");
419
+ }
420
+ }
421
+
422
+ // transform attribute name if needed
423
+ attribute = attr.replace(/-/g, "_");
424
+ attributes[attribute] = value;
425
+ }
426
+
427
+ return attributes;
428
+ },
429
+
430
+ _loadSingle : function(doc) {
431
+ var attributes;
432
+ if (this._format == "json")
433
+ attributes = this._attributesFromJSON(doc);
434
+ else
435
+ attributes = this._attributesFromTree(doc[this._singular_xml]);
436
+
437
+ return this.build(attributes);
438
+ },
439
+
440
+ _loadCollection : function(doc) {
441
+ var collection;
442
+ if (this._format == "json") {
443
+ collection = doc.map( bind(this, function(item) {
444
+ return this.build(this._attributesFromJSON(item));
445
+ }));
446
+ }
447
+ else {
448
+ // if only one result, wrap it in an array
449
+ if (!Jester.Resource.elementHasMany(doc[this._plural_xml]))
450
+ doc[this._plural_xml][this._singular_xml] = [doc[this._plural_xml][this._singular_xml]];
451
+
452
+ collection = doc[this._plural_xml][this._singular_xml].map( bind(this, function(elem) {
453
+ return this.build(this._attributesFromTree(elem));
454
+ }));
455
+ }
456
+ return collection;
457
+ }
458
+
459
+ });
460
+
461
+ Object.extend(Jester.Resource.prototype, {
462
+ initialize : function(attributes) {
463
+ // Initialize no attributes, no associations
464
+ this._properties = [];
465
+ this._associations = [];
466
+
467
+ this.setAttributes(this.klass._attributes || {});
468
+ this.setAttributes(attributes);
469
+
470
+ // Initialize with no errors
471
+ this.errors = [];
472
+
473
+ // Establish custom URL helpers
474
+ for (var url in this.klass._urls)
475
+ eval('this._' + url + '_url = function(params) {return this._url_for("' + url + '", params);}');
476
+ },
477
+ after_initialization: function(){},
478
+
479
+ new_record : function() {return !(this.id);},
480
+ valid : function() {return ! this.errors.any();},
481
+
482
+ reload : function(callback) {
483
+ var reloadWork = bind(this, function(copy) {
484
+ this._resetAttributes(copy.attributes(true));
485
+
486
+ if (callback)
487
+ return callback(this);
488
+ else
489
+ return this;
490
+ });
491
+
492
+ if (this.id) {
493
+ if (callback)
494
+ return this.klass.find(this.id, {}, reloadWork);
495
+ else
496
+ return reloadWork(this.klass.find(this.id));
497
+ }
498
+ else
499
+ return this;
500
+ },
501
+
502
+ // Destroys a REST object. Can be used as follows:
503
+ // object.destroy() - when called on an instance of a model, destroys that instance
504
+ // Model.destroy(1) - destroys the Model object with ID 1
505
+ // Model.destroy({parent: 3, id: 1}) - destroys the Model object with Parent ID 3 and ID 1
506
+ //
507
+ // Any of these forms can also be passed a callback function as an additional parameter and it works as you expect.
508
+ destroy : function(params, callback) {
509
+ if (params === undefined) {
510
+ params = {};
511
+ }
512
+ if (typeof(params) == "function") {
513
+ callback = params;
514
+ params = {};
515
+ }
516
+ if (typeof(params) == "number") {
517
+ params = {id: params};
518
+ }
519
+ if (!params.id) {
520
+ params.id = this.id;
521
+ }
522
+ if (!params.id) return false;
523
+
524
+ // collect params from instance if we're being called as an instance method
525
+ if (this._properties !== undefined) {
526
+ (this._properties).each( bind(this, function(value, i) {
527
+ if (params[value] === undefined) {
528
+ params[value] = this[value];
529
+ }
530
+ }));
531
+ }
532
+
533
+ var destroyWork = bind(this, function(transport) {
534
+ if (transport.status == 200) {
535
+ if (!params.id || this.id == params.id)
536
+ this.id = null;
537
+ return this;
538
+ }
539
+ else
540
+ return false;
541
+ });
542
+
543
+ return this.klass.request(destroyWork, this._destroy_url(params), {method: "delete"}, callback);
544
+ },
545
+
546
+ save : function(params, callback) {
547
+ // allow a params hash to be omitted and a callback function given directly
548
+ if (!callback && typeof(params) == "function") {
549
+ callback = params;
550
+ params = null;
551
+ }
552
+
553
+ var saveWork = bind(this, function(transport) {
554
+ var saved = false;
555
+
556
+ if (transport.responseText && (transport.responseText.strip() != "")) {
557
+ var errors = this._errorsFrom(transport.responseText);
558
+ if (errors)
559
+ this._setErrors(errors);
560
+ else {
561
+ var attributes;
562
+ if (this.klass._format == "json") {
563
+ attributes = this._attributesFromJSON(transport.responseText);
564
+ }
565
+ else {
566
+ var doc = Jester.Tree.parseXML(transport.responseText);
567
+ if (doc[this.klass._singular_xml])
568
+ attributes = this._attributesFromTree(doc[this.klass._singular_xml]);
569
+ }
570
+ if (attributes)
571
+ this._resetAttributes(attributes);
572
+ }
573
+ }
574
+
575
+ // Get ID from the location header if it's there
576
+ if (this.new_record() && transport.status == 201) {
577
+ loc = transport.getResponseHeader("location");
578
+ if (loc) {
579
+ id = parseInt(loc.match(/\/([^\/]*?)(\.\w+)?$/)[1]);
580
+ if (!isNaN(id))
581
+ this._setProperty("id", id)
582
+ }
583
+ }
584
+
585
+ return (transport.status >= 200 && transport.status < 300 && this.errors.length == 0);
586
+ });
587
+
588
+ // reset errors
589
+ this._setErrors([]);
590
+
591
+ var url = null;
592
+ var method = null;
593
+
594
+ // collect params
595
+ var objParams = {};
596
+ var urlParams = Object.clone(this.klass._defaultParams);
597
+ if (params) {
598
+ Object.extend(urlParams, params);
599
+ }
600
+ (this._properties).each( bind(this, function(value, i) {
601
+ objParams[this.klass._singular + "[" + value + "]"] = this[value];
602
+ urlParams[value] = this[value];
603
+ }));
604
+
605
+ // distinguish between create and update
606
+ if (this.new_record()) {
607
+ url = this._create_url(urlParams);
608
+ method = "post";
609
+ }
610
+ else {
611
+ url = this._update_url(urlParams);
612
+ method = "put";
613
+ }
614
+
615
+ // send the request
616
+ return this.klass.request(saveWork, url, {parameters: objParams, method: method}, callback);
617
+ },
618
+
619
+ setAttributes : function(attributes)
620
+ {
621
+ $H(attributes).each(bind(this, function(attr){ this._setAttribute(attr.key, attr.value) }));
622
+ return attributes;
623
+ },
624
+
625
+ updateAttributes : function(attributes, callback)
626
+ {
627
+ this.setAttributes(attributes);
628
+ return this.save(callback);
629
+ },
630
+
631
+ // mimics ActiveRecord's behavior of omitting associations, but keeping foreign keys
632
+ attributes : function(include_associations) {
633
+ var attributes = {}
634
+ for (var i=0; i<this._properties.length; i++)
635
+ attributes[this._properties[i]] = this[this._properties[i]];
636
+ if (include_associations) {
637
+ for (var i=0; i<this._associations.length; i++)
638
+ attributes[this._associations[i]] = this[this._associations[i]];
639
+ }
640
+ return attributes;
641
+ },
642
+
643
+ /*
644
+ Internal methods.
645
+ */
646
+
647
+ _attributesFromJSON: function()
648
+ {
649
+ return this.klass._attributesFromJSON.apply(this.klass, arguments);
650
+ },
651
+
652
+ _attributesFromTree: function()
653
+ {
654
+ return this.klass._attributesFromTree.apply(this.klass, arguments);
655
+ },
656
+
657
+ _errorsFrom : function(raw) {
658
+ if (this.klass._format == "json")
659
+ return this._errorsFromJSON(raw);
660
+ else
661
+ return this._errorsFromXML(raw);
662
+ },
663
+
664
+ // Pulls errors from JSON
665
+ _errorsFromJSON : function(json) {
666
+ try {
667
+ json = eval(json); // okay for arrays
668
+ } catch(e) {
669
+ return false;
670
+ }
671
+
672
+ if (!(json && json.constructor == Array && json[0] && json[0].constructor == Array)) return false;
673
+
674
+ return json.map(function(pair) {
675
+ return pair[0].capitalize() + " " + pair[1];
676
+ });
677
+ },
678
+
679
+ // Pulls errors from XML
680
+ _errorsFromXML : function(xml) {
681
+ if (!xml) return false;
682
+ var doc = Jester.Tree.parseXML(xml);
683
+
684
+ if (doc && doc.errors) {
685
+ var errors = [];
686
+ if (typeof(doc.errors.error) == "string")
687
+ doc.errors.error = [doc.errors.error];
688
+
689
+ doc.errors.error.each(function(value, index) {
690
+ errors.push(value);
691
+ });
692
+
693
+ return errors;
694
+ }
695
+ else return false;
696
+ },
697
+
698
+ // Sets errors with an array. Could be extended at some point to include breaking error messages into pairs (attribute, msg).
699
+ _setErrors : function(errors) {
700
+ this.errors = errors;
701
+ },
702
+
703
+
704
+ // Sets all attributes and associations at once
705
+ // Deciding between the two on whether the attribute is a complex object or a scalar
706
+ _resetAttributes : function(attributes) {
707
+ this._clear();
708
+ for (var attr in attributes)
709
+ this._setAttribute(attr, attributes[attr]);
710
+ },
711
+
712
+ _setAttribute : function(attribute, value) {
713
+ if (value && typeof(value) == "object" && value.constructor != Date)
714
+ this._setAssociation(attribute, value);
715
+ else
716
+ this._setProperty(attribute, value);
717
+ },
718
+
719
+ _setProperties : function(properties) {
720
+ this._clearProperties();
721
+ for (var prop in properties)
722
+ this._setProperty(prop, properties[prop])
723
+ },
724
+
725
+ _setAssociations : function(associations) {
726
+ this._clearAssociations();
727
+ for (var assoc in associations)
728
+ this._setAssociation(assoc, associations[assoc])
729
+ },
730
+
731
+ _setProperty : function(property, value) {
732
+ this[property] = value;
733
+ if (!(this._properties.include(property)))
734
+ this._properties.push(property);
735
+ },
736
+
737
+ _setAssociation : function(association, value) {
738
+ this[association] = value;
739
+ if (!(this._associations.include(association)))
740
+ this._associations.push(association);
741
+ },
742
+
743
+ _clear : function() {
744
+ this._clearProperties();
745
+ this._clearAssociations();
746
+ },
747
+
748
+ _clearProperties : function() {
749
+ for (var i=0; i<this._properties.length; i++)
750
+ this[this._properties[i]] = null;
751
+ this._properties = [];
752
+ },
753
+
754
+ _clearAssociations : function() {
755
+ for (var i=0; i<this._associations.length; i++)
756
+ this[this._associations[i]] = null;
757
+ this._associations = [];
758
+ },
759
+
760
+ // helper URLs
761
+ _url_for : function(action, params) {
762
+ if (!params) params = this.id;
763
+ if (typeof(params) == "object" && !params.id)
764
+ params.id = this.id;
765
+
766
+ return this.klass._url_for(action, params);
767
+ }
768
+
769
+ });
770
+
771
+ // Returns true if the element has more objects beneath it, or just 1 or more attributes.
772
+ // It's not perfect, this would mess up if an object had only one attribute, and it was an array.
773
+ // For now, this is just one of the difficulties of dealing with ObjTree.
774
+ Jester.Resource.elementHasMany = function(element) {
775
+ var i = 0;
776
+ var singular = null;
777
+ var has_many = false;
778
+ for (var val in element) {
779
+ if (i == 0)
780
+ singular = val;
781
+ i += 1;
782
+ }
783
+
784
+ return (element[singular] && typeof(element[singular]) == "object" && element[singular].length != null && i == 1);
785
+ }
786
+
787
+ // This bind function is a modification of the standard Prototype bind function.
788
+ // Use this instead of Prototype's when running in XULRunner due to a longstanding
789
+ // bug in the javascript interpreter.
790
+
791
+ function bind(context, func) {
792
+ var __method = func, args = $A(func.arguments), object = context;
793
+
794
+ return function() {
795
+ return __method.apply(object, args.concat($A(arguments)));
796
+ }
797
+ }
798
+
799
+ // If there is no object already called Resource, we define one to make things a little cleaner for us.
800
+ if(typeof(Resource) == "undefined")
801
+ Resource = Jester.Resource;
802
+
803
+
804
+
805
+
806
+ /*
807
+ Inflector library, contributed graciously to Jester by Ryan Schuft.
808
+ The library in full is a complete port of Rails' Inflector, though Jester only uses its pluralization.
809
+ Its home page can be found at: http://code.google.com/p/inflection-js/
810
+ */
811
+
812
+ if (!String.prototype.pluralize) String.prototype.pluralize = function(plural) {
813
+ var str=this;
814
+ if(plural)str=plural;
815
+ else {
816
+ var uncountable_words=['equipment','information','rice','money','species','series','fish','sheep','moose'];
817
+ var uncountable=false;
818
+ for(var x=0;!uncountable&&x<uncountable_words.length;x++)uncountable=(uncountable_words[x].toLowerCase()==str.toLowerCase());
819
+ if(!uncountable) {
820
+ var rules=[
821
+ [new RegExp('(m)an$','gi'),'$1en'],
822
+ [new RegExp('(pe)rson$','gi'),'$1ople'],
823
+ [new RegExp('(child)$','gi'),'$1ren'],
824
+ [new RegExp('(ax|test)is$','gi'),'$1es'],
825
+ [new RegExp('(octop|vir)us$','gi'),'$1i'],
826
+ [new RegExp('(alias|status)$','gi'),'$1es'],
827
+ [new RegExp('(bu)s$','gi'),'$1ses'],
828
+ [new RegExp('(buffal|tomat)o$','gi'),'$1oes'],
829
+ [new RegExp('([ti])um$','gi'),'$1a'],
830
+ [new RegExp('sis$','gi'),'ses'],
831
+ [new RegExp('(?:([^f])fe|([lr])f)$','gi'),'$1$2ves'],
832
+ [new RegExp('(hive)$','gi'),'$1s'],
833
+ [new RegExp('([^aeiouy]|qu)y$','gi'),'$1ies'],
834
+ [new RegExp('(x|ch|ss|sh)$','gi'),'$1es'],
835
+ [new RegExp('(matr|vert|ind)ix|ex$','gi'),'$1ices'],
836
+ [new RegExp('([m|l])ouse$','gi'),'$1ice'],
837
+ [new RegExp('^(ox)$','gi'),'$1en'],
838
+ [new RegExp('(quiz)$','gi'),'$1zes'],
839
+ [new RegExp('s$','gi'),'s'],
840
+ [new RegExp('$','gi'),'s']
841
+ ];
842
+ var matched=false;
843
+ for(var x=0;!matched&&x<=rules.length;x++) {
844
+ matched=str.match(rules[x][0]);
845
+ if(matched)str=str.replace(rules[x][0],rules[x][1]);
846
+ }
847
+ }
848
+ }
849
+ return str;
850
+ };
851
+
852
+ /*
853
+
854
+ This is a lighter form of ObjTree, with parts Jester doesn't use removed.
855
+ Compressed using http://dean.edwards.name/packer/.
856
+ Homepage: http://www.kawa.net/works/js/xml/objtree-e.html
857
+
858
+ XML.ObjTree -- XML source code from/to JavaScript object like E4X
859
+
860
+ Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
861
+ This program is free software; you can redistribute it and/or
862
+ modify it under the Artistic license. Or whatever license I choose,
863
+ which I will do instead of keeping this documentation like it is.
864
+
865
+ */
866
+
867
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('5(p(o)==\'w\')o=v(){};o.r=v(){m 9};o.r.1i="0.1b";o.r.u.14=\'<?L 1s="1.0" 1o="1n-8" ?>\\n\';o.r.u.Y=\'-\';o.r.u.1c=\'1a/L\';o.r.u.N=v(a){6 b;5(W.U){6 c=K U();6 d=c.1r(a,"1p/L");5(!d)m;b=d.A}q 5(W.10){c=K 10(\'1k.1h\');c.1g=z;c.1e(a);b=c.A}5(!b)m;m 9.E(b)};o.r.u.1d=v(c,d,e){6 f={};y(6 g 19 d){f[g]=d[g]}5(!f.M){5(p(f.18)=="w"&&p(f.17)=="w"&&p(f.16)=="w"){f.M="15"}q{f.M="13"}}5(e){f.X=V;6 h=9;6 i=e;6 j=f.T;f.T=v(a){6 b;5(a&&a.x&&a.x.A){b=h.E(a.x.A)}q 5(a&&a.J){b=h.N(a.J)}i(b,a);5(j)j(a)}}q{f.X=z}6 k;5(p(S)!="w"&&S.I){f.1q=c;6 l=K S.I(f);5(l)k=l.12}q 5(p(Q)!="w"&&Q.I){6 l=K Q.I(c,f);5(l)k=l.12}5(e)m k;5(k&&k.x&&k.x.A){m 9.E(k.x.A)}q 5(k&&k.J){m 9.N(k.J)}};o.r.u.E=v(a){5(!a)m;9.H={};5(9.P){y(6 i=0;i<9.P.t;i++){9.H[9.P[i]]=1}}6 b=9.O(a);5(9.H[a.F]){b=[b]}5(a.B!=11){6 c={};c[a.F]=b;b=c}m b};o.r.u.O=v(a){5(a.B==7){m}5(a.B==3||a.B==4){6 b=a.G.1j(/[^\\1f-\\1l]/);5(b==1m)m z;m a.G}6 c;6 d={};5(a.D&&a.D.t){c={};y(6 i=0;i<a.D.t;i++){6 e=a.D[i].F;5(p(e)!="Z")C;6 f=a.D[i].G;5(!f)C;e=9.Y+e;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}5(a.s&&a.s.t){6 g=V;5(c)g=z;y(6 i=0;i<a.s.t&&g;i++){6 h=a.s[i].B;5(h==3||h==4)C;g=z}5(g){5(!c)c="";y(6 i=0;i<a.s.t;i++){c+=a.s[i].G}}q{5(!c)c={};y(6 i=0;i<a.s.t;i++){6 e=a.s[i].F;5(p(e)!="Z")C;6 f=9.O(a.s[i]);5(f==z)C;5(p(d[e])=="w")d[e]=0;d[e]++;9.R(c,e,d[e],f)}}}m c};o.r.u.R=v(a,b,c,d){5(9.H[b]){5(c==1)a[b]=[];a[b][a[b].t]=d}q 5(c==1){a[b]=d}q 5(c==2){a[b]=[a[b],d]}q{a[b][a[b].t]=d}};',62,91,'|||||if|var|||this|||||||||||||return||XML|typeof|else|ObjTree|childNodes|length|prototype|function|undefined|responseXML|for|false|documentElement|nodeType|continue|attributes|parseDOM|nodeName|nodeValue|__force_array|Request|responseText|new|xml|method|parseXML|parseElement|force_array|Ajax|addNode|HTTP|onComplete|DOMParser|true|window|asynchronous|attr_prefix|string|ActiveXObject||transport|post|xmlDecl|get|parameters|postbody|postBody|in|text|24|overrideMimeType|parseHTTP|loadXML|x00|async|XMLDOM|VERSION|match|Microsoft|x20|null|UTF|encoding|application|uri|parseFromString|version'.split('|'),0,{}))
868
+
869
+ /*
870
+
871
+ This is a Date parsing library by Nicholas Barthelemy, packed to keep jester.js light.
872
+ Homepage: https://svn.nbarthelemy.com/date-js/
873
+ Compressed using http://dean.edwards.name/packer/.
874
+
875
+ */
876
+
877
+ eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('N.q.F||(N.q.F=t(a){o u.1d().F(a)});O.q.F||(O.q.F=t(a){o\'0\'.1H(a-u.K)+u});O.q.1H||(O.q.1H=t(a){v s=\'\',i=0;2k(i++<a){s+=u}o s});N.q.1j||(N.q.1j=t(){o u.1d().1j()});O.q.1j||(O.q.1j=t(){v n=u,l=n.K,i=-1;2k(i++<l){u.20(i,i+1)==0?n=n.20(1,n.K):i=l}o n});k.1m="2H 2F 2z 2y 2x 2u 2r 3q 3n 3k 3i 3d".1x(" ");k.1o="38 35 2Y 2U 2Q 2O 2M".1x(" ");k.2K="31 28 31 30 31 30 31 31 30 31 30 31".1x(" ");k.1A={2G:"%Y-%m-%d %H:%M:%S",2w:"%Y-%m-%2v%H:%M:%S%T",2s:"%a, %d %b %Y %H:%M:%S %Z",3p:"%d %b %H:%M",3o:"%B %d, %Y %H:%M"};k.3l=-1;k.3j=-2;(t(){v d=k;d["3h"]=1;d["2i"]=1t;d["2h"]=d["2i"]*19;d["2e"]=d["2h"]*19;d["P"]=d["2e"]*24;d["37"]=d["P"]*7;d["34"]=d["P"]*31;d["1q"]=d["P"]*2X;d["2W"]=d["1q"]*10;d["2R"]=d["1q"]*23;d["2P"]=d["1q"]*1t})();k.q.1D||(k.q.1D=t(){o D k(u.1k())});k.q.26||(k.q.26=t(a,b){u.1F(u.1k()+((a||k.P)*(b||1)));o u});k.q.2a||(k.q.2a=t(a,b){u.1F(u.1k()-((a||k.P)*(b||1)));o u});k.q.1Z||(k.q.1Z=t(){u.1Y(0);u.1X(0);u.1U(0);u.1T(0);o u});k.q.1I||(k.q.1I=t(a,b){C(1i a==\'1p\')a=k.1J(a);o 18.2l((u.1k()-a.1k())/(b|k.P))});k.q.1N||(k.q.1N=k.q.1I);k.q.2n||(k.q.2n=t(){d=O(u);o d.1f(-(18.1y(d.K,2)))>3&&d.1f(-(18.1y(d.K,2)))<21?"V":["V","17","16","1a","V"][18.1y(N(d)%10,4)]});k.q.1w||(k.q.1w=t(){v f=(D k(u.1h(),0,1)).1e();o 18.2t((u.1n()+(f>3?f-4:f+3))/7)});k.q.1M=t(){o u.1d().1v(/^.*? ([A-Z]{3}) [0-9]{4}.*$/,"$1").1v(/^.*?\\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\\)$/,"$1$2$3")};k.q.2p=t(){o(u.1u()>0?"-":"+")+O(18.2l(u.1u()/19)).F(2)+O(u.1u()%19,2,"0").F(2)};k.q.1n||(k.q.1n=t(){o((k.2o(u.1h(),u.1c(),u.1b()+1,0,0,0)-k.2o(u.1h(),0,1,0,0,0))/k.P)});k.q.2m||(k.q.2m=t(){v a=u.1D();a.15(a.1c()+1);a.L(0);o a.1b()});k.2j||(k.2j=t(a,b){a=(a+12)%12;C(k.1K(b)&&a==1)o 29;o k.3g.3f[a]});k.1K||(k.1K=t(a){o(((a%4)==0)&&((a%23)!=0)||((a%3e)==0))});k.q.1B||(k.q.1B=t(c){C(!u.3c())o\'&3b;\';v d=u;C(k.1A[c.2g()])c=k.1A[c.2g()];o c.1v(/\\%([3a])/g,t(a,b){39(b){E\'a\':o k.1l(d.1e()).1f(0,3);E\'A\':o k.1l(d.1e());E\'b\':o k.13(d.1c()).1f(0,3);E\'B\':o k.13(d.1c());E\'c\':o d.1d();E\'d\':o d.1b().F(2);E\'H\':o d.1G().F(2);E\'I\':o((h=d.1G()%12)?h:12).F(2);E\'j\':o d.1n().F(3);E\'m\':o(d.1c()+1).F(2);E\'M\':o d.36().F(2);E\'p\':o d.1G()<12?\'33\':\'32\';E\'S\':o d.2Z().F(2);E\'U\':o d.1w().F(2);E\'W\':R Q("%W 2V 2T 2S 25");E\'w\':o d.1e();E\'x\':o d.1r("%m/%d/%Y");E\'X\':o d.1r("%I:%M%p");E\'y\':o d.1h().1d().1f(2);E\'Y\':o d.1h();E\'T\':o d.2p();E\'Z\':o d.1M()}})});k.q.1r||(k.q.1r=k.q.1B);k.22=k.1J;k.1J=t(a){C(1i a!=\'1p\')o a;C(a.K==0||(/^\\s+$/).1E(a))o;2N(v i=0;i<k.1g.K;i++){v r=k.1g[i].J.2L(a);C(r)o k.1g[i].G(r)}o D k(k.22(a))};k.13||(k.13=t(c){v d=-1;C(1i c==\'2J\'){o k.1m[c.1c()]}2I C(1i c==\'27\'){d=c-1;C(d<0||d>11)R D Q("1s 1C 2b 2q 1W 1V 2d 1 2c 12:"+d);o k.1m[d]}v m=k.1m.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1C 1p");C(m.K>1)R D Q("1Q 1C");o k.1m[d]});k.1l||(k.1l=t(c){v d=-1;C(1i c==\'27\'){d=c-1;C(d<0||d>6)R D Q("1s 1z 2b 2q 1W 1V 2d 1 2c 7");o k.1o[d]}v m=k.1o.1S(t(a,b){C(D 1O("^"+c,"i").1E(a)){d=b;o 1R}o 2f});C(m.K==0)R D Q("1s 1z 1p");C(m.K>1)R D Q("1Q 1z");o k.1o[d]});k.1g||(k.1g=[{J:/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})/,G:t(a){v d=D k();d.1L(a[3]);d.L(14(a[2],10));d.15(14(a[1],10)-1);o d}},{J:/(\\d{4})(?:-?(\\d{2})(?:-?(\\d{2})(?:[T ](\\d{2})(?::?(\\d{2})(?::?(\\d{2})(?:\\.(\\d+))?)?)?(?:Z|(?:([-+])(\\d{2})(?::?(\\d{2}))?)?)?)?)?)?/,G:t(a){v b=0;v d=D k(a[1],0,1);C(a[2])d.15(a[2]-1);C(a[3])d.L(a[3]);C(a[4])d.1Y(a[4]);C(a[5])d.1X(a[5]);C(a[6])d.1U(a[6]);C(a[7])d.1T(N("0."+a[7])*1t);C(a[9]){b=(N(a[9])*19)+N(a[10]);b*=((a[8]==\'-\')?1:-1)}b-=d.1u();1P=(N(d)+(b*19*1t));d.1F(N(1P));o d}},{J:/^2E/i,G:t(){o D k()}},{J:/^2D/i,G:t(){v d=D k();d.L(d.1b()+1);o d}},{J:/^2C/i,G:t(){v d=D k();d.L(d.1b()-1);o d}},{J:/^(\\d{1,2})(17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[1],10));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+)$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));o d}},{J:/^(\\d{1,2})(?:17|16|1a|V)? (\\w+),? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[1],10));d.15(k.13(a[2]));d.1L(a[3]);o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));o d}},{J:/^(\\w+) (\\d{1,2})(?:17|16|1a|V)?,? (\\d{4})$/i,G:t(a){v d=D k();d.L(14(a[2],10));d.15(k.13(a[1]));d.1L(a[3]);o d}},{J:/^3m (\\w+)$/i,G:t(a){v d=D k();v b=d.1e();v c=k.1l(a[1]);v e=c-b;C(c<=b){e+=7}d.L(d.1b()+e);o d}},{J:/^2B (\\w+)$/i,G:t(a){R D Q("2A 25 3r");}}]);',62,214,'||||||||||||||||||||Date||||return||prototype|||function|this|var|||||||if|new|case|zf|handler|||re|length|setDate||Number|String|DAY|Error|throw||||th||||||||parseMonth|parseInt|setMonth|nd|st|Math|60|rd|getDate|getMonth|toString|getDay|substr|__PARSE_PATTERNS|getFullYear|typeof|rz|getTime|parseDay|MONTH_NAMES|getDayOfYear|DAY_NAMES|string|YEAR|format|Invalid|1000|getTimezoneOffset|replace|getWeek|split|min|day|FORMATS|strftime|month|clone|test|setTime|getHours|str|diff|parse|isLeapYear|setYear|getTimezone|compare|RegExp|time|Ambiguous|true|findAll|setMilliseconds|setSeconds|be|must|setMinutes|setHours|clearTime|substring||__native_parse|100||yet|increment|number|||decrement|index|and|between|HOUR|false|toLowerCase|MINUTE|SECOND|daysInMonth|while|floor|lastDayOfMonth|getOrdinal|UTC|getGMTOffset|value|July|rfc822|round|June|dT|iso8601|May|April|March|Not|last|yes|tom|tod|February|db|January|else|object|DAYS_PER_MONTH|exec|Saturday|for|Friday|MILLENNIUM|Thursday|CENTURY|supported|not|Wednesday|is|DECADE|365|Tuesday|getSeconds|||PM|AM|MONTH|Monday|getMinutes|WEEK|Sunday|switch|aAbBcdHIjmMpSUWwxXyYTZ|nbsp|valueOf|December|400|DAYS_IN_MONTH|Convensions|MILLISECOND|November|ERA|October|EPOCH|next|September|long|short|August|implemented'.split('|'),0,{}))