Porównaj commity

...

118 Commity

Autor SHA1 Wiadomość Data
Wiktor Przybylski 29827264f8
oauth fixes 2024-03-16 18:54:57 +01:00
Wiktor Przybylski 6c318019a2
token settings tweaking, debug off 2024-03-16 18:45:48 +01:00
Wiktor Przybylski 178da60c1c
token settings tweaking, debug off 2024-03-16 18:40:24 +01:00
Wiktor Przybylski a2cfd4dbc4
token settings tweaking, debug off 2024-03-16 18:13:31 +01:00
Wiktor Przybylski 4a68ebff10
little debug 2024-03-16 18:03:35 +01:00
Wiktor Przybylski 70f5056c0d
lacking slashes 2024-03-16 17:29:03 +01:00
Wiktor Przybylski 37b0eb5f9c
back to square one with addresses 2024-03-16 16:59:16 +01:00
Wiktor Przybylski 0b99716566
requesrt token method to post check 2024-03-16 16:46:46 +01:00
Wiktor Przybylski 5f95e25bc4
Update backend.py - access_token_method? 2024-03-16 16:41:03 +01:00
Wiktor Przybylski 7276f56aa2
Update backend.py - internal token url check 2024-03-16 16:30:27 +01:00
Wiktor 69a107352d remove pg image 2024-03-16 14:36:47 +01:00
Wiktor d9df9f7bfe myslozbrodnia 2024-03-15 23:03:56 +01:00
Dariusz Niemczyk 9983511136
fix: ignore new devcontainer files 2024-02-04 16:13:34 +01:00
Dariusz Niemczyk dad68031df
fix: readme typo 2024-02-04 16:00:22 +01:00
Dariusz Niemczyk 1dc6d8b76d
fix: correctly lock all dependencies 2024-02-01 12:49:37 +01:00
Dariusz Niemczyk 8812e6c0d3
fix: run app as spejstore user, not root 2024-02-01 12:49:36 +01:00
Dariusz Niemczyk b94ab204d8
dx: Un-ignore .vscode 2024-02-01 12:49:36 +01:00
Dariusz Niemczyk 47b682c509
feat: Add awesome devcontainer intro and better readme 2024-02-01 12:49:36 +01:00
Dariusz Niemczyk 70fc374d0d
feat: add proper devcontainer support 2024-02-01 12:49:36 +01:00
Dariusz Niemczyk 77ddd6bb45
fix: ignore .DS_Store
MacOS creates .DS_Store files that are usually ignored by mac users.
Unfortunately, this is not a default for devcontainers.
2024-02-01 12:49:35 +01:00
radex 8ddc4da3b6 Merge pull request 'fix: properly create svgs static paths' (#2) from fix/static-svgs into master
Reviewed-on: https://code.hackerspace.pl/hswaw/spejstore/pulls/2
2024-01-31 13:24:05 +00:00
Dariusz Niemczyk 23c008914b
fix: properly create svgs static paths 2024-01-31 14:22:17 +01:00
radex 3c22a7b8c1
fix printing 2024-01-31 09:25:15 +01:00
radex b32e5a15fc
Revert "fix category icons"
This reverts commit cba1e2fd86.
2024-01-31 09:12:17 +01:00
radex c0d805781b
use spejstore-labelmaker v2 api 2024-01-31 00:27:48 +01:00
radex cba1e2fd86
fix category icons 2024-01-30 23:38:56 +01:00
Dariusz Niemczyk a991063084
fix: server static on s3 correctly 2024-01-19 22:31:39 +01:00
Dariusz Niemczyk 5bf31f26a1
cleanup: docker-compose and .env.example 2024-01-19 19:22:36 +01:00
Patryk Jakuszew 1bbd28933f Add S3 storage configuration 2024-01-14 23:56:26 +01:00
Dariusz Niemczyk 650fc34115
fix: add missing migration (just for clarity) 2024-01-14 21:48:55 +01:00
Dariusz Niemczyk ab631aeb90
feat: make media uploaded files generate uuid name 2024-01-14 21:47:19 +01:00
Dariusz Niemczyk 81417f58be
fix: trusted origins settings.py 2024-01-14 21:46:54 +01:00
Dariusz Niemczyk 6c81441f00
fix: make wiki link optional in admin panel 2024-01-14 15:31:13 +01:00
Dariusz Niemczyk d36a84e25b
fix: very dumb accidental commit of ALLOWED_HOSTS 2024-01-14 15:21:35 +01:00
Dariusz Niemczyk 67ac858323
fix: missing comma because python 2024-01-14 15:20:04 +01:00
Dariusz Niemczyk fe496e85f8
fix: add csrf config 2024-01-14 15:15:14 +01:00
Dariusz Niemczyk ea5e223fcc
fix: properly use threads and workers 2024-01-14 13:53:39 +01:00
Dariusz Niemczyk 2323263ccb
fix: wrong static template tag usage 2024-01-14 13:52:59 +01:00
Dariusz Niemczyk f5740e1543
fix: make wiki link nullable 2024-01-14 00:24:44 +01:00
Dariusz Niemczyk c9be16b76f
fix: missing files config 2024-01-14 00:21:30 +01:00
Dariusz Niemczyk 35a48c3eee
fix: statics 2024-01-14 00:21:23 +01:00
Dariusz Niemczyk 6af75328f4
fix: .env file is not explicitely needed 2024-01-13 23:34:05 +01:00
Dariusz Niemczyk c3c80d650c
fix: weird docker compose hacks 2024-01-13 23:30:50 +01:00
Dariusz Niemczyk 142e38ad95
fix: bump django version 2024-01-12 18:57:02 +01:00
Dariusz Niemczyk f94e7b3207
fix: use proper uri rewrites 2024-01-12 18:56:27 +01:00
Dariusz Niemczyk ee2c93908a
fix: use static template 2024-01-12 18:56:08 +01:00
Dariusz Niemczyk d1b9beca6d
feat: Add wiki_link field 2023-12-03 19:55:26 +01:00
Dariusz Niemczyk 1fcbbd9dd3
vscode: Add .devcontainer support 2023-12-03 19:54:29 +01:00
Dariusz Niemczyk 2892923389
Fix wrong psql healthcheck user 2023-11-06 20:50:00 +01:00
Dariusz Niemczyk d71885d264
Revert "hscloud: Add single address authentication"
This reverts commit f1143dc4f1.
2023-09-19 18:21:11 +02:00
Dariusz Niemczyk f1143dc4f1
hscloud: Add single address authentication 2023-09-19 18:03:57 +02:00
Dariusz Niemczyk 38c7245a3f
auth: cleanup 2023-09-09 19:26:36 +02:00
Dariusz Niemczyk 156df0a8a5
auth: add login button 2023-09-09 19:25:44 +02:00
Dariusz Niemczyk f8b3dd6bf7
auth: require necessary authentication or in lan
middleware was not written properly, now requires authentication or
being in lan for readaccess, otherwise redirecting to login page
2023-09-09 17:00:19 +02:00
Dariusz Niemczyk 3c3ba16811
middleware: Fix ordering again 2023-09-09 16:31:49 +02:00
Dariusz Niemczyk f1335f0565
auth: fix auth paths 2023-09-09 15:43:23 +02:00
Dariusz Niemczyk daea8dda22
404: fix wrong status code in html 2023-08-25 21:52:13 +02:00
Dariusz Niemczyk f92635f5f3
Revert "auth: do not automatically staff new members"
This reverts commit 0fa9762bea.
2023-08-25 21:51:09 +02:00
Dariusz Niemczyk 8ce869393e
cache: cache statics 2023-08-25 21:51:04 +02:00
Dariusz Niemczyk 0fa9762bea
auth: do not automatically staff new members 2023-08-25 21:36:19 +02:00
Dariusz Niemczyk 15bf813b04
django: force auth for all requests 2023-08-25 21:06:01 +02:00
Dariusz Niemczyk 401fcc088d
errors: add cute error pages 2023-08-25 21:04:43 +02:00
Dariusz Niemczyk 820f04cc01
settings: properly order middleware and and gzip 2023-08-25 21:03:55 +02:00
Dariusz Niemczyk c5a9fba034
Add cache 2023-08-25 20:55:17 +02:00
radex 7f19fe7c7a improve readme for docker-compose newbs 2023-08-21 21:38:13 +02:00
radex cc3fddfd22 settings: fix oauth_redirect_is_http 2023-08-21 21:36:44 +02:00
radex 20303a14a7 update readme 2023-08-18 16:39:14 +02:00
radex cc6f00da08 docker-compose: clean up dev-override 2023-08-18 16:38:41 +02:00
radex 23244bdf24 fix colors in light mode 2023-08-18 15:31:53 +02:00
radex ee9e9becf5 settings: make redirect_is_https configurable separate from prod 2023-08-18 15:28:57 +02:00
radex 6cd7ec529b docker-compose: change postgres healthcheck interval to 1s 2023-08-18 15:27:56 +02:00
radex d615da3a0f add error logging in production 2023-08-18 15:27:33 +02:00
radex 4fc47030db docker-compose: build locally 2023-08-18 13:00:53 +02:00
radex ce3f07de5f add whitenoise lib to serve statics in production w/o nginx 2023-08-18 13:00:32 +02:00
radex e3229ffd7e Docker: fix broken build_static 2023-08-17 14:21:10 +02:00
radex d4a305c362 Docker: update dev overrides so that app starts 2023-08-17 14:20:39 +02:00
radex 255e0f0d08 Docker: wait for postgres until starting web container 2023-08-17 14:20:22 +02:00
radex af9cb2db32 Docker: update postgres to 15.4 2023-08-17 14:20:00 +02:00
Dariusz Niemczyk 9200bdbb3b
authentication: always require if defined env
if SPEJSTORE_REQUIRE_AUTH is 'true' then always require auth
otherwise make it read-only on unauthorized access
2023-08-13 20:10:00 +02:00
Dariusz Niemczyk d942c99cb9
Fix parent autocompletes 2023-08-04 18:02:08 +02:00
Dariusz Niemczyk e1a22100c4
Remove unused select2 refs 2023-08-04 17:46:08 +02:00
Dariusz Niemczyk 4fc3629dcd
Fix users searchfield not working 2023-08-04 17:26:25 +02:00
Dariusz Niemczyk 875e385f68
WIP new docs 2023-07-23 17:31:46 +02:00
Dariusz Niemczyk 8048fccede
Make paths publically available due to beyondspace 2023-07-23 17:31:37 +02:00
Dariusz Niemczyk 3a286a5bc6
Add autocomplete for users in Admin and fix perms 2023-07-20 14:49:44 +02:00
Dariusz Niemczyk 2a70d2cb31
Add configurable auth-by-lan values 2023-07-17 23:12:02 +02:00
Dariusz Niemczyk ae219a2533
Properly collectstatic on docker build 2023-07-17 23:09:11 +02:00
Dariusz Niemczyk d026e41ac5
Update docker-compose setup 2023-07-17 23:08:24 +02:00
Dariusz Niemczyk a6705a956f
Remove legacy labels from django admin 2023-07-17 23:04:30 +02:00
Dariusz Niemczyk 5012a10298
Hack: admin props combobox works(?) 2023-07-17 23:04:30 +02:00
Dariusz Niemczyk 027bcfcde5
Hack: admin props combobox works(?) 2023-07-17 22:44:44 +02:00
Dariusz Niemczyk 154e1079da
Make HSLan always authenticated for GET 2023-07-17 21:35:45 +02:00
Dariusz Niemczyk 30c3c3eb7a
Deduplicate print function 2023-07-17 20:18:18 +02:00
Dariusz Niemczyk c15f1bb840
Make gunicorn properly report errots 2023-07-17 15:47:29 +02:00
Dariusz Niemczyk f7688262e4
Fix API views 2023-07-17 15:46:00 +02:00
Dariusz Niemczyk d2e25c0801
Add overrides to docker-ignore 2023-07-14 22:38:48 +02:00
Dariusz Niemczyk e2e82b1a2e
Replace docker-compose to gunicorn run 2023-07-14 16:29:47 +02:00
Dariusz Niemczyk 8210381027
Fix docker port to 8000 2023-07-14 16:29:39 +02:00
Dariusz Niemczyk b09016ea3b
Add .env to .gitignore 2023-07-14 16:28:07 +02:00
Dariusz Niemczyk af85d191ad
Force authentication for API usage 2023-07-13 22:37:53 +02:00
Dariusz Niemczyk a20e14a8d3
Add LOGIN_URL to redirect properly 2023-07-13 21:01:41 +02:00
Dariusz Niemczyk b6ce1516d2
Migrate deprecated field in settings 2023-07-13 21:01:17 +02:00
Dariusz Niemczyk a8f7530263
Force authorization for all routes 2023-07-13 21:01:00 +02:00
Dariusz Niemczyk b74c1b3c8f
Add inventory.hackerspace.pl to ALLOWED_HOSTS 2023-07-11 23:36:35 +02:00
Dariusz Niemczyk bfe9d27d71
Make dockerable for hscloud 2023-07-11 23:29:54 +02:00
Dariusz Niemczyk ad73094b67
Fix import for oauth 2023-07-11 23:29:39 +02:00
Dariusz Niemczyk a0c6d87adb
Add missing migration (django unhappy) 2023-07-11 23:29:28 +02:00
Dariusz Niemczyk 5ed4128151
Add terrible dark mode for inventory 2023-07-11 18:34:18 +02:00
Dariusz Niemczyk d473901f8c
Minify bootstrap.css 2023-07-11 18:34:01 +02:00
Dariusz Niemczyk 878f246b08
Admin dark-mode properly done 2023-07-11 18:12:12 +02:00
Dariusz Niemczyk af1be4aca7
Add __pycache__ to gitignore 2023-07-11 17:54:36 +02:00
Dariusz Niemczyk 72e668622d
Remove unnecessary create_extension 2023-07-11 17:53:17 +02:00
Dariusz Niemczyk 150c405468
Fix settings 2023-07-11 17:52:38 +02:00
Dariusz Niemczyk efcd932481
re-implement get_roots in view 2023-07-11 17:52:38 +02:00
Dariusz Niemczyk 837734a655
Fix admin not searching properly and make it great 2023-07-11 17:52:38 +02:00
Dariusz Niemczyk 45ad9bf88c
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.
2023-07-11 17:52:38 +02:00
Dariusz Niemczyk 659f04ce9c
Blackify the code (autoformat) 2023-07-11 15:34:35 +02:00
Dariusz Niemczyk 3fdf788168
Add .venv and .vscode to gitignore 2023-07-11 15:34:26 +02:00
73 zmienionych plików z 1904 dodań i 7979 usunięć

2
.devcontainer/.gitignore vendored 100644
Wyświetl plik

@ -0,0 +1,2 @@
\.*
!.gitignore

Wyświetl plik

@ -0,0 +1,50 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
{
"name": "Extend base docker-compose for development purposes",
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],
// The 'service' property is the name of the service for the container that VS Code should
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
"service": "web",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/sshd:1": {}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",
// Uncomment the next line to run commands after the container is created.
"postStartCommand": "${containerWorkspaceFolder}/manage.py migrate",
"postCreateCommand": "${containerWorkspaceFolder}/manage.py collectstatic --no-input --clear",
"customizations": {
"vscode": {
"settings": {
"extensions.verifySignature": false
},
"extensions": [
"ms-python.python",
"mikestead.dotenv",
"VisualStudioExptTeam.vscodeintellicode",
"ms-python.black-formatter",
"VisualStudioExptTeam.intellicode-api-usage-examples",
"ms-azuretools.vscode-docker",
"DavidAnson.vscode-markdownlint",
"yzhang.markdown-all-in-one"
]
}
},
"containerUser": "spejstore"
}

