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