kopia lustrzana https://github.com/HackerspaceKRK/spejstore
Porównaj commity
118 Commity
8537d13e4d
...
29827264f8
Autor | SHA1 | Data |
---|---|---|
Wiktor Przybylski | 29827264f8 | |
Wiktor Przybylski | 6c318019a2 | |
Wiktor Przybylski | 178da60c1c | |
Wiktor Przybylski | a2cfd4dbc4 | |
Wiktor Przybylski | 4a68ebff10 | |
Wiktor Przybylski | 70f5056c0d | |
Wiktor Przybylski | 37b0eb5f9c | |
Wiktor Przybylski | 0b99716566 | |
Wiktor Przybylski | 5f95e25bc4 | |
Wiktor Przybylski | 7276f56aa2 | |
Wiktor | 69a107352d | |
Wiktor | d9df9f7bfe | |
Dariusz Niemczyk | 9983511136 | |
Dariusz Niemczyk | dad68031df | |
Dariusz Niemczyk | 1dc6d8b76d | |
Dariusz Niemczyk | 8812e6c0d3 | |
Dariusz Niemczyk | b94ab204d8 | |
Dariusz Niemczyk | 47b682c509 | |
Dariusz Niemczyk | 70fc374d0d | |
Dariusz Niemczyk | 77ddd6bb45 | |
radex | 8ddc4da3b6 | |
Dariusz Niemczyk | 23c008914b | |
radex | 3c22a7b8c1 | |
radex | b32e5a15fc | |
radex | c0d805781b | |
radex | cba1e2fd86 | |
Dariusz Niemczyk | a991063084 | |
Dariusz Niemczyk | 5bf31f26a1 | |
Patryk Jakuszew | 1bbd28933f | |
Dariusz Niemczyk | 650fc34115 | |
Dariusz Niemczyk | ab631aeb90 | |
Dariusz Niemczyk | 81417f58be | |
Dariusz Niemczyk | 6c81441f00 | |
Dariusz Niemczyk | d36a84e25b | |
Dariusz Niemczyk | 67ac858323 | |
Dariusz Niemczyk | fe496e85f8 | |
Dariusz Niemczyk | ea5e223fcc | |
Dariusz Niemczyk | 2323263ccb | |
Dariusz Niemczyk | f5740e1543 | |
Dariusz Niemczyk | c9be16b76f | |
Dariusz Niemczyk | 35a48c3eee | |
Dariusz Niemczyk | 6af75328f4 | |
Dariusz Niemczyk | c3c80d650c | |
Dariusz Niemczyk | 142e38ad95 | |
Dariusz Niemczyk | f94e7b3207 | |
Dariusz Niemczyk | ee2c93908a | |
Dariusz Niemczyk | d1b9beca6d | |
Dariusz Niemczyk | 1fcbbd9dd3 | |
Dariusz Niemczyk | 2892923389 | |
Dariusz Niemczyk | d71885d264 | |
Dariusz Niemczyk | f1143dc4f1 | |
Dariusz Niemczyk | 38c7245a3f | |
Dariusz Niemczyk | 156df0a8a5 | |
Dariusz Niemczyk | f8b3dd6bf7 | |
Dariusz Niemczyk | 3c3ba16811 | |
Dariusz Niemczyk | f1335f0565 | |
Dariusz Niemczyk | daea8dda22 | |
Dariusz Niemczyk | f92635f5f3 | |
Dariusz Niemczyk | 8ce869393e | |
Dariusz Niemczyk | 0fa9762bea | |
Dariusz Niemczyk | 15bf813b04 | |
Dariusz Niemczyk | 401fcc088d | |
Dariusz Niemczyk | 820f04cc01 | |
Dariusz Niemczyk | c5a9fba034 | |
radex | 7f19fe7c7a | |
radex | cc3fddfd22 | |
radex | 20303a14a7 | |
radex | cc6f00da08 | |
radex | 23244bdf24 | |
radex | ee9e9becf5 | |
radex | 6cd7ec529b | |
radex | d615da3a0f | |
radex | 4fc47030db | |
radex | ce3f07de5f | |
radex | e3229ffd7e | |
radex | d4a305c362 | |
radex | 255e0f0d08 | |
radex | af9cb2db32 | |
Dariusz Niemczyk | 9200bdbb3b | |
Dariusz Niemczyk | d942c99cb9 | |
Dariusz Niemczyk | e1a22100c4 | |
Dariusz Niemczyk | 4fc3629dcd | |
Dariusz Niemczyk | 875e385f68 | |
Dariusz Niemczyk | 8048fccede | |
Dariusz Niemczyk | 3a286a5bc6 | |
Dariusz Niemczyk | 2a70d2cb31 | |
Dariusz Niemczyk | ae219a2533 | |
Dariusz Niemczyk | d026e41ac5 | |
Dariusz Niemczyk | a6705a956f | |
Dariusz Niemczyk | 5012a10298 | |
Dariusz Niemczyk | 027bcfcde5 | |
Dariusz Niemczyk | 154e1079da | |
Dariusz Niemczyk | 30c3c3eb7a | |
Dariusz Niemczyk | c15f1bb840 | |
Dariusz Niemczyk | f7688262e4 | |
Dariusz Niemczyk | d2e25c0801 | |
Dariusz Niemczyk | e2e82b1a2e | |
Dariusz Niemczyk | 8210381027 | |
Dariusz Niemczyk | b09016ea3b | |
Dariusz Niemczyk | af85d191ad | |
Dariusz Niemczyk | a20e14a8d3 | |
Dariusz Niemczyk | b6ce1516d2 | |
Dariusz Niemczyk | a8f7530263 | |
Dariusz Niemczyk | b74c1b3c8f | |
Dariusz Niemczyk | bfe9d27d71 | |
Dariusz Niemczyk | ad73094b67 | |
Dariusz Niemczyk | a0c6d87adb | |
Dariusz Niemczyk | 5ed4128151 | |
Dariusz Niemczyk | d473901f8c | |
Dariusz Niemczyk | 878f246b08 | |
Dariusz Niemczyk | af1be4aca7 | |
Dariusz Niemczyk | 72e668622d | |
Dariusz Niemczyk | 150c405468 | |
Dariusz Niemczyk | efcd932481 | |
Dariusz Niemczyk | 837734a655 | |
Dariusz Niemczyk | 45ad9bf88c | |
Dariusz Niemczyk | 659f04ce9c | |
Dariusz Niemczyk | 3fdf788168 |
|
@ -0,0 +1,2 @@
|
|||
\.*
|
||||
!.gitignore
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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 }}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
15
Dockerfile
15
Dockerfile
|
@ -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"
|
||||
|
|
59
README.md
59
README.md
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
def auth_redirect(request):
|
||||
return redirect('social:begin', 'hswaw')
|
||||
return redirect("social:begin", "hswaw")
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
version: "3"
|
||||
services:
|
||||
web:
|
||||
environment:
|
||||
- SPEJSTORE_ENV=dev
|
||||
- SPEJSTORE_ALLOWED_HOSTS=localhost,127.0.0.1
|
|
@ -1,6 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
db:
|
||||
volumes:
|
||||
- /var/spejstore-data:/var/lib/postgresql/data
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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", "")
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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
|
@ -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
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
105
storage/admin.py
105
storage/admin.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -4,4 +4,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class StorageConfig(AppConfig):
|
||||
name = 'storage'
|
||||
name = "storage"
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
|
@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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(),
|
||||
]
|
|
@ -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"),
|
||||
]
|
|
@ -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"),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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=''),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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")),
|
||||
]
|
||||
|
|
144
storage/views.py
144
storage/views.py
|
@ -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
|
||||
],
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "error_template.html" %}
|
||||
{% block content %}
|
||||
<div>400</div>
|
||||
<div class="txt">Invalid request<span class="blink">_</span></div>
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -0,0 +1,8 @@
|
|||
{% extends "error_template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div>404</div>
|
||||
<div class="txt">Not found<span class="blink">_</span></div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -66,5 +66,7 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
django.jQuery(function() { initDjangoHStoreWidget('{{ field_name }}') });
|
||||
window.addEventListener("load", function (event) {
|
||||
django.jQuery(function() { initDjangoHStoreWidget('{{ field_name }}') });
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Ładowanie…
Reference in New Issue