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:
Jens L 2020-11-23 20:50:19 +01:00 committed by GitHub
parent 91e9f176a5
commit 665839133f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 139 additions and 21 deletions

3
.gitignore vendored
View File

@ -197,5 +197,6 @@ local.env.yml
**/charts/*.tgz **/charts/*.tgz
# Selenium Screenshots # Selenium Screenshots
selenium_screenshots/** selenium_screenshots/
backups/ backups/
media/

View File

@ -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:

15
helm/templates/pvc.yaml Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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">&#42;</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 }}">

View File

@ -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>/",

View File

@ -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"] = []

View File

@ -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(

View File

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

View File

@ -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

View File

@ -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

View File

@ -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.