Merge branch 'master' of stigma.nowoczesnapolska.org.pl:platforma
[redakcja.git] / platforma / static / js / models.js
1 /*globals Editor fileId SplitView PanelContainerView EditorView FlashView messageCenter*/
2 Editor.Model = Editor.Object.extend({
3     synced: false,
4     data: null
5 });
6
7 Editor.ToolbarButtonsModel = Editor.Model.extend({
8     className: 'Editor.ToolbarButtonsModel',
9     buttons: {},
10   
11     init: function() {
12         this._super();
13     },
14   
15     load: function() {
16         if (!this.get('buttons').length) {
17             $.ajax({
18                 url: documentInfo.toolbarURL,
19                 dataType: 'json',
20                 success: this.loadSucceeded.bind(this)
21             });
22         }
23     },
24   
25     loadSucceeded: function(data)
26     {
27         // do some escaping
28         $.each(data, function() {
29             $.each(this.buttons, function() {
30                 //do some lame escapes
31                 this.tooltip = this.tooltip.replace(/"/g, """);
32             });
33         });
34         this.set('buttons', data);
35     }
36 });
37
38
39 // Stany modelu:
40 //
41 //                  -> error -> loading
42 //                 /
43 // empty -> loading -> synced -> unsynced -> loading
44 //                           \
45 //                            -> dirty -> updating -> updated -> synced
46 //
47 Editor.XMLModel = Editor.Model.extend({
48     _className: 'Editor.XMLModel',
49     serverURL: null,
50     data: '',
51     state: 'empty',
52   
53     init: function(document, serverURL) {
54         this._super();
55         this.set('state', 'empty');
56         this.set('revision', document.get('revision'));
57         this.document = document;
58         this.serverURL = serverURL;
59         this.toolbarButtonsModel = new Editor.ToolbarButtonsModel();
60         this.addObserver(this, 'data', this.dataChanged.bind(this));
61     },
62   
63     load: function(force) {
64         if (force || this.get('state') == 'empty') {
65             this.set('state', 'loading');
66             messageCenter.addMessage('info', 'xmlload', 'Wczytuję XML...');
67             $.ajax({
68                 url: this.serverURL,
69                 dataType: 'text',
70                 data: {
71                     revision: this.get('revision'),
72                     user: this.document.get('user')
73                     },
74                 success: this.loadingSucceeded.bind(this),
75                 error: this.loadingFailed.bind(this)
76             });
77             return true;
78         }
79         return false;
80     },
81   
82     loadingSucceeded: function(data) {
83         if (this.get('state') != 'loading') {
84             alert('erroneous state:', this.get('state'));
85         }
86         this.set('data', data);
87         this.set('state', 'synced');
88         messageCenter.addMessage('success', 'xmlload', 'Wczytałem XML :-)');
89     },
90   
91     loadingFailed: function() {
92         if (this.get('state') != 'loading') {
93             alert('erroneous state:', this.get('state'));
94         }
95         var message = parseXHRError(response);
96         
97         this.set('error', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
98         this.set('state', 'error');
99         messageCenter.addMessage('error', 'xmlload', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-(');
100     },
101   
102     save: function(message) {
103         if (this.get('state') == 'dirty') {
104             this.set('state', 'updating');
105             messageCenter.addMessage('info', 'xmlsave', 'Zapisuję XML...');
106       
107             var payload = {
108                 contents: this.get('data'),
109                 revision: this.get('revision'),
110                 user: this.document.get('user')
111             };
112             if (message) {
113                 payload.message = message;
114             }
115       
116             $.ajax({
117                 url: this.serverURL,
118                 type: 'post',
119                 dataType: 'json',
120                 data: payload,
121                 success: this.saveSucceeded.bind(this),
122                 error: this.saveFailed.bind(this)
123             });
124             return true;
125         }
126         return false;
127     },
128   
129     saveSucceeded: function(data) {
130         if (this.get('state') != 'updating') {
131             alert('erroneous state:', this.get('state'));
132         }
133         this.set('revision', data.revision);
134         this.set('state', 'updated');
135         messageCenter.addMessage('success', 'xmlsave', 'Zapisałem XML :-)');
136     },
137   
138     saveFailed: function() {
139         if (this.get('state') != 'updating') {
140             alert('erroneous state:', this.get('state'));
141         }
142         messageCenter.addMessage('error', 'xmlsave', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-(');
143         this.set('state', 'dirty');
144     },
145   
146     // For debbuging
147     set: function(property, value) {
148         if (property == 'state') {
149             console.log(this.description(), ':', property, '=', value);
150         }
151         return this._super(property, value);
152     },
153   
154     dataChanged: function(property, value) {
155         if (this.get('state') == 'synced') {
156             this.set('state', 'dirty');
157         }
158     },
159   
160     dispose: function() {
161         this.removeObserver(this);
162         this._super();
163     }
164 });
165
166
167 Editor.HTMLModel = Editor.Model.extend({
168     _className: 'Editor.HTMLModel',
169     dataURL: null,
170     htmlURL: null,
171     renderURL: null,
172     displaData: '',
173     xmlParts: {},
174     state: 'empty',
175   
176     init: function(document, dataURL, htmlURL) {
177         this._super();
178         this.set('state', 'empty');
179         this.set('revision', document.get('revision'));        
180         
181         this.document = document;
182         this.htmlURL = htmlURL;
183         this.dataURL = dataURL;
184         this.renderURL = documentInfo.renderURL;
185         this.xmlParts = {};
186     },
187   
188     load: function(force) {
189         if (force || this.get('state') == 'empty') {
190             this.set('state', 'loading');
191
192             // load the transformed data
193             // messageCenter.addMessage('info', 'Wczytuję HTML...');
194
195             $.ajax({
196                 url: this.htmlURL,
197                 dataType: 'text',
198                 data: {
199                     revision: this.get('revision'),
200                     user: this.document.get('user')
201                     },
202                 success: this.loadingSucceeded.bind(this),
203                 error: this.loadingFailed.bind(this)
204             });
205         }
206     },    
207   
208     loadingSucceeded: function(data) {
209         if (this.get('state') != 'loading') {
210             alert('erroneous state:', this.get('state'));
211         }
212         this.set('data', data);
213         this.set('state', 'synced');
214     },
215   
216     loadingFailed: function(response) {
217         if (this.get('state') != 'loading') {
218             alert('erroneous state:', this.get('state'));
219         }
220         
221         var message = parseXHRError(response);
222         
223         this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + message);
224         this.set('state', 'error');        
225     },
226
227     getXMLPart: function(elem, callback)
228     {
229         var path = elem.attr('wl2o:path');
230         if(!this.xmlParts[path])
231             this.loadXMLPart(elem, callback);
232         else
233             callback(path, this.xmlParts[path]);
234     },
235
236     loadXMLPart: function(elem, callback)
237     {
238         var path = elem.attr('wl2o:path');
239         var self = this;
240
241         $.ajax({
242             url: this.dataURL,
243             dataType: 'text',
244             data: {
245                 revision: this.get('revision'),
246                 user: this.document.get('user'),
247                 part: path
248             },
249             success: function(data) {
250                 self.xmlParts[path] = data;
251                 callback(path, data);
252             },
253             // TODO: error handling
254             error: function(data) {
255                 console.log('Failed to load fragment');
256                 callback(undefined, undefined);
257             }
258         });
259     },
260
261     putXMLPart: function(elem, data) {
262         var self = this;
263       
264         var path = elem.attr('wl2o:path');
265         this.xmlParts[path] = data;
266
267         this.set('state', 'unsynced');
268
269         /* re-render the changed fragment */
270         $.ajax({
271             url: this.renderURL,
272             type: "POST",
273             dataType: 'text; charset=utf-8',
274             data: {
275                 fragment: data,
276                 part: path
277             },
278             success: function(htmldata) {
279                 elem.replaceWith(htmldata);
280                 self.set('state', 'dirty');
281             }
282         });
283     },
284
285     save: function(message) {
286         if (this.get('state') == 'dirty') {
287             this.set('state', 'updating');
288
289             var payload = {
290                 chunks: $.toJSON(this.xmlParts),
291                 revision: this.get('revision'),
292                 user: this.document.get('user')
293             };
294
295             if (message) {
296                 payload.message = message;
297             }
298
299             console.log(payload)
300
301             $.ajax({
302                 url: this.dataURL,
303                 type: 'post',
304                 dataType: 'json',
305                 data: payload,
306                 success: this.saveSucceeded.bind(this),
307                 error: this.saveFailed.bind(this)
308             });
309             return true;
310         }
311         return false;
312       
313     },
314
315     saveSucceeded: function(data) {
316         if (this.get('state') != 'updating') {
317             alert('erroneous state:', this.get('state'));
318         }
319
320         // flush the cache
321         this.xmlParts = {};
322     
323         this.set('revision', data.revision);
324         this.set('state', 'updated');
325     },
326
327     saveFailed: function() {
328         if (this.get('state') != 'updating') {
329             alert('erroneous state:', this.get('state'));
330         }        
331         this.set('state', 'dirty');
332     },
333
334     // For debbuging
335     set: function(property, value) {
336         if (property == 'state') {
337             console.log(this.description(), ':', property, '=', value);
338         }
339         return this._super(property, value);
340     }
341 });
342
343
344 Editor.ImageGalleryModel = Editor.Model.extend({
345     _className: 'Editor.ImageGalleryModel',
346     serverURL: null,
347     data: [],
348     state: 'empty',
349
350     init: function(document, serverURL) {
351         this._super();
352         this.set('state', 'empty');
353         this.serverURL = serverURL;
354         // olewać data
355         this.pages = [];
356     },
357
358     load: function(force) {
359         if (force || this.get('state') == 'empty') {
360             console.log("setting state");
361             this.set('state', 'loading');
362             console.log("going ajax");
363             $.ajax({
364                 url: this.serverURL,
365                 dataType: 'json',
366                 success: this.loadingSucceeded.bind(this),
367                 error: this.loadingFailed.bind(this)
368             });
369         }
370     },
371
372     loadingSucceeded: function(data) 
373     {
374         console.log("success");        
375         
376         if (this.get('state') != 'loading') {
377             alert('erroneous state:', this.get('state'));
378         }
379
380         console.log('galleries:', data);
381
382         if (data.length === 0) {
383             this.set('data', []);
384         } else {            
385             this.set('data', data[0].pages);
386         }
387
388         this.set('state', 'synced');
389     },
390
391     loadingFailed: function(data) {
392         console.log("failed");
393
394         if (this.get('state') != 'loading') {
395             alert('erroneous state:', this.get('state'));
396         }       
397
398         this.set('state', 'error');
399     },
400
401     set: function(property, value) {
402         if (property == 'state') {
403             console.log(this.description(), ':', property, '=', value);
404         }
405         return this._super(property, value);
406     }
407 });
408
409
410 Editor.DocumentModel = Editor.Model.extend({
411     _className: 'Editor.DocumentModel',
412     data: null, // name, text_url, revision, latest_shared_rev, parts_url, dc_url, size, merge_url
413     contentModels: {},
414     state: 'empty',
415     errors: '',
416     revision: '',
417     user: '',
418   
419     init: function() {
420         this._super();
421         this.set('state', 'empty');        
422     },
423   
424     load: function() {
425         if (this.get('state') == 'empty') {
426             this.set('state', 'loading');
427             messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...');
428             $.ajax({
429                 cache: false,
430                 url: documentInfo.docURL,
431                 dataType: 'json',
432                 success: this.successfulLoad.bind(this),
433                 error: this.failedLoad.bind(this)
434             });
435         }
436     },
437   
438     successfulLoad: function(data) {
439         this.set('data', data);
440         this.set('state', 'synced');
441
442         this.set('revision', data.revision);
443         this.set('user', data.user);
444
445         this.contentModels = {
446             'xml': new Editor.XMLModel(this, data.text_url),
447             'html': new Editor.HTMLModel(this, data.text_url, data.html_url),
448             'gallery': new Editor.ImageGalleryModel(this, data.gallery_url)
449         };        
450
451         for (var key in this.contentModels) {
452             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
453         }
454
455         this.error = '';
456
457         messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)');
458     },
459
460     failedLoad: function(response) {
461         if (this.get('state') != 'loading') {
462             alert('erroneous state:', this.get('state'));
463         }
464         
465         var message = parseXHRError(response);        
466         this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+message+"</p>");
467         this.set('state', 'error');
468     },
469   
470     contentModelStateChanged: function(property, value, contentModel) {
471         if (value == 'dirty') {
472             this.set('state', 'dirty');
473             for (var key in this.contentModels) {
474                 if (this.contentModels[key].guid() != contentModel.guid()) {
475                     this.contentModels[key].set('state', 'unsynced');
476                 }
477             }
478         } else if (value == 'updated') {
479             this.set('state', 'synced');
480             for (key in this.contentModels) {
481                 if (this.contentModels[key].guid() == contentModel.guid()) {
482                     this.contentModels[key].set('state', 'synced');
483                     this.revision = this.contentModels[key].get('revision');
484
485                 }
486             }
487             for (key in this.contentModels) {
488                 if (this.contentModels[key].guid() != contentModel.guid()) {
489                     this.contentModels[key].set('revision', this.revision);
490                     this.contentModels[key].set('state', 'empty');
491                 }
492             }
493         }
494     },
495   
496     saveDirtyContentModel: function(message) {
497         for (var key in this.contentModels) {
498             if (this.contentModels[key].get('state') == 'dirty') {
499                 this.contentModels[key].save(message);
500                 break;
501             }
502         }
503     },
504   
505     update: function() {
506         this.set('state', 'loading');
507
508         messageCenter.addMessage('info', 'doc_update',
509             'Uaktualniam dokument...');
510             
511         $.ajax({
512             url: this.data.merge_url,
513             dataType: 'json',
514             type: 'post',
515             data: {
516                 type: 'update',
517                 revision: this.get('revision'),
518                 user: this.get('user')
519             },
520             complete: this.updateCompleted.bind(this),
521             success: function(data) {
522                 this.set('updateData', data);
523                 console.log("new data:", data)
524             }.bind(this)
525         });
526     },
527   
528     updateCompleted: function(xhr, textStatus) {
529         console.log(xhr.status, textStatus);
530         
531         if (xhr.status == 200) 
532         {
533             var udata = this.get('updateData');
534             if(udata.timestamp == udata.parent_timestamp)
535             {
536                 // no change
537                 messageCenter.addMessage('info', 'doc_update',
538                     'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?');
539
540             }
541             else {
542                 this.set('revision', udata.revision);
543                 this.set('user', udata.user);
544                 messageCenter.addMessage('info', 'doc_update', 
545                     'Uaktualnienie dokumentu do wersji ' + udata.revision);
546
547                 for (var key in this.contentModels) {
548                     this.contentModels[key].set('revision', this.get('revision') );
549                     this.contentModels[key].set('state', 'empty');
550                 }
551             }        
552         } else if (xhr.status == 409) { // Konflikt podczas operacji
553             messageCenter.addMessage('error', 'doc_update',
554                 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-(');
555         } else {
556             messageCenter.addMessage('critical', 'doc_update',
557                 'Nieoczekiwany błąd. Pędź po programistów! :-(');
558         }
559         
560         this.set('state', 'synced');
561         this.set('updateData', null);
562     },
563   
564     merge: function(message) {
565         this.set('state', 'loading');
566         messageCenter.addMessage('info', null, 
567             'Scalam dokument z głównym repozytorium...');
568             
569         $.ajax({
570             url: this.data.merge_url,
571             type: 'post',
572             dataType: 'json',
573             data: {
574                 type: 'share',
575                 revision: this.get('revision'),
576                 user: this.get('user'),
577                 message: message
578             },
579             complete: this.mergeCompleted.bind(this),
580             success: function(data) {
581                 this.set('mergeData', data);
582             }.bind(this)
583         });
584     },
585   
586     mergeCompleted: function(xhr, textStatus) {
587         console.log(xhr.status, textStatus);
588         if (xhr.status == 200) { // Sukces
589             this.set('revision', this.get('updateData').revision);
590             this.set('user', this.get('updateData').user);
591             
592             for (var key in this.contentModels) {
593                 this.contentModels[key].set('revision', this.get('revision'));
594                 this.contentModels[key].set('state', 'empty');
595             }
596
597             messageCenter.addMessage('success', null, 'Scaliłem dokument z głównym repozytorium :-)');
598         } else if (xhr.status == 202) { // Wygenerowano PullRequest
599             messageCenter.addMessage('success', null, 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.');
600         } else if (xhr.status == 204) { // Nic nie zmieniono
601             messageCenter.addMessage('info', null, 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?');
602         } else if (xhr.status == 409) { // Konflikt podczas operacji
603             messageCenter.addMessage('error', null, 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-(');
604         } else if (xhr.status == 500) {
605             messageCenter.addMessage('critical', null, 'Błąd serwera. Pędź po programistów! :-(');
606         }
607         this.set('state', 'synced');
608         this.set('mergeData', null);
609     },
610   
611     // For debbuging
612     set: function(property, value) {
613         if (property == 'state') {
614             console.log(this.description(), ':', property, '=', value);
615         }
616         return this._super(property, value);
617     }
618 });
619
620
621 var leftPanelView, rightPanelContainer, doc;
622
623 $(function()
624 {
625     var flashView = new FlashView('#flashview', messageCenter);
626     
627     doc = new Editor.DocumentModel();
628
629     EditorView = new EditorView('#body-wrap', doc);
630     EditorView.freeze("<h1>Wczytuję dokument...</h1>");
631
632     leftPanelView = new PanelContainerView('#left-panel-container', doc);
633     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
634
635     
636 });