1
0
Fork 0

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.
This commit is contained in:
Dariusz Niemczyk 2023-07-10 19:40:15 +02:00
parent 659f04ce9c
commit 45ad9bf88c
No known key found for this signature in database
GPG Key ID: 28DFE7164F497CB6
20 changed files with 490 additions and 347 deletions

View File

@ -1,16 +1,34 @@
certifi==2017.4.17 asgiref==3.7.2
chardet==3.0.3 certifi==2023.5.7
Django==1.11.15 cffi==1.15.1
git+https://github.com/djangonauts/django-hstore@61427e474cb2f4be8fdfce225d78a5330bc77eb0#egg=django-hstore chardet==5.1.0
git+https://github.com/d42/django-tree@687c01c02d91cada9ca1912e34e482da9e73e27a#egg=django-tree charset-normalizer==3.2.0
django-appconf==1.0.2 colorclass==2.2.2
django-flat-responsive==2.0 cryptography==41.0.1
social-auth-app-django==2.1.0 defusedxml==0.7.1
Django-Select2==6.3.1 Django==3.2.20
djangorestframework==3.5.4 django-admin-hstore-widget==1.2.1
Pillow==3.3.1 django-appconf==1.0.5
psycopg2==2.7.5 django-hstore==1.4.2
djangorestframework-hstore==1.3 django-markdown2==0.3.1
requests==2.16.5 django-select2==8.1.2
urllib3==1.21.1 django-tree==0.5.6
django_markdown2==0.3.0 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

View File

@ -32,7 +32,9 @@ SECRET_KEY = env("SECRET_KEY", "#hjthi7_udsyt*9eeyb&nwgw5x=%pk_lnz3+u2tg9@=w3p1m
DEBUG = not PROD DEBUG = not PROD
ALLOWED_HOSTS = env( 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(",") ).split(",")
LOGIN_REDIRECT_URL = "/admin/" LOGIN_REDIRECT_URL = "/admin/"
@ -40,7 +42,6 @@ LOGIN_REDIRECT_URL = "/admin/"
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"flat_responsive",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@ -49,13 +50,13 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.postgres", "django.contrib.postgres",
"social_django", "social_django",
"django_hstore",
"tree", "tree",
"django_select2", "django_select2",
"rest_framework", "rest_framework",
"rest_framework.authtoken", "rest_framework.authtoken",
"django_markdown2", "django_markdown2",
"storage", "storage",
"django_admin_hstore_widget",
] ]
MIDDLEWARE = [ 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 = ( AUTHENTICATION_BACKENDS = (
"auth.backend.HSWawOAuth2", "auth.backend.HSWawOAuth2",

View File

@ -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;
}
});

View File

@ -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;
}
});

View File

@ -1,16 +1,17 @@
from django import forms from django import forms
from django.contrib import admin 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 .models import Item, ItemImage, Category, Label
from .widgets import ItemSelectWidget, PropsSelectWidget
from .widgets import PropsSelectWidget
class ModelAdminMixin(object): class ModelAdminMixin(object):
def has_add_permission(self, request, obj=None): 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_change_permission = has_add_permission
has_delete_permission = has_add_permission has_delete_permission = has_add_permission
@ -24,7 +25,6 @@ class ItemForm(forms.ModelForm):
model = Item model = Item
exclude = [] exclude = []
widgets = { widgets = {
"parent": ItemSelectWidget(model=Item),
"categories": Select2MultipleWidget, "categories": Select2MultipleWidget,
"props": PropsSelectWidget, "props": PropsSelectWidget,
} }
@ -45,9 +45,11 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
form = ItemForm form = ItemForm
inlines = [ItemImageInline, LabelInline] inlines = [ItemImageInline, LabelInline]
save_on_top = True save_on_top = True
autocomplete_fields = ["parent"]
search_fields = ["parent"]
def _name(self, obj): 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): def save_model(self, request, obj, form, change):
super(ItemAdmin, self).save_model(request, obj, form, change) super(ItemAdmin, self).save_model(request, obj, form, change)
@ -67,10 +69,6 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
return data return data
class Media: class Media:
js = (
# Required by select2
"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js",
)
css = {"all": ("css/admin.css",)} css = {"all": ("css/admin.css",)}
def response_action(self, request, queryset): def response_action(self, request, queryset):

View File

