b3cc09bc186ff44a172e8d23baf32631010a50d0
[redakcja.git] / apps / explorer / views.py
1 # -*- coding: utf-8 -*-
2 import urllib2
3 import hg
4 from datetime import date
5
6 from librarian import html, parser, dcparser, wrap_text
7 from librarian import ParseError, ValidationError
8
9 from django.conf import settings
10 from django.contrib.auth.decorators import login_required, permission_required
11
12 from django.core.urlresolvers import reverse
13 from django.http import HttpResponseRedirect, HttpResponse
14 from django.utils import simplejson as json
15 from django.views.generic.simple import direct_to_template
16
17 from explorer import forms, models
18 from toolbar import models as toolbar_models
19
20 #
21 # Some useful decorators
22
23 def file_branch(path, user=None):
24     return ('personal_'+user.username + '_' if user is not None else '') \
25         + 'file_' + path
26
27 def with_repo(view):
28     """Open a repository for this view"""
29     def view_with_repo(request, *args, **kwargs):          
30         kwargs['repo'] = hg.Repository(settings.REPOSITORY_PATH)
31         return view(request, *args, **kwargs)
32     return view_with_repo
33
34 #
35 def ajax_login_required(view):
36     """Similar ro @login_required, but instead of redirect, 
37     just return some JSON stuff with error."""
38     def view_with_auth(request, *args, **kwargs):
39         if request.user.is_authenticated():
40             return view(request, *args, **kwargs)
41         # not authenticated
42         return HttpResponse( json.dumps({'result': 'access_denied', 'errors': ['Brak dostępu.']}) );
43     return view_with_auth
44
45 #
46 # View all files
47 #
48 @with_repo
49 def file_list(request, repo):
50     #
51     latest_default = repo.get_branch_tip('default')
52     files = [ f for f in repo.repo[latest_default] if not f.startswith('.')]
53     bookform = forms.BookUploadForm()
54
55     return direct_to_template(request, 'explorer/file_list.html', extra_context={
56         'files': files, 'bookform': bookform,
57     })
58
59 @permission_required('explorer.can_add_files')
60 @with_repo
61 def file_upload(request, repo):
62     other_errors = []
63     if request.method == 'POST':
64         form = forms.BookUploadForm(request.POST, request.FILES)
65         if form.is_valid():
66             try:
67                 # prepare the data
68                 f = request.FILES['file']
69                 decoded = f.read().decode('utf-8')
70                 path = form.cleaned_data['bookname']
71
72                 if form.cleaned_data['autoxml']:
73                     decoded = wrap_text(decoded, unicode(date.today()) )
74                 
75                 def upload_action():
76                     repo._add_file(path, decoded.encode('utf-8') )
77                     repo._commit(message="File %s uploaded by user %s" % \
78                         (path, request.user.username), user=request.user.username)
79
80                 repo.in_branch(upload_action, 'default')
81
82                 # if everything is ok, redirect to the editor
83                 return HttpResponseRedirect( reverse('editor_view',
84                         kwargs={'path': path}) )
85
86             except hg.RepositoryException, e:
87                 other_errors.append(u'Błąd repozytorium: ' + unicode(e) )
88             #except UnicodeDecodeError, e:
89             #    other_errors.append(u'Niepoprawne kodowanie pliku: ' + e.reason \
90             #     + u'. Żądane kodowanie: ' + e.encoding)
91         # invalid form
92
93     # get
94     form = forms.BookUploadForm()
95     return direct_to_template(request, 'explorer/file_upload.html',\
96         extra_context = {'form' : form, 'other_errors': other_errors})
97    
98 #
99 # Edit the file
100 #
101
102 @ajax_login_required
103 @with_repo
104 def file_xml(request, repo, path):
105     if request.method == 'POST':
106         errors = None
107         warnings = None
108         form = forms.BookForm(request.POST)
109         if form.is_valid():
110             print 'Saving whole text.', request.user.username
111             try:
112                 # encode it back to UTF-8, so we can put it into repo
113                 encoded_data = form.cleaned_data['content'].encode('utf-8')
114
115                 def save_action():                    
116                     repo._add_file(path, encoded_data)
117                     repo._commit(message=(form.cleaned_data['commit_message'] or 'Lokalny zapis platformy.'),\
118                          user=request.user.username)
119
120                 try:
121                     # wczytaj dokument z ciągu znaków -> weryfikacja
122                     document = parser.WLDocument.from_string(form.cleaned_data['content'])
123                 except (ParseError, ValidationError), e:
124                     warnings = [u'Niepoprawny dokument XML: ' + unicode(e.message)]
125
126                 #  save to user's branch
127                 repo.in_branch(save_action, file_branch(path, request.user) );
128             except UnicodeDecodeError, e:
129                 errors = [u'Błąd kodowania danych przed zapisem: ' + unicode(e.message)]
130             except hg.RepositoryException, e:
131                 errors = [u'Błąd repozytorium: ' + unicode(e.message)]            
132
133         if not errors:
134             errors = dict( (field[0], field[1].as_text()) for field in form.errors.iteritems() )
135
136         return HttpResponse( json.dumps({'result': errors and 'error' or 'ok',
137             'errors': errors, 'warnings': warnings}) );
138
139     form = forms.BookForm()
140     data = repo.get_file(path, file_branch(path, request.user))
141     form.fields['content'].initial = data
142     return HttpResponse( json.dumps({'result': 'ok', 'content': data}) )
143
144 @ajax_login_required
145 @with_repo
146 def file_update_local(request, path, repo):
147     result = None
148     errors = None
149     
150     wlock = repo.write_lock()
151     try:
152         tipA = repo.get_branch_tip('default')
153         tipB = repo.get_branch_tip( file_branch(path, request.user) )
154
155         nodeA = repo.getnode(tipA)
156         nodeB = repo.getnode(tipB)
157         
158         # do some wild checks - see file_commit() for more info
159         if (repo.common_ancestor(tipA, tipB) == nodeA) \
160         or (nodeB in nodeA.parents()):
161             result = 'nothing-to-do'
162         else:
163             # Case 2+
164             repo.merge_revisions(tipB, tipA, \
165                 request.user.username, 'Personal branch update.')
166             result = 'done'
167     except hg.UncleanMerge, e:
168         errors = [e.message]
169         result = 'fatal-error'
170     except hg.RepositoryException, e:
171         errors = [e.message]
172         result = 'fatal-error'
173     finally:
174         wlock.release()
175
176     if result is None:
177         raise Exception("Ouch, this shouldn't happen!")
178     
179     return HttpResponse( json.dumps({'result': result, 'errors': errors}) );
180
181 @ajax_login_required
182 @with_repo
183 def file_commit(request, path, repo):
184     result = None
185     errors = None
186     local_modified = False
187     if request.method == 'POST':
188         form = forms.MergeForm(request.POST)
189
190         if form.is_valid():           
191             wlock = repo.write_lock()
192             try:
193                 tipA = repo.get_branch_tip('default')
194                 tipB = repo.get_branch_tip( file_branch(path, request.user) )
195
196                 nodeA = repo.getnode(tipA)
197                 nodeB = repo.getnode(tipB)
198
199                 print repr(nodeA), repr(nodeB), repo.common_ancestor(tipA, tipB), repo.common_ancestor(tipB, tipA)
200
201                 if repo.common_ancestor(tipB, tipA) == nodeA:
202                     # Case 1:
203                     #         * tipB
204                     #         |
205                     #         * <- can also be here!
206                     #        /|
207                     #       / |
208                     # tipA *  *
209                     #      |  |
210                     # The local branch has been recently updated,
211                     # so we don't need to update yet again, but we need to
212                     # merge down to default branch, even if there was
213                     # no commit's since last update
214                     repo.merge_revisions(tipA, tipB, \
215                         request.user.username, form.cleaned_data['message'])
216                     result = 'done'
217                 elif any( p.branch()==nodeB.branch() for p in nodeA.parents()):
218                     # Case 2:
219                     #
220                     # tipA *  * tipB
221                     #      |\ |
222                     #      | \|
223                     #      |  * 
224                     #      |  |
225                     # Default has no changes, to update from this branch
226                     # since the last merge of local to default.
227                     if nodeB not in nodeA.parents():
228                         repo.merge_revisions(tipA, tipB, \
229                             request.user.username, form.cleaned_data['message'])
230                         result = 'done'
231                     else:
232                         result = 'nothing-to-do'
233                 elif repo.common_ancestor(tipA, tipB) == nodeB:
234                     # Case 3:
235                     # tipA * 
236                     #      |
237                     #      * <- this case overlaps with previos one
238                     #      |\
239                     #      | \
240                     #      |  * tipB
241                     #      |  |
242                     #
243                     # There was a recent merge to the defaul branch and
244                     # no changes to local branch recently.
245                     # 
246                     # Use the fact, that user is prepared to see changes, to
247                     # update his branch if there are any
248                     if nodeB not in nodeA.parents():
249                         repo.merge_revisions(tipB, tipA, \
250                             request.user.username, 'Personal branch update during merge.')
251                         local_modified = True
252                         result = 'done'
253                     else:
254                         result = 'nothing-to-do'
255                 else:
256                     # both branches have changes made to them, so
257                     # first do an update
258                     repo.merge_revisions(tipB, tipA, \
259                         request.user.username, 'Personal branch update during merge.')
260
261                     local_modified = True
262
263                     # fetch the new tip
264                     tipB = repo.get_branch_tip( file_branch(path, request.user) )
265
266                     # and merge back to the default
267                     repo.merge_revisions(tipA, tipB, \
268                         request.user.username, form.cleaned_data['message'])
269                     result = 'done'
270             except hg.UncleanMerge, e:
271                 errors = [e.message]
272                 result = 'fatal-error'
273             except hg.RepositoryException, e:
274                 errors = [e.message]
275                 result = 'fatal-error'
276             finally:
277                 wlock.release()
278                 
279         if result is None:
280             errors = [ form.errors['message'].as_text() ]
281             if len(errors) > 0:
282                 result = 'fatal-error'
283
284         return HttpResponse( json.dumps({'result': result, 'errors': errors, 'localmodified': local_modified}) );
285
286     return HttpResponse( json.dumps({'result': 'fatal-error', 'errors': ['No data posted']}) )
287     
288
289 @ajax_login_required
290 @with_repo
291 def file_dc(request, path, repo):
292     errors = None
293
294     if request.method == 'POST':
295         form = forms.DublinCoreForm(request.POST)
296         
297         if form.is_valid():
298             
299             def save_action():
300                 file_contents = repo._get_file(path)
301
302                 # wczytaj dokument z repozytorium
303                 document = parser.WLDocument.from_string(file_contents)                    
304                 document.book_info.update(form.cleaned_data)             
305
306                 # zapisz
307                 repo._write_file(path, document.serialize().encode('utf-8'))
308                 repo._commit( \
309                     message=(form.cleaned_data['commit_message'] or 'Lokalny zapis platformy.'), \
310                     user=request.user.username )
311                 
312             try:
313                 repo.in_branch(save_action, file_branch(path, request.user) )
314             except UnicodeEncodeError, e:
315                 errors = ['Bład wewnętrzny: nie można zakodować pliku do utf-8']
316             except (ParseError, ValidationError), e:
317                 errors = [e.message]
318
319         if errors is None:
320             errors = ["Pole '%s': %s\n" % (field[0], field[1].as_text()) for field in form.errors.iteritems()]
321
322         return HttpResponse( json.dumps({'result': errors and 'error' or 'ok', 'errors': errors}) );
323     
324     # this is unused currently, but may come in handy 
325     content = []
326     
327     try:
328         fulltext = repo.get_file(path, file_branch(path, request.user))
329         bookinfo = dcparser.BookInfo.from_string(fulltext)
330         content = bookinfo.to_dict()
331     except (ParseError, ValidationError), e:
332         errors = [e.message]
333
334     return HttpResponse( json.dumps({'result': errors and 'error' or 'ok', 
335         'errors': errors, 'content': content }) ) 
336
337 # Display the main editor view
338
339 @login_required
340 @with_repo
341 def display_editor(request, path, repo):    
342
343     # this is the only entry point where we create an autobranch for the user
344     # if it doesn't exists. All other views SHOULD fail.
345     def ensure_branch_exists():
346         parent = repo.get_branch_tip('default')
347         repo._create_branch(file_branch(path, request.user), parent)
348         
349     try:
350         repo.with_wlock(ensure_branch_exists)
351         
352         return direct_to_template(request, 'explorer/editor.html', extra_context={
353             'hash': path,
354             'panel_list': ['lewy', 'prawy'],
355             'scriptlets': toolbar_models.Scriptlet.objects.all()
356         })
357     except KeyError:
358         return direct_to_template(request, 'explorer/nofile.html', \
359             extra_context = { 'path': path })
360
361 # ===============
362 # = Panel views =
363 # ===============
364
365 @ajax_login_required
366 @with_repo
367 def xmleditor_panel(request, path, repo):
368     text = repo.get_file(path, file_branch(path, request.user))
369     
370     return direct_to_template(request, 'explorer/panels/xmleditor.html', extra_context={
371         'fpath': path,
372         'text': text,
373     })
374     
375
376 @ajax_login_required
377 def gallery_panel(request, path):
378     return direct_to_template(request, 'explorer/panels/gallery.html', extra_context={
379         'fpath': path,
380         'form': forms.ImageFoldersForm(),
381     })
382
383 @ajax_login_required
384 @with_repo
385 def htmleditor_panel(request, path, repo):
386     user_branch = file_branch(path, request.user)
387     try:
388         return direct_to_template(request, 'explorer/panels/htmleditor.html', extra_context={
389             'fpath': path,
390             'html': html.transform(repo.get_file(path, user_branch), is_file=False),
391         })
392     except (ParseError, ValidationError), e:
393         return direct_to_template(request, 'explorer/panels/parse_error.html', extra_context={
394             'fpath': path, 'exception_type': type(e).__name__, 'exception': e, 'panel_name': 'Edytor HTML'}) 
395
396 @ajax_login_required
397 @with_repo
398 def dceditor_panel(request, path, repo):
399     user_branch = file_branch(path, request.user)
400
401     try:
402         doc_text = repo.get_file(path, user_branch)
403         document = parser.WLDocument.from_string(doc_text)
404         form = forms.DublinCoreForm(info=document.book_info)       
405         return direct_to_template(request, 'explorer/panels/dceditor.html', extra_context={
406             'fpath': path,
407             'form': form,
408         })
409     except (ParseError, ValidationError), e:
410         return direct_to_template(request, 'explorer/panels/parse_error.html', extra_context={
411             'fpath': path, 'exception_type': type(e).__name__, 'exception': e, 
412             'panel_name': 'Edytor DublinCore'}) 
413
414 # =================
415 # = Utility views =
416 # =================
417 @ajax_login_required
418 def folder_images(request, folder):
419     return direct_to_template(request, 'explorer/folder_images.html', extra_context={
420         'images': models.get_images_from_folder(folder),
421     })
422
423
424 def _add_references(message, issues):
425     return message + " - " + ", ".join(map(lambda issue: "Refs #%d" % issue['id'], issues))
426
427 def _get_issues_for_file(path):
428     if not path.endswith('.xml'):
429         raise ValueError('Path must end with .xml')
430
431     book_id = path[:-4]
432     uf = None
433
434     try:
435         uf = urllib2.urlopen(settings.REDMINE_URL + 'publications/issues/%s.json' % book_id)
436         return json.loads(uf.read())
437     except urllib2.HTTPError:
438         return []
439     finally:
440         if uf: uf.close()
441
442
443 # =================
444 # = Pull requests =
445 # =================
446 def pull_requests(request):
447     return direct_to_template(request, 'manager/pull_request.html', extra_context = {
448         'objects': models.PullRequest.objects.all()} )