├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bower.json ├── demo ├── db.sqlite3 ├── demo │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_showcase.py │ │ ├── 0003_showcase_textfield.py │ │ ├── 0004_showcase_readonly_field.py │ │ ├── 0005_showcase_time_only.py │ │ ├── 0006_auto_20160303_1857.py │ │ ├── 0007_auto_20160303_1859.py │ │ ├── 0008_auto_20160305_1055.py │ │ ├── 0009_showcase_collapsed_param.py │ │ ├── 0010_auto_20170211_2037.py │ │ ├── 0011_auto_20170211_2156.py │ │ ├── 0012_auto_20170407_1131.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── static │ │ ├── admin │ │ │ └── js │ │ │ │ └── jquery.init.js │ │ └── css │ │ │ └── demo.css │ ├── templates │ │ └── admin │ │ │ ├── base_site.html │ │ │ ├── custom_view.html │ │ │ ├── demo │ │ │ ├── continent │ │ │ │ └── change_list.html │ │ │ ├── country │ │ │ │ ├── tab_charts.html │ │ │ │ ├── tab_docs.html │ │ │ │ ├── tab_flag.html │ │ │ │ └── tab_notice.html │ │ │ └── showcase │ │ │ │ ├── change_form.html │ │ │ │ └── submit_line.html │ │ │ └── login.html │ ├── templatetags │ │ ├── __init__.py │ │ └── demo_tags.py │ ├── urls.py │ ├── views.py │ ├── widgets.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── docs ├── Makefile ├── conf.py ├── configure.rst ├── contribute.rst ├── index.rst └── install.rst ├── gulpfile.js ├── package.json ├── requirements-dev.txt ├── setup.py └── suit ├── .DS_Store ├── __init__.py ├── admin.py ├── admin_filters.py ├── apps.py ├── compat.py ├── config.py ├── menu.py ├── sass ├── _mixins.scss ├── _variables.scss ├── _vendor.scss ├── components │ ├── _alerts.scss │ ├── _breadcrumbs.scss │ ├── _buttons.scss │ ├── _cards.scss │ ├── _confirmations.scss │ ├── _forms.scss │ ├── _icons.scss │ ├── _results.scss │ ├── _sortables.scss │ ├── _submit_row.scss │ ├── _tables.scss │ ├── _tabs.scss │ └── _widgets.scss ├── layout │ ├── _content.scss │ ├── _footer.scss │ ├── _header.scss │ ├── _navbars.scss │ └── _vertical.scss ├── pages │ ├── _changeform.scss │ ├── _changelist.scss │ ├── _dashboard.scss │ └── _login.scss └── suit.scss ├── sortables.py ├── static ├── admin │ └── css │ │ ├── changelists.css │ │ ├── dashboard.css │ │ ├── forms.css │ │ └── login.css └── suit │ ├── css │ ├── font-awesome.min.css │ └── suit.css │ ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 │ └── js │ ├── autosize.min.js │ ├── suit.js │ └── suit.sortables.js ├── template.py ├── templates ├── admin │ ├── auth │ │ └── user │ │ │ └── add_form.html │ ├── base.html │ ├── base_site.html │ ├── change_form.html │ ├── change_list.html │ ├── change_list_results.html │ ├── filter_horizontal.html │ ├── includes │ │ └── fieldset.html │ └── login.html ├── registration │ └── password_change_form.html └── suit │ ├── change_form_includes.html │ ├── menu.html │ ├── menu_item.html │ └── search_form.html ├── templatetags ├── __init__.py ├── suit_forms.py ├── suit_list.py ├── suit_menu.py └── suit_tags.py ├── tests ├── __init__.py ├── settings.py ├── test_menu.py ├── test_routes.py └── urls.py └── widgets.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.egg* 3 | *.swp 4 | *~ 5 | *.log 6 | .idea 7 | docs/_build/ 8 | /build 9 | /dist/ 10 | /bower_components/ 11 | /node_modules/ 12 | /env/ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | - pypy 8 | env: 9 | - DJANGO=1.11 10 | - DJANGO=2.0 11 | install: 12 | - pip install -e . 13 | - pip install -q Django==$DJANGO 14 | script: 15 | - DJANGO_SETTINGS_MODULE=suit.tests.settings django-admin test suit 16 | matrix: 17 | exclude: 18 | - python: 2.7 19 | env: DJANGO=2.0 20 | - python: pypy 21 | env: DJANGO=2.0 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include docs * 4 | recursive-include suit/static * 5 | recursive-include suit/templates * 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Django Suit 3 | =========== 4 | 5 | **Modern theme for Django admin interface**. 6 | 7 | Django Suit is alternative theme/skin/extension for `Django `_ administration interface. 8 | 9 | * Project home: http://djangosuit.com/ 10 | * Live demo v1: http://djangosuit.com/admin/ 11 | * Live demo v2.0 alpha 1: http://v2.djangosuit.com/admin/ 12 | 13 | 14 | License 15 | ======= 16 | 17 | * Django Suit is licensed under `Creative Commons Attribution-NonCommercial 3.0 `_ license. 18 | * Licence and pricing: http://djangosuit.com/pricing/ 19 | 20 | 21 | Docs & Support 22 | ============== 23 | 24 | * Documentation v2: http://django-suit.readthedocs.org/en/v2/ 25 | * Documentation v1: http://django-suit.readthedocs.org/en/latest/ 26 | * Support: http://djangosuit.com/support/ 27 | * Follow `on Twitter `_ to get latest news 28 | 29 | 30 | Changelog 31 | ========= 32 | 33 | **Note:** Django Suit v2.0 is in active development and not yet ready for production use. 34 | 35 | Read more here: Todo: Add issue refernce 36 | 37 | 38 | Contributing 39 | ============ 40 | 41 | See `Contributing documentation `_ 42 | 43 | 44 | Build Status 45 | ============ 46 | 47 | Django Suit uses Travis CI to perform tests on different Django and Python versions. 48 | 49 | Tested using Python: 2.7-3.4 and PyPy. Django: 1.9+ and Django Suit v2.0 alpha: 50 | 51 | .. |v2| image:: https://travis-ci.org/darklow/django-suit.png?branch=v2 52 | :alt: Build Status - v2 branch 53 | :target: http://travis-ci.org/darklow/django-suit 54 | 55 | .. |develop| image:: https://travis-ci.org/darklow/django-suit.png?branch=develop 56 | :alt: Build Status - develop branch 57 | :target: http://travis-ci.org/darklow/django-suit 58 | 59 | |v2| |develop| 60 | 61 | 62 | Preview 63 | ======= 64 | 65 | 66 | .. image:: https://cloud.githubusercontent.com/assets/445304/12699480/3eee898e-c7c5-11e5-931c-ba1b0cabdecb.png 67 | :alt: Django Suit Preview 68 | :target: http://v2.djangosuit.com/admin/ 69 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-suit-contrib", 3 | "homepage": "https://github.com/darklow/django-suit", 4 | "authors": [ 5 | "Kaspars Sprogis " 6 | ], 7 | "description": "Django Suit development dependendencies", 8 | "private": true, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "devDependencies": { 17 | "bootstrap": "v4.0.0-alpha.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/demo/db.sqlite3 -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/apps.py: -------------------------------------------------------------------------------- 1 | from suit.apps import DjangoSuitConfig 2 | from suit.menu import ParentItem, ChildItem 3 | 4 | 5 | class SuitConfig(DjangoSuitConfig): 6 | menu = ( 7 | ParentItem('Content', children=[ 8 | ChildItem(model='demo.country'), 9 | ChildItem(model='demo.continent'), 10 | ChildItem(model='demo.showcase'), 11 | ChildItem('Custom view', url='/admin/custom/'), 12 | ], icon='fa fa-leaf'), 13 | ParentItem('Integrations', children=[ 14 | ChildItem(model='demo.city'), 15 | ]), 16 | ParentItem('Users', children=[ 17 | ChildItem(model='auth.user'), 18 | ChildItem('User groups', 'auth.group'), 19 | ], icon='fa fa-users'), 20 | ParentItem('Right Side Menu', children=[ 21 | ChildItem('Password change', url='admin:password_change'), 22 | ChildItem('Open Google', url='http://google.com', target_blank=True), 23 | 24 | ], align_right=True, icon='fa fa-cog'), 25 | ) 26 | 27 | def ready(self): 28 | super(SuitConfig, self).ready() 29 | 30 | # DO NOT COPY FOLLOWING LINE 31 | # It is only to prevent updating last_login in DB for demo app 32 | self.prevent_user_last_login() 33 | 34 | def prevent_user_last_login(self): 35 | """ 36 | Disconnect last login signal 37 | """ 38 | from django.contrib.auth import user_logged_in 39 | from django.contrib.auth.models import update_last_login 40 | user_logged_in.disconnect(update_last_login) 41 | -------------------------------------------------------------------------------- /demo/demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-30 18:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Continent', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=256)), 22 | ('order', models.PositiveIntegerField()), 23 | ], 24 | options={ 25 | 'ordering': ('name',), 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='Country', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('name', models.CharField(max_length=256)), 33 | ('code', models.CharField(help_text=b'ISO 3166-1 alpha-2 - two character country code', max_length=2)), 34 | ('independence_day', models.DateField(blank=True, null=True)), 35 | ('area', models.BigIntegerField(blank=True, null=True)), 36 | ('population', models.BigIntegerField(blank=True, null=True)), 37 | ('order', models.PositiveIntegerField(default=0)), 38 | ('description', models.TextField(blank=True, help_text=b'Try and enter few some more lines')), 39 | ('architecture', models.TextField(blank=True)), 40 | ('continent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='demo.Continent')), 41 | ], 42 | options={ 43 | 'ordering': ('name',), 44 | 'verbose_name_plural': 'Countries', 45 | }, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /demo/demo/migrations/0002_showcase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-18 09:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Showcase', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=64)), 20 | ('help_text', models.CharField(help_text=b'Enter fully qualified name', max_length=64)), 21 | ('multiple_in_row', models.CharField(help_text=b'Help text for multiple', max_length=64)), 22 | ('multiple2', models.CharField(blank=True, max_length=10)), 23 | ('date', models.DateField(blank=True, null=True)), 24 | ('date_and_time', models.DateTimeField(blank=True, null=True)), 25 | ('date_widget', models.DateField(blank=True, null=True)), 26 | ('datetime_widget', models.DateTimeField(blank=True, null=True)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /demo/demo/migrations/0003_showcase_textfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-18 09:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0002_showcase'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='showcase', 17 | name='textfield', 18 | field=models.TextField(blank=True, help_text=b'Try and enter few some more lines', verbose_name=b'Autosized textfield'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/demo/migrations/0004_showcase_readonly_field.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-18 09:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0003_showcase_textfield'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='showcase', 17 | name='readonly_field', 18 | field=models.CharField(default=b'Some value here', max_length=127), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/demo/migrations/0005_showcase_time_only.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-02-18 09:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0004_showcase_readonly_field'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='showcase', 17 | name='time_only', 18 | field=models.TimeField(blank=True, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/demo/migrations/0006_auto_20160303_1857.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-03-03 16:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('demo', '0005_showcase_time_only'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Book', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=64)), 21 | ('rating', models.SmallIntegerField(choices=[(1, b'Awesome'), (2, b'Good'), (3, b'Normal'), (4, b'Bad')], help_text=b'Choose wisely')), 22 | ('is_public', models.BooleanField(default=False)), 23 | ('order', models.PositiveIntegerField()), 24 | ], 25 | options={ 26 | 'ordering': ('order',), 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='Movie', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('title', models.CharField(max_length=64)), 34 | ('rating', models.SmallIntegerField(choices=[(1, b'Awesome'), (2, b'Good'), (3, b'Normal'), (4, b'Bad')], default=2)), 35 | ('description', models.TextField(blank=True)), 36 | ('is_public', models.BooleanField(default=False)), 37 | ('order', models.PositiveIntegerField()), 38 | ], 39 | options={ 40 | 'ordering': ('order',), 41 | }, 42 | ), 43 | migrations.AlterModelOptions( 44 | name='showcase', 45 | options={'verbose_name_plural': 'Showcase'}, 46 | ), 47 | migrations.AlterField( 48 | model_name='showcase', 49 | name='time_only', 50 | field=models.TimeField(blank=True, null=True, verbose_name=b'Time'), 51 | ), 52 | migrations.AddField( 53 | model_name='movie', 54 | name='showcase', 55 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='demo.Showcase'), 56 | ), 57 | migrations.AddField( 58 | model_name='book', 59 | name='showcase', 60 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='demo.Showcase'), 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /demo/demo/migrations/0007_auto_20160303_1859.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-03-03 16:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0006_auto_20160303_1857'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='book', 17 | old_name='is_public', 18 | new_name='is_released', 19 | ), 20 | migrations.RenameField( 21 | model_name='movie', 22 | old_name='is_public', 23 | new_name='is_released', 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /demo/demo/migrations/0008_auto_20160305_1055.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-03-05 08:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('demo', '0007_auto_20160303_1859'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='City', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=64)), 21 | ('is_capital', models.BooleanField()), 22 | ('area', models.BigIntegerField(blank=True, null=True)), 23 | ('population', models.BigIntegerField(blank=True, null=True)), 24 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='demo.Country')), 25 | ], 26 | options={ 27 | 'verbose_name_plural': 'Cities (django-select2)', 28 | }, 29 | ), 30 | migrations.AlterUniqueTogether( 31 | name='city', 32 | unique_together=set([('name', 'country')]), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /demo/demo/migrations/0009_showcase_collapsed_param.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.11 on 2017-02-11 18:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0008_auto_20160305_1055'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='showcase', 17 | name='collapsed_param', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/demo/migrations/0010_auto_20170211_2037.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.11 on 2017-02-11 18:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('demo', '0009_showcase_collapsed_param'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='showcase', 17 | name='boolean', 18 | field=models.BooleanField(default=True), 19 | ), 20 | migrations.AddField( 21 | model_name='showcase', 22 | name='boolean_with_help', 23 | field=models.BooleanField(default=False, help_text=b'Boolean field with help text'), 24 | ), 25 | migrations.AddField( 26 | model_name='showcase', 27 | name='choices', 28 | field=models.SmallIntegerField(choices=[(1, b'Tall'), (2, b'Normal'), (3, b'Short')], default=3, help_text=b'Help text'), 29 | ), 30 | migrations.AddField( 31 | model_name='showcase', 32 | name='horizontal_choices', 33 | field=models.SmallIntegerField(choices=[(1, b'Awesome'), (2, b'Good'), (3, b'Normal'), (4, b'Bad')], default=1, help_text=b'Horizontal choices look like this'), 34 | ), 35 | migrations.AddField( 36 | model_name='showcase', 37 | name='vertical_choices', 38 | field=models.SmallIntegerField(choices=[(1, b'Hot'), (2, b'Normal'), (3, b'Cold')], default=2, help_text=b'Some help on vertical choices'), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /demo/demo/migrations/0011_auto_20170211_2156.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.11 on 2017-02-11 19:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('demo', '0010_auto_20170211_2037'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='showcase', 18 | name='country', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='demo.Country'), 20 | ), 21 | migrations.AddField( 22 | model_name='showcase', 23 | name='country2', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='showcase_country2_set', to='demo.Country', verbose_name=b'asd'), 25 | ), 26 | migrations.AddField( 27 | model_name='showcase', 28 | name='raw_id_field', 29 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='showcase_raw_set', to='demo.Country'), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /demo/demo/migrations/0012_auto_20170407_1131.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.11 on 2017-04-07 08:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('demo', '0011_auto_20170211_2156'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='showcase', 18 | name='html5_color', 19 | field=models.CharField(blank=True, max_length=7, null=True), 20 | ), 21 | migrations.AddField( 22 | model_name='showcase', 23 | name='html5_date', 24 | field=models.DateField(blank=True, null=True), 25 | ), 26 | migrations.AddField( 27 | model_name='showcase', 28 | name='html5_number', 29 | field=models.IntegerField(blank=True, null=True), 30 | ), 31 | migrations.AlterField( 32 | model_name='showcase', 33 | name='country2', 34 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='showcase_country2_set', to='demo.Country', verbose_name=b'Django Select 2'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /demo/demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/demo/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | TYPE_CHOICES = ((1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')) 4 | TYPE_CHOICES2 = ((1, 'Hot'), (2, 'Normal'), (3, 'Cold')) 5 | TYPE_CHOICES3 = ((1, 'Tall'), (2, 'Normal'), (3, 'Short')) 6 | 7 | 8 | class Continent(models.Model): 9 | name = models.CharField(max_length=256) 10 | order = models.PositiveIntegerField() 11 | 12 | def __unicode__(self): 13 | return self.name 14 | 15 | class Meta: 16 | ordering = ('name',) 17 | 18 | 19 | class Country(models.Model): 20 | continent = models.ForeignKey(Continent, null=True) 21 | name = models.CharField(max_length=256) 22 | code = models.CharField(max_length=2, 23 | help_text='ISO 3166-1 alpha-2 - two character country code') 24 | independence_day = models.DateField(blank=True, null=True) 25 | area = models.BigIntegerField(blank=True, null=True) 26 | population = models.BigIntegerField(blank=True, null=True) 27 | description = models.TextField(blank=True, help_text='Try and enter few some more lines') 28 | architecture = models.TextField(blank=True) 29 | order = models.PositiveIntegerField(default=0) 30 | 31 | def __unicode__(self): 32 | return self.name 33 | 34 | class Meta: 35 | ordering = ('name',) 36 | verbose_name_plural = 'Countries' 37 | 38 | 39 | class City(models.Model): 40 | name = models.CharField(max_length=64) 41 | country = models.ForeignKey(Country) 42 | is_capital = models.BooleanField() 43 | area = models.BigIntegerField(blank=True, null=True) 44 | population = models.BigIntegerField(blank=True, null=True) 45 | 46 | def __unicode__(self): 47 | return self.name 48 | 49 | class Meta: 50 | verbose_name_plural = "Cities (django-select2)" 51 | unique_together = ('name', 'country') 52 | 53 | 54 | class Showcase(models.Model): 55 | name = models.CharField(max_length=64) 56 | help_text = models.CharField(max_length=64, 57 | help_text="Enter fully qualified name") 58 | multiple_in_row = models.CharField(max_length=64, 59 | help_text='Help text for multiple') 60 | textfield = models.TextField(blank=True, 61 | verbose_name='Autosized textfield', 62 | help_text='Try and enter few some more lines') 63 | readonly_field = models.CharField(max_length=127, default='Some value here') 64 | multiple2 = models.CharField(max_length=10, blank=True) 65 | date = models.DateField(blank=True, null=True) 66 | date_and_time = models.DateTimeField(blank=True, null=True) 67 | time_only = models.TimeField(blank=True, null=True, verbose_name='Time') 68 | 69 | date_widget = models.DateField(blank=True, null=True) 70 | datetime_widget = models.DateTimeField(blank=True, null=True) 71 | collapsed_param = models.BooleanField(default=False) 72 | 73 | TYPE_CHOICES = ((1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')) 74 | TYPE_CHOICES2 = ((1, 'Hot'), (2, 'Normal'), (3, 'Cold')) 75 | TYPE_CHOICES3 = ((1, 'Tall'), (2, 'Normal'), (3, 'Short')) 76 | boolean = models.BooleanField(default=True) 77 | boolean_with_help = models.BooleanField(default=False, help_text="Boolean field with help text") 78 | horizontal_choices = models.SmallIntegerField( 79 | choices=TYPE_CHOICES, default=1, help_text='Horizontal choices look like this') 80 | vertical_choices = models.SmallIntegerField( 81 | choices=TYPE_CHOICES2, default=2, help_text="Some help on vertical choices") 82 | choices = models.SmallIntegerField( 83 | choices=TYPE_CHOICES3, default=3, help_text="Help text") 84 | 85 | country = models.ForeignKey(Country, null=True, blank=True) 86 | country2 = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_country2_set', verbose_name='Django Select 2') 87 | raw_id_field = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_raw_set') 88 | # linked_foreign_key = models.ForeignKey(Country, limit_choices_to={ 89 | # 'continent__name': 'Europe'}, related_name='foreign_key_linked') 90 | html5_color = models.CharField(null=True, blank=True, max_length=7) 91 | html5_number = models.IntegerField(null=True, blank=True) 92 | html5_date = models.DateField(null=True, blank=True) 93 | 94 | class Meta: 95 | verbose_name_plural = 'Showcase' 96 | 97 | 98 | # Tabular inline model for Showcase 99 | class Movie(models.Model): 100 | showcase = models.ForeignKey(Showcase) 101 | title = models.CharField(max_length=64) 102 | rating = models.SmallIntegerField(choices=TYPE_CHOICES, default=2) 103 | description = models.TextField(blank=True) 104 | is_released = models.BooleanField(default=False) 105 | order = models.PositiveIntegerField() 106 | 107 | class Meta: 108 | ordering = ('order',) 109 | 110 | def __unicode__(self): 111 | return self.title 112 | 113 | 114 | # Stacked inline model for Showcase 115 | class Book(models.Model): 116 | showcase = models.ForeignKey(Showcase) 117 | title = models.CharField(max_length=64) 118 | rating = models.SmallIntegerField(choices=TYPE_CHOICES, help_text='Choose wisely') 119 | is_released = models.BooleanField(default=False) 120 | order = models.PositiveIntegerField() 121 | 122 | class Meta: 123 | ordering = ('order',) 124 | 125 | def __unicode__(self): 126 | return self.title 127 | -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '_b#(gd6_afuvrn!a$yq7_^7!u)m&-x4b80i@9ls!w@jnl$tzc3' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 35 | # Demo app 36 | 'demo', 37 | 38 | # Django Suit 39 | 'demo.apps.SuitConfig', 40 | 41 | # 3rd party apps 42 | 'django_select2', 43 | 44 | # Django 45 | 'django.contrib.admin', 46 | 'django.contrib.auth', 47 | 'django.contrib.contenttypes', 48 | 'django.contrib.sessions', 49 | 'django.contrib.messages', 50 | 'django.contrib.staticfiles', 51 | ] 52 | 53 | MIDDLEWARE_CLASSES = [ 54 | 'django.middleware.security.SecurityMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 60 | 'django.contrib.messages.middleware.MessageMiddleware', 61 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 62 | ] 63 | 64 | ROOT_URLCONF = 'demo.urls' 65 | 66 | TEMPLATES = [ 67 | { 68 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 69 | 'DIRS': [], 70 | 'APP_DIRS': True, 71 | 'OPTIONS': { 72 | 'context_processors': [ 73 | 'django.template.context_processors.debug', 74 | 'django.template.context_processors.request', 75 | 'django.contrib.auth.context_processors.auth', 76 | 'django.contrib.messages.context_processors.messages', 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | WSGI_APPLICATION = 'demo.wsgi.application' 83 | 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 87 | 88 | DATABASES = { 89 | 'default': { 90 | 'ENGINE': 'django.db.backends.sqlite3', 91 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 92 | } 93 | } 94 | 95 | 96 | # Password validation 97 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 98 | 99 | AUTH_PASSWORD_VALIDATORS = [ 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 111 | }, 112 | ] 113 | 114 | 115 | # Internationalization 116 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 117 | 118 | LANGUAGE_CODE = 'en-us' 119 | 120 | TIME_ZONE = 'Europe/Riga' 121 | 122 | USE_I18N = True 123 | 124 | USE_L10N = True 125 | 126 | USE_TZ = True 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | 134 | # For demo app specific only: 135 | # Use file backend for sessions, to not mess DB 136 | SESSION_ENGINE = 'django.contrib.sessions.backends.file' 137 | -------------------------------------------------------------------------------- /demo/demo/static/admin/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | var django = django || {}; 2 | django.jQuery = jQuery.noConflict(true); 3 | 4 | // Override Django jquery.init.js 5 | // Make jQuery global - needed for Django-Select2 6 | if (!window.jQuery) 7 | window.$ = window.jQuery = django.jQuery; 8 | 9 | -------------------------------------------------------------------------------- /demo/demo/static/css/demo.css: -------------------------------------------------------------------------------- 1 | .django-select2 { 2 | min-width: 250px; 3 | } 4 | 5 | /* Improve Bootstrap4 to make it more matching */ 6 | #content .select2-container--bootstrap { 7 | display: inline-block; 8 | margin-right: 5px; 9 | border-radius: .25rem; 10 | } 11 | 12 | #content .select2-container--bootstrap .select2-selection { 13 | box-shadow: none; 14 | -webkit-box-shadow: none; 15 | } 16 | 17 | #content .select2-container--bootstrap:not(.select2-container--focus):not(.select2-container--open) .select2-selection { 18 | border-color: #d9d9d9; 19 | } 20 | 21 | #content .select2-container--bootstrap .select2-selection--single { 22 | padding-top: 5px; 23 | padding-bottom: 5px; 24 | height: 32px; 25 | } 26 | 27 | #content .select2-container--bootstrap .select2-selection__choice { 28 | background-color: #F1F1F1; 29 | border: 1px solid #ddd; 30 | } 31 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | {% load suit_tags static %} 3 | 4 | {# Following is an example how to extend admin by custom CSS or JS files #} 5 | {# Add extra CSS for admin #} 6 | {% block extrastyle %} 7 | {{ block.super }} 8 | 9 | {% endblock %} 10 | 11 | {#{% block foot %}#} 12 | {# {{ block.super }}#} 13 | {# #} 14 | {#{% endblock %}#} 15 | 16 | 17 | {# Switch for demonstration purposes, not indended for production use #} 18 | {% block usertools %} 19 | {% with suit_layout='layout'|suit_conf:request %} 20 | {% if suit_layout == 'horizontal' %} 21 | 22 | Switch to vertical 23 | 24 | {% else %} 25 | 26 | Switch to horizontal 27 | 28 | {% endif %} 29 | 30 | {{ block.super }} 31 | 32 | {% endwith %} 33 | 34 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/custom_view.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n %} 3 | 4 | 5 | {% block content %} 6 |
7 |
8 | Isn't this neat? 9 |
10 |
11 |

