diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml new file mode 100644 index 00000000..b0571086 --- /dev/null +++ b/.github/workflows/pythonapp.yml @@ -0,0 +1,33 @@ +name: django-suit + +on: [push] + +jobs: + test: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: [ '3.9', '3.10.13' ] + django-version: [ '3.2.10', '4.0.10', '4.1.13', '4.2.9' ] + exclude: + - os: macos-latest + python-version: '3.8' + - os: windows-latest + python-version: '3.6' + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install Django==${{ matrix.django-version }} + - name: Run Tests + run: | + python manage.py test suit --settings=suit.tests.settings diff --git a/.gitignore b/.gitignore index a8a7e5b5..294980e9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ docs/_build/ /bower_components/ /node_modules/ /env/ +/package-lock.json +/.pypirc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9efaed5f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: python -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - pypy -env: - - DJANGO=1.11 - - DJANGO=2.0 -install: - - pip install -e . - - pip install -q Django==$DJANGO -script: - - DJANGO_SETTINGS_MODULE=suit.tests.settings django-admin test suit -matrix: - exclude: - - python: 2.7 - env: DJANGO=2.0 - - python: pypy - env: DJANGO=2.0 diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..72f1a111 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: mkdir -p demo/demo/static && python demo/manage.py collectstatic && python demo/manage.py runserver 0.0.0.0:$PORT \ No newline at end of file diff --git a/README.rst b/README.rst index 85cc7b6d..9d0ce386 100644 --- a/README.rst +++ b/README.rst @@ -6,14 +6,14 @@ Django Suit Django Suit is alternative theme/skin/extension for `Django `_ administration interface. -* Project home: http://djangosuit.com/ -* Live demo v1: http://djangosuit.com/admin/ -* Live demo v2.0 alpha 1: http://v2.djangosuit.com/admin/ +v2 version is working with Django 3.2 and Django 4.0, it is using Bootstrap 5.1 + +* Project home: http://djangosuit.com/ (not maintained) License ======= - + (not maintained) * Django Suit is licensed under `Creative Commons Attribution-NonCommercial 3.0 `_ license. * Licence and pricing: http://djangosuit.com/pricing/ @@ -21,18 +21,21 @@ License Docs & Support ============== -* Documentation v2: http://django-suit.readthedocs.org/en/v2/ -* Documentation v1: http://django-suit.readthedocs.org/en/latest/ -* Support: http://djangosuit.com/support/ -* Follow `on Twitter `_ to get latest news - +* Documentation v2: http://django-suit.readthedocs.org/en/v2/ (not maintained) +* Support: http://djangosuit.com/support/ (not maintained) Changelog ========= **Note:** Django Suit v2.0 is in active development and not yet ready for production use. -Read more here: Todo: Add issue refernce +Read more here: Todo: Add issue reference + +Install +========= +Install Django Suit v2 using ``pip`` or ``easy_install``:: + + pip install django-suit-v2-pm Contributing @@ -44,25 +47,4 @@ See `Contributing documentation " - ], - "description": "Django Suit development dependendencies", - "private": true, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "devDependencies": { - "bootstrap": "v4.0.0-alpha.5" - } -} diff --git a/demo/db.sqlite3 b/demo/db.sqlite3 index 2052bb12..76b8a265 100644 Binary files a/demo/db.sqlite3 and b/demo/db.sqlite3 differ diff --git a/demo/demo/admin.py b/demo/demo/admin.py index 388b8f68..34ad7958 100644 --- a/demo/demo/admin.py +++ b/demo/demo/admin.py @@ -1,10 +1,7 @@ -from django.conf import settings -from django.conf.urls import url -from django.contrib import admin from django.forms import ModelForm, Select, TextInput, NumberInput -from django.contrib.admin.views.decorators import staff_member_required from django.contrib import messages from django.shortcuts import redirect +from django.urls import re_path from django_select2.forms import ModelSelect2Widget from suit import apps @@ -46,9 +43,9 @@ class Meta: attrs={'placeholder': 'Country area'}), 'population': EnclosedInput( prepend='fa-users', - append='', - append_class='btn', attrs={'placeholder': 'Human population'}), + append='Search', + onclick_append="window.open(\'https://www.google.com/\')", + append_class='addon', attrs={'placeholder': 'Human population' }), 'description': AutosizedTextarea, 'architecture': AutosizedTextarea, } @@ -66,7 +63,7 @@ class PopulationFilter(IsNullFieldListFilter): class CountryAdmin(RelatedFieldAdmin): form = CountryForm search_fields = ('name', 'code') - list_display = ('name', 'code', 'link_to_continent', 'independence_day') + list_display = ('name', 'code', 'independence_day') list_filter = ('continent', 'independence_day', 'code', ('population', PopulationFilter)) suit_list_filter_horizontal = ('code', 'population') list_select_related = True @@ -314,7 +311,7 @@ def get_urls(self): """ urls = super(ShowcaseAdmin, self).get_urls() my_urls = [ - url(r'^(\d+)/clickme/$', showcase_custom_view_example, name='demo_showcase_clickme') + re_path(r'^(\d+)/clickme/$', showcase_custom_view_example, name='demo_showcase_clickme') ] return my_urls + urls @@ -327,3 +324,23 @@ def showcase_custom_view_example(request, pk): messages.success(request, 'Something legendary was done to "%s"' % instance) return redirect('admin:demo_showcase_change', pk) + +# +# class LargeFilterHorizontalForm(ModelForm): +# class Meta: +# pass +# +# +# @admin.register(LargeFilterHorizontal) +# class LargeFilterHorizontalAdmin(RelatedFieldAdmin): +# form = LargeFilterHorizontalForm +# search_fields = ('title',) +# list_display = ('horizontal_choices1', 'horizontal_choices2', 'horizontal_choices3', 'horizontal_choices4',) +# list_filter = ('horizontal_choices1', 'horizontal_choices2', 'horizontal_choices3', 'horizontal_choices4', +# 'horizontal_choices5', 'horizontal_choices6', 'horizontal_choices7', 'horizontal_choices8') +# suit_list_filter_horizontal = list_filter +# +# fieldsets = [ +# ('Main', {'fields': ['horizontal_choices1', 'horizontal_choices2', 'horizontal_choices3', 'horizontal_choices4', +# 'horizontal_choices5', 'horizontal_choices6', 'horizontal_choices7', 'horizontal_choices8']}), +# ] \ No newline at end of file diff --git a/demo/demo/apps.py b/demo/demo/apps.py index 6148a3ef..1678168a 100644 --- a/demo/demo/apps.py +++ b/demo/demo/apps.py @@ -5,7 +5,7 @@ class SuitConfig(DjangoSuitConfig): menu = ( ParentItem('Content', children=[ - ChildItem(model='demo.country'), + ChildItem(model='demo.country', params={'o': '1.2'}), ChildItem(model='demo.continent'), ChildItem(model='demo.showcase'), ChildItem('Custom view', url='/admin/custom/'), diff --git a/demo/demo/migrations/0013_auto_20201112_1223.py b/demo/demo/migrations/0013_auto_20201112_1223.py new file mode 100644 index 00000000..b2876264 --- /dev/null +++ b/demo/demo/migrations/0013_auto_20201112_1223.py @@ -0,0 +1,94 @@ +# Generated by Django 2.1.15 on 2020-11-12 11:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('demo', '0012_auto_20170407_1131'), + ] + + operations = [ + migrations.AlterField( + model_name='book', + name='rating', + field=models.SmallIntegerField(choices=[(1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')], help_text='Choose wisely'), + ), + migrations.AlterField( + model_name='country', + name='code', + field=models.CharField(help_text='ISO 3166-1 alpha-2 - two character country code', max_length=2), + ), + migrations.AlterField( + model_name='country', + name='description', + field=models.TextField(blank=True, help_text='Try and enter few some more lines'), + ), + migrations.AlterField( + model_name='movie', + name='rating', + field=models.SmallIntegerField(choices=[(1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')], default=2), + ), + migrations.AlterField( + model_name='showcase', + name='boolean_with_help', + field=models.BooleanField(default=False, help_text='Boolean field with help text'), + ), + migrations.AlterField( + model_name='showcase', + name='choices', + field=models.SmallIntegerField(choices=[(1, 'Tall'), (2, 'Normal'), (3, 'Short')], default=3, help_text='Help text'), + ), + migrations.AlterField( + model_name='showcase', + name='country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='demo.Country'), + ), + migrations.AlterField( + model_name='showcase', + name='country2', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='showcase_country2_set', to='demo.Country', verbose_name='Django Select 2'), + ), + migrations.AlterField( + model_name='showcase', + name='help_text', + field=models.CharField(help_text='Enter fully qualified name', max_length=64), + ), + migrations.AlterField( + model_name='showcase', + name='horizontal_choices', + field=models.SmallIntegerField(choices=[(1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')], default=1, help_text='Horizontal choices look like this'), + ), + migrations.AlterField( + model_name='showcase', + name='multiple_in_row', + field=models.CharField(help_text='Help text for multiple', max_length=64), + ), + migrations.AlterField( + model_name='showcase', + name='raw_id_field', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='showcase_raw_set', to='demo.Country'), + ), + migrations.AlterField( + model_name='showcase', + name='readonly_field', + field=models.CharField(default='Some value here', max_length=127), + ), + migrations.AlterField( + model_name='showcase', + name='textfield', + field=models.TextField(blank=True, help_text='Try and enter few some more lines', verbose_name='Autosized textfield'), + ), + migrations.AlterField( + model_name='showcase', + name='time_only', + field=models.TimeField(blank=True, null=True, verbose_name='Time'), + ), + migrations.AlterField( + model_name='showcase', + name='vertical_choices', + field=models.SmallIntegerField(choices=[(1, 'Hot'), (2, 'Normal'), (3, 'Cold')], default=2, help_text='Some help on vertical choices'), + ), + ] diff --git a/demo/demo/models.py b/demo/demo/models.py index 45a719ca..7a1908c7 100644 --- a/demo/demo/models.py +++ b/demo/demo/models.py @@ -17,7 +17,7 @@ class Meta: class Country(models.Model): - continent = models.ForeignKey(Continent, null=True) + continent = models.ForeignKey(Continent, null=True, on_delete=models.CASCADE) name = models.CharField(max_length=256) code = models.CharField(max_length=2, help_text='ISO 3166-1 alpha-2 - two character country code') @@ -38,7 +38,7 @@ class Meta: class City(models.Model): name = models.CharField(max_length=64) - country = models.ForeignKey(Country) + country = models.ForeignKey(Country, on_delete=models.CASCADE) is_capital = models.BooleanField() area = models.BigIntegerField(blank=True, null=True) population = models.BigIntegerField(blank=True, null=True) @@ -82,9 +82,9 @@ class Showcase(models.Model): choices = models.SmallIntegerField( choices=TYPE_CHOICES3, default=3, help_text="Help text") - country = models.ForeignKey(Country, null=True, blank=True) - country2 = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_country2_set', verbose_name='Django Select 2') - raw_id_field = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_raw_set') + country = models.ForeignKey(Country, null=True, blank=True, on_delete=models.SET_NULL) + country2 = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_country2_set', verbose_name='Django Select 2', on_delete=models.SET_NULL) + raw_id_field = models.ForeignKey(Country, null=True, blank=True, related_name='showcase_raw_set', on_delete=models.SET_NULL) # linked_foreign_key = models.ForeignKey(Country, limit_choices_to={ # 'continent__name': 'Europe'}, related_name='foreign_key_linked') html5_color = models.CharField(null=True, blank=True, max_length=7) @@ -97,7 +97,7 @@ class Meta: # Tabular inline model for Showcase class Movie(models.Model): - showcase = models.ForeignKey(Showcase) + showcase = models.ForeignKey(Showcase, on_delete=models.CASCADE) title = models.CharField(max_length=64) rating = models.SmallIntegerField(choices=TYPE_CHOICES, default=2) description = models.TextField(blank=True) @@ -113,7 +113,7 @@ def __unicode__(self): # Stacked inline model for Showcase class Book(models.Model): - showcase = models.ForeignKey(Showcase) + showcase = models.ForeignKey(Showcase, on_delete=models.CASCADE) title = models.CharField(max_length=64) rating = models.SmallIntegerField(choices=TYPE_CHOICES, help_text='Choose wisely') is_released = models.BooleanField(default=False) @@ -124,3 +124,39 @@ class Meta: def __unicode__(self): return self.title + + +# class LargeFilterHorizontal(models.Model): +# title = models.CharField(max_length=64) +# +# TYPE_CHOICES= ((1, 'Awesome'), (2, 'Good'), (3, 'Normal'), (4, 'Bad')) +# TYPE_CHOICES2 = ((1, 'Hot'), (2, 'Normal'), (3, 'Cold')) +# TYPE_CHOICES3 = ((1, 'Tall'), (2, 'Normal'), (3, 'Short')) +# TYPE_CHOICES4 = ((1, 'Black'), (2, 'Purple'), (3, 'Pink')) +# TYPE_CHOICES5 = ((1, 'Image'), (2, 'Video'), (3, 'Sound')) +# TYPE_CHOICES6 = ((1, 'Square'), (2, 'Circle'), (3, 'Triangle')) +# TYPE_CHOICES7 = ((1, 'GIF'), (2, 'JPG'), (3, 'PNG')) +# TYPE_CHOICES8 = ((1, 'Color'), (2, 'B&W'), (3, 'Others')) +# horizontal_choices1 = models.SmallIntegerField( +# choices=TYPE_CHOICES, default=1, help_text='Horizontal1 choices look like this') +# horizontal_choices2 = models.SmallIntegerField( +# choices=TYPE_CHOICES2, default=2, help_text="Horizontal2 choices look like this") +# horizontal_choices3 = models.SmallIntegerField( +# choices=TYPE_CHOICES3, default=3, help_text="Horizontal3 choices look like this") +# horizontal_choices4 = models.SmallIntegerField( +# choices=TYPE_CHOICES4, default=1, help_text='Horizontal4 choices look like this') +# horizontal_choices5 = models.SmallIntegerField( +# choices=TYPE_CHOICES5, default=2, help_text="Horizontal5 choices look like this") +# horizontal_choices6 = models.SmallIntegerField( +# choices=TYPE_CHOICES6, default=3, help_text="Horizontal6 choices look like this") +# horizontal_choices7 = models.SmallIntegerField( +# choices=TYPE_CHOICES7, default=2, help_text="Horizontal7 choices look like this") +# horizontal_choices8 = models.SmallIntegerField( +# choices=TYPE_CHOICES8, default=3, help_text="Horizontal8 choices look like this") +# +# class Meta: +# verbose_name = 'Large Filter Horizontal choice' +# verbose_name_plural = 'Large Filter Horizontal choices' +# +# def __unicode__(self): +# return self.title diff --git a/demo/demo/settings-heroku.py b/demo/demo/settings-heroku.py new file mode 100644 index 00000000..bdd2e861 --- /dev/null +++ b/demo/demo/settings-heroku.py @@ -0,0 +1,153 @@ +""" +Django settings for demo project. + +Generated by 'django-admin startproject' using Django 1.9.1. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv('SECRET_KEY', 'Optional default value') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False +INTERNAL_IPS = ("127.0.0.1", ) + +ALLOWED_HOSTS = ['django-suit-v2.herokuapp.com'] + + +# Application definition + +INSTALLED_APPS = [ + + # Demo app + 'demo', + + # Django Suit + 'demo.apps.SuitConfig', + + # 3rd party apps + 'django_select2', + + # Django + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + #'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'demo.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'demo.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Europe/Riga' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ +PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) +STATIC_ROOT = os.path.join(PROJECT_ROOT , 'staticfiles') +STATIC_URL = '/static/' +# Extra lookup directories for collectstatic to find static files +STATICFILES_DIRS = ( + os.path.join(PROJECT_ROOT , 'static'), +) + +# Add configuration for static files storage using whitenoise +# STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' +# Default STORAGES from Django documentation +# See: https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-STORAGES +STORAGES = { + "default": { "BACKEND": "django.core.files.storage.FileSystemStorage" }, + "staticfiles": { "BACKEND": 'whitenoise.storage.CompressedStaticFilesStorage' }, #'django.contrib.staticfiles.storage.StaticFilesStorage'} +} + +# For demo app specific only: +# Use file backend for sessions, to not mess DB +SESSION_ENGINE = 'django.contrib.sessions.backends.file' + +DEFAULT_AUTO_FIELD='django.db.models.AutoField' diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 8d19182a..81eca3db 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -24,8 +24,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +INTERNAL_IPS = ("127.0.0.1", ) -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] # Application definition @@ -50,13 +51,13 @@ 'django.contrib.staticfiles', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + #'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -134,3 +135,5 @@ # For demo app specific only: # Use file backend for sessions, to not mess DB SESSION_ENGINE = 'django.contrib.sessions.backends.file' + +DEFAULT_AUTO_FIELD='django.db.models.AutoField' diff --git a/demo/demo/templates/admin/custom_view.html b/demo/demo/templates/admin/custom_view.html index 8491b1c1..aa2d61c2 100644 --- a/demo/demo/templates/admin/custom_view.html +++ b/demo/demo/templates/admin/custom_view.html @@ -7,13 +7,65 @@
Isn't this neat?
-
+

Custom view

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

+

Table example

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
Go home View on github + + +
+ +
diff --git a/demo/demo/templates/admin/demo/country/tab_charts.html b/demo/demo/templates/admin/demo/country/tab_charts.html index efc67ad4..e950f795 100644 --- a/demo/demo/templates/admin/demo/country/tab_charts.html +++ b/demo/demo/templates/admin/demo/country/tab_charts.html @@ -7,7 +7,7 @@

Statistics

Activities
-
+
diff --git a/demo/demo/templates/admin/demo/country/tab_docs.html b/demo/demo/templates/admin/demo/country/tab_docs.html index 0fa1e13b..d1b76deb 100644 --- a/demo/demo/templates/admin/demo/country/tab_docs.html +++ b/demo/demo/templates/admin/demo/country/tab_docs.html @@ -6,7 +6,7 @@

Documentation

Tabs
-
+
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.

Tabs can contain fieldsets, inlines and custom/included templates @@ -20,7 +20,7 @@

Documentation

Includes
-
+
Django Suit provides handy shortcut to include templates into forms, into several positions: