From: zuber Date: Mon, 31 Aug 2009 10:10:30 +0000 (+0200) Subject: Merge branch 'master' of git@stigma.nowoczesnapolska.org.pl:platforma X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/b993aa15c106eadf473fd64edbb9f3d9ddb7754a?hp=4024af4373eb6755075aa3c5f259e511d0f1b5a4 Merge branch 'master' of git@stigma.nowoczesnapolska.org.pl:platforma --- diff --git a/apps/explorer/forms.py b/apps/explorer/forms.py index f7fcf07e..c2f7c3a0 100644 --- a/apps/explorer/forms.py +++ b/apps/explorer/forms.py @@ -7,7 +7,7 @@ from explorer import models class BookForm(forms.Form): content = forms.CharField(widget=forms.Textarea) - message = forms.CharField(required=False) + commit_message = forms.CharField(required=False) class ImageFoldersForm(forms.Form): folders = forms.ChoiceField(required=False) @@ -49,7 +49,7 @@ class DublinCoreForm(forms.Form): self.fields[name].initial = value def save(self, repository, path): - file_contents = repository.get_file(path).data() + file_contents = repository.get_file(path) doc = etree.fromstring(file_contents) book_info = dcparser.BookInfo() diff --git a/apps/explorer/models.py b/apps/explorer/models.py index f867189f..10491f6a 100644 --- a/apps/explorer/models.py +++ b/apps/explorer/models.py @@ -19,6 +19,22 @@ class PanelSettings(models.Model): return u"Panel settings for %s" % self.user.name +class PullRequest(models.Model): + comitter = models.ForeignKey(User) # the user who request the pull + file = models.CharField(max_length=256) # the file to request + source_rev = models.CharField(max_length=40) # revision number of the commiter + + comment = models.TextField() # addtional comments to the request + + # revision number in which the changes were merged (if any) + merged_rev = models.CharField(max_length=40, null=True) + + def __unicode__(self): + return u"Pull request from %s, source: %s %s, status: %s." % \ + (self.commiter, self.file, self.source_rev, \ + (("merged into "+self.merged_rev) if self.merged_rev else "pending") ) + + def get_image_folders(): return sorted(fn for fn in os.listdir(os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR)) if not fn.startswith('.')) diff --git a/apps/explorer/views.py b/apps/explorer/views.py index 896ed09c..7a34cad2 100644 --- a/apps/explorer/views.py +++ b/apps/explorer/views.py @@ -10,12 +10,30 @@ from django.contrib.auth.decorators import login_required from explorer import forms, models - -def with_repo(func): - def inner(request, *args, **kwargs): +# +# Some useful decorators +# +def with_repo(view): + """Open a repository for this view""" + def view_with_repo(request, *args, **kwargs): kwargs['repo'] = hg.Repository(settings.REPOSITORY_PATH) - return func(request, *args, **kwargs) - return inner + return view(request, *args, **kwargs) + return view_with_repo + +# +def ajax_login_required(view): + """Similar ro @login_required, but instead of redirect, + just return some JSON stuff with error.""" + def view_with_auth(request, *args, **kwargs): + if request.user.is_authenticated(): + return view(request, *args, **kwargs) + # not authenticated + return HttpResponse( json.dumps({'result': 'access_denied'}) ); + return view_with_auth + +# +# View all files +# @with_repo def file_list(request, repo): @@ -26,6 +44,7 @@ def file_list(request, repo): # Edit the file # +@ajax_login_required @with_repo def file_xml(request, repo, path): if request.method == 'POST': @@ -36,7 +55,7 @@ def file_xml(request, repo, path): print 'In branch: ' + repo.repo[None].branch() print repo._add_file(path, form.cleaned_data['content']) print repo.repo.status() - print repo._commit(message='Local save at %s' % time.ctime(), user=request.user.username) + print repo._commit(message=(form.cleaned_data['commit_message'] or 'Lokalny zapis platformy.'), user=request.user.username) print repo.in_branch(save_action, models.user_branch(request.user) ); result = "ok" @@ -45,13 +64,13 @@ def file_xml(request, repo, path): errors = dict( (field[0], field[1].as_text()) for field in form.errors.iteritems() ) return HttpResponse( json.dumps({'result': result, 'errors': errors}) ); - else: - form = forms.BookForm() - form.fields['content'].initial = repo.get_file(path, models.user_branch(request.user)) - return direct_to_template(request, 'explorer/edit_text.html', extra_context={ - 'form': form, - }) + form = forms.BookForm() + data = repo.get_file(path, models.user_branch(request.user)) + form.fields['content'].initial = data + return HttpResponse( json.dumps({'result': 'ok', 'content': data}) ) + +@ajax_login_required @with_repo def file_dc(request, path, repo): if request.method == 'POST': @@ -64,16 +83,14 @@ def file_dc(request, path, repo): errors = dict( (field[0], field[1].as_text()) for field in form.errors.iteritems() ) return HttpResponse( json.dumps({'result': result, 'errors': errors}) ); - else: - fulltext = repo.get_file(path, models.user_branch(request.user)) - form = forms.DublinCoreForm(text=fulltext) - - return direct_to_template(request, 'explorer/edit_dc.html', extra_context={ - 'form': form, - 'fpath': path, - }) + + fulltext = repo.get_file(path, models.user_branch(request.user)) + form = forms.DublinCoreForm(text=fulltext) + return HttpResponse( json.dumps({'result': 'ok', 'content': fulltext}) ) # Display the main editor view + +@login_required def display_editor(request, path): return direct_to_template(request, 'explorer/editor.html', extra_context={ 'hash': path, 'panel_list': ['lewy', 'prawy'], @@ -83,6 +100,7 @@ def display_editor(request, path): # = Panel views = # =============== +@ajax_login_required @with_repo def xmleditor_panel(request, path, repo): form = forms.BookForm() @@ -94,12 +112,14 @@ def xmleditor_panel(request, path, repo): }) +@ajax_login_required def gallery_panel(request, path): return direct_to_template(request, 'explorer/panels/gallery.html', extra_context={ 'fpath': path, 'form': forms.ImageFoldersForm(), }) +@ajax_login_required @with_repo def htmleditor_panel(request, path, repo): user_branch = models.user_branch(request.user) @@ -109,6 +129,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) @@ -124,6 +145,7 @@ def dceditor_panel(request, path, repo): # ================= # = Utility views = # ================= +@ajax_login_required def folder_images(request, folder): return direct_to_template(request, 'explorer/folder_images.html', extra_context={ 'images': models.get_images_from_folder(folder), @@ -147,3 +169,11 @@ def _get_issues_for_file(path): return [] finally: if uf: uf.close() + + +# ================= +# = Pull requests = +# ================= +def pull_requests(request): + return direct_to_template(request, 'manager/pull_request.html', extra_context = { + 'objects': models.PullRequest.objects.all()} ) diff --git a/lib/hg.py b/lib/hg.py index 60e9f80e..b8795b71 100644 --- a/lib/hg.py +++ b/lib/hg.py @@ -58,70 +58,10 @@ class Repository(object): # if path not in self._pending_files: # self._pending_files.append(path) - def _commit(self, message, user=None, key=None): + def _commit(self, message, user=None): return self.repo.commit(text=message, user=user) - def _commit2(self, message, key=None, user=None): - """ - Commit unsynchronized data to disk. - Arguments:: - - - message: mercurial's changeset message - - key: supply to sync only one key - """ - if isinstance(message, unicode): - message = message.encode('utf-8') - if isinstance(user, unicode): - user = user.encode('utf-8') - - commited = False - rev = None - files_to_add = [] - files_to_remove = [] - files_to_commit = [] - - # first of all, add absent data and clean removed - if key is None: - # will commit all keys - pending_files = self._pending_files - else: - if keys not in self._pending_files: - # key isn't changed - return None - else: - pending_files = [key] - for path in pending_files: - files_to_commit.append(path) - if path in self.all_files(): - if not os.path.exists(os.path.join(self.real_path, path)): - # file removed - files_to_remove.append(path) - else: - # file added - files_to_add.append(path) - # hg add - if files_to_add: - self.repo.add(files_to_add) - # hg forget - if files_to_remove: - self.repo.forget(files_to_remove) - # ---- hg commit - if files_to_commit: - for i, f in enumerate(files_to_commit): - if isinstance(f, unicode): - files_to_commit[i] = f.encode('utf-8') - matcher = match.match(self.repo.root, self.repo.root, files_to_commit, default='path') - rev = self.repo.commit(message, user=user, match=matcher) - commited = True - # clean pending keys - for key in pending_files: - self._pending_files.remove(key) - # if commited: - # reread keys - # self._keys = self.get_persisted_objects_keys() - # return node.hex(rev) - - def commit(self, message, key=None, user=None, branch='default'): + def commit(self, message, user=None, branch='default'): return self.in_branch(lambda: self._commit(message, key=key, user=user), branch) def in_branch(self, action, bname='default'): @@ -136,14 +76,30 @@ class Repository(object): finally: wlock.release() - def _switch_to_branch(self, bname): + def _switch_to_branch(self, bname, create=True): 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 + + # first switch to default branch + default_tip = self.repo.branchtags()['default'] + mercurial.merge.update(self.repo, default_tip, False, True, None) + + # set the dirstate to new branch + self.repo.dirstate.setbranch(bname) + self._commit('Initial commit for automatic branch "%s".' % bname, user="django-admin") + + # collect the new tip + tip = self.repo.branchtags()[bname] - tip = self.repo.branchtags()[bname] upstats = mercurial.merge.update(self.repo, tip, False, True, None) return current except KeyError, ke: diff --git a/project/static/css/master.css b/project/static/css/master.css index 604b69a7..3f5741c1 100644 --- a/project/static/css/master.css +++ b/project/static/css/master.css @@ -2,16 +2,20 @@ body { margin: 0; font: 10pt Helvetica, Verdana, sans-serif; overflow: hidden; + background: #AAA; } #header { - position: relative; + position: absolute; padding: 0.2em 0.5em; background-color: #CDCDCD; - border-bottom: 1px solid black; + border-bottom: 0.1px solid black; height: 1.8em; line-height: 1.8em; vertical-align: center; + + top: 0px; left: 0px; right: 0px; + z-index: 300; } #header #breadcrumbs { @@ -26,6 +30,13 @@ body { vertical-align: center; } +#content { + position: absolute; + top: 2.4em; left: 0px; right: 0px; bottom: 0px; + overflow: auto; + background-color: white; +} + ul { margin: 20px; padding: 0; @@ -96,7 +107,7 @@ label { #panels { position: absolute; - bottom: 0px; left: 0px; right: 0px; top: 1em; + bottom: 0px; left: 0px; right: 0px; top: 0px; } .panel-wrap { @@ -154,7 +165,7 @@ label { top: 0px; left:0px; right: 0px; height: 20px; padding: 0 0 2px 0; - border-top: 1px solid #AAA; + /* border-top: 1px solid #AAA; */ border-bottom: 1px solid #AAA; font-size: 12px; line-height: 20px; @@ -251,6 +262,7 @@ label { .toolbar-buttons { background-color: #EEE; border-bottom: 1px solid #AAA; + z-index: 71; } .toolbar-buttons li { @@ -333,3 +345,43 @@ div.isection p { .change-notification { color: red; } + +/* + * Popups + */ +#message-box { + position: fixed; + top: 2px; + left: 40%; +} + +.msg-error, .msg-success, .msg-warning { + overflow: hidden; + padding: 0.1em 0.2em; + text-align: center; + border: 1px solid; + -moz-border-radius: 8px; + -webkit-border-radius: 8px; + font-size: 10pt; + line-height: 11pt; + display: none; + + width: 200px; +} + + +.msg-error { + background-color: red; + border-color: red; +} + +.msg-success { + background-color: lightgreen; + border-color: lightgreen; +} + +.msg-warning { + background-color: yellow; + border-color: yellow; + +} diff --git a/project/static/js/editor.js b/project/static/js/editor.js index 0f56fa79..3b750cab 100644 --- a/project/static/js/editor.js +++ b/project/static/js/editor.js @@ -23,15 +23,14 @@ Panel.prototype.callHook = function() { args = $.makeArray(arguments) var hookName = args.splice(0,1)[0] var noHookAction = args.splice(0,1)[0] + var result = false; $.log('calling hook: ', hookName, 'with args: ', args); if(this.hooks && this.hooks[hookName]) - { - return this.hooks[hookName].apply(this, args); - } - else if (noHookAction instanceof Function) - return noHookAction(args); - else return false; + result = this.hooks[hookName].apply(this, args); + else if (noHookAction instanceof Function) + result = noHookAction(args); + return result; } Panel.prototype.load = function (url) { @@ -69,9 +68,10 @@ Panel.prototype.unload = function(event, data) { } Panel.prototype.refresh = function(event, data) { + var self = this; reload = function() { - $.log('hard reload for panel ', this.current_url); - this.load(this.current_url); + $.log('hard reload for panel ', self.current_url); + self.load(self.current_url); return true; } @@ -106,6 +106,8 @@ Panel.prototype.saveInfo = function() { function Editor() { this.rootDiv = $('#panels'); + this.popupQueue = []; + this.autosaveTimer = null; } Editor.prototype.setupUI = function() { @@ -113,7 +115,7 @@ Editor.prototype.setupUI = function() { var self = this; self.rootDiv.makeHorizPanel({}); // TODO: this probably doesn't belong into jQuery - self.rootDiv.css('top', ($('#header').outerHeight() ) + 'px'); + // self.rootDiv.css('top', ($('#header').outerHeight() ) + 'px'); $('#panels > *.panel-wrap').each(function() { var panelWrap = $(this); @@ -129,11 +131,10 @@ Editor.prototype.setupUI = function() { }); }); - $(document).bind('panel:contentChanged', function(event, data) { - $('#toolbar-button-save').removeAttr('disabled'); - }); + $(document).bind('panel:contentChanged', function() { self.onContentChanged.apply(self, arguments) }); $('#toolbar-button-save').click( function (event, data) { self.saveToBranch(); } ); + $('#toolbar-button-commit').click( function (event, data) { self.sendPullRequest(); } ); self.rootDiv.bind('stopResize', function() { self.savePanelOptions() }); } @@ -199,22 +200,26 @@ Editor.prototype.savePanelOptions = function() { $.cookie('options', $.toJSON(self.options), { expires: 7, path: '/'}); } -Editor.prototype.saveToBranch = function() { +Editor.prototype.saveToBranch = function(msg) +{ var changed_panel = $('.panel-wrap.changed'); var self = this; $.log('Saving to local branch - panel:', changed_panel); + if(!msg) msg = "Zapis z edytora platformy."; + if( changed_panel.length == 0) { $.log('Nothing to save.'); - return; /* no changes */ + return true; /* no changes */ } if( changed_panel.length > 1) { alert('Błąd: więcej niż jeden panel został zmodyfikowany. Nie można zapisać.'); - return; + return false; } saveInfo = changed_panel.data('ctrl').saveInfo(); + $.extend(saveInfo.postData, {'commit_message': msg}); $.ajax({ url: saveInfo.url, @@ -222,16 +227,42 @@ Editor.prototype.saveToBranch = function() { success: function(data, textStatus) { if (data.result != 'ok') $.log('save errors: ', data.errors) - else + else { self.refreshPanels(changed_panel); - $('#toolbar-button-save').attr('disabled', 'disabled'); + $('#toolbar-button-save').attr('disabled', 'disabled'); + $('#toolbar-button-commit').removeAttr('disabled'); + if(self.autosaveTimer) + clearTimeout(self.autosaveTimer); + + self.showPopup('save-successful'); + } }, error: function(rq, tstat, err) { - $.log('save error', rq, tstat, err); + self.showPopup('save-error'); }, type: 'POST', data: saveInfo.postData }); + + return true; +}; + +Editor.prototype.autoSave = function() +{ + this.autosaveTimer = null; + // first check if there is anything to save + $.log('Autosave'); + this.saveToBranch("Automatyczny zapis z edytora platformy."); +} + +Editor.prototype.onContentChanged = function(event, data) { + var self = this; + + $('#toolbar-button-save').removeAttr('disabled'); + $('#toolbar-button-commit').attr('disabled', 'disabled'); + + if(this.autosaveTimer) return; + this.autosaveTimer = setTimeout( function() { self.autoSave(); }, 300000 ); }; Editor.prototype.refreshPanels = function(goodPanel) { @@ -240,7 +271,7 @@ Editor.prototype.refreshPanels = function(goodPanel) { panels.each(function() { var panel = $(this).data('ctrl'); - $.log(this, panel); + $.log('Refreshing: ', this, panel); if ( panel.changed() ) panel.unmarkChanged(); else @@ -248,6 +279,54 @@ Editor.prototype.refreshPanels = function(goodPanel) { }); }; + +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: {} + }); */ +} + +Editor.prototype.showPopup = function(name) +{ + var self = this; + self.popupQueue.push(name) + + if( self.popupQueue.length > 1) + return; + + $('#message-box > #' + name).fadeIn(); + + self._nextPopup = function() { + var elem = self.popupQueue.pop() + if(elem) { + elem = $('#message-box > #' + elem); + elem.fadeOut(300, function() { + if( self.popupQueue.length > 0) + $('#message-box > #' + self.popupQueue[0]).fadeIn(); + setTimeout(self._nextPopup, 1700); + }); + + } + } + + setTimeout(self._nextPopup, 2000); +} + + $(function() { editor = new Editor(); diff --git a/project/templates/base.html b/project/templates/base.html index db77baad..ec326166 100644 --- a/project/templates/base.html +++ b/project/templates/base.html @@ -17,7 +17,8 @@ {% block header-toolbar %}{% endblock %} {% include "registration/head_login.html" %} +
{% block message-box %} {% endblock %}
- {% block maincontent %} {% endblock %} +
{% block maincontent %} {% endblock %}
diff --git a/project/templates/explorer/editor.html b/project/templates/explorer/editor.html index 7be73b34..75e62c9b 100644 --- a/project/templates/explorer/editor.html +++ b/project/templates/explorer/editor.html @@ -14,8 +14,16 @@ {% block breadcrumbs %}Platforma Redakcyjna ❯ plik {{ hash }}{% endblock breadcrumbs %} {% block header-toolbar %} + {% endblock %} + +{% block message-box %} +
Zapisano :)
+
Błąd przy zapisie.
+
Tej funkcji jeszcze nie ma :(
+{% endblock %} + {% block maincontent %}
{% for n in panel_list %} diff --git a/project/templates/explorer/panels/xmleditor.html b/project/templates/explorer/panels/xmleditor.html index 90771a1a..2253609c 100644 --- a/project/templates/explorer/panels/xmleditor.html +++ b/project/templates/explorer/panels/xmleditor.html @@ -98,9 +98,7 @@ panel_hooks = { }, - refresh: function() { - return false; - }, + //refresh: function() { }, // no support for refresh saveInfo: function(saveInfo) { var myInfo = { diff --git a/project/templates/registration/head_login.html b/project/templates/registration/head_login.html index d6df7fe7..ded03e97 100644 --- a/project/templates/registration/head_login.html +++ b/project/templates/registration/head_login.html @@ -1,6 +1,10 @@ {% if user.is_authenticated %} -{{ user.get_full_name }} | -Wyloguj +{{ user.username }} | +Wyloguj {% else %} -Logowanie +{% url django.contrib.auth.views.login as login_url %} +{% ifnotequal login_url request.path %} + Logowanie +{% endifnotequal %} + {% endif %} diff --git a/project/templates/registration/login.html b/project/templates/registration/login.html index 8bf89675..810bd4ec 100644 --- a/project/templates/registration/login.html +++ b/project/templates/registration/login.html @@ -8,7 +8,7 @@
{{ form.as_p }}

- +
diff --git a/project/urls.py b/project/urls.py index 4e672f3e..22627ac6 100644 --- a/project/urls.py +++ b/project/urls.py @@ -22,13 +22,16 @@ 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'), + # Admin panel url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/(.*)', admin.site.root), # Authorization - url(r'^accounts/login/$', 'django.contrib.auth.views.login', {'redirect_field_name': 'next_page'}), - url(r'^accounts/logout$', 'django.contrib.auth.views.logout', {'next_page': '/'}), # {'redirect_field_name': 'next_page'}), + url(r'^accounts/login/$', 'django.contrib.auth.views.login', {'redirect_field_name': 'next'}), + url(r'^accounts/logout$', 'django.contrib.auth.views.logout', {'next_page': '/'}), )