add saving favorite locations
authorMaciej Tronowski <mtro@man.poznan.pl>
Wed, 15 Apr 2015 16:04:05 +0000 (18:04 +0200)
committerMaciej Tronowski <mtro@man.poznan.pl>
Wed, 15 Apr 2015 16:04:05 +0000 (18:04 +0200)
14 files changed:
filex/admin.py
filex/forms.py
filex/migrations/0001_initial.py [new file with mode: 0644]
filex/migrations/0002_auto_20150415_1744.py [new file with mode: 0644]
filex/models.py
filex/static/filex/filex.js
filex/templates/filex/source.html
filex/templates/filex/source.js.html
filex/templatetags/__init__.py [new file with mode: 0644]
filex/templatetags/filex.py [new file with mode: 0644]
filex/urls.py
filex/views.py
qcg/static/qcg/main.css
qcg/templates/qcg/gridftp.html

index 8c38f3f..9c68c68 100644 (file)
@@ -1,3 +1,12 @@
 from django.contrib import admin
 
-# Register your models here.
+from filex.models import Favorite
+
+
+class FavoriteAdmin(admin.ModelAdmin):
+    list_display = ('owner', 'host', 'path')
+    list_filter = ('owner',)
+    date_hierarchy = 'created'
+
+
+admin.site.register(Favorite, FavoriteAdmin)
index 1c9ffe9..db6587b 100644 (file)
@@ -1,6 +1,15 @@
 # coding=utf-8
 from django import forms
 
+from filex.models import Favorite
+
+
+class FavoriteForm(forms.ModelForm):
+    class Meta:
+        model = Favorite
+        fields = ('owner', 'host', 'path')
+        widgets = {'owner': forms.HiddenInput()}
+
 
 class NewDirForm(forms.Form):
     host = forms.CharField(label=u'Host', max_length=256, widget=forms.HiddenInput())
diff --git a/filex/migrations/0001_initial.py b/filex/migrations/0001_initial.py
new file mode 100644 (file)
index 0000000..96acc89
--- /dev/null
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Favorite',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('host', models.CharField(max_length=256, verbose_name='Host')),
+                ('path', models.CharField(max_length=1024, verbose_name='\u015acie\u017cka')),
+                ('created', models.DateTimeField(auto_now_add=True, verbose_name='Utworzono')),
+                ('updated', models.DateTimeField(auto_now=True, verbose_name='Uaktualniono')),
+                ('owner', models.ForeignKey(related_name='favorites', verbose_name='W\u0142a\u015bciciel', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'verbose_name': 'Ulubiona lokalizacja',
+                'verbose_name_plural': 'Ulubione lokalizacje',
+            },
+            bases=(models.Model,),
+        ),
+        migrations.AlterUniqueTogether(
+            name='favorite',
+            unique_together=set([('owner', 'host', 'path')]),
+        ),
+    ]
diff --git a/filex/migrations/0002_auto_20150415_1744.py b/filex/migrations/0002_auto_20150415_1744.py
new file mode 100644 (file)
index 0000000..be6c972
--- /dev/null
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('filex', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='favorite',
+            name='path',
+            field=models.CharField(default=b'/', max_length=1024, verbose_name='\u015acie\u017cka'),
+            preserve_default=True,
+        ),
+    ]
index 71a8362..e7e8247 100644 (file)
@@ -1,3 +1,20 @@
+# coding=utf-8
+from django.conf import settings
 from django.db import models
 
