Fix error with revision check during saving.
[redakcja.git] / src / redakcja / static / js / wiki / wikiapi.js
1 (function($) {
2     $.wikiapi = {};
3     var noop = function() {
4     };
5     var noops = {
6         success: noop,
7         failure: noop
8     };
9     /*
10      * Return absolute reverse path of given named view. (at least he have it
11      * hard-coded in one place)
12      *
13      * TODO: think of a way, not to hard-code it here ;)
14      *
15      */
16     function reverse() {
17         var vname = arguments[0];
18         var base_path = "/editor";
19
20         if (vname == "ajax_document_text") {
21             var path = "/text/" + arguments[1] + '/';
22
23             if (arguments[2] !== undefined)
24                 path += arguments[2] + '/';
25
26             return base_path + path;
27         }
28
29         if (vname == "ajax_document_revert") {
30             return base_path + "/revert/" + arguments[1] + '/';
31         }
32
33
34         if (vname == "ajax_document_history") {
35             return base_path + "/history/" + arguments[1] + '/';
36         }
37
38         if (vname == "ajax_document_scans") {
39             return base_path + "/scans/" + arguments[1] + '/';
40         }
41         if (vname == "ajax_document_gallery") {
42             return base_path + "/gallery/" + arguments[1] + '/';
43         }
44
45         if (vname == "ajax_document_diff")
46             return base_path + "/diff/" + arguments[1] + '/';
47
48         if (vname == "ajax_document_rev")
49             return base_path + "/rev/" + arguments[1] + '/';
50
51         if (vname == "ajax_document_pubmark")
52             return base_path + "/pubmark/" + arguments[1] + '/';
53
54         if (vname == "ajax_cover_preview")
55             return "/cover/preview/";
56
57         console.log("Couldn't reverse match:", vname);
58         return "/404.html";
59     };
60     class Api {
61         static csrf = $('input[name="csrfmiddlewaretoken"]').val();
62
63         // TODO: Add waiting marker, error reporting.
64         static post(url, data) {
65             data['csrfmiddlewaretoken'] = this.csrf;
66             $.ajax({
67                 url: url,
68                 type: "POST",
69                 data: data
70             });
71         }
72
73         static get(url, callback) {
74             $.ajax({
75                 url: url,
76                 type: "GET",
77                 success: function(data) {
78                     callback(data);
79                 },
80             });
81         }
82
83         static setGallery(id, gallery) {
84             this.post(
85                 '/editor/set-gallery/' + id + '/',
86                 {gallery: gallery}
87             )
88         }
89         static setGalleryStart(id, start) {
90             this.post(
91                 '/editor/set-gallery-start/' + id + '/',
92                 {start: start}
93             )
94         }
95
96         static openGalleryEdit(bookSlug) {
97             window.open(
98                 '/documents/book/' + bookSlug + '/gallery/',
99                 '_blank'
100             ).focus();
101         }
102
103         static withGalleryList(callback) {
104             this.get(
105                 '/editor/galleries/',
106                 callback
107             );
108         }
109     }
110
111     /*
112      * Document Abstraction
113      */
114     class WikiDocument {
115         constructor(element_id) {
116             var meta = $('#' + element_id);
117             this.id = meta.attr('data-chunk-id');
118
119             this.revision = $("*[data-key='revision']", meta).text();
120             this.readonly = !!$("*[data-key='readonly']", meta).text();
121
122             this.bookSlug = $("*[data-key='book-slug']", meta).text();
123             this.scansLink = $("*[data-key='scans']", meta).text();
124             this.galleryLink = $("*[data-key='gallery']", meta).text();
125             this.galleryStart = parseInt($("*[data-key='gallery-start']", meta).text());
126             this.fullUri = $("*[data-key='full-uri']", meta).text();
127
128             this.text = null;
129             this.saving = false;
130             this.has_local_changes = false;
131             this.active = new Date();
132             this._lock = -1;
133             this._context_lock = -1;
134             this._lock_count = 0;
135         };
136
137         triggerDocumentChanged() {
138             $(document).trigger('wlapi_document_changed', this);
139         }
140
141         /*
142          * Fetch text of this document.
143          */
144         fetch(params) {
145             params = $.extend({}, noops, params);
146             var self = this;
147             $.ajax({
148                 method: "GET",
149                 url: reverse("ajax_document_text", self.id),
150                 data: {"revision": self.revision},
151                 dataType: 'json',
152                 success: function(data) {
153                     var changed = false;
154
155                     if (self.text === null || self.revision !== data.revision) {
156                         self.text = data.text;
157                         $.wiki.undo.push(data.text);
158                         self.revision = data.revision;
159                         self.gallery = data.gallery;
160                         changed = true;
161                         self.triggerDocumentChanged();
162                     };
163
164                     self.has_local_changes = false;
165                     params['success'](self, changed);
166                 },
167                 error: function() {
168                     params['failure'](self, "Nie udało się wczytać treści dokumentu.");
169                 }
170             });
171         }
172
173         /*
174          * Fetch history of this document.
175          */
176         fetchHistory(params) {
177             /* this doesn't modify anything, so no locks */
178             params = $.extend({}, noops, params);
179             var self = this;
180             $.ajax({
181                 method: "GET",
182                 url: reverse("ajax_document_history", self.id) + "?before=" + params.before,
183                 dataType: 'json',
184                 success: function(data) {
185                     params['success'](self, data);
186                 },
187                 error: function() {
188                     params['failure'](self, "Nie udało się wczytać historii dokumentu.");
189                 }
190             });
191         }
192
193         fetchDiff(params) {
194             /* this doesn't modify anything, so no locks */
195             var self = this;
196             params = $.extend({
197                 'from': self.revision,
198                 'to': self.revision
199             }, noops, params);
200             $.ajax({
201                 method: "GET",
202                 url: reverse("ajax_document_diff", self.id),
203                 dataType: 'html',
204                 data: {
205                     "from": params['from'],
206                     "to": params['to']
207                 },
208                 success: function(data) {
209                     params['success'](self, data);
210                 },
211                 error: function() {
212                     params['failure'](self, "Nie udało się wczytać porównania wersji.");
213                 }
214             });
215         }
216
217         checkRevision(params) {
218             /* this doesn't modify anything, so no locks */
219             var self = this;
220             let active = new Date() - self.active < 30 * 1000;
221             let saving = self.saving;
222             $.ajax({
223                 method: "GET",
224                 url: reverse("ajax_document_rev", self.id),
225                 data: {
226                     'a': active,
227                     'new': 1,
228                 },
229                 dataType: 'json',
230                 success: function(data) {
231                     if (data == '') {
232                         if (params.error)
233                             params.error();
234                     }
235                     else {
236                         let people = $('<div>');
237                         data.people.forEach((p) => {
238                             let item = $('<img>');
239                             item.attr('src', p.gravatar),
240                             item.attr(
241                                 'title',
242                                 p.name  + ' (' +
243                                     (p.active ? 'akt.' : 'nieakt.') +
244                                     ' od ' + p.since + ')')
245                             if (p.active) {
246                                 item.addClass('active');
247                             }
248                             people.append(item);
249                         });
250                         $("#people").html(people);
251
252                         if (!saving && (data.rev != self.revision)) {
253                             params.outdated();
254                         }
255                     }
256                 }
257             });
258         }
259
260         refreshImageGallery(params) {
261             if (this.galleryLink) {
262                 params = $.extend({}, params, {
263                     url: reverse("ajax_document_gallery", this.galleryLink)
264                 });
265             }
266             this.refreshGallery(params);
267         }
268
269         refreshScansGallery(params) {
270             if (this.scansLink) {
271                 params = $.extend({}, params, {
272                     url: reverse("ajax_document_scans", this.scansLink)
273                 });
274                 this.refreshGallery(params);
275             } else {
276                 // Fallback to image gallery.
277                 this.refreshImageGallery(params)
278             }
279         }
280         
281         /*
282          * Fetch gallery
283          */
284         refreshGallery(params) {
285             params = $.extend({}, noops, params);
286             var self = this;
287             if (!params.url) {
288                 params.failure('Brak galerii.');
289                 return;
290             }
291             $.ajax({
292                 method: "GET",
293                 url: params.url,
294                 dataType: 'json',
295                 success: function(data) {
296                     params.success(data);
297                 },
298                 error: function(xhr) {
299                     switch (xhr.status) {
300                     case 403:
301                         var msg = 'Galerie dostępne tylko dla zalogowanych użytkowników.';
302                         break;
303                     case 404:
304                         var msg = "Nie znaleziono galerii.";
305                     default:
306                         var msg = "Nie udało się wczytać galerii.";
307                     }
308                     params.failure(msg);
309                 }
310             });
311         }
312
313         setGallery(gallery) {
314             this.galleryLink = gallery;
315             Api.setGallery(this.id, gallery);
316         }
317
318         setGalleryStart(start) {
319             this.galleryStart = start;
320             Api.setGalleryStart(this.id, start);
321         }
322
323         openGalleryEdit(start) {
324             Api.openGalleryEdit(this.bookSlug);
325         }
326
327         withGalleryList(callback) {
328             Api.withGalleryList(callback);
329         }
330         
331         /*
332          * Set document's text
333          */
334         setText(text, silent=false) {
335             if (text == this.text) return;
336             if (!silent) {
337                 $.wiki.undo.push(text);
338             }
339             this.text = text;
340             this.has_local_changes = true;
341         }
342
343         undo() {
344             let ctx = $.wiki.exitContext();
345             this.setText(
346                 $.wiki.undo.undo(),
347                 true
348             );
349             $.wiki.enterContext(ctx);
350         }
351         redo() {
352             let ctx = $.wiki.exitContext();
353             this.setText(
354                 $.wiki.undo.redo(),
355                 true
356             );
357             $.wiki.enterContext(ctx);
358         }
359
360         /*
361          * Save text back to the server
362          */
363         save(params) {
364             params = $.extend({}, noops, params);
365             var self = this;
366
367             if (!self.has_local_changes) {
368                 console.log("Abort: no changes.");
369                 return params['success'](self, false, "Nie ma zmian do zapisania.");
370             };
371
372             // Serialize form to dictionary
373             var data = {};
374             $.each(params['form'].serializeArray(), function() {
375                 data[this.name] = this.value;
376             });
377
378             data['textsave-text'] = self.text;
379
380             self.saving = true;
381             $.ajax({
382                 url: reverse("ajax_document_text", self.id),
383                 type: "POST",
384                 dataType: "json",
385                 data: data,
386                 success: function(data) {
387                     var changed = false;
388
389                     $('#header').removeClass('saving');
390
391                     if (data.text) {
392                         self.text = data.text;
393                         self.revision = data.revision;
394                         self.gallery = data.gallery;
395                         changed = true;
396                         self.triggerDocumentChanged();
397                     };
398                     self.saving = false;
399
400                     params['success'](self, changed, ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna"));
401                 },
402                 error: function(xhr) {
403                     self.saving = false;
404                     if ($('#header').hasClass('saving')) {
405                         $('#header').removeClass('saving');
406                         $.blockUI({
407                             message: "<p>Nie udało się zapisać zmian. <br/><button onclick='$.unblockUI()'>OK</button></p>"
408                         })
409                     }
410                     else {
411                         try {
412                             params['failure'](self, $.parseJSON(xhr.responseText));
413                         }
414                         catch (e) {
415                             params['failure'](self, {
416                                 "__message": "<p>Nie udało się zapisać - błąd serwera.</p>"
417                             });
418                         };
419                     }
420                 }
421             });
422
423             $('#save-hide').click(function(){
424                 $('#header').addClass('saving');
425                 $.unblockUI();
426                 $.wiki.blocking.unblock();
427             });
428         } /* end of save() */
429
430         revertToVersion(params) {
431             var self = this;
432             params = $.extend({}, noops, params);
433
434             if (params.revision >= this.revision) {
435                 params.failure(self, 'Proszę wybrać rewizję starszą niż aktualna.');
436                 return;
437             }
438
439             // Serialize form to dictionary
440             var data = {};
441             $.each(params['form'].serializeArray(), function() {
442                 data[this.name] = this.value;
443             });
444
445             $.ajax({
446                 url: reverse("ajax_document_revert", self.id),
447                 type: "POST",
448                 dataType: "json",
449                 data: data,
450                 success: function(data) {
451                     if (data.text) {
452                         self.text = data.text;
453                         self.revision = data.revision;
454                         self.gallery = data.gallery;
455                         self.triggerDocumentChanged();
456
457                         params.success(self, "Udało się przywrócić wersję :)");
458                     }
459                     else {
460                         params.failure(self, "Przywracana wersja identyczna z aktualną. Anulowano przywracanie.");
461                     }
462                 },
463                 error: function(xhr) {
464                     params.failure(self, "Nie udało się przywrócić wersji - błąd serwera.");
465                 }
466             });
467         }
468
469         pubmark(params) {
470             params = $.extend({}, noops, params);
471             var self = this;
472             var data = {
473                 "pubmark-id": self.id,
474             };
475
476             /* unpack form */
477             $.each(params.form.serializeArray(), function() {
478                 data[this.name] = this.value;
479             });
480
481             $.ajax({
482                 url: reverse("ajax_document_pubmark", self.id),
483                 type: "POST",
484                 dataType: "json",
485                 data: data,
486                 success: function(data) {
487                     params.success(self, data.message);
488                 },
489                 error: function(xhr) {
490                     if (xhr.status == 403 || xhr.status == 401) {
491                         params.failure(self, {
492                             "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."]
493                         });
494                     }
495                     else {
496                         try {
497                             params.failure(self, $.parseJSON(xhr.responseText));
498                         }
499                         catch (e) {
500                             params.failure(self, {
501                                 "__all__": ["Nie udało się - błąd serwera."]
502                             });
503                         };
504                     };
505                 }
506             });
507         }
508
509         refreshCover(params) {
510             var self = this;
511             var data = {
512                 xml: self.text // TODO: send just DC
513             };
514             $.ajax({
515                 url: reverse("ajax_cover_preview"),
516                 type: "POST",
517                 data: data,
518                 success: function(data) {
519                     params.success(data);
520                 },
521                 error: function(xhr) {
522                     // params.failure("Nie udało się odświeżyć okładki - błąd serwera.");
523                 }
524             });
525         }
526
527         getLength(params) {
528             params = $.extend({}, noops, params);
529             var xml = this.text.replace(/\/(\s+)/g, '<br />$1');
530             var parser = new DOMParser();
531             var doc = parser.parseFromString(xml, 'text/xml');
532             var error = $('parsererror', doc);
533
534             if (error.length > 0) {
535                 throw "Not an XML document.";
536             }
537             $.xmlns["rdf"] = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
538             $('rdf|RDF', doc).remove();
539             if (params.noFootnotes) {
540                 $('pa, pe, pr, pt', doc).remove();
541             }
542             if (params.noThemes) {
543                 $('motyw', doc).remove();
544             }
545             var text = $(doc).text();
546             text = $.trim(text.replace(/\s{2,}/g, ' '));
547             return text.length;
548         }
549
550         /* Temporary workaround for relative images. */
551         getBase() {
552             return '/media/dynamic/images/' + this.galleryLink + '/';
553         }
554     }
555
556     $.wikiapi.WikiDocument = WikiDocument;
557 })(jQuery);