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)
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()
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('.'))
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):
# Edit the file
#
+@ajax_login_required
@with_repo
def file_xml(request, repo, path):
if request.method == 'POST':
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"
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':
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'],
# = Panel views =
# ===============
+@ajax_login_required
@with_repo
def xmleditor_panel(request, path, repo):
form = forms.BookForm()
})
+@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)
})
+@ajax_login_required
@with_repo
def dceditor_panel(request, path, repo):
user_branch = models.user_branch(request.user)
# =================
# = 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),
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()} )
# 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'):
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:
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 {
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;
#panels {
position: absolute;
- bottom: 0px; left: 0px; right: 0px; top: 1em;
+ bottom: 0px; left: 0px; right: 0px; top: 0px;
}
.panel-wrap {
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;
.toolbar-buttons {
background-color: #EEE;
border-bottom: 1px solid #AAA;
+ z-index: 71;
}
.toolbar-buttons li {
.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;
+
+}
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) {
}
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;
}
function Editor() {
this.rootDiv = $('#panels');
+ this.popupQueue = [];
+ this.autosaveTimer = null;
}
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);
});
});
- $(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() });
}
$.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,
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) {
panels.each(function() {
var panel = $(this).data('ctrl');
- $.log(this, panel);
+ $.log('Refreshing: ', this, panel);
if ( panel.changed() )
panel.unmarkChanged();
else
});
};
+
+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();
{% block header-toolbar %}{% endblock %}
<span id="login_info">{% include "registration/head_login.html" %}</span>
</span>
+ <div id="message-box">{% block message-box %} {% endblock %}</div>
</div>
- {% block maincontent %} {% endblock %}
+ <div id="content">{% block maincontent %} {% endblock %}</div>
</body>
</html>
{% block breadcrumbs %}<a href="{% url file_list %}">Platforma Redakcyjna</a> ❯ plik {{ hash }}{% endblock breadcrumbs %}
{% block header-toolbar %}
+ <button type="button" class="toolbar-button" id="toolbar-button-commit">Commit</button>
<button type="button" class="toolbar-button" id="toolbar-button-save" disabled="disabled">Zapisz</button>
{% endblock %}
+
+{% block message-box %}
+ <div class="msg-success" id="save-successful">Zapisano :)</div>
+ <div class="msg-error" id="save-error">Błąd przy zapisie. <span class="errors" /></div>
+ <div class="msg-warning" id="not-implemented">Tej funkcji jeszcze nie ma :(</div>
+{% endblock %}
+
{% block maincontent %}
<div id="panels">
{% for n in panel_list %}
},
- refresh: function() {
- return false;
- },
+ //refresh: function() { }, // no support for refresh
saveInfo: function(saveInfo) {
var myInfo = {
{% if user.is_authenticated %}
-<span class="user_name">{{ user.get_full_name }}</span> |
-<a href='{% url django.contrib.auth.views.logout %}?next_page={{request.get_full_path}}'>Wyloguj</a>
+<span class="user_name">{{ user.username }}</span> |
+<a href='{% url django.contrib.auth.views.logout %}?next={{request.get_full_path}}'>Wyloguj</a>
{% else %}
-<a href='{% url django.contrib.auth.views.login %}?next_page={{request.get_full_path}}'>Logowanie</a>
+{% url django.contrib.auth.views.login as login_url %}
+{% ifnotequal login_url request.path %}
+ <a href='{% url django.contrib.auth.views.login %}?next={{request.get_full_path}}'>Logowanie</a>
+{% endifnotequal %}
+
{% endif %}
<form method="POST" action="{% url django.contrib.auth.views.login %}">
{{ form.as_p }}
<p><input type="submit" value="Login" /></p>
-<input type="hidden" name="next_page" value="{{ next_page }}" />
+<input type="hidden" name="next" value="{{ next }}" />
</form>
</div>
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': '/'}),
)