From 45ad9bf88c910e7ce4835e4cf0c489c034044aed Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Mon, 10 Jul 2023 19:40:15 +0200 Subject: [PATCH] Migrate old django to the newest version Django 1.x is no longer supported, and the app needed migration to 4.x A lot of libraries has been unsupported or removed, so there's a few of unrelated changes, but necessary for the migration process to work. --- requirements.txt | 50 ++-- spejstore/settings.py | 12 +- .../django_admin_hstore_widget.js | 216 ++++++++++++++++++ .../admin/js/django_hstore/hstore-widget.js | 208 ----------------- storage/admin.py | 16 +- storage/apiviews.py | 25 +- storage/migrations/0001_initial.py | 8 +- .../0001_squashed_0008_item_state.py | 10 +- storage/migrations/0003_auto_20170424_2002.py | 4 +- storage/migrations/0005.py | 2 +- storage/migrations/0007_auto_20230710_1721.py | 22 ++ .../0008_force_extensions_via_django.py | 18 ++ .../migrations/0009_migrate_tree_fields.py | 29 +++ storage/models.py | 27 ++- storage/serializers.py | 3 +- .../admin/storage/item/change_form.html | 56 ++--- storage/views.py | 2 +- storage/widgets.py | 36 +-- templates/base.html | 89 +++++--- templates/hstore_default_widget.html | 4 +- 20 files changed, 490 insertions(+), 347 deletions(-) create mode 100644 static/admin/js/django_admin_hstore_widget/django_admin_hstore_widget.js delete mode 100644 static/admin/js/django_hstore/hstore-widget.js create mode 100644 storage/migrations/0007_auto_20230710_1721.py create mode 100644 storage/migrations/0008_force_extensions_via_django.py create mode 100644 storage/migrations/0009_migrate_tree_fields.py diff --git a/requirements.txt b/requirements.txt index 8f673f2..8172ddd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,34 @@ -certifi==2017.4.17 -chardet==3.0.3 -Django==1.11.15 -git+https://github.com/djangonauts/django-hstore@61427e474cb2f4be8fdfce225d78a5330bc77eb0#egg=django-hstore -git+https://github.com/d42/django-tree@687c01c02d91cada9ca1912e34e482da9e73e27a#egg=django-tree -django-appconf==1.0.2 -django-flat-responsive==2.0 -social-auth-app-django==2.1.0 -Django-Select2==6.3.1 -djangorestframework==3.5.4 -Pillow==3.3.1 -psycopg2==2.7.5 -djangorestframework-hstore==1.3 -requests==2.16.5 -urllib3==1.21.1 -django_markdown2==0.3.0 +asgiref==3.7.2 +certifi==2023.5.7 +cffi==1.15.1 +chardet==5.1.0 +charset-normalizer==3.2.0 +colorclass==2.2.2 +cryptography==41.0.1 +defusedxml==0.7.1 +Django==3.2.20 +django-admin-hstore-widget==1.2.1 +django-appconf==1.0.5 +django-hstore==1.4.2 +django-markdown2==0.3.1 +django-select2==8.1.2 +django-tree==0.5.6 +djangorestframework==3.14.0 +docopt==0.6.2 +idna==3.4 +markdown2==2.4.9 +oauthlib==3.2.2 +packaging==23.1 +Pillow==10.0.0 +psycopg2==2.9.6 +pycparser==2.21 +PyJWT==2.7.0 +python3-openid==3.2.0 +pytz==2023.3 +requests==2.31.0 +requests-oauthlib==1.3.1 +social-auth-app-django==5.2.0 +social-auth-core==4.4.2 +sqlparse==0.4.4 +terminaltables==3.1.10 +urllib3==2.0.3 diff --git a/spejstore/settings.py b/spejstore/settings.py index cc08f06..27227a6 100644 --- a/spejstore/settings.py +++ b/spejstore/settings.py @@ -32,7 +32,9 @@ SECRET_KEY = env("SECRET_KEY", "#hjthi7_udsyt*9eeyb&nwgw5x=%pk_lnz3+u2tg9@=w3p1m DEBUG = not PROD ALLOWED_HOSTS = env( - "ALLOWED_HOSTS", "devinventory,inventory.waw.hackerspace.pl,i,inventory" + "ALLOWED_HOSTS", + "devinventory,inventory.waw.hackerspace.pl,i,inventory" + + (",127.0.0.1" if PROD != True else ""), ).split(",") LOGIN_REDIRECT_URL = "/admin/" @@ -40,7 +42,6 @@ LOGIN_REDIRECT_URL = "/admin/" # Application definition INSTALLED_APPS = [ - "flat_responsive", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -49,13 +50,13 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "django.contrib.postgres", "social_django", - "django_hstore", "tree", "django_select2", "rest_framework", "rest_framework.authtoken", "django_markdown2", "storage", + "django_admin_hstore_widget", ] MIDDLEWARE = [ @@ -125,11 +126,6 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] -# select2 - -SELECT2_JS = "js/select2.min.js" -SELECT2_CSS = "css/select2.min.css" -SELECT2_I18N_PATH = "" AUTHENTICATION_BACKENDS = ( "auth.backend.HSWawOAuth2", diff --git a/static/admin/js/django_admin_hstore_widget/django_admin_hstore_widget.js b/static/admin/js/django_admin_hstore_widget/django_admin_hstore_widget.js new file mode 100644 index 0000000..07295a3 --- /dev/null +++ b/static/admin/js/django_admin_hstore_widget/django_admin_hstore_widget.js @@ -0,0 +1,216 @@ +var initDjangoHStoreWidget = function (hstore_field_name, inline_prefix) { + // ignore inline templates + // if hstore_field_name contains "__prefix__" + if (hstore_field_name.indexOf("__prefix__") > -1) { + return; + } + + var $ = django.jQuery; + + // processing inlines + if (hstore_field_name.indexOf("inline") > -1) { + var inlineClass = $("#id_" + hstore_field_name) + .parents(".inline-related, .grp-group") + .attr("class"); + // if using TabularInlines stop here + // TabularInlines not supported + if (inlineClass.indexOf("tabular") > -1) { + return; + } + } + + // reusable function that retrieves a template even if ID is not correct + // (written to support inlines) + var retrieveTemplate = function (template_name, field_name) { + var specific_template = $("#" + template_name + "-" + field_name); + // if found specific template return that + if (specific_template.length) { + return specific_template.html(); + } else { + // get fallback template + var html = $("." + template_name + "-inline").html(); + // replace all occurrences of __prefix__ with field_name + // and return + html = html.replace(/__prefix__/g, inline_prefix); + return html; + } + }; + + // reusable function that compiles the UI + var compileUI = function (params) { + var hstore_field_id = "id_" + hstore_field_name, + original_textarea = $("#" + hstore_field_id), + original_value = original_textarea.val(), + original_container = original_textarea + .parents(".form-row, .grp-row") + .eq(0), + errorHtml = original_container.find(".errorlist").html(), + json_data = {}; + + if (original_value !== "") { + // manage case in which textarea is blank + try { + json_data = JSON.parse(original_value); + } catch (e) { + alert("invalid JSON:\n" + e); + return false; + } + } + + var hstore_field_data = { + id: hstore_field_id, + label: original_container.find("label").text(), + name: hstore_field_name, + value: original_textarea.val(), + help: original_container.find(".grp-help, .help").text(), + errors: errorHtml, + data: json_data, + }, + // compile template + ui_html = retrieveTemplate("hstore-ui-template", hstore_field_name), + compiled_ui_html = _.template(ui_html, hstore_field_data); + + // this is just to DRY up a bit + if (params && params.replace_original === true) { + // remove original textarea to avoid having two textareas with same ID + original_textarea.remove(); + // inject compiled template and hide original + original_container.after(compiled_ui_html).hide(); + } + + return compiled_ui_html; + }; + + // generate UI + compileUI({ replace_original: true }); + + // cache other objects that we'll reuse + var row_html = retrieveTemplate("hstore-row-template", hstore_field_name), + empty_row = _.template(row_html, { key: "", value: "" }), + $hstore = $("#id_" + hstore_field_name).parents(".hstore"); + + // reusable function that updates the textarea value + var updateTextarea = function (container) { + // init empty json object + var new_value = {}, + raw_textarea = container.find("textarea"), + rows = container.find(".form-row, .grp-row"); + + // loop over each object and populate json + rows.each(function () { + var inputs = $(this).find("input"), + key = $(this).find(".hs-key").val(), + value = $(this).find(".hs-val").val(); + new_value[key] = value; + }); + + // update textarea value + $(raw_textarea).val(JSON.stringify(new_value, null, 4)); + }; + + // remove row link + $hstore.delegate("a.remove-row", "click", function (e) { + e.preventDefault(); + // cache container jquery object before $(this) gets removed + $(this).parents(".form-row, .grp-row").eq(0).remove(); + updateTextarea($hstore); + }); + + // add row link + $hstore.delegate("a.hs-add-row, .hs-add-row a", "click", function (e) { + e.preventDefault(); + $hstore.find(".hstore-rows").append(empty_row); + $(".django-select2").djangoSelect2(); + $("select").on("select2:close", function () { + $(this).focus(); + }); + }); + + // toggle textarea link + $hstore.delegate(".hstore-toggle-txtarea", "click", function (e) { + e.preventDefault(); + + var raw_textarea = $hstore.find(".hstore-textarea"), + hstore_rows = $hstore.find(".hstore-rows"), + add_row = $hstore.find(".hs-add-row"); + + if (raw_textarea.is(":visible")) { + var compiled_ui = compileUI(); + + // in case of JSON error + if (compiled_ui === false) { + return; + } + + // jquery < 1.8 + try { + var $ui = $(compiled_ui); + } catch (e) { + // jquery >= 1.8 + var $ui = $($.parseHTML(compiled_ui)); + } + + // update rows with only relevant content + hstore_rows.html($ui.find(".hstore-rows").html()); + + raw_textarea.hide(); + hstore_rows.show(); + add_row.show(); + + $(".django-select2").djangoSelect2(); + } else { + raw_textarea.show(); + hstore_rows.hide(); + add_row.hide(); + } + }); + + // update textarea whenever a field changes + $hstore.delegate(".hs-val", "keyup propertychange", function () { + updateTextarea($hstore); + }); + + $hstore.delegate(".hs-key", "change", function () { + updateTextarea($hstore); + }); +}; +window.addEventListener("load", function () { + // support inlines + // bind only once + if (django.hstoreWidgetBoundInlines === undefined) { + var $ = django.jQuery; + $( + ".grp-group .grp-add-handler, .inline-group .hs-add-row a, .inline-group .add-row" + ).click(function (e) { + var hstore_original_textareas = $(this) + .parents(".grp-group, .inline-group") + .eq(0) + .find(".hstore-original-textarea"); + // if module contains .hstore-original-textarea + if (hstore_original_textareas.length > 0) { + // loop over each inline + $(this) + .parents(".grp-group, .inline-group") + .find(".grp-items div.grp-dynamic-form, .inline-related") + .each(function (e, i) { + var prefix = i; + // loop each textarea + $(this) + .find(".hstore-original-textarea") + .each(function (e, i) { + // cache field name + var field_name = $(this).attr("name"); + // ignore templates + // if name attribute contains __prefix__ + if (field_name.indexOf("prefix") > -1) { + // skip to next + return; + } + initDjangoHStoreWidget(field_name, prefix); + }); + }); + } + }); + django.hstoreWidgetBoundInlines = true; + } +}); diff --git a/static/admin/js/django_hstore/hstore-widget.js b/static/admin/js/django_hstore/hstore-widget.js deleted file mode 100644 index a7f1bb4..0000000 --- a/static/admin/js/django_hstore/hstore-widget.js +++ /dev/null @@ -1,208 +0,0 @@ -var initDjangoHStoreWidget = function(hstore_field_name, inline_prefix) { - // ignore inline templates - // if hstore_field_name contains "__prefix__" - if(hstore_field_name.indexOf('__prefix__') > -1){ - return; - } - - // processing inlines - if(hstore_field_name.indexOf('inline') > -1){ - var inlineClass = $('#id_'+hstore_field_name).parents('.inline-related, .grp-group').attr('class'); - // if using TabularInlines stop here - // TabularInlines not supported - if (inlineClass.indexOf('tabular') > -1) { - return; - } - } - - // reusable function that retrieves a template even if ID is not correct - // (written to support inlines) - var retrieveTemplate = function(template_name, field_name){ - var specific_template = $('#'+template_name+'-'+field_name); - // if found specific template return that - if(specific_template.length){ - return specific_template.html(); - } - else{ - // get fallback template - var html = $('.'+template_name+'-inline').html(); - // replace all occurrences of __prefix__ with field_name - // and return - html = html.replace(/__prefix__/g, inline_prefix); - return html; - } - } - - // reusable function that compiles the UI - var compileUI = function(params){ - var hstore_field_id = 'id_'+hstore_field_name, - original_textarea = $('#'+hstore_field_id), - original_value = original_textarea.val(), - original_container = original_textarea.parents('.form-row, .grp-row').eq(0), - errorHtml = original_container.find('.errorlist').html(), - json_data = {}; - - if(original_value !== ''){ - // manage case in which textarea is blank - try{ - json_data = JSON.parse(original_value); - } - catch(e){ - alert('invalid JSON:\n'+e); - return false; - } - } - - var hstore_field_data = { - "id": hstore_field_id, - "label": original_container.find('label').text(), - "name": hstore_field_name, - "value": original_textarea.val(), - "help": original_container.find('.grp-help, .help').text(), - "errors": errorHtml, - "data": json_data - }, - // compile template - ui_html = retrieveTemplate('hstore-ui-template', hstore_field_name), - compiled_ui_html = _.template(ui_html, hstore_field_data); - - // this is just to DRY up a bit - if(params && params.replace_original === true){ - // remove original textarea to avoid having two textareas with same ID - original_textarea.remove(); - // inject compiled template and hide original - original_container.after(compiled_ui_html).hide(); - } - - return compiled_ui_html; - }; - - - - // generate UI - compileUI({ replace_original: true }); - - // cache other objects that we'll reuse - var row_html = retrieveTemplate('hstore-row-template', hstore_field_name), - empty_row = _.template(row_html, { 'key': '', 'value': '' }), - $hstore = $('#id_'+hstore_field_name).parents('.hstore'); - - // reusable function that updates the textarea value - var updateTextarea = function(container) { - // init empty json object - var new_value = {}, - raw_textarea = container.find('textarea'), - rows = container.find('.form-row, .grp-row'); - - // loop over each object and populate json - rows.each(function() { - var inputs = $(this).find('input'), - key = $(this).find('.hs-key').val(), - value = $(this).find('.hs-val').val(); - new_value[key] = value; - }); - - // update textarea value - $(raw_textarea).val(JSON.stringify(new_value, null, 4)); - }; - - // remove row link - $hstore.delegate('a.remove-row', 'click', function(e) { - e.preventDefault(); - // cache container jquery object before $(this) gets removed - $(this).parents('.form-row, .grp-row').eq(0).remove(); - updateTextarea($hstore); - }); - - // add row link - $hstore.delegate('a.hs-add-row, .hs-add-row a', 'click', function(e) { - e.preventDefault(); - $hstore.find('.hstore-rows').append(empty_row); - $('.django-select2').djangoSelect2() - $('select').on( 'select2:close', function () { - $(this).focus(); - }); - }); - - // toggle textarea link - $hstore.delegate('.hstore-toggle-txtarea', 'click', function(e) { - e.preventDefault(); - - var raw_textarea = $hstore.find('.hstore-textarea'), - hstore_rows = $hstore.find('.hstore-rows'), - add_row = $hstore.find('.hs-add-row'); - - if(raw_textarea.is(':visible')) { - - var compiled_ui = compileUI(); - - // in case of JSON error - if(compiled_ui === false){ - return; - } - - // jquery < 1.8 - try{ - var $ui = $(compiled_ui); - } - // jquery >= 1.8 - catch(e){ - var $ui = $($.parseHTML(compiled_ui)); - } - - // update rows with only relevant content - hstore_rows.html($ui.find('.hstore-rows').html()); - - raw_textarea.hide(); - hstore_rows.show(); - add_row.show(); - - $('.django-select2').djangoSelect2() - } - else{ - raw_textarea.show(); - hstore_rows.hide(); - add_row.hide(); - } - }); - - // update textarea whenever a field changes - $hstore.delegate('.hs-val', 'keyup propertychange', function() { - updateTextarea($hstore); - }); - - $hstore.delegate('.hs-key', 'change', function() { - updateTextarea($hstore); - }); -}; - -django.jQuery(window).load(function() { - // support inlines - // bind only once - if(django.hstoreWidgetBoundInlines === undefined){ - var $ = django.jQuery; - $('.grp-group .grp-add-handler, .inline-group .hs-add-row a, .inline-group .add-row').click(function(e){ - var hstore_original_textareas = $(this).parents('.grp-group, .inline-group').eq(0).find('.hstore-original-textarea'); - // if module contains .hstore-original-textarea - if(hstore_original_textareas.length > 0){ - // loop over each inline - $(this).parents('.grp-group, .inline-group').find('.grp-items div.grp-dynamic-form, .inline-related').each(function(e, i){ - var prefix = i; - // loop each textarea - $(this).find('.hstore-original-textarea').each(function(e, i){ - // cache field name - var field_name = $(this).attr('name'); - // ignore templates - // if name attribute contains __prefix__ - if(field_name.indexOf('prefix') > -1){ - // skip to next - return; - } - initDjangoHStoreWidget(field_name, prefix); - }); - }); - } - }); - django.hstoreWidgetBoundInlines = true; - } -}); diff --git a/storage/admin.py b/storage/admin.py index 9ec3bc4..d2404ce 100644 --- a/storage/admin.py +++ b/storage/admin.py @@ -1,16 +1,17 @@ from django import forms from django.contrib import admin -from django_select2.forms import ModelSelect2Widget, Select2MultipleWidget +from django_select2.forms import Select2MultipleWidget from .models import Item, ItemImage, Category, Label -from .widgets import ItemSelectWidget, PropsSelectWidget + +from .widgets import PropsSelectWidget class ModelAdminMixin(object): def has_add_permission(self, request, obj=None): - return request.user.is_authenticated() + return request.user.is_authenticated has_change_permission = has_add_permission has_delete_permission = has_add_permission @@ -24,7 +25,6 @@ class ItemForm(forms.ModelForm): model = Item exclude = [] widgets = { - "parent": ItemSelectWidget(model=Item), "categories": Select2MultipleWidget, "props": PropsSelectWidget, } @@ -45,9 +45,11 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin): form = ItemForm inlines = [ItemImageInline, LabelInline] save_on_top = True + autocomplete_fields = ["parent"] + search_fields = ["parent"] def _name(self, obj): - return "-" * obj.get_level() + "> " + obj.name + return ("-" * (obj.get_level() or 0)) + "> " + obj.name def save_model(self, request, obj, form, change): super(ItemAdmin, self).save_model(request, obj, form, change) @@ -67,10 +69,6 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin): return data class Media: - js = ( - # Required by select2 - "https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js", - ) css = {"all": ("css/admin.css",)} def response_action(self, request, queryset): diff --git a/storage/apiviews.py b/storage/apiviews.py index 15f3dc4..36bd6ce 100644 --- a/storage/apiviews.py +++ b/storage/apiviews.py @@ -1,6 +1,7 @@ from rest_framework import viewsets, generics, filters from rest_framework.response import Response -from rest_framework.decorators import detail_route +from rest_framework.decorators import action + from rest_framework.permissions import AllowAny from storage.models import Item, Label @@ -32,7 +33,7 @@ class LabelViewSet(viewsets.ModelViewSet): queryset = Label.objects serializer_class = LabelSerializer - @detail_route(methods=["post"], permission_classes=[AllowAny]) + @action(detail=True, methods=["post"], permission_classes=[AllowAny]) def print(self, request, pk): quantity = min(int(request.query_params.get("quantity", 1)), 5) obj = self.get_object() @@ -52,7 +53,7 @@ class ItemViewSet(viewsets.ModelViewSet): ordering_fields = "__all__" def get_queryset(self): - return Item.get_roots() + return Item.objects.filter_roots() def get_object(self): lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field @@ -73,7 +74,7 @@ class ItemViewSet(viewsets.ModelViewSet): except Label.DoesNotExist: raise Http404() - @detail_route(methods=["post"], permission_classes=[AllowAny]) + @action(detail=True, methods=["post"], permission_classes=[AllowAny]) def print(self, request, pk): # todo: deduplicate quantity = min(int(request.query_params.get("quantity", 1)), 5) @@ -82,28 +83,36 @@ class ItemViewSet(viewsets.ModelViewSet): obj.print() return obj - @detail_route() + @action( + detail=True, + ) def children(self, request, pk): item = self.get_object() return Response( self.serializer_class(item.get_children().all(), many=True).data ) - @detail_route() + @action( + detail=True, + ) def ancestors(self, request, pk): item = self.get_object() return Response( self.serializer_class(item.get_ancestors().all(), many=True).data ) - @detail_route() + @action( + detail=True, + ) def descendants(self, request, pk): item = self.get_object() return Response( self.serializer_class(item.get_descendants().all(), many=True).data ) - @detail_route() + @action( + detail=True, + ) def siblings(self, request, pk): item = self.get_object() return Response( diff --git a/storage/migrations/0001_initial.py b/storage/migrations/0001_initial.py index 6f2556d..ccd940c 100644 --- a/storage/migrations/0001_initial.py +++ b/storage/migrations/0001_initial.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals from django.db import migrations, models -import django_hstore.fields +from django.contrib.postgres.fields import HStoreField +from django.contrib.postgres.operations import HStoreExtension +from django.contrib.postgres.operations import TrigramExtension class Migration(migrations.Migration): @@ -12,6 +14,8 @@ class Migration(migrations.Migration): dependencies = [] operations = [ + HStoreExtension(), + TrigramExtension(), migrations.CreateModel( name="Item", fields=[ @@ -26,7 +30,7 @@ class Migration(migrations.Migration): ), ("name", models.TextField()), ("description", models.TextField()), - ("props", django_hstore.fields.DictionaryField()), + ("props", HStoreField()), ], ), ] diff --git a/storage/migrations/0001_squashed_0008_item_state.py b/storage/migrations/0001_squashed_0008_item_state.py index 0c5cf5a..6410e81 100644 --- a/storage/migrations/0001_squashed_0008_item_state.py +++ b/storage/migrations/0001_squashed_0008_item_state.py @@ -5,7 +5,11 @@ from __future__ import unicode_literals from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import django_hstore.fields +from django.contrib.postgres.fields import HStoreField +from django.contrib.postgres.operations import HStoreExtension +from django.contrib.postgres.operations import TrigramExtension + + import uuid @@ -28,12 +32,14 @@ class Migration(migrations.Migration): ] operations = [ + HStoreExtension(), + TrigramExtension(), migrations.CreateModel( name="Item", fields=[ ("name", models.TextField()), ("description", models.TextField(blank=True)), - ("props", django_hstore.fields.DictionaryField()), + ("props", HStoreField()), ( "uuid", models.UUIDField( diff --git a/storage/migrations/0003_auto_20170424_2002.py b/storage/migrations/0003_auto_20170424_2002.py index 4682695..3b1cbb3 100644 --- a/storage/migrations/0003_auto_20170424_2002.py +++ b/storage/migrations/0003_auto_20170424_2002.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion -import django_hstore.fields +from django.contrib.postgres.fields import HStoreField class Migration(migrations.Migration): @@ -35,7 +35,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="item", name="props", - field=django_hstore.fields.DictionaryField(blank=True), + field=HStoreField(blank=True), ), migrations.AddField( model_name="label", diff --git a/storage/migrations/0005.py b/storage/migrations/0005.py index 9c6e506..2a1ac99 100644 --- a/storage/migrations/0005.py +++ b/storage/migrations/0005.py @@ -12,6 +12,6 @@ class Migration(migrations.Migration): operations = [ DeleteTreeTrigger("storage.Item"), - CreateTreeTrigger("storage.Item", order_by=("name",)), + CreateTreeTrigger("storage.Item"), RebuildPaths("storage.Item"), ] diff --git a/storage/migrations/0007_auto_20230710_1721.py b/storage/migrations/0007_auto_20230710_1721.py new file mode 100644 index 0000000..e726682 --- /dev/null +++ b/storage/migrations/0007_auto_20230710_1721.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.20 on 2023-07-10 17:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('storage', '0006_category_icon_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'ordering': ['name'], 'verbose_name_plural': 'categories'}, + ), + migrations.AlterField( + model_name='item', + name='state', + field=models.CharField(choices=[('present', 'Present'), ('taken', 'Taken'), ('broken', 'Broken'), ('missing', 'Missing'), ('depleted', 'Depleted')], default='present', max_length=31), + ), + ] diff --git a/storage/migrations/0008_force_extensions_via_django.py b/storage/migrations/0008_force_extensions_via_django.py new file mode 100644 index 0000000..fa83a21 --- /dev/null +++ b/storage/migrations/0008_force_extensions_via_django.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-07-10 22:40 + +from django.db import migrations +from django.contrib.postgres.operations import HStoreExtension +from django.contrib.postgres.operations import TrigramExtension + + +# This migration is necessary for current production purposes. +# Technically is a no-op if extensions are turned on already. +class Migration(migrations.Migration): + dependencies = [ + ("storage", "0007_auto_20230710_1721"), + ] + + operations = [ + HStoreExtension(), + TrigramExtension(), + ] diff --git a/storage/migrations/0009_migrate_tree_fields.py b/storage/migrations/0009_migrate_tree_fields.py new file mode 100644 index 0000000..ee6f3a3 --- /dev/null +++ b/storage/migrations/0009_migrate_tree_fields.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.20 on 2023-07-11 12:27 + +from django.db import migrations +from django.contrib.postgres.operations import HStoreExtension +from django.contrib.postgres.operations import TrigramExtension +from tree.operations import ( + DeleteTreeTrigger, + CreateTreeTrigger, + RebuildPaths, +) +from tree.fields import PathField + + +class Migration(migrations.Migration): + dependencies = [ + ("storage", "0008_force_extensions_via_django"), + ] + + operations = [ + DeleteTreeTrigger("storage.Item"), + migrations.RemoveField("Item", "path"), + migrations.AddField( + model_name="item", + name="path", + field=PathField(db_index=True, order_by=["name"], size=None), + ), + CreateTreeTrigger("item"), + RebuildPaths("item"), + ] diff --git a/storage/models.py b/storage/models.py index 410ce3e..b546fb0 100644 --- a/storage/models.py +++ b/storage/models.py @@ -6,9 +6,10 @@ import re from django.db import models from django.conf import settings from django.contrib.auth.models import User -from django_hstore import hstore from tree.fields import PathField from tree.models import TreeModelMixin +from django.contrib.postgres.fields import HStoreField + import requests @@ -46,7 +47,7 @@ class Category(models.Model): class Item(models.Model, TreeModelMixin): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - parent = models.ForeignKey("self", null=True, blank=True) + parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE) path = PathField() name = models.TextField() @@ -54,17 +55,25 @@ class Item(models.Model, TreeModelMixin): description = models.TextField(blank=True, null=True) state = models.CharField(max_length=31, choices=STATES, default=STATES[0][0]) categories = models.ManyToManyField(Category, blank=True) - owner = models.ForeignKey(User, null=True, blank=True, related_name="owned_items") + owner = models.ForeignKey( + User, + null=True, + blank=True, + related_name="owned_items", + on_delete=models.CASCADE, + ) taken_by = models.ForeignKey( - User, null=True, blank=True, related_name="taken_items" + User, + null=True, + blank=True, + related_name="taken_items", + on_delete=models.CASCADE, ) taken_on = models.DateTimeField(blank=True, null=True) taken_until = models.DateTimeField(blank=True, null=True) - props = hstore.DictionaryField(blank=True) - - objects = hstore.HStoreManager() + props = HStoreField(blank=True) def short_id(self): # let's just hope we never have 4 294 967 296 things :) @@ -105,7 +114,7 @@ class Item(models.Model, TreeModelMixin): class ItemImage(models.Model): - item = models.ForeignKey(Item, related_name="images") + item = models.ForeignKey(Item, related_name="images", on_delete=models.CASCADE) image = models.ImageField() def __str__(self): @@ -114,7 +123,7 @@ class ItemImage(models.Model): class Label(models.Model): id = models.CharField(max_length=64, primary_key=True) - item = models.ForeignKey(Item, related_name="labels") + item = models.ForeignKey(Item, related_name="labels", on_delete=models.CASCADE) style = models.CharField( max_length=32, choices=(("basic_99012_v1", "Basic Dymo 89x36mm label"),), diff --git a/storage/serializers.py b/storage/serializers.py index 334f0cc..dbe96c2 100644 --- a/storage/serializers.py +++ b/storage/serializers.py @@ -1,10 +1,9 @@ from django.contrib.auth.models import User from storage.models import Item, Label, Category from rest_framework import serializers -from rest_framework_hstore.serializers import HStoreSerializer -class ItemSerializer(HStoreSerializer): +class ItemSerializer(serializers.HStoreField): categories = serializers.SlugRelatedField( queryset=Category.objects, many=True, slug_field="name" ) diff --git a/storage/templates/admin/storage/item/change_form.html b/storage/templates/admin/storage/item/change_form.html index ea0e6ad..b964c18 100644 --- a/storage/templates/admin/storage/item/change_form.html +++ b/storage/templates/admin/storage/item/change_form.html @@ -1,35 +1,29 @@ -{% extends "admin/change_form.html" %} - -{% block submit_buttons_top %} - {# We want add another to be default submit action #} - - {{ block.super }} -{% endblock %} - -{% block submit_buttons_bottom %} - {# We want add another to be default submit action #} - - {{ block.super }} -{% endblock %} - -{% block content %}{{ block.super }} +{% extends "admin/change_form.html" %} {% block submit_buttons_top %} {# We want +add another to be default submit action #} + +{{ block.super }} {% endblock %} {% block submit_buttons_bottom %} {# We want +add another to be default submit action #} + +{{ block.super }} {% endblock %} {% block content %}{{ block.super }} {% endblock %} diff --git a/storage/views.py b/storage/views.py index 7d2e13f..b6a0275 100644 --- a/storage/views.py +++ b/storage/views.py @@ -59,7 +59,7 @@ def apply_smart_search(query, objects): def index(request): - return render(request, "results.html", {"results": Item.get_roots()}) + return render(request, "results.html", {"results": Item.objects.filter_roots()}) def search(request): diff --git a/storage/widgets.py b/storage/widgets.py index d67d714..99f01b0 100644 --- a/storage/widgets.py +++ b/storage/widgets.py @@ -1,31 +1,39 @@ from pkg_resources import parse_version -from django_select2.forms import ModelSelect2Widget, HeavySelect2Widget -from django_hstore.forms import DictionaryFieldWidget +from django_select2.forms import HeavySelect2Widget + from django import get_version from django.urls import reverse from django.conf import settings +from django import forms from django.utils.safestring import mark_safe from django.template import Context from django.template.loader import get_template from django.contrib.admin.widgets import AdminTextareaWidget - -class ItemSelectWidget(ModelSelect2Widget): - def __init__(self, *args, **kwargs): - kwargs["data_view"] = "item-complete" - super(ItemSelectWidget, self).__init__(*args, **kwargs) - - def label_from_instance(self, obj): - return obj.name +from django_admin_hstore_widget.forms import HStoreFormWidget +from django.contrib.postgres.forms import forms +from django.templatetags.static import static -class PropsSelectWidget(DictionaryFieldWidget): +class PropsSelectWidget(HStoreFormWidget): + @property + def media(self): + internal_js = [ + "vendor/jquery/jquery.js", + "django_admin_hstore_widget/underscore-min.js", + "django_admin_hstore_widget/django_admin_hstore_widget.js", + ] + + js = [static("admin/js/%s" % path) for path in internal_js] + + return forms.Media(js=js) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def render(self, name, value, attrs=None): + def render(self, name, value, attrs=None, renderer=None): if attrs is None: attrs = {} # it's called "original" because it will be replaced by a copy @@ -35,7 +43,7 @@ class PropsSelectWidget(DictionaryFieldWidget): ) # get default HTML from AdminTextareaWidget - html = AdminTextareaWidget.render(self, name, value, attrs) + html = AdminTextareaWidget.render(self, name, value, attrs, renderer) # prepare template context template_context = { "field_name": name, @@ -46,7 +54,7 @@ class PropsSelectWidget(DictionaryFieldWidget): "w": w.build_attrs(base_attrs=w.attrs), } # get template object - template = get_template("hstore_%s_widget.html" % self.admin_style) + template = get_template("hstore_default_widget.html") # render additional html additional_html = template.render(template_context) diff --git a/templates/base.html b/templates/base.html index 5499910..e56f3ca 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,53 +2,76 @@ - + {% if title %}{{ title }} - {% endif %}Hackerspace Storage - + - - + + {% block body %}
- - {% block content %} - {% endblock %} + {% block content %} {% endblock %}
{% endblock %} - diff --git a/templates/hstore_default_widget.html b/templates/hstore_default_widget.html index de60c19..59f9e88 100644 --- a/templates/hstore_default_widget.html +++ b/templates/hstore_default_widget.html @@ -66,5 +66,7 @@