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 django.contrib.auth.models import User
from rest_framework import serializers 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 from cookbook.templatetags.custom_tags import markdown
@ -42,12 +42,6 @@ class KeywordSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = '__all__'
class UnitSerializer(serializers.ModelSerializer): class UnitSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Unit model = Unit
@ -61,14 +55,28 @@ class FoodSerializer(serializers.ModelSerializer):
class IngredientSerializer(serializers.ModelSerializer): class IngredientSerializer(serializers.ModelSerializer):
food = FoodSerializer(read_only=True)
unit = UnitSerializer(read_only=True)
class Meta: class Meta:
model = Ingredient model = Ingredient
fields = '__all__' fields = '__all__'
class CommentSerializer(serializers.ModelSerializer): class StepSerializer(serializers.ModelSerializer):
ingredients = IngredientSerializer(many=True, read_only=True)
class Meta: 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__' fields = '__all__'
@ -78,6 +86,12 @@ class RecipeImportSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class RecipeBookSerializer(serializers.ModelSerializer): class RecipeBookSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RecipeBook model = RecipeBook

View File

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% load theming_tags %} {% load theming_tags %}
@ -8,8 +7,11 @@
{% block title %}{% trans 'Edit Recipe' %}{% endblock %} {% block title %}{% trans 'Edit Recipe' %}{% endblock %}
{% block extra_head %} {% 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 %} {% endblock %}
@ -17,219 +19,101 @@
<h3>{% trans 'Edit Recipe' %}</h3> <h3>{% trans 'Edit Recipe' %}</h3>
<form action="." method="post" enctype="multipart/form-data" id="id_form"> <div id="app">
{% csrf_token %} <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> </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> </div>
{% endif %} <br/>
{% endfor %}
<hr> <div class="row">
<button class="btn btn-success" type="submit"><i class="fas fa-save"></i> {% trans 'Save' %}</button> <div class="col-md-6">
<a href="{% delete_url form.instance|get_class form.instance.pk %}" Image Edit Placeholder
class="btn btn-danger"><i class="fas fa-trash-alt"></i> {% trans 'Delete' %}</a> </div>
{% if view_url %} <div class="col-md-6">
<a href="{{ view_url }}" class="btn btn-info"><i class="far fa-eye"></i> {% trans 'View' %}</a> <label for="id_name"> {% trans 'Preperation Time' %}</label>
{% endif %} <input class="form-control" id="id_prep_time" v-model="recipe.working_time">
{% if form.instance.storage %}
<a href="{% url 'delete_recipe_source' form.instance.pk %}" class="btn btn-warning"><i <label for="id_name"> {% trans 'Waiting Time' %}</label>
class="fas fa-exclamation-triangle"></i> {% trans 'Delete original file' %}</a> <input class="form-control" id="id_wait_time" v-model="recipe.waiting_time">
{% endif %}
</form> <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"> <script type="application/javascript">
let csrftoken = Cookies.get('csrftoken');
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
$(function () { Vue.component('vue-multiselect', window.VueMultiselect.default)
$('[data-toggle="popover"]').popover()
});
$('.popover-dismiss').popover({ let app = new Vue({
trigger: 'focus' components: {
}); Multiselect: window.VueMultiselect.default
},
let select2UnitEditor = function (cell, onRendered, success, cancel, editorParams) { delimiters: ['[[', ']]'],
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_unit' %}') el: '#app',
}; data: {
recipe: undefined,
let select2IngredientEditor = function (cell, onRendered, success, cancel, editorParams) { keywords: [],
return select2Editor(cell, onRendered, success, cancel, editorParams, '{% url 'dal_food' %}') keywords_loading: false,
}; },
directives: {
let select2Editor = function (cell, onRendered, success, cancel, editorParams, url) { tabindex: {
inserted(el) {
let editor = document.createElement("select"); el.setAttribute('tabindex', 0);
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'
} }
});
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} mounted: function () {
], this.loadRecipe()
cellClick: function (e, cell) {
if (cell._cell.column.definition.editor === "input") {
input = cell.getElement().childNodes[0];
input.focus();
input.select();
}
}, },
}); methods: {
loadRecipe: function () {
// save ingredient data before submitting form this.$http.get("{% url 'api:recipe-list' %}{{ recipe.pk }}").then((response) => {
$('#id_form').submit(function () { this.recipe = response.data;
$('#id_ingredients').val(JSON.stringify(table.getData())); this.loading = false
return true; }).catch((err) => {
}); this.error = err.data
this.loading = false
// load initial value console.log(err)
$('#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()
}) })
},
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> </script>
{% endblock %} {% endblock %}

View File

@ -10,9 +10,12 @@ from cookbook.helper import dal
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'user-preference', api.UserPreferenceViewSet) 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'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-plan', api.MealPlanViewSet)
router.register(r'meal-type', api.MealTypeViewSet) router.register(r'meal-type', api.MealTypeViewSet)
router.register(r'view-log', api.ViewLogViewSet) 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.permission_helper import group_required, CustomIsOwner, CustomIsAdmin, CustomIsUser
from cookbook.helper.recipe_url_import import get_from_html 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.dropbox import Dropbox
from cookbook.provider.nextcloud import Nextcloud 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): class UserNameViewSet(viewsets.ModelViewSet):
@ -110,13 +111,37 @@ class MealTypeViewSet(viewsets.ModelViewSet):
return queryset 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): class RecipeViewSet(viewsets.ModelViewSet):
""" """
list: list:
optional parameters optional parameters
- **query**: search a recipe for a string contained in the recipe name (case in-sensitive) - **query**: search recipes for a string contained in the recipe name (case in-sensitive)
- **limit**: limits the amount of returned recipes - **limit**: limits the amount of returned results
""" """
queryset = Recipe.objects.all() queryset = Recipe.objects.all()
serializer_class = RecipeSerializer serializer_class = RecipeSerializer
@ -134,16 +159,28 @@ class RecipeViewSet(viewsets.ModelViewSet):
return queryset return queryset
class IngredientViewSet(viewsets.ModelViewSet): class KeywordViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all() """
serializer_class = IngredientSerializer 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] 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): limit = self.request.query_params.get('limit', None)
queryset = Food.objects.all() if limit is not None:
serializer_class = FoodSerializer queryset = queryset[:int(limit)]
permission_classes = [CustomIsUser] return queryset
class ViewLogViewSet(viewsets.ModelViewSet): class ViewLogViewSet(viewsets.ModelViewSet):

View File

@ -124,14 +124,8 @@ def internal_recipe_update(request, pk):
else: else:
messages.add_message(request, messages.ERROR, _('There was an error saving this recipe!')) messages.add_message(request, messages.ERROR, _('There was an error saving this recipe!'))
status = 403 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', {'recipe': recipe_instance})
return render(request, 'forms/edit_internal_recipe.html',
{'form': form, 'ingredients': json.dumps(list(ingredients)),
'view_url': reverse('view_recipe', args=[pk])}, status=status)
class SyncUpdate(GroupRequiredMixin, UpdateView): class SyncUpdate(GroupRequiredMixin, UpdateView):