mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-26 03:43:34 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78323d214 | ||
|
|
d2e866dd74 | ||
|
|
76687ad5df | ||
|
|
dab77e8e4f | ||
|
|
0b250c71aa | ||
|
|
571f670db0 | ||
|
|
4e9e628162 | ||
|
|
4f49b06704 | ||
|
|
8eb0c36665 | ||
|
|
6f69c09aca | ||
|
|
8e6f153882 | ||
|
|
07183fd40f | ||
|
|
04b7f0a398 | ||
|
|
1735fda48f | ||
|
|
1c9ea0eda7 | ||
|
|
83b5b6695c | ||
|
|
342fb3c96d | ||
|
|
b7a18466b5 | ||
|
|
0cdc4d51df | ||
|
|
e177669514 | ||
|
|
fd294dfcdd |
@@ -1,18 +1,18 @@
|
||||
name: Deploy Docker Image
|
||||
|
||||
name: publish dev image docker
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!master'
|
||||
jobs:
|
||||
build-push:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: AhnSeongHyun/action-tag-docker-build-push@v1.0.0
|
||||
uses: elgohr/Publish-Docker-Github-Action@2.13
|
||||
with:
|
||||
repo_name: vabene1111/recipes
|
||||
name: vabene1111/recipes
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
19
.github/workflows/docker-publish-latest.yml
vendored
Normal file
19
.github/workflows/docker-publish-latest.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: publish latest image docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: latest
|
||||
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
25
.github/workflows/docker-publish-release.yml
vendored
Normal file
25
.github/workflows/docker-publish-release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: publish tagged release docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build image job
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@master#
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||
- name: Build and publish image
|
||||
uses: ilteoood/docker_buildx@master
|
||||
with:
|
||||
publish: true
|
||||
imageName: vabene1111/recipes
|
||||
tag: ${{ steps.get_version.outputs.VERSION }}
|
||||
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
|
||||
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
|
||||
13
.github/workflows/docker-publish.yml
vendored
13
.github/workflows/docker-publish.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Publish Docker
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Publish to Registry
|
||||
uses: elgohr/Publish-Docker-Github-Action@2.13
|
||||
with:
|
||||
name: vabene1111/recipes
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -25,7 +25,7 @@ The docker image (`vabene1111/recipes`) simply exposes the application on port `
|
||||
|
||||
### Docker-Compose
|
||||
|
||||
2. Choose one of the included configurations [here](https://github.com/vabene1111/recipes/tree/develop/docs/docker).
|
||||
2. Choose one of the included configurations [here](docs/docker).
|
||||
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
|
||||
3. Start the container `docker-compose up -d`
|
||||
4. Create a default user by running `docker-compose exec web_recipes createsuperuser`.
|
||||
@@ -46,6 +46,10 @@ While intermediate updates can be skipped when updating please make sure to **re
|
||||
2. Pull the latest image using `docker-compose pull`
|
||||
3. Start the container again using `docker-compose up -d`
|
||||
|
||||
## Kubernetes
|
||||
|
||||
You can find a basic kubernetes setup [here](docs/k8s/). Please see the README in the folder for more detail.
|
||||
|
||||
# Documentation
|
||||
|
||||
Most things should be straight forward but there are some more complicated things.
|
||||
|
||||
@@ -31,10 +31,11 @@ class UserPreferenceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = UserPreference
|
||||
fields = ('theme', 'nav_color')
|
||||
fields = ('default_unit', 'theme', 'nav_color', 'default_page')
|
||||
|
||||
help_texts = {
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!')
|
||||
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
|
||||
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.')
|
||||
}
|
||||
|
||||
|
||||
|
||||
18
cookbook/migrations/0032_userpreference_default_unit.py
Normal file
18
cookbook/migrations/0032_userpreference_default_unit.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-13 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0031_auto_20200407_1841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='default_unit',
|
||||
field=models.CharField(default='g', max_length=32),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0033_userpreference_default_page.py
Normal file
18
cookbook/migrations/0033_userpreference_default_page.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.4 on 2020-04-13 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0032_userpreference_default_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userpreference',
|
||||
name='default_page',
|
||||
field=models.CharField(choices=[('SEARCH', 'Search'), ('PLAN', 'Meal-Plan')], default='SEARCH', max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -41,9 +41,18 @@ class UserPreference(models.Model):
|
||||
|
||||
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
|
||||
|
||||
# Default Page
|
||||
SEARCH = 'SEARCH'
|
||||
PLAN = 'PLAN'
|
||||
BOOKS = 'BOOKS'
|
||||
|
||||
PAGES = ((SEARCH, _('Search')), (PLAN, _('Meal-Plan')), (BOOKS, _('Books')), )
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
|
||||
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
|
||||
default_unit = models.CharField(max_length=32, default='g')
|
||||
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
|
||||
|
||||
def __str__(self):
|
||||
return self.user
|
||||
|
||||
@@ -16,9 +16,10 @@ class Nextcloud(Provider):
|
||||
@staticmethod
|
||||
def get_client(storage):
|
||||
options = {
|
||||
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
|
||||
'webdav_hostname': storage.url,
|
||||
'webdav_login': storage.username,
|
||||
'webdav_password': storage.password
|
||||
'webdav_password': storage.password,
|
||||
'webdav_root': '/remote.php/dav/files/' + storage.username
|
||||
}
|
||||
return wc.Client(options)
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'index,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
|
||||
<li class="nav-item {% if request.resolver_match.url_name in 'view_search,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
|
||||
<a class="nav-link" href="{% url 'view_search' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
|
||||
class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
data.push({
|
||||
ingredient__name: "{% trans 'Ingredient' %}",
|
||||
amount: "100",
|
||||
unit__name: "g",
|
||||
unit__name: "{{ request.user.userpreference.default_unit }}",
|
||||
note: "",
|
||||
id: Math.floor(Math.random() * 10000000),
|
||||
delete: false,
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
<div style="font-size: large">
|
||||
{% if recipe.instructions %}
|
||||
{{ recipe.instructions | markdown | safe }}
|
||||
{{ recipe.instructions | markdown | safe | urlize }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ class TestViewsGeneral(TestViews):
|
||||
|
||||
def test_index(self):
|
||||
r = self.client.get(reverse('index'))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
r = self.anonymous_client.get(reverse('index'))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
def test_books(self):
|
||||
url = reverse('view_books')
|
||||
|
||||
@@ -8,6 +8,7 @@ from cookbook.helper import dal
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('search/', views.search, name='view_search'),
|
||||
path('books/', views.books, name='view_books'),
|
||||
path('plan/', views.meal_plan, name='view_plan'),
|
||||
path('shopping/', views.shopping_list, name='view_shopping'),
|
||||
|
||||
@@ -6,7 +6,9 @@ from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse_lazy
|
||||
from django_tables2 import RequestConfig
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -16,6 +18,21 @@ from cookbook.tables import RecipeTable
|
||||
|
||||
|
||||
def index(request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseRedirect(reverse_lazy('view_search'))
|
||||
try:
|
||||
page_map = {
|
||||
UserPreference.SEARCH: reverse_lazy('view_search'),
|
||||
UserPreference.PLAN: reverse_lazy('view_plan'),
|
||||
UserPreference.BOOKS: reverse_lazy('view_books'),
|
||||
}
|
||||
|
||||
return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page))
|
||||
except UserPreference.DoesNotExist:
|
||||
return HttpResponseRedirect(reverse_lazy('view_search'))
|
||||
|
||||
|
||||
def search(request):
|
||||
if request.user.is_authenticated:
|
||||
f = RecipeFilter(request.GET, queryset=Recipe.objects.all().order_by('name'))
|
||||
|
||||
@@ -175,6 +192,8 @@ def settings(request):
|
||||
up = UserPreference(user=request.user)
|
||||
up.theme = form.cleaned_data['theme']
|
||||
up.nav_color = form.cleaned_data['nav_color']
|
||||
up.default_unit = form.cleaned_data['default_unit']
|
||||
up.default_page = form.cleaned_data['default_page']
|
||||
up.save()
|
||||
|
||||
if 'user_name_form' in request.POST:
|
||||
|
||||
33
docs/k8s/10-configmap.yaml
Normal file
33
docs/k8s/10-configmap.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
name: recipes-nginx-config
|
||||
data:
|
||||
nginx-config: |-
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 16M;
|
||||
|
||||
# serve static files
|
||||
location /static/ {
|
||||
alias /static/;
|
||||
}
|
||||
# serve media files
|
||||
location /media/ {
|
||||
alias /media/;
|
||||
}
|
||||
# pass requests for dynamic content to gunicorn
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
docs/k8s/30-pv.yaml
Normal file
50
docs/k8s/30-pv.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: db
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/db"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-media
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: media
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/media"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-static
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: static
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/static"
|
||||
52
docs/k8s/30-pvc.yaml
Normal file
52
docs/k8s/30-pvc.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: db
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-media
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: media
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-static
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: static
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
102
docs/k8s/50-deployment.yaml
Normal file
102
docs/k8s/50-deployment.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: recipes
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
tier: frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: recipes
|
||||
environment: production
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
spec:
|
||||
containers:
|
||||
- name: recipes-nginx
|
||||
image: nginx:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
name: http
|
||||
volumeMounts:
|
||||
- mountPath: '/media'
|
||||
name: media
|
||||
- mountPath: '/static'
|
||||
name: static
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx-config
|
||||
readOnly: true
|
||||
- name: recipes
|
||||
image: 'vabene1111/recipes:latest'
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
volumeMounts:
|
||||
- mountPath: '/opt/recipes/mediafiles'
|
||||
name: media
|
||||
- mountPath: '/opt/recipes/staticfiles'
|
||||
name: static
|
||||
env:
|
||||
- name: DEBUG
|
||||
value: "0"
|
||||
- name: ALLOWED_HOSTS
|
||||
value: '*'
|
||||
- name: SECRET_KEY
|
||||
value: # CHANGEME
|
||||
- name: DB_ENGINE
|
||||
value: django.db.backends.postgresql_psycopg2
|
||||
- name: POSTGRES_HOST
|
||||
value: localhost
|
||||
- name: POSTGRES_PORT
|
||||
value: "5432"
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
- name: recipes-db
|
||||
image: 'postgres:latest'
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- mountPath: '/var/lib/postgresql/data'
|
||||
name: database
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
volumes:
|
||||
- name: database
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-db
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-media
|
||||
- name: static
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-static
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: recipes-nginx-config
|
||||
15
docs/k8s/60-service.yaml
Normal file
15
docs/k8s/60-service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: recipes
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
app: recipes
|
||||
environment: production
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
protocol: TCP
|
||||
25
docs/k8s/README.md
Normal file
25
docs/k8s/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Kubernetes
|
||||
|
||||
This is a basic kubernetes setup. Please note that this does not necessarily follow Kubernetes best practices and should only used as a basis to build your own setup from!
|
||||
|
||||
## Important notes
|
||||
|
||||
State (database, static files and media files) is handled via `PersistentVolumes`.
|
||||
|
||||
Note that you will most likely have to change the `PersistentVolumes` in `30-pv.yaml`. The current setup is only usable for a single-node cluster because it uses local storage on the kubernetes worker nodes under `/data/recipes/`. It should just serve as an example.
|
||||
|
||||
Currently, the deployment in `50-deployment.yaml` just pulls the `latest` tag of all containers. In a production setup, you should set this to a fixed version!
|
||||
|
||||
See env variables tagged with `CHANGEME` in `50-deployment.yaml` and make sure to change those! A better setup would use kubernetes secrets but this is not implemented yet.
|
||||
|
||||
## Updates
|
||||
|
||||
These manifests are not tested against new versions.
|
||||
|
||||
## Apply the manifets
|
||||
|
||||
To apply the manifest with `kubectl`, use the following command:
|
||||
|
||||
```
|
||||
kubectl apply -f ./docs/k8s/
|
||||
```
|
||||
Reference in New Issue
Block a user