From 653c75b6951e028fe4c68e27cc9852e45fa418a6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Rekucki?= Date: Tue, 8 Sep 2009 12:34:39 +0200 Subject: [PATCH] Merge i commit dzialaja. Mozliwosc uaktualnienia swojej galezi dla danego pliku. Poprawki zeby lepiej dzialalo na Ie --- apps/explorer/forms.py | 7 +- apps/explorer/models.py | 2 - apps/explorer/views.py | 249 +++++++++++++++++++------ fixtures/przyciski.xml | 156 ++++++++++------ lib/hg.py | 168 +++++++++++++---- project/static/css/filelist.css | 2 +- project/static/css/master.css | 32 ---- project/static/js/codemirror/select.js | 4 +- project/static/js/editor.js | 115 +++++++++--- project/static/js/jquery.logging.js | 46 ++--- project/templates/explorer/editor.html | 26 ++- project/urls.py | 3 + 12 files changed, 573 insertions(+), 237 deletions(-) diff --git a/apps/explorer/forms.py b/apps/explorer/forms.py index 0e1ec868..da8a1179 100644 --- a/apps/explorer/forms.py +++ b/apps/explorer/forms.py @@ -55,8 +55,13 @@ class BookForm(forms.Form): content = forms.CharField(widget=forms.Textarea) commit_message = forms.CharField(required=False) +class MergeForm(forms.Form): + message = forms.CharField(error_messages={'required': 'Please write a merge description.'}) + class BookUploadForm(forms.Form): - file = forms.FileField() + file = forms.FileField(label='Source OCR file') + bookname = forms.RegexField(regex='[\w-]+', \ + label='Publication name', help_text='Example: slowacki-beniowski') class ImageFoldersForm(forms.Form): folders = forms.ChoiceField(required=False) diff --git a/apps/explorer/models.py b/apps/explorer/models.py index e47c26fe..5fc31e7e 100644 --- a/apps/explorer/models.py +++ b/apps/explorer/models.py @@ -52,5 +52,3 @@ def get_images_from_folder(folder): in os.listdir(os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, folder)) if not fn.startswith('.')) -def user_branch(user): - return 'personal_'+user.username diff --git a/apps/explorer/views.py b/apps/explorer/views.py index 82c9d398..19cc10b1 100644 --- a/apps/explorer/views.py +++ b/apps/explorer/views.py @@ -16,7 +16,11 @@ from toolbar import models as toolbar_models # # Some useful decorators -# + +def file_branch(path, user=None): + return ('personal_'+user.username + '_' if user is not None else '') \ + + 'file_' + path + def with_repo(view): """Open a repository for this view""" def view_with_repo(request, *args, **kwargs): @@ -32,7 +36,7 @@ def ajax_login_required(view): if request.user.is_authenticated(): return view(request, *args, **kwargs) # not authenticated - return HttpResponse( json.dumps({'result': 'access_denied'}) ); + return HttpResponse( json.dumps({'result': 'access_denied', 'errors': ['Brak dostępu.']}) ); return view_with_auth # @@ -40,7 +44,8 @@ def ajax_login_required(view): # @with_repo def file_list(request, repo): - latest_default = repo.repo.branchtags()['default'] + # + latest_default = repo.get_branch_tip('default') files = list( repo.repo[latest_default] ) bookform = forms.BookUploadForm() @@ -59,34 +64,29 @@ def file_upload(request, repo): # prepare the data f = request.FILES['file'] decoded = f.read().decode('utf-8') - + path = form.cleaned_data['bookname'] + def upload_action(): - print 'Adding file: %s' % f.name - repo._add_file(f.name, decoded.encode('utf-8') ) - repo._commit( - message="File %s uploaded from platform by %s" %\ - (f.name, request.user.username), \ - user=request.user.username \ - ) - - # end of upload + repo._add_file(path ,decoded.encode('utf-8') ) + repo._commit(message="File %s uploaded by user %s" % \ + (path, request.user.username), user=request.user.username) repo.in_branch(upload_action, 'default') # if everything is ok, redirect to the editor return HttpResponseRedirect( reverse('editor_view', - kwargs={'path': f.name}) ) + kwargs={'path': path}) ) except hg.RepositoryException, e: other_errors.append(u'Błąd repozytorium: ' + unicode(e) ) - except UnicodeDecodeError, e: - other_errors.append(u'Niepoprawne kodowanie pliku: ' + e.reason \ - + u'. Żądane kodowanie: ' + e.encoding) + #except UnicodeDecodeError, e: + # other_errors.append(u'Niepoprawne kodowanie pliku: ' + e.reason \ + # + u'. Żądane kodowanie: ' + e.encoding) # invalid form # get form = forms.BookUploadForm() - return direct_to_template(request, 'explorer/file_upload.html', + return direct_to_template(request, 'explorer/file_upload.html',\ extra_context = {'form' : form, 'other_errors': other_errors}) # @@ -118,10 +118,10 @@ def file_xml(request, repo, path): warnings = [u'Niepoprawny dokument XML: ' + unicode(e.message)] # save to user's branch - repo.in_branch(save_action, models.user_branch(request.user) ); + repo.in_branch(save_action, file_branch(path, request.user) ); except UnicodeDecodeError, e: errors = [u'Błąd kodowania danych przed zapisem: ' + unicode(e.message)] - except RepositoryException, e: + except hg.RepositoryException, e: errors = [u'Błąd repozytorium: ' + unicode(e.message)] if not errors: @@ -131,9 +131,154 @@ def file_xml(request, repo, path): 'errors': errors, 'warnings': warnings}) ); form = forms.BookForm() - data = repo.get_file(path, models.user_branch(request.user)) + data = repo.get_file(path, file_branch(path, request.user)) form.fields['content'].initial = data - return HttpResponse( json.dumps({'result': 'ok', 'content': data}) ) + return HttpResponse( json.dumps({'result': 'ok', 'content': data}) ) + +@ajax_login_required +@with_repo +def file_update_local(request, path, repo): + result = None + errors = None + + wlock = repo.write_lock() + try: + tipA = repo.get_branch_tip('default') + tipB = repo.get_branch_tip( file_branch(path, request.user) ) + + nodeA = repo.getnode(tipA) + nodeB = repo.getnode(tipB) + + # do some wild checks - see file_commit() for more info + if (repo.common_ancestor(tipA, tipB) == nodeA) \ + or (nodeB in nodeA.parents()): + result = 'nothing-to-do' + else: + # Case 2+ + repo.merge_revisions(tipB, tipA, \ + request.user.username, 'Personal branch update.') + result = 'done' + except hg.UncleanMerge, e: + errors = [e.message] + result = 'fatal-error' + except hg.RepositoryException, e: + errors = [e.message] + result = 'fatal-error' + finally: + wlock.release() + + if result is None: + raise Exception("Ouch, this shouldn't happen!") + + return HttpResponse( json.dumps({'result': result, 'errors': errors}) ); + +@ajax_login_required +@with_repo +def file_commit(request, path, repo): + result = None + errors = None + local_modified = False + if request.method == 'POST': + form = forms.MergeForm(request.POST) + + if form.is_valid(): + wlock = repo.write_lock() + try: + tipA = repo.get_branch_tip('default') + tipB = repo.get_branch_tip( file_branch(path, request.user) ) + + nodeA = repo.getnode(tipA) + nodeB = repo.getnode(tipB) + + print repr(nodeA), repr(nodeB), repo.common_ancestor(tipA, tipB), repo.common_ancestor(tipB, tipA) + + if repo.common_ancestor(tipB, tipA) == nodeA: + # Case 1: + # * tipB + # | + # * <- can also be here! + # /| + # / | + # tipA * * + # | | + # The local branch has been recently updated, + # so we don't need to update yet again, but we need to + # merge down to default branch, even if there was + # no commit's since last update + repo.merge_revisions(tipA, tipB, \ + request.user.username, form.cleaned_data['message']) + result = 'done' + elif any( p.branch()==nodeB.branch() for p in nodeA.parents()): + # Case 2: + # + # tipA * * tipB + # |\ | + # | \| + # | * + # | | + # Default has no changes, to update from this branch + # since the last merge of local to default. + if nodeB not in nodeA.parents(): + repo.merge_revisions(tipA, tipB, \ + request.user.username, form.cleaned_data['message']) + result = 'done' + else: + result = 'nothing-to-do' + elif repo.common_ancestor(tipA, tipB) == nodeB: + # Case 3: + # tipA * + # | + # * <- this case overlaps with previos one + # |\ + # | \ + # | * tipB + # | | + # + # There was a recent merge to the defaul branch and + # no changes to local branch recently. + # + # Use the fact, that user is prepared to see changes, to + # update his branch if there are any + if nodeB not in nodeA.parents(): + repo.merge_revisions(tipB, tipA, \ + request.user.username, 'Personal branch update during merge.') + local_modified = True + result = 'done' + else: + result = 'nothing-to-do' + else: + # both branches have changes made to them, so + # first do an update + repo.merge_revisions(tipB, tipA, \ + request.user.username, 'Personal branch update during merge.') + + local_modified = True + + # fetch the new tip + tipB = repo.get_branch_tip( file_branch(path, request.user) ) + + # and merge back to the default + repo.merge_revisions(tipA, tipB, \ + request.user.username, form.cleaned_data['message']) + result = 'done' + except hg.UncleanMerge, e: + errors = [e.message] + result = 'fatal-error' + except hg.RepositoryException, e: + errors = [e.message] + result = 'fatal-error' + finally: + wlock.release() + + if result is None: + errors = [ form.errors['message'].as_text() ] + if len(errors) > 0: + result = 'fatal-error' + + return HttpResponse( json.dumps({'result': result, 'errors': errors, 'localmodified': local_modified}) ); + + return HttpResponse( json.dumps({'result': 'fatal-error', 'errors': ['No data posted']}) ) + @ajax_login_required @with_repo @@ -144,23 +289,24 @@ def file_dc(request, path, repo): form = forms.DublinCoreForm(request.POST) if form.is_valid(): + def save_action(): file_contents = repo._get_file(path) # wczytaj dokument z repozytorium document = parser.WLDocument.from_string(file_contents) - document.book_info.update(form.cleaned_data) - - print "SAVING DC" + document.book_info.update(form.cleaned_data) # zapisz - repo._write_file(path, document.serialize()) + repo._write_file(path, document.serialize().encode('utf-8')) repo._commit( \ message=(form.cleaned_data['commit_message'] or 'Lokalny zapis platformy.'), \ user=request.user.username ) try: - repo.in_branch(save_action, models.user_branch(request.user) ) + repo.in_branch(save_action, file_branch(path, request.user) ) + except UnicodeEncodeError, e: + errors = ['Bład wewnętrzny: nie można zakodować pliku do utf-8'] except (ParseError, ValidationError), e: errors = [e.message] @@ -173,7 +319,7 @@ def file_dc(request, path, repo): content = [] try: - fulltext = repo.get_file(path, models.user_branch(request.user)) + fulltext = repo.get_file(path, file_branch(path, request.user)) bookinfo = dcparser.BookInfo.from_string(fulltext) content = bookinfo.to_dict() except (ParseError, ValidationError), e: @@ -186,28 +332,25 @@ def file_dc(request, path, repo): @login_required @with_repo -def display_editor(request, path, repo): - - if not repo.file_exists(path, models.user_branch(request.user)): - try: - data = repo.get_file(path, 'default') - print type(data) - - def new_file(): - repo._add_file(path, data) - repo._commit(message='File import from default branch', - user=request.user.username) - - repo.in_branch(new_file, models.user_branch(request.user) ) - except hg.RepositoryException, e: - return direct_to_template(request, 'explorer/file_unavailble.html',\ - extra_context = { 'path': path, 'error': e }) - - return direct_to_template(request, 'explorer/editor.html', extra_context={ - 'hash': path, - 'panel_list': ['lewy', 'prawy'], - 'scriptlets': toolbar_models.Scriptlet.objects.all() - }) +def display_editor(request, path, repo): + + # this is the only entry point where we create an autobranch for the user + # if it doesn't exists. All other views SHOULD fail. + def ensure_branch_exists(): + parent = repo.get_branch_tip('default') + repo._create_branch(file_branch(path, request.user), parent) + + try: + repo.with_wlock(ensure_branch_exists) + + return direct_to_template(request, 'explorer/editor.html', extra_context={ + 'hash': path, + 'panel_list': ['lewy', 'prawy'], + 'scriptlets': toolbar_models.Scriptlet.objects.all() + }) + except KeyError: + return direct_to_template(request, 'explorer/nofile.html', \ + extra_context = { 'path': path }) # =============== # = Panel views = @@ -216,7 +359,7 @@ def display_editor(request, path, repo): @ajax_login_required @with_repo def xmleditor_panel(request, path, repo): - text = repo.get_file(path, models.user_branch(request.user)) + text = repo.get_file(path, file_branch(path, request.user)) return direct_to_template(request, 'explorer/panels/xmleditor.html', extra_context={ 'fpath': path, @@ -234,7 +377,7 @@ def gallery_panel(request, path): @ajax_login_required @with_repo def htmleditor_panel(request, path, repo): - user_branch = models.user_branch(request.user) + user_branch = file_branch(path, request.user) try: return direct_to_template(request, 'explorer/panels/htmleditor.html', extra_context={ 'fpath': path, @@ -247,7 +390,7 @@ def htmleditor_panel(request, path, repo): @ajax_login_required @with_repo def dceditor_panel(request, path, repo): - user_branch = models.user_branch(request.user) + user_branch = file_branch(path, request.user) try: doc_text = repo.get_file(path, user_branch) diff --git a/fixtures/przyciski.xml b/fixtures/przyciski.xml index 287a0cc9..d67f3ed0 100644 --- a/fixtures/przyciski.xml +++ b/fixtures/przyciski.xml @@ -1,62 +1,98 @@ -Autokorektaautokorekta0Formatowanieformatowanie0Widokdisplay_options2A<sup>+</sup>increase_font_size({change: 2})codemirror_fontsize+Zwiększ rozmiar czcionki.A<sup>-</sup>descrease_font_size({change: -2})codemirror_fontsize-Zmniejsz rozmiar czcionki.A<sup>=</sup>reset_font_size({fontSize: 13})codemirror_fontsize=Przywróć orginalny rozmiar czcionki.Novelpagesnovelpages({exprs: [ - ["\\,\\.\\.|\\.\\,\\.|\\.\\.\\,", "..."], - ["„", ",,"] /* DOUBLE LOW-9 QUOTATION MARK */ -]}) - lineregexpWykonuję operację z novel-pages.Usuń spacjęstrip_whitespace({exprs: [ ["^\\s+|\\s+$", ""], ["\\s+", " "] ]})lineregexpUsuwa zbędne spację z dokumentu.Wersinsert_verse({tag: 'wers'})insert_tagwOtacza zaznaczony tekst tagiem 'wers'.Zamień dywizzamien_dywiz({exprs:[ ["—","---"] ]})lineregexpZamienia '—' na '---'.$.log(editor, panel, params); - -var texteditor = panel.texteditor; -var text = texteditor.selection(); -texteditor.replaceSelection('<' + params.tag + '>' + text + '</' + params.tag + '>'); -if (text.length == 0) -{ - var pos = texteditor.cursorPosition(); - texteditor.selectLines(pos.line, pos.character + params.tag.length + 2); -} - -panel.fireEvent('contentChanged');// params: {exprs: list of {expr: "", repl: "" [, opts: "g"]}} -var cm = panel.texteditor; - -var exprs = $.map(params.exprs, function(expr) { - var opts = "g"; - if(expr.length > 2) - opts = expr[2]; - return {rx: new RegExp(expr[0], opts), repl: expr[1]}; -}); - -var selection = cm.selection(); - -if(selection) -{ - var lines = selection.split('\n'); - lines = $.map(lines, function(line) { - $(exprs).each(function() { - var expr = this; - line = line.replace(expr.rx, expr.repl); - }); - return line; - }); - cm.replaceSelection( lines.join('\n') ); -} -else { - var line = cm.firstLine(); - do { - var content = cm.lineContent(line); - $.log("Swapping line: $" + content + "$"); - - $(exprs).each(function() { var expr = this; - content = content.replace(expr.rx, expr.repl); - }); - cm.setLineContent(line, content); - line = cm.nextLine(line); - } while( !(line === false) ); -}var texteditor = panel.texteditor; -var frameBody = $('body', $(texteditor.frame).contents()); - -if(params.fontSize) { - frameBody.css('font-size', params.fontSize); -} -else { - var old_size = parseInt(frameBody.css('font-size')); - frameBody.css('font-size', old_size + (params.change || 0) ); +Akapity i długie cytatyakapity-i-dlugie-cytaty0Autokorektaautokorekta0Blokibloki0Bloki początkowebloki-poczatkowe0Deklaracjedeklaracje0Dramat wierszowanydramat-wierszowany0Dramat współczesnydramat-wspolczesny0Elementy początkoweelementy-poczatkowe0Masterymastery0Nagłówkinaglowki0Początek dramatupoczatek-dramatu0Poleceniapolecenia0Strukturalnestrukturalne0Style znakowestyle-znakowe0Wersywersy0Widokdisplay_options2A<sup>+</sup>increase_font_size({change: 2})codemirror_fontsize+Zwiększ rozmiar czcionki.A<sup>-</sup>descrease_font_size({change: -2})codemirror_fontsize-Zmniejsz rozmiar czcionki.A<sup>=</sup>reset_font_size({fontSize: 13})codemirror_fontsize=Przywróć orginalny rozmiar czcionki.akapitakapit({tag:"akap"})insert_tagakapit cd.akapit-cd({tag:"akap_cd"})insert_tagakapit dialogowyakapit-dialogowy({tag:"akap_dialog"})insert_tagaktakt({tag:"akt"})insert_tagautorautor({tag:"autor"})insert_tagczęść/księgaczesc({tag:"naglowek_czesc"})insert_tagdedykacjadedykacja({tag:"dedykacja"})insert_tagdedykacjadedykacja({tag:"dedykacja"})insert_tagdidaskaliadidaskalia({tag:"didaskalia"})insert_tagdidaskaliadidaskalia({tag:"didaskalia"})insert_tagdidaskalia wewn.didaskalia-wewn({tag:"didask_tekst"})insert_tagdidaskalia wewn.didaskalia-wewn({tag:"didask_tekst"})insert_tagdramat wiersz.dramat-wiersz({tag:"dramat_wierszowany_l"})insert_tagdramat wiersz./w. łamdramat-wiersz-w-lam({tag:"dramat_wierszowany_lp"})insert_tagdramat współczesnydramat-wspolczesny({tag:"dramat_wspolczesny"})insert_tagdzieło nadrzędnedzielo-nadrzedne({tag:"dzielo_nadrzedne"})insert_tagdługi cyt. poet.dlugi-cyt-poet({tag:"poezja_cyt"})insert_tagdługi cyta. poet.dlugi-cyt-poet({tag:"poezja_cyt"})insert_tagdługi cytatdlugi-cytat({tag:"dlugi_cyt"})insert_tagdługi cytatdlugi-cytatdlugi_cytatinsert_tagekstraekstra({tag:"ekstra"})insert_tagkwestiakwestia({tag:"kwestia"})insert_tagkwestiakwestia({tag:"kwestia"})insert_taglirykaliryka({tag:"liryka_l"})insert_tagliryka/w. łamliryka-w-lam({tag:"liryka_lp"})insert_tagmamtemat.matemat({tag:"mat"})insert_tagmottomotto({tag:"motto"})insert_tagmottomotto({tag:"motto"})insert_tagmotto podpismotto-podpis({tag:"motto_podpis"})insert_tagnagłówek kwestiinaglowek-kwestii({tag:"naglowek_osoba"})insert_tagnotanota({tag:"nota"})insert_tagNovelpagesnovelpages({exprs: [ + + ["\\,\\.\\.|\\.\\,\\.|\\.\\.\\,", "..."], + + ["„", ",,"] /* DOUBLE LOW-9 QUOTATION MARK */ + +]})lineregexpWykonuję operację z novel-pages.opowiadanieopowiadanie({tag:"opowiadanie"})insert_tagosobaosoba({tag:"osoba"})insert_tagosobaosoba({tag:"osoba"})insert_tagpodrozdziałpodrozdzial({tag:"naglowek_podrozdzial"})insert_tagpodtytułpodtytul({tag:"podtytul"})insert_tagpowieśćpowiesc({tag:"powiesc"})insert_tagprzypis autorskiprzypis-autorski({tag:"pa"})insert_tagprzypis edytorskiprzypis-edytorski({tag:"pe"})insert_tagprzypis redaktorskiprzypis-redaktorski({tag:"pr"})insert_tagprzypis tłumaczaprzypis-tlumacza({tag:"pt"})insert_tagrozdziałrozdzial({tag:"naglowek_rozdzial"})insert_tagscenascena({tag:"naglowek_scena"})insert_tagsep. asterykssep-asteryks({tag:"sekcja_asteryks"})insert_tagsep. liniasep-linia({tag:"separator_linia"})insert_tagsep. światłosep-swiatlo({tag:"sekcja_swiatlo"})insert_tagśródtytułsrodtytul({tag:"srodtytul"})insert_tagstrofastrofa({tag"strofa"})insert_tagstrofastrofa({tag:"strofa"})insert_tagsłowo obceslowo-obce({tag:"slowo_obce"})insert_tagtagi głównetagi-glowne({tag:"utwor"})insert_tagtytułtytul({tag:"nazwa_utworu"})insert_tagtytuł dziełatytul-dziela({tag:"tytul_dziela"})insert_tagUsuń spacjęstrip_whitespace({exprs: [ ["^\\s+|\\s+$", ""], ["\\s+", " "] ]})lineregexpUsuwa zbędne spację z dokumentu.uwagauwaga({tag:"uwaga"})insert_tagwers akap.wers-akap({tag:"wers_akap"})insert_tagwers akap.wers-akap({tag:"wers_akap"})insert_tagwers cd.wers-cd({tag:"wers_cd"})insert_tagwers cd.wers-cd({tag:"wers_cd"})insert_tagwers wciętywers-wciety({tag:"wers_wciety"})insert_tagwers wciętywers-wciety({tag:"wers_wciety"})insert_tagwwwwww({tag:"www"})insert_tagwyróżnieniewyroznienie({tag:"wyroznienie"})insert_tagwywiadwywiad({tag:"wywiad"})insert_tagwywiad odpowiedźwywiad-odpowiedz({tag:"wywiad_odp"})insert_tagwywiad pytaniewywiad-pytanie({tag:"wywiad_pyt"})insert_tagZamień dywizzamien_dywiz({exprs:[ ["—","---"] ]})lineregexpZamienia '—' na '---'.zastępnik wersuzastepnik-wersu({tag:"zastepnik_wersu"})insert_tag$.log(editor, panel, params); + + + +var texteditor = panel.texteditor; + +var text = texteditor.selection(); + +texteditor.replaceSelection('<' + params.tag + '>' + text + '</' + params.tag + '>'); + +if (text.length == 0) + +{ + + var pos = texteditor.cursorPosition(); + + texteditor.selectLines(pos.line, pos.character + params.tag.length + 2); + +} + + + +panel.fireEvent('contentChanged');// params: {exprs: list of {expr: "", repl: "" [, opts: "g"]}} +var cm = panel.texteditor; + +var exprs = $.map(params.exprs, function(expr) { + var opts = "g"; + if(expr.length > 2) + opts = expr[2]; + return {rx: new RegExp(expr[0], opts), repl: expr[1]}; +}); + +var selection = cm.selection(); + +if(selection) +{ + var changed = false; + var lines = selection.split('\n'); + var lines = $.map(lines, function(line) { + var old_line = line; + $(exprs).each(function() { + var expr = this; + line = line.replace(expr.rx, expr.repl); + }); + if(old_line != line) changed = true; + return line; + }); + + if(changed) { + cm.replaceSelection( lines.join('\n') ); + panel.fireEvent('contentChanged'); + } +} +else { + var line = cm.firstLine(); + var hasChanges = false; + do { + var content = cm.lineContent(line); + var old_content = content; + $(exprs).each(function() { var expr = this; + content = content.replace(expr.rx, expr.repl); + }); + + if(old_content != content) { + cm.setLineContent(line, content); + hasChanges = true; + } + + line = cm.nextLine(line); + } while( !(line === false) ); + + if(hasChanges) panel.fireEvent('contentChanged'); +}var texteditor = panel.texteditor; + +var frameBody = $('body', $(texteditor.frame).contents()); + + + +if(params.fontSize) { + + frameBody.css('font-size', params.fontSize); + +} + +else { + + var old_size = parseInt(frameBody.css('font-size')); + + frameBody.css('font-size', old_size + (params.change || 0) ); + } diff --git a/lib/hg.py b/lib/hg.py index 07ec9a79..06e9f832 100644 --- a/lib/hg.py +++ b/lib/hg.py @@ -5,9 +5,13 @@ import mercurial.merge, mercurial.error encoding.encoding = 'utf-8' +X = 'g\xc5\xbceg\xc5\xbc\xc3\xb3\xc5\x82ka' -def clearpath(path): - return unicode(path).encode("utf-8") +def sanitize_string(path): + if isinstance(path, unicode): # + return path.encode('utf-8') + else: # it's a string, so we have no idea what encoding it is + return path class Repository(object): """Abstrakcja repozytorium Mercurial. Działa z Mercurial w wersji 1.3.1.""" @@ -17,11 +21,10 @@ class Repository(object): self.ui.config('ui', 'quiet', 'true') self.ui.config('ui', 'interactive', 'false') - self.real_path = os.path.realpath(path) - self.repo = self.open_repository(self.real_path, create) - self._pending_files = [] - - def open_repository(self, path, create=False): + self.real_path = sanitize_string(os.path.realpath(path)) + self.repo = self._open_repository(self.real_path, create) + + def _open_repository(self, path, create=False): if os.path.isdir(path): try: return localrepo.localrepository(self.ui, path) @@ -44,8 +47,7 @@ class Repository(object): return self.in_branch(lambda: self._get_file(path), branch) def _get_file(self, path): - path = clearpath(path) - + path = sanitize_string(path) if not self._file_exists(path): raise RepositoryException("File not availble in this branch.") @@ -55,26 +57,26 @@ class Repository(object): return self.in_branch(lambda: self._file_exists(path), branch) def _file_exists(self, path): - path = clearpath(path) + path = sanitize_string(path) return self.repo.dirstate[path] != "?" def write_file(self, path, value, branch): return self.in_branch(lambda: self._write_file(path, value), branch) def _write_file(self, path, value): - path = clearpath(path) + path = sanitize_string(path) return self.repo.wwrite(path, value, []) def add_file(self, path, value, branch): return self.in_branch(lambda: self._add_file(path, value), branch) def _add_file(self, path, value): - path = clearpath(path) + path = sanitize_string(path) self._write_file(path, value) return self.repo.add( [path] ) def _commit(self, message, user=None): - return self.repo.commit(text=message, user=user) + return self.repo.commit(text=sanitize_string(message), user=sanitize_string(user)) def commit(self, message, branch, user=None): return self.in_branch(lambda: self._commit(message, key=key, user=user), branch) @@ -91,49 +93,143 @@ class Repository(object): finally: wlock.release() - def _switch_to_branch(self, bname, create=True): + def merge_branches(self, bnameA, bnameB, user, message): wlock = self.repo.wlock() try: - current = self.repo[None].branch() - if current == bname: - return current - try: - tip = self.repo.branchtags()[bname] - except KeyError, ke: - if not create: raise ke - - # create the branch on the fly + return self.merge_revisions(self.get_branch_tip(bnameA), + self.get_branch_tip(bnameB), user, message) + finally: + wlock.release() - # first switch to default branch - default_tip = self.repo.branchtags()['default'] - mercurial.merge.update(self.repo, default_tip, False, True, None) + def diff(self, revA, revB): + return UpdateStatus(self.repo.status(revA, revB)) - # set the dirstate to new branch - self.repo.dirstate.setbranch(bname) - self._commit('Initial commit for automatic branch "%s".' % bname, user="django-admin") + def merge_revisions(self, revA, revB, user, message): + wlock = self.repo.wlock() + try: + old = self.repo[None] + + self._checkout(revA) + mergestatus = self._merge(revB) + if not mergestatus.isclean(): + # revert the failed merge + self.repo.recover() + raise UncleanMerge(u'Failed to merge %d files.' % len(mergestatus.unresolved)) + + # commit the clean merge + self._commit(message, user) + + # cleanup after yourself + self._checkout(old.rev()) + except util.Abort, ae: + raise RepositoryException(u'Failed merge: ' + ae.message) + finally: + wlock.release() - # collect the new tip - tip = self.repo.branchtags()[bname] + def common_ancestor(self, revA, revB): + return self.repo[revA].ancestor(self.repo[revB]) + + def _checkout(self, rev, force=True): + return MergeStatus(mercurial.merge.update(self.repo, rev, False, force, None)) + + def _merge(self, rev): + """ Merge the revision into current working directory """ + return MergeStatus(mercurial.merge.update(self.repo, rev, True, False, None)) - upstats = mercurial.merge.update(self.repo, tip, False, True, None) - return current + def _switch_to_branch(self, bname): + bname = sanitize_string(bname) + wlock = self.repo.wlock() + try: + current = self.repo[None].branch() + if current == bname: + return current + + tip = self.get_branch_tip(bname) + status = self._checkout(tip) + + if not status.isclean(): + raise RepositoryException("Unclean branch switch. This IS REALLY bad.") + + return current except KeyError, ke: - raise RepositoryException("Can't switch to branch '%s': no such branch." % bname , ke) + raise RepositoryException((u"Can't switch to branch '%s': no such branch." % bname) , ke) except util.Abort, ae: - raise RepositoryException("Can't switch to branch '%s': %s" % (bname, ae.message), ae) + raise RepositoryException(u"Can't switch to branch '%s': %s" % (bname, ae.message), ae) finally: wlock.release() + def with_wlock(self, action): + wlock = self.repo.wlock() + try: + action() + finally: + wlock.release() + + def _create_branch(self, name, parent_rev, msg=None, before_commit=None): + """WARNING: leaves the working directory in the new branch""" + name = sanitize_string(name) + + if self.has_branch(name): return # just exit + + self._checkout(parent_rev) + self.repo.dirstate.setbranch(name) + + if msg is None: + msg = "Initial commit for branch '%s'." % name + + if before_commit: before_commit() + self._commit(msg, user='platform') + return self.get_branch_tip(name) + def write_lock(self): """Returns w write lock to the repository.""" return self.repo.wlock() + def has_branch(self, name): + name = sanitize_string(name) + return (name in self.repo.branchmap().keys()) + + def get_branch_tip(self, name): + name = sanitize_string(name) + return self.repo.branchtags()[name] -class RepositoryException(Exception): + def getnode(self, rev): + return self.repo[rev] +class MergeStatus(object): + + def __init__(self, mstatus): + self.updated = mstatus[0] + self.merged = mstatus[1] + self.removed = mstatus[2] + self.unresolved = mstatus[3] + + def isclean(self): + return self.unresolved == 0 + +class UpdateStatus(object): + + def __init__(self, mstatus): + self.modified = mstatus[0] + self.added = mstatus[1] + self.removed = mstatus[2] + self.deleted = mstatus[3] + self.untracked = mstatus[4] + self.ignored = mstatus[5] + self.clean = mstatus[6] + + def has_changes(self): + return bool( len(self.modified) + len(self.added) + \ + len(self.removed) + len(self.deleted) ) + +class RepositoryException(Exception): def __init__(self, msg, cause=None): Exception.__init__(self, msg) self.cause = cause +class UncleanMerge(RepositoryException): + pass + class RepositoryDoesNotExist(RepositoryException): pass + diff --git a/project/static/css/filelist.css b/project/static/css/filelist.css index f5b8ce0c..0e4f2a0e 100755 --- a/project/static/css/filelist.css +++ b/project/static/css/filelist.css @@ -24,7 +24,7 @@ .upload-file-widget { min-width: 20%; - width: 25%; + width: 35%; } diff --git a/project/static/css/master.css b/project/static/css/master.css index c321c782..31d811b3 100644 --- a/project/static/css/master.css +++ b/project/static/css/master.css @@ -334,35 +334,3 @@ div.isection p { background-color: yellow; border-color: yellow; } - - -/* - * Object list table - */ - table.object-list { - border-top: 2px solid black; - border-left: 2px solid black; - border-right: 1px solid black; - border-bottom: 1px solid black; - width: 60%; - margin: auto; - } - - - table.object-list td, table.object-list th { - border-bottom: 1px solid black; - border-right: 1px solid black; - padding: 0.2em 0.5em; - } - - table.object-list th { - text-align: center; - background-color: #8080d0; - font-size: 120% - } - - table.object-list td.page-navigation { - position: relative; - text-align: center; - background-color: #CCC; - } diff --git a/project/static/js/codemirror/select.js b/project/static/js/codemirror/select.js index 9ceb24e7..002004e2 100644 --- a/project/static/js/codemirror/select.js +++ b/project/static/js/codemirror/select.js @@ -238,7 +238,7 @@ var select = {}; // Move the start of a range to the start of a node, // compensating for the fact that you can't call // moveToElementText with text nodes. - function moveToNodeStart(range, node) { + function moveToNodeStart(range, node) { if (node.nodeType == 3) { var count = 0, cur = node.previousSibling; while (cur && cur.nodeType == 3) { @@ -253,7 +253,7 @@ var select = {}; else range.moveToElementText(node.parentNode); if (count) range.move("character", count); } - else range.moveToElementText(node); + else try{range.moveToElementText(node);} catch(e) {}; } // Do a binary search through the container object, comparing diff --git a/project/static/js/editor.js b/project/static/js/editor.js index 13c70978..3490b9a1 100644 --- a/project/static/js/editor.js +++ b/project/static/js/editor.js @@ -151,7 +151,14 @@ Panel.prototype.connectToolbar = function() action_buttons.each(function() { var button = $(this); var hk = button.attr('ui:hotkey'); - var params = $.evalJSON(button.attr('ui:action-params')); + + try { + var params = $.evalJSON(button.attr('ui:action-params')); + } catch(object) { + $.log('JSON exception in ', button, ': ', object); + button.attr('disabled', 'disabled'); + return; + } var callback = function() { editor.callScriptlet(button.attr('ui:action'), self, params); @@ -242,8 +249,21 @@ Editor.prototype.setupUI = function() { $('#toolbar-button-save').click( function (event, data) { self.saveToBranch(); } ); + + $('#toolbar-button-update').click( function (event, data) { + if (self.updateUserBranch()) { + // commit/update can be called only after proper, save + // this means all panels are clean, and will get refreshed + // do this only, when there are any changes to local branch + self.refreshPanels(); + } + } ); + $('#toolbar-button-commit').click( function (event, data) { self.sendPullRequest(); + event.preventDefault(); + event.stopPropagation(); + return false; } ); self.rootDiv.bind('stopResize', function() { self.savePanelOptions() @@ -364,9 +384,10 @@ Editor.prototype.saveToBranch = function(msg) self.showPopup('save-error', (data.errors && data.errors[0]) || 'Nieznany błąd X_X.'); } else { - self.refreshPanels(changed_panel); + self.refreshPanels(); $('#toolbar-button-save').attr('disabled', 'disabled'); $('#toolbar-button-commit').removeAttr('disabled'); + $('#toolbar-button-update').removeAttr('disabled'); if(self.autosaveTimer) clearTimeout(self.autosaveTimer); @@ -402,6 +423,7 @@ Editor.prototype.onContentChanged = function(event, data) { $('#toolbar-button-save').removeAttr('disabled'); $('#toolbar-button-commit').attr('disabled', 'disabled'); + $('#toolbar-button-update').attr('disabled', 'disabled'); if(this.autosaveTimer) return; this.autosaveTimer = setTimeout( function() { @@ -409,11 +431,10 @@ Editor.prototype.onContentChanged = function(event, data) { }, 300000 ); }; -Editor.prototype.refreshPanels = function(goodPanel) { +Editor.prototype.refreshPanels = function() { var self = this; - var panels = $('#' + self.rootDiv.attr('id') +' > *.panel-wrap', self.rootDiv.parent()); - panels.each(function() { + self.allPanels().each(function() { var panel = $(this).data('ctrl'); $.log('Refreshing: ', this, panel); if ( panel.changed() ) @@ -424,24 +445,70 @@ Editor.prototype.refreshPanels = function(goodPanel) { }; +Editor.prototype.updateUserBranch = function() { + if( $('.panel-wrap.changed').length != 0) + alert("There are unsaved changes - can't update."); + + var self = this; + $.ajax({ + url: $('#toolbar-button-update').attr('ui:ajax-action'), + dataType: 'json', + success: function(data, textStatus) { + switch(data.result) { + case 'done': + self.showPopup('generic-yes', 'Plik uaktualniony.'); + self.refreshPanels() + break; + case 'nothing-to-do': + self.showPopup('generic-info', 'Brak zmian do uaktualnienia.'); + break; + default: + self.showPopup('generic-error', data.errors && data.errors[0]); + } + }, + error: function(rq, tstat, err) { + self.showPopup('generic-error', 'Błąd serwera: ' + err); + }, + type: 'POST', + data: {} + }); +} + Editor.prototype.sendPullRequest = function () { if( $('.panel-wrap.changed').length != 0) - alert("There are unsaved changes - can't make a pull request."); - - this.showPopup('not-implemented'); -/* - $.ajax({ - url: '/pull-request', - dataType: 'json', - success: function(data, textStatus) { - $.log('data: ' + data); - }, - error: function(rq, tstat, err) { - $.log('commit error', rq, tstat, err); - }, - type: 'POST', - data: {} - }); */ + alert("There are unsaved changes - can't commit."); + + var self = this; + + /* this.showPopup('not-implemented'); */ + + $.log('URL !: ', $('#toolbar-commit-form').attr('action')); + + $.ajax({ + url: $('#toolbar-commit-form').attr('action'), + dataType: 'json', + success: function(data, textStatus) { + switch(data.result) { + case 'done': + self.showPopup('generic-yes', 'Łączenie zmian powiodło się.'); + + if(data.localmodified) + self.refreshPanels() + + break; + case 'nothing-to-do': + self.showPopup('generic-info', 'Brak zmian do połaczenia.'); + break; + default: + self.showPopup('generic-error', data.errors && data.errors[0]); + } + }, + error: function(rq, tstat, err) { + self.showPopup('generic-error', 'Błąd serwera: ' + err); + }, + type: 'POST', + data: {'message': $('#toolbar-commit-message').val() } + }); } Editor.prototype.showPopup = function(name, text, timeout) @@ -454,7 +521,7 @@ Editor.prototype.showPopup = function(name, text, timeout) return; var box = $('#message-box > #' + name); - $('*.data', box).html(text); + $('*.data', box).html(text || ''); box.fadeIn(); if(timeout > 0) @@ -482,6 +549,10 @@ Editor.prototype.advancePopupQueue = function() { } }; +Editor.prototype.allPanels = function() { + return $('#' + this.rootDiv.attr('id') +' > *.panel-wrap', this.rootDiv.parent()); +} + Editor.prototype.registerScriptlet = function(scriptlet_id, scriptlet_func) { diff --git a/project/static/js/jquery.logging.js b/project/static/js/jquery.logging.js index 315d48f5..d6cee14d 100644 --- a/project/static/js/jquery.logging.js +++ b/project/static/js/jquery.logging.js @@ -3,35 +3,39 @@ var LEVEL_INFO = 2; var LEVEL_WARN = 3; var LOG_LEVEL = LEVEL_DEBUG; - - var mozillaLog = function() { - if (window.console) - console.log.apply(this, arguments); - }; - - var safariLog = function() { - if (window.console) - console.log.apply(console, arguments); - }; + + var standardLog = function() { + if (window.console) + console.log.apply(console, arguments); + }; var operaLog = function() { opera.postError(arguments.join(' ')); }; - var defaultLog = function() { return false; }; + var msieLog = function() { + var args = $.makeArray(arguments); + var vals = $.map(args, function(n) { + try { + return JSON.stringify(n); + } catch(e) { + return ('' + n); + } + }); - $.log = function( ) { + if (window.console) + console.log(vals.join(" ")); + }; + + $.log = function() { return $.log.browserLog.apply(this, arguments); }; - if ($.browser.mozilla) - $.log.browserLog = mozillaLog; - else if ($.browser.safari) - $.log.browserLog = safariLog; - else if($.browser.opera) - $.log.browserLog = operaLog; - else - $.log.browserLog = defaultLog; - + if($.browser.opera) + $.log.browserLog = operaLog; + else if($.browser.msie) + $.log.browserLog = msieLog; + else + $.log.browserLog = standardLog; })(jQuery); diff --git a/project/templates/explorer/editor.html b/project/templates/explorer/editor.html index b3a2e4c6..fe9d93ab 100644 --- a/project/templates/explorer/editor.html +++ b/project/templates/explorer/editor.html @@ -29,16 +29,28 @@ {% block breadcrumbs %}Platforma Redakcyjna > plik {{ hash }}{% endblock breadcrumbs %} {% block header-toolbar %} - - + +
+ + +
+ + {% endblock %} {% block message-box %} -

