Compare commits

...

3 Commits

Author SHA1 Message Date
9566f203a0 Make a basic recipe display page.
No modificiation possible from here yet.
2024-06-18 15:16:22 -04:00
d6aa8e8d6a Add a bunch of properties for beer calculation. 2024-06-18 15:15:22 -04:00
8a3c80b517 Add batch size to beer model. 2024-06-18 15:14:28 -04:00
7 changed files with 327 additions and 121 deletions

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-18 11:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0005_recipehop_time_recipehop_use'),
]
operations = [
migrations.AddField(
model_name='batchrecipe',
name='batch_size',
field=models.DecimalField(decimal_places=2, default=11, max_digits=6),
),
migrations.AlterField(
model_name='batchrecipe',
name='efficiency',
field=models.DecimalField(decimal_places=2, default=75, max_digits=6),
),
]

View File

@ -72,14 +72,106 @@ class CustomIngredient(CustomModel):
class Meta:
abstract = True
class BatchRecipe(CustomModel):
""" Recipe to be stored with a batch."""
name = models.CharField(max_length=50)
batch_recipe = models.BooleanField(null=True)
recipe_json = models.TextField(null=True, blank=True)
mash = models.ForeignKey('Mash' , on_delete=models.PROTECT, null=True, blank=True)
efficiency = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
efficiency = models.DecimalField(max_digits=6, decimal_places=2, default=75)
batch_size = models.DecimalField(max_digits=6, decimal_places=2, default=11)
@property
def fermentables(self):
return [x for x in list(self.recipefermentable_set.all())]
@property
def hops(self):
return [x for x in list(self.recipehop_set.all())]
@property
def final_volume(self):
return float(self.batch_size) + self.hop_water_loss + self.net_kettle_deadspace + self.kettle_hose_loss
@property
def ferm_yield(self):
ferm_yield = 0
for f in self.fermentables:
if f.fermentable.fermentable_type == 3: # Is sugar
ferm_yield += f.quantity * (f.fermentable.potential - 1) * 1000
else:
ferm_yield += f.quantity * (self.efficiency / 100) * (f.fermentable.potential - 1) * 1000
return float(ferm_yield)
@property
def mash_yield(self):
mash_yield = 0
for f in self.fermentables:
if f.fermentable.fermentable_type != 3: # Is not sugar
mash_yield += f.quantity * (self.efficiency / 100) * (f.fermentable.potential - 1) * 1000
return float(mash_yield)
@property
def original_sg(self):
return round(1 + self.ferm_yield / self.final_volume / 1000, 3)
@property
def pre_boil_sg(self):
return self.ferm_yield / (self.final_volume + self.boil_off_gph)
@property
def hop_water_loss(self):
hop_absorption = .025 # gallons per ounce
return sum([float(x.quantity) * hop_absorption for x in self.hops])
@property
def net_kettle_deadspace(self):
# If hops in kettle deadspace
result = self.kettle_dead_space - self.hop_water_loss
return float(max(0, result)) # No deadspace if its all filled with hop trub)
# Else hops in bag or removed
return 0
@property
def kettle_hose_loss(self):
return .25 # TODO
@property
def kettle_dead_space(self):
return .25 # TODO
@property
def boil_off_gph(self):
return .8 # TODO
@property
def ibu_tinseth(self):
return sum([x.ibu_tinseth for x in self.hops])
@property
def srm(self):
color_total = sum([x.srm for x in self.fermentables])
return 1.4922*(color_total**0.6859)
@property
def srm_hex(self):
SRM_HEX = {
1: 'F3F993',2: 'F5F75C',3: 'F6F513',4: 'EAE615',5: 'E0D01B',
6: 'D5BC26',7: 'CDAA37',8: 'C1963C',9: 'BE8C3A',10: 'BE823A',
11: 'C17A37',12: 'BF7138',13: 'BC6733',14: 'B26033',15: 'A85839',
16: '985336',17: '8D4C32',18: '7C452D',19: '6B3A1E',20: '5D341A',
21: '4E2A0C',22: '4A2727',23: '361F1B',24: '261716',25: '231716',
26: '19100F',27: '16100F',28: '120D0C',29: '100B0A',30: '050B0A'
}
return '#{}'.format(SRM_HEX[int(self.srm)])
def __str__(self):
return self.name
@ -96,6 +188,7 @@ class Fermentable(CustomIngredient):
types = {
1: 'Grain',
2: 'Adjunct',
3: 'Sugar'
}
grain_category = models.IntegerField(choices=categories, default=1)
@ -118,6 +211,10 @@ class RecipeFermentable(CustomModel):
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
@property
def srm(self):
return round(float(self.fermentable.lovibond) * float(self.quantity) / self.recipe.final_volume, 1)
class Hop(CustomIngredient):
uses = {
1: 'Bittering',
@ -154,6 +251,25 @@ class RecipeHop(CustomModel):
time = models.IntegerField(default=60, validators=[MinValueValidator(0)])
use = models.IntegerField(choices=uses, default=1)
@property
def ibu_tinseth(self):
type_bonus = {
1: 1.1, # Pellet
2: 1.0, # Leaf
3: 1.1, # Cryo
4: 1.4, # CO2 Extract
}
ibu = 0
average_wort_sg = (self.recipe.pre_boil_sg/1000 + (self.recipe.original_sg-1)) / 2
if self.use == 1:
conc = ((float(self.hop.alpha) / 100) * float(self.quantity)) * 7490 / self.recipe.final_volume
util = (type_bonus[self.hop.hop_type] * 1.65 * (0.000125**average_wort_sg)) * ((1-2.71828182845904**(-0.04 * self.time))/4.15)
ibu = conc * util
return float(ibu)
class Misc(CustomIngredient):
uses = {
1: 'Mash',

BIN
beer/static/beer_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -17,123 +17,10 @@ input, label {
<div class="container" id="main">
<h1>Comming Soon?</h1>
<ul>
{% for recipe in recipes %}
<div class="container-fluid">
<h3>{{ recipe.name }}</h3>
<!-- Ferm and Hop Row -->
<div class="row">
<!-- Fermentables -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Fermentables (xx.x lbs)</div>
<table class="table table-sm ">
<tbody>
{% for f in recipe.recipefermentable_set.all %}
<tr><td>{{ f.quantity }}</td><td>{{ f.fermentable.name }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Pre-Boil Gravity: <b>1.XXX</b><br>
Original Gravity: <b>1.XXX</b><br>
Color: <b>XXX SRM</b>
</p>
</div>
</div>
<!-- Hops -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Hops (xx.x oz)</div>
<table class="table table-sm ">
<tbody>
{% for h in recipe.recipehop_set.all %}
<tr><td>{{ h.quantity }}</td><td>{{ h.hop.name }}</td><td>{{ h.time }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Total IBU: <b>XX</b><br>
BU/GU: <b>0.XX</b><br>
RBR: <b>0.XX</b>
</p>
</div>
</div>
</div>
<!-- Misc and Yeast Row -->
<div class="row">
<!-- Misc -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Misc.</div>
<table class="table table-sm">
<tbody>
{% for m in recipe.recipemisc_set.all %}
<tr><td>{{ m.misc.name }}</td><td>{{ m.misc.quantity }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Yeast -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Yeast</div>
<table class="table table-sm">
<tbody>
{% for y in recipe.recipeyeast_set.all %}
<tr><td>{{ y.yeast.name }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Mash and Fermentation Row -->
<div class="row">
<!-- Mash -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Mash Profile {{ recipe.mash.name }}</div>
<table class="table table-sm ">
<tbody>
{% for step in recipe.mash.mashstep_set.all %}
<tr><td>{{ step.name }}</td><td>{{ step.step_temp }} &deg;F</td><td>{{ step.step_time }} min</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Fermentation -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Fermentation Profile</div>
<table class="table table-sm">
<tbody>
<!-- {% for y in recipe.recipeyeast_set.all %} -->
<!-- <tr><td>{{ y.yeast.name }}</td></tr> -->
<!-- {% endfor %} -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<p>
<hr/>
<p>
<li><a href="{% url 'beer:recipe' recipe.id %}">{{ recipe.name }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,165 @@
{% extends "base.html" %}
{% load mathfilters %}
{% load funcs %}
{% load static %}
{% block style %}
input, label {
display:block;
}
.container-beer {
background: {{ recipe.srm_hex }};
}
{% endblock %}
{% block title %}Recipes{% endblock %}
{% block jumbotron %}{{ recipe.name }}{% endblock %}
{% block jumbotronsub %}
<div class="row">
<div class="col-lg d-flex justify-content-between" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div>
<div class="container-beer"><img src="{% static "beer_back.png" %}" alt="" class="img-responsive d-none d-sm-block"></div>
</div>
<dl class="row">
<dt>Recipe</dt>
<dd>{{ recipe.name }}</dd>
<dt>Author</dt>
<dd>Author</dd>
<dt>Type</dt>
<dd>All Grain</dd>
</dl>
</div>
<div class="col-lg-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div>Equipment Selection</div>
<b>Batch Size:</b> {{ recipe.batch_size }} gal <b>Actual Volume:</b> {{ recipe.final_volume|floatformat:2 }}<br>
<b>Mash Efficiency:</b> {{ recipe.efficiency|floatformat:2 }} %
</div>
<div class="col-lg-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
Style Data
</div>
</div>
{% endblock %}
{% block content %}
<div class="container" id="main">
<div class="container-fluid">
<!-- Ferm and Hop Row -->
<div class="row">
<!-- Fermentables -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Fermentables ({{ fermentable_weight|floatformat:2 }} lbs)</div>
<table class="table table-sm ">
<tbody>
{% for f in recipe.recipefermentable_set.all %}
<tr><td>{{ f.quantity|floatformat:2 }} lb</td><td>{{ f.fermentable.name }}<br>{{ f.srm }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Pre-Boil Gravity: <b>1.XXX</b><br>
Original Gravity: <b>{{ recipe.original_sg }}</b><br>
Color: <b>XXX SRM</b>
</p>
</div>
</div>
<!-- Hops -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Hops ({{ hop_weight|floatformat:2 }} oz)</div>
<table class="table table-sm ">
<tbody>
{% for h in recipe.recipehop_set.all %}
<tr><td>{{ h.quantity|floatformat:2 }} oz</td><td>{{ h.hop.name }} {{ h.hop.alpha|floatformat:1 }} %<br>{{ h.ibu_tinseth|floatformat:1 }} IBU</td><td>{{ h.time }} min</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Total IBU: <b>{{ recipe.ibu_tinseth|floatformat:1 }}</b><br>
BU/GU: <b>0.XX</b><br>
RBR: <b>0.XX</b>
</p>
</div>
</div>
</div>
<!-- Misc and Yeast Row -->
<div class="row">
<!-- Misc -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Misc.</div>
<table class="table table-sm">
<tbody>
{% for m in recipe.recipemisc_set.all %}
<tr><td>{{ m.misc.name }}</td><td>{{ m.misc.quantity }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Yeast -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Yeast</div>
<table class="table table-sm">
<tbody>
{% for y in recipe.recipeyeast_set.all %}
<tr><td>{{ y.yeast.name }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Mash and Fermentation Row -->
<div class="row">
<!-- Mash -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Mash Profile {{ recipe.mash.name }}</div>
<table class="table table-sm ">
<tbody>
{% for step in recipe.mash.mashstep_set.all %}
<tr><td>{{ step.name }}</td><td>{{ step.step_temp }} &deg;F</td><td>{{ step.step_time }} min</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Fermentation -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Fermentation Profile</div>
<table class="table table-sm">
<tbody>
<!-- {% for y in recipe.recipeyeast_set.all %} -->
<!-- <tr><td>{{ y.yeast.name }}</td></tr> -->
<!-- {% endfor %} -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<p>
<hr/>
<p>
</div>
{% endblock %}

View File

@ -1,6 +1,10 @@
from django.urls import path
from .views import home
from django.conf import settings
from django.conf.urls.static import static
from .views import home, view_recipe
urlpatterns = [
path('', home, name='home'),
]
path('recipes/<int:recipe_id>/', view_recipe, name='recipe'),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -1,5 +1,5 @@
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView
from django.views.generic import ListView, CreateView
from django.http import HttpResponse
from .models import UserProfile, BatchRecipe, Batch
@ -36,3 +36,14 @@ def home(request):
batch_obj.save()
return render(request, 'beer/home.html',{'recipes':BatchRecipe.objects.all()})
def view_recipe(request, recipe_id):
recipe = get_object_or_404(BatchRecipe, pk=recipe_id)
context = {
'recipe': recipe,
'fermentable_weight': sum([x.quantity for x in recipe.fermentables]),
'hop_weight': sum([x.quantity for x in recipe.hops]),
}
return render(request, 'beer/recipe.html', context)