super basic edit view

This commit is contained in:
vabene1111
2020-06-25 23:24:09 +02:00
parent 8b814669af
commit 4a6b32d9af
5 changed files with 167 additions and 235 deletions

View File

@ -1,7 +1,7 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food
from cookbook.models import MealPlan, MealType, Recipe, ViewLog, UserPreference, Storage, Sync, SyncLog, Keyword, Unit, Ingredient, Comment, RecipeImport, RecipeBook, RecipeBookEntry, ShareLink, CookLog, Food, Step
from cookbook.templatetags.custom_tags import markdown
@ -42,12 +42,6 @@ class KeywordSerializer(serializers.ModelSerializer):
fields = '__all__'
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = '__all__'
class UnitSerializer(serializers.ModelSerializer):
class Meta:
model = Unit
@ -61,14 +55,28 @@ class FoodSerializer(serializers.ModelSerializer):
class IngredientSerializer(serializers.ModelSerializer):
food = FoodSerializer(read_only=True)
unit = UnitSerializer(read_only=True)
class Meta:
model = Ingredient
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class StepSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True, read_only=True)
class Meta:
model = Comment
model = Step
fields = '__all__'
class RecipeSerializer(serializers.ModelSerializer):
steps = StepSerializer(many=True, read_only=True)
keywords = KeywordSerializer(many=True, read_only=True)
class Meta:
model = Recipe
fields = '__all__'
@ -78,6 +86,12 @@ class RecipeImportSerializer(serializers.ModelSerializer):
fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class RecipeBookSerializer(serializers.ModelSerializer):
class Meta:
model = RecipeBook

View File

