diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..c5953c574 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Clean release +run-name: ${{ github.actor }} release +on: + push: + tags: + - '*' +jobs: + pypi-release: + runs-on: ubuntu-latest + container: + image: yourlabs/python + steps: + - name: Check out repository code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Fix permissions + run: chown -R app:app . + - name: Update npm + run: npm update -g npm + - name: Update version in setup.py and docs/conf.py + run: | + short=$(echo ${GITHUB_REF##*/} | grep -Eo '[^.]+\.[^.]+') + sed -i "s/version\": \"[^\"]*\"/version\": \"${GITHUB_REF##*/}\"/" package.json + sed -i "s/version=[^,]*,/version='${GITHUB_REF##*/}',/" setup.py + sed -i "s/release = [^,]*,/release = '${GITHUB_REF##*/}'/" docs/conf.py + sed -i "s/version = [^,]*,/version = '${GITHUB_REF##*/}'/" docs/conf.py + - name: Build js + run: su - app -c "cd $(pwd) && npm install && npm run build" + - name: Update changelog + run: | + export GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" + echo -e "$(python changelog.py ${GITHUB_REF##*/})\n$(cat CHANGELOG)" > CHANGELOG + - name: Fix git dubious ownership + run: git config --global --add safe.directory /__w/django-autocomplete-light/django-autocomplete-light + - name: Tell git we are on master branch + run: | + git fetch origin master:refs/remotes/origin/master + git reset --soft origin/master + - name: Get last commit message + id: last-commit-message + run: echo "msg=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT + - name: Build python package + run: python setup.py sdist + - name: Twine upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + run: twine upload dist/django-autocomplete-light-${GITHUB_REF##*/}.tar.gz + - name: Commit all generated files + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_options: '--amend --no-edit' + status_options: '--untracked-files=no' + push_options: '--force' + commit_message: ${{ steps.last-commit-message.outputs.msg }} + branch: master + skip_fetch: true diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..9f72f0cde --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/dal_alight/static/dal_alight"] + path = src/dal_alight/static/dal_alight + url = https://yourlabs.io/oss/autocomplete-light.git diff --git a/CHANGELOG b/CHANGELOG index bf7e58157..604d88dc5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,5 @@ - - - - 3.9.5-rc6 2023-04-07 Release 3.9.5-rc6 diff --git a/package-lock.json b/package-lock.json index 7848c8184..cc01e4104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "django-autocomplete-light", - "version": "3.5.1", + "version": "3.9.8rc25", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "django-autocomplete-light", - "version": "3.5.1", + "version": "3.9.8rc25", "license": "MIT", "dependencies": { "semver": "latest" diff --git a/package.json b/package.json index c99264b89..a5ac4fbc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "django-autocomplete-light", - "version": "3.5.1", + "version": "3.9.8rc25", "description": "A fresh approach to autocomplete implementations, specially for Django. Status: v3 stable, 2.x.x stable, 1.x.x deprecated. Please DO regularely ping us with your link at #yourlabs IRC channel https://django-autocomplete-light.readthedocs.io/", "directories": { "doc": "docs" diff --git a/release.sh b/release.sh index 75e0742f9..3850a5a22 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,4 @@ -#!/bin/bash -eu +#!/bin/bash -eux # Release a new version of django-autocomplete-light # # Usage: ./release.sh 1.2.3-rc0 diff --git a/setup.py b/setup.py index 77b7185a6..4397a51f5 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def read(fname): setup( name='django-autocomplete-light', - version='3.9.7', + version='3.9.8rc25', description='Fresh autocompletes for Django', author='James Pic', author_email='jamespic@gmail.com', diff --git a/src/dal/autocomplete.py b/src/dal/autocomplete.py index 7f201b893..69ab5c30e 100644 --- a/src/dal/autocomplete.py +++ b/src/dal/autocomplete.py @@ -34,6 +34,15 @@ def _installed(*apps): return True +if _installed('dal_alight'): + from dal_alight.widgets import ( + ModelAlight, + ) + from dal_alight.views import ( + AlightQuerySetView, + ) + + if _installed('dal_select2'): from dal_select2.widgets import ( Select2, diff --git a/src/dal/widgets.py b/src/dal/widgets.py index 1b6e22675..65f8b0889 100644 --- a/src/dal/widgets.py +++ b/src/dal/widgets.py @@ -154,7 +154,10 @@ def render(self, name, value, attrs=None, renderer=None, **kwargs): except (KeyError, TypeError): field_id = name conf = self.render_forward_conf(field_id) - return mark_safe(widget + conf) + html = widget + conf + if getattr(self, 'component', None): + html = f'<{self.component}>{html}' + return mark_safe(html) def _get_url(self): if self._url is None: diff --git a/src/dal_alight/__init__.py b/src/dal_alight/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/dal_alight/admin.py b/src/dal_alight/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/src/dal_alight/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/dal_alight/apps.py b/src/dal_alight/apps.py new file mode 100644 index 000000000..4a93b1e27 --- /dev/null +++ b/src/dal_alight/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DalAlightConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "dal_alight" diff --git a/src/dal_alight/migrations/__init__.py b/src/dal_alight/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/dal_alight/models.py b/src/dal_alight/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/src/dal_alight/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/src/dal_alight/static/dal_alight b/src/dal_alight/static/dal_alight new file mode 160000 index 000000000..bd9d8ba4a --- /dev/null +++ b/src/dal_alight/static/dal_alight @@ -0,0 +1 @@ +Subproject commit bd9d8ba4a5e8c872426c0d51d8c1ec5296d3272d diff --git a/src/dal_alight/tests.py b/src/dal_alight/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/dal_alight/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/dal_alight/views.py b/src/dal_alight/views.py new file mode 100644 index 000000000..069a4cc22 --- /dev/null +++ b/src/dal_alight/views.py @@ -0,0 +1,17 @@ +from django import http +from dal.views import BaseQuerySetView + + + +class AlightQuerySetView(BaseQuerySetView): + def render_to_response(self, context): + """Return a JSON response in Select2 format.""" + html = [] + for result in context['object_list']: + html.append(f''' +
+ {self.get_result_label(result)} +
+ ''') + + return http.HttpResponse(html) diff --git a/src/dal_alight/widgets.py b/src/dal_alight/widgets.py new file mode 100644 index 000000000..f1a7eb030 --- /dev/null +++ b/src/dal_alight/widgets.py @@ -0,0 +1,41 @@ +from django import forms +from django.urls import reverse +from dal.widgets import ( + QuerySetSelectMixin, +) + + +class AlightWidgetMixin: + component = 'autocomplete-select' + + @property + def media(self): + return forms.Media( + css=dict(all=['dal_alight/autocomplete-light.css']), + js=['dal_alight/autocomplete-light.js'], + ) + + def render(self, name, value, attrs=None, renderer=None, **kwargs): + # this prevents ModelChoiceIterator from rendering empty option that we + # don't need with autocompletes + self.choices.field.empty_label = None + + attrs = attrs or {} + attrs.setdefault('slot', 'select') + widget = super().render(name, value, attrs=attrs, renderer=renderer, + **kwargs) + deck = '
' + input = f''' + + + + ''' + html = widget + deck + input + return html + + + +class ModelAlight(QuerySetSelectMixin, + AlightWidgetMixin, + forms.Select): + pass diff --git a/test_project/alight_foreign_key/admin.py b/test_project/alight_foreign_key/admin.py new file mode 100644 index 000000000..ea547b699 --- /dev/null +++ b/test_project/alight_foreign_key/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from .forms import TForm +from .models import TModel + + +class TestInline(admin.TabularInline): + form = TForm + model = TModel + fk_name = 'for_inline' + + +class TestAdmin(admin.ModelAdmin): + form = TForm + inlines = [TestInline] +admin.site.register(TModel, TestAdmin) diff --git a/test_project/alight_foreign_key/forms.py b/test_project/alight_foreign_key/forms.py new file mode 100644 index 000000000..0fb0bf8ad --- /dev/null +++ b/test_project/alight_foreign_key/forms.py @@ -0,0 +1,14 @@ +from dal import autocomplete + +from django import forms + +from .models import TModel + + +class TForm(forms.ModelForm): + class Meta: + model = TModel + fields = ('name', 'test') + widgets = { + 'test': autocomplete.ModelAlight(url='alight_fk') + } diff --git a/test_project/alight_foreign_key/migrations/0001_initial.py b/test_project/alight_foreign_key/migrations/0001_initial.py new file mode 100644 index 000000000..02ca68a15 --- /dev/null +++ b/test_project/alight_foreign_key/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.1 on 2023-05-22 09:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="TModel", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ( + "for_inline", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="inline_test_models", + to="alight_foreign_key.tmodel", + ), + ), + ( + "test", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="related_test_models", + to="alight_foreign_key.tmodel", + ), + ), + ], + ), + ] diff --git a/test_project/alight_foreign_key/migrations/__init__.py b/test_project/alight_foreign_key/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_project/alight_foreign_key/models.py b/test_project/alight_foreign_key/models.py new file mode 100644 index 000000000..8d843e3b7 --- /dev/null +++ b/test_project/alight_foreign_key/models.py @@ -0,0 +1,24 @@ +from django.db import models + + +class TModel(models.Model): + name = models.CharField(max_length=200) + + test = models.ForeignKey( + 'self', + models.CASCADE, + null=True, + blank=True, + related_name='related_test_models' + ) + + for_inline = models.ForeignKey( + 'self', + models.CASCADE, + null=True, + blank=True, + related_name='inline_test_models' + ) + + def __str__(self): + return self.name diff --git a/test_project/alight_foreign_key/urls.py b/test_project/alight_foreign_key/urls.py new file mode 100644 index 000000000..55cf2d522 --- /dev/null +++ b/test_project/alight_foreign_key/urls.py @@ -0,0 +1,14 @@ +from dal import autocomplete + +from django.urls import re_path as url + +from .models import TModel + + +urlpatterns = [ + url( + 'test-autocomplete/$', + autocomplete.AlightQuerySetView.as_view(model=TModel), + name='alight_fk', + ), +] diff --git a/test_project/settings/base.py b/test_project/settings/base.py index 897d99155..15214a272 100644 --- a/test_project/settings/base.py +++ b/test_project/settings/base.py @@ -63,6 +63,7 @@ def get_databases(base_dir): 'django.contrib.staticfiles', # test apps + 'alight_foreign_key', 'select2_foreign_key', 'select2_list', 'select2_generic_foreign_key', @@ -83,6 +84,7 @@ def get_databases(base_dir): # Autocomplete 'dal', # Enable plugins + 'dal_alight', 'dal_select2', 'queryset_sequence', 'dal_queryset_sequence', diff --git a/test_project/urls.py b/test_project/urls.py index d52e0753d..8a06a2a53 100644 --- a/test_project/urls.py +++ b/test_project/urls.py @@ -11,6 +11,8 @@ url(r'^admin/', admin.site.urls), url(r'^login/', views.LoginView.as_view()), + url(r'^alight_foreign_key/', include('alight_foreign_key.urls')), + url(r'^dal_single/', views.BasicDALView, name='isolated_dal_single'), url(r'^dal_multi/', views.BasicDALMultiView, name='isolated_dal_multi'),