using new import/export module
This commit is contained in:
parent
d2783429a1
commit
086570ce90
@ -9,7 +9,7 @@ WORKDIR /opt/recipes
|
|||||||
COPY . ./
|
COPY . ./
|
||||||
RUN chmod +x boot.sh
|
RUN chmod +x boot.sh
|
||||||
|
|
||||||
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libressl-dev libffi-dev && \
|
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libressl-dev libffi-dev openssl-dev && \
|
||||||
python -m venv venv && \
|
python -m venv venv && \
|
||||||
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
|
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
|
||||||
apk --purge del .build-deps
|
apk --purge del .build-deps
|
||||||
|
@ -131,39 +131,17 @@ class ShoppingForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExportForm(forms.Form):
|
|
||||||
recipe = forms.ModelChoiceField(
|
|
||||||
queryset=Recipe.objects.filter(internal=True).all(),
|
|
||||||
widget=SelectWidget
|
|
||||||
)
|
|
||||||
image = forms.BooleanField(
|
|
||||||
help_text=_('Export Base64 encoded image?'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
download = forms.BooleanField(
|
|
||||||
help_text=_('Download export directly or show on page?'),
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
|
||||||
recipe = forms.CharField(
|
|
||||||
widget=forms.Textarea,
|
|
||||||
help_text=_('Simply paste a JSON export into this textarea and click import.') # noqa: E501
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ImportExportBase(forms.Form):
|
class ImportExportBase(forms.Form):
|
||||||
DEFAULT = 'Default'
|
DEFAULT = 'Default'
|
||||||
|
|
||||||
type = forms.ChoiceField(choices=((DEFAULT, _('Default')),))
|
type = forms.ChoiceField(choices=((DEFAULT, _('Default')),))
|
||||||
|
|
||||||
|
|
||||||
class NewImportForm(ImportExportBase):
|
class ImportForm(ImportExportBase):
|
||||||
files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
|
files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True}))
|
||||||
|
|
||||||
|
|
||||||
class NewExportForm(ImportExportBase):
|
class ExportForm(ImportExportBase):
|
||||||
recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget)
|
recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget)
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class BaseAutocomplete(autocomplete.Select2QuerySetView):
|
|||||||
qs = self.model.objects.all()
|
qs = self.model.objects.all()
|
||||||
|
|
||||||
if self.q:
|
if self.q:
|
||||||
qs = qs.filter(name__istartswith=self.q)
|
qs = qs.filter(name__icontains=self.q)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
|
||||||
{% load crispy_forms_filters %}
|
{% load crispy_forms_filters %}
|
||||||
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
|
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
|
||||||
@ -9,8 +9,9 @@
|
|||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% trans 'Export' %}</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<form action="." method="post">
|
<form action="." method="post">
|
||||||
@ -21,50 +22,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if export %}
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-12">
|
|
||||||
<label for="id_export">
|
|
||||||
{% trans 'Exported Recipe' %}</label>
|
|
||||||
<textarea id="id_export" class="form-control" rows="12">
|
|
||||||
{{ export }}
|
|
||||||
</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-12 text-center">
|
|
||||||
<button class="btn btn-success" onclick="copy()" style="width: 15vw" data-toggle="tooltip"
|
|
||||||
data-placement="right" title="{% trans 'Copy to clipboard' %}" id="id_btn_copy"
|
|
||||||
onmouseout="resetTooltip()"><i
|
|
||||||
class="far fa-copy"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
function copy() {
|
|
||||||
let json = $('#id_export');
|
|
||||||
|
|
||||||
json.select();
|
|
||||||
|
|
||||||
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copied!' %}').tooltip('show');
|
|
||||||
|
|
||||||
document.execCommand("copy");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTooltip() {
|
|
||||||
setTimeout(function () {
|
|
||||||
$('#id_btn_copy').attr('data-original-title', '{% trans 'Copy list to clipboard' %}');
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,14 +1,20 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
|
{% block title %}{% trans 'Import Recipes' %}{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h2>{% trans 'Import' %}</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<form action="." method="post">
|
<form action="." method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
|
<button class="btn btn-success" type="submit"><i class="fas fa-file-import"></i> {% trans 'Import' %}
|
||||||
@ -16,4 +22,5 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,76 +1,24 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from json import JSONDecodeError
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from rest_framework.renderers import JSONRenderer
|
|
||||||
|
|
||||||
from cookbook.forms import ExportForm, ImportForm
|
from cookbook.forms import ExportForm, ExportForm, ImportForm
|
||||||
from cookbook.helper.permission_helper import group_required
|
from cookbook.helper.permission_helper import group_required
|
||||||
|
from cookbook.integration.default import Default
|
||||||
from cookbook.models import Recipe
|
from cookbook.models import Recipe
|
||||||
from cookbook.serializer import RecipeSerializer, RecipeExportSerializer
|
|
||||||
|
|
||||||
|
def get_integration(request, export_type):
|
||||||
|
return Default(request)
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def import_recipe(request):
|
def import_recipe(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = ImportForm(request.POST)
|
form = ImportForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
try:
|
integration = Default(request)
|
||||||
data = json.loads(
|
return integration.do_import(request.FILES.getlist('files'))
|
||||||
re.sub(r'"id":([0-9]+),', '', re.sub(r',(\s)*"recipe":([0-9]+)', '', form.cleaned_data['recipe']))
|
|
||||||
)
|
|
||||||
|
|
||||||
sr = RecipeExportSerializer(data=data, context={'request': request})
|
|
||||||
if sr.is_valid():
|
|
||||||
sr.validated_data['created_by'] = request.user
|
|
||||||
recipe = sr.save()
|
|
||||||
|
|
||||||
if data['image']:
|
|
||||||
try:
|
|
||||||
fmt, img = data['image'].split(';base64,')
|
|
||||||
ext = fmt.split('/')[-1]
|
|
||||||
# TODO possible security risk,
|
|
||||||
# maybe some checks needed
|
|
||||||
recipe.image = (ContentFile(
|
|
||||||
base64.b64decode(img),
|
|
||||||
name=f'{recipe.pk}.{ext}')
|
|
||||||
)
|
|
||||||
recipe.save()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.SUCCESS,
|
|
||||||
_('Recipe imported successfully!')
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
reverse_lazy('view_recipe', args=[recipe.pk])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
_('Something went wrong during the import!')
|
|
||||||
)
|
|
||||||
messages.add_message(
|
|
||||||
request, messages.WARNING, sr.errors
|
|
||||||
)
|
|
||||||
except JSONDecodeError as e:
|
|
||||||
print(e)
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
messages.ERROR,
|
|
||||||
_('Could not parse the supplied JSON!')
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = ImportForm()
|
form = ImportForm()
|
||||||
|
|
||||||
@ -79,41 +27,17 @@ def import_recipe(request):
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def export_recipe(request):
|
def export_recipe(request):
|
||||||
context = {}
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = ExportForm(request.POST)
|
form = ExportForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
recipe = form.cleaned_data['recipe']
|
integration = Default(request)
|
||||||
if recipe.internal:
|
return integration.do_export(form.cleaned_data['recipes'])
|
||||||
export = RecipeExportSerializer(recipe).data
|
|
||||||
|
|
||||||
if recipe.image and form.cleaned_data['image']:
|
|
||||||
with open(recipe.image.path, 'rb') as img_f:
|
|
||||||
export['image'] = f'data:image/png;base64,{base64.b64encode(img_f.read()).decode("utf-8")}' # noqa: E501
|
|
||||||
|
|
||||||
json_string = JSONRenderer().render(export).decode("utf-8")
|
|
||||||
|
|
||||||
if form.cleaned_data['download']:
|
|
||||||
response = HttpResponse(
|
|
||||||
json_string, content_type='text/plain'
|
|
||||||
)
|
|
||||||
response['Content-Disposition'] = f'attachment; filename={recipe.name}.json' # noqa: E501
|
|
||||||
return response
|
|
||||||
|
|
||||||
context['export'] = re.sub(r'"id":([0-9])+,', '', json_string)
|
|
||||||
else:
|
|
||||||
form.add_error(
|
|
||||||
'recipe',
|
|
||||||
_('External recipes cannot be exported, please share the file directly or select an internal recipe.') # noqa: E501
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
form = ExportForm()
|
form = ExportForm()
|
||||||
recipe = request.GET.get('r')
|
recipe = request.GET.get('r')
|
||||||
if recipe:
|
if recipe:
|
||||||
if re.match(r'^([0-9])+$', recipe):
|
if re.match(r'^([0-9])+$', recipe):
|
||||||
if recipe := Recipe.objects.filter(pk=int(recipe)).first():
|
if recipe := Recipe.objects.filter(pk=int(recipe)).first():
|
||||||
form = ExportForm(initial={'recipe': recipe})
|
form = ExportForm(initial={'recipes': recipe})
|
||||||
|
|
||||||
context['form'] = form
|
return render(request, 'export.html', {'form': form})
|
||||||
|
|
||||||
return render(request, 'export.html', context)
|
|
||||||
|
@ -22,7 +22,7 @@ from rest_framework.authtoken.models import Token
|
|||||||
from cookbook.filters import RecipeFilter
|
from cookbook.filters import RecipeFilter
|
||||||
from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
|
from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
|
||||||
UserCreateForm, UserNameForm, UserPreference,
|
UserCreateForm, UserNameForm, UserPreference,
|
||||||
UserPreferenceForm, ImportForm, NewImportForm, NewExportForm)
|
UserPreferenceForm, ImportForm, ImportForm, ExportForm)
|
||||||
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
|
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
|
||||||
from cookbook.integration.default import Default
|
from cookbook.integration.default import Default
|
||||||
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
||||||
@ -490,27 +490,7 @@ def test(request):
|
|||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = NewImportForm(request.POST, request.FILES)
|
|
||||||
if form.is_valid():
|
|
||||||
integration = Default(request)
|
|
||||||
return integration.do_import(request.FILES.getlist('files'))
|
|
||||||
else:
|
|
||||||
form = NewImportForm()
|
|
||||||
|
|
||||||
return render(request, 'test.html', {'form': form})
|
|
||||||
|
|
||||||
|
|
||||||
def test2(request):
|
def test2(request):
|
||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = NewExportForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
integration = Default(request)
|
|
||||||
return integration.do_export(form.cleaned_data['recipes'])
|
|
||||||
else:
|
|
||||||
form = NewExportForm()
|
|
||||||
|
|
||||||
return render(request, 'test2.html', {'form': form})
|
|
||||||
|
Loading…
Reference in New Issue
Block a user