Custom view

12 |

This is an example how easy you can create custom views and add them to menu. 13 |
Django + Django Suit + Bootstrap 4 = awesome!

14 |

15 | Go home 16 | View on github 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/continent/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_list.html" %} 2 | {# Extended just to add disclaimer #} 3 | 4 | {% block search %} 5 | {{ block.super }} 6 |
7 | An example of 8 | changelist sortable and also 9 | suit_row_attributes, 10 | suit_cell_attributes and suit_column_attributes callables for ModelAdmin class which allows styling (colors, alignment, etc.) for rows and cells based on field and object instance. See code of this exact example 11 | on github. 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/country/tab_charts.html: -------------------------------------------------------------------------------- 1 |
2 |

Statistics

3 |
Google Charts example initiated on shown.suit.tab event
4 | 5 |
6 | 9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 | 18 | 19 | 58 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/country/tab_docs.html: -------------------------------------------------------------------------------- 1 |
2 |

Documentation

3 | 4 |
5 | 8 |
9 |
10 | Tabs you see above are based on mostly CSS/JS solution, therefore integration of tabs is simple and non intrusive - all your form handling will work the same as before. 11 |

12 | Tabs can contain fieldsets, inlines and custom/included templates 13 |
14 | Form tabs documentation 15 |
16 |
17 |
18 |
19 | 22 |
23 |
24 | Django Suit provides handy shortcut to include templates into forms, into several positions: 25 |
    26 |
  • top - above fieldsets
  • 27 |
  • middle - between fieldsets and inlines
  • 28 |
  • bottom - after inlines
  • 29 |
30 | Suit includes are nothing but a shortcut. The same can be achieved by extending change_form.html and hooking into particular blocks. Suit includes can be used in combination with or without tabs. 31 |
32 | Form includes documentation 33 |
34 |
35 |
36 | 37 |
38 | 39 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/country/tab_flag.html: -------------------------------------------------------------------------------- 1 |
2 |

Country flag

3 | 4 |
This is another custom form include example
5 | 6 |
7 | 10 |
11 | 14 |
15 |
16 | 17 | 18 |
19 | 22 |
23 |
24 | {% if original.pk %} 25 | 26 | 27 | {% else %} 28 |
29 | Item is not saved yet 30 |
31 | {% endif %} 32 |
33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/country/tab_notice.html: -------------------------------------------------------------------------------- 1 |
2 |

Custom include

