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(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             this.set('state', 'loading');
361             $.ajax({
362                 url: this.serverURL,
363                 dataType: 'json',
364                 success: this.loadingSucceeded.bind(this)
365             });
366         }
367     },
368
369     loadingSucceeded: function(data) {
370         if (this.get('state') != 'loading') {
371             alert('erroneous state:', this.get('state'));
372         }
373
374         console.log('galleries:', data);
375
376         if (data.length === 0) {
377             this.set('data', []);
378         } else {            
379             this.set('data', data[0].pages);
380         }
381
382         this.set('state', 'synced');
383     },
384
385     set: function(property, value) {
386         if (property == 'state') {
387             console.log(this.description(), ':', property, '=', value);
388         }
389         return this._super(property, value);
390     }
391 });
392
393
394 Editor.DocumentModel = Editor.Model.extend({
395     _className: 'Editor.DocumentModel',
396     data: null, // name, text_url, revision, latest_shared_rev, parts_url, dc_url, size, merge_url
397     contentModels: {},
398     state: 'empty',
399     errors: '',
400     revision: '',
401     user: '',
402   
403     init: function() {
404         this._super();
405         this.set('state', 'empty');        
406     },
407   
408     load: function() {
409         if (this.get('state') == 'empty') {
410             this.set('state', 'loading');
411             messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...');
412             $.ajax({
413                 cache: false,
414                 url: documentInfo.docURL,
415                 dataType: 'json',
416                 success: this.successfulLoad.bind(this),
417                 error: this.failedLoad.bind(this)
418             });
419         }
420     },
421   
422     successfulLoad: function(data) {
423         this.set('data', data);
424         this.set('state', 'synced');
425
426         this.set('revision', data.revision);
427         this.set('user', data.user);
428
429         this.contentModels = {
430             'xml': new Editor.XMLModel(this, data.text_url),
431             'html': new Editor.HTMLModel(this, data.text_url, data.html_url),
432             'gallery': new Editor.ImageGalleryModel(this, data.gallery_url)
433         };        
434
435         for (var key in this.contentModels) {
436             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
437         }
438
439         this.error = '';
440
441         messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)');
442     },
443
444     failedLoad: function(response) {
445         if (this.get('state') != 'loading') {
446             alert('erroneous state:', this.get('state'));
447         }
448         
449         var message = parseXHRError(response);        
450         this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+message+"</p>");
451         this.set('state', 'error');
452     },
453   
454     contentModelStateChanged: function(property, value, contentModel) {
455         if (value == 'dirty') {
456             this.set('state', 'dirty');
457             for (var key in this.contentModels) {
458                 if (this.contentModels[key].guid() != contentModel.guid()) {
459                     this.contentModels[key].set('state', 'unsynced');
460                 }
461             }
462         } else if (value == 'updated') {
463             this.set('state', 'synced');
464             for (key in this.contentModels) {
465                 if (this.contentModels[key].guid() == contentModel.guid()) {
466                     this.contentModels[key].set('state', 'synced');
467                     this.revision = this.contentModels[key].get('revision');
468
469                 }
470             }
471             for (key in this.contentModels) {
472                 if (this.contentModels[key].guid() != contentModel.guid()) {
473                     this.contentModels[key].set('revision', this.revision);
474                     this.contentModels[key].set('state', 'empty');
475                 }
476             }
477         }
478     },
479   
480     saveDirtyContentModel: function(message) {
481         for (var key in this.contentModels) {
482             if (this.contentModels[key].get('state') == 'dirty') {
483                 this.contentModels[key].save(message);
484                 break;
485             }
486         }
487     },
488   
489     update: function() {
490         this.set('state', 'loading');
491
492         messageCenter.addMessage('info', 'doc_update',
493             'Uaktualniam dokument...');
494             
495         $.ajax({
496             url: this.data.merge_url,
497             dataType: 'json',
498             type: 'post',
499             data: {
500                 type: 'update',
501                 revision: this.get('revision'),
502                 user: this.get('user')
503             },
504             complete: this.updateCompleted.bind(this),
505             success: function(data) {
506                 this.set('updateData', data);
507                 console.log("new data:", data)
508             }.bind(this)
509         });
510     },
511   
512     updateCompleted: function(xhr, textStatus) {
513         console.log(xhr.status, textStatus);
514         
515         if (xhr.status == 200) 
516         {
517             var udata = this.get('updateData');
518             if(udata.timestamp == udata.parent_timestamp)
519             {
520                 // no change
521                 messageCenter.addMessage('info', 'doc_update',
522                     'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?');
523
524             }
525             else {
526                 this.set('revision', udata.revision);
527                 this.set('user', udata.user);
528                 messageCenter.addMessage('info', 'doc_update', 
529                     'Uaktualnienie dokumentu do wersji ' + udata.revision);
530
531                 for (var key in this.contentModels) {
532                     this.contentModels[key].set('revision', this.get('revision') );
533                     this.contentModels[key].set('state', 'empty');
534                 }
535             }        
536         } else if (xhr.status == 409) { // Konflikt podczas operacji
537             messageCenter.addMessage('error', 'doc_update',
538                 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-(');
539         } else {
540             messageCenter.addMessage('critical', 'doc_update',
541                 'Nieoczekiwany błąd. Pędź po programistów! :-(');
542         }
543         
544         this.set('state', 'synced');
545         this.set('updateData', null);
546     },
547   
548     merge: function(message) {
549         this.set('state', 'loading');
550         messageCenter.addMessage('info', null, 
551             'Scalam dokument z głównym repozytorium...');
552             
553         $.ajax({
554             url: this.data.merge_url,
555             type: 'post',
556             dataType: 'json',
557             data: {
558                 type: 'share',
559                 revision: this.get('revision'),
560                 user: this.get('user'),
561                 message: message
562             },
563             complete: this.mergeCompleted.bind(this),
564             success: function(data) {
565                 this.set('mergeData', data);
566             }.bind(this)
567         });
568     },
569   
570     mergeCompleted: function(xhr, textStatus) {
571         console.log(xhr.status, textStatus);
572         if (xhr.status == 200) { // Sukces
573             this.set('revision', this.get('updateData').revision);
574             this.set('user', this.get('updateData').user);
575             
576             for (var key in this.contentModels) {
577                 this.contentModels[key].set('revision', this.get('revision'));
578                 this.contentModels[key].set('state', 'empty');
579             }
580
581             messageCenter.addMessage('success', null, 'Scaliłem dokument z głównym repozytorium :-)');
582         } else if (xhr.status == 202) { // Wygenerowano PullRequest
583             messageCenter.addMessage('success', null, 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.');
584         } else if (xhr.status == 204) { // Nic nie zmieniono
585             messageCenter.addMessage('info', null, 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?');
586         } else if (xhr.status == 409) { // Konflikt podczas operacji
587             messageCenter.addMessage('error', null, 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-(');
588         } else if (xhr.status == 500) {
589             messageCenter.addMessage('critical', null, 'Błąd serwera. Pędź po programistów! :-(');
590         }
591         this.set('state', 'synced');
592         this.set('mergeData', null);
593     },
594   
595     // For debbuging
596     set: function(property, value) {
597         if (property == 'state') {
598             console.log(this.description(), ':', property, '=', value);
599         }
600         return this._super(property, value);
601     }
602 });
603
604
605 var leftPanelView, rightPanelContainer, doc;
606
607 $(function()
608 {
609     var flashView = new FlashView('#flashview', messageCenter);
610     
611     doc = new Editor.DocumentModel();
612
613     EditorView = new EditorView('#body-wrap', doc);
614     EditorView.freeze("<h1>Wczytuję dokument...</h1>");
615
616     leftPanelView = new PanelContainerView('#left-panel-container', doc);
617     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
618
619     
620 });