Compare commits

..

13 Commits
0.7.0 ... 0.7.1

Author SHA1 Message Date
vabene1111
dfb1d80ca0 fixed duplicates in recent view 2020-05-27 09:38:57 +02:00
vabene1111
744fbc7a46 revert psql distinct change 2020-05-15 13:12:52 +02:00
vabene1111
cd11cc58cf possible duplicate fix 2020-05-15 12:49:31 +02:00
vabene1111
569e385915 Revert "Create FUNDING.yml"
This reverts commit abf552cd18.
2020-05-13 13:27:46 +02:00
vabene1111
abf552cd18 Create FUNDING.yml 2020-05-13 13:22:26 +02:00
vabene1111
c6959488dc fixed typo on search page 2020-05-11 13:08:49 +02:00
vabene1111
85e3155b50 added group required filter to history view 2020-05-11 13:08:00 +02:00
vabene1111
f6aa50bbfc added history page 2020-05-11 12:59:54 +02:00
vabene1111
5ad27c015e markdown info central blockquote css 2020-05-11 12:44:31 +02:00
vabene1111
4a68a99907 show last viewd recipes on search page 2020-05-11 12:42:55 +02:00
vabene1111
123dc1a74d meal plan entry view 2020-05-08 00:10:23 +02:00
vabene1111
2e23fcfd5d added sharing to meal plan + fixed meal plan visibility 2020-05-07 23:16:24 +02:00
vabene1111
edbc21df19 Update README.md 2020-05-06 08:21:53 +02:00
19 changed files with 318 additions and 71 deletions

View File

