Blackify the code (autoformat)

pull/1/head
Dariusz Niemczyk 2023-07-11 15:34:35 +02:00
parent 3fdf788168
commit 659f04ce9c
No known key found for this signature in database
GPG Key ID: 28DFE7164F497CB6
21 changed files with 519 additions and 369 deletions

View File

@ -1,36 +1,35 @@
from social_core.backends.oauth import BaseOAuth2 from social_core.backends.oauth import BaseOAuth2
from six.moves.urllib_parse import urlencode, unquote from six.moves.urllib_parse import urlencode, unquote
class HSWawOAuth2(BaseOAuth2): class HSWawOAuth2(BaseOAuth2):
"""Hackerspace OAuth authentication backend""" """Hackerspace OAuth authentication backend"""
name = 'hswaw'
ID_KEY = 'username' name = "hswaw"
AUTHORIZATION_URL = 'https://sso.hackerspace.pl/oauth/authorize' ID_KEY = "username"
ACCESS_TOKEN_URL = 'https://sso.hackerspace.pl/oauth/token' AUTHORIZATION_URL = "https://sso.hackerspace.pl/oauth/authorize"
DEFAULT_SCOPE = ['profile:read'] ACCESS_TOKEN_URL = "https://sso.hackerspace.pl/oauth/token"
DEFAULT_SCOPE = ["profile:read"]
REDIRECT_STATE = False REDIRECT_STATE = False
SCOPE_SEPARATOR = ',' SCOPE_SEPARATOR = ","
EXTRA_DATA = [ EXTRA_DATA = [("expires", "expires_in")]
('expires', 'expires_in')
]
def get_user_details(self, response): def get_user_details(self, response):
"""Return user details from Hackerspace account""" """Return user details from Hackerspace account"""
personal_email = None personal_email = None
if response.get('personal_email'): if response.get("personal_email"):
personal_email = response.get('personal_email')[0] personal_email = response.get("personal_email")[0]
return {'username': response.get('username'), return {
'email': response.get('email'), "username": response.get("username"),
'personal_email': personal_email, "email": response.get("email"),
} "personal_email": personal_email,
}
def user_data(self, access_token, *args, **kwargs): def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service""" """Loads user data from service"""
url = 'https://sso.hackerspace.pl/api/1/profile' url = "https://sso.hackerspace.pl/api/1/profile"
headers = { headers = {"Authorization": "Bearer {}".format(access_token)}
'Authorization': 'Bearer {}'.format(access_token)
}
return self.get_json(url, headers=headers) return self.get_json(url, headers=headers)
def auth_url(self): def auth_url(self):
@ -40,4 +39,4 @@ class HSWawOAuth2(BaseOAuth2):
params.update(self.get_scope_argument()) params.update(self.get_scope_argument())
params.update(self.auth_extra_arguments()) params.update(self.auth_extra_arguments())
params = urlencode(params) params = urlencode(params)
return '{0}?{1}'.format(self.authorization_url(), params) return "{0}?{1}".format(self.authorization_url(), params)

View File

@ -5,13 +5,19 @@ from django.contrib.auth.models import Group
def staff_me_up(backend, details, response, uid, user, *args, **kwargs): def staff_me_up(backend, details, response, uid, user, *args, **kwargs):
user.is_staff = True user.is_staff = True
try: try:
user.groups.set([Group.objects.get(name='member')]) user.groups.set([Group.objects.get(name="member")])
except Group.DoesNotExist: except Group.DoesNotExist:
pass pass
user.save() user.save()
def associate_by_personal_email(backend, details, user=None, *args, **kwargs): def associate_by_personal_email(backend, details, user=None, *args, **kwargs):
return associate_by_email(backend, { return associate_by_email(
'email': details.get('personal_email'), backend,
}, user, *args, **kwargs) {
"email": details.get("personal_email"),
},
user,
*args,
**kwargs
)

View File

@ -1,4 +1,5 @@
from django.shortcuts import redirect from django.shortcuts import redirect
def auth_redirect(request): def auth_redirect(request):
return redirect('social:begin', 'hswaw') return redirect("social:begin", "hswaw")

View File

@ -12,97 +12,97 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os import os
def env(name, default=None): def env(name, default=None):
return os.getenv('SPEJSTORE_' + name, default) return os.getenv("SPEJSTORE_" + name, default)
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_ROOT = os.path.join(BASE_DIR, 'build_static') STATIC_ROOT = os.path.join(BASE_DIR, "build_static")
PROD = os.getenv('SPEJSTORE_ENV') == 'prod' PROD = os.getenv("SPEJSTORE_ENV") == "prod"
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', '#hjthi7_udsyt*9eeyb&nwgw5x=%pk_lnz3+u2tg9@=w3p1m*k') SECRET_KEY = env("SECRET_KEY", "#hjthi7_udsyt*9eeyb&nwgw5x=%pk_lnz3+u2tg9@=w3p1m*k")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = not PROD DEBUG = not PROD
ALLOWED_HOSTS = env('ALLOWED_HOSTS', 'devinventory,inventory.waw.hackerspace.pl,i,inventory').split(',') ALLOWED_HOSTS = env(
LOGIN_REDIRECT_URL = '/admin/' "ALLOWED_HOSTS", "devinventory,inventory.waw.hackerspace.pl,i,inventory"
).split(",")
LOGIN_REDIRECT_URL = "/admin/"
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'flat_responsive', "flat_responsive",
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'django.contrib.postgres', "django.contrib.postgres",
"social_django",
'social_django', "django_hstore",
"tree",
'django_hstore', "django_select2",
'tree', "rest_framework",
'django_select2', "rest_framework.authtoken",
'rest_framework', "django_markdown2",
'rest_framework.authtoken', "storage",
'django_markdown2',
'storage',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'social_django.middleware.SocialAuthExceptionMiddleware', "social_django.middleware.SocialAuthExceptionMiddleware",
] ]
ROOT_URLCONF = 'spejstore.urls' ROOT_URLCONF = "spejstore.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': ['templates/'], "DIRS": ["templates/"],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
'social_django.context_processors.backends', "social_django.context_processors.backends",
'social_django.context_processors.login_redirect', "social_django.context_processors.login_redirect",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'spejstore.wsgi.application' WSGI_APPLICATION = "spejstore.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': env('DB_ENGINE', 'django.db.backends.postgresql_psycopg2'), "ENGINE": env("DB_ENGINE", "django.db.backends.postgresql_psycopg2"),
'NAME': env('DB_NAME', 'postgres'), "NAME": env("DB_NAME", "postgres"),
'USER': env('DB_USER', 'postgres'), "USER": env("DB_USER", "postgres"),
'PASSWORD': env('DB_PASSWORD', None), "PASSWORD": env("DB_PASSWORD", None),
'HOST': env('DB_HOST', 'db'), "HOST": env("DB_HOST", "db"),
'PORT': env('DB_PORT', 5432), "PORT": env("DB_PORT", 5432),
} }
} }
@ -112,49 +112,49 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
}, },
] ]
# select2 # select2
SELECT2_JS = 'js/select2.min.js' SELECT2_JS = "js/select2.min.js"
SELECT2_CSS = 'css/select2.min.css' SELECT2_CSS = "css/select2.min.css"
SELECT2_I18N_PATH = '' SELECT2_I18N_PATH = ""
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'auth.backend.HSWawOAuth2', "auth.backend.HSWawOAuth2",
'django.contrib.auth.backends.ModelBackend', "django.contrib.auth.backends.ModelBackend",
) )
SOCIAL_AUTH_PIPELINE = ( SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details', "social_core.pipeline.social_auth.social_details",
'social_core.pipeline.social_auth.social_uid', "social_core.pipeline.social_auth.social_uid",
'social_core.pipeline.social_auth.social_user', "social_core.pipeline.social_auth.social_user",
'social_core.pipeline.user.get_username', "social_core.pipeline.user.get_username",
'social_core.pipeline.social_auth.associate_by_email', "social_core.pipeline.social_auth.associate_by_email",
'auth.pipeline.associate_by_personal_email', "auth.pipeline.associate_by_personal_email",
'social_core.pipeline.user.create_user', "social_core.pipeline.user.create_user",
'social_core.pipeline.social_auth.associate_user', "social_core.pipeline.social_auth.associate_user",
'social_core.pipeline.social_auth.load_extra_data', "social_core.pipeline.social_auth.load_extra_data",
'social_core.pipeline.user.user_details', "social_core.pipeline.user.user_details",
'auth.pipeline.staff_me_up', "auth.pipeline.staff_me_up",
) )
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
@ -162,32 +162,32 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"), os.path.join(BASE_DIR, "static"),
] ]
MEDIA_URL = '/media/' MEDIA_URL = "/media/"
MEDIA_ROOT = env('MEDIA_ROOT', os.path.join(BASE_DIR, "media")) MEDIA_ROOT = env("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
# REST Framework # REST Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [ "DEFAULT_PERMISSION_CLASSES": [
'rest_framework.permissions.IsAuthenticatedOrReadOnly', "rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
], ],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
]
} }
SOCIAL_AUTH_HSWAW_KEY = env('CLIENT_ID') SOCIAL_AUTH_HSWAW_KEY = env("CLIENT_ID")
SOCIAL_AUTH_HSWAW_SECRET = env('SECRET') SOCIAL_AUTH_HSWAW_SECRET = env("SECRET")
SOCIAL_AUTH_REDIRECT_IS_HTTPS = PROD SOCIAL_AUTH_REDIRECT_IS_HTTPS = PROD
SOCIAL_AUTH_POSTGRES_JSONFIELD = True SOCIAL_AUTH_POSTGRES_JSONFIELD = True
LABEL_API = env('LABEL_API', 'http://label.waw.hackerspace.pl:4567') LABEL_API = env("LABEL_API", "http://label.waw.hackerspace.pl:4567")

View File

@ -16,19 +16,25 @@ from auth.views import auth_redirect
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'items', apiviews.ItemViewSet) router.register(r"items", apiviews.ItemViewSet)
router.register(r'labels', apiviews.LabelViewSet) router.register(r"labels", apiviews.LabelViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = ([ urlpatterns = (
url(r'^admin/login/.*', auth_redirect), (
] if settings.PROD else []) + [ [
url(r'^admin/', admin.site.urls), url(r"^admin/login/.*", auth_redirect),
url(r'^select2/', include('django_select2.urls')), ]
if settings.PROD
url(r'^', include('storage.urls')), else []
url(r'^api/1/', include(router.urls)), )
] \ + [
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ url(r"^admin/", admin.site.urls),
url(r"^select2/", include("django_select2.urls")),
url(r"^", include("storage.urls")),
url(r"^api/1/", include(router.urls)),
]
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
)

View File

@ -7,8 +7,8 @@ from django_select2.forms import ModelSelect2Widget, Select2MultipleWidget
from .models import Item, ItemImage, Category, Label from .models import Item, ItemImage, Category, Label
from .widgets import ItemSelectWidget, PropsSelectWidget from .widgets import ItemSelectWidget, 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()
@ -24,10 +24,10 @@ class ItemForm(forms.ModelForm):
model = Item model = Item
exclude = [] exclude = []
widgets = { widgets = {
'parent': ItemSelectWidget(model=Item), "parent": ItemSelectWidget(model=Item),
'categories': Select2MultipleWidget, "categories": Select2MultipleWidget,
'props': PropsSelectWidget "props": PropsSelectWidget,
} }
class ItemImageInline(ModelAdminMixin, admin.TabularInline): class ItemImageInline(ModelAdminMixin, admin.TabularInline):
@ -39,30 +39,29 @@ class LabelInline(ModelAdminMixin, admin.TabularInline):
model = Label model = Label
class ItemAdmin(ModelAdminMixin, admin.ModelAdmin): class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
list_display = ('_name',) list_display = ("_name",)
list_filter = ('categories',) list_filter = ("categories",)
form = ItemForm form = ItemForm
inlines = [ItemImageInline, LabelInline] inlines = [ItemImageInline, LabelInline]
save_on_top = True save_on_top = True
def _name(self, obj): def _name(self, obj):
return '-' * obj.get_level() + '> ' + obj.name return "-" * obj.get_level() + "> " + 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)
# Store last input parent to use as default on next creation # Store last input parent to use as default on next creation
if obj.parent: if obj.parent:
request.session['last-parent'] = str(obj.parent.uuid) request.session["last-parent"] = str(obj.parent.uuid)
else: else:
request.session['last-parent'] = str(obj.uuid) request.session["last-parent"] = str(obj.uuid)
def get_changeform_initial_data(self, request): def get_changeform_initial_data(self, request):
data = { data = {
'parent': request.GET.get('parent') or request.session.get('last-parent') "parent": request.GET.get("parent") or request.session.get("last-parent")
} }
data.update(super(ItemAdmin, self).get_changeform_initial_data(request)) data.update(super(ItemAdmin, self).get_changeform_initial_data(request))
return data return data
@ -70,11 +69,9 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
class Media: class Media:
js = ( js = (
# Required by select2 # Required by select2
'https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', "https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js",
) )
css = { css = {"all": ("css/admin.css",)}
'all': ('css/admin.css',)
}
def response_action(self, request, queryset): def response_action(self, request, queryset):
with Item.disabled_tree_trigger(): with Item.disabled_tree_trigger():
@ -85,8 +82,8 @@ class NormalModelAdmin(ModelAdminMixin, admin.ModelAdmin):
pass pass
admin.site.site_title = 'Hackerspace Storage Admin' admin.site.site_title = "Hackerspace Storage Admin"
admin.site.site_header = 'Hackerspace Storage Admin' admin.site.site_header = "Hackerspace Storage Admin"
admin.site.register(Item, ItemAdmin) admin.site.register(Item, ItemAdmin)
admin.site.register(Category, NormalModelAdmin) admin.site.register(Category, NormalModelAdmin)
@ -94,13 +91,14 @@ admin.site.register(Category, NormalModelAdmin)
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
User.add_to_class('get_short_name', User.get_username) User.add_to_class("get_short_name", User.get_username)
User.add_to_class('get_full_name', User.get_username) User.add_to_class("get_full_name", User.get_username)
admin.site.unregister(User) admin.site.unregister(User)
admin.site.unregister(Group) admin.site.unregister(Group)
from social_django.admin import UserSocialAuth, Nonce, Association from social_django.admin import UserSocialAuth, Nonce, Association
admin.site.unregister(UserSocialAuth) admin.site.unregister(UserSocialAuth)
admin.site.unregister(Nonce) admin.site.unregister(Nonce)
admin.site.unregister(Association) admin.site.unregister(Association)

View File

@ -17,7 +17,7 @@ class SmartSearchFilterBackend(filters.BaseFilterBackend):
""" """
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
search_query = request.query_params.get('smartsearch', None) search_query = request.query_params.get("smartsearch", None)
if search_query: if search_query:
return apply_smart_search(search_query, queryset) return apply_smart_search(search_query, queryset)
@ -28,12 +28,13 @@ class LabelViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows items to be viewed or edited. API endpoint that allows items to be viewed or edited.
""" """
queryset = Label.objects queryset = Label.objects
serializer_class = LabelSerializer serializer_class = LabelSerializer
@detail_route(methods=['post'], permission_classes=[AllowAny]) @detail_route(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()
for _ in range(quantity): for _ in range(quantity):
obj.print() obj.print()
@ -44,11 +45,11 @@ class ItemViewSet(viewsets.ModelViewSet):
""" """
API endpoint that allows items to be viewed or edited. API endpoint that allows items to be viewed or edited.
""" """
queryset = Item.objects queryset = Item.objects
serializer_class = ItemSerializer serializer_class = ItemSerializer
filter_backends = (SmartSearchFilterBackend, filters.OrderingFilter) filter_backends = (SmartSearchFilterBackend, filters.OrderingFilter)
ordering_fields = '__all__' ordering_fields = "__all__"
def get_queryset(self): def get_queryset(self):
return Item.get_roots() return Item.get_roots()
@ -63,7 +64,7 @@ class ItemViewSet(viewsets.ModelViewSet):
def get_item_by_id_or_label(self, id): def get_item_by_id_or_label(self, id):
try: try:
item = Item.objects.get(uuid__startswith=id) # look up by short id item = Item.objects.get(uuid__startswith=id) # look up by short id
return item return item
except Item.DoesNotExist: except Item.DoesNotExist:
try: try:
@ -72,10 +73,10 @@ class ItemViewSet(viewsets.ModelViewSet):
except Label.DoesNotExist: except Label.DoesNotExist:
raise Http404() raise Http404()
@detail_route(methods=['post'], permission_classes=[AllowAny]) @detail_route(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)
obj = self.get_object() obj = self.get_object()
for _ in range(quantity): for _ in range(quantity):
obj.print() obj.print()
@ -84,19 +85,27 @@ class ItemViewSet(viewsets.ModelViewSet):
@detail_route() @detail_route()
def children(self, request, pk): def children(self, request, pk):
item = self.get_object() item = self.get_object()
return Response(self.serializer_class(item.get_children().all(), many=True).data) return Response(
self.serializer_class(item.get_children().all(), many=True).data
)
@detail_route() @detail_route()
def ancestors(self, request, pk): def ancestors(self, request, pk):
item = self.get_object() item = self.get_object()
return Response(self.serializer_class(item.get_ancestors().all(), many=True).data) return Response(
self.serializer_class(item.get_ancestors().all(), many=True).data
)
@detail_route() @detail_route()
def descendants(self, request, pk): def descendants(self, request, pk):
item = self.get_object() item = self.get_object()
return Response(self.serializer_class(item.get_descendants().all(), many=True).data) return Response(
self.serializer_class(item.get_descendants().all(), many=True).data
)
@detail_route() @detail_route()
def siblings(self, request, pk): def siblings(self, request, pk):
item = self.get_object() item = self.get_object()
return Response(self.serializer_class(item.get_siblings().all(), many=True).data) return Response(
self.serializer_class(item.get_siblings().all(), many=True).data
)

View File

@ -4,4 +4,4 @@ from django.apps import AppConfig
class StorageConfig(AppConfig): class StorageConfig(AppConfig):
name = 'storage' name = "storage"

View File

@ -3,28 +3,29 @@ from storage.models import Item
from io import StringIO from io import StringIO
import csv import csv
class Command(BaseCommand): class Command(BaseCommand):
help = 'Imports book library from specified wiki page dump' help = "Imports book library from specified wiki page dump"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('parent') parser.add_argument("parent")
parser.add_argument('file') parser.add_argument("file")
def handle(self, *args, **options): def handle(self, *args, **options):
with open(options['file']) as fd: with open(options["file"]) as fd:
sio = StringIO(fd.read()) sio = StringIO(fd.read())
reader = csv.reader(sio, delimiter='|') reader = csv.reader(sio, delimiter="|")
parent = Item.objects.get(pk=options['parent']) parent = Item.objects.get(pk=options["parent"])
for line in reader: for line in reader:
line = list(map(str.strip, line)) line = list(map(str.strip, line))
item = Item(parent=parent) item = Item(parent=parent)
item.name = line[2] item.name = line[2]
item.props['author'] = line[1] item.props["author"] = line[1]
item.props['owner'] = line[3] item.props["owner"] = line[3]
item.props['can_borrow'] = line[4] item.props["can_borrow"] = line[4]
item.props['borrowed_by'] = line[5] item.props["borrowed_by"] = line[5]
item.save() item.save()
self.stdout.write(self.style.NOTICE('Book added: %r') % item) self.stdout.write(self.style.NOTICE("Book added: %r") % item)
self.stdout.write(self.style.SUCCESS('Successfully imported data')) self.stdout.write(self.style.SUCCESS("Successfully imported data"))

View File

@ -7,20 +7,26 @@ import django_hstore.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Item', name="Item",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.TextField()), "id",
('description', models.TextField()), models.AutoField(
('props', django_hstore.fields.DictionaryField()), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField()),
("description", models.TextField()),
("props", django_hstore.fields.DictionaryField()),
], ],
), ),
] ]

View File

@ -10,8 +10,16 @@ import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
replaces = [
replaces = [('storage', '0001_initial'), ('storage', '0002_auto_20160929_2125'), ('storage', '0003_auto_20160929_2134'), ('storage', '0004_auto_20160929_2143'), ('storage', '0005_auto_20160929_2151'), ('storage', '0006_auto_20160929_2153'), ('storage', '0007_auto_20160929_2153'), ('storage', '0008_item_state')] ("storage", "0001_initial"),
("storage", "0002_auto_20160929_2125"),
("storage", "0003_auto_20160929_2134"),
("storage", "0004_auto_20160929_2143"),
("storage", "0005_auto_20160929_2151"),
("storage", "0006_auto_20160929_2153"),
("storage", "0007_auto_20160929_2153"),
("storage", "0008_item_state"),
]
initial = True initial = True
@ -21,62 +29,114 @@ class Migration(migrations.Migration):
operations = [ operations = [
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", django_hstore.fields.DictionaryField()),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), (
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='ItemImage', name="ItemImage",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('image', models.ImageField(upload_to='')), "id",
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='storage.Item')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("image", models.ImageField(upload_to="")),
(
"item",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="images",
to="storage.Item",
),
),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Category', name="Category",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=127)), "id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=127)),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='categories', name="categories",
field=models.ManyToManyField(to='storage.Category'), field=models.ManyToManyField(to="storage.Category"),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='owner', name="owner",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_items', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="owned_items",
to=settings.AUTH_USER_MODEL,
),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='taken_by', name="taken_by",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='taken_items', to=settings.AUTH_USER_MODEL), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="taken_items",
to=settings.AUTH_USER_MODEL,
),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='taken_on', name="taken_on",
field=models.DateTimeField(blank=True, null=True), field=models.DateTimeField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='taken_until', name="taken_until",
field=models.DateTimeField(blank=True, null=True), field=models.DateTimeField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='item', model_name="item",
name='description', name="description",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='state', name="state",
field=models.CharField(choices=[('present', 'Present'), ('taken', 'Taken'), ('broken', 'Broken'), ('missing', 'Missing')], default='present', max_length=31), field=models.CharField(
choices=[
("present", "Present"),
("taken", "Taken"),
("broken", "Broken"),
("missing", "Missing"),
],
default="present",
max_length=31,
),
), ),
] ]

View File

@ -9,22 +9,26 @@ from tree.operations import CreateTreeTrigger
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('storage', '0001_squashed_0008_item_state'), ("storage", "0001_squashed_0008_item_state"),
('tree', '0001_initial'), ("tree", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='parent', name="parent",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='storage.Item'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="storage.Item",
),
), ),
migrations.AddField( migrations.AddField(
model_name='item', model_name="item",
name='path', name="path",
field=tree.fields.PathField(), field=tree.fields.PathField(),
), ),
CreateTreeTrigger('storage.Item'), CreateTreeTrigger("storage.Item"),
] ]

View File

@ -8,36 +8,42 @@ import django_hstore.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('storage', '0002_auto_20170215_0115'), ("storage", "0002_auto_20170215_0115"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Label', name="Label",
fields=[ fields=[
('id', models.CharField(max_length=64, primary_key=True, serialize=False)), (
('revision', models.IntegerField()), "id",
models.CharField(max_length=64, primary_key=True, serialize=False),
),
("revision", models.IntegerField()),
], ],
), ),
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='item', name="item",
options={'ordering': ('path',)}, options={"ordering": ("path",)},
), ),
migrations.AlterField( migrations.AlterField(
model_name='item', model_name="item",
name='categories', name="categories",
field=models.ManyToManyField(blank=True, to='storage.Category'), field=models.ManyToManyField(blank=True, to="storage.Category"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='item', model_name="item",
name='props', name="props",
field=django_hstore.fields.DictionaryField(blank=True), field=django_hstore.fields.DictionaryField(blank=True),
), ),
migrations.AddField( migrations.AddField(
model_name='label', model_name="label",
name='item', name="item",
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='labels', to='storage.Item'), field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="labels",
to="storage.Item",
),
), ),
] ]

View File

@ -7,25 +7,30 @@ import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('storage', '0003_auto_20170424_2002'), ("storage", "0003_auto_20170424_2002"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(
model_name='label', model_name="label",
name='revision', name="revision",
), ),
migrations.AddField( migrations.AddField(
model_name='label', model_name="label",
name='created', name="created",
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='label', model_name="label",
name='style', name="style",
field=models.CharField(choices=[('basic_99012_v1', 'Basic Dymo 89x36mm label')], default='basic_99012_v1', max_length=32), field=models.CharField(
choices=[("basic_99012_v1", "Basic Dymo 89x36mm label")],
default="basic_99012_v1",
max_length=32,
),
), ),
] ]

View File

@ -4,14 +4,14 @@ from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
from tree.operations import CreateTreeTrigger, DeleteTreeTrigger, RebuildPaths from tree.operations import CreateTreeTrigger, DeleteTreeTrigger, RebuildPaths
class Migration(migrations.Migration):
class Migration(migrations.Migration):
dependencies = [ dependencies = [
('storage', '0004_auto_20170528_1945'), ("storage", "0004_auto_20170528_1945"),
] ]
operations = [ operations = [
DeleteTreeTrigger('storage.Item'), DeleteTreeTrigger("storage.Item"),
CreateTreeTrigger('storage.Item', order_by=('name',)), CreateTreeTrigger("storage.Item", order_by=("name",)),
RebuildPaths('storage.Item') RebuildPaths("storage.Item"),
] ]

View File

@ -6,15 +6,14 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('storage', '0005'), ("storage", "0005"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='category', model_name="category",
name='icon_id', name="icon_id",
field=models.CharField(blank=True, max_length=64, null=True), field=models.CharField(blank=True, max_length=64, null=True),
), ),
] ]

View File

@ -14,11 +14,11 @@ import requests
STATES = ( STATES = (
('present', 'Present'), ("present", "Present"),
('taken', 'Taken'), ("taken", "Taken"),
('broken', 'Broken'), ("broken", "Broken"),
('missing', 'Missing'), ("missing", "Missing"),
('depleted', 'Depleted'), ("depleted", "Depleted"),
) )
@ -31,7 +31,7 @@ class Category(models.Model):
return self.name return self.name
class Meta: class Meta:
ordering = ['name'] ordering = ["name"]
verbose_name_plural = "categories" verbose_name_plural = "categories"
@ -42,10 +42,11 @@ class Category(models.Model):
# also qrcody w stylu //s/ID (żeby się resolvowało w sieci lokalnej) # also qrcody w stylu //s/ID (żeby się resolvowało w sieci lokalnej)
# Also ID zawierające część name # Also ID zawierające część name
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)
path = PathField() path = PathField()
name = models.TextField() name = models.TextField()
@ -53,9 +54,11 @@ 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")
taken_by = models.ForeignKey(User, null=True, blank=True, related_name='taken_items') taken_by = models.ForeignKey(
User, null=True, blank=True, related_name="taken_items"
)
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)
@ -65,19 +68,20 @@ class Item(models.Model, TreeModelMixin):
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 :)
return str(self.pk)[:8] # collisions? what collisions? return str(self.pk)[:8] # collisions? what collisions?
def __str__(self): def __str__(self):
return '- ' * (self.get_level() or 0) + self.name return "- " * (self.get_level() or 0) + self.name
def get_absolute_url(self): def get_absolute_url(self):
from django.urls import reverse from django.urls import reverse
return reverse('item-display', kwargs={'pk': str(self.pk)})
return reverse("item-display", kwargs={"pk": str(self.pk)})
def get_or_create_label(self, **kwargs): def get_or_create_label(self, **kwargs):
defaults = { defaults = {
'id': re.sub('[^A-Z0-9]', '', self.name.upper())[:16], "id": re.sub("[^A-Z0-9]", "", self.name.upper())[:16],
} }
defaults.update(kwargs) defaults.update(kwargs)
@ -92,33 +96,35 @@ class Item(models.Model, TreeModelMixin):
def print(self): def print(self):
# todo: deduplicate # todo: deduplicate
resp = requests.post( resp = requests.post(
'{}/api/1/print/{}'.format(settings.LABEL_API, self.short_id())) "{}/api/1/print/{}".format(settings.LABEL_API, self.short_id())
)
resp.raise_for_status() resp.raise_for_status()
class Meta: class Meta:
ordering = ('path',) ordering = ("path",)
class ItemImage(models.Model): class ItemImage(models.Model):
item = models.ForeignKey(Item, related_name='images') item = models.ForeignKey(Item, related_name="images")
image = models.ImageField() image = models.ImageField()
def __str__(self): def __str__(self):
return '{}'.format(self.image.name) return "{}".format(self.image.name)
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")
style = models.CharField(max_length=32, choices=( style = models.CharField(
('basic_99012_v1', 'Basic Dymo 89x36mm label'), max_length=32,
), default='basic_99012_v1') choices=(("basic_99012_v1", "Basic Dymo 89x36mm label"),),
default="basic_99012_v1",
)
created = models.DateTimeField(auto_now_add=True, blank=True) created = models.DateTimeField(auto_now_add=True, blank=True)
def __str__(self): def __str__(self):
return '{}'.format(self.id) return "{}".format(self.id)
def print(self): def print(self):
resp = requests.post( resp = requests.post("{}/api/1/print/{}".format(settings.LABEL_API, self.id))
'{}/api/1/print/{}'.format(settings.LABEL_API, self.id))
resp.raise_for_status() resp.raise_for_status()

View File

@ -5,16 +5,37 @@ from rest_framework_hstore.serializers import HStoreSerializer
class ItemSerializer(HStoreSerializer): class ItemSerializer(HStoreSerializer):
categories = serializers.SlugRelatedField(queryset=Category.objects, many=True, slug_field='name') categories = serializers.SlugRelatedField(
owner = serializers.SlugRelatedField(queryset=User.objects, slug_field='username') queryset=Category.objects, many=True, slug_field="name"
taken_by = serializers.SlugRelatedField(queryset=User.objects, slug_field='username') )
owner = serializers.SlugRelatedField(queryset=User.objects, slug_field="username")
taken_by = serializers.SlugRelatedField(
queryset=User.objects, slug_field="username"
)
class Meta: class Meta:
model = Item model = Item
fields = ('uuid', 'short_id', 'name', 'description', 'props', 'state', 'parent', 'labels', 'owner', 'taken_by', 'taken_on', 'taken_until', 'categories') fields = (
"uuid",
"short_id",
"name",
"description",
"props",
"state",
"parent",
"labels",
"owner",
"taken_by",
"taken_on",
"taken_until",
"categories",
)
class LabelSerializer(serializers.ModelSerializer): class LabelSerializer(serializers.ModelSerializer):
item = ItemSerializer(required=False) item = ItemSerializer(required=False)
item_id = serializers.PrimaryKeyRelatedField(queryset=Item.objects, source='item') item_id = serializers.PrimaryKeyRelatedField(queryset=Item.objects, source="item")
class Meta: class Meta:
model = Label model = Label
fields = ('id', 'item', 'item_id', 'style') fields = ("id", "item", "item_id", "style")

View File

@ -1,16 +1,21 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from storage.views import ( from storage.views import (
index, search, item_display, label_lookup, apitoken, ItemSelectView, index,
PropSelectView search,
item_display,
label_lookup,
apitoken,
ItemSelectView,
PropSelectView,
) )
urlpatterns = [ urlpatterns = [
url(r'^$', index), url(r"^$", index),
url(r'^search$', search), url(r"^search$", search),
url(r'^apitoken$', apitoken), url(r"^apitoken$", apitoken),
url(r'^item/(?P<pk>.*)$', item_display, name='item-display'), url(r"^item/(?P<pk>.*)$", item_display, name="item-display"),
url(r'^autocomplete.json$', ItemSelectView.as_view(), name='item-complete'), url(r"^autocomplete.json$", ItemSelectView.as_view(), name="item-complete"),
url(r'^autocomplete_prop.json$', PropSelectView.as_view(), name='prop-complete'), url(r"^autocomplete_prop.json$", PropSelectView.as_view(), name="prop-complete"),
url(r'^(?P<pk>[^/]*)$', label_lookup, name='label-lookup'), url(r"^(?P<pk>[^/]*)$", label_lookup, name="label-lookup"),
url('', include('social_django.urls', namespace='social')), url("", include("social_django.urls", namespace="social")),
] ]

View File

@ -13,29 +13,30 @@ from storage.models import Item, Label
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
def apply_smart_search(query, objects): def apply_smart_search(query, objects):
general_term = [] general_term = []
filters = {} filters = {}
for prop in shlex.split(query): for prop in shlex.split(query):
if ':' not in prop: if ":" not in prop:
general_term.append(prop) general_term.append(prop)
else: else:
key, value = prop.split(':', 1) key, value = prop.split(":", 1)
if key in ['owner', 'taken_by']: if key in ["owner", "taken_by"]:
filters[key + '__username'] = value filters[key + "__username"] = value
elif hasattr(Item, key): elif hasattr(Item, key):
filters[key + '__search'] = value filters[key + "__search"] = value
elif key == 'ancestor': elif key == "ancestor":
objects = Item.objects.get(pk=value).get_children() objects = Item.objects.get(pk=value).get_children()
elif key == 'prop' or value: elif key == "prop" or value:
if key == 'prop': if key == "prop":
key, _, value = value.partition(':') key, _, value = value.partition(":")
if not value: if not value:
filters['props__isnull'] = {key: False} filters["props__isnull"] = {key: False}
else: else:
filters['props__contains'] = {key: value} filters["props__contains"] = {key: value}
else: else:
# "Whatever:" # "Whatever:"
general_term.append(prop) general_term.append(prop)
@ -44,35 +45,39 @@ def apply_smart_search(query, objects):
if not general_term: if not general_term:
return objects return objects
general_term = ' '.join(general_term) general_term = " ".join(general_term)
objects = objects.annotate( objects = (
search=SearchVector('name', 'description', 'props', config='simple'), objects.annotate(
similarity=TrigramSimilarity('name', general_term) search=SearchVector("name", "description", "props", config="simple"),
).filter( similarity=TrigramSimilarity("name", general_term),
Q(similarity__gte=0.15) | Q(search__contains=general_term) )
).order_by('-similarity') .filter(Q(similarity__gte=0.15) | Q(search__contains=general_term))
.order_by("-similarity")
)
return objects return objects
def index(request): def index(request):
return render(request, 'results.html', { return render(request, "results.html", {"results": Item.get_roots()})
'results': Item.get_roots()
})
def search(request): def search(request):
query = request.GET.get('q', '') query = request.GET.get("q", "")
results = apply_smart_search(query, Item.objects).all() results = apply_smart_search(query, Item.objects).all()
if results and (len(results) == 1 or getattr(results[0], 'similarity', 0) == 1): if results and (len(results) == 1 or getattr(results[0], "similarity", 0) == 1):
return redirect(results[0]) return redirect(results[0])
return render(request, 'results.html', { return render(
'query': query, request,
'results': results, "results.html",
}) {
"query": query,
"results": results,
},
)
def item_display(request, pk): def item_display(request, pk):
@ -83,18 +88,22 @@ def item_display(request, pk):
labels = item.labels.all() labels = item.labels.all()
has_one_label = len(labels) == 1 has_one_label = len(labels) == 1
return render(request, 'item.html', { return render(
'title': item.name, request,
'item': item, "item.html",
'categories': item.categories.all(), {
'props': sorted(item.props.items()), "title": item.name,
'images': item.images.all(), "item": item,
'labels': labels, "categories": item.categories.all(),
'has_one_label': has_one_label, "props": sorted(item.props.items()),
'history': LogEntry.objects.filter(object_id=item.pk), "images": item.images.all(),
'ancestors': item.get_ancestors(), "labels": labels,
'children': item.get_children().prefetch_related('categories'), "has_one_label": has_one_label,
}) "history": LogEntry.objects.filter(object_id=item.pk),
"ancestors": item.get_ancestors(),
"children": item.get_children().prefetch_related("categories"),
},
)
def label_lookup(request, pk): def label_lookup(request, pk):
@ -114,49 +123,56 @@ def label_lookup(request, pk):
def apitoken(request): def apitoken(request):
print(Token) print(Token)
token, created = Token.objects.get_or_create(user=request.user) token, created = Token.objects.get_or_create(user=request.user)
return HttpResponse(token.key, content_type='text/plain') return HttpResponse(token.key, content_type="text/plain")
class ItemSelectView(AutoResponseView): class ItemSelectView(AutoResponseView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.widget = self.get_widget_or_404() self.widget = self.get_widget_or_404()
self.term = kwargs.get('term', request.GET.get('term', '')) self.term = kwargs.get("term", request.GET.get("term", ""))
self.object_list = apply_smart_search(self.term, Item.objects) self.object_list = apply_smart_search(self.term, Item.objects)
context = self.get_context_data() context = self.get_context_data()
return JsonResponse({ return JsonResponse(
'results': [ {
{ "results": [
'text': obj.name, {
'path': [o.name for o in obj.get_ancestors()], "text": obj.name,
'id': obj.pk, "path": [o.name for o in obj.get_ancestors()],
} "id": obj.pk,
for obj in context['object_list'] }
for obj in context["object_list"]
], ],
'more': context['page_obj'].has_next() "more": context["page_obj"].has_next(),
}) }
)
class PropSelectView(AutoResponseView): class PropSelectView(AutoResponseView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# self.widget = self.get_widget_or_404() # self.widget = self.get_widget_or_404()
self.term = kwargs.get('term', request.GET.get('term', '')) self.term = kwargs.get("term", request.GET.get("term", ""))
# context = self.get_context_data() # context = self.get_context_data()
with connection.cursor() as c: with connection.cursor() as c:
c.execute(""" c.execute(
"""
SELECT key, count(*) FROM SELECT key, count(*) FROM
(SELECT (each(props)).key FROM storage_item) AS stat (SELECT (each(props)).key FROM storage_item) AS stat
WHERE key like %s WHERE key like %s
GROUP BY key GROUP BY key
ORDER BY count DESC, key ORDER BY count DESC, key
limit 10; limit 10;
""", ['%' + self.term + '%']) """,
["%" + self.term + "%"],
)
props = [e[0] for e in c.fetchall()] props = [e[0] for e in c.fetchall()]
return JsonResponse({ return JsonResponse(
'results': [ {
{ "results": [
'text': p, {
'id': p, "text": p,
} "id": p,
for p in props }
for p in props
], ],
}) }
)

View File

@ -13,9 +13,8 @@ from django.contrib.admin.widgets import AdminTextareaWidget
class ItemSelectWidget(ModelSelect2Widget): class ItemSelectWidget(ModelSelect2Widget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['data_view'] = 'item-complete' kwargs["data_view"] = "item-complete"
super(ItemSelectWidget, self).__init__(*args, **kwargs) super(ItemSelectWidget, self).__init__(*args, **kwargs)
def label_from_instance(self, obj): def label_from_instance(self, obj):
@ -30,21 +29,24 @@ class PropsSelectWidget(DictionaryFieldWidget):
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
attrs['class'] = 'hstore-original-textarea' attrs["class"] = "hstore-original-textarea"
w = HeavySelect2Widget(data_view='prop-complete', attrs={'data-tags': 'true', 'class': 'hs-key'}) w = HeavySelect2Widget(
data_view="prop-complete", attrs={"data-tags": "true", "class": "hs-key"}
)
# get default HTML from AdminTextareaWidget # get default HTML from AdminTextareaWidget
html = AdminTextareaWidget.render(self, name, value, attrs) html = AdminTextareaWidget.render(self, name, value, attrs)
# prepare template context # prepare template context
template_context = { template_context = {
'field_name': name, "field_name": name,
'STATIC_URL': settings.STATIC_URL, "STATIC_URL": settings.STATIC_URL,
'use_svg': parse_version(get_version()) >= parse_version('1.9'), # use svg icons if django >= 1.9 "use_svg": parse_version(get_version())
'ajax_url': reverse('prop-complete'), >= parse_version("1.9"), # use svg icons if django >= 1.9
'w': w.build_attrs(base_attrs=w.attrs) "ajax_url": reverse("prop-complete"),
"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_%s_widget.html" % self.admin_style)
# render additional html # render additional html
additional_html = template.render(template_context) additional_html = template.render(template_context)