Wyświetl plik

@ -0,0 +1,24 @@
version: "3.4"
services:
web:
volumes:
- ..:/workspaces:cached
restart: unless-stopped
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
environment:
- SPEJSTORE_ENV=dev
- SPEJSTORE_DB_NAME=postgres
- SPEJSTORE_DB_USER=postgres
- SPEJSTORE_DB_PASSWORD=postgres
- SPEJSTORE_DB_HOST=db
# - SPEJSTORE_DB_PORT=
- SPEJSTORE_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# - SPEJSTORE_CLIENT_ID=
# - SPEJSTORE_SECRET=
# - SPEJSTORE_MEDIA_ROOT=
# - SPEJSTORE_REQUIRE_AUTH=true
- SPEJSTORE_OAUTH_REDIRECT_IS_HTTPS=false
- SPEJSTORE_SPEJSTORE_FILE_STORAGE_TYPE="filesystem"
# - SPEJSTORE_PROXY_TRUSTED_IPS=172.21.37.1

Wyświetl plik

@ -10,4 +10,14 @@ postgres-hstore/
.ropeproject/
docker-compose.yml
docker-compose.override.yml
docker-compose.prod-override.yml
docker-compose.dev-override.yml
build_static
.venv
.vscode
.history
log
.Dockerfile
.env
.devcontainer
readme

20
.env.example 100644
Wyświetl plik

@ -0,0 +1,20 @@
SPEJSTORE_CLIENT_ID=OAUTH_ID
SPEJSTORE_SECRET=OAUTH_SECRET
SPEJSTORE_ENV=prod
SPEJSTORE_DB_NAME=postgres
SPEJSTORE_DB_PASSWORD=postgres
SPEJSTORE_DB_USER=postgres
SPEJSTORE_DB_HOST=db
SPEJSTORE_HOST="https://inventory.hackerspace.pl"
SPEJSTORE_LABEL_API=https://label.waw.hackerspace.pl
# "filesystem" or "s3"
SPEJSTORE_FILE_STORAGE_TYPE="filesystem"
# S3 bucket
SPEJSTORE_S3_ACCESS_KEY="SPEJSTORE_S3_ACCESS_KEY"
SPEJSTORE_S3_SECRET_KEY="SPEJSTORE_S3_SECRET_KEY"
SPEJSTORE_S3_ENDPOINT_URL="SPEJSTORE_S3_ENDPOINT_URL"
SPEJSTORE_S3_BUCKET_NAME="SPEJSTORE_S3_BUCKET_NAME"
SPEJSTORE_S3_MEDIA_LOCATION="SPEJSTORE_S3_MEDIA_LOCATION"

