added sharing links and appropriate tests
This commit is contained in:
parent
17946c8dac
commit
dee7249347
@ -3,10 +3,14 @@ Source: https://djangosnippets.org/snippets/1703/
|
|||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import user_passes_test
|
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.utils.translation import gettext as _
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
|
|
||||||
|
from cookbook.models import ShareLink
|
||||||
|
|
||||||
|
|
||||||
def get_allowed_groups(groups_required):
|
def get_allowed_groups(groups_required):
|
||||||
groups_allowed = tuple(groups_required)
|
groups_allowed = tuple(groups_required)
|
||||||
@ -66,3 +70,11 @@ class OwnerRequiredMixin(object):
|
|||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
|
||||||
return super(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs)
|
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
|
||||||
|
27
cookbook/migrations/0054_sharelink.py
Normal file
27
cookbook/migrations/0054_sharelink.py
Normal file
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
from annoying.fields import AutoOneToOneField
|
from annoying.fields import AutoOneToOneField
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.contrib.auth.models import User
|
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}'
|
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):
|
class CookLog(models.Model):
|
||||||
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
@ -53,6 +53,9 @@
|
|||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
data-placement="top" title="{% trans 'Export recipe' %}"><i
|
data-placement="top" title="{% trans 'Export recipe' %}"><i
|
||||||
class="fas fa-file-export"></i></a>
|
class="fas fa-file-export"></i></a>
|
||||||
|
<a class="btn btn-primary" href="{% url 'new_share_link' recipe.pk %}" target="_blank" rel="noopener noreferrer"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
data-placement="top" title="{% trans 'Create share link' %}"><i class="fas fa-share-alt"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -40,6 +40,8 @@ def markdown(value):
|
|||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def recipe_rating(recipe, user):
|
def recipe_rating(recipe, user):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return ''
|
||||||
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
|
rating = recipe.cooklog_set.filter(created_by=user).aggregate(Avg('rating'))
|
||||||
if rating['rating__avg']:
|
if rating['rating__avg']:
|
||||||
|
|
||||||
@ -57,6 +59,8 @@ def recipe_rating(recipe, user):
|
|||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def recipe_last(recipe, user):
|
def recipe_last(recipe, user):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return ''
|
||||||
last = recipe.cooklog_set.filter(created_by=user).last()
|
last = recipe.cooklog_set.filter(created_by=user).last()
|
||||||
if last:
|
if last:
|
||||||
return last.created_at
|
return last.created_at
|
||||||
|
@ -11,6 +11,7 @@ class TestBase(TestCase):
|
|||||||
guest_client_1 = None
|
guest_client_1 = None
|
||||||
guest_client_2 = None
|
guest_client_2 = None
|
||||||
superuser_client = None
|
superuser_client = None
|
||||||
|
anonymous_client = None
|
||||||
|
|
||||||
def create_login_user(self, name, group):
|
def create_login_user(self, name, group):
|
||||||
client = Client()
|
client = Client()
|
||||||
|
44
cookbook/tests/views/test_views_recipe_share.py
Normal file
44
cookbook/tests/views/test_views_recipe_share.py
Normal file
@ -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)
|
@ -30,8 +30,10 @@ urlpatterns = [
|
|||||||
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'),
|
||||||
|
path('view/recipe/<int:pk>/<slug:share>', views.recipe_view, name='view_recipe'),
|
||||||
|
|
||||||
path('new/recipe_import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
|
path('new/recipe-import/<int:import_id>/', new.create_new_external_recipe, name='new_recipe_import'),
|
||||||
|
path('new/share-link/<int:pk>/', new.share_link, name='new_share_link'),
|
||||||
|
|
||||||
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
|
path('edit/recipe/<int:pk>/', edit.switch_recipe, name='edit_recipe'),
|
||||||
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
|
path('edit/recipe/internal/<int:pk>/', edit.internal_recipe_update, name='edit_internal_recipe'), # for internal use only
|
||||||
|
@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
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.urls import reverse_lazy, reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import CreateView
|
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, \
|
from cookbook.forms import ImportRecipeForm, RecipeImport, KeywordForm, Storage, StorageForm, InternalRecipeForm, \
|
||||||
RecipeBookForm, MealPlanForm
|
RecipeBookForm, MealPlanForm
|
||||||
from cookbook.helper.permission_helper import GroupRequiredMixin, group_required
|
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):
|
class RecipeCreate(GroupRequiredMixin, CreateView):
|
||||||
@ -36,6 +36,13 @@ class RecipeCreate(GroupRequiredMixin, CreateView):
|
|||||||
return context
|
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):
|
class KeywordCreate(GroupRequiredMixin, CreateView):
|
||||||
groups_required = ['user']
|
groups_required = ['user']
|
||||||
template_name = "generic/new_template.html"
|
template_name = "generic/new_template.html"
|
||||||
|
@ -18,7 +18,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from cookbook.filters import RecipeFilter
|
from cookbook.filters import RecipeFilter
|
||||||
from cookbook.forms import *
|
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 cookbook.tables import RecipeTable, RecipeTableSmall, CookLogTable, ViewLogTable
|
||||||
|
|
||||||
from recipes.version import *
|
from recipes.version import *
|
||||||
@ -70,9 +70,13 @@ def search(request):
|
|||||||
return render(request, 'index.html')
|
return render(request, 'index.html')
|
||||||
|
|
||||||
|
|
||||||
@group_required('guest')
|
def recipe_view(request, pk, share=None):
|
||||||
def recipe_view(request, pk):
|
|
||||||
recipe = get_object_or_404(Recipe, pk=pk)
|
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)
|
ingredients = RecipeIngredient.objects.filter(recipe=recipe)
|
||||||
comments = Comment.objects.filter(recipe=recipe)
|
comments = Comment.objects.filter(recipe=recipe)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user