瀏覽代碼

Merge branch 'dev'

Conflicts:
	setup.py
Denis K 9 年之前
父節點
當前提交
773f3d3867

+ 32 - 0
.travis.yml

@@ -0,0 +1,32 @@
+language: python
+python:
+  - 2.6
+  - 2.7
+  - 3.2
+  - 3.3
+  - 3.4
+  - pypy
+env:
+  - DJANGO=1.6.11
+  - DJANGO=1.7.7
+  - DJANGO=1.8.3
+before_install:
+  - export DJANGO_SETTINGS_MODULE=jet.tests.settings
+install:
+  - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install importlib; fi
+  - pip install -q Django==$DJANGO --use-mirrors
+  - pip install . --use-mirrors
+  - pip install coveralls
+script:
+  - coverage run --source=jet --omit=*/migrations/*,*/south_migrations/*,*/tests/* manage.py test jet
+after_success:
+  - coverage report
+  - coveralls
+matrix:
+  exclude:
+    - python: 2.6
+      env: DJANGO=1.6.11
+    - python: 2.6
+      env: DJANGO=1.7.7
+    - python: 2.6
+      env: DJANGO=1.8.3

+ 7 - 0
CHANGELOG.rst

@@ -1,6 +1,13 @@
 Changelog
 Changelog
 =========
 =========
 
 
+0.0.6
+-----
+
+* [Feature] Added initial unit tests
+* [Fixes] Compatibility fixes
+
+
 0.0.5
 0.0.5
 -----
 -----
 
 

+ 1 - 1
jet/__init__.py

@@ -1 +1 @@
-VERSION = '0.0.5'
+VERSION = '0.0.6'

+ 9 - 8
jet/dashboard.py

@@ -5,6 +5,7 @@ from jet import modules
 from jet.models import UserDashboardModule
 from jet.models import UserDashboardModule
 from django.core.context_processors import csrf
 from django.core.context_processors import csrf
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
+from jet.ordered_set import OrderedSet
 from jet.utils import get_admin_site_name
 from jet.utils import get_admin_site_name
 
 
 
 
@@ -111,23 +112,23 @@ class Dashboard(object):
         return render_to_string('jet/dashboard/dashboard_tools.html', context)
         return render_to_string('jet/dashboard/dashboard_tools.html', context)
 
 
     def media(self):
     def media(self):
-        unique_css = {}
-        unique_js = {}
+        unique_css = OrderedSet()
+        unique_js = OrderedSet()
 
 
         for js in getattr(self.Media, 'js', ()):
         for js in getattr(self.Media, 'js', ()):
-            unique_js[js] = True
+            unique_js.add(js)
         for css in getattr(self.Media, 'css', ()):
         for css in getattr(self.Media, 'css', ()):
-            unique_css[css] = True
+            unique_css.add(css)
 
 
         for module in self.modules:
         for module in self.modules:
             for js in getattr(module.Media, 'js', ()):
             for js in getattr(module.Media, 'js', ()):
-                unique_js[js] = True
+                unique_js.add(js)
             for css in getattr(module.Media, 'css', ()):
             for css in getattr(module.Media, 'css', ()):
-                unique_css[css] = True
+                unique_css.add(css)
 
 
         class Media:
         class Media:
-            css = unique_css.keys()
-            js = unique_js.keys()
+            css = list(unique_css)
+            js = list(unique_js)
 
 
         return Media
         return Media
 
 

+ 12 - 4
jet/forms.py

@@ -22,6 +22,14 @@ class AddBookmarkForm(forms.ModelForm):
         model = Bookmark
         model = Bookmark
         fields = ['url', 'title']
         fields = ['url', 'title']
 
 
+    def clean(self):
+        data = super(AddBookmarkForm, self).clean()
+        if not self.request.user.is_authenticated():
+            raise ValidationError('error')
+        if not self.request.user.has_perm('jet.change_bookmark'):
+            raise ValidationError('error')
+        return data
+
     def save(self, commit=True):
     def save(self, commit=True):
         self.instance.user = self.request.user.pk
         self.instance.user = self.request.user.pk
         return super(AddBookmarkForm, self).save(commit)
         return super(AddBookmarkForm, self).save(commit)
@@ -37,12 +45,12 @@ class RemoveBookmarkForm(forms.ModelForm):
         fields = []
         fields = []
 
 
     def clean(self):
     def clean(self):
-        cleaned_data = super(RemoveBookmarkForm, self).clean()
-
+        data = super(RemoveBookmarkForm, self).clean()
+        if not self.request.user.is_authenticated():
+            raise ValidationError('error')
         if self.instance.user != self.request.user.pk:
         if self.instance.user != self.request.user.pk:
             raise ValidationError('error')
             raise ValidationError('error')
-
-        return cleaned_data
+        return data
 
 
     def save(self, commit=True):
     def save(self, commit=True):
         if commit:
         if commit:

+ 4 - 4
jet/modules.py

@@ -177,11 +177,11 @@ class AppList(DashboardModule):
 
 
         for app in app_list:
         for app in app_list:
             app['models'] = filter(
             app['models'] = filter(
-                lambda model: self.models is None or model['object_name'] in self.models or app['app_label'] + '.*' in self.models,
+                lambda model: self.models is None or model['object_name'] in self.models or app.get('app_label', app.get('name')) + '.*' in self.models,
                 app['models']
                 app['models']
             )
             )
             app['models'] = filter(
             app['models'] = filter(
-                lambda model: self.exclude is None or model['object_name'] not in self.exclude and app['app_label'] + '.*' not in self.exclude,
+                lambda model: self.exclude is None or model['object_name'] not in self.exclude and app.get('app_label', app.get('name')) + '.*' not in self.exclude,
                 app['models']
                 app['models']
             )
             )
             app['models'] = list(app['models'])
             app['models'] = list(app['models'])
@@ -218,11 +218,11 @@ class ModelList(DashboardModule):
 
 
         for app in app_list:
         for app in app_list:
             app['models'] = filter(
             app['models'] = filter(
-                lambda model: self.models is None or model['object_name'] in self.models or app['app_label'] + '.*' in self.models,
+                lambda model: self.models is None or model['object_name'] in self.models or app.get('app_label', app.get('name')) + '.*' in self.models,
                 app['models']
                 app['models']
             )
             )
             app['models'] = filter(
             app['models'] = filter(
-                lambda model: self.exclude is None or model['object_name'] not in self.exclude and app['app_label'] + '.*' not in self.exclude,
+                lambda model: self.exclude is None or model['object_name'] not in self.exclude and app.get('app_label', app.get('name')) + '.*' not in self.exclude,
                 app['models']
                 app['models']
             )
             )
             app['models'] = list(app['models'])
             app['models'] = list(app['models'])

+ 67 - 0
jet/ordered_set.py

@@ -0,0 +1,67 @@
+import collections
+
+
+class OrderedSet(collections.MutableSet):
+    def __init__(self, iterable=None):
+        self.end = end = []
+        end += [None, end, end]         # sentinel node for doubly linked list
+        self.map = {}                   # key --> [key, prev, next]
+        if iterable is not None:
+            self |= iterable
+
+    def __len__(self):
+        return len(self.map)
+
+    def __contains__(self, key):
+        return key in self.map
+
+    def add(self, key):
+        if key not in self.map:
+            end = self.end
+            curr = end[1]
+            curr[2] = end[1] = self.map[key] = [key, curr, end]
+
+    def discard(self, key):
+        if key in self.map:
+            key, prev, next = self.map.pop(key)
+            prev[2] = next
+            next[1] = prev
+
+    def __iter__(self):
+        end = self.end
+        curr = end[2]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[2]
+
+    def __reversed__(self):
+        end = self.end
+        curr = end[1]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[1]
+
+    def pop(self, last=True):
+        if not self:
+            raise KeyError('set is empty')
+        key = self.end[1][0] if last else self.end[2][0]
+        self.discard(key)
+        return key
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, list(self))
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedSet):
+            return len(self) == len(other) and list(self) == list(other)
+        return set(self) == set(other)
+
+
+if __name__ == '__main__':
+    s = OrderedSet('abracadaba')
+    t = OrderedSet('simsalabim')
+    print(s | t)
+    print(s & t)
+    print(s - t)

+ 1 - 1
jet/templates/admin/change_form.html

@@ -32,7 +32,7 @@
                             {% block object-tools-items %}
                             {% block object-tools-items %}
                                 <li>
                                 <li>
                                     {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
                                     {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
-                                    <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
+                                    <a href="{% jet_add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
                                 </li>
                                 </li>
                                 {% if has_absolute_url %}
                                 {% if has_absolute_url %}
                                     <li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>
                                     <li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>

+ 2 - 2
jet/templates/admin/submit_line.html

@@ -1,4 +1,4 @@
-{% load i18n admin_urls %}
+{% load i18n admin_urls jet_tags %}
 
 
 <div class="submit-row">
 <div class="submit-row">
     {% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
     {% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
@@ -8,6 +8,6 @@
 
 
     {% if show_delete_link %}
     {% if show_delete_link %}
         {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
         {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
-        <p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink button button-red">{% trans "Delete" %}</a></p>
+        <p class="deletelink-box"><a href="{% jet_add_preserved_filters delete_url %}" class="deletelink button button-red">{% trans "Delete" %}</a></p>
     {% endif %}
     {% endif %}
 </div>
 </div>

+ 10 - 10
jet/templatetags/jet_tags.py

@@ -1,16 +1,12 @@
-from distutils.version import StrictVersion
+from __future__ import unicode_literals
 from django import template
 from django import template
-import django
-from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.db.models import OneToOneField
 from django.db.models import OneToOneField
 from django.forms import CheckboxInput, ModelChoiceField, Select, ModelMultipleChoiceField, SelectMultiple
 from django.forms import CheckboxInput, ModelChoiceField, Select, ModelMultipleChoiceField, SelectMultiple
-from django.utils.encoding import smart_text
 from django.utils.formats import get_format
 from django.utils.formats import get_format
 from django.template import loader, Context
 from django.template import loader, Context
 from jet import settings
 from jet import settings
 from jet.models import Bookmark, PinnedApplication
 from jet.models import Bookmark, PinnedApplication
-from django.utils.translation import ugettext_lazy as _
 import re
 import re
 from jet.utils import get_app_list, get_model_instance_label, get_current_dashboard
 from jet.utils import get_app_list, get_model_instance_label, get_current_dashboard
 
 
@@ -48,7 +44,7 @@ class FormatBreadcrumbsNode(template.Node):
 
 
         regex = re.compile('<[^!(a>)]([^>]|\n)*[^!(/a)]>', re.IGNORECASE)
         regex = re.compile('<[^!(a>)]([^>]|\n)*[^!(/a)]>', re.IGNORECASE)
         clean = re.sub(regex, '', output)
         clean = re.sub(regex, '', output)
-        clean = clean.replace(u'\u203A', '&rsaquo;')
+        clean = clean.replace('\u203A', '&rsaquo;')
         items = clean.split('&rsaquo;')
         items = clean.split('&rsaquo;')
 
 
         items = map(lambda i: i.strip(), items)
         items = map(lambda i: i.strip(), items)
@@ -150,7 +146,7 @@ def get_menu(context):
                 app['current'] = True
                 app['current'] = True
                 current_found = True
                 current_found = True
 
 
-        if app['app_label'] in pinned:
+        if app.get('app_label', app.get('name')) in pinned:
             pinned_apps.append(app)
             pinned_apps.append(app)
         else:
         else:
             apps.append(app)
             apps.append(app)
@@ -213,9 +209,13 @@ def select2_lookups(field):
 @register.simple_tag(takes_context=True)
 @register.simple_tag(takes_context=True)
 def jet_add_preserved_filters(context, url, popup=False, to_field=None):
 def jet_add_preserved_filters(context, url, popup=False, to_field=None):
     try:
     try:
-        return add_preserved_filters(context, url, popup, to_field)
-    except TypeError:
-        return add_preserved_filters(context, url, popup)  # old django
+        from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
+        try:
+            return add_preserved_filters(context, url, popup, to_field)
+        except TypeError:
+            return add_preserved_filters(context, url, popup)  # old django
+    except ImportError:
+        return url
 
 
 
 
 @register.assignment_tag(takes_context=True)
 @register.assignment_tag(takes_context=True)

+ 0 - 3
jet/tests.py

@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.

+ 3 - 0
jet/tests/__init__.py

@@ -0,0 +1,3 @@
+from jet.tests.test_dashboard import DashboardTestCase
+from jet.tests.test_utils import UtilsTestCase
+from jet.tests.test_views import ViewsTestCase

+ 33 - 0
jet/tests/dashboard.py

@@ -0,0 +1,33 @@
+from jet import modules
+from jet.dashboard import Dashboard
+
+
+class TestIndexDashboard(Dashboard):
+    columns = 3
+    init_with_context_called = False
+
+    class Media:
+        js = ('file.js', 'file2.js')
+        css = ('file.css', 'file2.css')
+
+    def init_with_context(self, context):
+        self.init_with_context_called = True
+        self.available_children.append(modules.LinkList)
+        self.available_children.append(modules.Feed)
+
+        # append a recent actions module
+        self.children.append(modules.RecentActions(
+            'Recent Actions',
+            10,
+            column=0,
+            order=1
+        ))
+
+        # append a feed module
+        self.children.append(modules.Feed(
+            'Latest Django News',
+            feed_url='http://www.djangoproject.com/rss/weblog/',
+            limit=5,
+            column=1,
+            order=1
+        ))

+ 11 - 0
jet/tests/models.py

@@ -0,0 +1,11 @@
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class TestModel(models.Model):
+    field1 = models.CharField(max_length=255)
+    field2 = models.IntegerField()
+
+    def __str__(self):
+        return '%s%d' % (self.field1, self.field2)

+ 52 - 0
jet/tests/settings.py

@@ -0,0 +1,52 @@
+from django.conf import global_settings
+
+SECRET_KEY = '!DJANGO_JET_TESTS!'
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+DEBUG_PROPAGATE_EXCEPTIONS = True
+
+ROOT_URLCONF = 'jet.tests.urls'
+
+INSTALLED_APPS = (
+    'jet',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.messages',
+    'django.contrib.admin',
+    'jet.tests',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
+    'django.core.context_processors.request',
+)
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+    }
+}
+
+TIME_ZONE = 'UTC'
+LANGUAGE_CODE = 'en-US'
+USE_I18N = True
+USE_L10N = True
+
+MEDIA_ROOT = ''
+MEDIA_URL = ''
+
+STATIC_URL = '/static/'
+
+
+
+

+ 65 - 0
jet/tests/test_dashboard.py

@@ -0,0 +1,65 @@
+from django.contrib.auth.models import User
+from django.test import TestCase, Client
+from jet.modules import LinkList, RecentActions
+from jet.models import UserDashboardModule
+from jet.tests.dashboard import TestIndexDashboard
+
+
+class DashboardTestCase(TestCase):
+    class Request:
+        def __init__(self, user):
+            self.user = user
+
+    def setUp(self):
+        self._login()
+        self._init_dashboard()
+
+    def _login(self):
+        username = 'admin'
+        email = 'admin@example.com'
+        password = 'admin'
+        self.admin = Client()
+        self.admin_user = User.objects.create_superuser(username, email, password)
+        return self.admin.login(username=username, password=password)
+
+    def _init_dashboard(self):
+        UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.LinkList',
+            app_label=None,
+            user=self.admin_user.pk,
+            column=0,
+            order=0
+        )
+        UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.RecentActions',
+            app_label=None,
+            user=self.admin_user.pk,
+            column=0,
+            order=1
+        )
+        self.dashboard = TestIndexDashboard({'request': self.Request(self.admin_user)})
+
+    def test_custom_columns(self):
+        self.assertEqual(self.dashboard.columns, 3)
+
+    def test_init_with_context_called(self):
+        self.assertTrue(self.dashboard.init_with_context_called)
+
+    def test_load_modules(self):
+        self.assertEqual(len(self.dashboard.modules), 2)
+        self.assertTrue(isinstance(self.dashboard.modules[0], LinkList))
+        self.assertTrue(isinstance(self.dashboard.modules[1], RecentActions))
+
+    def test_media(self):
+        media = self.dashboard.media()
+        self.assertEqual(len(media.js), 2)
+        self.assertEqual(media.js[0], 'file.js')
+        self.assertEqual(media.js[1], 'file2.js')
+        self.assertEqual(len(media.css), 2)
+        self.assertEqual(media.css[0], 'file.css')
+        self.assertEqual(media.css[1], 'file2.css')
+
+
+

+ 22 - 0
jet/tests/test_utils.py

@@ -0,0 +1,22 @@
+import json
+from django.test import TestCase
+from jet.tests.models import TestModel
+from jet.utils import JsonResponse, get_model_instance_label
+
+
+class UtilsTestCase(TestCase):
+    def test_json_response(self):
+        response = JsonResponse({'str': 'string', 'int': 1})
+        response_dict = json.loads(response.content.decode())
+        expected_dict = {"int": 1, "str": "string"}
+        self.assertEqual(response_dict, expected_dict)
+        self.assertEqual(response.get('Content-Type'), 'application/json')
+
+    def test_get_model_instance_label(self):
+        field1 = 'value'
+        field2 = 2
+        pinned_application = TestModel.objects.create(field1=field1, field2=field2)
+        self.assertEqual(get_model_instance_label(pinned_application), '%s%d' % (field1, field2))
+
+
+

+ 213 - 0
jet/tests/test_views.py

@@ -0,0 +1,213 @@
+import json
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import TestCase, Client
+from jet.modules import LinkList
+from jet.models import UserDashboardModule, Bookmark
+
+
+class ViewsTestCase(TestCase):
+    def setUp(self):
+        self.assertTrue(self._login())
+
+    def _login(self):
+        username = 'admin'
+        email = 'admin@example.com'
+        password = 'admin'
+        self.admin = Client()
+        self.admin_user = User.objects.create_superuser(username, email, password)
+        return self.admin.login(username=username, password=password)
+
+    def test_module_update_view(self):
+        title = 'Quick links Test'
+        new_title = title + '2'
+        new_layout = 'stacked'
+        module = UserDashboardModule.objects.create(
+            title=title,
+            module='jet.modules.LinkList',
+            app_label=None,
+            user=self.admin_user.pk,
+            column=0,
+            order=0,
+            settings='{"layout": "inline"}',
+            children='[]'
+        )
+
+        response = self.admin.get(reverse('jet:update_module', kwargs={'pk': module.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(response.context['module'] is not None)
+        self.assertTrue(isinstance(response.context['module'], LinkList))
+        self.assertEqual(response.context['module'].title, title)
+
+        post = {
+            'title': new_title,
+            'layout': new_layout,
+            'children-TOTAL_FORMS': '2',
+            'children-INITIAL_FORMS': '1',
+            'children-MIN_NUM_FORMS': '0',
+            'children-MAX_NUM_FORMS': '1000',
+            'children-0-url': 'http://docs.djangoproject.com/',
+            'children-0-title': 'Django documentation',
+            'children-0-external': 'on',
+            'children-1-url': '',
+            'children-1-title': '',
+            'children-__prefix__-url': '',
+            'children-__prefix__-title': '',
+            '_save': 'Save'
+        }
+
+        self.admin.post(reverse('jet:update_module', kwargs={'pk': module.pk}), post)
+        self.assertEqual(response.status_code, 200)
+        module = UserDashboardModule.objects.get(pk=module.pk)
+        settings = json.loads(module.settings)
+        self.assertEqual(module.title, new_title)
+        self.assertEqual(settings['layout'], new_layout)
+
+        module.delete()
+
+    def test_add_bookmark_view(self):
+        url = 'http://test.com/'
+        title = 'Title'
+        response = self.admin.post(reverse('jet:add_bookmark'), {'url': url, 'title': title})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertNotEqual(response['id'], None)
+        bookmark = Bookmark.objects.get(pk=response['id'])
+        self.assertNotEqual(bookmark, None)
+        self.assertEqual(bookmark.title, title)
+        self.assertEqual(bookmark.url, url)
+
+    def test_add_bookmark_view_unauthorized(self):
+        url = 'http://test.com/'
+        title = 'Title'
+        response = self.client.post(reverse('jet:add_bookmark'), {'url': url, 'title': title})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertTrue(response['error'])
+
+    def test_remove_bookmark_view(self):
+        url = 'http://test.com/'
+        title = 'Title'
+        bookmark = Bookmark.objects.create(url=url, title=title, user=self.admin_user.pk)
+        response = self.admin.post(reverse('jet:remove_bookmark'), {'id': bookmark.id})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertFalse(Bookmark.objects.filter(pk=bookmark.pk).exists())
+
+    def test_toggle_application_pin_view(self):
+        app_label = 'test_app'
+
+        response = self.admin.post(reverse('jet:toggle_application_pin'), {'app_label': app_label})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertTrue(response['pinned'])
+
+        response = self.admin.post(reverse('jet:toggle_application_pin'), {'app_label': app_label})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertFalse(response['pinned'])
+
+    def test_update_dashboard_modules_view(self):
+        app_label = None
+        module_0 = UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.LinkList',
+            app_label=app_label,
+            user=self.admin_user.pk,
+            column=0,
+            order=0
+        )
+        module_1 = UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.LinkList',
+            app_label=app_label,
+            user=self.admin_user.pk,
+            column=0,
+            order=1
+        )
+        response = self.admin.post(reverse('jet:update_dashboard_modules'), {
+            'app_label': '',
+            'modules': json.dumps([
+                {'id': module_0.pk, 'column': 0, 'order': 1},
+                {'id': module_1.pk, 'column': 0, 'order': 0}
+            ])
+        })
+
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+
+        module_0 = UserDashboardModule.objects.get(pk=module_0.pk)
+        module_1 = UserDashboardModule.objects.get(pk=module_1.pk)
+
+        self.assertEqual(module_0.order, 1)
+        self.assertEqual(module_1.order, 0)
+
+        module_0.delete()
+        module_1.delete()
+
+    def test_add_user_dashboard_module_view(self):
+        response = self.admin.post(reverse('jet:add_user_dashboard_module'), {
+            'app_label': '',
+            'type': 'available_children',
+            'module': 0
+        })
+
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertNotEqual(response['id'], None)
+        module = UserDashboardModule.objects.get(pk=response['id'])
+        self.assertNotEqual(module, None)
+
+    def test_update_dashboard_module_collapse_view(self):
+        module = UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.LinkList',
+            app_label=None,
+            user=self.admin_user.pk,
+            column=0,
+            order=0
+        )
+        response = self.admin.post(reverse('jet:update_dashboard_module_collapse'), {
+            'id': module.pk, 'collapsed': True
+        })
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertTrue(response['collapsed'])
+
+        module = UserDashboardModule.objects.get(pk=module.pk)
+        response = self.admin.post(reverse('jet:update_dashboard_module_collapse'), {
+            'id': module.pk, 'collapsed': False
+        })
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertFalse(response['collapsed'])
+
+        module.delete()
+
+    def test_remove_dashboard_module_view(self):
+        module = UserDashboardModule.objects.create(
+            title='',
+            module='jet.modules.LinkList',
+            app_label=None,
+            user=self.admin_user.pk,
+            column=0,
+            order=0
+        )
+        response = self.admin.post(reverse('jet:remove_dashboard_module'), {'id': module.pk})
+        self.assertEqual(response.status_code, 200)
+        response = json.loads(response.content.decode())
+        self.assertFalse(response['error'])
+        self.assertFalse(UserDashboardModule.objects.filter(pk=module.pk).exists())
+
+
+
+
+

+ 11 - 0
jet/tests/urls.py

@@ -0,0 +1,11 @@
+from django.conf.urls import patterns, include, url
+from django.contrib import admin
+
+admin.autodiscover()
+
+urlpatterns = patterns(
+    '',
+    url(r'^jet/', include('jet.urls', 'jet')),
+    url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+    url(r'^admin/', include(admin.site.urls)),
+)

+ 18 - 0
jet/utils.py

@@ -9,6 +9,7 @@ from django.utils.encoding import force_text
 from django.utils.encoding import smart_text
 from django.utils.encoding import smart_text
 from django.utils.functional import Promise
 from django.utils.functional import Promise
 from jet import settings
 from jet import settings
+from django.contrib import messages
 
 
 
 
 class JsonResponse(HttpResponse):
 class JsonResponse(HttpResponse):
@@ -83,3 +84,20 @@ def get_model_instance_label(instance):
     if getattr(instance, "related_label", None):
     if getattr(instance, "related_label", None):
         return instance.related_label()
         return instance.related_label()
     return smart_text(instance)
     return smart_text(instance)
+
+
+class SuccessMessageMixin(object):
+    """
+    Adds a success message on successful form submission.
+    """
+    success_message = ''
+
+    def form_valid(self, form):
+        response = super(SuccessMessageMixin, self).form_valid(form)
+        success_message = self.get_success_message(form.cleaned_data)
+        if success_message:
+            messages.success(self.request, success_message)
+        return response
+
+    def get_success_message(self, cleaned_data):
+        return self.success_message % cleaned_data

+ 5 - 5
jet/views.py

@@ -1,12 +1,11 @@
 from django.contrib import messages
 from django.contrib import messages
-from django.contrib.messages.views import SuccessMessageMixin
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.forms.formsets import formset_factory
 from django.forms.formsets import formset_factory
 from django.views.decorators.http import require_POST, require_GET
 from django.views.decorators.http import require_POST, require_GET
 from jet.forms import AddBookmarkForm, RemoveBookmarkForm, ToggleApplicationPinForm, UpdateDashboardModulesForm, \
 from jet.forms import AddBookmarkForm, RemoveBookmarkForm, ToggleApplicationPinForm, UpdateDashboardModulesForm, \
     AddUserDashboardModuleForm, UpdateDashboardModuleCollapseForm, RemoveDashboardModuleForm, ModelLookupForm
     AddUserDashboardModuleForm, UpdateDashboardModuleCollapseForm, RemoveDashboardModuleForm, ModelLookupForm
 from jet.models import Bookmark, UserDashboardModule
 from jet.models import Bookmark, UserDashboardModule
-from jet.utils import JsonResponse, get_app_list
+from jet.utils import JsonResponse, get_app_list, SuccessMessageMixin
 from django.views.generic import UpdateView
 from django.views.generic import UpdateView
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
@@ -75,7 +74,7 @@ class UpdateDashboardModuleView(SuccessMessageMixin, UpdateView):
         app_list = get_app_list({'request': self.request})
         app_list = get_app_list({'request': self.request})
 
 
         for app in app_list:
         for app in app_list:
-            if app['app_label'] == self.object.app_label:
+            if app.get('app_label', app.get('name')) == self.object.app_label:
                 return app
                 return app
 
 
     def get_context_data(self, **kwargs):
     def get_context_data(self, **kwargs):
@@ -105,14 +104,14 @@ class UpdateDashboardModuleView(SuccessMessageMixin, UpdateView):
                 settings = settings_form.cleaned_data
                 settings = settings_form.cleaned_data
                 data['settings'] = self.module.dump_settings(settings)
                 data['settings'] = self.module.dump_settings(settings)
             else:
             else:
-                return self.form_invalid(self.get_form())
+                return self.form_invalid(self.get_form(self.get_form_class()))
 
 
         if children_formset:
         if children_formset:
             if children_formset.is_valid():
             if children_formset.is_valid():
                 self.module.children = self.clean_children_data(children_formset.cleaned_data)
                 self.module.children = self.clean_children_data(children_formset.cleaned_data)
                 data['children'] = self.module.dump_children()
                 data['children'] = self.module.dump_children()
             else:
             else:
-                return self.form_invalid(self.get_form())
+                return self.form_invalid(self.get_form(self.get_form_class()))
 
 
         request.POST = data
         request.POST = data
 
 
@@ -192,6 +191,7 @@ def add_user_dashboard_module_view(request):
 
 
     if form.is_valid():
     if form.is_valid():
         module = form.save()
         module = form.save()
+        result['id'] = module.pk
         messages.success(request, _('Widget has been successfully added'))
         messages.success(request, _('Widget has been successfully added'))
 
 
         if module.app_label:
         if module.app_label:

+ 10 - 0
manage.py

@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jet.tests.settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

+ 1 - 1
setup.py

@@ -12,7 +12,7 @@ setup(
     long_description=read('README.rst'),
     long_description=read('README.rst'),
     author='Denis Kildishev',
     author='Denis Kildishev',
     author_email='support@jet.geex-arts.com',
     author_email='support@jet.geex-arts.com',
-    url='https://github.com/geex-arts/jet',
+    url='https://github.com/geex-arts/django-jet',
     packages=find_packages(),
     packages=find_packages(),
     license='Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License',
     license='Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License',
     classifiers=[
     classifiers=[