@ -1,5 +1,4 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load custom_tags %}
{% load theming_tags %}
@ -8,8 +7,11 @@
{% block title %}{% trans 'Edit Recipe' %}{% endblock %}
{% block extra_head %}
<script src="{% static 'tabulator/tabulator.min.js' %}"></script>
<link rel="stylesheet" href="{% tabulator_theme_url request %}"/>
{% include 'include/vue_base.html' %}
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/vue-multiselect.min.css' %}">
{% endblock %}
@ -17,219 +19,101 @@
<h3>{% trans 'Edit Recipe' %}</h3>
<form action="." method="post" enctype="multipart/form-data" id="id_form">
{% csrf_token %}
<div id="app">
<template v-if="recipe">
<div class="row">
<div class="col-md-12">
<label for="id_name"> {% trans 'Name' %}</label>
<input class="form-control" id="id_name" v-model="recipe.name">
{% for field in form %}
<div class="fieldWrapper">
{{ field|as_crispy_field }}
</div>
{% if field.name == 'name' %}
<label>{% trans 'Ingredients' %}</label>
{{ form.ingredients.errors }}
<div id="ingredients-table"></div>
<br>
<div class="table-controls" style="text-align: center">
<button class="btn btn-success" id="new_empty" type="button" style="min-width: 20vw"><i
class="fas fa-plus-circle"></i></button>
<button class="btn btn-warning" id="new_header" type="button" data-toggle="tooltip"
data-placement="top" title="{% trans 'Insert a header between the ingredients.' %}"><i class="fas fa-heading"></i></button>
<button type="button" class="btn btn-secondary" data-container="body" data-toggle="popover"
data-placement="right" data-html="true" data-trigger="focus"
data-content="{% trans 'Use <b>Ctrl</b>+<b>Space</b> to insert new Ingredient!<br/>You can also save the recipe using <b>Ctrl</b>+<b>Shift</b>+<b>S</b>.' %}">
<i class="fas fa-question"></i>
</button>
<br/>
<br/>
</div>
{% endif %}
{% endfor %}
<br/>
<hr>
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
<a href="{% delete_url form.instance|get_class form.instance.pk %}"
class="btn btn-danger"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a>
{% if view_url %}
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a>
{% endif %}
{% if form.instance.storage %}
<a href="{% url 'delete_recipe_source' form.instance.pk %}" class="btn btn-warning"><i
class="fas fa-exclamation-triangle"></i> {% trans 'Delete original file' %}</a>
{% endif %}
</form>
<div class="row">
<div class="col-md-6">
Image Edit Placeholder
</div>
<div class="col-md-6">
<label for="id_name"> {% trans 'Preperation Time' %}</label>
<input class="form-control" id="id_prep_time" v-model="recipe.working_time">
<label for="id_name"> {% trans 'Waiting Time' %}</label>
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time">
<label for="id_name"> {% trans 'Keywords' %}</label>
<multiselect
v-model="recipe.keywords"
:options="keywords"
:close-on-select="false"
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
placeholder="{% trans 'Select one' %}"
label="name"
track-by="id"
id="id_keywords"
:multiple="true"
:loading="keywords_loading"
@search-change="searchKeywords">
</multiselect>
</div>
</div>
</template>
</div>
<script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
$(function () {
$('[data-toggle="popover"]').popover()
});
Vue.component('vue-multiselect', window.VueMultiselect.default)
$('.popover-dismiss').popover({
trigger: 'focus'
});
let select2UnitEditor = function (cell, onRendered, success, cancel, editorParams) {
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_unit' %}')
};
let select2IngredientEditor = function (cell, onRendered, success, cancel, editorParams) {
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_food' %}')
};
let select2Editor = function (cell, onRendered, success, cancel, editorParams, url) {
let editor = document.createElement("select");
editor.setAttribute("class", "form-control");
editor.setAttribute("style", "height: 100%; color: #00ff00");
onRendered(function () {
let select_2 = $(editor);
select_2.select2({
tags: true,
ajax: {
url: url,
dataType: 'json'
let app = new Vue({
components: {
Multiselect: window.VueMultiselect.default
},
delimiters: ['[[', ']]'],
el: '#app',
data: {
recipe: undefined,
keywords: [],
keywords_loading: false,
},
directives: {
tabindex: {
inserted(el) {
el.setAttribute('tabindex', 0);
}
});
select_2.select2('open');
select_2.on('select2:select', function (e) {
success(e.params.data.text);
});
select_2.on('select2:close', function (e) {
if (e.target.textContent === "") {
cancel();
}
});
});
//add editor to cell
return editor;
};
//converts multiselct in recipe edit to searchable multiselect
//shitty solution that needs to be redone at some point
$(document).ready(function () {
$('#id_keywords').select2();
let ingredients = {{ ingredients|safe }}
ingredients.forEach(function (cur, i) {
cur.delete = false
});
let data = ingredients;
let table = new Tabulator("#ingredients-table", {
index: "id",
layout: "fitColumns",
reactiveData: true,
data: data,
movableRows: true,
headerSort: false,
columns: [
{
title: "<i class='fas fa-sort'></i>",
rowHandle: true,
formatter: "handle",
headerSort: false,
frozen: true,
width: 36,
minWidth: 36
},
{
title: "{% trans 'Ingredient' %}",
field: "ingredient__name",
validator: "required",
editor: select2IngredientEditor
},
{title: "{% trans 'Amount' %}", field: "amount", validator: "required", editor: "number"},
{
title: "{% trans 'Unit' %}",
field: "unit__name",
validator: "required",
editor: select2UnitEditor
},
{title: "{% trans 'Note' %}", field: "note", editor: "input"},
{
formatter: function (cell, formatterParams) {
return "<span style='color:red'><i class=\"fas fa-trash-alt\"></i></span>"
},
align: "center",
title: "{% trans 'Delete' %}",
headerSort: false,
cellClick: function (e, cell) {
if (confirm('{% trans 'Are you sure that you want to delete this ingredient?' %}'))
cell.getRow().delete();
}
},
{title: "id", field: "id", visible: false}
],
cellClick: function (e, cell) {
if (cell._cell.column.definition.editor === "input") {
input = cell.getElement().childNodes[0];
input.focus();
input.select();
}
mounted: function () {
this.loadRecipe()
},
});
// save ingredient data before submitting form
$('#id_form').submit(function () {
$('#id_ingredients').val(JSON.stringify(table.getData()));
return true;
});
// load initial value
$('#id_ingredients').val(JSON.stringify(data));
function addIngredientRow() {
data.push({
ingredient__name: "{% trans 'Ingredient' %}",
amount: "100",
unit__name: "{{ request.user.userpreference.default_unit }}",
note: "",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
input = table.rowManager.rows[((table.rowManager.rows).length) - 1].cells[1].getElement()
input.focus();
input.select();
}
function addHeaderRow(type) {
data.push({
ingredient__name: '{% trans 'Header' %}',
amount: "0",
unit__name: "Special:Header",
note: "{% trans 'write header here' %}",
id: Math.floor(Math.random() * 10000000),
delete: false,
});
input = table.rowManager.rows[((table.rowManager.rows).length) - 1].cells[4].getElement()
input.focus();
input.select();
}
document.onkeyup = function (e) {
if (e.shiftKey && e.ctrlKey && (e.which === 83 || e.keyCode === 83)) {
$('#id_form').submit()
} else if (e.ctrlKey && (e.which === 83 || e.keyCode === 32)) {
addIngredientRow();
}
};
document.getElementById("new_empty").addEventListener("click", addIngredientRow);
document.getElementById("new_header").addEventListener("click", addHeaderRow);
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
methods: {
loadRecipe: function () {
this.$http.get("{% url 'api:recipe-list' %}{{ recipe.pk }}").then((response) => {
this.recipe = response.data;
this.loading = false
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
})
},
searchKeywords: function (query) {
this.keywords_loading = true
this.$http.get("{% url 'api:keyword-list' %}" + '?query=' + query).then((response) => {
this.keywords = response.data;
this.keywords_loading = false
}).catch((err) => {
console.log(err)
})
},
}
});
</script>
{% endblock %}

View File

@ -10,9 +10,12 @@ from cookbook.helper import dal
router = routers.DefaultRouter()
router.register(r'user-preference', api.UserPreferenceViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'unit', api.UnitViewSet)
router.register(r'food', api.FoodViewSet)
router.register(r'recipe-ingredient', api.IngredientViewSet)
router.register(r'step', api.StepViewSet)
router.register(r'recipe', api.RecipeViewSet)
router.register(r'keyword', api.KeywordViewSet)
router.register(r'ingredient', api.IngredientViewSet)
router.register(r'meal-plan', api.MealPlanViewSet)
router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'view-log', api.ViewLogViewSet)

View File

@ -18,10 +18,11 @@ from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListMode
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser
from cookbook.helper.recipe_url_import import get_from_html
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Ingredient, Food, Step, Keyword
from cookbook.provider.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer, IngredientSerializer, FoodSerializer, StepSerializer, \
KeywordSerializer
class UserNameViewSet(viewsets.ModelViewSet):
@ -110,13 +111,37 @@ class MealTypeViewSet(viewsets.ModelViewSet):
return queryset
class UnitViewSet(viewsets.ModelViewSet):
queryset = Food.objects.all()
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]
class FoodViewSet(viewsets.ModelViewSet):
queryset = Food.objects.all()
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]
class IngredientViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
permission_classes = [CustomIsUser]
class StepViewSet(viewsets.ModelViewSet):
queryset = Step.objects.all()
serializer_class = StepSerializer
permission_classes = [CustomIsUser]
class RecipeViewSet(viewsets.ModelViewSet):
"""
list:
optional parameters
- **query**: search a recipe for a string contained in the recipe name (case in-sensitive)
- **limit**: limits the amount of returned recipes
- **query**: search recipes for a string contained in the recipe name (case in-sensitive)
- **limit**: limits the amount of returned results
"""
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
@ -134,16 +159,28 @@ class RecipeViewSet(viewsets.ModelViewSet):
return queryset
class IngredientViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
class KeywordViewSet(viewsets.ModelViewSet):
"""
list:
optional parameters
- **query**: search keywords for a string contained in the keyword name (case in-sensitive)
- **limit**: limits the amount of returned results
"""
queryset = Keyword.objects.all()
serializer_class = KeywordSerializer
permission_classes = [CustomIsUser]
def get_queryset(self):
queryset = Keyword.objects.all()
query = self.request.query_params.get('query', None)
if query is not None:
queryset = queryset.filter(name__icontains=query)
class FoodViewSet(viewsets.ModelViewSet):
queryset = Food.objects.all()
serializer_class = FoodSerializer
permission_classes = [CustomIsUser]
limit = self.request.query_params.get('limit', None)
if limit is not None:
queryset = queryset[:int(limit)]
return queryset
class ViewLogViewSet(viewsets.ModelViewSet):

View File

@ -124,14 +124,8 @@ def internal_recipe_update(request, pk):
else:
messages.add_message(request, messages.ERROR, _('There was an error saving this recipe!'))
status = 403
else:
form = InternalRecipeForm(instance=recipe_instance)
ingredients = Ingredient.objects.select_related('unit__name', 'food__name').filter(recipe=recipe_instance).values('food__name', 'unit__name', 'amount', 'note').order_by('id')
return render(request, 'forms/edit_internal_recipe.html',
{'form': form, 'ingredients': json.dumps(list(ingredients)),
'view_url': reverse('view_recipe', args=[pk])}, status=status)
return render(request, 'forms/edit_internal_recipe.html', {'recipe': recipe_instance})
class SyncUpdate(GroupRequiredMixin, UpdateView):