diff --git a/cookbook/helper/permission_helper.py b/cookbook/helper/permission_helper.py
index 10a310cc..466747a5 100644
--- a/cookbook/helper/permission_helper.py
+++ b/cookbook/helper/permission_helper.py
@@ -3,10 +3,14 @@ Source: https://djangosnippets.org/snippets/1703/
"""
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
+from django.core.exceptions import ValidationError
+from django.db.models import Q
from django.utils.translation import gettext as _
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy, reverse
+from cookbook.models import ShareLink
+
def get_allowed_groups(groups_required):
groups_allowed = tuple(groups_required)
@@ -66,3 +70,11 @@ class OwnerRequiredMixin(object):
return HttpResponseRedirect(reverse('index'))
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
+
+
+def share_link_valid(recipe, share):
+ print(share, recipe)
+ try:
+ return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
+ except ValidationError:
+ return False
diff --git a/cookbook/migrations/0054_sharelink.py b/cookbook/migrations/0054_sharelink.py
new file mode 100644
index 00000000..c84b33ef
--- /dev/null
+++ b/cookbook/migrations/0054_sharelink.py
@@ -0,0 +1,27 @@
+# Generated by Django 3.0.7 on 2020-06-16 08:57
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('cookbook', '0053_auto_20200611_2217'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ShareLink',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.UUIDField(default=uuid.UUID('dbbf5150-0795-4305-b9bd-3952dfa2264b'))),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Recipe')),
+ ],
+ ),
+ ]
diff --git a/cookbook/models.py b/cookbook/models.py
index 68f0388f..e88f465b 100644
--- a/cookbook/models.py
+++ b/cookbook/models.py
@@ -1,5 +1,5 @@
import re
-
+import uuid
from annoying.fields import AutoOneToOneField
from django.contrib import auth
from django.contrib.auth.models import User
@@ -242,6 +242,13 @@ class MealPlan(models.Model):
return f'{self.get_label()} - {self.date} - {self.meal_type.name}'
+class ShareLink(models.Model):
+ recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
+ uuid = models.UUIDField(default=uuid.uuid4())
+ created_by = models.ForeignKey(User, on_delete=models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+
class CookLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
diff --git a/cookbook/templates/recipe_view.html b/cookbook/templates/recipe_view.html
index c5f7a42a..8ba19f31 100644
--- a/cookbook/templates/recipe_view.html
+++ b/cookbook/templates/recipe_view.html
@@ -53,6 +53,9 @@
data-toggle="tooltip"
data-placement="top" title="{% trans 'Export recipe' %}">
+
diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py
index 877f6364..ad19a38b 100644
--- a/cookbook/templatetags/custom_tags.py
+++ b/cookbook/templatetags/custom_tags.py
@@ -40,6 +40,8 @@ def markdown(value):
@register.simple_tag
def recipe_rating(recipe, user):
+ if not user.is_authenticated:
+ return ''
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
if rating['rating__avg']:
@@ -57,6 +59,8 @@ def recipe_rating(recipe, user):
@register.simple_tag
def recipe_last(recipe, user):
+ if not user.is_authenticated:
+ return ''
last = recipe.cooklog_set.filter(created_by=user).last()
if last:
return last.created_at
diff --git a/cookbook/tests/test_setup.py b/cookbook/tests/test_setup.py
index 80e903f3..57f84b12 100644
--- a/cookbook/tests/test_setup.py
+++ b/cookbook/tests/test_setup.py
@@ -11,6 +11,7 @@ class TestBase(TestCase):
guest_client_1 = None
guest_client_2 = None
superuser_client = None
+ anonymous_client = None
def create_login_user(self, name, group):
client = Client()
diff --git a/cookbook/tests/views/test_views_recipe_share.py b/cookbook/tests/views/test_views_recipe_share.py
new file mode 100644
index 00000000..2c057569
--- /dev/null
+++ b/cookbook/tests/views/test_views_recipe_share.py
@@ -0,0 +1,44 @@
+import uuid
+
+from django.contrib import auth
+from django.urls import reverse
+
+from cookbook.helper.permission_helper import share_link_valid
+from cookbook.models import Recipe, ShareLink
+from cookbook.tests.views.test_views import TestViews
+
+
+class TestViewsGeneral(TestViews):
+
+ def test_share(self):
+ internal_recipe = Recipe.objects.create(
+ name='Test',
+ internal=True,
+ created_by=auth.get_user(self.user_client_1)
+ )
+
+ url = reverse('view_recipe', kwargs={'pk': internal_recipe.pk})
+ r = self.user_client_1.get(url)
+ self.assertEqual(r.status_code, 200)
+
+ r = self.anonymous_client.get(url)
+ self.assertEqual(r.status_code, 302)
+
+ url = reverse('new_share_link', kwargs={'pk': internal_recipe.pk})
+ r = self.user_client_1.get(url)
+ self.assertEqual(r.status_code, 302)
+ share = ShareLink.objects.filter(recipe=internal_recipe).first()
+ self.assertIsNotNone(share)
+ self.assertTrue(share_link_valid(internal_recipe, share.uuid))
+
+ url = reverse('view_recipe', kwargs={'pk': internal_recipe.pk, 'share': share.uuid})
+ r = self.anonymous_client.get(url)
+ self.assertEqual(r.status_code, 200)
+
+ url = reverse('view_recipe', kwargs={'pk': (internal_recipe.pk + 1), 'share': share.uuid})
+ r = self.anonymous_client.get(url)
+ self.assertEqual(r.status_code, 404)
+
+ url = reverse('view_recipe', kwargs={'pk': internal_recipe.pk, 'share': uuid.uuid4()})
+ r = self.anonymous_client.get(url)
+ self.assertEqual(r.status_code, 302)
diff --git a/cookbook/urls.py b/cookbook/urls.py
index 99dc90b9..ffeb261c 100644
--- a/cookbook/urls.py
+++ b/cookbook/urls.py
@@ -30,8 +30,10 @@ urlpatterns = [
path('export/', import_export.export_recipe, name='view_export'),
path('view/recipe/', views.recipe_view, name='view_recipe'),
+ path('view/recipe//', views.recipe_view, name='view_recipe'),
- path('new/recipe_import//', new.create_new_external_recipe, name='new_recipe_import'),
+ path('new/recipe-import//', new.create_new_external_recipe, name='new_recipe_import'),
+ path('new/share-link//', new.share_link, name='new_share_link'),
path('edit/recipe//', edit.switch_recipe, name='edit_recipe'),
path('edit/recipe/internal//', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
diff --git a/cookbook/views/new.py b/cookbook/views/new.py
index fd937251..3ec4e8cc 100644
--- a/cookbook/views/new.py
+++ b/cookbook/views/new.py
@@ -3,7 +3,7 @@ from datetime import datetime
from django.contrib import messages
from django.http import HttpResponseRedirect
-from django.shortcuts import render, redirect
+from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext as _
from django.views.generic import CreateView
@@ -11,7 +11,7 @@ from django.views.generic import CreateView
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
RecipeBookForm, MealPlanForm
from cookbook.helper.permission_helper import GroupRequiredMixin, group_required
-from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan
+from cookbook.models import Keyword, Recipe, RecipeBook, MealPlan, ShareLink
class RecipeCreate(GroupRequiredMixin, CreateView):
@@ -36,6 +36,13 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
return context
+@group_required('user')
+def share_link(request, pk):
+ recipe = get_object_or_404(Recipe, pk=pk)
+ link = ShareLink.objects.create(recipe=recipe, created_by=request.user)
+ return HttpResponseRedirect(reverse('view_recipe', kwargs={'pk': pk, 'share': link.uuid}))
+
+
class KeywordCreate(GroupRequiredMixin, CreateView):
groups_required = ['user']
template_name = "generic/new_template.html"
diff --git a/cookbook/views/views.py b/cookbook/views/views.py
index e3d82353..9f29fdf8 100644
--- a/cookbook/views/views.py
+++ b/cookbook/views/views.py
@@ -18,7 +18,7 @@ from django.conf import settings
from cookbook.filters import RecipeFilter
from cookbook.forms import *
-from cookbook.helper.permission_helper import group_required
+from cookbook.helper.permission_helper import group_required, share_link_valid
from cookbook.tables import RecipeTable, RecipeTableSmall, CookLogTable, ViewLogTable
from recipes.version import *
@@ -70,9 +70,13 @@ def search(request):
return render(request, 'index.html')
-@group_required('guest')
-def recipe_view(request, pk):
+def recipe_view(request, pk, share=None):
recipe = get_object_or_404(Recipe, pk=pk)
+
+ if not request.user.is_authenticated and not share_link_valid(recipe, share):
+ messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
+ return HttpResponseRedirect(reverse('index'))
+
ingredients = RecipeIngredient.objects.filter(recipe=recipe)
comments = Comment.objects.filter(recipe=recipe)