@@ -35,6 +35,8 @@ The docker image (`vabene1111/recipes`) simply exposes the application on port `
4. Create a default user by running `docker-compose exec web_recipes createsuperuser`.
### Manual
**Python >= 3.8** is required to run this!
Copy `.env.template` to `.env` and fill in the missing values accordingly.
Make sure all variables are available to whatever serves your application.

View File

@@ -31,11 +31,16 @@ class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'search_style')
fields = ('default_unit', 'theme', 'nav_color', 'default_page', 'search_style', 'plan_share')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.')
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.'),
'plan_share': _('Default user to share newly created meal plan entries with.')
}
widgets = {
'plan_share': MultiSelectWidget
}
@@ -87,7 +92,7 @@ class InternalRecipeForm(forms.ModelForm):
}
widgets = {'keywords': MultiSelectWidget}
help_texts = {
'instructions': _('You can use markdown to format the instructions. See the <a href="/docs/markdown/">docs here</a>')
'instructions': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
}
@@ -257,6 +262,11 @@ class MealPlanForm(forms.ModelForm):
class Meta:
model = MealPlan
fields = ('recipe', 'title', 'meal', 'note', 'date')
fields = ('recipe', 'title', 'meal', 'note', 'date', 'shared')
widgets = {'recipe': SelectWidget, 'date': DateWidget}
help_texts = {
'shared': _('You can list default users to share recipes with in the settings.'),
'note': _('You can use markdown to format this field. See the <a href="/docs/markdown/">docs here</a>')
}
widgets = {'recipe': SelectWidget, 'date': DateWidget, 'shared': MultiSelectWidget}

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.5 on 2020-05-07 21:02
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0042_cooklog'),
]
operations = [
migrations.AddField(
model_name='mealplan',
name='shared',
field=models.ManyToManyField(blank=True, related_name='plan_share', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='userpreference',
name='plan_share',
field=models.ManyToManyField(blank=True, related_name='plan_share_default', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.0.5 on 2020-05-11 10:21
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', '0043_auto_20200507_2302'),
]
operations = [
migrations.CreateModel(
name='ViewLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
],
),
]

View File

@@ -61,6 +61,7 @@ class UserPreference(models.Model):
default_unit = models.CharField(max_length=32, default='g')
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
search_style = models.CharField(choices=SEARCH_STYLE, max_length=64, default=LARGE)
plan_share = models.ManyToManyField(User, blank=True, related_name='plan_share_default')
def __str__(self):
return str(self.user)
@@ -223,6 +224,7 @@ class MealPlan(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, blank=True, null=True)
title = models.CharField(max_length=64, blank=True, default='')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
shared = models.ManyToManyField(User, blank=True, related_name='plan_share')
meal = models.CharField(choices=MEAL_TYPES, max_length=128, default=BREAKFAST)
note = models.TextField(blank=True)
date = models.DateField()
@@ -232,6 +234,10 @@ class MealPlan(models.Model):
return self.title
return str(self.recipe)
def get_meal_name(self):
meals = dict(self.MEAL_TYPES)
return meals.get(self.meal)
class CookLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
@@ -242,3 +248,12 @@ class CookLog(models.Model):
def __str__(self):
return self.recipe.name
class ViewLog(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.recipe.name

View File

@@ -0,0 +1,21 @@
/* css classes needed to render markdown blockquotes */
blockquote {
background: #f9f9f9;
border-left: 4px solid #ccc;
margin: 1.5em 10px;
padding: .5em 10px;
quotes: none;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: .1em;
margin-right: .25em;
vertical-align: -.4em;
}
blockquote p {
display: inline;
}

View File

@@ -106,3 +106,21 @@ class RecipeImportTable(tables.Table):
model = RecipeImport
template_name = 'generic/table_template.html'
fields = ('id', 'name', 'file_path')
class ViewLogTable(tables.Table):
recipe = tables.LinkColumn('view_recipe', args=[A('recipe_id')])
class Meta:
model = ViewLog
template_name = 'generic/table_template.html'
fields = ('recipe', 'created_at')
class CookLogTable(tables.Table):
recipe = tables.LinkColumn('view_recipe', args=[A('recipe_id')])
class Meta:
model = CookLog
template_name = 'generic/table_template.html'
fields = ('recipe', 'rating', 'serving', 'created_at')

View File

@@ -70,7 +70,7 @@
class="sr-only">(current)</span></a>
</li>
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_books,view_plan,view_shopping,list_ingredient' %}active{% endif %}">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_books,view_plan,view_shopping,list_ingredient,view_plan_entry' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fas fa-mortar-pestle"></i> {% trans 'Utensils' %}
@@ -128,7 +128,7 @@
<ul class="navbar-nav ml-auto">
{% if user.is_authenticated %}
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings' %}active{% endif %}">
<li class="nav-item dropdown {% if request.resolver_match.url_name in 'view_settings,view_history' %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"><i
class="fas fa-user-alt"></i> {{ user.get_user_name }}
@@ -137,6 +137,7 @@
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'view_settings' %}"><i
class="fas fa-user-cog fa-fw"></i> {% trans 'Settings' %}</a>
<a class="dropdown-item" href="{% url 'view_history' %}"><i class="fas fa-history"></i> {% trans 'History' %}</a>
{% if user.is_superuser %}
<a class="dropdown-item" href="{% url 'admin:index' %}"><i
class="fas fa-user-shield fa-fw"></i> {% trans 'Admin' %}</a>

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load django_tables2 %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "History" %}{% endblock %}
{% block extra_head %}
{% endblock %}
{% block content %}
<h3>{% trans 'History' %}</h3>
<br/>
<ul class="nav nav-tabs" id="id_tab_nav" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#view_log" role="tab" aria-controls="view_log"
aria-selected="true">{% trans 'View Log' %}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#cook_log" role="tab" aria-controls="cook_log"
aria-selected="false">{% trans 'Cook Log' %}</a>
</li>
</ul>
<div class="tab-content" id="id_tab_content">
<div class="tab-pane fade show active" id="view_log" role="tabpanel" aria-labelledby="view-log">
{% render_table view_log %}
</div>
<div class="tab-pane fade" id="cook_log" role="tabpanel" aria-labelledby="profile-tab">
{% render_table cook_log %}
</div>
</div>
{% endblock %}

View File

@@ -77,6 +77,12 @@
{% endif %}
<br/>
{% if last_viewed %}
<h4>{% trans 'Last viewed' %}</h4>
{% render_table last_viewed %}
<h4>{% trans 'Recipes' %}</h4>
{% endif %}
{% if user.is_authenticated and recipes %}
{% render_table recipes %}
{% else %}

View File

@@ -5,32 +5,7 @@
{% block title %}{% trans "Markdown Info" %}{% endblock %}
{% block extra_head %}
<style>
.code-block {
background: #DDDDDD;
}
blockquote {
background: #f9f9f9;
border-left: 4px solid #ccc;
margin: 1.5em 10px;
padding: .5em 10px;
quotes: none;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: .1em;
margin-right: .25em;
vertical-align: -.4em;
}
blockquote p {
display: inline;
}
</style>
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
{% endblock %}
{% block content %}

View File

@@ -77,13 +77,13 @@
href="{% url 'new_meal_plan' %}?date={{ day_key|date:'Y-m-d' }}&meal={{ plan_key }}"><i
class="fas fa-plus"></i></a>
{% for mp in days_value %}
{% if mp.recipe %}
<a href="{% url 'edit_meal_plan' mp.pk %}"><i class="fas fa-edit"></i></a>
<a href="{% url 'view_recipe' mp.recipe.pk %}">{{ mp.recipe }}</a><br/>
{% else %}
<a href="{% url 'edit_meal_plan' mp.pk %}">{{ mp.title }}</a><br/>
{% endif %}
<a href="{% url 'view_plan_entry' mp.pk %}">
{% if mp.recipe %}
{{ mp.recipe }}
{% else %}
{{ mp.title }}
{% endif %}
</a><br/>
{% endfor %}
</td>
{% endfor %}

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% load static %}
{% load custom_tags %}
{% load i18n %}
{% block title %}{% trans 'Meal Plan View' %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12">
<h3>{{ plan.get_meal_name }} {{ plan.date }} <a href="{% url 'edit_meal_plan' plan.pk %}"
class="d-print-none"><i class="fas fa-pencil-alt"></i></a>
</h3>
<small class="text-muted">{% trans 'Created by' %} {{ plan.created_by.get_user_name }}</small>
{% if plan.shared.all %}
<br/><small class="text-muted">{% trans 'Shared with' %}
{% for x in plan.shared.all %}{{ x.get_user_name }}{% if not forloop.last %}, {% endif %} {% endfor %}</small>
{% endif %}
</div>
</div>
<br/>
<br/>
{% if plan.title %}
<div class="row">
<div class="col col-12">
<h4>{{ plan.title }}</h4>
</div>
</div>
{% endif %}
{% if plan.recipe %}
<div class="row">
<div class="col col-12">
<div class="card">
<div class="card-body">
{% recipe_rating plan.recipe request.user as rating %}
<h5 class="card-title"><a
href="{% url 'view_recipe' plan.recipe.pk %}">{{ plan.recipe }}</a> {{ rating|safe }}
</h5>
{% recipe_last plan.recipe request.user as last_cooked %}
{% if last_cooked %}
{% trans 'Last cooked' %} {{ last_cooked|date }}
{% else %}
{% trans 'Never cooked before.' %}
{% endif %}
{% if plan.recipe.keywords %}
<br/>
<br/>
{{ plan.recipe.all_tags }}
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{% if plan.note %}
<br/>
<div class="row">
<div class="col col-12">
{{ plan.note | markdown | safe }}
</div>
</div>
{% endif %}
{% if same_day_plan %}
<br/>
<h4>{% trans 'Other meals on this day' %}</h4>
<ul class="list-group list-group-flush">
{% for x in same_day_plan %}
<li class="list-group-item"><a href="{% url 'view_plan_entry' x.pk %}">{{ x.get_label }}
({{ x.get_meal_name }})</a></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% load i18n %}
{% load l10n %}
@@ -10,8 +11,8 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pretty-checkbox@3.0/dist/pretty-checkbox.min.css"
integrity="sha384-ICB8i/maQ/5+tGLDUEcswB7Ch+OO9Oj8Z4Ov/Gs0gxqfTgLLkD3F43MhcEJ2x6/D" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'custom/css/markdown_blockquote.css' %}">
<!-- prevent weired character stuff escaping the pdf box -->
<style>
/* fixes print layout being disturbed by print button tooltip */
@media print {
@@ -20,6 +21,7 @@
}
}
/* prevent weired character stuff escaping the pdf box */
.textLayer > span {
color: transparent;
position: absolute;
@@ -27,28 +29,6 @@
cursor: text;
transform-origin: 0% 0%;
}
/* css classes needed to render markdown blockquotes */
blockquote {
background: #f9f9f9;
border-left: 4px solid #ccc;
margin: 1.5em 10px;
padding: .5em 10px;
quotes: none;
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: .1em;
margin-right: .25em;
vertical-align: -.4em;
}
blockquote p {
display: inline;
}
</style>
{% endblock %}

View File

@@ -5,6 +5,11 @@
{% block title %}{% trans 'Settings' %}{% endblock %}
{% block extra_head %}
{{ preference_form.media }}
{% endblock %}
{% block content %}
<h3>

View File

@@ -11,8 +11,10 @@ urlpatterns = [
path('search/', views.search, name='view_search'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
path('shopping/', views.shopping_list, name='view_shopping'),
path('settings/', views.settings, name='view_settings'),
path('settings/', views.user_settings, name='view_settings'),
path('history/', views.history, name='view_history'),
path('import/', import_export.import_recipe, name='view_import'),
path('export/', import_export.export_recipe, name='view_export'),

View File

@@ -272,7 +272,7 @@ class MealPlanUpdate(OwnerRequiredMixin, UpdateView):
form_class = MealPlanForm
def get_success_url(self):
return reverse('view_plan')
return reverse('view_plan_entry', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(MealPlanUpdate, self).get_context_data(**kwargs)

View File

@@ -128,7 +128,8 @@ class MealPlanCreate(GroupRequiredMixin, CreateView):
def get_initial(self):
return dict(
meal=self.request.GET['meal'] if 'meal' in self.request.GET else None,
date=datetime.strptime(self.request.GET['date'], '%Y-%m-%d') if 'date' in self.request.GET else None
date=datetime.strptime(self.request.GET['date'], '%Y-%m-%d') if 'date' in self.request.GET else None,
shared=self.request.user.userpreference.plan_share.all() if self.request.user.userpreference.plan_share else None
)
def form_valid(self, form):

View File

@@ -7,14 +7,16 @@ from django.contrib.auth.forms import PasswordChangeForm
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.urls import reverse_lazy
from django.utils import timezone
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
from django.conf import settings
from cookbook.filters import RecipeFilter
from cookbook.forms import *
from cookbook.helper.permission_helper import group_required
from cookbook.tables import RecipeTable, RecipeTableSmall
from cookbook.tables import RecipeTable, RecipeTableSmall, CookLogTable, ViewLogTable
def index(request):
@@ -42,7 +44,21 @@ def search(request):
table = RecipeTableSmall(f.qs)
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'index.html', {'recipes': table, 'filter': f})
if request.GET == {}:
qs = Recipe.objects.filter(viewlog__created_by=request.user).order_by('-viewlog__created_at').all()
recent_list = []
for r in qs:
if r not in recent_list:
recent_list.append(r)
if len(recent_list) >= 5:
break
last_viewed = RecipeTable(recent_list)
else:
last_viewed = None
return render(request, 'index.html', {'recipes': table, 'filter': f, 'last_viewed': last_viewed})
else:
return render(request, 'index.html')
@@ -78,6 +94,10 @@ def recipe_view(request, pk):
comment_form = CommentForm()
bookmark_form = RecipeBookEntryForm()
if request.user.is_authenticated:
if not ViewLog.objects.filter(recipe=recipe).filter(created_by=request.user).filter(created_at__gt=(timezone.now() - timezone.timedelta(minutes=5))).exists():
ViewLog.objects.create(recipe=recipe, created_by=request.user)
return render(request, 'recipe_view.html',
{'recipe': recipe, 'ingredients': ingredients, 'comments': comments, 'comment_form': comment_form,
'bookmark_form': bookmark_form})
@@ -130,13 +150,26 @@ def meal_plan(request):
plan[t[0]] = {'type_name': t[1], 'days': copy.deepcopy(days_dict)}
for d in days:
plan_day = MealPlan.objects.filter(date=d).all()
plan_day = MealPlan.objects.filter(date=d).filter(Q(created_by=request.user) | Q(shared=request.user)).distinct().all()
for p in plan_day:
plan[p.meal]['days'][d].append(p)
return render(request, 'meal_plan.html', {'js_week': js_week, 'plan': plan, 'days': days, 'surrounding_weeks': surrounding_weeks})
@group_required('user')
def meal_plan_entry(request, pk):
plan = MealPlan.objects.get(pk=pk)
if plan.created_by != request.user and plan.shared != request.user:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
same_day_plan = MealPlan.objects.filter(date=plan.date).exclude(pk=plan.pk).filter(Q(created_by=request.user) | Q(shared=request.user)).order_by('meal').all()
return render(request, 'meal_plan_entry.html', {'plan': plan, 'same_day_plan': same_day_plan})
@group_required('user')
def shopping_list(request):
markdown_format = True
@@ -178,7 +211,7 @@ def shopping_list(request):
@group_required('guest')
def settings(request):
def user_settings(request):
up = request.user.userpreference
user_name_form = UserNameForm(instance=request.user)
@@ -195,6 +228,7 @@ def settings(request):
up.default_unit = form.cleaned_data['default_unit']
up.default_page = form.cleaned_data['default_page']
up.search_style = form.cleaned_data['search_style']
up.plan_share.set(form.cleaned_data['plan_share'])
up.save()
if 'user_name_form' in request.POST:
@@ -218,5 +252,12 @@ def settings(request):
return render(request, 'settings.html', {'preference_form': preference_form, 'user_name_form': user_name_form, 'password_form': password_form})
@group_required('guest')
def history(request):
view_log = ViewLogTable(ViewLog.objects.filter(created_by=request.user).order_by('-created_at').all())
cook_log = CookLogTable(CookLog.objects.filter(created_by=request.user).order_by('-created_at').all())
return render(request, 'history.html', {'view_log': view_log, 'cook_log': cook_log})
def markdown_info(request):
return render(request, 'markdown_info.html', {})