Compare commits
3 Commits
1a4d352d85
...
9566f203a0
Author | SHA1 | Date | |
---|---|---|---|
9566f203a0 | |||
d6aa8e8d6a | |||
8a3c80b517 |
@ -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),
|
||||
),
|
||||
]
|
120
beer/models.py
120
beer/models.py
@ -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
BIN
beer/static/beer_back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
@ -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 }} °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 %}
|
||||
|
165
beer/templates/beer/recipe.html
Normal file
165
beer/templates/beer/recipe.html
Normal 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 }} °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 %}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user