3 | 4 |
5 | Example of include between fieldsets and inlines 6 |
7 | 8 |
9 |
10 | Read more on including templates in the last tab. 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/showcase/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n admin_urls demo_tags %} 3 | 4 | {# Example of extending object-tools-items #} 5 | {% block object-tools-items %} 6 |
  • 7 | {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} 8 | 9 | {% trans "History" %} 10 |
  • 11 | 12 | {% if has_absolute_url %} 13 |
  • {% trans "View on site" %} 14 |
  • 15 | {% endif %} 16 | 17 |
  • 18 | 19 | Do something useful 20 |
  • 21 | {% endblock %} 22 | 23 | {% block object-tools %} 24 | {{ block.super }} 25 | 30 | 35 | {% endblock %} 36 | 37 | {% block submit_buttons_bottom %} 38 | {# Currently only way extending submit_row.html is by custom template tag #} 39 | {% submit_row_custom %} 40 | {% endblock %} 41 | 42 | {% block footer %} 43 | {{ block.super }} 44 | 45 | 46 | 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/demo/showcase/submit_line.html: -------------------------------------------------------------------------------- 1 | {% load i18n admin_urls %} 2 |
    3 | {% url opts|admin_urlname:'clickme' original.pk|admin_urlquote as custom_url %} 4 | Click me 5 | 6 | {% if show_save %} 7 | 8 | 9 | 10 | 11 | 12 | {% endif %} 13 | 14 | {% if show_delete_link %} 15 | {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} 16 | 19 | {% endif %} 20 | 21 | {% if show_save %} 22 | 23 | {% endif %} 24 | 25 | {% if show_save_as_new %} 26 | {% endif %} 27 | 28 | {% if show_save_and_continue %} 29 | {% endif %} 30 | {% if show_save_as_new %} 31 | {% endif %} 32 | {# {% if show_save_and_add_another %}#} 33 | {# {% endif %}#} 34 | 35 |
    36 | -------------------------------------------------------------------------------- /demo/demo/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/login.html' %} 2 | {% load i18n %} 3 | 4 | {% block branding %} 5 | 20 | {{ block.super }} 21 | {% endblock %} 22 | 23 | {% block footer %} 24 |

    25 | For demo use: 26 |
    27 | Username: demo
    28 | Password: demodemo 29 |

    30 | {{ block.super }} 31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /demo/demo/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/demo/demo/templatetags/__init__.py -------------------------------------------------------------------------------- /demo/demo/templatetags/demo_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.admin.templatetags.admin_modify import submit_row as django_submit_row 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('admin/demo/showcase/submit_line.html', takes_context=True) 7 | def submit_row_custom(context): 8 | """ 9 | Currently only way of overriding Django admin submit_line.html is by replacing 10 | submit_row template tag in change_form.html 11 | """ 12 | return django_submit_row(context) 13 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from django.views.generic import RedirectView 19 | 20 | from . import views 21 | 22 | urlpatterns = [ 23 | 24 | # Django Suit custom admin view 25 | url(r'^admin/custom/$', views.custom_admin_view), 26 | 27 | url(r'^admin/', admin.site.urls), 28 | 29 | # Django-Select2 30 | url(r'^select2/', include('django_select2.urls')), 31 | 32 | # Documentation url for menu documentation link 33 | url(r'^admin/custom2/', RedirectView.as_view(url='http://djangosuit.com/support/'), name='django-admindocs-docroot'), 34 | 35 | url(r'^$', views.home, name='home'), 36 | ] 37 | -------------------------------------------------------------------------------- /demo/demo/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.admin.views.decorators import staff_member_required 3 | from django.http import HttpResponse 4 | from django.shortcuts import render 5 | 6 | 7 | @staff_member_required 8 | def custom_admin_view(request): 9 | """ 10 | If you're using multiple admin sites with independent views you'll need to set 11 | current_app manually and use correct admin.site 12 | # request.current_app = 'admin' 13 | """ 14 | context = admin.site.each_context(request) 15 | context.update({ 16 | 'title': 'Custom view', 17 | }) 18 | 19 | template = 'admin/custom_view.html' 20 | return render(request, template, context) 21 | 22 | 23 | def home(request): 24 | return HttpResponse(""" 25 | 26 | 27 | 28 | Welcome to Django Suit demo app.
    29 | Go to admin to explore all the features. 30 | """) 31 | -------------------------------------------------------------------------------- /demo/demo/widgets.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.forms import forms 3 | from django.contrib.staticfiles.templatetags.staticfiles import static 4 | 5 | 6 | class Bootstrap4Select(object): 7 | def build_attrs(self, extra_attrs=None, **kwargs): 8 | attrs = super(Bootstrap4Select, self).build_attrs(extra_attrs, **kwargs) 9 | attrs.setdefault('data-theme', 'bootstrap') 10 | return attrs 11 | 12 | def _get_media(self): 13 | """ 14 | Construct Media as a dynamic property. 15 | .. Note:: For more information visit 16 | https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property 17 | """ 18 | return forms.Media( 19 | js=( 20 | settings.SELECT2_JS, 21 | static('django_select2/django_select2.js'), 22 | ), 23 | css={'screen': ( 24 | settings.SELECT2_CSS, 25 | # static('css/select2-bootstrap.css'), 26 | '//cdnjs.cloudflare.com/ajax/libs/select2-bootstrap-theme/0.1.0-beta.6/select2-bootstrap.min.css',)} 27 | ) 28 | 29 | media = property(_get_media) 30 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10.5 2 | Django-Select2==5.8.10 -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoSuit.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoSuit.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoSuit" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoSuit" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/configure.rst: -------------------------------------------------------------------------------- 1 | Configure 2 | ========= 3 | 4 | Documentation is in development. 5 | 6 | Currently you can use demo app code as an example for v2: https://github.com/darklow/django-suit/blob/v2/demo/demo/apps.py 7 | 8 | v2 live demo: http://v2.djangosuit.com/admin/ 9 | 10 | v2 demo app code: https://github.com/darklow/django-suit/tree/v2/demo 11 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | To contribute to Django Suit fork Django Suit on github and clone it locally: 5 | 6 | .. code-block:: bash 7 | 8 | git clone -b v2 git@github.com:YOUR_USERNAME/django-suit.git suit 9 | cd suit 10 | 11 | 12 | DEV environment 13 | --------------- 14 | 15 | After you forked and cloned repository I suggest you create virtual environment using ``virtualenv``. Feel free to use other virtualenv layout, but here is mine: 16 | 17 | .. code-block:: bash 18 | 19 | # In cloned suit directory create virtualenv 20 | virtualenv env 21 | 22 | # Activate virtualenv 23 | source env/bin/activate 24 | 25 | # Install Django Suit in editable mode 26 | pip install -e . 27 | 28 | # Install dev and demo app requirements 29 | pip install -r requirements-dev.txt 30 | pip install -r demo/requirements.txt 31 | 32 | # Run Django Suit demo app 33 | python demo/manage.py runserver 0.0.0.0:8000 34 | 35 | 36 | SASS compiling 37 | -------------- 38 | 39 | SASS compiling is done in ``nodejs`` using ``gulp`` tasks and ``node-sass`` (uses ``libsass``). Gulp tasks are watching ``.scss`` and ``.html`` files and automatically reload browser on changes, making development much easier. 40 | 41 | .. code-block:: bash 42 | 43 | # Install dependencies 44 | npm install 45 | bower install 46 | 47 | # Run Django Suit demo app 48 | python demo/manage.py runserver 0.0.0.0:8000 49 | 50 | # Run gulp tasks and it should automatically open http://localhost:8001/. 51 | # If it didn't, open it manually. 52 | gulp 53 | 54 | 55 | Documentation 56 | ------------- 57 | 58 | Compile docs: 59 | 60 | .. code-block:: bash 61 | 62 | # Compile docs 63 | make -C docs html 64 | 65 | # Clean & compile 66 | make -C docs clean html 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Django Suit documentation 2 | ========================= 3 | 4 | **Django Suit - Modern theme for Django admin interface**. 5 | 6 | About 7 | ----- 8 | 9 | Django Suit is alternative theme/skin/extension for `Django `_ admin app (administration interface). 10 | 11 | 12 | Licence 13 | ------- 14 | 15 | * Django Suit is licensed under `Creative Commons Attribution-NonCommercial 3.0 `_ license. 16 | * See licence and pricing: http://djangosuit.com/pricing/ 17 | 18 | 19 | Resources 20 | --------- 21 | 22 | * Home page: http://djangosuit.com 23 | * Demo v1: http://djangosuit.com/admin/ 24 | * Demo v2: http://v2.djangosuit.com/admin/ 25 | * Licence and Pricing: http://djangosuit.com/pricing/ 26 | * Github: https://github.com/darklow/django-suit 27 | * Demo app v1 on Github: https://github.com/darklow/django-suit-examples 28 | * Demo app v2 on Github: https://github.com/darklow/django-suit/tree/v2/demo 29 | 30 | 31 | 32 | Contents: 33 | 34 | .. toctree:: 35 | :maxdepth: 3 36 | 37 | install 38 | configure 39 | contribute 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ======= 3 | 4 | To install Django Suit 5 | 6 | 1. Install Django Suit v2-dev using ``pip`` or ``easy_install``:: 7 | 8 | pip install https://github.com/darklow/django-suit/tarball/v2 9 | 10 | 11 | 2. Create ``SuitConfig`` class and add it to the ``INSTALLED_APPS`` **before** ``django.contrib.admin`` app: 12 | 13 | .. code-block:: py 14 | 15 | # my_project_app/apps.py 16 | from suit.apps import DjangoSuitConfig 17 | 18 | class SuitConfig(DjangoSuitConfig): 19 | layout = 'horizontal' 20 | 21 | 22 | .. code-block:: py 23 | 24 | INSTALLED_APPS = ( 25 | ... 26 | 'my_project_app.apps.SuitConfig', 27 | 'django.contrib.admin', 28 | ) 29 | 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var sass = require('gulp-sass'); 5 | var browserSync = require('browser-sync'); 6 | var reload = browserSync.reload; 7 | var autoprefixer = require('gulp-autoprefixer'); 8 | var plumber = require('gulp-plumber'); 9 | 10 | var config = { 11 | djangoHost: 'localhost', 12 | djangoPort: 8000, 13 | jsPort: 8001, 14 | watchSassFiles: 'suit/sass/**/*.scss', 15 | cssOutputDir: 'suit/static/suit/css/', 16 | watchHtmlFiles: [ 17 | 'suit/templates/**/*.html', 18 | 'demo/demo/templates/**/*.html' 19 | ] 20 | }; 21 | 22 | gulp.task('styles', function () { 23 | return gulp.src(config.watchSassFiles) 24 | .pipe(plumber()) 25 | .pipe(sass({outputStyle: 'compact'})).on('error', sass.logError) 26 | .pipe(autoprefixer({browsers: ['last 2 version', '> 5%']})) 27 | .pipe(gulp.dest(config.cssOutputDir)) 28 | .pipe(reload({stream: true})) 29 | ; 30 | }); 31 | 32 | gulp.task('watch', function () { 33 | browserSync({ 34 | port: config.jsPort, 35 | ui: false, 36 | notify: false, 37 | ghostMode: false, 38 | https: false, 39 | startPath: '/admin/', 40 | proxy: { 41 | target: config.djangoHost + ':' + config.djangoPort 42 | } 43 | }); 44 | 45 | gulp.watch(config.watchSassFiles, ['styles']); 46 | gulp.watch(config.watchHtmlFiles).on('change', reload); 47 | }); 48 | 49 | gulp.task('default', ['styles', 'watch']); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-suit-dev", 3 | "version": "2.0.0", 4 | "description": "Django Suit development tools", 5 | "author": "Kaspars Sprogis", 6 | "devDependencies": { 7 | "browser-sync": "^2.17.1", 8 | "gulp": "^3.9.1", 9 | "gulp-autoprefixer": "^3.1.0", 10 | "gulp-plumber": "^1.0.1", 11 | "gulp-sass": "^2.3.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.3.5 2 | sphinx-rtd-theme==0.1.9 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='django-suit', 5 | version=__import__('suit').VERSION, 6 | description='Modern theme for Django admin interface.', 7 | author='Kaspars Sprogis (darklow)', 8 | author_email='info@djangosuit.com', 9 | url='http://djangosuit.com', 10 | packages=['suit', 'suit.templatetags'], 11 | zip_safe=False, 12 | include_package_data=True, 13 | classifiers=[ 14 | 'Development Status :: 5 - Production/Stable', 15 | 'Framework :: Django', 16 | 'License :: Free for non-commercial use', 17 | 'Intended Audience :: Developers', 18 | 'Intended Audience :: System Administrators', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2.7', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | 'Environment :: Web Environment', 26 | 'Topic :: Software Development', 27 | 'Topic :: Software Development :: User Interfaces', 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /suit/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/.DS_Store -------------------------------------------------------------------------------- /suit/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '2.0a2' 2 | default_app_config = 'suit.apps.DjangoSuitConfig' 3 | -------------------------------------------------------------------------------- /suit/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db import models 3 | from django.utils.safestring import mark_safe 4 | try: 5 | from django.urls import reverse_lazy 6 | except: 7 | from django.core.urlresolvers import reverse_lazy 8 | 9 | """ 10 | Adapted by using following examples: 11 | https://djangosnippets.org/snippets/2887/ 12 | http://stackoverflow.com/a/7192721/641263 13 | """ 14 | 15 | link_to_prefix = 'link_to_' 16 | 17 | 18 | def get_admin_url(instance, admin_prefix='admin', current_app=None): 19 | if not instance.pk: 20 | return 21 | return reverse_lazy( 22 | '%s:%s_%s_change' % (admin_prefix, instance._meta.app_label, instance._meta.model_name), 23 | args=(instance.pk,), 24 | current_app=current_app 25 | ) 26 | 27 | 28 | def get_related_field(name, short_description=None, admin_order_field=None, admin_prefix='admin'): 29 | """ 30 | Create a function that can be attached to a ModelAdmin to use as a list_display field, e.g: 31 | client__name = get_related_field('client__name', short_description='Client') 32 | """ 33 | as_link = name.startswith(link_to_prefix) 34 | if as_link: 35 | name = name[len(link_to_prefix):] 36 | related_names = name.split('__') 37 | 38 | def getter(self, obj): 39 | for related_name in related_names: 40 | if not obj: 41 | continue 42 | obj = getattr(obj, related_name) 43 | if obj and as_link: 44 | obj = mark_safe(u'%s' % \ 45 | (get_admin_url(obj, admin_prefix, current_app=self.admin_site.name), obj)) 46 | return obj 47 | 48 | getter.admin_order_field = admin_order_field or name 49 | getter.short_description = short_description or related_names[-1].title().replace('_', ' ') 50 | if as_link: 51 | getter.allow_tags = True 52 | return getter 53 | 54 | 55 | class RelatedFieldAdminMetaclass(type(admin.ModelAdmin)): 56 | related_field_admin_prefix = 'admin' 57 | 58 | def __new__(cls, name, bases, attrs): 59 | new_class = super(RelatedFieldAdminMetaclass, cls).__new__(cls, name, bases, attrs) 60 | 61 | for field in new_class.list_display: 62 | if '__' in field or field.startswith(link_to_prefix): 63 | if not hasattr(new_class, field): 64 | setattr(new_class, field, get_related_field( 65 | field, admin_prefix=cls.related_field_admin_prefix)) 66 | 67 | return new_class 68 | 69 | 70 | class RelatedFieldAdmin(admin.ModelAdmin): 71 | """ 72 | Version of ModelAdmin that can use linked and related fields in list_display, e.g.: 73 | list_display = ('link_to_user', 'address__city', 'link_to_address__city', 'address__country__country_code') 74 | """ 75 | __metaclass__ = RelatedFieldAdminMetaclass 76 | 77 | def get_queryset(self, request): 78 | qs = super(RelatedFieldAdmin, self).get_queryset(request) 79 | 80 | # Include all related fields in queryset 81 | select_related = [] 82 | for field in self.list_display: 83 | if '__' in field: 84 | if field.startswith(link_to_prefix): 85 | field = field[len(link_to_prefix):] 86 | select_related.append(field.rsplit('__', 1)[0]) 87 | 88 | # Include all foreign key fields in queryset. 89 | # This is based on ChangeList.get_query_set(). 90 | # We have to duplicate it here because select_related() only works once. 91 | # Can't just use list_select_related because we might have multiple__depth__fields it won't follow. 92 | model = qs.model 93 | for field_name in self.list_display: 94 | try: 95 | field = model._meta.get_field(field_name) 96 | except models.FieldDoesNotExist: 97 | continue 98 | 99 | if isinstance(field.remote_field, models.ManyToOneRel): 100 | select_related.append(field_name) 101 | 102 | return qs.select_related(*select_related) 103 | -------------------------------------------------------------------------------- /suit/admin_filters.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy as _ 2 | from django.contrib.admin import FieldListFilter 3 | 4 | 5 | class IsNullFieldListFilter(FieldListFilter): 6 | notnull_label = _('Is present') 7 | isnull_label = _('Is Null') 8 | 9 | def __init__(self, field, request, params, model, model_admin, field_path): 10 | self.lookup_kwarg = '%s__isnull' % field_path 11 | self.lookup_val = request.GET.get(self.lookup_kwarg, None) 12 | super(IsNullFieldListFilter, self).__init__(field, 13 | request, params, model, 14 | model_admin, field_path) 15 | 16 | def expected_parameters(self): 17 | return [self.lookup_kwarg] 18 | 19 | def choices(self, cl): 20 | for lookup, title in ( 21 | (None, _('All')), 22 | ('False', self.notnull_label), 23 | ('True', self.isnull_label), 24 | ): 25 | yield { 26 | 'selected': self.lookup_val == lookup, 27 | 'query_string': cl.get_query_string({ 28 | self.lookup_kwarg: lookup, 29 | }), 30 | 'display': title, 31 | } 32 | -------------------------------------------------------------------------------- /suit/apps.py: -------------------------------------------------------------------------------- 1 | from django import get_version 2 | from django.apps import AppConfig 3 | from django.contrib.admin.options import ModelAdmin 4 | from . import VERSION 5 | 6 | ALL_FIELDS = '__all__' 7 | 8 | # Form row sizing as Bootstrap CSS grid classes: (for label, for field column) 9 | SUIT_FORM_SIZE_LABEL = 'col-xs-12 col-sm-3 col-md-2' 10 | SUIT_FORM_SIZE_INLINE = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-9 col-md-10 form-inline') 11 | SUIT_FORM_SIZE_SMALL = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-6 col-md-5 col-lg-4') 12 | SUIT_FORM_SIZE_HALF = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-7 col-md-6 col-lg-5') 13 | SUIT_FORM_SIZE_LARGE = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-8 col-md-7 col-lg-6') 14 | SUIT_FORM_SIZE_X_LARGE = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-9 col-md-8 col-lg-7') 15 | SUIT_FORM_SIZE_XX_LARGE = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-9 col-md-10 col-lg-8') 16 | SUIT_FORM_SIZE_XXX_LARGE = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-9 col-md-10 col-lg-9') 17 | SUIT_FORM_SIZE_FULL = (SUIT_FORM_SIZE_LABEL, 'col-xs-12 col-sm-9 col-md-10') 18 | 19 | 20 | class DjangoSuitConfig(AppConfig): 21 | name = 'suit' 22 | verbose_name = 'Django Suit' 23 | django_version = get_version() 24 | version = VERSION 25 | 26 | # Menu and header layout - horizontal or vertical 27 | layout = 'horizontal' 28 | 29 | # Set default list per page 30 | list_per_page = 20 31 | 32 | # Show changelist top actions only if any row is selected 33 | toggle_changelist_top_actions = True 34 | 35 | # Define menu 36 | #: :type: list of suit.menu.ParentItem 37 | menu = [] 38 | 39 | # Automatically add home link 40 | menu_show_home = True 41 | 42 | # Define callback / handler to change menu before it is getting rendered 43 | menu_handler = None 44 | 45 | # Enables two column layout for change forms with submit row on the right 46 | form_submit_on_right = True 47 | 48 | # Hide name/"original" column for all tabular inlines. 49 | # May be overridden in Inline class by suit_form_inlines_hide_original = False 50 | form_inlines_hide_original = False 51 | 52 | # For size 53 | form_size = { 54 | 'default': SUIT_FORM_SIZE_X_LARGE, 55 | # 'fields': {} 56 | 'widgets': { 57 | 'RelatedFieldWidgetWrapper': SUIT_FORM_SIZE_XXX_LARGE 58 | } 59 | # 'fieldsets': {} 60 | } 61 | 62 | # form_size setting can be overridden in ModelAdmin using suit_form_size parameter 63 | # 64 | # Example: 65 | # ---------------------------------------------- 66 | # suit_form_size = { 67 | # 'default': 'col-xs-12 col-sm-2', 'col-xs-12 col-sm-10', 68 | # 'fields': { 69 | # 'field_name': SUIT_FORM_SIZE_LARGE, 70 | # 'field_name2': SUIT_FORM_SIZE_X_LARGE, 71 | # }, 72 | # 'widgets': { 73 | # 'widget_class_name': SUIT_FORM_SIZE_FULL, 74 | # 'AdminTextareaWidget': SUIT_FORM_SIZE_FULL, 75 | # }, 76 | # 'fieldsets': { 77 | # 'fieldset_name': SUIT_FORM_SIZE_FULL, 78 | # 'fieldset_name2': SUIT_FORM_SIZE_FULL, 79 | # } 80 | # } 81 | 82 | def __init__(self, app_name, app_module): 83 | self.setup_model_admin_defaults() 84 | super(DjangoSuitConfig, self).__init__(app_name, app_module) 85 | 86 | def ready(self): 87 | super(DjangoSuitConfig, self).ready() 88 | 89 | def setup_model_admin_defaults(self): 90 | """ 91 | Override some ModelAdmin defaults 92 | """ 93 | if self.toggle_changelist_top_actions: 94 | ModelAdmin.actions_on_top = True 95 | ModelAdmin.actions_on_bottom = True 96 | 97 | if self.list_per_page: 98 | ModelAdmin.list_per_page = self.list_per_page 99 | -------------------------------------------------------------------------------- /suit/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | # Python 3. 3 | from urllib.parse import parse_qs 4 | except ImportError: 5 | # Python 2.6+ 6 | from urlparse import parse_qs 7 | -------------------------------------------------------------------------------- /suit/config.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.template import VariableDoesNotExist 3 | from suit.apps import DjangoSuitConfig 4 | 5 | 6 | def get_config_instance(app_name=None): 7 | """ 8 | :rtype: DjangoSuitConfig() 9 | """ 10 | try: 11 | config = apps.get_app_config(app_name or 'suit') 12 | if isinstance(config, DjangoSuitConfig): 13 | return config 14 | except LookupError: 15 | pass 16 | return apps.get_app_config('suit') 17 | 18 | 19 | #: :type: DjangoSuitConfig() 20 | suit_config_cls = DjangoSuitConfig 21 | 22 | 23 | def get_config(param=None, request=None): 24 | suit_config = get_config_instance(get_current_app(request) if request else None) 25 | 26 | # Allow overriding suit config by request 27 | # Used only for demo purposes 28 | req_key = '__suit_config_by_request' 29 | if request and not hasattr(req_key, req_key): 30 | setattr(request, req_key, True) 31 | for k, v in request.GET.items(): 32 | if k.startswith('__suit_'): 33 | setattr(suit_config, k[7:], v) 34 | 35 | if param: 36 | value = getattr(suit_config, param, None) 37 | if value is None: 38 | value = getattr(suit_config_cls, param, None) 39 | return value 40 | 41 | return suit_config 42 | 43 | 44 | def get_current_app(request): 45 | try: 46 | return request.current_app 47 | except (VariableDoesNotExist, AttributeError): 48 | pass 49 | return None 50 | 51 | 52 | def set_config_value(name, value): 53 | config = get_config() 54 | # Store previous value to reset later if needed 55 | prev_value_key = '_%s' % name 56 | if not hasattr(config, prev_value_key): 57 | setattr(config, prev_value_key, getattr(config, name)) 58 | setattr(config, name, value) 59 | 60 | 61 | def reset_config_value(name): 62 | config = get_config() 63 | prev_value_key = '_%s' % name 64 | if hasattr(config, prev_value_key): 65 | setattr(config, name, getattr(config, prev_value_key)) 66 | del config.__dict__[prev_value_key] 67 | -------------------------------------------------------------------------------- /suit/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin reset-list() { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | @mixin suit-box-shadow($darken: 5%) { 7 | box-shadow: 0 1px 0 0 darken($body-bg, $darken); 8 | } 9 | @mixin text-semibold() { font-weight: 500; } 10 | .text-lighter { font-weight: 200; } 11 | .text-light { font-weight: 300; } 12 | .text-normal { font-weight: 400; } 13 | .text-semibold { @include text-semibold(); } 14 | .text-light-bold { 15 | @extend .text-light; 16 | strong { 17 | @extend .text-semibold; 18 | } 19 | } 20 | @mixin hide-text-indent() { 21 | text-indent: 100%; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | } 25 | -------------------------------------------------------------------------------- /suit/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | // Enable Flex | Welcome to the future :) 2 | // -------------------------------------------------- 3 | $enable-flex: true; 4 | 5 | // Colors 6 | $brand-primary: #279bee; 7 | $brand-primary: #4298DE; 8 | $brand-success: #4ACB68; 9 | $brand-danger: #E04F3C; 10 | $brand-warning: #F1C40F; 11 | $brand-warning: #F3C544; 12 | $link-color-brighter: #279bee; 13 | $link-color-bright: darken($link-color-brighter, 10%); 14 | $link-color: darken($link-color-brighter, 20%); 15 | $body-bg: #f1f1f1; 16 | $inverse: #252830; 17 | //$inverse: #292C3A; 18 | $header-bg: $inverse; 19 | $inverse-light: lighten($inverse, 10%); 20 | $inverse-lighter: lighten($inverse, 15%); 21 | $inverse-lightest: lighten($inverse, 25%); 22 | $top-nav-bg: saturate(lighten($inverse, 9%), 2%); 23 | //$top-nav-bg: desaturate(#363B4D, 1%); 24 | $header-color: #fff; 25 | $header-muted-color: lighten($header-bg, 30%); 26 | 27 | // Import all Bootstrap variables 28 | @import "../../bower_components/bootstrap/scss/variables"; 29 | 30 | 31 | // Typography 32 | // -------------------------------------------------- 33 | $font-size-h1: 1.714rem; 34 | $font-size-h2: 1.571rem; 35 | $font-size-h3: 1.429rem; 36 | $font-size-h4: 1.286rem; 37 | $font-size-h5: 1.143rem; 38 | $font-size-h6: 1rem; 39 | $font-size-sm: .929rem; 40 | $font-size-xs: .857rem; 41 | 42 | $font-size-root: 14px; 43 | $font-family-sans-serif: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; 44 | $font-family-base: $font-family-sans-serif; 45 | $headings-font-weight: 500; 46 | $font-size-lg: $font-size-h5; 47 | 48 | // http://pxtoem.com/ 49 | 50 | // Suit 51 | // -------------------------------------------------- 52 | $gray-light: lighten($gray-light, 10%); 53 | $text-muted: $gray-light; 54 | $sidebar-width: 200px; 55 | $grid-gutter-width-base: 1.875rem; 56 | $header-padding-horizontal: $grid-gutter-width-base; 57 | $header-padding-vertical: $grid-gutter-width-base/1.8; 58 | $nav-padding-horizontal: $grid-gutter-width-base + .75rem; 59 | $footer-height: 66px; 60 | $vertical-menu-width: 230px; 61 | 62 | // Buttons 63 | // -------------------------------------------------- 64 | //$btn-line-height: $line-height-base; 65 | //$btn-padding-x: 1.2rem; 66 | //$btn-padding-y: .35rem; 67 | $btn-padding-x-sm: .75rem; 68 | $btn-padding-y-sm: .35rem; 69 | //$btn-padding-x-md: .85rem; 70 | //$btn-padding-y-md: .35rem; 71 | $font-size-md: 0.9375rem; 72 | //$btn-padding-x-sm: .75rem; 73 | 74 | // Inputs 75 | // -------------------------------------------------- 76 | $input-padding-x-sm: $btn-padding-x-sm; 77 | $input-padding-y-sm: $btn-padding-y-sm; 78 | $textarea-line-height: $line-height-base; 79 | $custom-select-sm-padding-y: $btn-padding-x-sm; 80 | $custom-select-sm-font-size: $font-size-xs; 81 | 82 | //$input-padding-y: $btn-padding-y; 83 | 84 | // Tables 85 | // -------------------------------------------------- 86 | $table-th-padding: .7rem .8rem; 87 | $table-cell-padding: .4rem .8rem; 88 | $table-sm-cell-padding: .3rem; 89 | $table-bg-accent: darken(#fff, 3%); 90 | 91 | // Forms 92 | // -------------------------------------------------- 93 | $form-bg: lighten($body-bg, 2%); 94 | $form-border-color: darken($body-bg, 1%); 95 | $form-label-bg: #fff; 96 | 97 | // Cards 98 | // -------------------------------------------------- 99 | $card-border-radius: 0; 100 | $card-border-radius-inner: 0; 101 | $card-border-color: transparent; 102 | $card-cap-bg: $inverse-lightest; 103 | $card-cap-color: #fff; 104 | 105 | 106 | // Blockquotes 107 | // -------------------------------------------------- 108 | $blockquote-font-size: $font-size-base; 109 | $blockquote-border-color: darken($gray-lighter, 10%); 110 | -------------------------------------------------------------------------------- /suit/sass/_vendor.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.0.0-alpha.5 (https://getbootstrap.com) 3 | * Copyright 2011-2016 The Bootstrap Authors 4 | * Copyright 2011-2016 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | 8 | // Core variables and mixins 9 | //@import "../../bower_components/bootstrap/scss/variables"; 10 | @import "../../bower_components/bootstrap/scss/mixins"; 11 | 12 | // Reset and dependencies 13 | @import "../../bower_components/bootstrap/scss/normalize"; 14 | @import "../../bower_components/bootstrap/scss/print"; 15 | 16 | // Core CSS 17 | @import "../../bower_components/bootstrap/scss/reboot"; 18 | @import "../../bower_components/bootstrap/scss/type"; 19 | @import "../../bower_components/bootstrap/scss/images"; 20 | @import "../../bower_components/bootstrap/scss/code"; 21 | @import "../../bower_components/bootstrap/scss/grid"; 22 | @import "../../bower_components/bootstrap/scss/tables"; 23 | @import "../../bower_components/bootstrap/scss/forms"; 24 | @import "../../bower_components/bootstrap/scss/buttons"; 25 | 26 | // Components 27 | @import "../../bower_components/bootstrap/scss/animation"; 28 | @import "../../bower_components/bootstrap/scss/dropdown"; 29 | @import "../../bower_components/bootstrap/scss/button-group"; 30 | @import "../../bower_components/bootstrap/scss/input-group"; 31 | @import "../../bower_components/bootstrap/scss/custom-forms"; 32 | @import "../../bower_components/bootstrap/scss/nav"; 33 | //@import "../../bower_components/bootstrap/scss/navbar"; 34 | @import "../../bower_components/bootstrap/scss/card"; 35 | //@import "../../bower_components/bootstrap/scss/breadcrumb"; 36 | //@import "../../bower_components/bootstrap/scss/pagination"; 37 | @import "../../bower_components/bootstrap/scss/tags"; 38 | //@import "../../bower_components/bootstrap/scss/jumbotron"; 39 | @import "../../bower_components/bootstrap/scss/alert"; 40 | @import "../../bower_components/bootstrap/scss/progress"; 41 | //@import "../../bower_components/bootstrap/scss/media"; 42 | //@import "../../bower_components/bootstrap/scss/list-group"; 43 | @import "../../bower_components/bootstrap/scss/responsive-embed"; 44 | @import "../../bower_components/bootstrap/scss/close"; 45 | 46 | // Components w/ JavaScript 47 | @import "../../bower_components/bootstrap/scss/modal"; 48 | @import "../../bower_components/bootstrap/scss/tooltip"; 49 | //@import "../../bower_components/bootstrap/scss/popover"; 50 | //@import "../../bower_components/bootstrap/scss/carousel"; 51 | 52 | // Utility classes 53 | @import "../../bower_components/bootstrap/scss/utilities"; 54 | -------------------------------------------------------------------------------- /suit/sass/components/_alerts.scss: -------------------------------------------------------------------------------- 1 | .errornote { 2 | @extend .alert; 3 | @extend .alert-danger; 4 | } 5 | .messagelist { 6 | @include reset-list(); 7 | display: block; 8 | margin: 1rem $grid-gutter-width-base 0; 9 | li { 10 | @extend .alert; 11 | &:last-child { 12 | margin-bottom: 0; 13 | } 14 | &.success { 15 | @extend .alert-success; 16 | } 17 | &.info { 18 | @extend .alert-info; 19 | } 20 | &.warning { 21 | @extend .alert-warning; 22 | } 23 | &.danger, &.error { 24 | @extend .alert-danger; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /suit/sass/components/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | font-size: $font-size-sm; 3 | color: $header-muted-color; 4 | position: relative; 5 | li { 6 | float: left; 7 | } 8 | a { 9 | color: $link-color; 10 | display: inline-block; 11 | margin: 0 .2rem; 12 | &:first-child { 13 | margin-left: 0; 14 | } 15 | } 16 | } 17 | 18 | body.suit_layout_vertical { 19 | #container { 20 | > .breadcrumbs { 21 | display: block; 22 | position: absolute; 23 | padding: 1.5rem $grid-gutter-width-base; 24 | left: $vertical-menu-width; 25 | } 26 | #content { 27 | .breadcrumbs { 28 | display: none; 29 | } 30 | .messagelist { 31 | margin: 0 0 $grid-gutter-width-base/2 0; 32 | } 33 | } 34 | } 35 | #container { 36 | > .messagelist { 37 | display: none; 38 | } 39 | } 40 | /*&.change-form { 41 | #container { 42 | > .breadcrumbs { 43 | display: none; 44 | } 45 | } 46 | #content { 47 | .breadcrumbs { 48 | } 49 | } 50 | }*/ 51 | } 52 | 53 | body.suit_layout_horizontal { 54 | .breadcrumbs { 55 | padding: 1.5rem $nav-padding-horizontal 0; 56 | } 57 | &.change-list { 58 | .breadcrumbs { 59 | display: none; 60 | } 61 | } 62 | &.change-form { 63 | #content { 64 | .breadcrumbs { 65 | display: none; 66 | padding: 1.5rem 0; 67 | position: relative; 68 | z-index: 6; 69 | li { 70 | float: left; 71 | } 72 | } 73 | } 74 | } 75 | #content { 76 | .content-wrap { 77 | .messagelist { 78 | display: none; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /suit/sass/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | 2 | .btn-round { 3 | border-radius: 40px; 4 | } 5 | @mixin btn-mixin($state, $icon: none, $size: none, $round: false) { 6 | display: inline-block; 7 | @extend .btn; 8 | @extend .btn-#{$state}; 9 | @if $size { 10 | @extend .btn-#{$size} !optional; 11 | } 12 | @if $round { 13 | border-radius: 70px; 14 | } 15 | @if $icon { 16 | &:before { 17 | margin-right: 3px; 18 | display: inline-block; 19 | @include fa-icon-font(); 20 | content: $icon; 21 | } 22 | } 23 | } 24 | 25 | .btn-outline-danger { 26 | border-color: transparentize($brand-danger, .25); 27 | background-color: #fff; 28 | } 29 | -------------------------------------------------------------------------------- /suit/sass/components/_cards.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | @include suit-box-shadow(); 3 | border: none; 4 | .card-header { 5 | color: $card-cap-color; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /suit/sass/components/_confirmations.scss: -------------------------------------------------------------------------------- 1 | body.delete-confirmation { 2 | .content-wrap { 3 | display: block; 4 | @extend .alert; 5 | @extend .alert-danger; 6 | margin: 1.5rem $nav-padding-horizontal; 7 | //padding: 1.5rem $header-padding-horizontal; 8 | h1, h2, ul { 9 | display: block; 10 | float: none; 11 | } 12 | h1 { 13 | font-size: $font-size-h2; 14 | } 15 | h2 { 16 | margin-top: 1.2rem; 17 | font-size: $font-size-h5; 18 | } 19 | form { 20 | margin-top: 2rem; 21 | } 22 | input[type='button'], input[type='submit'], button { 23 | @extend .btn; 24 | @extend .btn-lg; 25 | margin-right: 1.5rem; 26 | } 27 | input[type='button'], button { 28 | @extend .btn-secondary; 29 | } 30 | input[type='submit'] { 31 | @extend .btn-danger; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /suit/sass/components/_icons.scss: -------------------------------------------------------------------------------- 1 | @mixin fa-icon-font() { 2 | font-family: FontAwesome; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-rendering: auto; 6 | } 7 | 8 | $icon-ok: "\f00c"; 9 | $icon-remove: "\f00d"; 10 | $icon-cog: "\f013"; 11 | $icon-home: "\f015"; 12 | $icon-plus: "\f067"; 13 | $icon-plus-circle: "\f055"; 14 | $icon-pencil: "\f040"; 15 | $icon-chevron-up: "\f077"; 16 | $icon-chevron-down: "\f078"; 17 | $icon-search: "\f002"; 18 | $icon-calendar: "\f073"; 19 | $icon-calendar-o: "\f133"; 20 | $icon-time: "\f017"; 21 | .link-with-icon { 22 | .fa { 23 | vertical-align: text-bottom; 24 | margin-left: 3px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /suit/sass/components/_results.scss: -------------------------------------------------------------------------------- 1 | $table-border-color: darken(#fff, 5%); 2 | $tr-odd-bg-color: $table-bg-accent; 3 | $tr-hover-bg-color: darken(#fff, 10%); 4 | $tr-odd-hover-bg-color: darken($tr-odd-bg-color, 6%); 5 | 6 | #result_list { 7 | width: 100%; 8 | border: 0; 9 | background-color: #fff; 10 | //border-spacing: 1px; 11 | border-collapse: collapse; 12 | thead { 13 | > tr { 14 | th { 15 | box-shadow: inset 1px 0 0 rgba(255, 255, 255, .2); 16 | font-weight: normal; 17 | background-color: $inverse-lightest; 18 | color: $gray-lighter; 19 | padding: 0; 20 | position: relative; 21 | line-height: normal; 22 | &.action-checkbox-column { 23 | width: 2rem; 24 | } 25 | &.sorted { 26 | background-color: $inverse-lighter; 27 | &:after { 28 | content: ''; 29 | position: absolute; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | height: 3px; 34 | background-color: $link-color-brighter; 35 | } 36 | } 37 | .sortoptions { 38 | float: right; 39 | font-size: $font-size-xs; 40 | margin: .1rem .3rem 0 0; 41 | .sortpriority, .sortremove, .toggle { 42 | display: block; 43 | float: right; 44 | padding: .2rem; 45 | } 46 | .sortremove, .toggle { 47 | &:hover { 48 | text-decoration: none; 49 | &:before { 50 | color: $link-color-brighter; 51 | } 52 | } 53 | &:before { 54 | @include fa-icon-font(); 55 | } 56 | } 57 | .sortremove:before { 58 | content: $icon-remove; 59 | } 60 | .toggle.ascending:before { 61 | content: $icon-chevron-up; 62 | } 63 | .toggle.descending:before { 64 | content: $icon-chevron-down; 65 | } 66 | } 67 | div.text { 68 | span, a { 69 | display: block; 70 | padding: $table-th-padding; 71 | } 72 | } 73 | a { 74 | color: $gray-lighter; 75 | display: block; 76 | } 77 | } 78 | } 79 | } 80 | tbody { 81 | > tr { 82 | > td, > th { 83 | padding: $table-cell-padding; 84 | font-size: $font-size-xs; 85 | //box-shadow: inset 1px 1px 0 #fff; 86 | //border-color: $table-border-color; 87 | border: 1px solid $table-border-color; 88 | border-left: 0; 89 | border-right: 0; 90 | } 91 | > th { 92 | @extend .text-semibold; 93 | } 94 | &:nth-child(even) { 95 | background-color: $tr-odd-bg-color; 96 | } 97 | &:nth-child(even):hover { 98 | background-color: $tr-odd-hover-bg-color; 99 | } 100 | &:hover { 101 | background-color: $tr-hover-bg-color; 102 | > th, > td { 103 | border-color: transparent; 104 | } 105 | } 106 | &:first-child { 107 | > th, > td { 108 | border-top: 0 !important; 109 | } 110 | } 111 | &.selected { 112 | background-color: lighten($inverse-lightest, 15%) !important; 113 | &:hover { 114 | background-color: darken($inverse-lightest, 5%); 115 | } 116 | &:nth-child(even) { 117 | background-color: lighten($inverse-lightest, 5%) !important; 118 | &:hover { 119 | background-color: darken($inverse-lightest, 5%); 120 | } 121 | } 122 | > th, > td { 123 | border-color: transparent; 124 | &, a { 125 | color: #fff; 126 | } 127 | } 128 | } 129 | // Bootstrap colors 130 | &.table-danger, &.table-warning, &.table-info, &.table-success { 131 | &.selected { 132 | > td, > th { 133 | background-color: transparent; 134 | } 135 | } 136 | } 137 | &.table-danger { 138 | > td, > th { 139 | border-bottom: 1px solid $state-danger-border; 140 | } 141 | } 142 | &.table-warning { 143 | > td, > th { 144 | border-bottom: 1px solid $state-warning-border; 145 | } 146 | } 147 | &.table-info { 148 | > td, > th { 149 | border-bottom: 1px solid $state-info-border; 150 | } 151 | } 152 | &.table-success { 153 | > td, > th { 154 | border-bottom: 1px solid $state-success-border; 155 | } 156 | } 157 | // Per cell styling 158 | th, td { 159 | &.table-danger { 160 | background-color: $state-danger-bg; 161 | border-bottom: 1px solid $state-danger-border; 162 | } 163 | &.table-warning { 164 | background-color: $state-warning-bg; 165 | border-bottom: 1px solid $state-warning-border; 166 | } 167 | &.table-info { 168 | background-color: $state-info-bg; 169 | border-bottom: 1px solid $state-info-border; 170 | } 171 | &.table-success { 172 | background-color: $state-success-bg; 173 | border-bottom: 1px solid $state-success-border; 174 | } 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /suit/sass/components/_sortables.scss: -------------------------------------------------------------------------------- 1 | /* TabularInlines Sortables */ 2 | .inline-sortable { 3 | white-space: nowrap; 4 | a { 5 | color: #000; 6 | padding: .3rem .4rem 0 .2rem; 7 | #result_list & { 8 | padding-top: .05rem; 9 | } 10 | display: inline-block; 11 | opacity: .4; 12 | &:last-child { 13 | padding: 0; 14 | } 15 | &:hover { 16 | opacity: .7; 17 | } 18 | } 19 | } 20 | // For debugging purposes 21 | .suit-sortable { 22 | //display: block !important; 23 | color: $body-color; 24 | } 25 | .selected td { 26 | .inline-sortable a { 27 | color: #fff; 28 | &:hover { 29 | } 30 | } 31 | } 32 | tr:first-child td .inline-sortable .sortable-up { 33 | visibility: hidden; 34 | cursor: default; 35 | } 36 | .tabular { 37 | // Class added by sortable JS 38 | tr.last-sortable, 39 | tr.form-row:nth-last-child(2) 40 | // Can't use following 3rd child as it will be wrong if max inlines limit is reached 41 | //tr.form-row:nth-last-child(3), 42 | { 43 | td .inline-sortable .sortable-down { 44 | visibility: hidden; 45 | cursor: default; 46 | } 47 | } 48 | } 49 | #result_list tr:last-child { 50 | td .inline-sortable .sortable-down { 51 | visibility: hidden; 52 | cursor: default; 53 | } 54 | } 55 | /* StackedInline sortables */ 56 | .stacked-inline-sortable { 57 | float: left; 58 | :first-child { 59 | padding-right: 1px; 60 | } 61 | &:nth-last-child(2) { 62 | margin-right: 10px; 63 | } 64 | a { 65 | color: $gray-lighter; 66 | &:hover { 67 | color: #fff; 68 | } 69 | } 70 | } 71 | .inline-group > div:first-of-type .stacked-inline-sortable .sortable-up, 72 | .inline-group > div:nth-last-child(3) .stacked-inline-sortable .sortable-down { 73 | opacity: .15 !important; 74 | cursor: default; 75 | } 76 | -------------------------------------------------------------------------------- /suit/sass/components/_submit_row.scss: -------------------------------------------------------------------------------- 1 | .submit-row { 2 | .deletelink-box { 3 | float: right; 4 | margin: 0; 5 | .deletelink { 6 | @extend .btn; 7 | @extend .btn-outline-danger; 8 | } 9 | } 10 | input[type='submit'], input[type='button'], button, .btn { 11 | margin-bottom: .5rem; 12 | } 13 | input[type='submit'], input[type='button'], button { 14 | &:not([class*="btn-"]) { 15 | @extend .btn; 16 | @extend .btn-lg; 17 | @extend .btn-secondary; 18 | &:first-child { 19 | @extend .btn-primary; 20 | } 21 | } 22 | } 23 | &.fixed { 24 | position: fixed; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | background-color: #fff; 29 | padding: 1.2rem $header-padding-horizontal 1rem; 30 | z-index: 5000; 31 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.15); 32 | } 33 | } 34 | body.suit_form_submit_on_right .col-right { 35 | .submit-row { 36 | @include media-breakpoint-up(lg) { 37 | position: relative; 38 | padding: $grid-gutter-width-base/2; 39 | background-color: $form-bg; 40 | display: flex; 41 | flex-wrap: wrap; 42 | @include suit-box-shadow(); 43 | a.btn:not(.deletelink), button, input[type='submit'], input[type='button'] { 44 | padding-left: 0; 45 | padding-right: 0; 46 | } 47 | > * { 48 | width: 100%; 49 | } 50 | .deletelink-box { 51 | text-align: right; 52 | margin-top: $grid-gutter-width-base/2; 53 | order: 10; 54 | } 55 | } 56 | } 57 | .object-tools { 58 | @include reset-list(); 59 | margin-top: $grid-gutter-width-base; 60 | //padding: $grid-gutter-width/2; 61 | @include media-breakpoint-down(md) { 62 | display: none; 63 | li { 64 | display: inline-block; 65 | margin-right: 10px; 66 | &.heading { 67 | display: block; 68 | font-weight: bold; 69 | } 70 | } 71 | } 72 | @include media-breakpoint-up(lg) { 73 | li { 74 | &.list-item, &:not(.list-item) > a { 75 | display: block; 76 | margin-top: 1px; 77 | background-color: $form-bg; 78 | padding: $grid-gutter-width-base/3 $grid-gutter-width-base/2; 79 | @include suit-box-shadow(); 80 | } 81 | &:not(.list-item) a { 82 | &:hover { 83 | text-decoration: none; 84 | background-color: #fff; 85 | } 86 | .fa { 87 | margin-right: .2rem; 88 | } 89 | } 90 | &.heading { 91 | @include text-semibold(); 92 | font-size: $font-size-lg; 93 | padding: 0 $grid-gutter-width-base/3 $grid-gutter-width-base/4; 94 | &.heading-inverse { 95 | background-color: $inverse-lightest; 96 | color: #fff; 97 | font-weight: normal; 98 | font-size: $font-size-base; 99 | padding: $grid-gutter-width-base/4 $grid-gutter-width-base/3; 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /suit/sass/components/_tables.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | background-color: #fff; 3 | } 4 | .table-inverse { 5 | color: $gray-lighter; 6 | background-color: #fff; 7 | th, 8 | td, 9 | thead th { 10 | border-color: $table-border-color; 11 | font-weight: normal; 12 | } 13 | thead th { 14 | background-color: $inverse-lightest; 15 | } 16 | &:not(.table-bordered) { 17 | thead th:not(:first-child) { 18 | box-shadow: inset 1px 0 0 rgba(255, 255, 255, .2); 19 | } 20 | } 21 | tbody { 22 | th, td { 23 | color: $body-color; 24 | }; 25 | } 26 | } 27 | .thead-inverse { 28 | th { 29 | background-color: $inverse-lightest; 30 | color: $gray-lighter; 31 | font-weight: normal; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /suit/sass/components/_tabs.scss: -------------------------------------------------------------------------------- 1 | .nav-tabs-suit { 2 | margin-bottom: 1rem; 3 | .nav-item { 4 | &:first-child { 5 | margin-left: .75rem; 6 | } 7 | + .nav-item { 8 | margin-left: .3rem; 9 | } 10 | } 11 | .nav-link { 12 | border-radius: 2px; 13 | padding: 0.65em 1.5em; 14 | background-color: #fff; 15 | border-color: $nav-tabs-border-color; 16 | @include hover-focus { 17 | $border-hover: lighten(desaturate($link-color-brighter, 45%), 15%); 18 | //$border-hover: #fff; 19 | border-color: $border-hover $border-hover $nav-tabs-border-color; 20 | } 21 | &.active { 22 | &, &:focus { 23 | color: #222; 24 | @extend .text-semibold; 25 | } 26 | } 27 | &.has-error { 28 | &, &.active, &:focus { 29 | color: $state-danger-text; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /suit/sass/layout/_content.scss: -------------------------------------------------------------------------------- 1 | body.change-form { 2 | #container { 3 | br.clear:last-child { 4 | display: none; 5 | } 6 | } 7 | } 8 | #content { 9 | padding: 1.5rem $header-padding-horizontal; 10 | display: flex; 11 | flex-direction: row; 12 | flex-wrap: wrap; 13 | justify-content: flex-start; 14 | align-content: flex-end; 15 | align-items: flex-start; 16 | .content-wrap { 17 | body.dashboard & { 18 | display: flex; 19 | } 20 | flex-basis: 100%; 21 | > h1 { 22 | flex-basis: 100%; 23 | } 24 | > h1:first-child, > .messagelist + h1 { 25 | display: none; 26 | } 27 | } 28 | 29 | #content-main { 30 | flex-grow: 1; 31 | } 32 | #content-related { 33 | flex-grow: 1; 34 | } 35 | } 36 | #content-main { 37 | > .object-tools { 38 | @include reset-list(); 39 | position: relative; 40 | overflow: hidden; 41 | z-index: 6; 42 | > li { 43 | display: inline; 44 | &:not(:first-child) { 45 | margin-left: .5rem; 46 | } 47 | > a { 48 | &:not([class*="btn-"]) { 49 | @extend .btn-round; 50 | @include btn-mixin(info, none, sm, round=true); 51 | &.addlink { 52 | @include btn-mixin(success, $icon-plus-circle, sm, round=true); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /suit/sass/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles */ 2 | @include media-breakpoint-up(sm) { 3 | html { 4 | position: relative; 5 | min-height: 100%; 6 | } 7 | body { 8 | &.suit_layout_horizontal, &.login { 9 | margin-bottom: $footer-height; 10 | #footer { 11 | position: absolute; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | width: 100%; 16 | min-height: $footer-height; 17 | margin-top: -$footer-height; 18 | } 19 | } 20 | } 21 | } 22 | 23 | /* Styles */ 24 | .footer { 25 | min-height: $footer-height; 26 | background-color: darken($body-bg, 5%); 27 | color: $gray; 28 | font-size: $font-size-sm; 29 | > .container-fluid { 30 | padding: 1rem $grid-gutter-width-base; 31 | > .row { 32 | justify-content: center; 33 | align-content: center; 34 | align-items: center; 35 | > div { 36 | &:not(:first-child) { 37 | @include media-breakpoint-down(xs) { 38 | margin-top: .75rem; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | .footer-links { 45 | a { 46 | white-space: nowrap; 47 | &:not(:last-child) { 48 | margin-right: .75rem; 49 | } 50 | } 51 | } 52 | } 53 | 54 | /* Override BS4 [hidden] in reboot.scss to show Django debug toolbar */ 55 | #djDebug { 56 | &[hidden], [hidden][style*="display: block"], [hidden][style*="display:block"] { 57 | display: block !important; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /suit/sass/layout/_header.scss: -------------------------------------------------------------------------------- 1 | body.suit_layout_horizontal { 2 | #header { 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | justify-content: flex-start; 7 | align-content: center; 8 | align-items: center; 9 | background-color: $header-bg; 10 | padding-top: $header-padding-vertical; 11 | a { 12 | color: $header-color; 13 | } 14 | #branding, #user-tools { 15 | flex-basis: map-get($grid-breakpoints, sm)/2; 16 | } 17 | .header-label { 18 | font-size: $font-size-h1/2; 19 | color: $header-muted-color; 20 | text-transform: uppercase; 21 | font-weight: normal; 22 | } 23 | #branding { 24 | min-width: $sidebar-width; 25 | padding-left: $header-padding-horizontal; 26 | #site-name { 27 | font-size: $font-size-h1; 28 | a { 29 | display: inline-block; 30 | &:hover { 31 | text-decoration: none; 32 | color: transparentize($header-color, .2); 33 | } 34 | .header-label { 35 | display: block; 36 | margin-top: 2px; 37 | text-align: right; 38 | @include media-breakpoint-down(sm) { 39 | text-align: left; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | #user-tools { 46 | flex-grow: 2; 47 | padding: 0 $header-padding-horizontal; 48 | text-align: right; 49 | color: $header-muted-color; 50 | //font-size: $font-size-sm; 51 | strong { 52 | color: $header-color; 53 | @extend .text-light; 54 | } 55 | a { 56 | display: inline-block; 57 | margin: 0 2px; 58 | &:nth-child(2):not(:last-child) { 59 | margin-left: 20px; 60 | } 61 | color: lighten(desaturate($link-color-bright, 5%), 20%); 62 | &:hover { 63 | color: #fff; 64 | text-decoration: none; 65 | } 66 | } 67 | } 68 | .suit-user-tools { 69 | .welcome { 70 | display: inline-block; 71 | } 72 | .user-links { 73 | display: inline-block; 74 | } 75 | } 76 | #site-name { 77 | margin: 0; 78 | font-weight: normal; 79 | a { 80 | color: $header-color; 81 | } 82 | } 83 | } 84 | } 85 | body.suit_layout_vertical { 86 | #header { 87 | background-color: $header-bg; 88 | #branding { 89 | padding: $header-padding-vertical $header-padding-horizontal/3 $header-padding-vertical; 90 | #site-name { 91 | margin: 0; 92 | font-weight: normal; 93 | font-size: $font-size-h3; 94 | line-height: $line-height-lg; 95 | text-align: center; 96 | a { 97 | display: inline-block; 98 | &:hover { 99 | text-decoration: none; 100 | color: transparentize($header-color, .2); 101 | } 102 | .header-label { 103 | display: none; 104 | margin-top: 2px; 105 | text-align: left; 106 | } 107 | } 108 | } 109 | } 110 | #site-name { 111 | a { 112 | color: $header-color; 113 | } 114 | } 115 | a { 116 | color: $header-color; 117 | } 118 | .header-label { 119 | font-size: $font-size-h1/2; 120 | color: $header-muted-color; 121 | text-transform: uppercase; 122 | font-weight: normal; 123 | } 124 | #user-tools:not(.suit-user-tools) { 125 | padding: $header-padding-vertical/1.2 $header-padding-horizontal/3; 126 | font-size: $font-size-xs; 127 | color: $header-muted-color; 128 | background-color: darken($top-nav-bg, 7%); 129 | strong { 130 | color: $header-color; 131 | @extend .text-light; 132 | } 133 | a { 134 | display: inline-block; 135 | margin: 0 2px; 136 | &:nth-child(2):not(:last-child) { 137 | margin-left: 5px; 138 | } 139 | //margin-right: .25rem; 140 | color: lighten(desaturate($link-color-bright, 5%), 20%); 141 | } 142 | } 143 | .suit-user-tools { 144 | background-color: $top-nav-bg; 145 | padding: $header-padding-vertical*1.1 $header-padding-horizontal/2; 146 | text-align: center; 147 | font-size: 13px; 148 | .separator { 149 | display: none; 150 | } 151 | .welcome { 152 | display: block; 153 | font-size: 12px; 154 | color: $header-muted-color; 155 | margin-bottom: .25rem; 156 | strong { 157 | color: $header-color; 158 | font-weight: normal; 159 | } 160 | .fa, .icon-link:before { 161 | margin-right: 2px; 162 | font-size: 13px; 163 | } 164 | } 165 | .user-links { 166 | display: inline-block; 167 | margin-right: .75rem; 168 | margin-bottom: .25rem; 169 | } 170 | .icon-link { 171 | @include text-hide(); 172 | display: inline-block; 173 | padding: .4rem 0; 174 | width: 26px; 175 | text-align: center; 176 | border-radius: $border-radius-sm; 177 | position: relative; 178 | transition: background-color .2s, color .2s; 179 | color: #fff; 180 | &:hover { 181 | background-color: darken($top-nav-bg, 7%); 182 | color: $link-color-brighter; 183 | &:after { 184 | opacity: 1; 185 | } 186 | } 187 | &:before { 188 | @include fa-icon-font(); 189 | font-size: $font-size-lg; 190 | line-height: normal; 191 | display: inline-block; 192 | } 193 | &:after { 194 | font-family: $font-family-base; 195 | display: block; 196 | position: absolute; 197 | content: attr(data-title); 198 | left: 0; 199 | top: 40px; 200 | font-size: 10px; 201 | white-space: nowrap; 202 | opacity: 0; 203 | transition: opacity .2s; 204 | } 205 | &.view-site-link:before { 206 | content: $icon-home; 207 | } 208 | &.change-password-link:before { 209 | content: "\f084"; 210 | } 211 | &.documentation-link:before { 212 | content: "\f02d"; 213 | } 214 | &.logout-link:before { 215 | content: "\f08b"; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /suit/sass/layout/_navbars.scss: -------------------------------------------------------------------------------- 1 | body.suit_layout_horizontal { 2 | #suit-nav { 3 | flex-basis: 100%; 4 | margin-top: $header-padding-vertical; 5 | background-color: $top-nav-bg; 6 | > ul { 7 | @include reset-list(); 8 | margin: 0 $header-padding-horizontal; 9 | > li { 10 | position: relative; 11 | display: block; 12 | float: left; 13 | a { 14 | display: block; 15 | //font-size: $font-size-sm; 16 | color: transparentize(#fff, .3); 17 | &:hover { 18 | color: #fff; 19 | text-decoration: none; 20 | } 21 | } 22 | > a { 23 | position: relative; 24 | padding: $header-padding-vertical/1.5 $header-padding-horizontal/1.4; 25 | border-left: 1px solid $header-bg; 26 | } 27 | &:last-child { 28 | > a { 29 | border-right: 1px solid $header-bg; 30 | } 31 | } 32 | &.active { 33 | a { 34 | background-color: #fff; 35 | color: $body-color; 36 | font-weight: bold; 37 | box-shadow: inset 0 3px 0 $link-color-brighter; 38 | &:hover { 39 | @include gradient-y(darken($body-bg, 3%), #fff); 40 | } 41 | } 42 | } 43 | &:hover { 44 | > a { 45 | background-color: lighten($top-nav-bg, 5%); 46 | } 47 | > ul { 48 | background-color: lighten($top-nav-bg, 5%); 49 | display: block; 50 | } 51 | } 52 | > ul { 53 | @include reset-list(); 54 | z-index: 1000; 55 | display: none; 56 | background-color: $top-nav-bg; 57 | position: absolute; 58 | min-width: 180px; 59 | box-shadow: 0 -1px 2px 0 rgba(0, 0, 0, .07); 60 | font-size: $font-size-sm; 61 | > li { 62 | > a { 63 | display: block; 64 | padding: $header-padding-vertical/2 $header-padding-horizontal/1.4; 65 | border-bottom: 1px solid darken($top-nav-bg, 5%); 66 | &:hover { 67 | background-color: darken($top-nav-bg, 2%); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | &.suit-nav-right { 74 | float: right; 75 | > li > ul { 76 | right: 0; 77 | } 78 | } 79 | } 80 | } 81 | #suit-sub-nav { 82 | flex-basis: 100%; 83 | display: block; 84 | background-color: #fff; 85 | box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.15); 86 | ul { 87 | @include reset-list(); 88 | margin: 0 $nav-padding-horizontal; 89 | > li { 90 | > a { 91 | color: $link-color; 92 | display: block; 93 | float: left; 94 | font-size: $font-size-sm; 95 | padding: $header-padding-vertical/1.3 4px; 96 | margin: 2px $header-padding-horizontal/3.75 0; 97 | margin-bottom: -3px; 98 | } 99 | &:first-child { 100 | a { 101 | margin-left: 0; 102 | } 103 | } 104 | &.active { 105 | a { 106 | color: $body-color; 107 | font-weight: bold; 108 | border-bottom: 3px solid $link-color-bright; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | body.suit_layout_vertical { 116 | #suit-nav { 117 | flex-basis: 100%; 118 | background-color: $top-nav-bg; 119 | > ul { 120 | @include reset-list(); 121 | > li { 122 | position: relative; 123 | a { 124 | &:hover, &:focus { 125 | text-decoration: none; 126 | } 127 | } 128 | &.active { 129 | //background-color: $link-color-brighter; 130 | background-color: $header-bg; 131 | &:after { 132 | @include fa-icon-font(); 133 | color: $inverse-lightest; 134 | position: absolute; 135 | right: 1rem; 136 | top: .85rem; 137 | font-size: 9px; 138 | } 139 | &.has-children { 140 | &:after { 141 | content: "\f078"; 142 | } 143 | } 144 | &:not(.has-children) { 145 | &:after { 146 | content: "\f054"; 147 | } 148 | } 149 | > a { 150 | &, &:hover { 151 | color: #fff; 152 | } 153 | } 154 | > ul { 155 | display: block; 156 | } 157 | } 158 | > a { 159 | display: block; 160 | padding: .6rem $header-padding-horizontal/1.5; 161 | color: transparentize(#fff, .3); 162 | .fa { 163 | margin-right: .7rem; 164 | } 165 | } 166 | &:not(.active) { 167 | a { 168 | &:hover { 169 | color: #fff; 170 | } 171 | } 172 | &:hover { 173 | background-color: darken($top-nav-bg, 5%); 174 | > ul { 175 | box-shadow: 0 0 2px 2px rgba(0, 0, 0, .1); 176 | top: 0; 177 | left: 70%; 178 | display: block; 179 | position: absolute; 180 | z-index: 1000; 181 | padding: 0; 182 | > li { 183 | > a { 184 | white-space: nowrap; 185 | &:hover { 186 | background-color: $inverse-lighter; 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | > ul { 194 | @include reset-list(); 195 | background-color: #fff; 196 | display: none; 197 | font-size: 13px; 198 | padding: 0; 199 | > li { 200 | &:not(:last-child){ 201 | border-bottom: 1px solid lighten($body-bg, 2%); 202 | } 203 | &.active { 204 | background-color: #fff; 205 | a { 206 | box-shadow: inset 4px 0 0 $link-color-brighter; 207 | &, &:hover, &:focus { 208 | color: $link-color-brighter; 209 | } 210 | } 211 | } 212 | a { 213 | color: $header-muted-color; 214 | display: block; 215 | padding: .4rem $header-padding-horizontal/1.5; 216 | &:hover { 217 | background-color: lighten($body-bg, 2%); 218 | color: darken($header-muted-color, 25%); 219 | } 220 | //border-bottom: 1px solid $body-bg; 221 | } 222 | } 223 | } 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /suit/sass/layout/_vertical.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body.suit_layout_vertical:not(.login) { 6 | display: flex; 7 | min-height: 100vh; 8 | //flex-direction: column; 9 | #container { 10 | flex: 1; 11 | display: flex; 12 | position: relative; 13 | align-content: flex-start; 14 | #header { 15 | display: flex; 16 | flex-direction: column; 17 | width: $vertical-menu-width; 18 | flex-shrink: 0; 19 | } 20 | #content { 21 | flex: 1; 22 | align-items: flex-start; 23 | align-content: flex-start; 24 | flex-wrap: wrap; 25 | padding-bottom: $footer-height + 30px; // Footer compensation 26 | padding-top: 4rem; // Breadcrumbs compensation 27 | } 28 | #footer { 29 | left: $vertical-menu-width; 30 | bottom: 0; 31 | right: 0; 32 | position: absolute; 33 | flex-wrap: wrap; 34 | flex-basis: 100%; 35 | //width: 100%; 36 | } 37 | 38 | } 39 | > #footer { 40 | 41 | } 42 | &.dashboard:not([class*="app-"]) { 43 | #container { 44 | #content { 45 | padding-top: $grid-gutter-width-base; 46 | } 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /suit/sass/pages/_changeform.scss: -------------------------------------------------------------------------------- 1 | body.change-form { 2 | #content-main { 3 | > .object-tools { 4 | float: right; 5 | margin-top: -2.85rem; 6 | margin-bottom: 20px; 7 | overflow: hidden; 8 | li { 9 | display: inline-block; 10 | &.list-item { 11 | display: none; 12 | } 13 | } 14 | } 15 | > .object-tools + form { 16 | width: 100%; 17 | clear: both; 18 | } 19 | } 20 | .edit-row { 21 | display: flex; 22 | flex-wrap: wrap; 23 | > div { 24 | margin: 0 $grid-gutter-width-base/2; 25 | } 26 | } 27 | &:not(.suit_form_submit_on_right){ 28 | .edit-row { 29 | > div { 30 | &.col-left, &.col-right { 31 | flex: 1; 32 | flex-basis: 100%; 33 | } 34 | &.col-right { 35 | .object-tools { 36 | display: none; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | &.suit_form_submit_on_right { 43 | // Hide object-tools from top in 2column layout 44 | #content-main { 45 | > .object-tools { 46 | margin-top: 0; 47 | li.heading { 48 | display: none; 49 | } 50 | @include media-breakpoint-up(lg) { 51 | display: none; 52 | } 53 | } 54 | } 55 | .edit-row { 56 | display: flex; 57 | > div { 58 | margin: 0 $grid-gutter-width-base/2; 59 | &.col-left { 60 | flex: 1; 61 | } 62 | &.col-right { 63 | width: 22%; 64 | @include media-breakpoint-only(lg) { 65 | width: 25%; 66 | } 67 | @include media-breakpoint-down(md) { 68 | width: auto; 69 | flex: 1; 70 | flex-basis: 100%; 71 | } 72 | } 73 | } 74 | 75 | } 76 | } 77 | } 78 | // History table 79 | table#change-history { 80 | @extend .table; 81 | @extend #result_list; 82 | thead { 83 | > tr th { 84 | padding: $table-th-padding !important; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /suit/sass/pages/_changelist.scss: -------------------------------------------------------------------------------- 1 | $filter-col-width: 15rem; 2 | #changelist { 3 | display: block; 4 | position: relative; 5 | &.filtered { 6 | clear: both; 7 | display: flex; 8 | flex-wrap: wrap; 9 | #toolbar { 10 | flex: 10; 11 | flex-basis: 100%; 12 | } 13 | #changelist-filter { 14 | &:not(:empty) { 15 | flex-basis: 15rem; 16 | order: 3; 17 | } 18 | &:empty { 19 | display: none; 20 | } 21 | } 22 | } 23 | #changelist-filter:not(:empty) + #changelist-form { 24 | margin-right: 2rem; 25 | } 26 | #toolbar { 27 | margin-bottom: 1.5rem; 28 | #changelist-search { 29 | label { 30 | display: none; 31 | } 32 | input[type='submit'] { 33 | @extend .btn; 34 | @extend .btn-primary; 35 | @include btn-mixin(primary, $icon-plus-circle); 36 | margin-right: 1rem; 37 | } 38 | input[type='text'] { 39 | @extend .form-control; 40 | display: inline-block; 41 | width: auto; 42 | vertical-align: middle; 43 | &:before { 44 | @include fa-icon-font(); 45 | content: $icon-plus; 46 | } 47 | } 48 | .small { 49 | font-size: $font-size-sm; 50 | } 51 | } 52 | } 53 | #changelist-filter { 54 | float: right; 55 | width: $filter-col-width; 56 | //position: absolute; 57 | top: 0; 58 | right: 0; 59 | font-size: $font-size-sm; 60 | h2 { 61 | margin-bottom: $grid-gutter-width-base/2; 62 | display: none; 63 | } 64 | h3 { 65 | background-color: $inverse-lightest; 66 | color: #fff; 67 | font-size: $font-size-sm; 68 | font-weight: normal; 69 | padding: $grid-gutter-width-base/4 $grid-gutter-width-base/3; 70 | margin: 0; 71 | } 72 | ul { 73 | background-color: #fff; 74 | @include reset-list(); 75 | padding: $grid-gutter-width-base/3 0; 76 | margin-bottom: $grid-gutter-width-base/1.5; 77 | li { 78 | border-left: 3px solid transparent; 79 | margin-left: -3px; 80 | &.selected { 81 | &:not(:first-child) { 82 | border-left-color: $link-color-bright; 83 | } 84 | a { 85 | font-weight: bold; 86 | color: $body-color; 87 | } 88 | } 89 | a { 90 | padding: .1rem; 91 | padding-left: $grid-gutter-width-base / 1.5; 92 | display: block; 93 | } 94 | } 95 | } 96 | } 97 | #changelist-form { 98 | flex: 1; 99 | .actions { 100 | margin: -.5rem 0 1rem 0; 101 | font-size: $font-size-sm; 102 | align-items: baseline; 103 | &[style*='block'] { 104 | display: flex !important; 105 | } 106 | button { 107 | //line-height: default; 108 | @extend .btn; 109 | @extend .btn-secondary; 110 | @include btn-mixin(primary, null); 111 | margin-right: 1rem; 112 | } 113 | select { 114 | @extend .form-control; 115 | display: inline; 116 | width: auto; 117 | height: 2.05rem; 118 | margin-right: .25rem; 119 | } 120 | span.all, 121 | span.action-counter, 122 | span.clear, 123 | span.question { 124 | font-size: 13px; 125 | margin: 0 0.5em; 126 | display: none; 127 | } 128 | } 129 | // Hide actions, show only after results 130 | .actions { 131 | .suit_toggle_changelist_top_actions & { 132 | display: none; 133 | } 134 | } 135 | // Actions after results 136 | .results + .actions { 137 | .suit_toggle_changelist_top_actions & { 138 | display: flex; 139 | } 140 | margin: 1rem 0 0; 141 | } 142 | 143 | .paginator { 144 | margin-top: 1rem; 145 | font-size: $font-size-sm; 146 | a:not(.showall), span { 147 | font-size: 1rem; 148 | display: inline-block; 149 | //float: left; 150 | padding: .5rem; 151 | background-color: #fff; 152 | line-height: normal; 153 | min-width: 2.3rem; 154 | text-align: center; 155 | margin-left: -.1rem; 156 | &.end { 157 | margin-right: 1rem; 158 | } 159 | } 160 | a { 161 | &:hover { 162 | background-color: lighten($body-bg, 3%); 163 | text-decoration: none; 164 | } 165 | } 166 | span.this-page { 167 | background-color: $inverse-lightest; 168 | color: #fff; 169 | } 170 | // This is odd, but django admin has Save in paginator 171 | input[type='submit'] { 172 | @extend .btn; 173 | @extend .btn-primary; 174 | float: right; 175 | margin-top: -.75rem; 176 | } 177 | } 178 | } 179 | // Django date-hierarchy feature 180 | .xfull { 181 | flex-basis: 100%; 182 | } 183 | .toplinks { 184 | @include reset-list(); 185 | margin-right: $filter-col-width + 2rem; 186 | li { 187 | display: inline-block; 188 | a { 189 | margin-right: .28rem; 190 | display: inline-block; 191 | } 192 | } 193 | } 194 | } 195 | 196 | // Only add negative margin if object-tools are present 197 | .object-tools + #changelist.filtered { 198 | #toolbar { 199 | margin-top: -3.5rem; 200 | } 201 | } 202 | 203 | body.change-list { 204 | #content-main { 205 | .object-tools { 206 | float: right; 207 | margin-bottom: 1.5rem; 208 | min-height: 2rem; 209 | } 210 | } 211 | .hiddenfields { 212 | display: none; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /suit/sass/pages/_dashboard.scss: -------------------------------------------------------------------------------- 1 | body.dashboard { 2 | .module { 3 | margin: 0 $grid-gutter-width-base $grid-gutter-width-base/2 0; 4 | table { 5 | width: 100%; 6 | border-spacing: 1px; 7 | border-collapse: separate; 8 | caption { 9 | caption-side: inherit; 10 | font-weight: $headings-font-weight; 11 | font-size: $font-size-h4; 12 | padding: 0; 13 | } 14 | tr { 15 | td, th { 16 | font-size: $font-size-sm; 17 | padding: $table-cell-padding; 18 | background-color: lighten($body-bg, 2%); 19 | } 20 | th { 21 | width: 200px; 22 | background-color: #fff; 23 | font-weight: normal; 24 | } 25 | td { 26 | 27 | } 28 | } 29 | } 30 | } 31 | #recent-actions-module { 32 | h2 { 33 | display: none; 34 | } 35 | h3 { 36 | font-size: $font-size-h4; 37 | } 38 | ul.actionlist { 39 | @include reset-list(); 40 | > li { 41 | background-color: lighten($body-bg, 2%); 42 | margin-bottom: 1px; 43 | padding: $table-cell-padding; 44 | font-size: $font-size-sm; 45 | padding-left: 2.8rem; 46 | overflow: auto; 47 | position: relative; 48 | span { 49 | display: block; 50 | float: right; 51 | color: $text-muted; 52 | font-size: $font-size-xs; 53 | } 54 | br { 55 | display: none; 56 | } 57 | &:before { 58 | min-width: 2rem; 59 | text-align: center; 60 | //position: relative; 61 | background-color: #fff; 62 | border-right: 1px solid $body-bg; 63 | position: absolute; 64 | padding-top: .4rem; 65 | top: 0; 66 | left: 0; 67 | height: 100%; 68 | 69 | } 70 | } 71 | } 72 | } 73 | .addlink, .changelink, .deletelink { 74 | &:before { 75 | @include fa-icon-font(); 76 | display: block; 77 | float: left; 78 | min-width: 1.2rem; 79 | } 80 | } 81 | .addlink { 82 | &:before { 83 | content: $icon-plus; 84 | color: $brand-success; 85 | } 86 | } 87 | .changelink { 88 | &:before { 89 | content: $icon-pencil; 90 | color: $brand-warning; 91 | } 92 | } 93 | .deletelink { 94 | &:before { 95 | content: $icon-remove; 96 | color: $brand-danger; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /suit/sass/pages/_login.scss: -------------------------------------------------------------------------------- 1 | .login #container { 2 | background: #fff; 3 | border-radius: 4px; 4 | overflow: hidden; 5 | width: 28em; 6 | margin: 0 auto; 7 | @include media-breakpoint-up(sm) { 8 | min-width: 400px; 9 | margin-top: 15vh; 10 | } 11 | } 12 | 13 | body.login { 14 | $spacing: 1.5rem; 15 | svg { 16 | * { 17 | fill: $body-color; 18 | } 19 | } 20 | .suit-login-graphic { 21 | display: block; 22 | margin: 2rem auto $spacing; 23 | } 24 | #content { 25 | padding: $spacing; 26 | .errornote { 27 | border-radius: 0; 28 | border: 0; 29 | } 30 | } 31 | #header { 32 | background-color: transparent; 33 | padding: 0; 34 | #branding { 35 | padding: 0; 36 | flex-basis: 100%; 37 | text-align: center; 38 | #site-name { 39 | margin: 0 auto; 40 | &:first-child { 41 | margin-top: $spacing*1.5; 42 | } 43 | a { 44 | @extend .text-semibold; 45 | &, &:hover { 46 | color: $body-color; 47 | } 48 | .header-label { 49 | font-weight: normal; 50 | text-align: center; 51 | margin-top: .3rem; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | .errorlist { 58 | @include reset-list(); 59 | margin: -.5rem $spacing/2 $spacing; 60 | color: $alert-danger-text; 61 | } 62 | .form-row { 63 | input:not([type='hidden']) { 64 | padding-left: $spacing/2; 65 | padding-right: $spacing/2; 66 | margin-bottom: $spacing/1.75; 67 | &:not(:focus) { 68 | background-color: $body-bg; 69 | border-color: $body-bg; 70 | } 71 | } 72 | &.has-danger { 73 | input:not([type='hidden']) { 74 | border-color: $alert-danger-text; 75 | } 76 | } 77 | } 78 | .submit-row { 79 | label { 80 | display: none; 81 | } 82 | input[type='submit'] { 83 | display: block; 84 | width: 100%; 85 | margin: $spacing 0 0 0; 86 | border: none; 87 | //color: #fff !important; 88 | text-align: center; 89 | @extend .btn; 90 | @extend .btn-lg; 91 | @extend .btn-primary; 92 | //background-color: desaturate($link-color-brighter, 5%); 93 | padding: $spacing/1.5 $spacing; 94 | //&:hover, &:focus { 95 | // background-color: $link-color-bright; 96 | // &:active { 97 | // background-color: $link-color; 98 | // } 99 | //} 100 | //&:active { 101 | // background-color: $link-color; 102 | //} 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /suit/sass/suit.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'vendor'; 3 | @import 'mixins'; 4 | 5 | @import 'components/icons'; 6 | @import 'components/buttons'; 7 | @import 'components/forms'; 8 | @import 'components/sortables'; 9 | @import 'components/widgets'; 10 | @import 'components/alerts'; 11 | @import 'components/confirmations'; 12 | @import 'components/submit_row'; 13 | @import 'components/breadcrumbs'; 14 | @import 'components/results'; 15 | @import 'components/tabs'; 16 | @import 'components/tables'; 17 | @import 'components/cards'; 18 | 19 | @import 'layout/vertical'; 20 | @import 'layout/header'; 21 | @import 'layout/navbars'; 22 | @import 'layout/content'; 23 | @import 'layout/footer'; 24 | 25 | 26 | @import 'pages/login'; 27 | @import 'pages/dashboard'; 28 | @import 'pages/changelist'; 29 | @import 'pages/changeform'; 30 | -------------------------------------------------------------------------------- /suit/sortables.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy, copy 2 | from django.contrib import admin 3 | from django.contrib.admin.views.main import ChangeList 4 | from django.contrib.contenttypes.admin import GenericTabularInline, GenericStackedInline 5 | from django.forms import ModelForm, NumberInput 6 | from django.db import models 7 | 8 | 9 | class SortableModelAdminBase(object): 10 | """ 11 | Base class for SortableTabularInline and SortableModelAdmin 12 | """ 13 | sortable = 'order' 14 | 15 | class Media: 16 | js = ('suit/js/suit.sortables.js',) 17 | 18 | 19 | class SortableListForm(ModelForm): 20 | """ 21 | Just Meta holder class 22 | """ 23 | 24 | class Meta: 25 | widgets = { 26 | 'order': NumberInput( 27 | attrs={'class': 'hidden-xs-up suit-sortable'}) 28 | } 29 | 30 | 31 | class SortableChangeList(ChangeList): 32 | """ 33 | Class that forces ordering by sortable param only 34 | """ 35 | 36 | def get_ordering(self, request, queryset): 37 | if self.model_admin.sortable_is_enabled(): 38 | return [self.model_admin.sortable, '-' + self.model._meta.pk.name] 39 | return super(SortableChangeList, self).get_ordering(request, queryset) 40 | 41 | 42 | class SortableTabularInlineBase(SortableModelAdminBase): 43 | """ 44 | Sortable tabular inline 45 | """ 46 | 47 | def __init__(self, *args, **kwargs): 48 | super(SortableTabularInlineBase, self).__init__(*args, **kwargs) 49 | 50 | self.ordering = (self.sortable,) 51 | self.fields = self.fields or [] 52 | if self.fields and self.sortable not in self.fields: 53 | self.fields = list(self.fields) + [self.sortable] 54 | 55 | def formfield_for_dbfield(self, db_field, **kwargs): 56 | if db_field.name == self.sortable: 57 | kwargs['widget'] = SortableListForm.Meta.widgets['order'] 58 | return super(SortableTabularInlineBase, self).formfield_for_dbfield( 59 | db_field, **kwargs) 60 | 61 | 62 | class SortableTabularInline(SortableTabularInlineBase, admin.TabularInline): 63 | pass 64 | 65 | 66 | class SortableGenericTabularInline(SortableTabularInlineBase, 67 | GenericTabularInline): 68 | pass 69 | 70 | 71 | class SortableStackedInlineBase(SortableModelAdminBase): 72 | """ 73 | Sortable stacked inline 74 | """ 75 | 76 | def __init__(self, *args, **kwargs): 77 | super(SortableStackedInlineBase, self).__init__(*args, **kwargs) 78 | self.ordering = (self.sortable,) 79 | 80 | def get_fieldsets(self, *args, **kwargs): 81 | """ 82 | Iterate all fieldsets and make sure sortable is in the first fieldset 83 | Remove sortable from every other fieldset, if by some reason someone 84 | has added it 85 | """ 86 | fieldsets = super(SortableStackedInlineBase, self).get_fieldsets(*args, **kwargs) 87 | 88 | sortable_added = False 89 | for fieldset in fieldsets: 90 | for line in fieldset: 91 | if not line or not isinstance(line, dict): 92 | continue 93 | 94 | fields = line.get('fields') 95 | if self.sortable in fields: 96 | fields.remove(self.sortable) 97 | 98 | # Add sortable field always as first 99 | if not sortable_added: 100 | fields.insert(0, self.sortable) 101 | sortable_added = True 102 | break 103 | 104 | return fieldsets 105 | 106 | def formfield_for_dbfield(self, db_field, **kwargs): 107 | if db_field.name == self.sortable: 108 | kwargs['widget'] = deepcopy(SortableListForm.Meta.widgets['order']) 109 | kwargs['widget'].attrs['class'] += ' suit-sortable-stacked' 110 | kwargs['widget'].attrs['rowclass'] = ' suit-sortable-stacked-row' 111 | return super(SortableStackedInlineBase, self).formfield_for_dbfield(db_field, **kwargs) 112 | 113 | 114 | class SortableStackedInline(SortableStackedInlineBase, admin.StackedInline): 115 | pass 116 | 117 | 118 | class SortableGenericStackedInline(SortableStackedInlineBase, 119 | GenericStackedInline): 120 | pass 121 | 122 | 123 | class SortableModelAdmin(SortableModelAdminBase, admin.ModelAdmin): 124 | """ 125 | Sortable change list 126 | """ 127 | 128 | def __init__(self, *args, **kwargs): 129 | super(SortableModelAdmin, self).__init__(*args, **kwargs) 130 | 131 | # Keep originals for restore 132 | self._original_ordering = copy(self.ordering) 133 | self._original_list_display = copy(self.list_display) 134 | self._original_list_editable = copy(self.list_editable) 135 | self._original_exclude = copy(self.exclude) 136 | self._original_list_per_page = self.list_per_page 137 | 138 | self.enable_sortable() 139 | 140 | def merge_form_meta(self, form): 141 | """ 142 | Prepare Meta class with order field widget 143 | """ 144 | if not getattr(form, 'Meta', None): 145 | form.Meta = SortableListForm.Meta 146 | if not getattr(form.Meta, 'widgets', None): 147 | form.Meta.widgets = {} 148 | form.Meta.widgets[self.sortable] = SortableListForm.Meta.widgets[ 149 | 'order'] 150 | 151 | def get_changelist_form(self, request, **kwargs): 152 | form = super(SortableModelAdmin, self).get_changelist_form(request, 153 | **kwargs) 154 | self.merge_form_meta(form) 155 | return form 156 | 157 | def get_changelist(self, request, **kwargs): 158 | return SortableChangeList 159 | 160 | def enable_sortable(self): 161 | self.list_per_page = 500 162 | self.ordering = (self.sortable,) 163 | if self.list_display and self.sortable not in self.list_display: 164 | self.list_display = list(self.list_display) + [self.sortable] 165 | 166 | self.list_editable = self.list_editable or [] 167 | if self.sortable not in self.list_editable: 168 | self.list_editable = list(self.list_editable) + [self.sortable] 169 | 170 | self.exclude = self.exclude or [] 171 | if self.sortable not in self.exclude: 172 | self.exclude = list(self.exclude) + [self.sortable] 173 | 174 | def disable_sortable(self): 175 | if not self.sortable_is_enabled(): 176 | return 177 | self.ordering = self._original_ordering 178 | self.list_display = self._original_list_display 179 | self.list_editable = self._original_list_editable 180 | self.exclude = self._original_exclude 181 | self.list_per_page = self._original_list_per_page 182 | 183 | def sortable_is_enabled(self): 184 | return self.list_display and self.sortable in self.list_display 185 | 186 | def save_model(self, request, obj, form, change): 187 | if not obj.pk: 188 | max_order = obj.__class__.objects.aggregate( 189 | models.Max(self.sortable)) 190 | try: 191 | next_order = max_order['%s__max' % self.sortable] + 1 192 | except TypeError: 193 | next_order = 1 194 | setattr(obj, self.sortable, next_order) 195 | super(SortableModelAdmin, self).save_model(request, obj, form, change) 196 | -------------------------------------------------------------------------------- /suit/static/admin/css/changelists.css: -------------------------------------------------------------------------------- 1 | /* Overridden by Django Suit empty stylesheet to reset original style. */ 2 | -------------------------------------------------------------------------------- /suit/static/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* Overridden by Django Suit empty stylesheet to reset original style. */ 2 | -------------------------------------------------------------------------------- /suit/static/admin/css/forms.css: -------------------------------------------------------------------------------- 1 | /* Overridden by Django Suit empty stylesheet to reset original style. */ 2 | -------------------------------------------------------------------------------- /suit/static/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* Overridden by Django Suit empty stylesheet to reset original style. */ 2 | -------------------------------------------------------------------------------- /suit/static/suit/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/static/suit/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /suit/static/suit/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/static/suit/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /suit/static/suit/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/static/suit/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /suit/static/suit/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/static/suit/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /suit/static/suit/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darklow/django-suit/87346f513199a89b24a156bd35a9a13e43a69668/suit/static/suit/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /suit/static/suit/js/autosize.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Autosize 3.0.15 3 | license: MIT 4 | http://www.jacklmoore.com/autosize 5 | */ 6 | !function(e,t){if("function"==typeof define&&define.amd)define(["exports","module"],t);else if("undefined"!=typeof exports&&"undefined"!=typeof module)t(exports,module);else{var n={exports:{}};t(n.exports,n),e.autosize=n.exports}}(this,function(e,t){"use strict";function n(e){function t(){var t=window.getComputedStyle(e,null);p=t.overflowY,"vertical"===t.resize?e.style.resize="none":"both"===t.resize&&(e.style.resize="horizontal"),c="content-box"===t.boxSizing?-(parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)):parseFloat(t.borderTopWidth)+parseFloat(t.borderBottomWidth),isNaN(c)&&(c=0),i()}function n(t){var n=e.style.width;e.style.width="0px",e.offsetWidth,e.style.width=n,p=t,f&&(e.style.overflowY=t),o()}function o(){var t=window.pageYOffset,n=document.body.scrollTop,o=e.style.height;e.style.height="auto";var i=e.scrollHeight+c;return 0===e.scrollHeight?void(e.style.height=o):(e.style.height=i+"px",v=e.clientWidth,document.documentElement.scrollTop=t,void(document.body.scrollTop=n))}function i(){var t=e.style.height;o();var i=window.getComputedStyle(e,null);if(i.height!==e.style.height?"visible"!==p&&n("visible"):"hidden"!==p&&n("hidden"),t!==e.style.height){var r=d("autosize:resized");e.dispatchEvent(r)}}var s=void 0===arguments[1]?{}:arguments[1],a=s.setOverflowX,l=void 0===a?!0:a,u=s.setOverflowY,f=void 0===u?!0:u;if(e&&e.nodeName&&"TEXTAREA"===e.nodeName&&!r.has(e)){var c=null,p=null,v=e.clientWidth,h=function(){e.clientWidth!==v&&i()},y=function(t){window.removeEventListener("resize",h,!1),e.removeEventListener("input",i,!1),e.removeEventListener("keyup",i,!1),e.removeEventListener("autosize:destroy",y,!1),e.removeEventListener("autosize:update",i,!1),r["delete"](e),Object.keys(t).forEach(function(n){e.style[n]=t[n]})}.bind(e,{height:e.style.height,resize:e.style.resize,overflowY:e.style.overflowY,overflowX:e.style.overflowX,wordWrap:e.style.wordWrap});e.addEventListener("autosize:destroy",y,!1),"onpropertychange"in e&&"oninput"in e&&e.addEventListener("keyup",i,!1),window.addEventListener("resize",h,!1),e.addEventListener("input",i,!1),e.addEventListener("autosize:update",i,!1),r.add(e),l&&(e.style.overflowX="hidden",e.style.wordWrap="break-word"),t()}}function o(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var t=d("autosize:destroy");e.dispatchEvent(t)}}function i(e){if(e&&e.nodeName&&"TEXTAREA"===e.nodeName){var t=d("autosize:update");e.dispatchEvent(t)}}var r="function"==typeof Set?new Set:function(){var e=[];return{has:function(t){return Boolean(e.indexOf(t)>-1)},add:function(t){e.push(t)},"delete":function(t){e.splice(e.indexOf(t),1)}}}(),d=function(e){return new Event(e)};try{new Event("test")}catch(s){d=function(e){var t=document.createEvent("Event");return t.initEvent(e,!0,!1),t}}var a=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?(a=function(e){return e},a.destroy=function(e){return e},a.update=function(e){return e}):(a=function(e,t){return e&&Array.prototype.forEach.call(e.length?e:[e],function(e){return n(e,t)}),e},a.destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e},a.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],i),e}),t.exports=a}); 7 | -------------------------------------------------------------------------------- /suit/static/suit/js/suit.js: -------------------------------------------------------------------------------- 1 | Suit = {}; 2 | window.Suit = Suit; 3 | 4 | (function ($) { 5 | if (!$) 6 | return; 7 | 8 | Suit.$ = $; 9 | 10 | // Register callbacks to perform after inline has been added 11 | Suit.after_inline = function () { 12 | var functions = {}; 13 | var register = function (fn_name, fn_callback) { 14 | functions[fn_name] = fn_callback; 15 | }; 16 | 17 | var run = function (inline_prefix, row) { 18 | for (var fn_name in functions) { 19 | functions[fn_name](inline_prefix, row); 20 | } 21 | }; 22 | 23 | return { 24 | register: register, 25 | run: run 26 | }; 27 | }(); 28 | 29 | Suit.ListActionsToggle = function () { 30 | var $topActions; 31 | 32 | var init = function () { 33 | $(document).ready(function () { 34 | $topActions = $('.results').parent().find('.actions').eq(0); 35 | if (!$topActions.length) 36 | return; 37 | 38 | $('tr input.action-select, #action-toggle').on('click', function() { 39 | window.setTimeout(checkIfSelected, 5) 40 | }) 41 | }); 42 | }; 43 | 44 | var checkIfSelected = function () { 45 | if ($('tr.selected').length) { 46 | $topActions.slideDown('fast'); 47 | } else { 48 | $topActions.slideUp('fast'); 49 | } 50 | }; 51 | 52 | return { 53 | init: init 54 | } 55 | 56 | }(); 57 | 58 | 59 | Suit.FixedBar = function () { 60 | var didScroll = false, $fixedItem, $fixedItemParent, $win, $body, 61 | itemOffset, 62 | extraOffset = 0, 63 | fixed = false; 64 | 65 | function init(selector) { 66 | $fixedItem = $(selector || '.submit-row'); 67 | if (!$fixedItem.length) 68 | return; 69 | 70 | $fixedItemParent = $fixedItem.parents('form'); 71 | itemOffset = $fixedItem.offset(); 72 | $win = $(window); 73 | window.onscroll = onScroll; 74 | window.onresize = onScroll; 75 | onScroll(); 76 | 77 | setInterval(function () { 78 | if (didScroll) { 79 | didScroll = false; 80 | } 81 | }, 200); 82 | } 83 | 84 | function onScroll() { 85 | didScroll = true; 86 | 87 | var itemHeight = $fixedItem.height(), 88 | scrollTop = $win.scrollTop(); 89 | 90 | if (scrollTop + $win.height() - itemHeight - extraOffset < itemOffset.top) { 91 | if (!fixed) { 92 | $fixedItem.addClass('fixed'); 93 | $fixedItemParent.addClass('fixed').css('padding-bottom', itemHeight + 'px'); 94 | fixed = true; 95 | } 96 | } else { 97 | if (fixed) { 98 | $fixedItem.removeClass('fixed'); 99 | $fixedItemParent.removeClass('fixed').css('padding-bottom', ''); 100 | fixed = false; 101 | } 102 | } 103 | } 104 | 105 | return { 106 | init: init 107 | }; 108 | }(); 109 | 110 | /** 111 | * Avoids double-submit issues in the change_form. 112 | */ 113 | $.fn.suitFormDebounce = function () { 114 | var $form = $(this), 115 | $saveButtons = $form.find('.submit-row button, .submit-row input[type=button], .submit-row input[type=submit]'), 116 | submitting = false; 117 | 118 | $form.submit(function () { 119 | if (submitting) { 120 | return false; 121 | } 122 | 123 | submitting = true; 124 | $saveButtons.addClass('disabled'); 125 | 126 | setTimeout(function () { 127 | $saveButtons.removeClass('disabled'); 128 | submitting = false; 129 | }, 5000); 130 | }); 131 | }; 132 | 133 | /** 134 | * Content tabs 135 | */ 136 | $.fn.suitFormTabs = function () { 137 | 138 | var $tabs = $(this); 139 | var tabPrefix = $tabs.data('tab-prefix'); 140 | if (!tabPrefix) 141 | return; 142 | 143 | var $tabLinks = $tabs.find('a'); 144 | 145 | function tabContents($link) { 146 | return $('.' + tabPrefix + '-' + $link.attr('href').replace('#', '')); 147 | } 148 | 149 | function activateTabs() { 150 | // Init tab by error, by url hash or init first tab 151 | if (window.location.hash) { 152 | var foundError; 153 | $tabLinks.each(function () { 154 | var $link = $(this); 155 | if (tabContents($link).find('.error, .errorlist').length != 0) { 156 | $link.addClass('has-error'); 157 | $link.trigger('click'); 158 | foundError = true; 159 | } 160 | }); 161 | !foundError && $($tabs).find('a[href=\\' + window.location.hash + ']').click(); 162 | } else { 163 | $tabLinks.first().trigger('click'); 164 | } 165 | } 166 | 167 | $tabLinks.click(function () { 168 | var $link = $(this), 169 | showEvent = $.Event('shown.suit.tab', { 170 | relatedTarget: $link, 171 | tab: $link.attr('href').replace('#', '') 172 | }); 173 | $link.parent().parent().find('.active').removeClass('active'); 174 | $link.addClass('active'); 175 | $('.' + tabPrefix).removeClass('show').addClass('hidden-xs-up'); 176 | tabContents($link).removeClass('hidden-xs-up').addClass('show'); 177 | $link.trigger(showEvent); 178 | }); 179 | 180 | activateTabs(); 181 | }; 182 | 183 | /* Characters count for CharacterCountTextarea */ 184 | $.fn.suitCharactersCount = function () { 185 | var $elements = $(this); 186 | 187 | if (!$elements.length) 188 | return; 189 | 190 | $elements.each(function () { 191 | var $el = $(this), 192 | $countEl = $('
    '); 193 | $el.after($countEl); 194 | $el.on('keyup', function (e) { 195 | updateCount($(e.currentTarget)); 196 | }); 197 | updateCount($el); 198 | }); 199 | 200 | function updateCount($el) { 201 | var maxCount = $el.data('suit-maxcount'), 202 | twitterCount = $el.data('suit-twitter-count'), 203 | value = $el.val(), 204 | len = twitterCount ? getTweetLength(value) : value.length, 205 | count = maxCount ? maxCount - len : len; 206 | if (count < 0) 207 | count = '' + count + ''; 208 | 209 | $el.next().first().html(count); 210 | } 211 | 212 | function getTweetLength(input) { 213 | var tmp = ""; 214 | for (var i = 0; i < 23; i++) { 215 | tmp += "o" 216 | } 217 | return input.replace(/(http:\/\/[\S]*)/g, tmp).length; 218 | } 219 | }; 220 | 221 | /** 222 | * Search filters - submit only changed fields 223 | */ 224 | $.fn.suitSearchFilters = function () { 225 | $(this).change(function () { 226 | var $field = $(this); 227 | var $option = $field.find('option:selected'); 228 | var select_name = $option.data('name'); 229 | if (select_name) { 230 | $field.attr('name', select_name); 231 | } else { 232 | $field.removeAttr('name'); 233 | } 234 | // Handle additional values for date filters 235 | var additional = $option.data('additional'); 236 | console.log($field, additional) 237 | if (additional) { 238 | var hiddenId = $field.data('name') + '_add'; 239 | var $hidden = $('#' + hiddenId); 240 | if (!$hidden.length) { 241 | $hidden = $('').attr('type', 'hidden').attr('id', hiddenId); 242 | $field.after($hidden); 243 | } 244 | additional = additional.split('='); 245 | $hidden.attr('name', additional[0]).val(additional[1]) 246 | } 247 | }); 248 | $(this).trigger('change'); 249 | }; 250 | 251 | 252 | })(typeof django !== 'undefined' ? django.jQuery : undefined); 253 | -------------------------------------------------------------------------------- /suit/static/suit/js/suit.sortables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List sortables 3 | */ 4 | (function ($) { 5 | $.fn.suit_list_sortable = function () { 6 | var $inputs = $(this); 7 | if (!$inputs.length) 8 | return; 9 | 10 | // Detect if this is normal or mptt table 11 | var mptt_table = $inputs.first().closest('table').hasClass('table-mptt'); 12 | 13 | function performMove($arrow, $row) { 14 | var $next, $prev; 15 | 16 | $row.closest('table').find('tr.selected').removeClass('selected'); 17 | if (mptt_table) { 18 | function getPadding($tr) { 19 | return parseInt($tr.find('th:first').css('padding-left')); 20 | } 21 | 22 | function findWithChildren($tr) { 23 | var padding = getPadding($tr); 24 | return $tr.nextUntil(function () { 25 | return getPadding($(this)) <= padding 26 | }).andSelf(); 27 | } 28 | 29 | var padding = getPadding($row); 30 | var $rows_to_move = findWithChildren($row); 31 | if ($arrow.data('dir') === 'down') { 32 | $next = $rows_to_move.last().next(); 33 | if ($next.length && getPadding($next) === padding) { 34 | var $after = findWithChildren($next).last(); 35 | if ($after.length) { 36 | $rows_to_move.insertAfter($after).addClass('selected'); 37 | } 38 | } 39 | } else { 40 | $prev = $row.prevUntil(function () { 41 | return getPadding($(this)) <= padding 42 | }).andSelf().first().prev(); 43 | if ($prev.length && getPadding($prev) === padding) { 44 | $rows_to_move.insertBefore($prev).addClass('selected') 45 | } 46 | } 47 | } else { 48 | if ($arrow.data('dir') === 'down') { 49 | $next = $row.next(); 50 | if ($next.is(':visible') && $next.length) { 51 | $row.insertAfter($next).addClass('selected') 52 | } 53 | } else { 54 | $prev = $row.prev(); 55 | if ($prev.is(':visible') && $prev.length) { 56 | $row.insertBefore($prev).addClass('selected') 57 | } 58 | } 59 | } 60 | markLastInline($row.parent()); 61 | } 62 | 63 | function onArrowClick(e) { 64 | var $sortable = $(this); 65 | var $row = $sortable.closest( 66 | $sortable.hasClass('sortable-stacked') ? 'div.inline-related' : 'tr' 67 | ); 68 | performMove($sortable, $row); 69 | e.preventDefault(); 70 | } 71 | 72 | function createLink(text, direction, on_click_func, is_stacked) { 73 | return $('').attr('href', '#') 74 | .addClass('sortable sortable-' + direction + 75 | (is_stacked ? ' sortable-stacked' : '')) 76 | .attr('data-dir', direction).html(text) 77 | .on('click', on_click_func); 78 | } 79 | 80 | function markLastInline($rowParent) { 81 | $rowParent.find(' > .last-sortable').removeClass('last-sortable'); 82 | $rowParent.find('tr.form-row:visible:last').addClass('last-sortable'); 83 | } 84 | 85 | var $lastSortable; 86 | $inputs.each(function () { 87 | var $inline_sortable = $('
    '), 88 | icon = '', 89 | $sortable = $(this), 90 | is_stacked = $sortable.hasClass('suit-sortable-stacked'); 91 | 92 | var $up_link = createLink(icon, 'up', onArrowClick, is_stacked), 93 | $down_link = createLink(icon.replace('-up', '-down'), 'down', onArrowClick, is_stacked); 94 | 95 | if (is_stacked) { 96 | var $sortable_row = $sortable.closest('div.form-group'), 97 | $stacked_block = $sortable.closest('div.inline-related'), 98 | $links_span = $('').attr('class', 'stacked-inline-sortable'); 99 | 100 | // Add arrows to header h3, move order input and remove order field row 101 | $links_span.append($up_link).append($down_link); 102 | $links_span.insertAfter($stacked_block.find('.inline_label')); 103 | $stacked_block.append($sortable); 104 | $sortable_row.remove(); 105 | } else { 106 | $sortable.parent().append($inline_sortable); 107 | $inline_sortable.append($up_link); 108 | $inline_sortable.append($down_link); 109 | $lastSortable = $sortable; 110 | } 111 | }); 112 | 113 | $lastSortable && markLastInline($lastSortable.closest('.form-row').parent()); 114 | 115 | // Filters out unchanged checkboxes, selects and sortable field itself 116 | function filter_unchanged(i, input) { 117 | if (input.type == 'checkbox') { 118 | if (input.defaultChecked == input.checked) { 119 | return false; 120 | } 121 | } else if (input.type == 'select-one' || input.type == 'select-multiple') { 122 | var options = input.options, option; 123 | for (var j = 0; j < options.length; j++) { 124 | option = options[j]; 125 | if (option.selected && option.selected == option.defaultSelected) { 126 | return false; 127 | } 128 | } 129 | } else if ($(input).hasClass('suit-sortable')) { 130 | if (input.defaultValue == input.value && input.value == 0) { 131 | return false; 132 | } 133 | } 134 | return true; 135 | } 136 | 137 | // Update input count right before submit 138 | if ($inputs && $inputs.length) { 139 | var $last_input = $inputs.last(); 140 | var selector = $(this).selector; 141 | $($last_input[0].form).submit(function (e) { 142 | var i = 0, value; 143 | // e.preventDefault(); 144 | $(selector).each(function () { 145 | var $input = $(this); 146 | var fieldset_id = $input.attr('name').split(/-\d+-/)[0]; 147 | // Check if any of new dynamic block values has been added 148 | var $set_block = $input.closest('.dynamic-' + fieldset_id); 149 | var $changed_fields = $set_block.find(":input[type!='hidden']:not(.suit-sortable)").filter( 150 | function () { 151 | return $(this).val() != ""; 152 | }).filter(filter_unchanged); 153 | // console.log($changed_fields.length, $changed_fields); 154 | var is_changelist = !$set_block.length; 155 | if (is_changelist 156 | || $set_block.hasClass('has_original') 157 | || $changed_fields.serializeArray().length 158 | // Since jQuery serialize() doesn't include type=file do additional check 159 | || $changed_fields.find(":input[type='file']").addBack().length) { 160 | value = i++; 161 | $input.val(value); 162 | } 163 | }); 164 | }); 165 | } 166 | 167 | Suit.after_inline.register('bind_sortable_arrows', function (prefix, row) { 168 | $(row).find('.suit-sortable').on('click', onArrowClick); 169 | markLastInline($(row).parent()); 170 | }); 171 | }; 172 | 173 | 174 | $(function () { 175 | $('.suit-sortable').suit_list_sortable(); 176 | }); 177 | 178 | }(django.jQuery)); 179 | 180 | // Call Suit.after_inline 181 | django.jQuery(document).on('formset:added', function (e, row, prefix) { 182 | Suit.after_inline.run(prefix, row); 183 | }); 184 | -------------------------------------------------------------------------------- /suit/template.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, join, abspath 2 | from django.template.loaders.filesystem import Loader as FilesystemLoader 3 | 4 | _cache = {} 5 | 6 | 7 | class Loader(FilesystemLoader): 8 | is_usable = True 9 | 10 | def get_template_sources(self, template_name, template_dirs=None): 11 | """ 12 | Returns the absolute paths to "template_name" in the specified app. 13 | If the name does not contain an app name (no colon), an empty list 14 | is returned. 15 | The parent FilesystemLoader.load_template_source() will take care 16 | of the actual loading for us. 17 | """ 18 | if not ':' in template_name: 19 | return [] 20 | app_name, template_name = template_name.split(":", 1) 21 | template_dir = get_app_template_dir(app_name) 22 | if template_dir: 23 | return [join(template_dir, template_name)] 24 | else: 25 | return [] 26 | 27 | 28 | def get_app_template_dir(app_name): 29 | """Get the template directory for an application 30 | 31 | We do not use django.db.models.get_app, because this will fail if an 32 | app does not have any models. 33 | 34 | Returns a full path, or None if the app was not found. 35 | """ 36 | from django.conf import settings 37 | from importlib import import_module 38 | if app_name in _cache: 39 | return _cache[app_name] 40 | template_dir = None 41 | for app in settings.INSTALLED_APPS: 42 | if app.split('.')[-1] == app_name: 43 | # Do not hide import errors; these should never happen at this point 44 | # anyway 45 | mod = import_module(app) 46 | template_dir = join(abspath(dirname(mod.__file__)), 'templates') 47 | break 48 | _cache[app_name] = template_dir 49 | return template_dir 50 | -------------------------------------------------------------------------------- /suit/templates/admin/auth/user/add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | {% load i18n %} 3 | 4 | {% block form_top %} 5 | {{ block.super }} 6 | 7 | {% if not is_popup %} 8 |

    {% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}

    9 | {% else %} 10 |

    {% trans "Enter a username and password." %}

    11 | {% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /suit/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base.html' %} 2 | {% load i18n static suit_tags %} 3 | 4 | {% block stylesheet %}{% static "suit/css/suit.css" %}{% endblock %} 5 | 6 | {% block extrastyle %} 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block bodyclass %}{{ block.super|suit_body_class:request }}{% endblock %} 12 | 13 | {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 14 | 15 | {% block branding %} 16 |

    17 | 18 | {{ site_header|default:_('Django administration') }} 19 | {% trans 'Admin' %} 20 |

    21 | {% endblock %} 22 | 23 | {% block pretitle %} 24 |
    25 | {% block messages %} 26 | {% if messages %} 27 |
      {% for message in messages %} 28 | {{ message|capfirst }} 29 | {% endfor %}
    30 | {% endif %} 31 | {% endblock messages %} 32 | {% endblock %} 33 | 34 | {% block sidebar %} 35 |
    36 | {% endblock %} 37 | 38 | {% block usertools %} 39 | {% if has_permission %} 40 |
    41 | {% block welcome-msg %} 42 | 43 | Welcome, 44 | {% firstof user.get_short_name user.get_username %}. 45 | 46 | {% endblock %} 47 | 68 |
    69 | {% endif %} 70 | {% endblock %} 71 | 72 | {% block nav-global %} 73 | {% include 'suit/menu.html' %} 74 | {% endblock %} 75 | 76 | {% block footer %} 77 | {#
    #} 78 | {% if not is_popup %} 79 |