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)