Zapisuję dane na serwerze.

-

Zapisano :)

-

Błąd przy zapisie.

-

Tej funkcji jeszcze nie ma :(

-

Zapisano. Uwagi: (

+

Zapisuję dane na serwerze.

+

Zapisano :)

+

Zapisano. Uwagi: (

+

Błąd przy zapisie.

+ +

+

+

+ +

Tej funkcji jeszcze nie ma :(

+ {% endblock %} {% block maincontent %} diff --git a/project/urls.py b/project/urls.py index 972c33dd..cf082478 100644 --- a/project/urls.py +++ b/project/urls.py @@ -12,6 +12,8 @@ urlpatterns = patterns('', url(r'^file/text/'+PATH_END, 'explorer.views.file_xml', name='file_xml'), url(r'^file/dc/'+PATH_END, 'explorer.views.file_dc', name='file_dc'), url(r'^file/upload', 'explorer.views.file_upload', name='file_upload'), + url(r'^file/commit/'+PATH_END, 'explorer.views.file_commit', name='file_commit'), + url(r'^file/update/'+PATH_END, 'explorer.views.file_update_local', name='file_update'), url(r'^images/(?P[^/]+)/$', 'explorer.views.folder_images', name='folder_image'), url(r'^images/$', 'explorer.views.folder_images', {'folder': '.'}, name='folder_image_ajax'), @@ -23,6 +25,7 @@ urlpatterns = patterns('', url(r'^editor/panel/dceditor/'+PATH_END, 'explorer.views.dceditor_panel', name='dceditor_panel'), url(r'^editor/'+PATH_END, 'explorer.views.display_editor', name='editor_view'), + # Task managment url(r'^manager/pull-requests$', 'explorer.views.pull_requests'), -- 2.20.1