Fix after rebase
This commit is contained in:
@ -7,8 +7,7 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
|
|||||||
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
|
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
|
||||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
||||||
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
||||||
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
|
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, ImportLog, TelegramBot)
|
||||||
ImportLog, TelegramBot, BookmarkletImport)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUserAdmin(UserAdmin):
|
class CustomUserAdmin(UserAdmin):
|
||||||
@ -223,13 +222,6 @@ class ImportLogAdmin(admin.ModelAdmin):
|
|||||||
admin.site.register(ImportLog, ImportLogAdmin)
|
admin.site.register(ImportLog, ImportLogAdmin)
|
||||||
|
|
||||||
|
|
||||||
class BookmarkletImportAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('id', 'url', 'created_by', 'created_at',)
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(BookmarkletImport, BookmarkletImportAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
class TelegramBotAdmin(admin.ModelAdmin):
|
class TelegramBotAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'name', 'created_by',)
|
list_display = ('id', 'name', 'created_by',)
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ with scopes_disabled():
|
|||||||
model = Recipe
|
model = Recipe
|
||||||
fields = ['name', 'keywords', 'foods', 'internal']
|
fields = ['name', 'keywords', 'foods', 'internal']
|
||||||
|
|
||||||
|
|
||||||
class FoodFilter(django_filters.FilterSet):
|
class FoodFilter(django_filters.FilterSet):
|
||||||
name = django_filters.CharFilter(lookup_expr='icontains')
|
name = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ with scopes_disabled():
|
|||||||
model = Food
|
model = Food
|
||||||
fields = ['name']
|
fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListFilter(django_filters.FilterSet):
|
class ShoppingListFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
@ -75,9 +75,11 @@ def get_recipe_from_source(text, url, space):
|
|||||||
images = []
|
images = []
|
||||||
text = unquote(text)
|
text = unquote(text)
|
||||||
|
|
||||||
# text = normalize_string(text)
|
|
||||||
try:
|
try:
|
||||||
parse_list.append(remove_graph(json.loads(text)))
|
parse_list.append(remove_graph(json.loads(text)))
|
||||||
|
if not url and 'url' in parse_list[0]:
|
||||||
|
url = parse_list[0]['url']
|
||||||
|
scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
|
||||||
|
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
soup = BeautifulSoup(text, "html.parser")
|
||||||
@ -85,6 +87,8 @@ def get_recipe_from_source(text, url, space):
|
|||||||
images += get_images_from_source(soup, url)
|
images += get_images_from_source(soup, url)
|
||||||
for el in soup.find_all('script', type='application/ld+json'):
|
for el in soup.find_all('script', type='application/ld+json'):
|
||||||
el = remove_graph(el)
|
el = remove_graph(el)
|
||||||
|
if not url and 'url' in el:
|
||||||
|
url = el['url']
|
||||||
if type(el) == list:
|
if type(el) == list:
|
||||||
for le in el:
|
for le in el:
|
||||||
parse_list.append(le)
|
parse_list.append(le)
|
||||||
@ -97,15 +101,6 @@ def get_recipe_from_source(text, url, space):
|
|||||||
parse_list.append(le)
|
parse_list.append(le)
|
||||||
elif type(el) == dict:
|
elif type(el) == dict:
|
||||||
parse_list.append(el)
|
parse_list.append(el)
|
||||||
|
|
||||||
# if a url was not provided, try to find one in the first document
|
|
||||||
if not url and len(parse_list) > 0:
|
|
||||||
if 'url' in parse_list[0]:
|
|
||||||
url = parse_list[0]['url']
|
|
||||||
|
|
||||||
if type(text) == dict:
|
|
||||||
scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
|
|
||||||
elif type(text) == str:
|
|
||||||
scrape = text_scraper(text, url=url)
|
scrape = text_scraper(text, url=url)
|
||||||
|
|
||||||
recipe_json = helper.get_from_scraper(scrape, space)
|
recipe_json = helper.get_from_scraper(scrape, space)
|
||||||
|
@ -294,8 +294,8 @@ def parse_keywords(keyword_json, space):
|
|||||||
# keywords as list
|
# keywords as list
|
||||||
for kw in keyword_json:
|
for kw in keyword_json:
|
||||||
kw = normalize_string(kw)
|
kw = normalize_string(kw)
|
||||||
|
if len(kw) != 0:
|
||||||
if k := Keyword.objects.filter(name=kw, space=space).first():
|
if k := Keyword.objects.filter(name=kw, space=space).first():
|
||||||
if len(k) != 0:
|
|
||||||
keywords.append({'id': str(k.id), 'text': str(k)})
|
keywords.append({'id': str(k.id), 'text': str(k)})
|
||||||
else:
|
else:
|
||||||
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw})
|
keywords.append({'id': random.randrange(1111111, 9999999, 1), 'text': kw})
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
# Generated by Django 3.1.7 on 2021-03-29 11:05
|
|
||||||
|
|
||||||
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', '0119_auto_20210411_2101'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='userpreference',
|
|
||||||
name='use_fractions',
|
|
||||||
field=models.BooleanField(default=True),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='BookmarkletImport',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('html', models.TextField()),
|
|
||||||
('url', models.CharField(blank=True, max_length=128, null=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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -671,13 +671,3 @@ class ImportLog(models.Model, PermissionModelMixin):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.created_at}:{self.type}"
|
return f"{self.created_at}:{self.type}"
|
||||||
|
|
||||||
|
|
||||||
class BookmarkletImport(models.Model, PermissionModelMixin):
|
|
||||||
html = models.TextField()
|
|
||||||
url = models.CharField(max_length=128, null=True, blank=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)
|
|
@ -14,8 +14,7 @@ from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
|||||||
RecipeBook, RecipeBookEntry, RecipeImport,
|
RecipeBook, RecipeBookEntry, RecipeImport,
|
||||||
ShareLink, ShoppingList, ShoppingListEntry,
|
ShareLink, ShoppingList, ShoppingListEntry,
|
||||||
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
||||||
Unit, UserPreference, ViewLog, SupermarketCategory,
|
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation, ImportLog)
|
||||||
Supermarket, SupermarketCategoryRelation, ImportLog, BookmarkletImport)
|
|
||||||
from cookbook.templatetags.custom_tags import markdown
|
from cookbook.templatetags.custom_tags import markdown
|
||||||
|
|
||||||
|
|
||||||
@ -467,20 +466,6 @@ class ImportLogSerializer(serializers.ModelSerializer):
|
|||||||
read_only_fields = ('created_by',)
|
read_only_fields = ('created_by',)
|
||||||
|
|
||||||
|
|
||||||
# CORS, REST and Scopes aren't currently working
|
|
||||||
# Scopes are evaluating before REST has authenticated the user assiging a None space
|
|
||||||
# I've made the change below to fix the bookmarklet, other serializers likely need a similar/better fix
|
|
||||||
class BookmarkletImportSerializer(serializers.ModelSerializer):
|
|
||||||
def create(self, validated_data):
|
|
||||||
validated_data['created_by'] = self.context['request'].user
|
|
||||||
validated_data['space'] = self.context['request'].user.userpreference.space
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = BookmarkletImport
|
|
||||||
fields = ('id', 'url', 'html', 'created_by', 'created_at')
|
|
||||||
read_only_fields = ('created_by', 'space')
|
|
||||||
|
|
||||||
# Export/Import Serializers
|
# Export/Import Serializers
|
||||||
|
|
||||||
class KeywordExportSerializer(KeywordSerializer):
|
class KeywordExportSerializer(KeywordSerializer):
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
(function(){
|
|
||||||
|
|
||||||
var v = "1.3.2";
|
|
||||||
|
|
||||||
if (window.jQuery === undefined || window.jQuery.fn.jquery < v) {
|
|
||||||
var done = false;
|
|
||||||
var script = document.createElement("script");
|
|
||||||
script.src = "https://ajax.googleapis.com/ajax/libs/jquery/" + v + "/jquery.min.js";
|
|
||||||
script.onload = script.onreadystatechange = function(){
|
|
||||||
if (!done && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete")) {
|
|
||||||
done = true;
|
|
||||||
initBookmarklet();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.getElementsByTagName("head")[0].appendChild(script);
|
|
||||||
} else {
|
|
||||||
initBookmarklet();
|
|
||||||
}
|
|
||||||
function initBookmarklet() {
|
|
||||||
(window.bookmarkletTandoor = function() {
|
|
||||||
let recipe = document.documentElement.innerHTML
|
|
||||||
let windowName = "ImportRecipe"
|
|
||||||
let url = localStorage.getItem('importURL')
|
|
||||||
let redirect = localStorage.getItem('redirectURL')
|
|
||||||
let token = localStorage.getItem('token')
|
|
||||||
let params = { 'html' : recipe,'url': window.location.href};
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open('POST', url, true);
|
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
||||||
xhr.setRequestHeader('Authorization', 'Token ' + token);
|
|
||||||
|
|
||||||
// listen for `onload` event
|
|
||||||
xhr.onload = () => {
|
|
||||||
// process response
|
|
||||||
if (xhr.readyState == 4 && xhr.status == 201) {
|
|
||||||
// parse JSON data
|
|
||||||
window.open(redirect.concat('?id=', JSON.parse(xhr.response).id) )
|
|
||||||
} else {
|
|
||||||
console.error('Error!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send(JSON.stringify(params));
|
|
||||||
}
|
|
||||||
)();
|
|
||||||
}
|
|
||||||
})();
|
|
@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
|
<link rel="shortcut icon" type="image/x-icon" href="{% static 'assets/favicon.svg' %}">
|
||||||
<link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}">
|
<link rel="shortcut icon" href="{% static 'assets/favicon.svg' %}">
|
||||||
<link rel="icon" type="image/png" href="{% static 'assets/.svg' %}" sizes="32x32">
|
<link rel="icon" type="image/png" href="{% static 'assets/favicon-32x32.png' %}" sizes="32x32">
|
||||||
<link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16" >
|
<link rel="icon" type="image/png" href="{% static 'assets/favicon-16x16.png' %}" sizes="16x16">
|
||||||
|
|
||||||
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
|
<link rel="mask-icon" href="{% static 'assets/safari-pinned-tab.svg' %}" color="#161616">
|
||||||
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
|
<link rel="apple-touch-icon" href="{% static 'assets/apple-touch-icon.png' %}" sizes="180x180">
|
||||||
@ -24,10 +24,9 @@
|
|||||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
|
||||||
|
|
||||||
|
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616">
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#161616">
|
||||||
<meta name="msapplication-TileColor" content="#161616">
|
<meta name="msapplication-TileColor" content="#161616">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
|
@ -1,385 +0,0 @@
|
|||||||
<!--I PROBLABLY DON'T NEED THIS??-->
|
|
||||||
{% extends "base.html" %}
|
|
||||||
{% load crispy_forms_filters %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{% trans 'Import Recipe' %}{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
{% include 'include/vue_base.html' %}
|
|
||||||
|
|
||||||
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
|
|
||||||
<link rel="stylesheet" href="{% static 'css/vue-multiselect.min.css' %}">
|
|
||||||
<script src="{% static 'js/vue-jstree.js' %}"></script>
|
|
||||||
<style>
|
|
||||||
.tree-anchor {
|
|
||||||
width:95%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div id="app">
|
|
||||||
<div v-if="!parsed">
|
|
||||||
<h2>{% trans 'Import From Source' %}</h2>
|
|
||||||
<div class="input-group input-group-lg">
|
|
||||||
<textarea class="form-control" v-model="html_recipe" rows="12" style="font-size: 12px"></textarea>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">Simply paste a web page source or JSON document into this textarea and click import.</small>
|
|
||||||
<br>
|
|
||||||
<button @click="loadRecipe()" class="btn btn-success" type="button"
|
|
||||||
id="id_btn_import"><i class="fas fa-code"></i>{% trans 'Import' %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div v-if="loading" class="text-center">
|
|
||||||
<br/>
|
|
||||||
<i class="fas fa-spinner fa-spin fa-8x"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="recipe_data !== undefined">
|
|
||||||
<p>this is the recipe form
|
|
||||||
</template>
|
|
||||||
<div v-if="recipe_tree !== undefined" id="manage_tree">
|
|
||||||
<v-jstree :data="recipe_tree"
|
|
||||||
whole-row
|
|
||||||
collapse:true
|
|
||||||
draggable
|
|
||||||
@item-click="itemClick"
|
|
||||||
@item-drag-start="itemDragStart"
|
|
||||||
@item-drag-end="itemDragEnd"
|
|
||||||
@item-drop-before = "itemDropBefore"
|
|
||||||
@item-drop="itemDrop">
|
|
||||||
|
|
||||||
<template scope="_">
|
|
||||||
<div class="container-fluid" >
|
|
||||||
<div @click.ctrl="customItemClickWithCtrl">
|
|
||||||
<div class="row clearfix" style="width:80%; cursor: grab;" >
|
|
||||||
<i :class="_.vm.themeIconClasses" role="presentation" v-if="!_.model.loading"></i>
|
|
||||||
{% verbatim %}
|
|
||||||
[[_.model.name]]
|
|
||||||
{% endverbatim %}
|
|
||||||
<button style="border: 0px; background-color: transparent; cursor: pointer;"
|
|
||||||
@click="deleteNode(_.vm, _.model, $event)"><i class="fa fa-remove"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-jstree>
|
|
||||||
</div>
|
|
||||||
<template v-if="error !== undefined">
|
|
||||||
<p>something terrible happened
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
|
|
||||||
<script src="{% url 'javascript-catalog' %}"></script>
|
|
||||||
<script type="application/javascript">
|
|
||||||
let csrftoken = Cookies.get('csrftoken');
|
|
||||||
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
|
||||||
|
|
||||||
//Vue.component('vue-multiselect', window.VueMultiselect.default)
|
|
||||||
|
|
||||||
let app = new Vue({
|
|
||||||
components: {
|
|
||||||
Multiselect: window.VueMultiselect.default
|
|
||||||
},
|
|
||||||
delimiters: ['[[', ']]'],
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
html_recipe: '',
|
|
||||||
keywords: [],
|
|
||||||
keywords_loading: false,
|
|
||||||
units: [],
|
|
||||||
units_loading: false,
|
|
||||||
ingredients: [],
|
|
||||||
ingredients_loading: false,
|
|
||||||
recipe_data: undefined,
|
|
||||||
recipe_tree: [],
|
|
||||||
error: undefined,
|
|
||||||
loading: false,
|
|
||||||
all_keywords: false,
|
|
||||||
importing_recipe: false,
|
|
||||||
parsed: false,
|
|
||||||
searchText: '',
|
|
||||||
editingItem: {},
|
|
||||||
editingNode: null,
|
|
||||||
itemEvents: {
|
|
||||||
mouseover: function () {
|
|
||||||
console.log('mouseover')
|
|
||||||
},
|
|
||||||
contextmenu: function () {
|
|
||||||
console.log(arguments[2])
|
|
||||||
arguments[2].preventDefault()
|
|
||||||
console.log('contextmenu')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
directives: {
|
|
||||||
tabindex: {
|
|
||||||
inserted(el) {
|
|
||||||
el.setAttribute('tabindex', 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
|
||||||
this.searchKeywords('')
|
|
||||||
this.searchUnits('')
|
|
||||||
this.searchIngredients('')
|
|
||||||
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
makeToast: function (title, message, variant = null) {
|
|
||||||
//TODO remove duplicate function in favor of central one
|
|
||||||
this.$bvToast.toast(message, {
|
|
||||||
title: title,
|
|
||||||
variant: variant,
|
|
||||||
toaster: 'b-toaster-top-center',
|
|
||||||
solid: true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadRecipe: function () {
|
|
||||||
this.recipe_data = undefined
|
|
||||||
this.recipe_tree = undefined
|
|
||||||
this.error = undefined
|
|
||||||
this.parsed = true
|
|
||||||
this.loading = true
|
|
||||||
this.$http.post("{% url 'api_manual_recipe_from_json' %}", {'html_text': this.html_recipe}, {emulateJSON: true}).then((response) => {
|
|
||||||
console.log(response.data)
|
|
||||||
this.recipe_data = response.data['recipe_data'];
|
|
||||||
this.recipe_tree = response.data['recipe_tree'];
|
|
||||||
this.loading = false
|
|
||||||
}).catch((err) => {
|
|
||||||
this.error = err.data
|
|
||||||
this.loading = false
|
|
||||||
this.parsed = false
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
importRecipe: function () {
|
|
||||||
if (this.importing_recipe) {
|
|
||||||
this.makeToast(gettext('Error'), gettext('Already importing the selected recipe, please wait!'), 'danger')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.importing_recipe = true
|
|
||||||
this.$set(this.recipe_data, 'all_keywords', this.all_keywords)
|
|
||||||
this.$http.post(`{% url 'data_import_url' %}`, this.recipe_data).then((response) => {
|
|
||||||
window.location.href = response.data
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
this.makeToast(gettext('Error'), gettext('An error occurred while trying to import this recipe!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteIngredient: function (i) {
|
|
||||||
this.recipe_data.recipeIngredient = this.recipe_data.recipeIngredient.filter(item => item !== i)
|
|
||||||
},
|
|
||||||
addIngredient: function (i) {
|
|
||||||
this.recipe_data.recipeIngredient.push({
|
|
||||||
unit: {id: Math.random() * 1000, text: '{{ request.user.userpreference.default_unit }}'},
|
|
||||||
amount: 0,
|
|
||||||
ingredient: {id: Math.random() * 1000, text: ''}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addIngredientType: function (tag, index) {
|
|
||||||
index = index.replace('ingredient_', '')
|
|
||||||
let new_ingredient = this.recipe_data.recipeIngredient[index]
|
|
||||||
new_ingredient.ingredient = {'id': Math.random() * 1000, 'text': tag}
|
|
||||||
this.ingredients.push(new_ingredient.ingredient)
|
|
||||||
this.recipe_data.recipeIngredient[index] = new_ingredient
|
|
||||||
},
|
|
||||||
addUnitType: function (tag, index) {
|
|
||||||
index = index.replace('unit_', '')
|
|
||||||
let new_unit = this.recipe_data.recipeIngredient[index]
|
|
||||||
new_unit.unit = {'id': Math.random() * 1000, 'text': tag}
|
|
||||||
this.units.push(new_unit.unit)
|
|
||||||
this.recipe_data.recipeIngredient[index] = new_unit
|
|
||||||
},
|
|
||||||
addKeyword: function (tag) {
|
|
||||||
let new_keyword = {'text':tag,'id':null}
|
|
||||||
this.recipe_data.keywords.push(new_keyword)
|
|
||||||
},
|
|
||||||
openUnitSelect: function (id) {
|
|
||||||
let index = id.replace('unit_', '')
|
|
||||||
if (this.recipe_data.recipeIngredient[index].unit !== null) {
|
|
||||||
this.$set(app.$refs.unit[index].$data, 'search', this.recipe_data.recipeIngredient[index].unit.text)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openIngredientSelect: function (id) {
|
|
||||||
let index = id.replace('ingredient_', '')
|
|
||||||
this.$set(this.$refs.ingredient[index].$data, 'search', this.recipe_data.recipeIngredient[index].ingredient.text)
|
|
||||||
},
|
|
||||||
searchKeywords: function (query) {
|
|
||||||
this.keywords_loading = true
|
|
||||||
this.$http.get("{% url 'dal_keyword' %}" + '?q=' + query).then((response) => {
|
|
||||||
this.keywords = response.data.results;
|
|
||||||
this.keywords_loading = false
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
searchUnits: function (query) {
|
|
||||||
this.units_loading = true
|
|
||||||
this.$http.get("{% url 'dal_unit' %}" + '?q=' + query).then((response) => {
|
|
||||||
this.units = response.data.results;
|
|
||||||
if (this.recipe_data !== undefined) {
|
|
||||||
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
|
||||||
if (x.unit !== null && x.unit.text !== '') {
|
|
||||||
this.units = this.units.filter(item => item.text !== x.unit.text)
|
|
||||||
this.units.push(x.unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.units_loading = false
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
searchIngredients: function (query) {
|
|
||||||
this.ingredients_loading = true
|
|
||||||
this.$http.get("{% url 'dal_food' %}" + '?q=' + query).then((response) => {
|
|
||||||
this.ingredients = response.data.results
|
|
||||||
if (this.recipe_data !== undefined) {
|
|
||||||
for (let x of Array.from(this.recipe_data.recipeIngredient)) {
|
|
||||||
if (x.ingredient.text !== '') {
|
|
||||||
this.ingredients = this.ingredients.filter(item => item.text !== x.ingredient.text)
|
|
||||||
this.ingredients.push(x.ingredient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ingredients_loading = false
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
itemClick (node) {
|
|
||||||
this.editingNode = node
|
|
||||||
this.editingItem = node.model
|
|
||||||
console.log(node.model.text + ' clicked !')
|
|
||||||
},
|
|
||||||
itemDragStart (node) {
|
|
||||||
console.log(node.model.text + ' drag start !')
|
|
||||||
},
|
|
||||||
itemDragEnd (node) {
|
|
||||||
console.log(node.model.text + ' drag end !')
|
|
||||||
},
|
|
||||||
itemDropBefore (node, item, draggedItem , e) {
|
|
||||||
if (!draggedItem) {
|
|
||||||
item.addChild({
|
|
||||||
text: "newNode",
|
|
||||||
value: "newNode"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemDrop (node, item, draggedItem , e) {
|
|
||||||
var sortBy = function(attr,rev) {
|
|
||||||
if (rev == undefined) {
|
|
||||||
rev = 1;
|
|
||||||
} else {
|
|
||||||
rev = (rev) ? 1 : -1;
|
|
||||||
}
|
|
||||||
return function (a, b) {
|
|
||||||
a = a[attr];
|
|
||||||
b = b[attr];
|
|
||||||
if (a < b) {
|
|
||||||
return rev * -1;
|
|
||||||
}
|
|
||||||
if (a > b) {
|
|
||||||
return rev * 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item.children.sort(sortBy('text', true))
|
|
||||||
this.$refs.tree.handleRecursionNodeChildren(draggedItem, function (childrenItem) {
|
|
||||||
childrenItem.selected = item.selected
|
|
||||||
})
|
|
||||||
console.log(node.model.text + ' drop !')
|
|
||||||
},
|
|
||||||
inputKeyUp: function () {
|
|
||||||
var text = this.searchText
|
|
||||||
const patt = new RegExp(text);
|
|
||||||
this.$refs.tree.handleRecursionNodeChilds(this.$refs.tree, function (node) {
|
|
||||||
if (text !== '' && node.model !== undefined) {
|
|
||||||
const str = node.model.text
|
|
||||||
if (patt.test(str)) {
|
|
||||||
node.$el.querySelector('.tree-anchor').style.color = 'red'
|
|
||||||
} else {
|
|
||||||
node.$el.querySelector('.tree-anchor').style.color = '#000'
|
|
||||||
} // or other operations
|
|
||||||
} else {
|
|
||||||
node.$el.querySelector('.tree-anchor').style.color = '#000'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addChildNode: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
this.editingItem.addChild({
|
|
||||||
text: "newNode",
|
|
||||||
value: "newNode"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeNode: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
var index = this.editingNode.parentItem.indexOf(this.editingItem)
|
|
||||||
this.editingNode.parentItem.splice(index, 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addBeforeNode: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
this.editingItem.addBefore({
|
|
||||||
text: "newNode",
|
|
||||||
value: "newNode"
|
|
||||||
}, this.editingNode)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addAfterNode: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
this.editingItem.addAfter({
|
|
||||||
text: "newNode",
|
|
||||||
value: "newNode"
|
|
||||||
}, this.editingNode)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openChildren: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
this.editingItem.openChildren()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeChildren: function () {
|
|
||||||
if (this.editingItem.id !== undefined) {
|
|
||||||
this.editingItem.closeChildren()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refreshNode: function () {
|
|
||||||
this.asyncData = [
|
|
||||||
this.$refs.tree2.initializeLoading()
|
|
||||||
]
|
|
||||||
this.$refs.tree2.handleAsyncLoad(this.asyncData, this.$refs.tree2)
|
|
||||||
},
|
|
||||||
customItemClick: function (node ,item, e) {
|
|
||||||
e.stopPropagation()
|
|
||||||
var index = node.parentItem.indexOf(item)
|
|
||||||
node.parentItem.splice(index, 1)
|
|
||||||
},
|
|
||||||
customItemClickWithCtrl: function () {
|
|
||||||
console.log('click + ctrl')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,7 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load custom_tags %}
|
|
||||||
|
|
||||||
{% block title %}{% trans 'URL Import' %}{% endblock %}
|
{% block title %}{% trans 'URL Import' %}{% endblock %}
|
||||||
|
|
||||||
@ -21,15 +20,9 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="row px-3" style="justify-content:space-between;">
|
<h2> {% trans 'Import' %} </h2>
|
||||||
<h2> {% trans 'Import' %}</h2>
|
|
||||||
<a class="btn btn-outline-info btn-sm"
|
|
||||||
style="height:50%"
|
|
||||||
href="{% bookmarklet request %}"
|
|
||||||
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}">
|
|
||||||
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
|
|
||||||
</div>
|
|
||||||
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
|
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
|
||||||
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url" aria-selected="true" @click="mode='url'">URL</a>
|
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url" aria-selected="true" @click="mode='url'">URL</a>
|
||||||
<a class="nav-link" href="#nav-app" data-toggle="tab" role="tab" aria-controls="nav-app" @click="mode='app'">App</a>
|
<a class="nav-link" href="#nav-app" data-toggle="tab" role="tab" aria-controls="nav-app" @click="mode='app'">App</a>
|
||||||
@ -41,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<!-- Import URL -->
|
<!-- Import URL -->
|
||||||
<div class="tab-pane fade show active" role="tabpanel">
|
<div class="tab-pane fade show active" id="nav-url" role="tabpanel">
|
||||||
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||||
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
|
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
|
||||||
<input type="radio" autocomplete="off" checked> Automatic
|
<input type="radio" autocomplete="off" checked> Automatic
|
||||||
@ -605,6 +598,7 @@
|
|||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
let csrftoken = Cookies.get('csrftoken');
|
let csrftoken = Cookies.get('csrftoken');
|
||||||
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
||||||
|
|
||||||
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
||||||
|
|
||||||
let app = new Vue({
|
let app = new Vue({
|
||||||
@ -625,7 +619,7 @@
|
|||||||
recipe_data: undefined,
|
recipe_data: undefined,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
preview: undefined,
|
preview: false,
|
||||||
preview_type: 'json',
|
preview_type: 'json',
|
||||||
all_keywords: false,
|
all_keywords: false,
|
||||||
importing_recipe: false,
|
importing_recipe: false,
|
||||||
@ -651,12 +645,7 @@
|
|||||||
this.searchKeywords('')
|
this.searchKeywords('')
|
||||||
this.searchUnits('')
|
this.searchUnits('')
|
||||||
this.searchIngredients('')
|
this.searchIngredients('')
|
||||||
let uri = window.location.search.substring(1);
|
|
||||||
let params = new URLSearchParams(uri);
|
|
||||||
q = params.get("id")
|
|
||||||
if (q) {
|
|
||||||
this.loadBookmarklet(q)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
makeToast: function (title, message, variant = null) {
|
makeToast: function (title, message, variant = null) {
|
||||||
@ -706,41 +695,6 @@
|
|||||||
this.makeToast(gettext('Error'), msg, 'danger')
|
this.makeToast(gettext('Error'), msg, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
loadBookmarklet: function(id_bkmk) {
|
|
||||||
let uri = window.location.search.substring(1);
|
|
||||||
let params = new URLSearchParams(uri);
|
|
||||||
q = params.get("id")
|
|
||||||
console.log(q)
|
|
||||||
this.error = undefined
|
|
||||||
this.loading = true
|
|
||||||
this.$http.get("{% url 'api:bookmarkletimport-list' %}?id=" + id_bkmk ).then((response) => {
|
|
||||||
console.log(response.data)
|
|
||||||
this.automatic = false
|
|
||||||
this.source_data = response.data[0]['html']
|
|
||||||
this.remote_url = response.data[0]['url']
|
|
||||||
this.loadSource()
|
|
||||||
}).catch((err) => {
|
|
||||||
this.error = err.data
|
|
||||||
this.loading = false
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error loading bookmarklet!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteBookmarklet: function(id_bkmk) {
|
|
||||||
this.error = undefined
|
|
||||||
this.$http.delete("{% url 'api:bookmarkletimport-list' %}" + id_bkmk +"/").then((response) => {
|
|
||||||
}).catch((err) => {
|
|
||||||
this.error = err.data
|
|
||||||
console.log(err)
|
|
||||||
this.makeToast(gettext('Error'), gettext('There was an error deleting bookmarklet!') + err.bodyText, 'danger')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadRecipeManual: function () {
|
|
||||||
this.error = undefined
|
|
||||||
this.preview = false
|
|
||||||
this.recipe_data = this.recipe_json
|
|
||||||
},
|
|
||||||
showRecipe: function() {
|
showRecipe: function() {
|
||||||
this.preview = false
|
this.preview = false
|
||||||
this.recipe_data = this.recipe_json
|
this.recipe_data = this.recipe_json
|
||||||
@ -761,12 +715,6 @@
|
|||||||
this.importing_recipe = true
|
this.importing_recipe = true
|
||||||
this.$set(this.recipe_data, 'all_keywords', this.all_keywords)
|
this.$set(this.recipe_data, 'all_keywords', this.all_keywords)
|
||||||
this.$http.post(`{% url 'data_import_url' %}`, this.recipe_data).then((response) => {
|
this.$http.post(`{% url 'data_import_url' %}`, this.recipe_data).then((response) => {
|
||||||
let uri = window.location.search.substring(1);
|
|
||||||
let params = new URLSearchParams(uri);
|
|
||||||
q = params.get("id")
|
|
||||||
if (q) {
|
|
||||||
this.deleteBookmarklet(q)
|
|
||||||
}
|
|
||||||
window.location.href = response.data
|
window.location.href = response.data
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import bleach
|
import bleach
|
||||||
import re
|
|
||||||
import markdown as md
|
import markdown as md
|
||||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||||
@ -7,10 +6,8 @@ from cookbook.helper.mdx_urlize import UrlizeExtension
|
|||||||
from cookbook.models import Space, get_model_name
|
from cookbook.models import Space, get_model_name
|
||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Avg
|
from django.db.models import Avg
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from recipes import settings
|
from recipes import settings
|
||||||
from rest_framework.authtoken.models import Token
|
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -109,29 +106,3 @@ def message_of_the_day():
|
|||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def is_debug():
|
def is_debug():
|
||||||
return settings.DEBUG
|
return settings.DEBUG
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def bookmarklet(request):
|
|
||||||
if request.is_secure():
|
|
||||||
prefix = "https://"
|
|
||||||
else:
|
|
||||||
prefix = "http://"
|
|
||||||
server = prefix + request.get_host()
|
|
||||||
# TODO is it safe to store the token in clear text in a bookmark?
|
|
||||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
|
||||||
api_token = Token.objects.create(user=request.user)
|
|
||||||
|
|
||||||
bookmark = "javascript: \
|
|
||||||
(function(){ \
|
|
||||||
if(window.bookmarkletTandoor!==undefined){ \
|
|
||||||
bookmarkletTandoor(); \
|
|
||||||
} else { \
|
|
||||||
localStorage.setItem('importURL', '" + server + reverse('api:bookmarkletimport-list') + "'); \
|
|
||||||
localStorage.setItem('redirectURL', '" + server + reverse('data_import_url') + "'); \
|
|
||||||
localStorage.setItem('token', '" + api_token.__str__() + "'); \
|
|
||||||
document.body.appendChild(document.createElement(\'script\')).src=\'" \
|
|
||||||
+ server + static('js/bookmarklet.js') + "? \
|
|
||||||
r=\'+Math.floor(Math.random()*999999999);}})();"
|
|
||||||
|
|
||||||
return re.sub(r"[\n\t\s]*", "", bookmark)
|
|
||||||
|
@ -128,10 +128,8 @@ ALLRECIPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AMERICAS_TEST_KITCHEN = {
|
AMERICAS_TEST_KITCHEN = {
|
||||||
"file": [
|
"file": ['americastestkitchen.html'],
|
||||||
'americastestkitchen.html',
|
'url': 'http://www.americastestkitchen.com/recipes/1771-pasta-with-chicken-broccoli-and-sun-dried-tomatoes',
|
||||||
'cookscountry.html'
|
|
||||||
],
|
|
||||||
"name": "Pasta with Chicken, Broccoli, and Sun-dried Tomatoes",
|
"name": "Pasta with Chicken, Broccoli, and Sun-dried Tomatoes",
|
||||||
"description": "This restaurant-chain classic can be as off-putting as a bad horror movie: drab colors, tough meat, and a main character—the pasta—with no bite.",
|
"description": "This restaurant-chain classic can be as off-putting as a bad horror movie: drab colors, tough meat, and a main character—the pasta—with no bite.",
|
||||||
"servings": 4,
|
"servings": 4,
|
||||||
@ -140,13 +138,12 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"image": "https://res.cloudinary.com/hksqkdlah/image/upload/ar_1:1,c_fill,dpr_2.0,f_auto,fl_lossy.progressive.strip_profile,g_faces:auto,q_auto:low,w_150/33255_sfs-pasta-with-chicken-broccoli-sun-dried-tomatoes-15",
|
"image": "https://res.cloudinary.com/hksqkdlah/image/upload/ar_1:1,c_fill,dpr_2.0,f_auto,fl_lossy.progressive.strip_profile,g_faces:auto,q_auto:low,w_150/33255_sfs-pasta-with-chicken-broccoli-sun-dried-tomatoes-15",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
{
|
{
|
||||||
"id": "21",
|
|
||||||
"text": "main courses"
|
"text": "main courses"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"recipeIngredient": [
|
"recipeIngredient": [
|
||||||
{
|
{
|
||||||
"amount": 4,
|
"amount": 4.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "tablespoons"
|
"text": "tablespoons"
|
||||||
},
|
},
|
||||||
@ -157,7 +154,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "4 tablespoons unsalted butter"
|
"original": "4 tablespoons unsalted butter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "pound"
|
"text": "pound"
|
||||||
},
|
},
|
||||||
@ -168,7 +165,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "1 pound boneless, skinless chicken breast, trimmed of fat and cut crosswise into 1/4-inch slices"
|
"original": "1 pound boneless, skinless chicken breast, trimmed of fat and cut crosswise into 1/4-inch slices"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "small"
|
"text": "small"
|
||||||
},
|
},
|
||||||
@ -190,7 +187,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": " Table salt"
|
"original": " Table salt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 6,
|
"amount": 6.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "medium"
|
"text": "medium"
|
||||||
},
|
},
|
||||||
@ -212,7 +209,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "1/4 teaspoon red pepper flakes"
|
"original": "1/4 teaspoon red pepper flakes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "teaspoons"
|
"text": "teaspoons"
|
||||||
},
|
},
|
||||||
@ -223,7 +220,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "2 teaspoons chopped fresh thyme leaves"
|
"original": "2 teaspoons chopped fresh thyme leaves"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "teaspoons"
|
"text": "teaspoons"
|
||||||
},
|
},
|
||||||
@ -234,7 +231,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "2 teaspoons all-purpose flour"
|
"original": "2 teaspoons all-purpose flour"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "cup"
|
"text": "cup"
|
||||||
},
|
},
|
||||||
@ -245,7 +242,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "1 cup dry white wine"
|
"original": "1 cup dry white wine"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "cups"
|
"text": "cups"
|
||||||
},
|
},
|
||||||
@ -256,7 +253,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "2 cups low-sodium chicken broth"
|
"original": "2 cups low-sodium chicken broth"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "bunch"
|
"text": "bunch"
|
||||||
},
|
},
|
||||||
@ -278,7 +275,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "1/2 pound penne pasta, ziti, cavatappi, or campanelle"
|
"original": "1/2 pound penne pasta, ziti, cavatappi, or campanelle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2,
|
"amount": 2.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "ounces"
|
"text": "ounces"
|
||||||
},
|
},
|
||||||
@ -289,7 +286,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "2 ounces grated Asiago cheese(1 cup), plus extra for serving"
|
"original": "2 ounces grated Asiago cheese(1 cup), plus extra for serving"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "jar"
|
"text": "jar"
|
||||||
},
|
},
|
||||||
@ -300,7 +297,7 @@ AMERICAS_TEST_KITCHEN = {
|
|||||||
"original": "1 jar oil-packed sun-dried tomato(7 to 8 1/2 ounces), rinsed, patted dry, and cut into 1/4-inch strips (about 1 cup)"
|
"original": "1 jar oil-packed sun-dried tomato(7 to 8 1/2 ounces), rinsed, patted dry, and cut into 1/4-inch strips (about 1 cup)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 1,
|
"amount": 1.0,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "tablespoon"
|
"text": "tablespoon"
|
||||||
},
|
},
|
||||||
@ -1705,7 +1702,7 @@ MADAME_DESSERT = {
|
|||||||
"servings": 6,
|
"servings": 6,
|
||||||
"prepTime": 0,
|
"prepTime": 0,
|
||||||
"cookTime": 20,
|
"cookTime": 20,
|
||||||
"image": "https://madamedessert.de/wp-content/uploads/2020/02/Madame-Dessert_Schokopudding-Schokoladenpudding-mit-echter-Schokolade-0238-scaled.jpg",
|
"image": "https://assets.madamedessert.de/wp-content/uploads/2020/02/25163328/Madame-Dessert_Schokopudding-Schokoladenpudding-mit-echter-Schokolade-0238-scaled.jpg",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
{
|
{
|
||||||
"id": 7588432,
|
"id": 7588432,
|
||||||
@ -1795,7 +1792,7 @@ MADAME_DESSERT = {
|
|||||||
"original": "1 TL Vanilleextrakt"
|
"original": "1 TL Vanilleextrakt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 125,
|
"amount": 150,
|
||||||
"unit": {
|
"unit": {
|
||||||
"text": "g",
|
"text": "g",
|
||||||
"id": 24254
|
"id": 24254
|
||||||
@ -1805,7 +1802,7 @@ MADAME_DESSERT = {
|
|||||||
"id": 42645
|
"id": 42645
|
||||||
},
|
},
|
||||||
"note": "",
|
"note": "",
|
||||||
"original": "125 g Zucker"
|
"original": "150 g Zucker"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 30,
|
"amount": 30,
|
||||||
|
File diff suppressed because one or more lines are too long
@ -36,7 +36,6 @@ router.register(r'recipe-book', api.RecipeBookViewSet)
|
|||||||
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
|
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
|
||||||
router.register(r'supermarket', api.SupermarketViewSet)
|
router.register(r'supermarket', api.SupermarketViewSet)
|
||||||
router.register(r'import-log', api.ImportLogViewSet)
|
router.register(r'import-log', api.ImportLogViewSet)
|
||||||
router.register(r'bookmarklet-import', api.BookmarkletImportViewSet)
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.index, name='index'),
|
path('', views.index, name='index'),
|
||||||
@ -60,6 +59,7 @@ urlpatterns = [
|
|||||||
path('test2/', views.test2, name='view_test2'),
|
path('test2/', views.test2, name='view_test2'),
|
||||||
|
|
||||||
path('import/', import_export.import_recipe, name='view_import'),
|
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/', import_export.export_recipe, name='view_export'),
|
||||||
|
|
||||||
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
path('view/recipe/<int:pk>', views.recipe_view, name='view_recipe'),
|
||||||
|
@ -13,11 +13,9 @@ from django.core import management
|
|||||||
from django.core.exceptions import FieldError, ValidationError
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import FileResponse, HttpResponse, JsonResponse, HttpResponseRedirect
|
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||||
from django.shortcuts import redirect, render, get_object_or_404
|
from django.shortcuts import redirect, get_object_or_404
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from icalendar import Calendar, Event
|
from icalendar import Calendar, Event
|
||||||
from rest_framework import decorators, viewsets, status
|
from rest_framework import decorators, viewsets, status
|
||||||
from rest_framework.exceptions import APIException, PermissionDenied, NotFound, MethodNotAllowed
|
from rest_framework.exceptions import APIException, PermissionDenied, NotFound, MethodNotAllowed
|
||||||
@ -29,14 +27,14 @@ from cookbook.helper.ingredient_parser import parse
|
|||||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
|
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
|
||||||
CustomIsOwner, CustomIsShare,
|
CustomIsOwner, CustomIsShare,
|
||||||
CustomIsShared, CustomIsUser,
|
CustomIsShared, CustomIsUser,
|
||||||
group_required)
|
group_required, share_link_valid)
|
||||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||||
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
||||||
MealType, Recipe, RecipeBook, ShoppingList,
|
MealType, Recipe, RecipeBook, ShoppingList,
|
||||||
ShoppingListEntry, ShoppingListRecipe, Step,
|
ShoppingListEntry, ShoppingListRecipe, Step,
|
||||||
Storage, Sync, SyncLog, Unit, UserPreference,
|
Storage, Sync, SyncLog, Unit, UserPreference,
|
||||||
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport)
|
ViewLog, RecipeBookEntry, Supermarket, ImportLog)
|
||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
from cookbook.provider.local import Local
|
from cookbook.provider.local import Local
|
||||||
from cookbook.provider.nextcloud import Nextcloud
|
from cookbook.provider.nextcloud import Nextcloud
|
||||||
@ -51,9 +49,7 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
|
|||||||
StorageSerializer, SyncLogSerializer,
|
StorageSerializer, SyncLogSerializer,
|
||||||
SyncSerializer, UnitSerializer,
|
SyncSerializer, UnitSerializer,
|
||||||
UserNameSerializer, UserPreferenceSerializer,
|
UserNameSerializer, UserPreferenceSerializer,
|
||||||
ViewLogSerializer, CookLogSerializer,
|
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer)
|
||||||
RecipeBookEntrySerializer, RecipeOverviewSerializer,
|
|
||||||
SupermarketSerializer, ImportLogSerializer, BookmarkletImportSerializer)
|
|
||||||
from recipes.settings import DEMO
|
from recipes.settings import DEMO
|
||||||
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
|
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
|
||||||
|
|
||||||
@ -414,15 +410,6 @@ class ImportLogViewSet(viewsets.ModelViewSet):
|
|||||||
return self.queryset.filter(space=self.request.space).all()
|
return self.queryset.filter(space=self.request.space).all()
|
||||||
|
|
||||||
|
|
||||||
class BookmarkletImportViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = BookmarkletImport.objects
|
|
||||||
serializer_class = BookmarkletImportSerializer
|
|
||||||
permission_classes = [CustomIsUser]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.queryset.filter(space=self.request.space).all()
|
|
||||||
|
|
||||||
|
|
||||||
# -------------- non django rest api views --------------------
|
# -------------- non django rest api views --------------------
|
||||||
|
|
||||||
def get_recipe_provider(recipe):
|
def get_recipe_provider(recipe):
|
||||||
|
@ -93,7 +93,9 @@ def batch_edit(request):
|
|||||||
msg = ngettext(
|
msg = ngettext(
|
||||||
'Batch edit done. %(count)d recipe was updated.',
|
'Batch edit done. %(count)d recipe was updated.',
|
||||||
'Batch edit done. %(count)d Recipes where updated.',
|
'Batch edit done. %(count)d Recipes where updated.',
|
||||||
count) % {'count': count, }
|
count) % {
|
||||||
|
'count': count,
|
||||||
|
}
|
||||||
messages.add_message(request, messages.SUCCESS, msg)
|
messages.add_message(request, messages.SUCCESS, msg)
|
||||||
|
|
||||||
return redirect('data_batch_edit')
|
return redirect('data_batch_edit')
|
||||||
@ -186,12 +188,7 @@ def import_url(request):
|
|||||||
|
|
||||||
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
|
return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
|
||||||
|
|
||||||
elif 'id' in request.GET:
|
return render(request, 'url_import.html', {})
|
||||||
context = {'bookmarklet': request.GET.get('id')}
|
|
||||||
else:
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
return render(request, 'url_import.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
class Object(object):
|
class Object(object):
|
||||||
|
@ -107,3 +107,7 @@ def export_recipe(request):
|
|||||||
|
|
||||||
return render(request, 'export.html', {'form': form})
|
return render(request, 'export.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def import_response(request, pk):
|
||||||
|
return render(request, 'import_response.html', {'pk': pk})
|
||||||
|
@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/2.0/ref/settings/
|
|||||||
import ast
|
import ast
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from corsheaders.defaults import default_headers
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
@ -75,7 +74,6 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.postgres',
|
'django.contrib.postgres',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
'corsheaders',
|
|
||||||
'django_filters',
|
'django_filters',
|
||||||
'crispy_forms',
|
'crispy_forms',
|
||||||
'emoji_picker',
|
'emoji_picker',
|
||||||
@ -97,7 +95,6 @@ SOCIALACCOUNT_PROVIDERS = ast.literal_eval(
|
|||||||
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
os.getenv('SOCIALACCOUNT_PROVIDERS') if os.getenv('SOCIALACCOUNT_PROVIDERS') else '{}')
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
@ -251,17 +248,5 @@ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|||||||
|
|
||||||
TEST_RUNNER = "cookbook.helper.CustomTestRunner.CustomTestRunner"
|
TEST_RUNNER = "cookbook.helper.CustomTestRunner.CustomTestRunner"
|
||||||
|
|
||||||
|
# future versions of django will make undeclared default django.db.models.BigAutoField which will force migrations on all models
|
||||||
# settings for cross site origin (CORS)
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
# all origins allowed to support bookmarklet
|
|
||||||
# all of this may or may not work with nginx or other web servers
|
|
||||||
# TODO make this user configureable - enable or disable bookmarklets
|
|
||||||
# TODO since token auth is enabled - this all should be https by default
|
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
|
||||||
|
|
||||||
# django rest_framework requires authentication header
|
|
||||||
#CORS_ALLOW_HEADERS = list(default_headers) + ['authentication',]
|
|
||||||
|
|
||||||
# enable CORS only for bookmarklet api and only for posts, get and options
|
|
||||||
CORS_URLS_REGEX = r'^/api/bookmarklet-import.*$'
|
|
||||||
CORS_ALLOW_METHODS = ['GET', 'OPTIONS', 'POST']
|
|
||||||
|
@ -31,8 +31,7 @@ Jinja2==2.11.3
|
|||||||
django-webpack-loader==0.7.0
|
django-webpack-loader==0.7.0
|
||||||
django-js-reverse==0.9.1
|
django-js-reverse==0.9.1
|
||||||
django-allauth==0.44.0
|
django-allauth==0.44.0
|
||||||
recipe-scrapers==12.2.1
|
recipe-scrapers==12.2.2
|
||||||
django-scopes==1.2.0
|
django-scopes==1.2.0
|
||||||
pytest==6.2.2
|
pytest==6.2.3
|
||||||
pytest-django==4.1.0
|
pytest-django==4.2.0
|
||||||
django-cors-headers==3.7.0
|
|
||||||
|
Reference in New Issue
Block a user