Skip to content

Commit

Permalink
Added user app with basic functions and rating model.
Browse files Browse the repository at this point in the history
  • Loading branch information
PatReis committed Aug 27, 2024
1 parent 67e280f commit 1706cee
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 40 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ This is a toy web applications for me to learn some HTML, CSS and Django.
It is intended to be hosted in a private network to store and manage your personal cooking recipes.
The website is in German and designed very minimalistic, i.e. no theme etc.

Additional features that could/should be added are proper security and deployment (if publicly hosted), user authentication, comments to and rating of recipes.
Version [1.x.x](https://github.com/PatReis/fav_reps/releases/tag/v1.0.0) only features the basic recipe forms. With version 2.x.x there is also a simple user and rating system.

Additional features that could/should be added are proper security and deployment (if publicly hosted).

## Installation

Expand All @@ -27,14 +29,18 @@ You can upload your recipes in a form to separate entries like ingredients, cook
The recipes can be searched, sorted and also filtered by their assigned topics. They are rendered in an HTML template from the database with ingredients calculated based on the number of persons.
Topics are to be created by the admin ``python manage.py createsuperuser`` . And by visiting the `/admin` url.
You can also access/export the database via REST API at url `/api` and `/api/recipes` in json format.
In the latest version you have to create a user account to post your recipes and rate other users recipes.

Example of the website.

<p align="center">
<img src="https://github.com/PatReis/fav_reps/blob/main/static/images/example_1.jpg" />
<img src="https://github.com/PatReis/fav_reps/blob/main/static/images/example_3.jpg" />
</p>
<p align="center">
<img src="https://github.com/PatReis/fav_reps/blob/main/static/images/example_4.jpg" />
</p>
<p align="center">
<img src="https://github.com/PatReis/fav_reps/blob/main/static/images/example_2.jpg" />
<img src="https://github.com/PatReis/fav_reps/blob/main/static/images/example_5.jpg" />
</p>

# Serving on Raspberry Pi
Expand Down
Binary file modified db.sqlite3
Binary file not shown.
3 changes: 2 additions & 1 deletion recipes_app/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.contrib import admin
from .models import Topic, Recipe
from .models import Topic, Recipe, Rating


admin.site.register(Topic)
admin.site.register(Recipe)
admin.site.register(Rating)
2 changes: 1 addition & 1 deletion recipes_app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class RecipeForm(ModelForm):
class Meta:
model = Recipe
fields = '__all__'
exclude = ["owner"]
exclude = ["owner", "rating_mean", "rating_count"]
# widgets = {
# 'name': TextInput(attrs={
# 'class': "form-control",
Expand Down
18 changes: 16 additions & 2 deletions recipes_app/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-08-24 14:16
# Generated by Django 5.0.7 on 2024-08-27 14:03

from django.db import migrations, models

Expand All @@ -11,6 +11,19 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='Rating',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stars', models.IntegerField(default=0)),
('body', models.CharField(blank=True, max_length=500, null=True)),
('updated', models.DateTimeField(auto_now=True)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-updated', '-created'],
},
),
migrations.CreateModel(
name='Recipe',
fields=[
Expand All @@ -31,7 +44,8 @@ class Migration(migrations.Migration):
('expected_time_rest', models.FloatField(blank=True, default=0.0, null=True)),
('difficulty', models.IntegerField(blank=True, default=0, null=True)),
('image_meal', models.ImageField(blank=True, upload_to='recipes')),
('rating', models.FloatField(blank=True, default=0.0)),
('rating_mean', models.FloatField(blank=True, default=0.0, null=True)),
('rating_count', models.IntegerField(blank=True, default=0, null=True)),
],
options={
'ordering': ['-updated', '-created'],
Expand Down
12 changes: 11 additions & 1 deletion recipes_app/migrations/0002_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-08-24 14:16
# Generated by Django 5.0.7 on 2024-08-27 14:03

import django.db.models.deletion
from django.conf import settings
Expand All @@ -15,11 +15,21 @@ class Migration(migrations.Migration):
]

operations = [
migrations.AddField(
model_name='rating',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='recipe',
name='owner',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='rating',
name='recipe',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes_app.recipe'),
),
migrations.AddField(
model_name='recipe',
name='topic',
Expand Down
21 changes: 20 additions & 1 deletion recipes_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,29 @@ class Recipe(models.Model):
expected_time_rest = models.FloatField(null=True, blank=True, default=0.0)
difficulty = models.IntegerField(null=True, blank=True, default=0)
image_meal = models.ImageField(blank=True, upload_to="recipes")
rating = models.FloatField(blank=True, default=0.0)

rating_mean = models.FloatField(null=True, blank=True, default=0.0)
rating_count = models.IntegerField(null=True, blank=True, default=0)

class Meta:
ordering = ['-updated', '-created']

def __str__(self):
return self.name


class Rating(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)

stars = models.IntegerField(default=0)
body = models.CharField(null=True, blank=True, max_length=500)

updated = models.DateTimeField(auto_now=True)
created = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ['-updated', '-created']

def __str__(self):
return self.body
6 changes: 3 additions & 3 deletions recipes_app/templates/recipes_app/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ <h3>Kategorien</h3>
<h3>Sortieren</h3>
<form method="GET" action="{% url 'home' %}">
<select name="srtval" id="srtval">
<option value="rating">Bewertung</option>
<option value="rating_mean">Bewertung</option>
<option value="persons">Personen</option>
<option value="created">Erstellt</option>
<option value="updated">Geändert</option>
Expand All @@ -73,7 +73,7 @@ <h3>Sortieren</h3>
{% endif %}
</div>
<a href="{% url 'recipe' recipe.id %}">{{recipe.name}}</a>
<p>Schwierigkeit: {{recipe.difficulty}}, Bewertung: {{recipe.rating}} </p>
<p>Schwierigkeit: {{recipe.difficulty}}, Bewertung: {{recipe.rating_mean}} </p>
<p>Gesamtzeit: {{ recipe.expected_time_total|floatformat:"0" }} min </p>
</div>
{% endfor %}
Expand Down Expand Up @@ -107,7 +107,7 @@ <h3>Sortieren</h3>
<h3>Ein Top Rezept der Seite</h3>
{% if recipe_best %}
<p><a href="{% url 'recipe' recipe_best.id %}">{{recipe_best.name}}</a></p>
<p>Schwierigkeit: {{recipe_best.difficulty}}, Bewertung: {{recipe_best.rating}} </p>
<p>Schwierigkeit: {{recipe_best.difficulty}}, Bewertung: {{recipe_best.rating_mean}} </p>
<p>Gesamtzeit: {{ recipe_best.expected_time_total|floatformat:"0" }} min </p>
{% endif %}
</div>
Expand Down
41 changes: 33 additions & 8 deletions recipes_app/templates/recipes_app/recipe.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
font-family: arial,
sans-serif;
border-collapse: collapse;
width: 50%;
width: 60%;
margin: auto;
border: 1;
}
Expand Down Expand Up @@ -45,7 +45,7 @@ <h1>{{recipe.name}}</h1>
{% endfor %}
</p>
{% if recipe.image_meal %}
<img src="{{ recipe.image_meal.url }}" style="width: 30%; " >
<img src="{{ recipe.image_meal.url }}" style="width: auto; height: 300px;" >
{% endif %}
<p>
Gesamtzeit: {{ recipe.expected_time_total|floatformat:"0" }} min,
Expand All @@ -54,19 +54,18 @@ <h1>{{recipe.name}}</h1>
Ruhezeit: {{ recipe.expected_time_rest|floatformat:"0" }} min
</p>
<p>
Schwierigkeit: "{{ recipe.difficulty|default_if_none:"-" }}" mit Bewertung "{{ recipe.rating|default_if_none:"-" }}"
Schwierigkeit: "{{ recipe.difficulty|default_if_none:"-" }}" mit Bewertung "{{ recipe.rating_mean|default_if_none:"-" }}"
</p>
<p>
Nährwerte pro Portion: {{ recipe.nutrients_person|default_if_none:"-" }} kcal
</p>
<br>
<h3 id="ingredients">Zutaten: </h3>

<form method="POST" action="#ingredients" enctype="multipart/form-data">
{% csrf_token %}
<form method="GET" action="#ingredients" >
<label for="id_persons">Personen:</label>
<input type="number" id="id_persons" name="persons" min="1" max="100" value="{{required_persons}}">
<input type="submit" value="Berechne" style="width: 65px;"/>
<input type="submit" value="Berechne" style="width: 70px;"/>
</form>
<br>
<table>
Expand All @@ -79,14 +78,14 @@ <h3 id="ingredients">Zutaten: </h3>
</table>
<br>
<h3>Zubereitung:</h3>
<div style="text-align: left; display: inline-block; width: 65%">
<div style="text-align: left; display: inline-block; width: 75%">
<p>
{{ recipe.steps|linebreaks }}
</p>
</div>
<br>
<h3>Tipps:</h3>
<div style="text-align: left; display: inline-block; width: 65%">
<div style="text-align: left; display: inline-block; width: 75%">
<p>
{{ recipe.tips|linebreaks }}
</p>
Expand All @@ -109,10 +108,36 @@ <h3>Nährwerte pro Person:</h3>
erstellt vor {{ recipe.created|timesince }},
geändert vor {{ recipe.updated|timesince }}
</p>
<br>


{% if request.user.is_authenticated %}
<h4>Meine Bewertung</h4>
<form method="POST" action="" >
{% csrf_token %}
<label for="id_stars">Sterne:</label>
<input type="number" id="id_stars" name="stars" required min="1" max="5" value="0">
<label for="id_body">Text (optional):</label>
<input type="text" name="body" maxlength="500" id="id_body" style="width: 300px;">
<input type="submit" value="Bewerten" style="width: 65px;"/>
</form>
<br>
{% endif %}



<h4>User Bewertungen</h4>
{% for rate in ratings %}
<p>
{{rate.stars}} Sterne von <a href="{% url 'user-profile' rate.user.id %}" >{{rate.user.username|default_if_none:"Unkown"}}</a> : {{rate.body}}
</p>
{% endfor %}
</div>





</div>

{% endblock %}
4 changes: 0 additions & 4 deletions recipes_app/templates/recipes_app/recipe_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ <h3>{{title_of_form}}</h3>
<label for="id_persons">Personen:</label>
{{form.persons}}
</p>
<p>
<label for="id_rating">Bewertung [0 - 5 Sterne]:</label>
{{form.rating}}
</p>
<p>
<label for="id_ingredients">Zutaten:</label>
{{form.ingredients}}
Expand Down
50 changes: 35 additions & 15 deletions recipes_app/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.shortcuts import render, redirect
from .models import Recipe, Topic
from .models import Recipe, Topic, Rating
from .forms import RecipeForm
import math
from django.contrib.auth.decorators import login_required
Expand Down Expand Up @@ -29,16 +29,16 @@ def home(request):
recipes_count = recipes_filter.count()

if sortval is not None:
recipes_filter = recipes_filter.order_by(sortdir + sortval)
recipes_filter = recipes_filter.order_by(sortdir + sortval).distinct()

page_size = 50
num_pages = math.ceil(recipes_count/page_size)
pgNr = min(max(0, pgNr), num_pages - 1)
recipes = recipes_filter[pgNr*page_size:(pgNr+1)*page_size] if num_pages > 0 else []

recipe_best = None
if num_pages>0:
rating_values = [r.rating for r in recipes]
if num_pages > 0:
rating_values = [r.rating_mean for r in recipes]
recipe_best = recipes[rating_values.index(max(rating_values))]

context = {"recipes": recipes,
Expand All @@ -51,17 +51,11 @@ def home(request):

def recipe(request, pk):
recipe_from_key = Recipe.objects.get(id=pk)
recipe_fields = {getattr(recipe_from_key, field.name) for field in recipe_from_key._meta.get_fields()}

# recipe_fields = {getattr(recipe_from_key, field.name) for field in recipe_from_key._meta.get_fields() if hasattr(recipe_from_key, field.name)}
recipe_topics = recipe_from_key.topic.all()

persons_default = recipe_from_key.persons
if request.method == 'POST':
required_persons = request.POST.get("persons")
recompute_persons = True
else:
required_persons = persons_default
recompute_persons = False
required_persons = int(request.GET.get("persons")) if request.GET.get("persons") is not None else persons_default
recompute_persons = True if request.GET.get("persons") is not None else False

ingredients_lines = [x.replace('\t', ' ') for x in recipe_from_key.ingredients.split('\n')]
ingredients_formated = []
Expand All @@ -77,8 +71,34 @@ def recipe(request, pk):
else:
ingredients_formated.append((' ', x))

context = {"recipe": recipe_from_key, "recipe_fields": recipe_fields, "ingredients_formated": ingredients_formated,
"required_persons": required_persons, "recipe_topics": recipe_topics}
if request.method == 'POST' and request.user.is_authenticated:
stars = int(request.POST.get("stars")) if request.POST.get("stars") is not None else 0
body = request.POST.get("body")
if stars > 0:
ratings = recipe_from_key.rating_set.all()
has_rated = False
for r in ratings:
if request.user == r.user:
r.stars = stars
r.body = body
r.save()
has_rated = True
continue
if not has_rated:
Rating.objects.create(
user=request.user,
recipe=recipe_from_key,
stars=stars,
body=body,
)

ratings = recipe_from_key.rating_set.all()
recipe_from_key.rating_count = ratings.count()
recipe_from_key.rating_mean = sum([r.stars for r in ratings])/float(ratings.count() if ratings.count() > 0 else 1.0)
recipe_from_key.save(update_fields=['rating_mean', 'rating_count'])

context = {"recipe": recipe_from_key, "ingredients_formated": ingredients_formated,
"required_persons": required_persons, "recipe_topics": recipe_topics, "ratings": ratings}
return render(request, 'recipes_app/recipe.html', context)


Expand Down
Binary file added static/images/example_3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/example_4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/images/example_5.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion user_app/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.7 on 2024-08-24 14:16
# Generated by Django 5.0.7 on 2024-08-27 14:03

import django.contrib.auth.models
import django.contrib.auth.validators
Expand Down

0 comments on commit 1706cee

Please sign in to comment.