Wyświetl plik

@ -0,0 +1,42 @@
name: Create and publish Docker images
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

12
.gitignore vendored
Wyświetl plik

@ -2,6 +2,7 @@
db.sqlite3
*.swp
spejstore.env
.env
env/
backups
media/
@ -10,3 +11,14 @@ postgres-hstore/
.ropeproject/
docker-compose.override.yml
build_static
.venv
__pycache__
# Is not ignored by default in devcontainers, macs bad
.DS_Store
# Ignore vscode devcontainer files
.vscode-server
.ssh
.gitconfig

5
.vscode/extensions.json vendored 100644
Wyświetl plik

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-vscode-remote.remote-containers"
]
}

20
.vscode/launch.json vendored 100644
Wyświetl plik

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Django",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": [
"runserver",
"0.0.0.0:8000",
],
"django": true,
"justMyCode": true
}
]
}

5
.vscode/settings.json vendored 100644
Wyświetl plik

@ -0,0 +1,5 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
}

27
.vscode/tasks.json vendored 100644
Wyświetl plik

@ -0,0 +1,27 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "migrate",
"type": "shell",
"command": "${workspaceFolder}/manage.py migrate"
},
{
"label": "makemigrations",
"type": "shell",
"command": "${workspaceFolder}/manage.py makemigrations"
},
{
"label": "runserver",
"type": "shell",
"command": "${workspaceFolder}/manage.py runserver"
},
{
"label": "collectstatic",
"type": "shell",
"command": "${workspaceFolder}/manage.py collectstatic"
}
]
}

Wyświetl plik

@ -1,11 +1,18 @@
FROM python:3.5.9@sha256:3a71fd2dac2343263993f4ab898c9398dfbfd0235dafe41e784876b69bdfa899
FROM python:3.11.4-slim-bookworm
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
gcc \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt /code/
RUN pip install --no-cache-dir -r requirements.txt
RUN wget https://github.com/vishnubob/wait-for-it/raw/8ed92e8cab83cfed76ff012ed4a36cef74b28096/wait-for-it.sh -O /usr/local/bin/wait-for-it && chmod +x /usr/local/bin/wait-for-it
ADD . /code/
RUN python manage.py collectstatic
CMD bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
RUN groupadd --gid 1000 spejstore && useradd --uid 1000 --gid 1000 --home /code --shell /bin/bash spejstore
USER spejstore
CMD bash -c "python manage.py collectstatic --no-input --clear && python manage.py migrate && gunicorn --workers 1 --threads 4 -b 0.0.0.0:8000 --capture-output --error-logfile - --access-logfile - spejstore.wsgi:application"

Wyświetl plik