-# Create your models here.
+
+class Favorite(models.Model):
+    owner = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=u"Właściciel", related_name='favorites')
+    host = models.CharField(u"Host", max_length=256)
+    path = models.CharField(u"Ścieżka", max_length=1024, default='/')
+
+    created = models.DateTimeField(u"Utworzono", auto_now_add=True)
+    updated = models.DateTimeField(u"Uaktualniono", auto_now=True)
+
+    class Meta:
+        verbose_name = u"Ulubiona lokalizacja"
+        verbose_name_plural = u"Ulubione lokalizacje"
+        unique_together = ('owner', 'host', 'path')
+
+    def __unicode__(self):
+        return u'{}{} ({})'.format(self.host, self.path, self.owner)
index 5132ec7..29589c3 100644 (file)
@@ -95,8 +95,7 @@ $(function(){
         model: Filex.PathBit,
 
         initialize: function() {
-            this.listenTo(this, 'reset', this.setActive);
-            this.listenTo(this, 'add', this.setActive);
+            this.listenTo(this, 'reset add', this.setActive);
         },
 
         setActive: function() {
@@ -210,7 +209,9 @@ $(function(){
         events: {
             'change #show-hidden': 'toggleHidden',
             'click #select-all': 'selectAll',
-            'click #btn-refresh': 'reloadFiles'
+            'click #btn-refresh': 'reloadFiles',
+            'click #btn-favorites': 'toggleFavorites',
+            'click #btn-host': 'editHost'
         },
 
         initialize: function(options) {
@@ -218,14 +219,15 @@ $(function(){
             this.$error = $('#error');
             this.$showHidden = $('#show-hidden');
             this.$selectAll = $('#select-all');
+            this.$favorites = $('#btn-favorites');
+            this.$host = $('#btn-host');
 
-            this.host = options.host;
             this.path = new Filex.Path();
             this.files = new Filex.FileList();
 
             this.listenTo(this.path, 'reset', this.resetPath);
-            this.listenTo(this.path, 'add', this.reloadFiles);
             this.listenTo(this.path, 'add', this.addPath);
+            this.listenTo(this.path, 'add reset', this.changedPath);
             this.listenTo(this.path, 'selected', this.selectedPath);
             this.listenTo(this.files, 'reset', this.resetFiles);
             this.listenTo(this.files, 'selected:dir', this.selectedDir);
@@ -236,58 +238,102 @@ $(function(){
                 optionTemplate = _.template('<div><div><%= host %></div><div class="small text-muted"><%= path %></div></div>');
 
             this.$('#host-selector').selectize({
-                valueField: 'host',
+                optgroupField: 'group',
                 labelField: 'host',
                 searchField: ['host', 'path'],
                 sortField: [
                     {field: 'host', direction: 'asc'},
                     {field: 'path', direction: 'asc'}
                 ],
-                items: [this.host],
-                options: options.hostOptions,
+                options: options.locations,
+                optgroups: [
+                    {value: 'sys', label: 'Podstawowe'},
+                    {value: 'usr', label: 'Użytkownika'}
+                ],
+                lockOptgroupOrder: true,
+                create: function(input, callback) {
+                    var $form = $('#favorite-form'),
+                        parts = input.split('/', 1),
+                        callback_called = false;
+
+                    $form.find('#id_host').val(parts[0]);
+
+                    if (parts.length > 1)
+                        $form.find('#id_path').val(parts[1]);
+
+                    $form.on('submit', function(e) {
+                        var $this = $(this),
+                            $btn = $this.find('[type="submit"]');
+
+                        e.preventDefault();
+                        $btn.button('loading');
+
+                        $.post($this.attr('action'), $this.serialize(), function(data) {
+                            callback(data);
+                            callback_called = true;
+
+                            $this.modal('hide');
+                            $btn.button('reset');
+                        }, 'json').fail(function() {
+                            console.error(arguments);
+                            $btn.button('error');
+                        });
+                    });
+
+                    $form.one('hide.bs.modal', function() {
+                        if (!callback_called)
+                            callback();
+                        $form.off();
+                    });
+
+                    $form.modal();
+                },
                 render: {
                     option: function(item) {
                         return optionTemplate(item);
+                    },
+                    option_create : function(data, escape) {
+                        return '<div class="create">Dodaj <em>' + escape(data.input) + '</em>&hellip;</div>';
                     }
                 },
-                onDropdownClose: function() {
+                onItemRemove: function(value) {
+                    this.oldValue = value;
+                },
+                onItemAdd: function(value) {
+                    view.load(value);
                     this.blur();
                 },
                 onBlur: function() {
-                    var value = this.getValue();
-                    if (!value) {
-                        this.addItem(view.host, true);
-                        return;
-                    }
-
-                    if (value != view.host) {
-                        var location = this.options[value];
+                    if (!this.getValue() && this.oldValue)
+                        this.addItem(this.oldValue);
 
-                        view.host = location.host;
-                        view.navigate(location.path);
-                    }
+                    $('#host').removeClass('edit');
+                    this.clear();
                 }
             });
             this.hostSelectize = this.$('#host-selector')[0].selectize;
 
             this.render();
-            this.navigate(this.hostSelectize.options[this.host].path);
         },
 
         render: function() {
             this.updateSelectAll();
+            this.updateFavorites();
             this.$noItems.toggle(!Boolean(this.visibleFiles().length));
             this.$error.hide();
         },
 
-        navigate: function(path) {
-            var pathBits = [new Filex.PathBit({'text': '/', 'path': ''})];
-
-            pathBits = pathBits.concat(_.map(path.replace(/(^\/+|\/+$)/g, '').split('/'), function(name) {
-                return new Filex.PathBit({'text': name, 'path': name});
-            }));
+        load: function(location) {
+            var path = location.replace(/(^\/+|\/+$)/g, '').split('/'),
+                host = path.shift(),
+                pathBits = [new Filex.PathBit({'text': '/', 'path': ''})].concat(_.map(path, function(name) {
+                    return new Filex.PathBit({'text': name, 'path': name});
+                }));
 
+            this.host = host;
             this.path.reset(pathBits);
+
+            this.$host.text(this.host);
         },
 
         reloadFiles: function() {
@@ -321,8 +367,6 @@ $(function(){
         },
 
         resetPath: function(models, options) {
-            this.reloadFiles();
-
             _.each(options.previousModels, function(model) {
                 model.trigger('remove');
             });
@@ -351,9 +395,7 @@ $(function(){
         },
 
         selectedPath: function(bit) {
-            var newPath = this.path.slice(0, this.path.indexOf(bit) + 1);
-            this.path.set(newPath);
-            this.reloadFiles();
+            this.path.reset(this.path.slice(0, this.path.indexOf(bit) + 1));
         },
 
         showHidden: function() {
@@ -401,8 +443,81 @@ $(function(){
             _.each(this.visibleFiles(), function(item) {
                 item.set('checked', false);
             });
+        },
 
-            this.updateSelectAll();
+        toggleFavorites: function() {
+            var $btn = this.$favorites,
+                locations = this.hostSelectize,
+                is_active = $btn.hasClass('active'),
+                url = is_active ? '/filex/fav/delete/' : '/filex/fav/add/',
+                data = {
+                    host: this.host,
+                    path: this.path.full()
+                };
+
+            $btn.button('loading');
+
+            $.post(url, data, 'json').done(function () {
+                $btn.button('reset');
+
+                if (is_active) {
+                    locations.removeOption(data.host + data.path);
+                }
+                else {
+                    locations.addOption({
+                        group: 'usr',
+                        host: data.host,
+                        path: data.path,
+                        value: data.host + data.path
+                    });
+                }
+            }).fail(function() {
+                $btn.button('reset');
+                $btn.button('toggle');
+
+                console.error(arguments);
+            });
+        },
+
+        initialLoad: function() {
+            if (!this.host) {
+                var opts = this.hostSelectize.options;
+
+                this.load(opts[Object.keys(opts)[0]].value);
+            }
+        },
+
+        changedPath: function () {
+            this.reloadFiles();
+            this.updateFavorites();
+        },
+
+        updateFavorites: function() {
+            var loc = this.host + this.path.full(),
+                favorites = this.hostSelectize.options;
+
+            if (favorites.hasOwnProperty(loc)) {
+                if (favorites[loc].group == 'sys') {
+                    this.$favorites.addClass('disabled').prop('disabled', true);
+                    if (this.$favorites.hasClass('active'))
+                        this.$favorites.button('toggle');
+                }
+                else {
+                    this.$favorites.removeClass('disabled').prop('disabled', false);
+                    if (!this.$favorites.hasClass('active'))
+                        this.$favorites.button('toggle');
+                }
+            }
+            else {
+                this.$favorites.removeClass('disabled').prop('disabled', false);
+                if (this.$favorites.hasClass('active'))
+                    this.$favorites.button('toggle');
+            }
+        },
+
+        editHost: function() {
+            $('#host').addClass('edit');
+            this.hostSelectize.focus();
         }
     });
 });
index 4a3b21e..ba88414 100644 (file)
@@ -1,15 +1,24 @@
+{% load filex %}
+
 <div id="filex">
     <nav aria-label="Pasek narzędzi nawigacyjnych">
         <div id="host">
-            <label for="host-selector" class="sr-only">Wybierz host</label>
-            <select id="host-selector"></select>
+            <div class="list">
+                <label for="host-selector" class="sr-only">Wybierz host</label>
+                <select id="host-selector"></select>
+            </div>
+            <button id="btn-host" type="button" class="btn btn-default"></button>
         </div>
 
         <div id="controls">
-            <button id="btn-refresh" type="button" class="btn btn-default" aria-label="Odśwież">
+            <button id="btn-refresh" type="button" class="btn btn-default" title="Odśwież">
                 <span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
             </button>
 
+            <button id="btn-favorites" type="button" class="btn btn-default" title="Ulubione" data-toggle="button" data-loading-text="Zapisywanie..." aria-pressed="false">
+                <span class="glyphicon glyphicon-star" aria-hidden="true"></span>
+            </button>
+
             <div class="btn-group" data-toggle="buttons">
                 <label class="btn btn-default">
                     <input id="show-hidden" type="checkbox" autocomplete="off"> Pokaż ukryte
 
     <div id="spinner-overlay"></div>
 </div>
+
+<form id="favorite-form" action="{% url 'filex:fav_add' %}" class="modal fade form-horizontal" tabindex="-1" role="dialog" aria-labelledby="favorite-modal-label" aria-hidden="true">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+                <h4 class="modal-title" id="favorite-modal-label">Dodaj lokalizację do ulubionych</h4>
+            </div>
+            <div class="modal-body">
+                {% csrf_token %}
+                {% fav_form %}
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-default" data-dismiss="modal">Anuluj</button>
+                <button type="submit" class="btn btn-primary" data-loading-text="Zapisywanie..." data-error-text="Błąd">OK</button>
+            </div>
+        </div>
+    </div>
+</form>
index f7336aa..18cc281 100644 (file)
@@ -1,4 +1,4 @@
-{% load staticfiles %}
+{% load staticfiles filex %}
 
 <script src="{% static 'qcg/selectize/selectize.min.js' %}"></script>
 <script src="{% static 'filex/underscore/underscore.js' %}"></script>
@@ -8,16 +8,7 @@
 <script>
     var filex;
     $(function() {
-        filex = new Filex.FilexView({
-            host: 'qcg.man.poznan.pl',
-            hostOptions: [
-                {host: 'qcg.man.poznan.pl', path: '/home/plgrid/{{ request.user.username }}/reef'},
-                {host: 'ui.grid.icm.edu.pl', path: '/icm/hydra/home/grid/{{ request.user.username }}'},
-                {host: 'ui.plgrid.wcss.wroc.pl', path: '/home/grid/users/{{ request.user.username }}'},
-                {host: 'ui.grid.task.gda.pl', path: '/home/plgrid/{{ request.user.username }}'},
-                {host: 'zeus.cyfronet.pl', path: '/people/{{ request.user.username }}'}
-            ]
-        });
+        filex = new Filex.FilexView({locations: {% locations %}});
     });
 </script>
 
diff --git a/filex/templatetags/__init__.py b/filex/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/filex/templatetags/filex.py b/filex/templatetags/filex.py
new file mode 100644 (file)
index 0000000..e854e49
--- /dev/null
@@ -0,0 +1,37 @@
+import json
+
+from bootstrap3.forms import render_form
+from django import template
+
+from ..forms import FavoriteForm
+
+
+register = template.Library()
+
+
+@register.simple_tag(takes_context=True)
+def locations(context):
+    user = context['request'].user
+
+    result = [
+        {'group': 'sys', 'host': 'moss.man.poznan.pl', 'path': '/home/plg-users/' + user.username},
+        {'group': 'sys', 'host': 'qcg.man.poznan.pl', 'path': '/home/plgrid/' + user.username},
+        {'group': 'sys', 'host': 'ui.grid.icm.edu.pl', 'path': '/icm/hydra/home/grid/' + user.username},
+        {'group': 'sys', 'host': 'ui.plgrid.wcss.wroc.pl', 'path': '/home/grid/users/' + user.username},
+        {'group': 'sys', 'host': 'ui.grid.task.gda.pl', 'path': '/home/plgrid/' + user.username},
+        {'group': 'sys', 'host': 'zeus.cyfronet.pl', 'path': '/people/' + user.username}
+    ]
+
+    for item in user.favorites.values('host', 'path'):
+        item['group'] = 'usr'
+        result.append(item)
+
+    for item in result:
+        item['value'] = item['host'] + item['path']
+
+    return json.dumps(result)
+
+
+@register.simple_tag
+def fav_form():
+    return render_form(FavoriteForm(), layout='horizontal')
index 883b8b4..2f73dec 100644 (file)
@@ -12,4 +12,7 @@ urlpatterns = patterns('',
     url(r'^move/$', views.move, name='move'),
     url(r'^compress/$', views.compress, name='compress'),
     url(r'^extract/$', views.extract, name='extract'),
+
+    url(r'^fav/add/$', views.fav_add, name='fav_add'),
+    url(r'^fav/delete/$', views.fav_delete, name='fav_delete'),
 )
index a85c265..5d23947 100644 (file)
@@ -1,13 +1,16 @@
 import mimetypes
 
+from django.contrib.auth.decorators import login_required
 from django.core.exceptions import PermissionDenied, SuspiciousOperation
 from django.http import JsonResponse, StreamingHttpResponse
+from django.shortcuts import get_object_or_404
 from django.template.defaultfilters import filesizeformat
 from django.utils.formats import date_format
 from django.views.decorators.http import require_POST
-from filex.forms import NewDirForm, RenameForm
 
+from filex.forms import NewDirForm, RenameForm, FavoriteForm
 from filex.ftp import FTPOperation, FTPException
+from filex.models import Favorite
 from filex.uploadhandler import with_ftp_upload_handler
 
 
@@ -231,3 +234,30 @@ def extract(request):
         return JsonResponse({'msg': e.message}, status=400)
     else:
         return JsonResponse({'success': True})
+
+
+@require_POST
+@login_required
+def fav_add(request):
+    data = request.POST.copy()
+    data['owner'] = request.user.id
+
+    form = FavoriteForm(data)
+
+    # TODO check if path exists
+    if form.is_valid():
+        instance = form.save()
+
+        return JsonResponse({'group': 'usr', 'host': instance.host, 'path': instance.path,
+                             'value': instance.host + instance.path})
+
+    return JsonResponse({'msg': form.errors}, status=400)
+
+
+@require_POST
+@login_required
+def fav_delete(request):
+    fav = get_object_or_404(Favorite, owner=request.user, host=request.POST['host'], path=request.POST['path'])
+    fav.delete()
+
+    return JsonResponse({'success': True})
index 1ff595a..692372c 100644 (file)
@@ -95,7 +95,7 @@ textarea {
 }
 
 #spinner-overlay {
-    background: url('/static/filex/spinner.gif') white center 100px no-repeat;
+    background: url('/static/filex/spinner.gif') white  center center no-repeat fixed;
     opacity: 0.5;
     position: absolute;
     top: 0;
@@ -117,6 +117,7 @@ textarea {
 #host {
     flex: 1 auto;
     margin-right: 10px;
+    white-space: nowrap;
 }
 
 #controls {
@@ -131,7 +132,7 @@ textarea {
 @media (min-width: 768px) {
     #host {
         order: 1;
-        flex: 1 auto;
+        flex: 0 auto;
         margin-right: 10px;
     }
 
@@ -147,3 +148,23 @@ textarea {
         flex: 0 auto;
     }
 }
+
+#host .list {
+    display: none;
+}
+
+#host.edit .list {
+    display: initial;
+}
+
+#host.edit #btn-host {
+    display: none;
+}
+
+#host .selectize-control {
+    min-width: 200px;
+}
+
+#host .selectize-input {
+    padding-right: 32px;
+}
index 52e4826..5e3e7a8 100644 (file)
@@ -16,6 +16,8 @@
         $(function () {
             'use strict';
 
+            filex.initialLoad();
+
             var statusTimeout;
 
             String.prototype.endsWith = function(suffix) {
@@ -61,7 +63,7 @@
                     $('#btn-extract').toggleClass('disabled', !is_archive);
                 }
                 else {
-                    $('#btn-extract').toggleClass('disabled', true);
+                    $('#btn-extract').addClass('disabled');
                 }
             });