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(response)
92     {
93         if (this.get('state') != 'loading') {
94             alert('erroneous state:', this.get('state'));
95         }
96         
97         var message = parseXHRError(response);
98         
99         this.set('error', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
100         this.set('state', 'error');
101         messageCenter.addMessage('error', 'xmlload', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-(');
102     },
103   
104     save: function(message) {
105         if (this.get('state') == 'dirty') {
106             this.set('state', 'updating');
107             messageCenter.addMessage('info', 'xmlsave', 'Zapisuję XML...');
108       
109             var payload = {
110                 contents: this.get('data'),
111                 revision: this.get('revision'),
112                 user: this.document.get('user')
113             };
114             if (message) {
115                 payload.message = message;
116             }
117       
118             $.ajax({
119                 url: this.serverURL,
120                 type: 'post',
121                 dataType: 'json',
122                 data: payload,
123                 success: this.saveSucceeded.bind(this),
124                 error: this.saveFailed.bind(this)
125             });
126             return true;
127         }
128         return false;
129     },
130   
131     saveSucceeded: function(data) {
132         if (this.get('state') != 'updating') {
133             alert('erroneous state:', this.get('state'));
134         }
135         this.set('revision', data.revision);
136         this.set('state', 'updated');
137         messageCenter.addMessage('success', 'xmlsave', 'Zapisałem XML :-)');
138     },
139   
140     saveFailed: function() {
141         if (this.get('state') != 'updating') {
142             alert('erroneous state:', this.get('state'));
143         }
144         messageCenter.addMessage('error', 'xmlsave', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-(');
145         this.set('state', 'dirty');
146     },
147   
148     // For debbuging
149     set: function(property, value) {
150         if (property == 'state') {
151             console.log(this.description(), ':', property, '=', value);
152         }
153         return this._super(property, value);
154     },
155   
156     dataChanged: function(property, value) {
157         if (this.get('state') == 'synced') {
158             this.set('state', 'dirty');
159         }
160     },
161   
162     dispose: function() {
163         this.removeObserver(this);
164         this._super();
165     }
166 });
167
168
169 Editor.HTMLModel = Editor.Model.extend({
170     _className: 'Editor.HTMLModel',
171     dataURL: null,
172     htmlURL: null,
173     renderURL: null,
174     displaData: '',
175     xmlParts: {},
176     state: 'empty',
177   
178     init: function(document, dataURL, htmlURL) {
179         this._super();
180         this.set('state', 'empty');
181         this.set('revision', document.get('revision'));        
182         
183         this.document = document;
184         this.htmlURL = htmlURL;
185         this.dataURL = dataURL;
186         this.renderURL = documentInfo.renderURL;
187         this.xmlParts = {};
188     },
189   
190     load: function(force) {
191         if (force || this.get('state') == 'empty') {
192             this.set('state', 'loading');
193
194             // load the transformed data
195             // messageCenter.addMessage('info', 'Wczytuję HTML...');
196
197             $.ajax({
198                 url: this.htmlURL,
199                 dataType: 'text',
200                 data: {
201                     revision: this.get('revision'),
202                     user: this.document.get('user')
203                     },
204                 success: this.loadingSucceeded.bind(this),
205                 error: this.loadingFailed.bind(this)
206             });
207         }
208     },    
209   
210     loadingSucceeded: function(data) {
211         if (this.get('state') != 'loading') {
212             alert('erroneous state:', this.get('state'));
213         }
214         this.set('data', data);
215         this.set('state', 'synced');
216     },
217   
218     loadingFailed: function(response) {
219         if (this.get('state') != 'loading') {
220             alert('erroneous state:', this.get('state'));
221         }
222         
223         var err = parseXHRError(response);
224         
225         this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + err.error_message);
226         this.set('state', 'error');        
227     },
228
229     getXMLPart: function(elem, callback)
230     {
231         var path = elem.attr('wl2o:path');
232         if(!this.xmlParts[path])
233             this.loadXMLPart(elem, callback);
234         else
235             callback(path, this.xmlParts[path]);
236     },
237
238     loadXMLPart: function(elem, callback)
239     {
240         var path = elem.attr('wl2o:path');
241         var self = this;
242
243         $.ajax({
244             url: this.dataURL,
245             dataType: 'text',
246             data: {
247                 revision: this.get('revision'),
248                 user: this.document.get('user'),
249                 chunk: path
250                 // format: 'nl'
251             },
252             success: function(data) {
253                 self.xmlParts[path] = data;
254                 console.log(data);
255                 callback(path, data);
256             },
257             // TODO: error handling
258             error: function(data) {
259                 console.log('Failed to load fragment');
260                 callback(undefined, undefined);
261             }
262         });
263     },
264
265     putXMLPart: function(elem, data) {
266         var self = this;
267       
268         var path = elem.attr('wl2o:path');
269         this.xmlParts[path] = data;
270
271         this.set('state', 'dirty');
272
273         /* re-render the changed fragment */
274         $.ajax({
275             url: this.renderURL,
276             type: "POST",
277             dataType: 'text; charset=utf-8',
278             data: {
279                 fragment: data,
280                 chunk: path
281                 // format: 'nl'
282             },
283             success: function(htmldata) {
284                 elem.html(htmldata);
285                 self.set('state', 'dirty');
286             }
287         });
288     },
289
290     save: function(message) {
291         if (this.get('state') == 'dirty') {
292             this.set('state', 'updating');
293
294             var payload = {
295                 chunks: $.toJSON(this.xmlParts),
296                 revision: this.get('revision'),
297                 user: this.document.get('user')
298             };
299
300             if (message) {
301                 payload.message = message;
302             }
303
304             console.log(payload)
305
306             $.ajax({
307                 url: this.dataURL,
308                 type: 'post',
309                 dataType: 'json',
310                 data: payload,
311                 success: this.saveSucceeded.bind(this),
312                 error: this.saveFailed.bind(this)
313             });
314             return true;
315         }
316         return false;
317       
318     },
319
320     saveSucceeded: function(data) {
321         if (this.get('state') != 'updating') {
322             alert('erroneous state:', this.get('state'));
323         }
324
325         // flush the cache
326         this.xmlParts = {};
327     
328         this.set('revision', data.revision);
329         this.set('state', 'updated');
330     },
331
332     saveFailed: function() {
333         if (this.get('state') != 'updating') {
334             alert('erroneous state:', this.get('state'));
335         }        
336         this.set('state', 'dirty');
337     },
338
339     // For debbuging
340     set: function(property, value) {
341         if (property == 'state') {
342             console.log(this.description(), ':', property, '=', value);
343         }
344         return this._super(property, value);
345     }
346 });
347
348
349 Editor.ImageGalleryModel = Editor.Model.extend({
350     _className: 'Editor.ImageGalleryModel',
351     serverURL: null,
352     data: [],
353     state: 'empty',
354
355     init: function(document, serverURL) {
356         this._super();
357         this.set('state', 'empty');
358         this.serverURL = serverURL;
359         // olewać data
360         this.pages = [];
361     },
362
363     load: function(force) {
364         if (force || this.get('state') == 'empty') {
365             console.log("setting state");
366             this.set('state', 'loading');
367             console.log("going ajax");
368             $.ajax({
369                 url: this.serverURL,
370                 dataType: 'json',
371                 success: this.loadingSucceeded.bind(this),
372                 error: this.loadingFailed.bind(this)
373             });
374         }
375     },
376
377     loadingSucceeded: function(data) 
378     {
379         console.log("success");        
380         
381         if (this.get('state') != 'loading') {
382             alert('erroneous state:', this.get('state'));
383         }
384
385         console.log('galleries:', data);
386
387         if (data.length === 0) {
388             this.set('data', []);
389         } else {            
390             this.set('data', data[0].pages);
391         }
392
393         this.set('state', 'synced');
394     },
395
396     loadingFailed: function(data) {
397         console.log("failed");
398
399         if (this.get('state') != 'loading') {
400             alert('erroneous state:', this.get('state'));
401         }       
402
403         this.set('state', 'error');
404     },
405
406     set: function(property, value) {
407         if (property == 'state') {
408             console.log(this.description(), ':', property, '=', value);
409         }
410         return this._super(property, value);
411     }
412 });
413
414
415 Editor.DocumentModel = Editor.Model.extend({
416     _className: 'Editor.DocumentModel',
417     data: null, // name, text_url, revision, latest_shared_rev, parts_url, dc_url, size, merge_url
418     contentModels: {},
419     state: 'empty',
420     errors: '',
421     revision: '',
422     user: '',
423   
424     init: function() {
425         this._super();
426         this.set('state', 'empty');        
427     },
428   
429     load: function() {
430         if (this.get('state') == 'empty') {
431             this.set('state', 'loading');
432             messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...');
433             $.ajax({
434                 cache: false,
435                 url: documentInfo.docURL,
436                 dataType: 'json',
437                 success: this.successfulLoad.bind(this),
438                 error: this.failedLoad.bind(this)
439             });
440         }
441     },
442   
443     successfulLoad: function(data) {
444         this.set('data', data);
445         this.set('state', 'synced');
446
447         this.set('revision', data.revision);
448         this.set('user', data.user);
449
450         this.contentModels = {
451             'xml': new Editor.XMLModel(this, data.text_url),
452             'html': new Editor.HTMLModel(this, data.text_url, data.html_url),
453             'gallery': new Editor.ImageGalleryModel(this, data.gallery_url)
454         };        
455
456         for (var key in this.contentModels) {
457             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
458         }
459
460         this.error = '';
461
462         messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)');
463     },
464
465     failedLoad: function(response) {
466         if (this.get('state') != 'loading') {
467             alert('erroneous state:', this.get('state'));
468         }
469         
470         var err = parseXHRError(response);
471         this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+err.error_message+"</p>");
472         this.set('state', 'error');
473     },
474   
475     contentModelStateChanged: function(property, value, contentModel) {
476         if (value == 'dirty') {
477             this.set('state', 'dirty');
478             for (var key in this.contentModels) {
479                 if (this.contentModels[key].guid() != contentModel.guid()) {
480                     this.contentModels[key].set('state', 'unsynced');
481                 }
482             }
483         } else if (value == 'updated') {
484             this.set('state', 'synced');
485             for (key in this.contentModels) {
486                 if (this.contentModels[key].guid() == contentModel.guid()) {
487                     this.contentModels[key].set('state', 'synced');
488                     this.revision = this.contentModels[key].get('revision');
489
490                 }
491             }
492             for (key in this.contentModels) {
493                 if (this.contentModels[key].guid() != contentModel.guid()) {
494                     this.contentModels[key].set('revision', this.revision);
495                     this.contentModels[key].set('state', 'empty');
496                 }
497             }
498         }
499     },
500   
501     saveDirtyContentModel: function(message) {
502         for (var key in this.contentModels) {
503             if (this.contentModels[key].get('state') == 'dirty') {
504                 this.contentModels[key].save(message);
505                 break;
506             }
507         }
508     },
509   
510     update: function() {
511         this.set('state', 'loading');
512
513         messageCenter.addMessage('info', 'doc_update',
514             'Uaktualniam dokument...');
515             
516         $.ajax({
517             url: this.data.merge_url,
518             dataType: 'json',
519             type: 'post',
520             data: {
521                 type: 'update',
522                 revision: this.get('revision'),
523                 user: this.get('user')
524             },
525             complete: this.updateCompleted.bind(this),            
526         });
527     },
528   
529     updateCompleted: function(xhr, textStatus)
530     {
531         console.log(xhr.status, xhr.responseText);
532         var response = parseXHRResponse(xhr);
533         if(response.success)
534         {
535             if( (response.data.result == 'no-op')
536              || (response.data.timestamp == response.data.parent_timestamp))
537             {
538                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
539                 {
540                     // we're out of sync
541                     this.set('state', 'unsynced');
542                     return;
543                 }
544                 
545                 messageCenter.addMessage('info', 'doc_update',
546                     'Już posiadasz najbardziej aktualną wersję.');
547                     this.set('state', 'synced');
548                 return;
549             }
550
551             // result: success
552             this.set('revision', response.data.revision);
553             this.set('user', response.data.user);
554
555             messageCenter.addMessage('info', 'doc_update',
556                 'Uaktualnienie dokumentu do wersji ' + response.data.revision);
557
558             for (var key in this.contentModels) {
559                 this.contentModels[key].set('revision', this.get('revision') );
560                 this.contentModels[key].set('state', 'empty');
561             }
562
563             this.set('state', 'synced');
564             return;
565         }
566
567         // no success means trouble
568         messageCenter.addMessage(response.error_level, 'doc_update', 
569             response.error_message);       
570         
571         this.set('state', 'unsynced');
572     },
573   
574     merge: function(message) {
575         this.set('state', 'loading');
576         messageCenter.addMessage('info', 'doc_merge',
577             'Scalam dokument z głównym repozytorium...');
578             
579         $.ajax({
580             url: this.data.merge_url,
581             type: 'post',
582             dataType: 'json',
583             data: {
584                 type: 'share',
585                 revision: this.get('revision'),
586                 user: this.get('user'),
587                 message: message
588             },
589             complete: this.mergeCompleted.bind(this),
590             success: function(data) {
591                 this.set('mergeData', data);
592             }.bind(this)
593         });
594     },
595   
596     mergeCompleted: function(xhr, textStatus) {
597         console.log(xhr.status, xhr.responseText);
598         var response = parseXHRResponse(xhr);
599         if(response.success)
600         {
601             if( (response.data.result == 'no-op')
602              || (response.data.shared_timestamp == response.data.shared_parent_timestamp))
603             {
604                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
605                 {
606                     // we're out of sync
607                     this.set('state', 'unsynced');
608                     return;
609                 }
610
611                 messageCenter.addMessage('info', 'doc_merge',
612                     'Twoja aktualna wersja nie różni się od ostatnio zatwierdzonej.');
613                 this.set('state', 'synced');
614                 return;
615             }
616
617             if( response.data.result == 'accepted')
618             {
619                 messageCenter.addMessage('info', 'doc_merge',
620                     'Prośba o zatwierdzenie została przyjęta i oczekuję na przyjęcie.');
621                 this.set('state', 'synced');
622                 return;
623             }
624
625             // result: success
626             this.set('revision', response.data.revision);
627             this.set('user', response.data.user);
628
629             messageCenter.addMessage('info', 'doc_merge',
630                 'Twoja wersja dokumentu została zatwierdzona.');
631             
632             this.set('state', 'synced');
633             return;
634         }
635
636         // no success means trouble
637         messageCenter.addMessage(response.error_level, 'doc_merge',
638             response.error_message);
639
640         this.set('state', 'unsynced');
641     },
642   
643     // For debbuging
644     set: function(property, value) {
645         if (property == 'state') {
646             console.log(this.description(), ':', property, '=', value);
647         }
648         return this._super(property, value);
649     }
650 });
651
652
653 var leftPanelView, rightPanelContainer, doc;
654
655 $(function()
656 {
657     var flashView = new FlashView('#flashview', messageCenter);
658     
659     doc = new Editor.DocumentModel();
660
661     EditorView = new EditorView('#body-wrap', doc);
662     EditorView.freeze("<h1>Wczytuję dokument...</h1>");
663
664     leftPanelView = new PanelContainerView('#left-panel-container', doc);
665     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
666
667     
668 });