@ -1,6 +1,7 @@
from rest_framework import viewsets, generics, filters from rest_framework import viewsets, generics, filters
from rest_framework.response import Response 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 rest_framework.permissions import AllowAny
from storage.models import Item, Label from storage.models import Item, Label
@ -32,7 +33,7 @@ class LabelViewSet(viewsets.ModelViewSet):
queryset = Label.objects queryset = Label.objects
serializer_class = LabelSerializer serializer_class = LabelSerializer
@detail_route(methods=["post"], permission_classes=[AllowAny]) @action(detail=True, methods=["post"], permission_classes=[AllowAny])
def print(self, request, pk): def print(self, request, pk):
quantity = min(int(request.query_params.get("quantity", 1)), 5) quantity = min(int(request.query_params.get("quantity", 1)), 5)
obj = self.get_object() obj = self.get_object()
@ -52,7 +53,7 @@ class ItemViewSet(viewsets.ModelViewSet):
ordering_fields = "__all__" ordering_fields = "__all__"
def get_queryset(self): def get_queryset(self):
return Item.get_roots() return Item.objects.filter_roots()
def get_object(self): def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
@ -73,7 +74,7 @@ class ItemViewSet(viewsets.ModelViewSet):
except Label.DoesNotExist: except Label.DoesNotExist:
raise Http404() raise Http404()
@detail_route(methods=["post"], permission_classes=[AllowAny]) @action(detail=True, methods=["post"], permission_classes=[AllowAny])
def print(self, request, pk): def print(self, request, pk):
# todo: deduplicate # todo: deduplicate
quantity = min(int(request.query_params.get("quantity", 1)), 5) quantity = min(int(request.query_params.get("quantity", 1)), 5)
@ -82,28 +83,36 @@ class ItemViewSet(viewsets.ModelViewSet):
obj.print() obj.print()
return obj return obj
@detail_route() @action(
detail=True,
)
def children(self, request, pk): def children(self, request, pk):
item = self.get_object() item = self.get_object()
return Response( return Response(
self.serializer_class(item.get_children().all(), many=True).data self.serializer_class(item.get_children().all(), many=True).data
) )
@detail_route() @action(
detail=True,
)
def ancestors(self, request, pk): def ancestors(self, request, pk):
item = self.get_object() item = self.get_object()
return Response( return Response(
self.serializer_class(item.get_ancestors().all(), many=True).data self.serializer_class(item.get_ancestors().all(), many=True).data
) )
@detail_route() @action(
detail=True,
)
def descendants(self, request, pk): def descendants(self, request, pk):
item = self.get_object() item = self.get_object()
return Response( return Response(
self.serializer_class(item.get_descendants().all(), many=True).data self.serializer_class(item.get_descendants().all(), many=True).data
) )
@detail_route() @action(
detail=True,
)
def siblings(self, request, pk): def siblings(self, request, pk):
item = self.get_object() item = self.get_object()
return Response( return Response(

View File

@ -3,7 +3,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import migrations, models 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): class Migration(migrations.Migration):
@ -12,6 +14,8 @@ class Migration(migrations.Migration):
dependencies = [] dependencies = []
operations = [ operations = [
HStoreExtension(),
TrigramExtension(),
migrations.CreateModel( migrations.CreateModel(
name="Item", name="Item",
fields=[ fields=[
@ -26,7 +30,7 @@ class Migration(migrations.Migration):
), ),
("name", models.TextField()), ("name", models.TextField()),
("description", models.TextField()), ("description", models.TextField()),
("props", django_hstore.fields.DictionaryField()), ("props", HStoreField()),
], ],
), ),
] ]

View File

