Application Icon upload (#341)
* core: add initial implementation for File Upload * root: add volumes to docker-compose for file upload * helm: add pvc for uploads * core: allow meta_icon to be overwritten with static files
This commit is contained in:
parent
91e9f176a5
commit
665839133f
|
@ -197,5 +197,6 @@ local.env.yml
|
||||||
**/charts/*.tgz
|
**/charts/*.tgz
|
||||||
|
|
||||||
# Selenium Screenshots
|
# Selenium Screenshots
|
||||||
selenium_screenshots/**
|
selenium_screenshots/
|
||||||
backups/
|
backups/
|
||||||
|
media/
|
||||||
|
|
|
@ -25,6 +25,8 @@ services:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
ports:
|
ports:
|
||||||
- 8000
|
- 8000
|
||||||
networks:
|
networks:
|
||||||
|
@ -60,11 +62,13 @@ services:
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: 'true'
|
traefik.enable: 'true'
|
||||||
traefik.docker.network: internal
|
traefik.docker.network: internal
|
||||||
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/robots.txt`, `/favicon.ico`)
|
traefik.http.routers.static-router.rule: PathPrefix(`/static`, `/media`, `/robots.txt`, `/favicon.ico`)
|
||||||
traefik.http.routers.static-router.tls: 'true'
|
traefik.http.routers.static-router.tls: 'true'
|
||||||
traefik.http.routers.static-router.service: static-service
|
traefik.http.routers.static-router.service: static-service
|
||||||
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
|
traefik.http.services.static-service.loadbalancer.healthcheck.path: /
|
||||||
traefik.http.services.static-service.loadbalancer.server.port: '80'
|
traefik.http.services.static-service.loadbalancer.server.port: '80'
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:2.3
|
image: traefik:2.3
|
||||||
command:
|
command:
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ include "passbook.fullname" . }}-uploads
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||||
|
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
|
@ -48,3 +48,10 @@ spec:
|
||||||
limits:
|
limits:
|
||||||
cpu: 20m
|
cpu: 20m
|
||||||
memory: 20M
|
memory: 20M
|
||||||
|
volumeMounts:
|
||||||
|
- name: passbook-uploads
|
||||||
|
mountPath: /usr/share/nginx/html/media
|
||||||
|
volumes:
|
||||||
|
- name: passbook-uploads
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "passbook.fullname" . }}-uploads
|
||||||
|
|
|
@ -90,6 +90,9 @@ spec:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: "{{ .Release.Name }}-postgresql"
|
name: "{{ .Release.Name }}-postgresql"
|
||||||
key: "postgresql-password"
|
key: "postgresql-password"
|
||||||
|
volumeMounts:
|
||||||
|
- name: passbook-uploads
|
||||||
|
mountPath: /media
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8000
|
containerPort: 8000
|
||||||
|
@ -115,3 +118,7 @@ spec:
|
||||||
limits:
|
limits:
|
||||||
cpu: 300m
|
cpu: 300m
|
||||||
memory: 500M
|
memory: 500M
|
||||||
|
volumes:
|
||||||
|
- name: passbook-uploads
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ include "passbook.fullname" . }}-uploads
|
||||||
|
|
|
@ -22,7 +22,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||||
"slug",
|
"slug",
|
||||||
"provider",
|
"provider",
|
||||||
"meta_launch_url",
|
"meta_launch_url",
|
||||||
"meta_icon_url",
|
"meta_icon",
|
||||||
"meta_description",
|
"meta_description",
|
||||||
"meta_publisher",
|
"meta_publisher",
|
||||||
"policies",
|
"policies",
|
||||||
|
|
|
@ -23,14 +23,13 @@ class ApplicationForm(forms.ModelForm):
|
||||||
"slug",
|
"slug",
|
||||||
"provider",
|
"provider",
|
||||||
"meta_launch_url",
|
"meta_launch_url",
|
||||||
"meta_icon_url",
|
"meta_icon",
|
||||||
"meta_description",
|
"meta_description",
|
||||||
"meta_publisher",
|
"meta_publisher",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
"meta_launch_url": forms.TextInput(),
|
"meta_launch_url": forms.TextInput(),
|
||||||
"meta_icon_url": forms.TextInput(),
|
|
||||||
"meta_publisher": forms.TextInput(),
|
"meta_publisher": forms.TextInput(),
|
||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
|
@ -44,7 +43,7 @@ class ApplicationForm(forms.ModelForm):
|
||||||
field_classes = {"provider": GroupedModelChoiceField}
|
field_classes = {"provider": GroupedModelChoiceField}
|
||||||
labels = {
|
labels = {
|
||||||
"meta_launch_url": _("Launch URL"),
|
"meta_launch_url": _("Launch URL"),
|
||||||
"meta_icon_url": _("Icon URL"),
|
"meta_icon": _("Icon"),
|
||||||
"meta_description": _("Description"),
|
"meta_description": _("Description"),
|
||||||
"meta_publisher": _("Publisher"),
|
"meta_publisher": _("Publisher"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.1.3 on 2020-11-23 17:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_core", "0014_auto_20201018_1158"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon_url",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="application",
|
||||||
|
name="meta_icon",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True, default="", upload_to="application-icons/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -171,7 +171,8 @@ class Application(PolicyBindingModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
meta_launch_url = models.URLField(default="", blank=True)
|
meta_launch_url = models.URLField(default="", blank=True)
|
||||||
meta_icon_url = models.TextField(default="", blank=True)
|
# For template applications, this can be set to /static/passbook/applications/*
|
||||||
|
meta_icon = models.FileField(upload_to="application-icons/", default="", blank=True)
|
||||||
meta_description = models.TextField(default="", blank=True)
|
meta_description = models.TextField(default="", blank=True)
|
||||||
meta_publisher = models.TextField(default="", blank=True)
|
meta_publisher = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
|
|
|
@ -13,12 +13,12 @@
|
||||||
{% if applications %}
|
{% if applications %}
|
||||||
<div class="pf-l-gallery pf-m-gutter">
|
<div class="pf-l-gallery pf-m-gutter">
|
||||||
{% for app in applications %}
|
{% for app in applications %}
|
||||||
<a href="{{ app.get_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
|
<a href="{{ app.get_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact pb-root-link">
|
||||||
<div class="pf-c-card__header">
|
<div class="pf-c-card__header">
|
||||||
{% if not app.meta_icon_url %}
|
{% if app.meta_icon %}
|
||||||
<i class="pf-icon pf-icon-arrow"></i>
|
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon.url }}" alt="{% trans 'Application Icon' %}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
|
<i class="pf-icon pf-icon-arrow"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-card__title">
|
<div class="pf-c-card__title">
|
|
@ -57,6 +57,26 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% elif field.field.widget|fieldtype == "ClearableFileInput" %}
|
||||||
|
<div class="pf-c-form__group-label">
|
||||||
|
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||||
|
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||||
|
{% if field.field.required %}
|
||||||
|
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-form__group-control">
|
||||||
|
<div class="c-form__horizontal-group">
|
||||||
|
<div class="pf-c-file-upload">
|
||||||
|
<div class="pf-c-file-upload__file-select">
|
||||||
|
<div class="pf-c-input-group">
|
||||||
|
{{ field|css_class:"pf-c-form-control" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="pf-c-form__group-label">
|
<div class="pf-c-form__group-label">
|
||||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"""passbook URL Configuration"""
|
"""passbook URL Configuration"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from passbook.core.views import impersonate, overview, shell, user
|
from passbook.core.views import impersonate, library, shell, user
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("", shell.ShellView.as_view(), name="shell"),
|
||||||
# User views
|
# User views
|
||||||
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
||||||
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
||||||
|
@ -22,9 +23,8 @@ urlpatterns = [
|
||||||
user.TokenDeleteView.as_view(),
|
user.TokenDeleteView.as_view(),
|
||||||
name="user-tokens-delete",
|
name="user-tokens-delete",
|
||||||
),
|
),
|
||||||
# Overview
|
# Libray
|
||||||
path("", shell.ShellView.as_view(), name="shell"),
|
path("-/overview/", library.LibraryView.as_view(), name="overview"),
|
||||||
path("-/overview/", overview.OverviewView.as_view(), name="overview"),
|
|
||||||
# Impersonation
|
# Impersonation
|
||||||
path(
|
path(
|
||||||
"-/impersonation/<int:user_id>/",
|
"-/impersonation/<int:user_id>/",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""passbook overview views"""
|
"""passbook library view"""
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
@ -7,11 +7,11 @@ from passbook.core.models import Application
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
|
||||||
|
|
||||||
class OverviewView(LoginRequiredMixin, TemplateView):
|
class LibraryView(LoginRequiredMixin, TemplateView):
|
||||||
"""Overview for logged in user, incase user opens passbook directly
|
"""Overview for logged in user, incase user opens passbook directly
|
||||||
and is not being forwarded"""
|
and is not being forwarded"""
|
||||||
|
|
||||||
template_name = "overview/index.html"
|
template_name = "library.html"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["applications"] = []
|
kwargs["applications"] = []
|
|
@ -48,6 +48,7 @@ LOGGER = structlog.get_logger()
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
STATIC_ROOT = BASE_DIR + "/static"
|
STATIC_ROOT = BASE_DIR + "/static"
|
||||||
|
MEDIA_ROOT = BASE_DIR + "/media"
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
|
||||||
|
@ -338,6 +339,7 @@ if not DEBUG and _ERROR_REPORTING:
|
||||||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
|
MEDIA_URL = "/"
|
||||||
|
|
||||||
|
|
||||||
structlog.configure_once(
|
structlog.configure_once(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""passbook URL Configuration"""
|
"""passbook URL Configuration"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
@ -60,4 +61,10 @@ urlpatterns += [
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
|
|
||||||
urlpatterns = [path("-/debug/", include(debug_toolbar.urls))] + urlpatterns
|
urlpatterns = (
|
||||||
|
[
|
||||||
|
path("-/debug/", include(debug_toolbar.urls)),
|
||||||
|
]
|
||||||
|
+ static("/media/", document_root=settings.MEDIA_ROOT)
|
||||||
|
+ urlpatterns
|
||||||
|
)
|
||||||
|
|
|
@ -6498,9 +6498,11 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
maxLength: 200
|
maxLength: 200
|
||||||
meta_icon_url:
|
meta_icon:
|
||||||
title: Meta icon url
|
title: Meta icon
|
||||||
type: string
|
type: string
|
||||||
|
readOnly: true
|
||||||
|
format: uri
|
||||||
meta_description:
|
meta_description:
|
||||||
title: Meta description
|
title: Meta description
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -4,6 +4,14 @@ title: Kubernetes installation
|
||||||
|
|
||||||
For a mid to high-load installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
For a mid to high-load installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||||
|
|
||||||
|
To install passbook using the helm chart, run these commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
helm repo add passbook https://docker.beryju.org/chartrepo/passbook
|
||||||
|
helm repo update
|
||||||
|
helm repo install passbook/passbook --devel -f values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
title: Upgrading to 0.13
|
||||||
|
---
|
||||||
|
|
||||||
|
**WIP**
|
||||||
|
|
||||||
|
# TODO: Changelog for 0.13
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
### docker-compose
|
||||||
|
|
||||||
|
Docker-compose users should download the latest docker-compose file from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml).
|
||||||
|
|
||||||
|
This includes a new shared volume, which is used for file Uploads.
|
||||||
|
|
||||||
|
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
The Helm chart contains a new PVC which is used to store all the files uploaded by users. This PVC is shared between the Server pods and the static pods.
|
Reference in New Issue