@ -1,31 +1,52 @@
# spejstore
# spejstore (AKA inventory)
The general HSWAW (and other polish hackerspaces) inventory system.
Because there is not enough general inventory software invented here yet.
Please use Python3, for the love of `$deity`...
## Usage
### Quick start
### Quick start (VSCode)
1. Run:
```sh
ln -s docker-compose.dev-override.yml docker-compose.override.yml
docker-compose up --build
```
2. Run `docker-compose run --rm web python manage.py createsuperuser` -- now you can dev authenticate w/o SSO
1. Copy `.env.example` as `.env`
2. Have `docker compose` 2.0. You can identify it by having `docker compose` command instead of `docker-compose`.
3. Customize your `.env` for your specific usecase.
### Build & run
#### VSCode
```sh
docker-compose up
```
0. Setup environment variables
1. Get VSCode from [here](https://code.visualstudio.com/download), *CAN NOT* be VSCodium, as the extension is a microsoft binary which does not work with VSCodium.
2. Install [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension
3. Clone the repository and open it with VSCode.
4. You should get a toast like this when re-opening directory with the cloned repository. ![Toast example](readme/toast-example.png 'Toast example')
1. If you don't get a toast, then use (CMD|Ctrl)+Shift+P to open actions menu and choose option **Rebuild Without Cache and Reopen in Container**. ![Command example](readme/command-example.png 'Command example')
5. Reopen the directory in container either via command or popup button.
6. Wait for the application and container to properly build.
1. Devcontainer's VSCode instance will be automatically configured with extensions to help your development process.
2. You might get a Toast telling you to re-open the directory due to Black not working properly. Do so for proper autoformatting support.
3. Make sure that extensions were installed in your vscode devcontainer. It might take a couple of minutes.
7. `manage.py migrate` will be run automatically after container creation, to make sure you have the latest migrations done on the development database without any need for interaction.
8. Run debug session with either command of "Start Debugging" (default hotkey F5), or with the Debug sidebar. ![Debug sidebar instructions](readme/debug-example.png 'Debug sidebar')
9. You should have automatically forwarded ports, so the only thing remaining is opening browser window with the url provided in terminal.
### Rebuild
#### Everything else (docker)
```sh
docker-compose build
```
1. Run `docker compose up`. This will create a production-ready setup with gunicorn. out of the box.
### Troubleshooting
### Everything else (python)
- https://askubuntu.com/q/615394/413683
1. Get python3
2. `pip install -r requirements.txt`
3. `python3 manage.py migrate`
4. `python3 manage.py collectstatic`
5. `python3 manage.py runserver 0.0.0.0:8000`
## New docs (WIP)
Spejstore is a simple inventory system made for Warsaw Hackerspace purposes. Includes some features very specific to hswaw requirements, which are:
- Label printing and label-system support (via `django-rest-api` api views and `SPEJSTORE_LABEL_API` env variable), using the [spejstore-labelmaker](https://code.hackerspace.pl/informatic/spejstore-labelmaker/) software
- Publically viewing all items and requiring users to sign in view oauth to manage inventory via `django-admin`
- Authorizing label printing via local network only, see `SPEJSTORE_LAN_ALLOWED_ADDRESS_SPACE` env variable
Currently inventory is deployed under `inventory.waw.hackerspace.pl`, with a [Beyondspace NGINX configuration](https://cs.hackerspace.pl/hscloud/-/blob/hswaw/machines/customs.hackerspace.pl/beyondspace.nix), which allows the inventory to be accessible from outside of the Warsaw Hackerspace network with a necessary oauth authorization, but does not allow printing of labels without physically being in the local network of HSWAW.

Wyświetl plik

@ -1,38 +1,41 @@
from urllib.parse import urlencode
from social_core.backends.oauth import BaseOAuth2
from six.moves.urllib_parse import urlencode, unquote
class HSWawOAuth2(BaseOAuth2):
"""Hackerspace OAuth authentication backend"""
name = 'hswaw'
ID_KEY = 'username'
AUTHORIZATION_URL = 'https://sso.hackerspace.pl/oauth/authorize'
ACCESS_TOKEN_URL = 'https://sso.hackerspace.pl/oauth/token'
DEFAULT_SCOPE = ['profile:read']
name = "hswaw"
ID_KEY = "username"
AUTHORIZATION_URL = "https://auth.apps.hskrk.pl/application/o/authorize/"
ACCESS_TOKEN_URL = "https://auth.apps.hskrk.pl/application/o/token/"
DEFAULT_SCOPE = ["email openid profile"]
ACCESS_TOKEN_METHOD = 'POST'
REQUEST_TOKEN_METHOD = 'POST'
REDIRECT_STATE = False
SCOPE_SEPARATOR = ','
EXTRA_DATA = [
('expires', 'expires_in')
]
SCOPE_SEPARATOR = ","
EXTRA_DATA = [("expires", "expires_in")]
def get_user_details(self, response):
"""Return user details from Hackerspace account"""
personal_email = None
if response.get('personal_email'):
personal_email = response.get('personal_email')[0]
if response.get("email"):
personal_email = response.get("email")
return {'username': response.get('username'),
'email': response.get('email'),
'personal_email': personal_email,
}
return {
"username": response.get("preferred_username"),
"email": response.get("email"),
"personal_email": personal_email,
}
def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service"""
url = 'https://sso.hackerspace.pl/api/1/profile'
headers = {
'Authorization': 'Bearer {}'.format(access_token)
}
url = "https://auth.apps.hskrk.pl/application/o/userinfo/"
headers = {"Authorization": "Bearer {}".format(access_token)}
# print(url)
# print(headers)
# print(access_token)
return self.get_json(url, headers=headers)
def auth_url(self):
"""Return redirect url"""
state = self.get_or_create_state()
@ -40,4 +43,4 @@ class HSWawOAuth2(BaseOAuth2):
params.update(self.get_scope_argument())
params.update(self.auth_extra_arguments())
params = urlencode(params)
return '{0}?{1}'.format(self.authorization_url(), params)
return "{0}?{1}".format(self.authorization_url(), params)

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -1,6 +0,0 @@
version: "3"
services:
web:
environment:
- SPEJSTORE_ENV=dev
- SPEJSTORE_ALLOWED_HOSTS=localhost,127.0.0.1

Wyświetl plik

@ -1,6 +0,0 @@
version: "3"
services:
db:
volumes:
- /var/spejstore-data:/var/lib/postgresql/data

Wyświetl plik

@ -1,22 +1,35 @@
version: "3"
services:
db:
build: postgres-hstore
restart: always
image: postgres:15.4
restart: unless-stopped
volumes:
- spejstore-db:/var/lib/postgresql/data
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
healthcheck:
#CHANGE 1: this command checks if the database is ready, right on the source db server
test: ["CMD-SHELL", "pg_isready -d postgres -U postgres"]
interval: 1s
timeout: 1s
retries: 5
web:
build: .
restart: always
command: bash -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
restart: unless-stopped
command: bash -c "python manage.py collectstatic --no-input --clear && python manage.py migrate && gunicorn --workers 1 --threads 4 -b 0.0.0.0:8000 --capture-output --error-logfile - --access-logfile - spejstore.wsgi:application"
volumes:
- .:/code
- /code/build_static
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
environment:
- SPEJSTORE_CLIENT_ID
- SPEJSTORE_SECRET
- SPEJSTORE_ENV
db:
condition: service_healthy
volumes:
spejstore-db:
external: false

Wyświetl plik

@ -1,3 +0,0 @@
FROM postgres:9.6.17@sha256:5b39dd4a26a02fee26902c84d8bafb2eb0ab9a2874fb0be22056107aa6508899
MAINTAINER Piotr Dobrowolski
ADD create_extension.sh /docker-entrypoint-initdb.d/create_extension.sh

Wyświetl plik

@ -1,16 +0,0 @@
#!/bin/bash
set -e
# Because both template1 and the user postgres database have already been created,
# we need to create the hstore extension in template1 and then recreate the postgres database.
#
# Running CREATE EXTENSION in both template1 and postgres can lead to
# the extensions having different eid's.
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname template1 <<EOSQL
CREATE EXTENSION hstore;
CREATE EXTENSION ltree;
CREATE EXTENSION pg_trgm;
DROP DATABASE $POSTGRES_USER;
CREATE DATABASE $POSTGRES_USER TEMPLATE template1;
EOSQL

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 30 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 152 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 45 KiB

Wyświetl plik

@ -1,16 +1,43 @@
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
boto3==1.34.32
botocore==1.34.32
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==5.0.1
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-storages==1.14.2
django-tree @ https://github.com/Palid/django-tree/archive/439e9a867e789b58f0c37e34e8ceb85ce1b806dc.zip#sha256=044e6766f4993512404492d7ec76949ca6b9ed44c88619ecc3a4e5339aa27b71
djangorestframework==3.14.0
docopt==0.6.2
gunicorn==21.2.0
idna==3.4
jmespath==1.0.1
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
python-dateutil==2.8.2
python3-openid==3.2.0
pytz==2023.3
requests==2.31.0
requests-oauthlib==1.3.1
s3transfer==0.10.0
six==1.16.0
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
whitenoise==6.5.0

Wyświetl plik

@ -1,3 +1,7 @@
SPEJSTORE_CLIENT_ID=inventory
SPEJSTORE_SECRET=secret
SPEJSTORE_ALLOWED_HOSTS=localhost,127.0.0.1
SPEJSTORE_DB_NAME=postgres
SPEJSTORE_DB_PASSWORD=postgres
SPEJSTORE_DB_USER=postgres
SPEJSTORE_DB_HOST=db

Wyświetl plik

@ -1,108 +1,120 @@
"""
Django settings for spejstore project.
Generated by 'django-admin startproject' using Django 1.10.1.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
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, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_ROOT = os.path.join(BASE_DIR, 'build_static')
PROD = os.getenv('SPEJSTORE_ENV') == 'prod'
STATIC_ROOT = os.path.join(BASE_DIR, "build_static")
PROD = os.getenv("SPEJSTORE_ENV") == "prod"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# 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!
DEBUG = not PROD
ALLOWED_HOSTS = env('ALLOWED_HOSTS', 'devinventory,inventory.waw.hackerspace.pl,i,inventory').split(',')
LOGIN_REDIRECT_URL = '/admin/'
ALLOWED_HOSTS = env(
"ALLOWED_HOSTS",
"devinventory,inventory.waw.hackerspace.pl,inventory.hackerspace.pl,i,inventory"
+ (",127.0.0.1,locahost,*" if not PROD else ""),
).split(",")
LOGIN_REDIRECT_URL = "/admin/"
CSRF_TRUSTED_ORIGINS = env("HOST", "https://inventory.hackerspace.pl").split(",")
# Application definition
INSTALLED_APPS = [
'flat_responsive',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.postgres',
'social_django',
'django_hstore',
'tree',
'django_select2',
'rest_framework',
'rest_framework.authtoken',
'django_markdown2',
'storage',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.postgres",
"storages", # django-storages s3boto support
"social_django",
"tree",
"django_select2",
"rest_framework",
"rest_framework.authtoken",
"django_markdown2",
"storage",
"django_admin_hstore_widget",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'social_django.middleware.SocialAuthExceptionMiddleware',
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.gzip.GZipMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"storage.middleware.is_authorized_or_in_lan_middleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"social_django.middleware.SocialAuthExceptionMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
]
ROOT_URLCONF = 'spejstore.urls'
ROOT_URLCONF = "spejstore.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": ["templates/"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
],
},
},
]
WSGI_APPLICATION = 'spejstore.wsgi.application'
WSGI_APPLICATION = "spejstore.wsgi.application"
# Logging
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': env('DB_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': env('DB_NAME', 'postgres'),
'USER': env('DB_USER', 'postgres'),
'PASSWORD': env('DB_PASSWORD', None),
'HOST': env('DB_HOST', 'db'),
'PORT': env('DB_PORT', 5432),
"default": {
"ENGINE": env("DB_ENGINE", "django.db.backends.postgresql_psycopg2"),
"NAME": env("DB_NAME", "postgres"),
"USER": env("DB_USER", "postgres"),
"PASSWORD": env("DB_PASSWORD", None),
"HOST": env("DB_HOST", "127.0.0.1"),
"PORT": env("DB_PORT", 5432),
}
}
@ -112,82 +124,148 @@ DATABASES = {
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_JS = 'js/select2.min.js'
SELECT2_CSS = 'css/select2.min.css'
SELECT2_I18N_PATH = ''
AUTHENTICATION_BACKENDS = (
'auth.backend.HSWawOAuth2',
'django.contrib.auth.backends.ModelBackend',
"auth.backend.HSWawOAuth2",
"django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.social_auth.associate_by_email',
'auth.pipeline.associate_by_personal_email',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'auth.pipeline.staff_me_up',
"social_core.pipeline.social_auth.social_details",
"social_core.pipeline.social_auth.social_uid",
"social_core.pipeline.social_auth.social_user",
"social_core.pipeline.user.get_username",
"social_core.pipeline.social_auth.associate_by_email",
"auth.pipeline.associate_by_personal_email",
"social_core.pipeline.user.create_user",
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
"auth.pipeline.staff_me_up",
)
# Determines the storage type for Django static files and media.
FILE_STORAGE_TYPE = env("FILE_STORAGE_TYPE", "filesystem")
# Make sure we check for correct file storage type
if not (FILE_STORAGE_TYPE == "filesystem" or FILE_STORAGE_TYPE == "s3"):
raise Exception("SPEJSTORE_FILE_STORAGE_TYPE must be 'filesystem' or 's3' ")
if FILE_STORAGE_TYPE == "filesystem":
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = "/static/"
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
MEDIA_URL = "/media/"
MEDIA_ROOT = env("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
elif FILE_STORAGE_TYPE == "s3":
S3_BUCKET_NAME = env("S3_BUCKET_NAME", "inventory")
S3_ENDPOINT_URL = env("S3_ENDPOINT_URL", "https://object.ceph-eu.hswaw.net")
S3_DOMAIN_NAME = env("S3_DOMAIN_NAME", "object.ceph-eu.hswaw.net")
S3_ACCESS_KEY = env("S3_ACCESS_KEY", "")
S3_SECRET_KEY = env("S3_SECRET_KEY", "=")
S3_STATIC_LOCATION = "static"
S3_MEDIA_LOCATION = "media"
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"access_key": S3_ACCESS_KEY,
"secret_key": S3_SECRET_KEY,
"endpoint_url": S3_ENDPOINT_URL,
"bucket_name": S3_BUCKET_NAME,
"default_acl": "public-read",
"location": S3_MEDIA_LOCATION,
"custom_domain": f"{S3_DOMAIN_NAME}/{S3_BUCKET_NAME}",
"file_overwrite": False,
},
},
"staticfiles": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"access_key": S3_ACCESS_KEY,
"secret_key": S3_SECRET_KEY,
"endpoint_url": S3_ENDPOINT_URL,
"bucket_name": S3_BUCKET_NAME,
"default_acl": "public-read",
"location": S3_STATIC_LOCATION,
"custom_domain": f"{S3_DOMAIN_NAME}/{S3_BUCKET_NAME}",
},
},
}
bucket_domain_name = f"{S3_ENDPOINT_URL}/{S3_BUCKET_NAME}"
STATIC_URL = f"{bucket_domain_name}/{S3_STATIC_LOCATION}/"
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
MEDIA_URL = "/media/"
STATIC_URL = f"{bucket_domain_name}/{S3_MEDIA_LOCATION}/"
MEDIA_ROOT = env("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
# Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
MEDIA_URL = '/media/'
MEDIA_ROOT = env('MEDIA_ROOT', os.path.join(BASE_DIR, "media"))
REQUIRE_AUTH = env("REQUIRE_AUTH", "true")
if REQUIRE_AUTH == "true":
REQUIRE_AUTH = True
elif REQUIRE_AUTH == "false":
REQUIRE_AUTH = False
# REST Framework
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly"
if REQUIRE_AUTH
else "rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"storage.authentication.LanAuthentication",
"rest_framework.authentication.BasicAuthentication",
"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_SECRET = env('SECRET')
SOCIAL_AUTH_REDIRECT_IS_HTTPS = PROD
SOCIAL_AUTH_HSWAW_KEY = env("CLIENT_ID")
SOCIAL_AUTH_HSWAW_SECRET = env("SECRET")
SOCIAL_AUTH_REDIRECT_IS_HTTPS = env("OAUTH_REDIRECT_IS_HTTPS", "true") == "true"
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_JSONFIELD_ENABLED = True
LABEL_API = env('LABEL_API', 'http://label.waw.hackerspace.pl:4567')
LABEL_API = env("LABEL_API", "http://label.waw.hackerspace.pl:4567")
LOGIN_URL = "/admin/login/"
# Local LAN address space
LAN_ALLOWED_ADDRESS_SPACE = env("LAN_ALLOWED_ADDRESS_SPACE", "")

Wyświetl plik

@ -3,7 +3,7 @@
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
"""
from django.conf.urls import url, include
from django.urls import re_path, include
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
@ -16,19 +16,25 @@ from auth.views import auth_redirect
router = routers.DefaultRouter()
router.register(r'items', apiviews.ItemViewSet)
router.register(r'labels', apiviews.LabelViewSet)
router.register(r"items", apiviews.ItemViewSet)
router.register(r"labels", apiviews.LabelViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = ([
url(r'^admin/login/.*', auth_redirect),
] if settings.PROD else []) + [
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) \
urlpatterns = (
(
[
re_path(r"^admin/login/.*", auth_redirect),
]
if settings.PROD
else []
)
+ [
re_path(r"^admin/", admin.site.urls),
re_path(r"^select2/", include("django_select2.urls")),
re_path(r"^", include("storage.urls")),
re_path(r"^api/1/", include(router.urls)),
]
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
)

Wyświetl plik

@ -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").select2();
$("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").select2();
} 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;
}
});

Wyświetl plik

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

Wyświetl plik

@ -1,4 +1,38 @@
/* django-admin CSS patches */
.select2-container {
min-width: 400px;
min-width: 400px;
}
@media (prefers-color-scheme: dark) {
/* Change the appearence of the bakground colour surrounding the search input field */
.select2-search {
background-color: #343a40 !important;
}
/* Change the appearence of the search input field */
.select2-search input {
color: #ffffff !important;
background-color: #343a40 !important;
}
/* Change the appearence of the search results container */
.select2-results {
background-color: #343a40 !important;
}
/* Change the appearence of the dropdown select container */
.select2-container .select2-selection {
border-color: #6c757d !important;
color: #ffffff !important;
background-color: #343a40 !important;
}
/* Change the caret down arrow symbol to white */
.select2-container .select2-selection__arrow b {
border-color: #fff transparent transparent transparent !important;
}
/* Change the color of the default selected item i.e. the first option */
.select2-container .select2-selection--single .select2-selection__rendered {
color: #ffffff !important;
}
}

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,32 +1,42 @@
.table td.placeholder {
text-align: center;
color: #999;
text-align: center;
color: #999;
}
.table td.placeholder a {
display: block;
text-decoration: none;
color: inherit;
font-weight: bold;
display: block;
text-decoration: none;
color: inherit;
font-weight: bold;
}
.containericon {
white-space: nowrap;
display: inline-block;
vertical-align: middle;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
.containericon span {
padding-left: 0.5rem;
font-weight: bold;
padding-left: 0.5rem;
font-weight: bold;
}
.label-item {
background: #f5f5f5;
border-radius: 3px;
display: inline-block;
background: #f5f5f5;
border-radius: 3px;
display: inline-block;
}
.label-item code {
margin: 0em 1em;
margin: 0em 1em;
}
@media (prefers-color-scheme: dark) {
a {
color: #81d4fa !important;
}
a.navbar-brand {
color: #fff !important;
}
}

File diff suppressed because one or more lines are too long

14
static/css/theme.min.css vendored 100644

File diff suppressed because one or more lines are too long

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,16 +1,15 @@
from django import forms
from django.contrib import admin
from django_select2.forms import ModelSelect2Widget, Select2MultipleWidget
from .models import Item, ItemImage, Category, StaffProxyModel
from .widgets import PropsSelectWidget
from .models import Item, ItemImage, Category, Label
from .widgets import ItemSelectWidget, 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
@ -19,15 +18,14 @@ class ModelAdminMixin(object):
class ItemForm(forms.ModelForm):
name = forms.CharField(widget=forms.TextInput())
wiki_link = forms.CharField(required=False, widget=forms.TextInput())
class Meta:
model = Item
exclude = []
widgets = {
'parent': ItemSelectWidget(model=Item),
'categories': Select2MultipleWidget,
'props': PropsSelectWidget
}
"props": PropsSelectWidget,
}
class ItemImageInline(ModelAdminMixin, admin.TabularInline):
@ -35,46 +33,50 @@ class ItemImageInline(ModelAdminMixin, admin.TabularInline):
extra = 1
class LabelInline(ModelAdminMixin, admin.TabularInline):
model = Label
class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
list_display = ('_name',)
list_filter = ('categories',)
list_display = ("_name",)
list_filter = ("categories",)
form = ItemForm
inlines = [ItemImageInline, LabelInline]
inlines = [ItemImageInline]
save_on_top = True
autocomplete_fields = [
"parent",
"owner",
"taken_by",
"categories",
]
search_fields = [
"name",
]
def formfield_for_foreignkey(self, db_field, request, **kwargs):
formfield = super().formfield_for_foreignkey(db_field, request, **kwargs)
if db_field.name == "owner":
formfield.queryset = formfield.queryset.order_by("username")
return formfield
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)
# Store last input parent to use as default on next creation
if obj.parent:
request.session['last-parent'] = str(obj.parent.uuid)
request.session["last-parent"] = str(obj.parent.uuid)
else:
request.session['last-parent'] = str(obj.uuid)
request.session["last-parent"] = str(obj.uuid)
def get_changeform_initial_data(self, request):
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))
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',)
}
css = {"all": ("css/admin.css",)}
def response_action(self, request, queryset):
with Item.disabled_tree_trigger():
@ -82,11 +84,12 @@ class ItemAdmin(ModelAdminMixin, admin.ModelAdmin):
class NormalModelAdmin(ModelAdminMixin, admin.ModelAdmin):
search_fields = ["name"]
pass
admin.site.site_title = 'Hackerspace Storage Admin'
admin.site.site_header = 'Hackerspace Storage Admin'
admin.site.site_title = "Hackerspace Storage Admin"
admin.site.site_header = "Hackerspace Storage Admin"
admin.site.register(Item, ItemAdmin)
admin.site.register(Category, NormalModelAdmin)
@ -94,13 +97,49 @@ admin.site.register(Category, NormalModelAdmin)
from django.contrib.auth.models import User
from django.contrib.auth.models import Group
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_short_name", User.get_username)
User.add_to_class("get_full_name", User.get_username)
from django.contrib.auth.admin import UserAdmin
class StaffProxyModelAdmin(UserAdmin):
def has_module_permission(self, request):
return request.user.is_superuser
def has_add_permission(self, request, obj=None):
return request.user.is_superuser
def __has_view_permission(self, request, obj=None):
return True
has_view_permission = __has_view_permission
has_change_permission = has_add_permission
has_delete_permission = has_add_permission
has_module_permission = has_add_permission
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
is_superuser = request.user.is_superuser
if not is_superuser:
for f in form.base_fields:
form.base_fields[f].disabled = True
return form
# admin.site.register(StaffProxyModel, StaffProxyModelAdmin)
admin.site.unregister(User)
admin.site.unregister(Group)
from social_django.admin import UserSocialAuth, Nonce, Association
admin.site.unregister(UserSocialAuth)
admin.site.unregister(Nonce)
admin.site.unregister(Association)
admin.site.register(StaffProxyModel, StaffProxyModelAdmin)

Wyświetl plik

@ -1,23 +1,30 @@
from rest_framework import viewsets, generics, filters
from rest_framework import viewsets, 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.authentication import LanAuthentication
from storage.models import Item, Label
from storage.serializers import ItemSerializer, LabelSerializer
from django.http import Http404
from django.shortcuts import get_object_or_404
from storage.views import apply_smart_search
def api_print(quantity, obj):
amount = min(int(quantity), 5)
for _ in range(amount):
obj.print()
return Response({"status": "success"})
class SmartSearchFilterBackend(filters.BaseFilterBackend):
"""
Filters query using smartsearch filter
"""
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:
return apply_smart_search(search_query, queryset)
@ -28,30 +35,23 @@ class LabelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows items to be viewed or edited.
"""
queryset = Label.objects
serializer_class = LabelSerializer
@detail_route(methods=['post'], permission_classes=[AllowAny])
def print(self, request, pk):
quantity = min(int(request.query_params.get('quantity', 1)), 5)
obj = self.get_object()
for _ in range(quantity):
obj.print()
return obj
queryset = Label.objects.all()
serializer_class = LabelSerializer
class ItemViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows items to be viewed or edited.
"""
queryset = Item.objects
queryset = Item.objects.all()
serializer_class = ItemSerializer
filter_backends = (SmartSearchFilterBackend, filters.OrderingFilter)
ordering_fields = '__all__'
ordering_fields = "__all__"
def get_queryset(self):
return Item.get_roots()
return Item.objects.filter(**{"path__level": 1})
def get_object(self):
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
@ -63,7 +63,7 @@ class ItemViewSet(viewsets.ModelViewSet):
def get_item_by_id_or_label(self, id):
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
except Item.DoesNotExist:
try:
@ -72,31 +72,40 @@ class ItemViewSet(viewsets.ModelViewSet):
except Label.DoesNotExist:
raise Http404()
@detail_route(methods=['post'], permission_classes=[AllowAny])
@action(
detail=True,
methods=["post"],
# AllowAny is correct here, as we require LanAuthentication anyways
permission_classes=[AllowAny],
authentication_classes=[LanAuthentication],
)
def print(self, request, pk):
# todo: deduplicate
quantity = min(int(request.query_params.get('quantity', 1)), 5)
obj = self.get_object()
for _ in range(quantity):
obj.print()
return obj
return api_print(request.query_params.get("quantity", 1), self.get_object())
@detail_route()
@action(detail=True, authentication_classes=[LanAuthentication])
def children(self, request, pk):
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()
@action(detail=True, authentication_classes=[LanAuthentication])
def ancestors(self, request, pk):
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()
@action(detail=True, authentication_classes=[LanAuthentication])
def descendants(self, request, pk):
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()
@action(detail=True, authentication_classes=[LanAuthentication])
def siblings(self, request, pk):
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
)

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,69 @@
import ipaddress
from rest_framework import exceptions
from rest_framework.authentication import SessionAuthentication
from spejstore.settings import (
LAN_ALLOWED_ADDRESS_SPACE,
)
headers_to_check_for_ip = [
"HTTP_X_FORWARDED_FOR",
"X_FORWARDED_FOR",
"HTTP_CLIENT_IP",
"HTTP_X_REAL_IP",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
]
def get_request_meta(request, key):
value = request.META.get(key, "")
if value == "":
return None
return value
def get_ip_from_request(request):
for header in headers_to_check_for_ip:
ip = get_request_meta(request, header)
if not ip:
ip = get_request_meta(request, header.replace("_", "-"))
if ip:
return ip
return None
def has_permission(request):
# We don't care if address space is undefined
if LAN_ALLOWED_ADDRESS_SPACE == '':
return (True, '')
client_ip = get_ip_from_request(request)
if client_ip is None:
# This should only happen on localhost env when fiddling with code.
# It's technically impossible to get there with proper headers.
return (False, "Unauthorized: no ip detected?")
in_local_space = ipaddress.IPv4Address(client_ip) in ipaddress.IPv4Network(
LAN_ALLOWED_ADDRESS_SPACE
)
if not in_local_space:
return (False, "Unauthorized: " + client_ip + " not in subnet of " + LAN_ALLOWED_ADDRESS_SPACE)
return (True, '')
class LanAuthentication(SessionAuthentication):
def authenticate(self, request):
is_session_authorized = super().authenticate(request)
if is_session_authorized:
return is_session_authorized
is_authorized, error_message = has_permission(request)
if is_authorized:
user = getattr(request._request, "user", None)
return (user, "authorized")
else:
raise exceptions.AuthenticationFailed(
error_message
)

Wyświetl plik

@ -3,28 +3,29 @@ from storage.models import Item
from io import StringIO
import csv
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):
parser.add_argument('parent')
parser.add_argument('file')
parser.add_argument("parent")
parser.add_argument("file")
def handle(self, *args, **options):
with open(options['file']) as fd:
with open(options["file"]) as fd:
sio = StringIO(fd.read())
reader = csv.reader(sio, delimiter='|')
parent = Item.objects.get(pk=options['parent'])
reader = csv.reader(sio, delimiter="|")
parent = Item.objects.get(pk=options["parent"])
for line in reader:
line = list(map(str.strip, line))
item = Item(parent=parent)
item.name = line[2]
item.props['author'] = line[1]
item.props['owner'] = line[3]
item.props['can_borrow'] = line[4]
item.props['borrowed_by'] = line[5]
item.props["author"] = line[1]
item.props["owner"] = line[3]
item.props["can_borrow"] = line[4]
item.props["borrowed_by"] = line[5]
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"))

Wyświetl plik

@ -0,0 +1,32 @@
from storage.authentication import has_permission
from django.http import HttpResponseRedirect
from spejstore.settings import STATIC_URL, MEDIA_URL, LOGIN_URL
from django.core.exceptions import PermissionDenied
def is_authorized_or_in_lan_middleware(get_response):
# One-time configuration and initialization.
login_paths_to_ignore = [
"/login",
LOGIN_URL[:-1],
STATIC_URL[:-1],
MEDIA_URL[:-1],
"/admin/static",
"/complete",
"/favicon.ico",
"/api/1",
]
def middleware(request):
if request.user.is_authenticated:
return get_response(request)
is_within_lan, error_message = has_permission(request)
if is_within_lan:
return get_response(request)
else:
for login_path in login_paths_to_ignore:
if request.path.startswith(login_path):
return get_response(request)
else:
raise PermissionDenied()
return middleware

Wyświetl plik

@ -3,24 +3,34 @@
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):
initial = True
dependencies = [
]
dependencies = []
operations = [
HStoreExtension(),
TrigramExtension(),
migrations.CreateModel(
name='Item',
name="Item",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
('description', models.TextField()),
('props', django_hstore.fields.DictionaryField()),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField()),
("description", models.TextField()),
("props", HStoreField()),
],
),
]

Wyświetl plik

@ -5,13 +5,25 @@ 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
class Migration(migrations.Migration):
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')]
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"),
]
initial = True
@ -20,63 +32,117 @@ class Migration(migrations.Migration):
]
operations = [
HStoreExtension(),
TrigramExtension(),
migrations.CreateModel(
name='Item',
name="Item",
fields=[
('name', models.TextField()),
('description', models.TextField(blank=True)),
('props', django_hstore.fields.DictionaryField()),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("name", models.TextField()),
("description", models.TextField(blank=True)),
("props", HStoreField()),
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
],
),
migrations.CreateModel(
name='ItemImage',
name="ItemImage",
fields=[
('id', 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')),
(
"id",
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(
name='Category',
name="Category",
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(
model_name='item',
name='categories',
field=models.ManyToManyField(to='storage.Category'),
model_name="item",
name="categories",
field=models.ManyToManyField(to="storage.Category"),
),
migrations.AddField(
model_name='item',
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),
model_name="item",
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,
),
),
migrations.AddField(
model_name='item',
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),
model_name="item",
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,
),
),
migrations.AddField(
model_name='item',
name='taken_on',
model_name="item",
name="taken_on",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='taken_until',
model_name="item",
name="taken_until",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='item',
name='description',
model_name="item",
name="description",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='state',
field=models.CharField(choices=[('present', 'Present'), ('taken', 'Taken'), ('broken', 'Broken'), ('missing', 'Missing')], default='present', max_length=31),
model_name="item",
name="state",
field=models.CharField(
choices=[
("present", "Present"),
("taken", "Taken"),
("broken", "Broken"),
("missing", "Missing"),
],
default="present",
max_length=31,
),
),
]

Wyświetl plik

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

Wyświetl plik

@ -4,40 +4,46 @@ 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):
dependencies = [
('storage', '0002_auto_20170215_0115'),
("storage", "0002_auto_20170215_0115"),
]
operations = [
migrations.CreateModel(
name='Label',
name="Label",
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(
name='item',
options={'ordering': ('path',)},
name="item",
options={"ordering": ("path",)},
),
migrations.AlterField(
model_name='item',
name='categories',
field=models.ManyToManyField(blank=True, to='storage.Category'),
model_name="item",
name="categories",
field=models.ManyToManyField(blank=True, to="storage.Category"),
),
migrations.AlterField(
model_name='item',
name='props',
field=django_hstore.fields.DictionaryField(blank=True),
model_name="item",
name="props",
field=HStoreField(blank=True),
),
migrations.AddField(
model_name='label',
name='item',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='labels', to='storage.Item'),
model_name="label",
name="item",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="labels",
to="storage.Item",
),
),
]

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,22 @@
# Generated by Django 3.2.20 on 2023-07-11 19:35
from django.db import migrations
import tree.fields
from tree.operations import (
RebuildPaths,
)
class Migration(migrations.Migration):
dependencies = [
("storage", "0009_migrate_tree_fields"),
]
operations = [
migrations.AlterField(
model_name="item",
name="path",
field=tree.fields.PathField(),
),
RebuildPaths("item"),
]

Wyświetl plik

@ -0,0 +1,42 @@
# Generated by Django 3.2.20 on 2023-07-20 12:40
from django.db import migrations, models
import django.db.models.deletion
import storage.models
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('storage', '0010_alter_item_path'),
]
operations = [
migrations.CreateModel(
name='StaffProxyModel',
fields=[
],
options={
'verbose_name': 'User',
'verbose_name_plural': 'Users',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('auth.user',),
managers=[
('objects', storage.models.StaffManager()),
],
),
migrations.AlterField(
model_name='item',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_items', to='storage.staffproxymodel'),
),
migrations.AlterField(
model_name='item',
name='taken_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='taken_items', to='storage.staffproxymodel'),
),
]

Wyświetl plik

@ -0,0 +1,17 @@
# Generated by Django 3.2.20 on 2023-12-03 18:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("storage", "0011_auto_20230720_1240"),
]
operations = [
migrations.AddField(
model_name="item",
name="wiki_link",
field=models.TextField(blank=True, null=True),
),
]

Wyświetl plik

@ -0,0 +1,19 @@
# Generated by Django 5.0.1 on 2024-01-14 20:48
import storage.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('storage', '0012_item_wiki_link'),
]
operations = [
migrations.AlterField(
model_name='itemimage',
name='image',
field=storage.models.ImageFieldWithUuid(upload_to=''),
),
]

Wyświetl plik

@ -1,4 +1,5 @@
from __future__ import unicode_literals
import os
import uuid
import re
@ -6,22 +7,36 @@ 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
from django.contrib.auth.models import UserManager
import requests
STATES = (
('present', 'Present'),
('taken', 'Taken'),
('broken', 'Broken'),
('missing', 'Missing'),
('depleted', 'Depleted'),
("present", "Present"),
("taken", "Taken"),
("broken", "Broken"),
("missing", "Missing"),
("depleted", "Depleted"),
)
def api_print(item):
resp = requests.post(
"{}/api/2/print".format(settings.LABEL_API),
params={
"id": item.short_id(),
"name": item.name,
"owner": item.owner,
}
)
resp.raise_for_status()
class Category(models.Model):
name = models.CharField(max_length=127)
@ -31,7 +46,7 @@ class Category(models.Model):
return self.name
class Meta:
ordering = ['name']
ordering = ["name"]
verbose_name_plural = "categories"
@ -42,42 +57,68 @@ class Category(models.Model):
# also qrcody w stylu //s/ID (żeby się resolvowało w sieci lokalnej)
# Also ID zawierające część name
class StaffManager(UserManager):
pass
class StaffProxyModel(User):
objects = StaffManager()
class Meta:
proxy = True
verbose_name = "User"
verbose_name_plural = "Users"
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()
wiki_link = models.TextField(null=True, blank=True)
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(
StaffProxyModel,
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')
taken_by = models.ForeignKey(
StaffProxyModel,
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 :)
return str(self.pk)[:8] # collisions? what collisions?
return str(self.pk)[:8] # collisions? what collisions?
def __str__(self):
return '- ' * (self.get_level() or 0) + self.name
return "- " * (self.get_level() or 0) + self.name
def get_absolute_url(self):
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):
defaults = {
'id': re.sub('[^A-Z0-9]', '', self.name.upper())[:16],
}
"id": re.sub("[^A-Z0-9]", "", self.name.upper())[:16],
}
defaults.update(kwargs)
@ -90,35 +131,37 @@ class Item(models.Model, TreeModelMixin):
return next((c for c in self.categories.all() if c.icon_id), None)
def print(self):
# todo: deduplicate
resp = requests.post(
'{}/api/1/print/{}'.format(settings.LABEL_API, self.short_id()))
resp.raise_for_status()
api_print(self)
class Meta:
ordering = ('path',)
ordering = ("path",)
class ImageFieldWithUuid(models.ImageField):
def generate_filename(self, instance, filename):
ext = filename.split(".")[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
return super().generate_filename(instance, filename)
class ItemImage(models.Model):
item = models.ForeignKey(Item, related_name='images')
image = models.ImageField()
item = models.ForeignKey(Item, related_name="images", on_delete=models.CASCADE)
image = ImageFieldWithUuid()
def __str__(self):
return '{}'.format(self.image.name)
return "{}".format(self.image.name)
# Deprecated, left in db due to legacy reasons
class Label(models.Model):
id = models.CharField(max_length=64, primary_key=True)
item = models.ForeignKey(Item, related_name='labels')
style = models.CharField(max_length=32, choices=(
('basic_99012_v1', 'Basic Dymo 89x36mm label'),
), default='basic_99012_v1')
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"),),
default="basic_99012_v1",
)
created = models.DateTimeField(auto_now_add=True, blank=True)
def __str__(self):
return '{}'.format(self.id)
def print(self):
resp = requests.post(
'{}/api/1/print/{}'.format(settings.LABEL_API, self.id))
resp.raise_for_status()
return "{}".format(self.id)

Wyświetl plik

@ -1,20 +1,40 @@
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):
categories = serializers.SlugRelatedField(queryset=Category.objects, many=True, slug_field='name')
owner = serializers.SlugRelatedField(queryset=User.objects, slug_field='username')
taken_by = serializers.SlugRelatedField(queryset=User.objects, slug_field='username')
class ItemSerializer(serializers.ModelSerializer):
categories = serializers.SlugRelatedField(
queryset=Category.objects, many=True, slug_field="name"
)
owner = serializers.SlugRelatedField(queryset=User.objects, slug_field="username")
taken_by = serializers.SlugRelatedField(
queryset=User.objects, slug_field="username"
)
class Meta:
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):
item = ItemSerializer(required=False)
item_id = serializers.PrimaryKeyRelatedField(queryset=Item.objects, source='item')
item_id = serializers.PrimaryKeyRelatedField(queryset=Item.objects, source="item")
class Meta:
model = Label
fields = ('id', 'item', 'item_id', 'style')
fields = ("id", "item", "item_id", "style")

Wyświetl plik

@ -14,22 +14,33 @@
{% block content %}{{ block.super }}
<script>
$(function() {
function fmt (state) {
if (!state.id) {
return state.text;
}
var result = $('<div><div><small></small></div><b></b></div>');
result.find('small').text(state.path.join(' → ')).css({
'opacity': 0.6,
'letter-spacing': -0.5
})
result.find('b').text(state.text)
return result;
};
$('.django-select2[name=parent]').djangoSelect2({
templateResult: fmt,
django.jQuery(function () {
function fmt(state) {
if (!state.id) {
return state.text;
}
var result = django.jQuery(
"<div><div><small></small></div><b></b></div>"
);
result.find("small").text(state.path.join(" → ")).css({
opacity: 0.6,
"letter-spacing": -0.5,
});
result.find("b").text(state.text);
return result;
}
django.jQuery(".django-select2[name=parent]").select2({
templateResult: fmt,
});
});
/**
* Extremely ugly hack to make sure autocomplete loads for the props
* There's like 4 select2 versions and jQuery versions.
* I give up trying to make it work in a better way.
* This is good enough.
*/
setTimeout(function () {
django.jQuery(".hstore-toggle-txtarea").click().click();
}, 100);
});
</script>
{% endblock %}

Wyświetl plik

@ -10,8 +10,18 @@
</ol>
<h2>
{% include "widgets/categoryicon.html" with category=item.primary_category %}
{{ item.name }}
{% if item.wiki_link %}
<a href="{{item.wiki_link}}" target="_blank">{{ item.name }}</a>
{% else %}
{{ item.name }}
{% endif %}
</h2>
{% if item.wiki_link %}
<span>Click <a href="{{item.wiki_link}}" target="_blank">HERE</a> or title for wiki</span>
{% endif %}
<div class="row">
<div class="col-md-4">

Wyświetl plik

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

Wyświetl plik

@ -13,29 +13,30 @@ from storage.models import Item, Label
from django.contrib.auth.decorators import login_required
from rest_framework.authtoken.models import Token
def apply_smart_search(query, objects):
general_term = []
filters = {}
for prop in shlex.split(query):
if ':' not in prop:
if ":" not in prop:
general_term.append(prop)
else:
key, value = prop.split(':', 1)
if key in ['owner', 'taken_by']:
filters[key + '__username'] = value
key, value = prop.split(":", 1)
if key in ["owner", "taken_by"]:
filters[key + "__username"] = value
elif hasattr(Item, key):
filters[key + '__search'] = value
elif key == 'ancestor':
filters[key + "__search"] = value
elif key == "ancestor":
objects = Item.objects.get(pk=value).get_children()
elif key == 'prop' or value:
if key == 'prop':
key, _, value = value.partition(':')
elif key == "prop" or value:
if key == "prop":
key, _, value = value.partition(":")
if not value:
filters['props__isnull'] = {key: False}
filters["props__isnull"] = {key: False}
else:
filters['props__contains'] = {key: value}
filters["props__contains"] = {key: value}
else:
# "Whatever:"
general_term.append(prop)
@ -44,35 +45,42 @@ def apply_smart_search(query, objects):
if not general_term:
return objects
general_term = ' '.join(general_term)
general_term = " ".join(general_term)
objects = objects.annotate(
search=SearchVector('name', 'description', 'props', config='simple'),
similarity=TrigramSimilarity('name', general_term)
).filter(
Q(similarity__gte=0.15) | Q(search__contains=general_term)
).order_by('-similarity')
objects = (
objects.annotate(
search=SearchVector("name", "description", "props", config="simple"),
similarity=TrigramSimilarity("name", general_term),
)
.filter(Q(similarity__gte=0.15) | Q(search__contains=general_term))
.order_by("-similarity")
)
return objects
def index(request):
return render(request, 'results.html', {
'results': Item.get_roots()
})
# get_roots was removed, so we're doing it this way now.
return render(
request, "results.html", {"results": Item.objects.filter(**{"path__level": 1})}
)
def search(request):
query = request.GET.get('q', '')
query = request.GET.get("q", "")
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 render(request, 'results.html', {
'query': query,
'results': results,
})
return render(
request,
"results.html",
{
"query": query,
"results": results,
},
)
def item_display(request, pk):
@ -83,18 +91,22 @@ def item_display(request, pk):
labels = item.labels.all()
has_one_label = len(labels) == 1
return render(request, 'item.html', {
'title': item.name,
'item': item,
'categories': item.categories.all(),
'props': sorted(item.props.items()),
'images': item.images.all(),
'labels': labels,
'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'),
})
return render(
request,
"item.html",
{
"title": item.name,
"item": item,
"categories": item.categories.all(),
"props": sorted(item.props.items()),
"images": item.images.all(),
"labels": labels,
"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):
@ -110,53 +122,59 @@ def label_lookup(request, pk):
raise Http404("Very sad to say, I could not find this thing")
@login_required
def apitoken(request):
print(Token)
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):
def get(self, request, *args, **kwargs):
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)
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': obj.name,
'path': [o.name for o in obj.get_ancestors()],
'id': obj.pk,
}
for obj in context['object_list']
return JsonResponse(
{
"results": [
{
"text": obj.name,
"path": [o.name for o in obj.get_ancestors()],
"id": obj.pk,
}
for obj in context["object_list"]
],
'more': context['page_obj'].has_next()
})
"more": context["page_obj"].has_next(),
}
)
class PropSelectView(AutoResponseView):
def get(self, request, *args, **kwargs):
# 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()
with connection.cursor() as c:
c.execute("""
c.execute(
"""
SELECT key, count(*) FROM
(SELECT (each(props)).key FROM storage_item) AS stat
WHERE key like %s
GROUP BY key
ORDER BY count DESC, key
limit 10;
""", ['%' + self.term + '%'])
""",
["%" + self.term + "%"],
)
props = [e[0] for e in c.fetchall()]
return JsonResponse({
'results': [
{
'text': p,
'id': p,
}
for p in props
return JsonResponse(
{
"results": [
{
"text": p,
"id": p,
}
for p in props
],
})
}
)

Wyświetl plik

@ -1,50 +1,60 @@
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
attrs['class'] = 'hstore-original-textarea'
w = HeavySelect2Widget(data_view='prop-complete', attrs={'data-tags': 'true', 'class': 'hs-key'})
attrs["class"] = "hstore-original-textarea"
w = HeavySelect2Widget(
data_view="prop-complete", attrs={"data-tags": "true", "class": "hs-key"}
)
# 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,
'STATIC_URL': settings.STATIC_URL,
'use_svg': parse_version(get_version()) >= parse_version('1.9'), # use svg icons if django >= 1.9
'ajax_url': reverse('prop-complete'),
'w': w.build_attrs(base_attrs=w.attrs)
"field_name": name,
"STATIC_URL": settings.STATIC_URL,
"use_svg": parse_version(get_version())
>= parse_version("1.9"), # use svg icons if django >= 1.9
"ajax_url": reverse("prop-complete"),
"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)

Wyświetl plik

@ -0,0 +1,5 @@
{% extends "error_template.html" %}
{% block content %}
<div>400</div>
<div class="txt">Invalid request<span class="blink">_</span></div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "error_template.html" %}
{% block content %}
<div>403</div>
<div class="txt">Forbidden<span class="blink">_</span></div>
<div><a href="/admin/login">Login</a></div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,8 @@
{% extends "error_template.html" %}
{% block content %}
<div>404</div>
<div class="txt">Not found<span class="blink">_</span></div>
{% endblock %}

Wyświetl plik

@ -0,0 +1,8 @@
{% extends "error_template.html" %}
{% block content %}
<div>500</div>
<div class="txt">Something went wrong...<span class="blink">_</span></div>
{% endblock %}

Wyświetl plik

@ -2,50 +2,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta charset="utf-8" />
<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" />
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}" media="screen">
<link rel="stylesheet" href="{% static 'css/custom.css' %}" media="screen">
<link
rel="stylesheet"
href="{% static 'css/bootstrap.css' %}"
media="screen"
/>
<link
rel="stylesheet"
href="{% static 'css/theme.min.css' %}"
media="(prefers-color-scheme: dark)"
/>
<link
rel="stylesheet"
href="{% static 'css/custom.css' %}"
media="screen"
/>
</head>
<body>
{% block body %}
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Hackerspace Storage</a>
</div>
<div class="container">
<div class="navbar-header">
<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="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Hackerspace Storage</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<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>
</ul>
<form class="navbar-form navbar-right" role="search" action="/search">
<div class="form-group">
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<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>
</ul>
<form class="navbar-form navbar-right" role="search" action="/search">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" placeholder="Search Hackerspace" name="q" autofocus>
<span class="input-group-btn">
<button class="btn btn-primary" type="submit"><i class="glyphicon glyphicon-search"></i></button>
</span>
<input
type="text"
class="form-control"
placeholder="Search Hackerspace"
name="q"
autofocus
/>
<span class="input-group-btn">
<button class="btn btn-primary" type="submit">
<i class="glyphicon glyphicon-search"></i>
</button>
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</form>
</div>
</div>
</nav>
<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>-->
{% block content %}
{% endblock %}
{% block content %} {% endblock %}
</div>
{% endblock %}
<script src="{% static 'js/jquery.min.js' %}"></script>

Wyświetl plik

@ -0,0 +1,81 @@
{% load static %}
<!DOCTYPE html>
<style>
@font-face {
font-family: "Press Start 2P";
src: url("{% static 'fonts/pressstart2p-regular-webfont.woff2' %}")
format("woff2"),
url("{% static 'fonts/pressstart2p-regular-webfont.woff' %}") format("woff");
font-weight: normal;
font-style: normal;
}
:root {
--color: #54fe55;
--glowSize: 10px;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
}
* {
font-family: "Press Start 2P", cursive;
box-sizing: border-box;
}
#app {
padding: 1rem;
background: black;
display: flex;
height: 100%;
justify-content: center;
align-items: center;
color: var(--color);
text-shadow: 0px 0px var(--glowSize);
font-size: 6rem;
flex-direction: column;
.txt {
font-size: 1.8rem;
}
}
@keyframes blink {
0% {
opacity: 0;
}
49% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 1;
}
}
.blink {
animation-name: blink;
animation-duration: 1s;
animation-iteration-count: infinite;
}
</style>
<html lang="en">
<div id="app">
{% block content %}
<div>MISSING_ERROR_CODE</div>
<div class="txt">
MISSING ERROR DESCRIPTION <span class="blink">_</span>
</div>
{% endblock %}
</div>
</html>

Wyświetl plik

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

Wyświetl plik

@ -1,3 +1,5 @@
{% load static %}
{% if category and category.icon_id %}
<div class="containericon" title="{{ category.name }}"><img src="/static/icons/{{ category.icon_id }}.svg" /></div>
<div class="containericon" title="{{ category.name }}"><img src="{% static 'icons/' %}{{ category.icon_id }}.svg" />
</div>
{% endif %}