@ -5,7 +5,11 @@ from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion 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 import uuid
@ -28,12 +32,14 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
HStoreExtension(),
TrigramExtension(),
migrations.CreateModel( migrations.CreateModel(
name="Item", name="Item",
fields=[ fields=[
("name", models.TextField()), ("name", models.TextField()),
("description", models.TextField(blank=True)), ("description", models.TextField(blank=True)),
("props", django_hstore.fields.DictionaryField()), ("props", HStoreField()),
( (
"uuid", "uuid",
models.UUIDField( models.UUIDField(

View File

@ -4,7 +4,7 @@ from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django_hstore.fields from django.contrib.postgres.fields import HStoreField
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -35,7 +35,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name="item", model_name="item",
name="props", name="props",
field=django_hstore.fields.DictionaryField(blank=True), field=HStoreField(blank=True),
), ),
migrations.AddField( migrations.AddField(
model_name="label", model_name="label",

View File

@ -12,6 +12,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
DeleteTreeTrigger("storage.Item"), DeleteTreeTrigger("storage.Item"),
CreateTreeTrigger("storage.Item", order_by=("name",)), CreateTreeTrigger("storage.Item"),
RebuildPaths("storage.Item"), RebuildPaths("storage.Item"),
] ]

View File

@ -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),
),
]

View File

@ -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(),
]

View File

@ -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"),
]

View File

@ -6,9 +6,10 @@ import re
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django_hstore import hstore
from tree.fields import PathField from tree.fields import PathField
from tree.models import TreeModelMixin from tree.models import TreeModelMixin
from django.contrib.postgres.fields import HStoreField
import requests import requests
@ -46,7 +47,7 @@ class Category(models.Model):
class Item(models.Model, TreeModelMixin): class Item(models.Model, TreeModelMixin):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 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() path = PathField()
name = models.TextField() name = models.TextField()
@ -54,17 +55,25 @@ class Item(models.Model, TreeModelMixin):
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
state = models.CharField(max_length=31, choices=STATES, default=STATES[0][0]) state = models.CharField(max_length=31, choices=STATES, default=STATES[0][0])
categories = models.ManyToManyField(Category, blank=True) 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( 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_on = models.DateTimeField(blank=True, null=True)
taken_until = models.DateTimeField(blank=True, null=True) taken_until = models.DateTimeField(blank=True, null=True)
props = hstore.DictionaryField(blank=True) props = HStoreField(blank=True)
objects = hstore.HStoreManager()
def short_id(self): def short_id(self):
# let's just hope we never have 4 294 967 296 things :) # 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): 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() image = models.ImageField()
def __str__(self): def __str__(self):
@ -114,7 +123,7 @@ class ItemImage(models.Model):
class Label(models.Model): class Label(models.Model):
id = models.CharField(max_length=64, primary_key=True) 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( style = models.CharField(
max_length=32, max_length=32,
choices=(("basic_99012_v1", "Basic Dymo 89x36mm label"),), choices=(("basic_99012_v1", "Basic Dymo 89x36mm label"),),

View File

@ -1,10 +1,9 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from storage.models import Item, Label, Category from storage.models import Item, Label, Category
from rest_framework import serializers from rest_framework import serializers
from rest_framework_hstore.serializers import HStoreSerializer
class ItemSerializer(HStoreSerializer): class ItemSerializer(serializers.HStoreField):
categories = serializers.SlugRelatedField( categories = serializers.SlugRelatedField(
queryset=Category.objects, many=True, slug_field="name" queryset=Category.objects, many=True, slug_field="name"
) )

View File

@ -1,33 +1,27 @@
{% extends "admin/change_form.html" %} {% extends "admin/change_form.html" %} {% block submit_buttons_top %} {# We want
add another to be default submit action #}
{% block submit_buttons_top %}
{# We want add another to be default submit action #}
<input type="submit" value="Save" class="hidden" name="_addanother" /> <input type="submit" value="Save" class="hidden" name="_addanother" />
{{ block.super }} {{ block.super }} {% endblock %} {% block submit_buttons_bottom %} {# We want
{% endblock %} add another to be default submit action #}
{% block submit_buttons_bottom %}
{# We want add another to be default submit action #}
<input type="submit" value="Save" class="hidden" name="_addanother" /> <input type="submit" value="Save" class="hidden" name="_addanother" />
{{ block.super }} {{ block.super }} {% endblock %} {% block content %}{{ block.super }}
{% endblock %}
{% block content %}{{ block.super }}
<script> <script>
$(function() { django.jQuery(function () {
function fmt(state) { function fmt(state) {
if (!state.id) { if (!state.id) {
return state.text; return state.text;
} }
var result = $('<div><div><small></small></div><b></b></div>'); var result = django.jQuery(
result.find('small').text(state.path.join(' → ')).css({ "<div><div><small></small></div><b></b></div>"
'opacity': 0.6, );
'letter-spacing': -0.5 result.find("small").text(state.path.join(" → ")).css({
}) opacity: 0.6,
result.find('b').text(state.text) "letter-spacing": -0.5,
});
result.find("b").text(state.text);
return result; return result;
}; }
$('.django-select2[name=parent]').djangoSelect2({ django.jQuery(".django-select2[name=parent]").djangoSelect2({
templateResult: fmt, templateResult: fmt,
}); });
}); });

View File

@ -59,7 +59,7 @@ def apply_smart_search(query, objects):
def index(request): 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): def search(request):

View File

@ -1,31 +1,39 @@
from pkg_resources import parse_version from pkg_resources import parse_version
from django_select2.forms import ModelSelect2Widget, HeavySelect2Widget from django_select2.forms import HeavySelect2Widget
from django_hstore.forms import DictionaryFieldWidget
from django import get_version from django import get_version
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django import forms
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.template import Context from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.contrib.admin.widgets import AdminTextareaWidget from django.contrib.admin.widgets import AdminTextareaWidget
from django_admin_hstore_widget.forms import HStoreFormWidget
class ItemSelectWidget(ModelSelect2Widget): from django.contrib.postgres.forms import forms
def __init__(self, *args, **kwargs): from django.templatetags.static import static
kwargs["data_view"] = "item-complete"
super(ItemSelectWidget, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return obj.name
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): def __init__(self, *args, **kwargs):
super().__init__(*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: if attrs is None:
attrs = {} attrs = {}
# it's called "original" because it will be replaced by a copy # it's called "original" because it will be replaced by a copy
@ -35,7 +43,7 @@ class PropsSelectWidget(DictionaryFieldWidget):
) )
# get default HTML from AdminTextareaWidget # get default HTML from AdminTextareaWidget
html = AdminTextareaWidget.render(self, name, value, attrs) html = AdminTextareaWidget.render(self, name, value, attrs, renderer)
# prepare template context # prepare template context
template_context = { template_context = {
"field_name": name, "field_name": name,
@ -46,7 +54,7 @@ class PropsSelectWidget(DictionaryFieldWidget):
"w": w.build_attrs(base_attrs=w.attrs), "w": w.build_attrs(base_attrs=w.attrs),
} }
# get template object # get template object
template = get_template("hstore_%s_widget.html" % self.admin_style) template = get_template("hstore_default_widget.html")
# render additional html # render additional html
additional_html = template.render(template_context) additional_html = template.render(template_context)

View File

@ -2,19 +2,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>{% if title %}{{ title }} - {% endif %}Hackerspace Storage</title> <title>{% if title %}{{ title }} - {% endif %}Hackerspace Storage</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" media="screen"> <link
<link rel="stylesheet" href="{% static 'css/custom.css' %}" media="screen"> rel="stylesheet"
href="{% static 'css/bootstrap.css' %}"
media="screen"
/>
<link
rel="stylesheet"
href="{% static 'css/custom.css' %}"
media="screen"
/>
</head> </head>
<body> <body>
{% block body %} {% block body %}
<nav class="navbar navbar-default"> <nav class="navbar navbar-default">
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <button
type="button"
class="navbar-toggle collapsed"
data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1"
>
<span class="sr-only">Toggle navigation</span> <span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
@ -26,14 +39,26 @@
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="/admin/storage/item/add/">Add thing</a></li> <li><a href="/admin/storage/item/add/">Add thing</a></li>
<li><a href="https://wiki.hackerspace.pl/members:services:inventory">How to use</a></li> <li>
<a href="https://wiki.hackerspace.pl/members:services:inventory"
>How to use</a
>
</li>
</ul> </ul>
<form class="navbar-form navbar-right" role="search" action="/search"> <form class="navbar-form navbar-right" role="search" action="/search">
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" placeholder="Search Hackerspace" name="q" autofocus> <input
type="text"
class="form-control"
placeholder="Search Hackerspace"
name="q"
autofocus
/>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-primary" type="submit"><i class="glyphicon glyphicon-search"></i></button> <button class="btn btn-primary" type="submit">
<i class="glyphicon glyphicon-search"></i>
</button>
</span> </span>
</div> </div>
</div> </div>
@ -44,11 +69,9 @@
<div class="container"> <div class="container">
<!--<h1 class="page-header">Warsaw Hackerspace <small class="hidden-sm <!--<h1 class="page-header">Warsaw Hackerspace <small class="hidden-sm
hidden-xs">Enjoy your stay</small></h1>--> hidden-xs">Enjoy your stay</small></h1>-->
{% block content %} {% block content %} {% endblock %}
{% endblock %}
</div> </div>
{% endblock %} {% endblock %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
</body> </body>
</html> </html>

View File

@ -66,5 +66,7 @@
</script> </script>
<script> <script>
window.addEventListener("load", function (event) {
django.jQuery(function() { initDjangoHStoreWidget('{{ field_name }}') }); django.jQuery(function() { initDjangoHStoreWidget('{{ field_name }}') });
});
</script> </script>