mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-10 00:28:22 -05:00
Merge branch 'develop' into feature/custom_filters
# Conflicts: # cookbook/serializer.py # cookbook/views/api.py # vue/src/utils/openapi/api.ts
This commit is contained in:
@@ -32,11 +32,12 @@ class Default(Integration):
|
||||
return None
|
||||
|
||||
def get_file_from_recipe(self, recipe):
|
||||
|
||||
export = RecipeExportSerializer(recipe).data
|
||||
|
||||
return 'recipe.json', JSONRenderer().render(export).decode("utf-8")
|
||||
|
||||
def get_files_from_recipes(self, recipes, cookie):
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
export_zip_stream = BytesIO()
|
||||
export_zip_obj = ZipFile(export_zip_stream, 'w')
|
||||
|
||||
@@ -50,13 +51,20 @@ class Default(Integration):
|
||||
recipe_stream.write(data)
|
||||
recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
|
||||
recipe_stream.close()
|
||||
|
||||
try:
|
||||
recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
recipe_zip_obj.close()
|
||||
|
||||
export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(r)
|
||||
el.save()
|
||||
|
||||
export_zip_obj.close()
|
||||
|
||||
return [[ 'export.zip', export_zip_stream.getvalue() ]]
|
||||
return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]]
|
||||
@@ -1,9 +1,12 @@
|
||||
import time
|
||||
import datetime
|
||||
import json
|
||||
import traceback
|
||||
import uuid
|
||||
from io import BytesIO, StringIO
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
from django.core.cache import cache
|
||||
import datetime
|
||||
|
||||
from bs4 import Tag
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
@@ -18,6 +21,7 @@ from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.image_processing import get_filetype, handle_image
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from recipes.settings import DEBUG
|
||||
from recipes.settings import EXPORT_FILE_CACHE_DURATION
|
||||
|
||||
|
||||
class Integration:
|
||||
@@ -61,35 +65,44 @@ class Integration:
|
||||
space=request.space
|
||||
)
|
||||
|
||||
def do_export(self, recipes):
|
||||
"""
|
||||
Perform the export based on a list of recipes
|
||||
:param recipes: list of recipe objects
|
||||
:return: HttpResponse with the file of the requested export format that is directly downloaded (When that format involve multiple files they are zipped together)
|
||||
"""
|
||||
|
||||
files = self.get_files_from_recipes(recipes, self.request.COOKIES)
|
||||
|
||||
if len(files) == 1:
|
||||
filename, file = files[0]
|
||||
export_filename = filename
|
||||
export_file = file
|
||||
def do_export(self, recipes, el):
|
||||
|
||||
else:
|
||||
export_filename = "export.zip"
|
||||
export_stream = BytesIO()
|
||||
export_obj = ZipFile(export_stream, 'w')
|
||||
with scope(space=self.request.space):
|
||||
el.total_recipes = len(recipes)
|
||||
el.cache_duration = EXPORT_FILE_CACHE_DURATION
|
||||
el.save()
|
||||
|
||||
for filename, file in files:
|
||||
export_obj.writestr(filename, file)
|
||||
files = self.get_files_from_recipes(recipes, el, self.request.COOKIES)
|
||||
|
||||
export_obj.close()
|
||||
export_file = export_stream.getvalue()
|
||||
if len(files) == 1:
|
||||
filename, file = files[0]
|
||||
export_filename = filename
|
||||
export_file = file
|
||||
|
||||
else:
|
||||
#zip the files if there is more then one file
|
||||
export_filename = self.get_export_file_name()
|
||||
export_stream = BytesIO()
|
||||
export_obj = ZipFile(export_stream, 'w')
|
||||
|
||||
for filename, file in files:
|
||||
export_obj.writestr(filename, file)
|
||||
|
||||
export_obj.close()
|
||||
export_file = export_stream.getvalue()
|
||||
|
||||
|
||||
cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION)
|
||||
el.running = False
|
||||
el.save()
|
||||
|
||||
response = HttpResponse(export_file, content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="' + export_filename + '"'
|
||||
return response
|
||||
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
"""
|
||||
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
|
||||
@@ -126,7 +139,7 @@ class Integration:
|
||||
for d in data_list:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
@@ -151,7 +164,7 @@ class Integration:
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
@@ -166,7 +179,7 @@ class Integration:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
@@ -183,7 +196,7 @@ class Integration:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(d)
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
il.imported_recipes += 1
|
||||
il.save()
|
||||
@@ -193,7 +206,7 @@ class Integration:
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(f['file'])
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
il.msg += self.get_recipe_processed_msg(recipe)
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
except BadZipFile:
|
||||
il.msg += 'ERROR ' + _(
|
||||
@@ -260,7 +273,7 @@ class Integration:
|
||||
"""
|
||||
raise NotImplementedError('Method not implemented in integration')
|
||||
|
||||
def get_files_from_recipes(self, recipes, cookie):
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
"""
|
||||
Takes a list of recipe object and converts it to a array containing each file.
|
||||
Each file is represented as an array [filename, data] where data is a string of the content of the file.
|
||||
@@ -279,3 +292,10 @@ class Integration:
|
||||
log.msg += exception.msg
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def get_export_file_name(self, format='zip'):
|
||||
return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format)
|
||||
|
||||
def get_recipe_processed_msg(self, recipe):
|
||||
return f'{recipe.pk} - {recipe.name} \n'
|
||||
|
||||
@@ -11,22 +11,25 @@ from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.serializer import RecipeExportSerializer
|
||||
|
||||
import django.core.management.commands.runserver as runserver
|
||||
from cookbook.models import ExportLog
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
import django.core.management.commands.runserver as runserver
|
||||
import logging
|
||||
|
||||
class PDFexport(Integration):
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
raise NotImplementedError('Method not implemented in storage integration')
|
||||
|
||||
async def get_files_from_recipes_async(self, recipes, cookie):
|
||||
async def get_files_from_recipes_async(self, recipes, el, cookie):
|
||||
cmd = runserver.Command()
|
||||
|
||||
browser = await launch(
|
||||
handleSIGINT=False,
|
||||
handleSIGTERM=False,
|
||||
handleSIGHUP=False,
|
||||
ignoreHTTPSErrors=True
|
||||
ignoreHTTPSErrors=True,
|
||||
)
|
||||
|
||||
cookies = {'domain': cmd.default_addr, 'name': 'sessionid', 'value': cookie['sessionid'], }
|
||||
@@ -39,17 +42,28 @@ class PDFexport(Integration):
|
||||
}
|
||||
}
|
||||
|
||||
page = await browser.newPage()
|
||||
await page.emulateMedia('print')
|
||||
await page.setCookie(cookies)
|
||||
|
||||
files = []
|
||||
for recipe in recipes:
|
||||
await page.goto('http://' + cmd.default_addr + ':' + cmd.default_port + '/view/recipe/' + str(recipe.id), {'waitUntil': 'networkidle0', })
|
||||
|
||||
page = await browser.newPage()
|
||||
await page.emulateMedia('print')
|
||||
await page.setCookie(cookies)
|
||||
|
||||
await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'})
|
||||
await page.waitForSelector('#printReady');
|
||||
|
||||
files.append([recipe.name + '.pdf', await page.pdf(options)])
|
||||
await page.close();
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(recipe)
|
||||
await sync_to_async(el.save, thread_sensitive=True)()
|
||||
|
||||
|
||||
await browser.close()
|
||||
return files
|
||||
|
||||
def get_files_from_recipes(self, recipes, cookie):
|
||||
return asyncio.run(self.get_files_from_recipes_async(recipes, cookie))
|
||||
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
return asyncio.run(self.get_files_from_recipes_async(recipes, el, cookie))
|
||||
|
||||
@@ -88,12 +88,16 @@ class RecipeSage(Integration):
|
||||
|
||||
return data
|
||||
|
||||
def get_files_from_recipes(self, recipes, cookie):
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
json_list = []
|
||||
for r in recipes:
|
||||
json_list.append(self.get_file_from_recipe(r))
|
||||
|
||||
return [['export.json', json.dumps(json_list)]]
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(r)
|
||||
el.save()
|
||||
|
||||
return [[self.get_export_file_name('json'), json.dumps(json_list)]]
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
return json.loads(file.read().decode("utf-8"))
|
||||
|
||||
@@ -87,10 +87,14 @@ class Saffron(Integration):
|
||||
|
||||
return recipe.name+'.txt', data
|
||||
|
||||
def get_files_from_recipes(self, recipes, cookie):
|
||||
def get_files_from_recipes(self, recipes, el, cookie):
|
||||
files = []
|
||||
for r in recipes:
|
||||
filename, data = self.get_file_from_recipe(r)
|
||||
files.append([ filename, data ])
|
||||
|
||||
el.exported_recipes += 1
|
||||
el.msg += self.get_recipe_processed_msg(r)
|
||||
el.save()
|
||||
|
||||
return files
|
||||
Binary file not shown.
@@ -15,8 +15,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: 2022-02-02 15:31+0000\n"
|
||||
"Last-Translator: Sven <tr@sutikal.de>\n"
|
||||
"PO-Revision-Date: 2022-02-06 21:31+0000\n"
|
||||
"Last-Translator: David Laukamp <dlkmp@live.de>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/de/>\n"
|
||||
"Language: de\n"
|
||||
@@ -415,6 +415,7 @@ msgstr ""
|
||||
#: .\cookbook\forms.py:501
|
||||
msgid "Days of recent shopping list entries to display."
|
||||
msgstr ""
|
||||
"Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen."
|
||||
|
||||
#: .\cookbook\forms.py:502
|
||||
msgid "Mark food 'On Hand' when checked off shopping list."
|
||||
@@ -478,11 +479,12 @@ msgstr "Automatisch als vorrätig markieren"
|
||||
|
||||
#: .\cookbook\forms.py:528
|
||||
msgid "Reset Food Inheritance"
|
||||
msgstr ""
|
||||
msgstr "Lebensmittelvererbung zurücksetzen"
|
||||
|
||||
#: .\cookbook\forms.py:529
|
||||
msgid "Reset all food to inherit the fields configured."
|
||||
msgstr ""
|
||||
"Alle Lebensmittel zurücksetzen, um die konfigurierten Felder zu übernehmen."
|
||||
|
||||
#: .\cookbook\forms.py:541
|
||||
msgid "Fields on food that should be inherited by default."
|
||||
|
||||
Binary file not shown.
@@ -14,16 +14,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-01-18 14:52+0100\n"
|
||||
"PO-Revision-Date: 2022-01-16 07:06+0000\n"
|
||||
"Last-Translator: Josselin du PLESSIS <josse@du-plessis.fr>\n"
|
||||
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/fr/>\n"
|
||||
"PO-Revision-Date: 2022-02-06 21:31+0000\n"
|
||||
"Last-Translator: Marion Kämpfer <marion@murphyslantech.de>\n"
|
||||
"Language-Team: French <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/fr/>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34
|
||||
#: .\cookbook\templates\space.html:50 .\cookbook\templates\stats.html:28
|
||||
@@ -123,10 +123,8 @@ msgstr ""
|
||||
"nouvellement créés seront partagés par défaut."
|
||||
|
||||
#: .\cookbook\forms.py:80
|
||||
#, fuzzy
|
||||
#| msgid "Try the new shopping list"
|
||||
msgid "Users with whom to share shopping lists."
|
||||
msgstr "Essayer la nouvelle liste de courses"
|
||||
msgstr "Utilisateurs avec lesquels partager des listes de courses."
|
||||
|
||||
#: .\cookbook\forms.py:82
|
||||
msgid "Show recently viewed recipes on search page."
|
||||
@@ -419,6 +417,8 @@ msgstr ""
|
||||
#: .\cookbook\forms.py:500
|
||||
msgid "Filter shopping list to only include supermarket categories."
|
||||
msgstr ""
|
||||
"Filtrer la liste de courses pour n’inclure que des catégories de "
|
||||
"supermarchés."
|
||||
|
||||
#: .\cookbook\forms.py:501
|
||||
msgid "Days of recent shopping list entries to display."
|
||||
@@ -430,17 +430,15 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:503
|
||||
msgid "Delimiter to use for CSV exports."
|
||||
msgstr ""
|
||||
msgstr "Caractère de séparation à utiliser pour les exportations CSV."
|
||||
|
||||
#: .\cookbook\forms.py:504
|
||||
msgid "Prefix to add when copying list to the clipboard."
|
||||
msgstr ""
|
||||
msgstr "Préfixe à utiliser pour copier la liste dans le presse-papiers."
|
||||
|
||||
#: .\cookbook\forms.py:508
|
||||
#, fuzzy
|
||||
#| msgid "New Shopping List"
|
||||
msgid "Share Shopping List"
|
||||
msgstr "Nouvelle liste de courses"
|
||||
msgstr "Partager la liste de courses"
|
||||
|
||||
#: .\cookbook\forms.py:509
|
||||
msgid "Autosync"
|
||||
@@ -448,7 +446,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:510
|
||||
msgid "Auto Add Meal Plan"
|
||||
msgstr ""
|
||||
msgstr "Ajouter le menu de la semaine automatiquement"
|
||||
|
||||
#: .\cookbook\forms.py:511
|
||||
msgid "Exclude On Hand"
|
||||
@@ -474,7 +472,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:516
|
||||
msgid "CSV Delimiter"
|
||||
msgstr ""
|
||||
msgstr "Caractère de séparation CSV"
|
||||
|
||||
#: .\cookbook\forms.py:517 .\cookbook\templates\shopping_list.html:322
|
||||
msgid "List Prefix"
|
||||
@@ -742,7 +740,7 @@ msgstr "Alias de mot-clé"
|
||||
|
||||
#: .\cookbook\serializer.py:175
|
||||
msgid "A user is required"
|
||||
msgstr ""
|
||||
msgstr "Un utilisateur est requis"
|
||||
|
||||
#: .\cookbook\serializer.py:195
|
||||
msgid "File uploads are not enabled for this Space."
|
||||
@@ -754,7 +752,7 @@ msgstr "Vous avez atteint votre limite de téléversement de fichiers."
|
||||
|
||||
#: .\cookbook\serializer.py:962
|
||||
msgid "Existing shopping list to update"
|
||||
msgstr ""
|
||||
msgstr "Liste de courses existante à mettre à jour"
|
||||
|
||||
#: .\cookbook\serializer.py:964
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
34
cookbook/migrations/0169_exportlog.py
Normal file
34
cookbook/migrations/0169_exportlog.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-03 15:03
|
||||
|
||||
import cookbook.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0168_add_unit_searchfields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExportLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.CharField(max_length=32)),
|
||||
('running', models.BooleanField(default=True)),
|
||||
('msg', models.TextField(default='')),
|
||||
('total_recipes', models.IntegerField(default=0)),
|
||||
('exported_recipes', models.IntegerField(default=0)),
|
||||
('cache_duration', models.IntegerField(default=0)),
|
||||
('possibly_not_expired', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-02 19:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0169_auto_20220121_1427'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.unit'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +1,44 @@
|
||||
# Generated by Django 3.2.11 on 2022-01-21 20:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
# Generated by Django 3.2.11 on 2022-02-07 17:48
|
||||
|
||||
import cookbook.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('cookbook', '0168_add_unit_searchfields'),
|
||||
('cookbook', '0169_exportlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='child_inherit_fields',
|
||||
field=models.ManyToManyField(blank=True, related_name='child_inherit', to='cookbook.FoodInheritField'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute',
|
||||
field=models.ManyToManyField(blank=True, related_name='_cookbook_food_substitute_+', to='cookbook.Food'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_children',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_siblings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.unit'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomFilter',
|
||||
fields=[
|
||||
@@ -29,13 +53,13 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
bases=(models.Model, cookbook.models.PermissionModelMixin),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='customfilter',
|
||||
constraint=models.UniqueConstraint(fields=('space', 'name'), name='cf_unique_name_per_space'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipebook',
|
||||
name='filter',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.customfilter'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='customfilter',
|
||||
constraint=models.UniqueConstraint(fields=('space', 'name'), name='cf_unique_name_per_space'),
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-02 19:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0170_alter_ingredient_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute',
|
||||
field=models.ManyToManyField(blank=True, related_name='_cookbook_food_substitute_+', to='cookbook.Food'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_children',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_siblings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.unit'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-04 17:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0171_auto_20220202_1340'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='child_inherit_fields',
|
||||
field=models.ManyToManyField(blank=True, related_name='child_inherit', to='cookbook.FoodInheritField'),
|
||||
),
|
||||
]
|
||||
@@ -1028,6 +1028,25 @@ class ImportLog(models.Model, PermissionModelMixin):
|
||||
def __str__(self):
|
||||
return f"{self.created_at}:{self.type}"
|
||||
|
||||
class ExportLog(models.Model, PermissionModelMixin):
|
||||
type = models.CharField(max_length=32)
|
||||
running = models.BooleanField(default=True)
|
||||
msg = models.TextField(default="")
|
||||
|
||||
total_recipes = models.IntegerField(default=0)
|
||||
exported_recipes = models.IntegerField(default=0)
|
||||
cache_duration = models.IntegerField(default=0)
|
||||
possibly_not_expired = models.BooleanField(default=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
objects = ScopedManager(space='space')
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.created_at}:{self.type}"
|
||||
|
||||
|
||||
class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models.Model, PermissionModelMixin):
|
||||
html = models.TextField()
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Avg, Q, QuerySet, Sum, Value
|
||||
from django.db.models.functions import Substr
|
||||
from django.db.models import Avg, Q, QuerySet, Sum
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.fields import empty
|
||||
|
||||
from cookbook.helper.HelperFunctions import str2bool
|
||||
from cookbook.helper.shopping_helper import RecipeShoppingEditor
|
||||
@@ -21,7 +18,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
|
||||
RecipeImport, ShareLink, ShoppingList, ShoppingListEntry,
|
||||
ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory,
|
||||
SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile,
|
||||
UserPreference, ViewLog)
|
||||
UserPreference, ViewLog, ExportLog)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
from recipes.settings import MEDIA_URL
|
||||
|
||||
@@ -92,7 +89,10 @@ class CustomOnHandField(serializers.Field):
|
||||
if request := self.context.get('request', None):
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
try:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
except AttributeError: # Anonymous users (using share links) don't have shared users
|
||||
shared_users = []
|
||||
return obj.onhand_users.filter(id__in=shared_users).exists()
|
||||
|
||||
def to_internal_value(self, data):
|
||||
@@ -405,7 +405,7 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
filter = Q(id__in=obj.substitute.all())
|
||||
if obj.substitute_siblings:
|
||||
filter |= Q(path__startswith=obj.path[:Food.steplen*(obj.depth-1)], depth=obj.depth)
|
||||
filter |= Q(path__startswith=obj.path[:Food.steplen * (obj.depth - 1)], depth=obj.depth)
|
||||
if obj.substitute_children:
|
||||
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
|
||||
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
|
||||
@@ -644,7 +644,7 @@ class CustomFilterSerializer(SpacedModelSerializer, WritableNestedModelSerialize
|
||||
|
||||
class Meta:
|
||||
model = CustomFilter
|
||||
fields = ('id', 'name', 'search', 'shared', 'created_by')
|
||||
fields = ('id', 'name', 'search', 'shared', 'created_by')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ class RecipeBookSerializer(SpacedModelSerializer, WritableNestedModelSerializer)
|
||||
class Meta:
|
||||
model = RecipeBook
|
||||
fields = ('id', 'name', 'description', 'icon', 'shared', 'created_by', 'filter')
|
||||
read_only_fields = ('created_by', )
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
class RecipeBookEntrySerializer(serializers.ModelSerializer):
|
||||
@@ -731,11 +731,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
value = Decimal(value)
|
||||
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
|
||||
return (
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
obj.name
|
||||
or getattr(obj.mealplan, 'title', None)
|
||||
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
|
||||
or obj.recipe.name
|
||||
) + f' ({value:.2g})'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# TODO remove once old shopping list
|
||||
@@ -895,6 +895,19 @@ class ImportLogSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
class ExportLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['created_by'] = self.context['request'].user
|
||||
validated_data['space'] = self.context['request'].space
|
||||
return super().create(validated_data)
|
||||
|
||||
class Meta:
|
||||
model = ExportLog
|
||||
fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at')
|
||||
read_only_fields = ('created_by',)
|
||||
|
||||
|
||||
class AutomationSerializer(serializers.ModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
|
||||
7
cookbook/static/css/app.min.css
vendored
7
cookbook/static/css/app.min.css
vendored
@@ -1140,3 +1140,10 @@
|
||||
min-width: 28rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print{
|
||||
#switcher{
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load i18n %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
|
||||
{% block title %}{% trans 'Export Recipes' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{{ form.media }}
|
||||
|
||||
{% block content %}
|
||||
<div id="app">
|
||||
<export-view></export-view>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.EXPORT_ID = {{pk}};
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}';
|
||||
</script>
|
||||
|
||||
{% render_bundle 'export_view' %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans 'Export' %}</h2>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button class="btn btn-success" type="submit"><i class="fas fa-file-export"></i> {% trans 'Export' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
cookbook/templates/export_response.html
Normal file
32
cookbook/templates/export_response.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Export' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="app">
|
||||
<export-response-view></export-response-view>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
window.EXPORT_ID = {{pk}};
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'export_response_view' %}
|
||||
{% endblock %}
|
||||
@@ -26,7 +26,7 @@
|
||||
<small class="card-title">{{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}</small> <a
|
||||
href="{% url 'edit_comment' c.pk %}" class="d-print-none"><i
|
||||
class="fas fa-pencil-alt"></i></a><br/>
|
||||
{{ c.text }}
|
||||
{{ c.text | urlize |linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
@@ -217,13 +217,13 @@
|
||||
</div>
|
||||
|
||||
<script type="application/javascript">
|
||||
$(function () {
|
||||
$(function() {
|
||||
$('#id_search-trigram_threshold').get(0).type = 'range';
|
||||
})
|
||||
});
|
||||
|
||||
function applyPreset (preset){
|
||||
$('#id_search-preset').val(preset)
|
||||
$('#search_form_button').click()
|
||||
function applyPreset(preset) {
|
||||
$('#id_search-preset').val(preset);
|
||||
$('#search_form_button').click();
|
||||
}
|
||||
|
||||
function copyToken() {
|
||||
@@ -239,29 +239,30 @@
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.nav-tabs a').on('shown.bs.tab', function (e) {
|
||||
$('.nav-tabs a').on('shown.bs.tab', function(e) {
|
||||
window.location.hash = e.target.hash;
|
||||
})
|
||||
|
||||
{% comment %}
|
||||
// listen for events
|
||||
{% comment %} $(document).ready(function(){
|
||||
$(document).ready(function() {
|
||||
hideShow()
|
||||
// call hideShow when the user clicks on the mealplan_autoadd checkbox
|
||||
$("#id_shopping-mealplan_autoadd_shopping").click(function(event){
|
||||
hideShow()
|
||||
$("#id_shopping-mealplan_autoadd_shopping").click(function(event) {
|
||||
hideShow();
|
||||
});
|
||||
})
|
||||
|
||||
function hideShow(){
|
||||
if(document.getElementById('id_shopping-mealplan_autoadd_shopping').checked == true)
|
||||
{
|
||||
$('#div_id_shopping-mealplan_autoexclude_onhand').show();
|
||||
$('#div_id_shopping-mealplan_autoinclude_related').show();
|
||||
}
|
||||
else
|
||||
{
|
||||
function hideShow() {
|
||||
if(document.getElementById('id_shopping-mealplan_autoadd_shopping').checked == true) {
|
||||
$('#div_id_shopping-mealplan_autoexclude_onhand').show();
|
||||
$('#div_id_shopping-mealplan_autoinclude_related').show();
|
||||
}
|
||||
else {
|
||||
$('#div_id_shopping-mealplan_autoexclude_onhand').hide();
|
||||
$('#div_id_shopping-mealplan_autoinclude_related').hide();
|
||||
} {% endcomment %}
|
||||
}
|
||||
}
|
||||
{% endcomment %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Recipe
|
||||
from cookbook.models import Recipe, ShareLink
|
||||
from cookbook.tests.conftest import get_random_json_recipe, validate_recipe
|
||||
|
||||
LIST_URL = 'api:recipe-list'
|
||||
@@ -38,6 +38,21 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 1
|
||||
|
||||
|
||||
def test_share_permission(recipe_1_s1, u1_s1, u1_s2, a_u):
|
||||
assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 200
|
||||
assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 404
|
||||
|
||||
with scopes_disabled():
|
||||
r = u1_s1.get(reverse('new_share_link', kwargs={'pk': recipe_1_s1.pk}))
|
||||
assert r.status_code == 302
|
||||
r = u1_s2.get(reverse('new_share_link', kwargs={'pk': recipe_1_s1.pk}))
|
||||
assert r.status_code == 404
|
||||
share = ShareLink.objects.filter(recipe=recipe_1_s1).first()
|
||||
assert a_u.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
|
||||
assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200
|
||||
assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 404 # TODO fix in https://github.com/TandoorRecipes/recipes/issues/1238
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
['g1_s1', 200],
|
||||
|
||||
25
cookbook/tests/other/test_export.py
Normal file
25
cookbook/tests/other/test_export.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.models import ExportLog
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def obj_1(space_1, u1_s1):
|
||||
return ExportLog.objects.create(type=ImportExportBase.DEFAULT, running=False, created_by=auth.get_user(u1_s1), space=space_1, exported_recipes=10, total_recipes=10)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 302],
|
||||
['g1_s1', 302],
|
||||
['u1_s1', 200],
|
||||
['a1_s1', 200],
|
||||
['u1_s2', 404],
|
||||
['a1_s2', 404],
|
||||
])
|
||||
def test_export_file_cache(arg, request, obj_1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
assert c.get(reverse('view_export_file', args=[obj_1.pk])).status_code == arg[1]
|
||||
@@ -81,7 +81,7 @@ def test_history(arg, request, ext_recipe_1_s1):
|
||||
['a_u', 302],
|
||||
['g1_s1', 302],
|
||||
['u1_s1', 302],
|
||||
['a1_s1', 200],
|
||||
['a1_s1', 302],
|
||||
])
|
||||
def test_system(arg, request, ext_recipe_1_s1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
|
||||
@@ -23,6 +23,7 @@ router.register(r'custom-filter', api.CustomFilterViewSet)
|
||||
router.register(r'food', api.FoodViewSet)
|
||||
router.register(r'food-inherit-field', api.FoodInheritFieldViewSet)
|
||||
router.register(r'import-log', api.ImportLogViewSet)
|
||||
router.register(r'export-log', api.ExportLogViewSet)
|
||||
router.register(r'ingredient', api.IngredientViewSet)
|
||||
router.register(r'keyword', api.KeywordViewSet)
|
||||
router.register(r'meal-plan', api.MealPlanViewSet)
|
||||
@@ -76,6 +77,8 @@ urlpatterns = [
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
|
||||
path('export/', import_export.export_recipe, name='view_export'),
|
||||
path('export-response/<int:pk>/', import_export.export_response, name='view_export_response'),
|
||||
path('export-file/<int:pk>/', import_export.export_file, name='view_export_file'),
|
||||
|
||||
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
||||
path('view/recipe/<int:pk>/<slug:share>', views.recipe_view, name='view_recipe'),
|
||||
|
||||
@@ -48,11 +48,13 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
|
||||
ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket,
|
||||
SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit,
|
||||
UserFile, UserPreference, ViewLog)
|
||||
from cookbook.models import (ExportLog)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
||||
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
|
||||
ExportLogSerializer,
|
||||
CookLogSerializer, CustomFilterSerializer,
|
||||
FoodInheritFieldSerializer, FoodSerializer,
|
||||
FoodShoppingUpdateSerializer, ImportLogSerializer,
|
||||
@@ -660,11 +662,13 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
share = self.request.query_params.get('share', None)
|
||||
|
||||
if self.detail:
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
if not share:
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
return super().get_queryset()
|
||||
|
||||
share = self.request.query_params.get('share', None)
|
||||
if not (share and self.detail):
|
||||
self.queryset = self.queryset.filter(space=self.request.space)
|
||||
|
||||
@@ -872,6 +876,17 @@ class ImportLogViewSet(viewsets.ModelViewSet):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
class ExportLogViewSet(viewsets.ModelViewSet):
|
||||
queryset = ExportLog.objects
|
||||
serializer_class = ExportLogSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(space=self.request.space)
|
||||
|
||||
|
||||
|
||||
class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
||||
queryset = BookmarkletImport.objects
|
||||
serializer_class = BookmarkletImportSerializer
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import re
|
||||
import threading
|
||||
from io import BytesIO
|
||||
from django.core.cache import cache
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@@ -29,7 +30,7 @@ from cookbook.integration.recipesage import RecipeSage
|
||||
from cookbook.integration.rezkonv import RezKonv
|
||||
from cookbook.integration.saffron import Saffron
|
||||
from cookbook.integration.pdfexport import PDFexport
|
||||
from cookbook.models import Recipe, ImportLog, UserPreference
|
||||
from cookbook.models import Recipe, ImportLog, ExportLog, UserPreference
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@@ -123,25 +124,57 @@ def export_recipe(request):
|
||||
if form.cleaned_data['all']:
|
||||
recipes = Recipe.objects.filter(space=request.space, internal=True).all()
|
||||
|
||||
if form.cleaned_data['type'] == ImportExportBase.PDF and not settings.ENABLE_PDF_EXPORT:
|
||||
messages.add_message(request, messages.ERROR, _('The PDF Exporter is not enabled on this instance as it is still in an experimental state.'))
|
||||
return render(request, 'export.html', {'form': form})
|
||||
integration = get_integration(request, form.cleaned_data['type'])
|
||||
return integration.do_export(recipes)
|
||||
except NotImplementedError:
|
||||
messages.add_message(request, messages.ERROR, _('Exporting is not implemented for this provider'))
|
||||
|
||||
if form.cleaned_data['type'] == ImportExportBase.PDF and not settings.ENABLE_PDF_EXPORT:
|
||||
return JsonResponse({'error': _('The PDF Exporter is not enabled on this instance as it is still in an experimental state.')})
|
||||
|
||||
el = ExportLog.objects.create(type=form.cleaned_data['type'], created_by=request.user, space=request.space)
|
||||
|
||||
t = threading.Thread(target=integration.do_export, args=[recipes, el])
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
return JsonResponse({'export_id': el.pk})
|
||||
except NotImplementedError:
|
||||
return JsonResponse(
|
||||
{
|
||||
'error': True,
|
||||
'msg': _('Importing is not implemented for this provider')
|
||||
},
|
||||
status=400
|
||||
)
|
||||
else:
|
||||
form = ExportForm(space=request.space)
|
||||
pk = ''
|
||||
recipe = request.GET.get('r')
|
||||
if recipe:
|
||||
if re.match(r'^([0-9])+$', recipe):
|
||||
if recipe := Recipe.objects.filter(pk=int(recipe), space=request.space).first():
|
||||
form = ExportForm(initial={'recipes': recipe}, space=request.space)
|
||||
pk = Recipe.objects.filter(pk=int(recipe), space=request.space).first().pk
|
||||
|
||||
return render(request, 'export.html', {'form': form})
|
||||
return render(request, 'export.html', {'pk': pk})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def import_response(request, pk):
|
||||
return render(request, 'import_response.html', {'pk': pk})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def export_response(request, pk):
|
||||
return render(request, 'export_response.html', {'pk': pk})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def export_file(request, pk):
|
||||
el = get_object_or_404(ExportLog, pk=pk, space=request.space)
|
||||
|
||||
cacheData = cache.get(f'export_file_{el.pk}')
|
||||
|
||||
if cacheData is None:
|
||||
el.possibly_not_expired = False
|
||||
el.save()
|
||||
return render(request, 'export_response.html', {'pk': pk})
|
||||
|
||||
response = HttpResponse(cacheData['file'], content_type='application/force-download')
|
||||
response['Content-Disposition'] = 'attachment; filename="' + cacheData['filename'] + '"'
|
||||
return response
|
||||
|
||||
@@ -446,6 +446,9 @@ def history(request):
|
||||
|
||||
@group_required('admin')
|
||||
def system(request):
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
postgres = False if (
|
||||
settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501
|
||||
or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501
|
||||
|
||||
Reference in New Issue
Block a user