mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-08 23:58:15 -05:00
Merge branch 'develop' into feature/allauth
# Conflicts: # requirements.txt
This commit is contained in:
@@ -5,7 +5,7 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShareLink,
|
||||
ShoppingList, ShoppingListEntry, ShoppingListRecipe,
|
||||
Space, Step, Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog)
|
||||
ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation)
|
||||
|
||||
|
||||
class SpaceAdmin(admin.ModelAdmin):
|
||||
@@ -43,6 +43,18 @@ class SyncAdmin(admin.ModelAdmin):
|
||||
admin.site.register(Sync, SyncAdmin)
|
||||
|
||||
|
||||
class SupermarketCategoryInline(admin.TabularInline):
|
||||
model = SupermarketCategoryRelation
|
||||
|
||||
|
||||
class SupermarketAdmin(admin.ModelAdmin):
|
||||
inlines = (SupermarketCategoryInline,)
|
||||
|
||||
|
||||
admin.site.register(Supermarket, SupermarketAdmin)
|
||||
admin.site.register(SupermarketCategory)
|
||||
|
||||
|
||||
class SyncLogAdmin(admin.ModelAdmin):
|
||||
list_display = ('sync', 'status', 'msg', 'created_at')
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class RecipeFilter(django_filters.FilterSet):
|
||||
if settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2': # noqa: E501
|
||||
queryset = queryset \
|
||||
.annotate(similarity=TrigramSimilarity('name', value), ) \
|
||||
.filter(Q(similarity__gt=0.1) | Q(name__icontains=value)) \
|
||||
.filter(Q(similarity__gt=0.1) | Q(name__unaccent__icontains=value)) \
|
||||
.order_by('-similarity')
|
||||
else:
|
||||
queryset = queryset.filter(name__icontains=value)
|
||||
|
||||
@@ -212,7 +212,7 @@ class KeywordForm(forms.ModelForm):
|
||||
class FoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('name', 'recipe')
|
||||
fields = ('name', 'description', 'ignore_shopping', 'recipe', 'supermarket_category')
|
||||
widgets = {'recipe': SelectWidget}
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ class StorageForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Storage
|
||||
fields = ('name', 'method', 'username', 'password', 'token', 'url')
|
||||
fields = ('name', 'method', 'username', 'password', 'token', 'url', 'path')
|
||||
|
||||
help_texts = {
|
||||
'url': _('Leave empty for dropbox and enter only base url for nextcloud (<code>/remote.php/webdav/</code> is added automatically)'), # noqa: E501
|
||||
|
||||
18
cookbook/migrations/0101_storage_path.py
Normal file
18
cookbook/migrations/0101_storage_path.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-22 18:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0100_recipe_servings_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='storage',
|
||||
name='path',
|
||||
field=models.CharField(blank=True, default='', max_length=256),
|
||||
),
|
||||
]
|
||||
46
cookbook/migrations/0102_auto_20210125_1147.py
Normal file
46
cookbook/migrations/0102_auto_20210125_1147.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-25 10:47
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0101_storage_path'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Supermarket',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128, unique=True, validators=[django.core.validators.MinLengthValidator(1)])),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SupermarketCategory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=128, unique=True, validators=[django.core.validators.MinLengthValidator(1)])),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='storage',
|
||||
name='method',
|
||||
field=models.CharField(choices=[('DB', 'Dropbox'), ('NEXTCLOUD', 'Nextcloud'), ('LOCAL', 'Local')], default='DB', max_length=128),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='supermarket_category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.supermarketcategory'),
|
||||
),
|
||||
]
|
||||
18
cookbook/migrations/0103_food_ignore_shopping.py
Normal file
18
cookbook/migrations/0103_food_ignore_shopping.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-25 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0102_auto_20210125_1147'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='ignore_shopping',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
28
cookbook/migrations/0104_auto_20210125_2133.py
Normal file
28
cookbook/migrations/0104_auto_20210125_2133.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-25 20:33
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0103_food_ignore_shopping'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SupermarketCategoryRelation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order', models.IntegerField(default=0)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.supermarketcategory')),
|
||||
('supermarket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.supermarket')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='supermarket',
|
||||
name='categories',
|
||||
field=models.ManyToManyField(through='cookbook.SupermarketCategoryRelation', to='cookbook.SupermarketCategory'),
|
||||
),
|
||||
]
|
||||
24
cookbook/migrations/0105_auto_20210126_1604.py
Normal file
24
cookbook/migrations/0105_auto_20210126_1604.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-26 15:04
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0104_auto_20210125_2133'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='supermarketcategoryrelation',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_to_supermarket', to='cookbook.supermarketcategory'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='supermarketcategoryrelation',
|
||||
name='supermarket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_to_supermarket', to='cookbook.supermarket'),
|
||||
),
|
||||
]
|
||||
19
cookbook/migrations/0106_shoppinglist_supermarket.py
Normal file
19
cookbook/migrations/0106_shoppinglist_supermarket.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.5 on 2021-01-26 15:21
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0105_auto_20210126_1604'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shoppinglist',
|
||||
name='supermarket',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.supermarket'),
|
||||
),
|
||||
]
|
||||
@@ -114,7 +114,8 @@ class UserPreference(models.Model):
|
||||
class Storage(models.Model):
|
||||
DROPBOX = 'DB'
|
||||
NEXTCLOUD = 'NEXTCLOUD'
|
||||
STORAGE_TYPES = ((DROPBOX, 'Dropbox'), (NEXTCLOUD, 'Nextcloud'))
|
||||
LOCAL = 'LOCAL'
|
||||
STORAGE_TYPES = ((DROPBOX, 'Dropbox'), (NEXTCLOUD, 'Nextcloud'), (LOCAL, 'Local'))
|
||||
|
||||
name = models.CharField(max_length=128)
|
||||
method = models.CharField(
|
||||
@@ -124,6 +125,7 @@ class Storage(models.Model):
|
||||
password = models.CharField(max_length=128, blank=True, null=True)
|
||||
token = models.CharField(max_length=512, blank=True, null=True)
|
||||
url = models.URLField(blank=True, null=True)
|
||||
path = models.CharField(blank=True, default='', max_length=256)
|
||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self):
|
||||
@@ -142,6 +144,32 @@ class Sync(models.Model):
|
||||
return self.path
|
||||
|
||||
|
||||
class SupermarketCategory(models.Model):
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Supermarket(models.Model):
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
description = models.TextField(blank=True, null=True)
|
||||
categories = models.ManyToManyField(SupermarketCategory, through='SupermarketCategoryRelation')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class SupermarketCategoryRelation(models.Model):
|
||||
supermarket = models.ForeignKey(Supermarket, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
||||
category = models.ForeignKey(SupermarketCategory, on_delete=models.CASCADE, related_name='category_to_supermarket')
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ('order',)
|
||||
|
||||
|
||||
class SyncLog(models.Model):
|
||||
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
|
||||
status = models.CharField(max_length=32)
|
||||
@@ -167,9 +195,7 @@ class Keyword(models.Model):
|
||||
|
||||
|
||||
class Unit(models.Model):
|
||||
name = models.CharField(
|
||||
unique=True, max_length=128, validators=[MinLengthValidator(1)]
|
||||
)
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
@@ -177,12 +203,11 @@ class Unit(models.Model):
|
||||
|
||||
|
||||
class Food(models.Model):
|
||||
name = models.CharField(
|
||||
unique=True, max_length=128, validators=[MinLengthValidator(1)]
|
||||
)
|
||||
recipe = models.ForeignKey(
|
||||
'Recipe', null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
name = models.CharField(unique=True, max_length=128, validators=[MinLengthValidator(1)])
|
||||
recipe = models.ForeignKey('Recipe', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
supermarket_category = models.ForeignKey(SupermarketCategory, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
ignore_shopping = models.BooleanField(default=False)
|
||||
description = models.TextField(default='', blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -408,9 +433,8 @@ class ShoppingList(models.Model):
|
||||
note = models.TextField(blank=True, null=True)
|
||||
recipes = models.ManyToManyField(ShoppingListRecipe, blank=True)
|
||||
entries = models.ManyToManyField(ShoppingListEntry, blank=True)
|
||||
shared = models.ManyToManyField(
|
||||
User, blank=True, related_name='list_share'
|
||||
)
|
||||
shared = models.ManyToManyField(User, blank=True, related_name='list_share')
|
||||
supermarket = models.ForeignKey(Supermarket, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
finished = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
59
cookbook/provider/local.py
Normal file
59
cookbook/provider/local.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
|
||||
from cookbook.models import Recipe, RecipeImport, SyncLog
|
||||
from cookbook.provider.provider import Provider
|
||||
|
||||
|
||||
class Local(Provider):
|
||||
|
||||
@staticmethod
|
||||
def import_all(monitor):
|
||||
files = [f for f in listdir(monitor.path) if isfile(join(monitor.path, f))]
|
||||
|
||||
import_count = 0
|
||||
for file in files:
|
||||
path = monitor.path + '/' + file
|
||||
if not Recipe.objects.filter(file_path__iexact=path).exists() \
|
||||
and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501
|
||||
name = os.path.splitext(file)[0]
|
||||
new_recipe = RecipeImport(
|
||||
name=name,
|
||||
file_path=path,
|
||||
storage=monitor.storage
|
||||
)
|
||||
new_recipe.save()
|
||||
import_count += 1
|
||||
|
||||
log_entry = SyncLog(
|
||||
status='SUCCESS',
|
||||
msg='Imported ' + str(import_count) + ' recipes',
|
||||
sync=monitor
|
||||
)
|
||||
log_entry.save()
|
||||
|
||||
monitor.last_checked = datetime.now()
|
||||
monitor.save()
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_file(recipe):
|
||||
file = io.BytesIO(open(recipe.file_path, 'rb').read())
|
||||
|
||||
return file
|
||||
|
||||
@staticmethod
|
||||
def rename_file(recipe, new_name):
|
||||
os.rename(recipe.file_path, os.path.join(os.path.dirname(recipe.file_path), (new_name + os.path.splitext(recipe.file_path)[1])))
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def delete_file(recipe):
|
||||
os.remove(recipe.file_path)
|
||||
return True
|
||||
@@ -20,6 +20,8 @@ class Nextcloud(Provider):
|
||||
'webdav_password': storage.password,
|
||||
'webdav_root': '/remote.php/dav/files/' + storage.username
|
||||
}
|
||||
if storage.path != '':
|
||||
options['webdav_root'] = storage.path
|
||||
return wc.Client(options)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -11,7 +11,7 @@ from cookbook.models import (Comment, CookLog, Food, Ingredient, Keyword,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
ShareLink, ShoppingList, ShoppingListEntry,
|
||||
ShoppingListRecipe, Step, Storage, Sync, SyncLog,
|
||||
Unit, UserPreference, ViewLog)
|
||||
Unit, UserPreference, ViewLog, SupermarketCategory, Supermarket, SupermarketCategoryRelation)
|
||||
from cookbook.templatetags.custom_tags import markdown
|
||||
|
||||
|
||||
@@ -140,7 +140,40 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
# since multi select tags dont have id's
|
||||
# duplicate names might be routed to create
|
||||
obj, created = SupermarketCategory.objects.get_or_create(**validated_data)
|
||||
return obj
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
return super(SupermarketCategorySerializer, self).update(instance, validated_data)
|
||||
|
||||
class Meta:
|
||||
model = SupermarketCategory
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
class SupermarketCategoryRelationSerializer(serializers.ModelSerializer):
|
||||
category = SupermarketCategorySerializer()
|
||||
|
||||
class Meta:
|
||||
model = SupermarketCategoryRelation
|
||||
fields = ('id', 'category', 'supermarket', 'order')
|
||||
|
||||
|
||||
class SupermarketSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
category_to_supermarket = SupermarketCategoryRelationSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Supermarket
|
||||
fields = ('id', 'name', 'category_to_supermarket')
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer):
|
||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
# since multi select tags dont have id's
|
||||
@@ -153,8 +186,7 @@ class FoodSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name', 'recipe')
|
||||
read_only_fields = ('id',)
|
||||
fields = ('id', 'name', 'recipe', 'ignore_shopping', 'supermarket_category')
|
||||
|
||||
|
||||
class IngredientSerializer(WritableNestedModelSerializer):
|
||||
@@ -310,7 +342,7 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
food = FoodSerializer(allow_null=True)
|
||||
unit = UnitSerializer(allow_null=True)
|
||||
unit = UnitSerializer(allow_null=True, required=False)
|
||||
amount = CustomDecimalField()
|
||||
|
||||
class Meta:
|
||||
@@ -318,7 +350,6 @@ class ShoppingListEntrySerializer(WritableNestedModelSerializer):
|
||||
fields = (
|
||||
'id', 'list_recipe', 'food', 'unit', 'amount', 'order', 'checked'
|
||||
)
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
class ShoppingListEntryCheckedSerializer(serializers.ModelSerializer):
|
||||
@@ -331,12 +362,13 @@ class ShoppingListSerializer(WritableNestedModelSerializer):
|
||||
recipes = ShoppingListRecipeSerializer(many=True, allow_null=True)
|
||||
entries = ShoppingListEntrySerializer(many=True, allow_null=True)
|
||||
shared = UserNameSerializer(many=True)
|
||||
supermarket = SupermarketSerializer(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = ShoppingList
|
||||
fields = (
|
||||
'id', 'uuid', 'note', 'recipes', 'entries',
|
||||
'shared', 'finished', 'created_by', 'created_at'
|
||||
'shared', 'finished', 'supermarket', 'created_by', 'created_at'
|
||||
)
|
||||
read_only_fields = ('id',)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -95,7 +95,7 @@
|
||||
class="fas fa-edit fa-fw"></i> {% trans 'Batch Edit' %}</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_storage,data_sync,list_recipe_import,list_sync_log,data_stats,edit_food' %}active{% endif %}">
|
||||
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'list_storage,data_sync,list_recipe_import,list_sync_log,data_stats,edit_food,edit_storage' %}active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false"><i class="fas fa-database"></i> {% trans 'Storage Data' %}
|
||||
</a>
|
||||
@@ -121,6 +121,9 @@
|
||||
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{% if user.is_authenticated %}
|
||||
{% page_help request.resolver_match.url_name as help_button %}
|
||||
{% if help_button %}{{ help_button|safe }}{% endif %}
|
||||
|
||||
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings,view_history,view_system,docs_markdown' %}active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false"><i
|
||||
|
||||
@@ -109,23 +109,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-striped" style="margin-top: 1vh">
|
||||
<table class="table table-sm" style="margin-top: 1vh">
|
||||
|
||||
<tbody is="draggable" group="people" :list="display_entries" tag="tbody" :empty-insert-threshold="10"
|
||||
handle=".handle" @sort="sortEntries()">
|
||||
|
||||
<tr v-for="(element, index) in display_entries" :key="element.id">
|
||||
<!--<td class="handle"><i class="fas fa-sort"></i></td>-->
|
||||
<td>[[element.amount]]</td>
|
||||
<td>[[element.unit.name]]</td>
|
||||
<td>[[element.food.name]]</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" v-if="element.list_recipe === null"
|
||||
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)">
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<template v-for="c in display_categories">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="5">[[c.name]]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries"
|
||||
@change="dragChanged(c, $event)">
|
||||
<tr v-for="(element, index) in c.entries" :key="element.id">
|
||||
<td class="handle"><i class="fas fa-sort"></i></td>
|
||||
<td>[[element.amount]]</td>
|
||||
<td>[[element.unit.name]]</td>
|
||||
<td>[[element.food.name]]</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" v-if="element.list_recipe === null"
|
||||
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)">
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
|
||||
</table>
|
||||
|
||||
@@ -199,6 +205,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col" style="margin-top: 1vh">
|
||||
<multiselect
|
||||
v-tabindex
|
||||
v-model="shopping_list.supermarket"
|
||||
:options="supermarkets"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="true"
|
||||
:allow-empty="true"
|
||||
:preserve-search="true"
|
||||
placeholder="{% trans 'Select Supermarket' %}"
|
||||
select-label="{% trans 'Select' %}"
|
||||
label="name"
|
||||
track-by="id"
|
||||
:multiple="false"
|
||||
:loading="supermarkets_loading"
|
||||
@search-change="searchSupermarket">
|
||||
</multiselect>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col" style="margin-top: 1vh">
|
||||
<multiselect
|
||||
@@ -238,27 +266,41 @@
|
||||
<div class="row" style="margin-top: 8px">
|
||||
<div class="col col-md-12">
|
||||
<table class="table">
|
||||
<tr v-for="x in display_entries">
|
||||
<template v-if="!x.checked">
|
||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
||||
@change="entryChecked(x)">
|
||||
</td>
|
||||
<td>[[x.amount]]</td>
|
||||
<td>[[x.unit.name]]</td>
|
||||
<td>[[x.food.name]]</td>
|
||||
<template v-for="c in display_categories">
|
||||
<template v-if="c.entries.filter(item => item.checked === false).length > 0">
|
||||
<tr>
|
||||
<td colspan="4">[[c.name]]</td>
|
||||
</tr>
|
||||
<tr v-for="x in c.entries">
|
||||
<template v-if="!x.checked">
|
||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
||||
@change="entryChecked(x)">
|
||||
</td>
|
||||
<td>[[x.amount]]</td>
|
||||
<td>[[x.unit.name]]</td>
|
||||
<td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span></td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
<tr>
|
||||
<td colspan="4"></td>
|
||||
</tr>
|
||||
<template v-for="c in display_categories">
|
||||
|
||||
<tr v-for="x in c.entries" class="text-muted">
|
||||
<template v-if="x.checked">
|
||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
||||
@change="entryChecked(x)">
|
||||
</td>
|
||||
<td>[[x.amount]]</td>
|
||||
<td>[[x.unit.name]]</td>
|
||||
<td>[[x.food.name]]</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
||||
<tr v-for="x in display_entries" class="text-muted">
|
||||
<template v-if="x.checked">
|
||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
||||
@change="entryChecked(x)">
|
||||
</td>
|
||||
<td>[[x.amount]]</td>
|
||||
<td>[[x.unit.name]]</td>
|
||||
<td>[[x.food.name]]</td>
|
||||
</template>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,13 +341,12 @@
|
||||
|
||||
</b-modal>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
|
||||
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
<script type="application/javascript">
|
||||
let csrftoken = Cookies.get('csrftoken');
|
||||
@@ -336,6 +377,8 @@
|
||||
foods_loading: false,
|
||||
units: [],
|
||||
units_loading: false,
|
||||
supermarkets: [],
|
||||
supermarkets_loading: false,
|
||||
users: [],
|
||||
users_loading: false,
|
||||
onLine: navigator.onLine,
|
||||
@@ -355,45 +398,104 @@
|
||||
})
|
||||
return cache
|
||||
},
|
||||
display_entries() {
|
||||
let entries = []
|
||||
recipe_cache() {
|
||||
let cache = {}
|
||||
this.shopping_list.recipes.forEach((r) => {
|
||||
cache[r.id] = r.recipe_name;
|
||||
})
|
||||
return cache
|
||||
},
|
||||
display_categories() {
|
||||
let categories = {
|
||||
no_category: {
|
||||
name: gettext('Uncategorized'),
|
||||
id: -1,
|
||||
entries: [],
|
||||
order: 99999999
|
||||
}
|
||||
}
|
||||
|
||||
//TODO merge multiple ingredients of same unit
|
||||
this.shopping_list.entries.forEach((e) => {
|
||||
if (e.food.supermarket_category !== null) {
|
||||
categories[e.food.supermarket_category.id] = {
|
||||
name: e.food.supermarket_category.name,
|
||||
id: e.food.supermarket_category.id,
|
||||
order: 0,
|
||||
entries: []
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
if (this.shopping_list.supermarket !== null) {
|
||||
this.shopping_list.supermarket.category_to_supermarket.forEach(el => {
|
||||
categories[el.category.id] = {
|
||||
name: el.category.name,
|
||||
id: el.category.id,
|
||||
order: el.order,
|
||||
entries: []
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
this.shopping_list.entries.forEach(element => {
|
||||
let item = {}
|
||||
Object.assign(item, element);
|
||||
if (item.list_recipe !== null) {
|
||||
item.amount = item.amount * this.servings_cache[item.list_recipe]
|
||||
item.recipes = []
|
||||
|
||||
let entry = this.findMergeEntry(categories, item)
|
||||
if (entry !== undefined) {
|
||||
entry.amount += item.amount * this.servings_cache[item.list_recipe]
|
||||
|
||||
if (item.list_recipe !== null && entry.recipes.indexOf(this.recipe_cache[item.list_recipe]) === -1) {
|
||||
entry.recipes.push(this.recipe_cache[item.list_recipe])
|
||||
}
|
||||
|
||||
entry.entries.push(item.id)
|
||||
} else {
|
||||
if (item.list_recipe !== null) {
|
||||
item.amount = item.amount * this.servings_cache[item.list_recipe]
|
||||
}
|
||||
item.unit = ((element.unit !== undefined && element.unit !== null) ? element.unit : {'name': ''})
|
||||
item.entries = [element.id]
|
||||
if (element.list_recipe !== null) {
|
||||
item.recipes.push(this.recipe_cache[element.list_recipe])
|
||||
}
|
||||
if (item.food.supermarket_category !== null) {
|
||||
categories[item.food.supermarket_category.id].entries.push(item)
|
||||
} else {
|
||||
categories['no_category'].entries.push(item)
|
||||
}
|
||||
}
|
||||
item.unit = ((element.unit !== undefined && element.unit !== null) ? element.unit : {'name': ''})
|
||||
entries.push(item)
|
||||
});
|
||||
|
||||
return entries
|
||||
let ordered_categories = []
|
||||
for (let [i, v] of Object.entries(categories)) {
|
||||
ordered_categories.push(v)
|
||||
}
|
||||
|
||||
ordered_categories.sort(function (a, b) {
|
||||
if (a.order < b.order) {
|
||||
return -1
|
||||
} else if (a.order > b.order) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
return ordered_categories
|
||||
},
|
||||
export_text() {
|
||||
let text = ''
|
||||
for (let e of this.display_entries.filter(item => item.checked === false)) {
|
||||
text += `${this.export_text_prefix}${e.amount} ${e.unit.name} ${e.food.name} \n`
|
||||
for (let c of this.display_categories) {
|
||||
for (let e of c.entries.filter(item => item.checked === false)) {
|
||||
text += `${this.export_text_prefix}${e.amount} ${e.unit.name} ${e.food.name} \n`
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
},
|
||||
/*
|
||||
watch: {
|
||||
recipe: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.recipe_changed = this.recipe_changed !== undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('beforeunload', this.warnPageLeave)
|
||||
},
|
||||
*/
|
||||
mounted: function () {
|
||||
this.loadShoppingList()
|
||||
|
||||
@@ -422,22 +524,35 @@
|
||||
{% endif %}
|
||||
|
||||
this.searchUsers('')
|
||||
this.searchSupermarket('')
|
||||
this.searchUnits('')
|
||||
this.searchFoods('')
|
||||
},
|
||||
methods: {
|
||||
findMergeEntry: function (categories, entry) {
|
||||
for (let [i, e] of Object.entries(categories)) {
|
||||
let found_entry = e.entries.find(item => {
|
||||
if (entry.food.id === item.food.id) {
|
||||
if (entry.unit === null && item.unit === null) {
|
||||
return true
|
||||
} else if (entry.unit !== null && item.unit !== null && entry.unit.id === item.unit.id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (found_entry !== undefined) {
|
||||
return found_entry
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
updateOnlineStatus(e) {
|
||||
const {
|
||||
type
|
||||
} = e;
|
||||
this.onLine = type === 'online';
|
||||
},
|
||||
/*
|
||||
warnPageLeave: function (event) {
|
||||
if (this.recipe_changed) {
|
||||
event.returnValue = ''
|
||||
return ''
|
||||
}
|
||||
},
|
||||
*/
|
||||
makeToast: function (title, message, variant = null) {
|
||||
//TODO remove duplicate function in favor of central one
|
||||
this.$bvToast.toast(message, {
|
||||
@@ -489,7 +604,8 @@
|
||||
"shared": [{% for u in request.user.userpreference.plan_share.all %}
|
||||
{'id': {{ u.pk }}, 'username': '{{ u.get_user_name }}'},
|
||||
{% endfor %}],
|
||||
"created_by": 1
|
||||
"created_by": {{ request.user.pk }},
|
||||
"supermarket": null
|
||||
}
|
||||
this.loading = false
|
||||
|
||||
@@ -554,16 +670,39 @@
|
||||
|
||||
})
|
||||
},
|
||||
sortEntries: function () {
|
||||
this.display_entries.forEach((item, index) => {
|
||||
sortEntries: function (a, b) {
|
||||
//TODO implement me (might be difficult because of computed drag changed stuff)
|
||||
},
|
||||
dragChanged: function (category, evt) {
|
||||
if (evt.added !== undefined) {
|
||||
if (evt.added.element.id === undefined) {
|
||||
this.makeToast(gettext('Warning'), gettext('This feature is only available after saving the shopping list'), 'warning')
|
||||
} else {
|
||||
this.shopping_list.entries.forEach(entry => {
|
||||
if (entry.id === evt.added.element.id) {
|
||||
if (category.id === -1) {
|
||||
entry.food.supermarket_category = null
|
||||
} else {
|
||||
entry.food.supermarket_category = {
|
||||
name: category.name,
|
||||
id: category.id
|
||||
}
|
||||
}
|
||||
this.$http.put(("{% url 'api:food-detail' 123456 %}").replace('123456', entry.food.id), entry.food).then((response) => {
|
||||
|
||||
})
|
||||
console.log("IMPLEMENT ME", this.display_entries)
|
||||
}).catch((err) => {
|
||||
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
entryChecked: function (entry) {
|
||||
console.log("checked entry: ", entry)
|
||||
this.shopping_list.entries.forEach((item) => {
|
||||
if (item.id === entry.id) { //TODO unwrap once same entries are merged
|
||||
if (entry.entries.includes(item.id)) {
|
||||
item.checked = entry.checked
|
||||
this.$http.put("{% url 'api:shoppinglistentry-detail' 123456 %}".replace('123456', item.id), item, {}).then((response) => {
|
||||
|
||||
@@ -572,7 +711,6 @@
|
||||
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
|
||||
this.loading = false
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -584,7 +722,7 @@
|
||||
'unit': this.new_entry.unit,
|
||||
'amount': parseFloat(this.new_entry.amount),
|
||||
'order': 0,
|
||||
'checked': false
|
||||
'checked': false,
|
||||
})
|
||||
|
||||
this.new_entry = {
|
||||
@@ -625,22 +763,25 @@
|
||||
"recipe_name": recipe.name,
|
||||
"servings": servings,
|
||||
}
|
||||
|
||||
this.shopping_list.recipes.push(slr)
|
||||
|
||||
for (let s of recipe.steps) {
|
||||
for (let i of s.ingredients) {
|
||||
if (!i.is_header && i.food !== null) {
|
||||
this.shopping_list.entries.push({
|
||||
'list_recipe': slr.id,
|
||||
'food': i.food,
|
||||
'unit': i.unit,
|
||||
'amount': i.amount,
|
||||
'order': 0
|
||||
})
|
||||
this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe.id)).then((response) => {
|
||||
for (let s of response.data.steps) {
|
||||
for (let i of s.ingredients) {
|
||||
if (!i.is_header && i.food !== null && i.food.ignore_shopping === false) {
|
||||
this.shopping_list.entries.push({
|
||||
'list_recipe': slr.id,
|
||||
'food': i.food,
|
||||
'unit': i.unit,
|
||||
'amount': i.amount,
|
||||
'order': 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
removeRecipeFromList: function (slr) {
|
||||
this.shopping_list.entries = this.shopping_list.entries.filter(item => item.list_recipe !== slr.id)
|
||||
@@ -676,7 +817,7 @@
|
||||
})
|
||||
},
|
||||
addFoodType: function (tag, index) { //TODO move to central component
|
||||
let new_food = {'name': tag}
|
||||
let new_food = {'name': tag, supermarket_category: null}
|
||||
this.foods.push(new_food)
|
||||
this.new_entry.food = new_food
|
||||
},
|
||||
@@ -694,6 +835,17 @@
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
searchSupermarket: function (query) { //TODO move to central component
|
||||
this.supermarkets_loading = true
|
||||
this.$http.get("{% url 'api:supermarket-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||
this.supermarkets = response.data
|
||||
this.supermarkets_loading = false
|
||||
}).catch((err) => {
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('online', this.updateOnlineStatus);
|
||||
|
||||
@@ -8,6 +8,7 @@ from django import template
|
||||
from django.db.models import Avg
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from recipes import settings
|
||||
from gettext import gettext as _
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -80,6 +81,21 @@ def recipe_last(recipe, user):
|
||||
return ''
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def page_help(page_name):
|
||||
help_pages = {
|
||||
'edit_storage': 'https://vabene1111.github.io/recipes/features/external_recipes/',
|
||||
'view_shopping': 'https://vabene1111.github.io/recipes/features/shopping/',
|
||||
}
|
||||
|
||||
link = help_pages.get(page_name, '')
|
||||
|
||||
if link != '':
|
||||
return f'<li class="nav-item"><a class="nav-link" target="_blank" rel="nofollow noreferrer" href="{link}"><i class="far fa-question-circle"></i>‌<span class="d-lg-none"> {_("Help")}</span></a></li>'
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def message_of_the_day():
|
||||
return Space.objects.first().message
|
||||
|
||||
@@ -10,12 +10,12 @@ class TestEditsRecipe(TestBase):
|
||||
# flake8: noqa
|
||||
def test_ld_json(self):
|
||||
test_list = [
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3222},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_1.html', 'result_length': 3237},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_2.html', 'result_length': 1510},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_3.html', 'result_length': 1629},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_4.html', 'result_length': 1729},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_4.html', 'result_length': 1744},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_itemList.html', 'result_length': 3206},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_multiple.html', 'result_length': 1606},
|
||||
{'file': 'cookbook/tests/resources/websites/ld_json_multiple.html', 'result_length': 1621},
|
||||
{'file': 'cookbook/tests/resources/websites/micro_data_1.html', 'result_length': 1079},
|
||||
{'file': 'cookbook/tests/resources/websites/micro_data_2.html', 'result_length': 1438},
|
||||
{'file': 'cookbook/tests/resources/websites/micro_data_3.html', 'result_length': 1148},
|
||||
|
||||
@@ -34,6 +34,7 @@ router.register(r'view-log', api.ViewLogViewSet)
|
||||
router.register(r'cook-log', api.CookLogViewSet)
|
||||
router.register(r'recipe-book', api.RecipeBookViewSet)
|
||||
router.register(r'recipe-book-entry', api.RecipeBookEntryViewSet)
|
||||
router.register(r'supermarket', api.SupermarketViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
|
||||
@@ -36,8 +36,9 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Step,
|
||||
Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog, RecipeBookEntry)
|
||||
ViewLog, RecipeBookEntry, Supermarket)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
|
||||
KeywordSerializer, MealPlanSerializer,
|
||||
@@ -50,10 +51,35 @@ from cookbook.serializer import (FoodSerializer, IngredientSerializer,
|
||||
StorageSerializer, SyncLogSerializer,
|
||||
SyncSerializer, UnitSerializer,
|
||||
UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer)
|
||||
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, RecipeOverviewSerializer, SupermarketSerializer)
|
||||
from recipes.settings import DEMO
|
||||
|
||||
|
||||
class StandardFilterMixin(ViewSetMixin):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
query = self.request.query_params.get('query', None)
|
||||
if query is not None:
|
||||
queryset = queryset.filter(name__icontains=query)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
if updated_at is not None:
|
||||
try:
|
||||
queryset = queryset.filter(updated_at__gte=updated_at)
|
||||
except FieldError:
|
||||
pass
|
||||
|
||||
limit = self.request.query_params.get('limit', None)
|
||||
random = self.request.query_params.get('random', False)
|
||||
if limit is not None:
|
||||
if random:
|
||||
queryset = queryset.random(int(limit))
|
||||
else:
|
||||
queryset = queryset[:int(limit)]
|
||||
return queryset
|
||||
|
||||
|
||||
class UserNameViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
list:
|
||||
@@ -115,29 +141,10 @@ class SyncLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission_classes = [CustomIsAdmin, ]
|
||||
|
||||
|
||||
class StandardFilterMixin(ViewSetMixin):
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset
|
||||
query = self.request.query_params.get('query', None)
|
||||
if query is not None:
|
||||
queryset = queryset.filter(name__icontains=query)
|
||||
|
||||
updated_at = self.request.query_params.get('updated_at', None)
|
||||
if updated_at is not None:
|
||||
try:
|
||||
queryset = queryset.filter(updated_at__gte=updated_at)
|
||||
except FieldError:
|
||||
pass
|
||||
|
||||
limit = self.request.query_params.get('limit', None)
|
||||
random = self.request.query_params.get('random', False)
|
||||
if limit is not None:
|
||||
if random:
|
||||
queryset = queryset.random(int(limit))
|
||||
else:
|
||||
queryset = queryset[:int(limit)]
|
||||
return queryset
|
||||
class SupermarketViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
queryset = Supermarket.objects.all()
|
||||
serializer_class = SupermarketSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
|
||||
|
||||
class KeywordViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
@@ -169,7 +176,7 @@ class FoodViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
queryset = RecipeBook.objects.all()
|
||||
serializer_class = RecipeBookSerializer
|
||||
permission_classes = [CustomIsOwner, CustomIsAdmin]
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = super(RecipeBookViewSet, self).get_queryset()
|
||||
@@ -181,7 +188,7 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
|
||||
queryset = RecipeBookEntry.objects.all()
|
||||
serializer_class = RecipeBookEntrySerializer
|
||||
permission_classes = [CustomIsOwner, CustomIsAdmin]
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
@@ -200,7 +207,7 @@ class MealPlanViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = MealPlan.objects.all()
|
||||
serializer_class = MealPlanSerializer
|
||||
permission_classes = [permissions.IsAuthenticated] # TODO fix permissions
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = MealPlan.objects.filter(
|
||||
@@ -225,11 +232,10 @@ class MealTypeViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = MealType.objects.order_by('order').all()
|
||||
serializer_class = MealTypeSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = MealType.objects.order_by('order', 'id') \
|
||||
.filter(created_by=self.request.user).all()
|
||||
queryset = MealType.objects.order_by('order', 'id').filter(created_by=self.request.user).all()
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -310,17 +316,19 @@ class RecipeViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
class ShoppingListRecipeViewSet(viewsets.ModelViewSet):
|
||||
queryset = ShoppingListRecipe.objects.all()
|
||||
serializer_class = ShoppingListRecipeSerializer
|
||||
permission_classes = [CustomIsUser, ] # TODO add custom validation
|
||||
permission_classes = [CustomIsOwner, ]
|
||||
|
||||
# TODO custom get qs
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(shoppinglist__created_by=self.request.user).all()
|
||||
|
||||
|
||||
class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
queryset = ShoppingListEntry.objects.all()
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [CustomIsOwner, ] # TODO add custom validation
|
||||
permission_classes = [CustomIsOwner, ]
|
||||
|
||||
# TODO custom get qs
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(shoppinglist__created_by=self.request.user).all()
|
||||
|
||||
|
||||
class ShoppingListViewSet(viewsets.ModelViewSet):
|
||||
@@ -345,12 +353,10 @@ class ShoppingListViewSet(viewsets.ModelViewSet):
|
||||
class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
queryset = ViewLog.objects.all()
|
||||
serializer_class = ViewLogSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ViewLog.objects \
|
||||
.filter(created_by=self.request.user).all()[:5]
|
||||
return queryset
|
||||
return CookLog.objects.filter(created_by=self.request.user).all()[:5]
|
||||
|
||||
|
||||
class CookLogViewSet(viewsets.ModelViewSet):
|
||||
@@ -359,7 +365,7 @@ class CookLogViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ViewLog.objects.filter(created_by=self.request.user).all()[:5]
|
||||
queryset = CookLog.objects.filter(created_by=self.request.user).all()[:5]
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -370,6 +376,8 @@ def get_recipe_provider(recipe):
|
||||
return Dropbox
|
||||
elif recipe.storage.method == Storage.NEXTCLOUD:
|
||||
return Nextcloud
|
||||
elif recipe.storage.method == Storage.LOCAL:
|
||||
return Local
|
||||
else:
|
||||
raise Exception('Provider not implemented')
|
||||
|
||||
@@ -394,15 +402,15 @@ def get_external_file_link(request, recipe_id):
|
||||
@group_required('user')
|
||||
def get_recipe_file(request, recipe_id):
|
||||
recipe = Recipe.objects.get(id=recipe_id)
|
||||
if not recipe.cors_link:
|
||||
update_recipe_links(recipe)
|
||||
# if not recipe.cors_link:
|
||||
# update_recipe_links(recipe)
|
||||
|
||||
return FileResponse(get_recipe_provider(recipe).get_file(recipe))
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def sync_all(request):
|
||||
if DEMO or True:
|
||||
if DEMO:
|
||||
messages.add_message(
|
||||
request, messages.ERROR, _('This feature is not available in the demo version!')
|
||||
)
|
||||
@@ -420,6 +428,10 @@ def sync_all(request):
|
||||
ret = Nextcloud.import_all(monitor)
|
||||
if not ret:
|
||||
error = True
|
||||
if monitor.storage.method == Storage.LOCAL:
|
||||
ret = Local.import_all(monitor)
|
||||
if not ret:
|
||||
error = True
|
||||
|
||||
if not error:
|
||||
messages.add_message(
|
||||
|
||||
@@ -13,6 +13,7 @@ from cookbook.models import (Comment, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport,
|
||||
Storage, Sync)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
|
||||
@@ -37,6 +38,8 @@ def delete_recipe_source(request, pk):
|
||||
Dropbox.delete_file(recipe)
|
||||
if recipe.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.delete_file(recipe)
|
||||
if recipe.storage.method == Storage.LOCAL:
|
||||
Local.delete_file(recipe)
|
||||
|
||||
recipe.storage = None
|
||||
recipe.file_path = ''
|
||||
|
||||
@@ -18,6 +18,7 @@ from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, RecipeImport,
|
||||
Storage, Sync)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
|
||||
|
||||
@@ -231,6 +232,8 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView):
|
||||
Dropbox.rename_file(old_recipe, self.object.name)
|
||||
if self.object.storage.method == Storage.NEXTCLOUD:
|
||||
Nextcloud.rename_file(old_recipe, self.object.name)
|
||||
if self.object.storage.method == Storage.LOCAL:
|
||||
Local.rename_file(old_recipe, self.object.name)
|
||||
|
||||
self.object.file_path = "%s/%s%s" % (
|
||||
os.path.dirname(self.object.file_path),
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -28,6 +28,7 @@ from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
||||
RecipeBook, RecipeBookEntry, ViewLog)
|
||||
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
||||
ViewLogTable)
|
||||
from recipes.settings import DEMO
|
||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||
|
||||
|
||||
@@ -263,6 +264,10 @@ def shopping_list(request, pk=None):
|
||||
|
||||
@group_required('guest')
|
||||
def user_settings(request):
|
||||
if DEMO:
|
||||
messages.add_message(request, messages.ERROR, _('This feature is not available in the demo version!'))
|
||||
return redirect('index')
|
||||
|
||||
up = request.user.userpreference
|
||||
|
||||
user_name_form = UserNameForm(instance=request.user)
|
||||
|
||||
Reference in New Issue
Block a user