Compare commits

...

16 Commits

Author SHA1 Message Date
7c25b8924a Use display weights for ingredients. 2024-06-27 12:55:12 -04:00
b39bfdd267 Add BJCP guidelines to the database. 2024-06-27 09:56:09 -04:00
53b7a1b128 Just PEP8 stuff. 2024-06-26 14:56:29 -04:00
e8c9196fc0 Field help text.
Specify units used.
2024-06-26 14:56:09 -04:00
93b93a1448 make timestamp non-editable.
Hides from admin panel since it's generated automatically.
2024-06-26 14:53:35 -04:00
3fd248d8b7 Fix page title for batch template. 2024-06-26 12:37:36 -04:00
90bf91c3ed Added additional database fields and properties.
Much calculation of efficiency and stuff now.
2024-06-26 11:48:34 -04:00
570ede627f Some more handy helper functions. 2024-06-26 11:47:04 -04:00
07c881bdfe SG and Plato conversion calcs. 2024-06-25 14:48:59 -04:00
1d830eb22d More batch info.
Please enter the commit message for your changes. Lines starting
2024-06-24 16:25:18 -04:00
1986d9fbd0 Remove as much list comprehension as possible.
Using aggregate functions instead.
2024-06-21 11:33:47 -04:00
671ea27d9f Add many to many field with through relationship for fermentables.
Gives direct access to fermentable objects.
2024-06-21 11:32:53 -04:00
b6be827d14 Getting interface ready for modals. 2024-06-21 07:22:50 -04:00
2f33a27e47 Stupid line endings. 2024-06-21 07:21:38 -04:00
12fca700da Rename BatchRecipe -> Recipe.
Much better now thanks.
2024-06-20 15:02:17 -04:00
ea803ed009 Run migrations when starting dev server. 2024-06-20 14:53:18 -04:00
34 changed files with 13834 additions and 15017 deletions

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django.utils.html import format_html
from django.apps import apps
from beer.models import Batch, BatchRecipe, Mash, MashStep, \
from beer.models import Batch, Recipe, Mash, MashStep, \
RecipeFermentable, RecipeHop, RecipeMisc, RecipeYeast
from yeast.models import Yeast
@ -34,9 +34,9 @@ class StrainInline(admin.TabularInline):
extra = 1
@admin.register(BatchRecipe)
class BatchRecipeAdmin(admin.ModelAdmin):
list_display = ['name']
@admin.register(Recipe)
class RecipeAdmin(admin.ModelAdmin):
list_display = ['name', 'total_extract_kg']
inlines = [
FermentableInline,
HopInline,
@ -47,11 +47,40 @@ class BatchRecipeAdmin(admin.ModelAdmin):
@admin.register(Batch)
class BeerBatchAdmin(admin.ModelAdmin):
list_display = ['brewfather_id', 'batch_url']
list_display = [
'brewfather_id',
'batch_url',
'brewhouse_efficiency',
'conversion_efficiency',
]
inlines = [
SampleInline,
]
fieldsets = [
[None, {
'fields': [
('brewfather_id', 'brewfather_num', 'brewfather_name'),
'recipe',
]
}],
['Mash', {
'fields': ['first_runnings', 'mash_ph'],
}],
['Boil', {
'fields': [
('pre_boil_vol', 'pre_boil_sg'),
('post_boil_vol', 'post_boil_sg'),
],
}],
['Ferment', {
'fields': [
('fermenter_topup_vol', 'fermenter_vol'),
('original_sg', 'final_sg'),
],
}],
]
def batch_url(self, obj):
url_string = ('<a href="{root}/tabs/batches/batch/{batch_id}">'
'Brewfather Batch ID: {batch_id}</a>')

View File

@ -9,6 +9,7 @@ RECIPE_URL = 'https://api.brewfather.app/v2/recipes'
BATCH_URL = 'https://api.brewfather.app/v2/batches'
PULL_LIMIT = 50
BREWFATHER_CONVERT_LOOKUP = { # local_name: brewfather_name
'all': {
'name': 'name',
@ -86,3 +87,77 @@ def get_batches(api_user, api_key, batch=''):
data = data + get_batches(batch=last_id)
return data
def sg_plato(sg):
"""Convert specific gravtiy to °P."""
sg = float(sg)
return (-1*616.868) + (1111.14*sg) - (630.272*sg**2) + (135.997*sg**3)
def plato_sg(plato):
"""Convert °P to specific gravtiy."""
return 1 + (plato / (258.6 - (plato/258.2) * 227.1))
def kg_extract(v, sg):
"""Calculate kg of extract based on volume and SG."""
return float(v) * float(sg) * sg_plato(sg)/100
def convert(value, start_unit, end_unit):
"""Convert units."""
family_dict = unit_family(start_unit)
if family_dict:
start = [val for key, val in family_dict.items()
if start_unit in key.split(',')][0]
end = [val for key, val in family_dict.items()
if end_unit in key.split(',')][0]
return float(value) * end / start
def unit_family(unit_name):
"""Find unit family base on unit name."""
unit_lookup = [
{'name': 'length',
'units': {
'm,meter,meters': 1.0,
'cm,centimeter,centimeters': 100,
'mm,millimeter,millimeters': 1000,
'in,inch,inches': 39.3701,
'yd,yard,yards': 1.09361,
'ft,foot,feet': 3.28084,
'mi,mile,miles': 0.000621371,
'km,killometer,killometers': .001,
}},
{'name': 'mass',
'units': {
'kg,kilogram,kilograms': 1.0,
'g,gram,grams': 1000,
'lb,pound,pounds': 2.20462,
'oz,ounce,ounces': 35.274,
'st,stone': 0.157473,
}},
{'name': 'volume',
'units': {
'l,liter,liters': 1.0,
'ml,milliliter,milliliters': 1000,
'floz,fluid ounce,fluid ounces': 33.814,
'cup,cups': 4.22675,
'qt,quart,quarts': 1.05669,
'gal,gallon,gallons': 0.264172,
'ft^3,cubic foot,cubic feet': 0.0353147,
'pt,pint,pints': 4.22675/2,
'tsp,teaspoon,teaspoons': 202.884,
'tbsp,tablespoon,tablespoons': 202.884/3,
}},
]
for family in unit_lookup:
if [key for key in family['units'] if unit_name in key.split(',')]:
return family['units']

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.6 on 2024-06-20 18:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('beer', '0009_batchrecipe_equipment_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='batchrecipe',
options={'verbose_name': 'Recipe', 'verbose_name_plural': 'Recipes'},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.6 on 2024-06-20 18:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('beer', '0010_alter_batchrecipe_options'),
]
operations = [
migrations.RenameModel(
old_name='BatchRecipe',
new_name='Recipe',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-21 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0011_rename_batchrecipe_recipe'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='fermentables',
field=models.ManyToManyField(through='beer.RecipeFermentable', to='beer.fermentable'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-24 18:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0012_recipe_fermentables'),
]
operations = [
migrations.AddField(
model_name='batch',
name='first_runnings',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AddField(
model_name='equipmentprofile',
name='mash_ratio',
field=models.DecimalField(decimal_places=2, default=2.6, max_digits=6),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.0.6 on 2024-06-25 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0013_batch_first_runnings_equipmentprofile_mash_ratio'),
]
operations = [
migrations.AddField(
model_name='batch',
name='post_boil_sg',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True),
),
migrations.AddField(
model_name='batch',
name='post_boil_vol',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AlterField(
model_name='fermentable',
name='moisture',
field=models.DecimalField(blank=True, decimal_places=4, default=0.04, max_digits=6, null=True),
),
migrations.AlterField(
model_name='fermentable',
name='potential',
field=models.DecimalField(decimal_places=4, default=0.8, max_digits=6),
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 5.0.6 on 2024-06-26 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0014_batch_post_boil_sg_batch_post_boil_vol_and_more'),
]
operations = [
migrations.AddField(
model_name='batch',
name='fermenter_topup_vol',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AddField(
model_name='batch',
name='fermenter_vol',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AddField(
model_name='batch',
name='final_sg',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
),
migrations.AddField(
model_name='batch',
name='mash_ph',
field=models.DecimalField(blank=True, decimal_places=3, max_digits=4, null=True),
),
migrations.AddField(
model_name='batch',
name='original_sg',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
),
migrations.AddField(
model_name='batch',
name='pre_boil_sg',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
),
migrations.AddField(
model_name='batch',
name='pre_boil_vol',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AlterField(
model_name='batch',
name='post_boil_sg',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
),
]

View File

@ -0,0 +1,103 @@
# Generated by Django 5.0.6 on 2024-06-26 17:49
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0015_batch_fermenter_topup_vol_batch_fermenter_vol_and_more'),
]
operations = [
migrations.AlterModelTableComment(
name='equipmentprofile',
table_comment='Volumes in liters and weights in kg.',
),
migrations.AlterField(
model_name='batch',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='equipmentprofile',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='equipmentprofile',
name='leaf_hop_trub',
field=models.DecimalField(db_comment='liters/gram', decimal_places=10, default=0.0083454045, max_digits=12),
),
migrations.AlterField(
model_name='equipmentprofile',
name='pellet_hop_trub',
field=models.DecimalField(db_comment='liters/gram', decimal_places=10, default=0.003338162, max_digits=12),
),
migrations.AlterField(
model_name='fermentable',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='hop',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='mash',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='mashstep',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='misc',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recipe',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recipefermentable',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recipehop',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recipemisc',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recipeyeast',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='supplier',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='unit',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='userprofile',
name='created_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
]

View File

@ -0,0 +1,68 @@
# Generated by Django 5.0.6 on 2024-06-26 18:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0016_alter_equipmentprofile_table_comment_and_more'),
]
operations = [
migrations.AlterField(
model_name='equipmentprofile',
name='batch_volume',
field=models.DecimalField(decimal_places=2, default=5.5, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='grain_absorption',
field=models.DecimalField(decimal_places=2, default=0.12, help_text='liters/kilogram', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='hlt_deadspace',
field=models.DecimalField(decimal_places=2, default=0.25, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='kettle_boil_rate',
field=models.DecimalField(decimal_places=2, default=0.5, help_text='liters/hr', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='kettle_deadspace',
field=models.DecimalField(decimal_places=2, default=0.25, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='kettle_plumbing_loss',
field=models.DecimalField(decimal_places=2, default=0.25, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='leaf_hop_trub',
field=models.DecimalField(decimal_places=10, default=0.0083454045, help_text='liters/gram', max_digits=12),
),
migrations.AlterField(
model_name='equipmentprofile',
name='mash_ratio',
field=models.DecimalField(decimal_places=2, default=2.6, help_text='liters/kilogram', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='mt_capacity',
field=models.DecimalField(decimal_places=2, default=10, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='mt_deadspace',
field=models.DecimalField(decimal_places=2, default=0.25, help_text='liters', max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='pellet_hop_trub',
field=models.DecimalField(decimal_places=10, default=0.003338162, help_text='liters/gram', max_digits=12),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-26 19:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0017_alter_equipmentprofile_batch_volume_and_more'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='bjcp_category_id',
field=models.CharField(default='1', max_length=3),
),
migrations.AddField(
model_name='recipe',
name='bjcp_style_id',
field=models.CharField(default='1A', max_length=3),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-26 19:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0018_recipe_bjcp_category_id_recipe_bjcp_style_id'),
]
operations = [
migrations.AlterField(
model_name='recipe',
name='bjcp_style_id',
field=models.CharField(choices=[('1A', 'American Light Lager'), ('1B', 'American Lager'), ('1C', 'Cream Ale'), ('1D', 'American Wheat Beer'), ('2A', 'International Pale Lager'), ('2B', 'International Amber Lager'), ('2C', 'International Dark Lager'), ('3A', 'Czech Pale Lager'), ('3B', 'Czech Premium Pale Lager'), ('3C', 'Czech Amber Lager'), ('3D', 'Czech Dark Lager'), ('4A', 'Munich Helles'), ('4B', 'Festbier'), ('4C', 'Helles Bock'), ('5A', 'German Leichtbier'), ('5B', 'Kölsch'), ('5C', 'German Helles Exportbier'), ('5D', 'German Pils'), ('6A', 'Märzen'), ('6B', 'Rauchbier'), ('6C', 'Dunkles Bock'), ('7A', 'Vienna Lager'), ('7B', 'Altbier'), ('8A', 'Munich Dunkel'), ('8B', 'Schwarzbier'), ('9A', 'Doppelbock'), ('9B', 'Eisbock'), ('9C', 'Baltic Porter'), ('10A', 'Weissbier'), ('10B', 'Dunkles Weissbier'), ('10C', 'Weizenbock'), ('11A', 'Ordinary Bitter'), ('11B', 'Best Bitter'), ('11C', 'Strong Bitter'), ('12A', 'British Golden Ale'), ('12B', 'Australian Sparkling Ale'), ('12C', 'English IPA'), ('13A', 'Dark Mild'), ('13B', 'British Brown Ale'), ('13C', 'English Porter'), ('14A', 'Scottish Light'), ('14B', 'Scottish Heavy'), ('14C', 'Scottish Export'), ('15A', 'Irish Red Ale'), ('15B', 'Irish Stout'), ('15C', 'Irish Extra Stout'), ('16A', 'Sweet Stout'), ('16B', 'Oatmeal Stout'), ('16C', 'Tropical Stout'), ('16D', 'Foreign Extra Stout'), ('17A', 'British Strong Ale'), ('17B', 'Old Ale'), ('17C', 'Wee Heavy'), ('17D', 'English Barley Wine'), ('18A', 'Blonde Ale'), ('18B', 'American Pale Ale'), ('19A', 'American Amber Ale'), ('19B', 'California Common'), ('19C', 'American Brown Ale'), ('20A', 'American Porter'), ('20B', 'American Stout'), ('20C', 'Imperial Stout'), ('21A', 'American IPA'), ('21B', 'Specialty IPA'), ('21C', 'Hazy IPA'), ('22A', 'Double IPA'), ('22B', 'American Strong Ale'), ('22C', 'American Barleywine'), ('22D', 'Wheatwine'), ('23A', 'Berliner Weisse'), ('23B', 'Flanders Red Ale'), ('23C', 'Oud Bruin'), ('23D', 'Lambic'), ('23E', 'Gueuze'), ('23F', 'Fruit Lambic'), ('23G', 'Gose'), ('24A', 'Witbier'), ('24B', 'Belgian Pale Ale'), ('24C', 'Bière de Garde'), ('25A', 'Belgian Blond Ale'), ('25B', 'Saison'), ('25C', 'Belgian Golden Strong Ale'), ('26A', 'Belgian Single'), ('26B', 'Belgian Dubbel'), ('26C', 'Belgian Tripel'), ('26D', 'Belgian Dark Strong Ale'), ('28A', 'Brett Beer'), ('28B', 'Mixed-Fermentation Sour Beer'), ('28C', 'Wild Specialty Beer'), ('28D', 'Straight Sour Beer'), ('29A', 'Fruit Beer'), ('29B', 'Fruit and Spice Beer'), ('29C', 'Specialty Fruit Beer'), ('29D', 'Grape Ale'), ('30A', 'Spice, Herb, or Vegetable Beer'), ('30B', 'Autumn Seasonal Beer'), ('30C', 'Winter Seasonal Beer'), ('30D', 'Specialty Spice Beer'), ('31A', 'Alternative Grain Beer'), ('31B', 'Alternative Sugar Beer'), ('32A', 'Classic Style Smoked Beer'), ('32B', 'Specialty Smoked Beer'), ('34A', 'Commercial Specialty Beer'), ('34B', 'Mixed-Style Beer'), ('34C', 'Experimental Beer'), ('X1', 'Dorada Pampeana'), ('X2', 'IPA Argenta'), ('X3', 'Italian Grape Ale'), ('X4', 'Catharina Sour'), ('X5', 'New Zealand Pilsner')], default='1A', max_length=3),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-26 19:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0019_alter_recipe_bjcp_style_id'),
]
operations = [
migrations.AlterField(
model_name='recipe',
name='bjcp_style_id',
field=models.CharField(choices=[('1A', '1A: American Light Lager'), ('1B', '1B: American Lager'), ('1C', '1C: Cream Ale'), ('1D', '1D: American Wheat Beer'), ('2A', '2A: International Pale Lager'), ('2B', '2B: International Amber Lager'), ('2C', '2C: International Dark Lager'), ('3A', '3A: Czech Pale Lager'), ('3B', '3B: Czech Premium Pale Lager'), ('3C', '3C: Czech Amber Lager'), ('3D', '3D: Czech Dark Lager'), ('4A', '4A: Munich Helles'), ('4B', '4B: Festbier'), ('4C', '4C: Helles Bock'), ('5A', '5A: German Leichtbier'), ('5B', '5B: Kölsch'), ('5C', '5C: German Helles Exportbier'), ('5D', '5D: German Pils'), ('6A', '6A: Märzen'), ('6B', '6B: Rauchbier'), ('6C', '6C: Dunkles Bock'), ('7A', '7A: Vienna Lager'), ('7B', '7B: Altbier'), ('8A', '8A: Munich Dunkel'), ('8B', '8B: Schwarzbier'), ('9A', '9A: Doppelbock'), ('9B', '9B: Eisbock'), ('9C', '9C: Baltic Porter'), ('10A', '10A: Weissbier'), ('10B', '10B: Dunkles Weissbier'), ('10C', '10C: Weizenbock'), ('11A', '11A: Ordinary Bitter'), ('11B', '11B: Best Bitter'), ('11C', '11C: Strong Bitter'), ('12A', '12A: British Golden Ale'), ('12B', '12B: Australian Sparkling Ale'), ('12C', '12C: English IPA'), ('13A', '13A: Dark Mild'), ('13B', '13B: British Brown Ale'), ('13C', '13C: English Porter'), ('14A', '14A: Scottish Light'), ('14B', '14B: Scottish Heavy'), ('14C', '14C: Scottish Export'), ('15A', '15A: Irish Red Ale'), ('15B', '15B: Irish Stout'), ('15C', '15C: Irish Extra Stout'), ('16A', '16A: Sweet Stout'), ('16B', '16B: Oatmeal Stout'), ('16C', '16C: Tropical Stout'), ('16D', '16D: Foreign Extra Stout'), ('17A', '17A: British Strong Ale'), ('17B', '17B: Old Ale'), ('17C', '17C: Wee Heavy'), ('17D', '17D: English Barley Wine'), ('18A', '18A: Blonde Ale'), ('18B', '18B: American Pale Ale'), ('19A', '19A: American Amber Ale'), ('19B', '19B: California Common'), ('19C', '19C: American Brown Ale'), ('20A', '20A: American Porter'), ('20B', '20B: American Stout'), ('20C', '20C: Imperial Stout'), ('21A', '21A: American IPA'), ('21B', '21B: Specialty IPA'), ('21C', '21C: Hazy IPA'), ('22A', '22A: Double IPA'), ('22B', '22B: American Strong Ale'), ('22C', '22C: American Barleywine'), ('22D', '22D: Wheatwine'), ('23A', '23A: Berliner Weisse'), ('23B', '23B: Flanders Red Ale'), ('23C', '23C: Oud Bruin'), ('23D', '23D: Lambic'), ('23E', '23E: Gueuze'), ('23F', '23F: Fruit Lambic'), ('23G', '23G: Gose'), ('24A', '24A: Witbier'), ('24B', '24B: Belgian Pale Ale'), ('24C', '24C: Bière de Garde'), ('25A', '25A: Belgian Blond Ale'), ('25B', '25B: Saison'), ('25C', '25C: Belgian Golden Strong Ale'), ('26A', '26A: Belgian Single'), ('26B', '26B: Belgian Dubbel'), ('26C', '26C: Belgian Tripel'), ('26D', '26D: Belgian Dark Strong Ale'), ('28A', '28A: Brett Beer'), ('28B', '28B: Mixed-Fermentation Sour Beer'), ('28C', '28C: Wild Specialty Beer'), ('28D', '28D: Straight Sour Beer'), ('29A', '29A: Fruit Beer'), ('29B', '29B: Fruit and Spice Beer'), ('29C', '29C: Specialty Fruit Beer'), ('29D', '29D: Grape Ale'), ('30A', '30A: Spice, Herb, or Vegetable Beer'), ('30B', '30B: Autumn Seasonal Beer'), ('30C', '30C: Winter Seasonal Beer'), ('30D', '30D: Specialty Spice Beer'), ('31A', '31A: Alternative Grain Beer'), ('31B', '31B: Alternative Sugar Beer'), ('32A', '32A: Classic Style Smoked Beer'), ('32B', '32B: Specialty Smoked Beer'), ('34A', '34A: Commercial Specialty Beer'), ('34B', '34B: Mixed-Style Beer'), ('34C', '34C: Experimental Beer'), ('X1', 'X1: Dorada Pampeana'), ('X2', 'X2: IPA Argenta'), ('X3', 'X3: Italian Grape Ale'), ('X4', 'X4: Catharina Sour'), ('X5', 'X5: New Zealand Pilsner')], default='1A', max_length=3),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.6 on 2024-06-26 19:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('beer', '0020_alter_recipe_bjcp_style_id'),
]
operations = [
migrations.RemoveField(
model_name='recipe',
name='bjcp_category_id',
),
]

View File

@ -0,0 +1,72 @@
# Generated by Django 5.0.6 on 2024-06-27 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0021_remove_recipe_bjcp_category_id'),
]
operations = [
migrations.CreateModel(
name='BjcpStyle',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField(blank=True, null=True)),
('category', models.TextField(blank=True, null=True)),
('category_id', models.TextField(blank=True, null=True)),
('style_id', models.TextField(blank=True, null=True)),
('category_description', models.TextField(blank=True, null=True)),
('overall_impression', models.TextField(blank=True, null=True)),
('aroma', models.TextField(blank=True, null=True)),
('appearance', models.TextField(blank=True, null=True)),
('flavor', models.TextField(blank=True, null=True)),
('mouthfeel', models.TextField(blank=True, null=True)),
('comments', models.TextField(blank=True, null=True)),
('history', models.TextField(blank=True, null=True)),
('style_comparison', models.TextField(blank=True, null=True)),
('tags', models.TextField(blank=True, null=True)),
('original_gravity_minimum_unit', models.TextField(blank=True, null=True)),
('original_gravity_minimum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('original_gravity_maximum_unit', models.TextField(blank=True, null=True)),
('original_gravity_maximum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('international_bitterness_units_minimum_unit', models.TextField(blank=True, null=True)),
('international_bitterness_units_minimum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('international_bitterness_units_maximum_unit', models.TextField(blank=True, null=True)),
('international_bitterness_units_maximum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('final_gravity_minimum_unit', models.TextField(blank=True, null=True)),
('final_gravity_minimum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('final_gravity_maximum_unit', models.TextField(blank=True, null=True)),
('final_gravity_maximum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('alcohol_by_volume_minimum_unit', models.TextField(blank=True, null=True)),
('alcohol_by_volume_minimum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('alcohol_by_volume_maximum_unit', models.TextField(blank=True, null=True)),
('alcohol_by_volume_maximum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('color_minimum_unit', models.TextField(blank=True, null=True)),
('color_minimum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('color_maximum_unit', models.TextField(blank=True, null=True)),
('color_maximum_value', models.DecimalField(blank=True, decimal_places=5, max_digits=10, null=True)),
('ingredients', models.TextField(blank=True, null=True)),
('examples', models.TextField(blank=True, null=True)),
('style_guide', models.TextField(blank=True, null=True)),
('type', models.TextField(blank=True, null=True)),
('entry_instructions', models.TextField(blank=True, null=True)),
('notes', models.TextField(blank=True, null=True)),
('currently_defined_types', models.TextField(blank=True, null=True)),
('strength_classifications', models.TextField(blank=True, null=True)),
('vital_statistics', models.TextField(blank=True, null=True)),
('profile', models.TextField(blank=True, null=True)),
('comparison', models.TextField(blank=True, null=True)),
],
options={
'db_table': 'beer_bjcp',
},
),
migrations.AlterField(
model_name='recipe',
name='bjcp_style_id',
field=models.CharField(choices=[('1A', '1A: American Light Lager'), ('1B', '1B: American Lager'), ('1C', '1C: Cream Ale'), ('1D', '1D: American Wheat Beer'), ('2A', '2A: International Pale Lager'), ('2B', '2B: International Amber Lager'), ('2C', '2C: International Dark Lager'), ('3A', '3A: Czech Pale Lager'), ('3B', '3B: Czech Premium Pale Lager'), ('3C', '3C: Czech Amber Lager'), ('3D', '3D: Czech Dark Lager'), ('4A', '4A: Munich Helles'), ('4B', '4B: Festbier'), ('4C', '4C: Helles Bock'), ('5A', '5A: German Leichtbier'), ('5B', '5B: Kölsch'), ('5C', '5C: German Helles Exportbier'), ('5D', '5D: German Pils'), ('6A', '6A: Märzen'), ('6B', '6B: Rauchbier'), ('6C', '6C: Dunkles Bock'), ('7A', '7A: Vienna Lager'), ('7B', '7B: Altbier'), ('8A', '8A: Munich Dunkel'), ('8B', '8B: Schwarzbier'), ('9A', '9A: Doppelbock'), ('9B', '9B: Eisbock'), ('9C', '9C: Baltic Porter'), ('10A', '10A: Weissbier'), ('10B', '10B: Dunkles Weissbier'), ('10C', '10C: Weizenbock'), ('11A', '11A: Ordinary Bitter'), ('11B', '11B: Best Bitter'), ('11C', '11C: Strong Bitter'), ('12A', '12A: British Golden Ale'), ('12B', '12B: Australian Sparkling Ale'), ('12C', '12C: English IPA'), ('13A', '13A: Dark Mild'), ('13B', '13B: British Brown Ale'), ('13C', '13C: English Porter'), ('14A', '14A: Scottish Light'), ('14B', '14B: Scottish Heavy'), ('14C', '14C: Scottish Export'), ('15A', '15A: Irish Red Ale'), ('15B', '15B: Irish Stout'), ('15C', '15C: Irish Extra Stout'), ('16A', '16A: Sweet Stout'), ('16B', '16B: Oatmeal Stout'), ('16C', '16C: Tropical Stout'), ('16D', '16D: Foreign Extra Stout'), ('17A', '17A: British Strong Ale'), ('17B', '17B: Old Ale'), ('17C', '17C: Wee Heavy'), ('17D', '17D: English Barley Wine'), ('18A', '18A: Blonde Ale'), ('18B', '18B: American Pale Ale'), ('19A', '19A: American Amber Ale'), ('19B', '19B: California Common'), ('19C', '19C: American Brown Ale'), ('20A', '20A: American Porter'), ('20B', '20B: American Stout'), ('20C', '20C: Imperial Stout'), ('21A', '21A: American IPA'), ('21B', '21B: Specialty IPA'), ('21C', '21C: Hazy IPA'), ('22A', '22A: Double IPA'), ('22B', '22B: American Strong Ale'), ('22C', '22C: American Barleywine'), ('22D', '22D: Wheatwine'), ('23A', '23A: Berliner Weisse'), ('23B', '23B: Flanders Red Ale'), ('23C', '23C: Oud Bruin'), ('23D', '23D: Lambic'), ('23E', '23E: Gueuze'), ('23F', '23F: Fruit Lambic'), ('23G', '23G: Gose'), ('24A', '24A: Witbier'), ('24B', '24B: Belgian Pale Ale'), ('24C', '24C: Bière de Garde'), ('25A', '25A: Belgian Blond Ale'), ('25B', '25B: Saison'), ('25C', '25C: Belgian Golden Strong Ale'), ('26A', '26A: Belgian Single'), ('26B', '26B: Belgian Dubbel'), ('26C', '26C: Belgian Tripel'), ('26D', '26D: Belgian Dark Strong Ale'), ('28A', '28A: Brett Beer'), ('28B', '28B: Mixed-Fermentation Sour Beer'), ('28C', '28C: Wild Specialty Beer'), ('28D', '28D: Straight Sour Beer'), ('29A', '29A: Fruit Beer'), ('29B', '29B: Fruit and Spice Beer'), ('29C', '29C: Specialty Fruit Beer'), ('29D', '29D: Grape Ale'), ('30A', '30A: Spice, Herb, or Vegetable Beer'), ('30B', '30B: Autumn Seasonal Beer'), ('30C', '30C: Winter Seasonal Beer'), ('30D', '30D: Specialty Spice Beer'), ('31A', '31A: Alternative Grain Beer'), ('31B', '31B: Alternative Sugar Beer'), ('32A', '32A: Classic Style Smoked Beer'), ('32B', '32B: Specialty Smoked Beer'), ('34A', '34A: Commercial Specialty Beer'), ('34B', '34B: Mixed-Style Beer'), ('34C', '34C: Experimental Beer'), ('X5', 'X5: New Zealand Pilsner')], default='1A', max_length=3),
),
]

View File

@ -0,0 +1,88 @@
# Generated by Django 5.0.6 on 2024-06-27 13:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0022_bjcpstyle_alter_recipe_bjcp_style_id'),
]
operations = [
migrations.AlterField(
model_name='bjcpstyle',
name='alcohol_by_volume_maximum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='alcohol_by_volume_minimum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='category',
field=models.TextField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='category_id',
field=models.TextField(blank=True, max_length=5, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='color_maximum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='color_minimum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='final_gravity_maximum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='final_gravity_minimum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='international_bitterness_units_maximum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='international_bitterness_units_minimum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='name',
field=models.TextField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='original_gravity_maximum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='original_gravity_minimum_unit',
field=models.TextField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='style_guide',
field=models.TextField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='style_id',
field=models.TextField(blank=True, max_length=5, null=True),
),
]

View File

@ -0,0 +1,88 @@
# Generated by Django 5.0.6 on 2024-06-27 13:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0023_alter_bjcpstyle_alcohol_by_volume_maximum_unit_and_more'),
]
operations = [
migrations.AlterField(
model_name='bjcpstyle',
name='alcohol_by_volume_maximum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='alcohol_by_volume_minimum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='category',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='category_id',
field=models.CharField(blank=True, max_length=5, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='color_maximum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='color_minimum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='final_gravity_maximum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='final_gravity_minimum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='international_bitterness_units_maximum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='international_bitterness_units_minimum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='name',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='original_gravity_maximum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='original_gravity_minimum_unit',
field=models.CharField(blank=True, max_length=10, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='style_guide',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='bjcpstyle',
name='style_id',
field=models.CharField(blank=True, max_length=5, null=True),
),
]

View File

@ -1,18 +1,25 @@
from django.db import models
from django.db.models import Sum, Q
from django.contrib.staticfiles import finders
from django.utils import timezone
from django_cryptography.fields import encrypt
from django.core.validators import MinValueValidator
from config.extras import BREWFATHER_APP_ROOT
from beer.extras import sg_plato, plato_sg, kg_extract, convert
from django.conf import settings
import json
import logging
logger = logging.getLogger('django')
class CustomModel(models.Model):
""" Custom model class with default fields to use. """
created_date = models.DateTimeField(default=timezone.now)
created_date = models.DateTimeField(
default=timezone.now,
editable=False
)
class Meta:
abstract = True
@ -33,7 +40,78 @@ class Batch(CustomModel):
brewfather_num = models.IntegerField(default=1)
brewfather_name = models.CharField(max_length=500, default='name')
recipe = models.OneToOneField(
'BatchRecipe', on_delete=models.CASCADE, default=1)
'Recipe', on_delete=models.CASCADE, default=1)
# -------------------------------------------------------------------------
# Brewday Measurements
# -------------------------------------------------------------------------
# Mash
first_runnings = models.DecimalField(
max_digits=8, decimal_places=4, null=True, blank=True)
mash_ph = models.DecimalField(
max_digits=4, decimal_places=3, null=True, blank=True)
# Boil
pre_boil_vol = models.DecimalField(
max_digits=8, decimal_places=4, null=True, blank=True)
pre_boil_sg = models.DecimalField(
max_digits=5, decimal_places=4, null=True, blank=True)
post_boil_vol = models.DecimalField(
max_digits=8, decimal_places=4, null=True, blank=True)
post_boil_sg = models.DecimalField(
max_digits=5, decimal_places=4, null=True, blank=True)
# Ferment
fermenter_topup_vol = models.DecimalField(
max_digits=8, decimal_places=4, null=True, blank=True)
fermenter_vol = models.DecimalField(
max_digits=8, decimal_places=4, null=True, blank=True)
original_sg = models.DecimalField(
max_digits=5, decimal_places=4, null=True, blank=True)
final_sg = models.DecimalField(
max_digits=5, decimal_places=4, null=True, blank=True)
# Properties Needed:
# braukaiser.com/wiki/index.php/Troubleshooting_Brewhouse_Efficiency
# - Mash Efficiency
# - ABV
# - Attenuation
# - Actual Trub/Chiller Loss
# -------------------------------------------------------------------------
# Batch Stats
# -------------------------------------------------------------------------
@property
def brewhouse_efficiency(self):
try:
return round(self.boil_extract_kg/self.recipe.total_extract_kg, 4)
except ZeroDivisionError:
return 0
@property
def boil_extract_kg(self):
return kg_extract(float(self.post_boil_vol)*.96, self.post_boil_sg)
@property
def conversion_efficiency(self):
""" Calculate conversion efficiency of mash."""
if (self.first_runnings is None
or self.recipe.fermentable_weight_kg == 0):
return '-'
return round((sg_plato(self.first_runnings)/self.recipe.fw_max)
* (100-self.recipe.fw_max)
/ (100-sg_plato(self.first_runnings)), 4)
@property
def boil_off_calcualted(self):
return float(self.pre_boil_vol - self.post_boil_vol) * .96
@property
def trub_loss_calculated(self):
transfered_volume = self.fermenter_vol - self.fermenter_topup_vol
return self.post_boil_vol-transfered_volume
@property
def brewfather_url(self):
@ -77,7 +155,10 @@ class Supplier(CustomModel):
class CustomIngredient(CustomModel):
""" Custom model class with default fields to use. """
created_date = models.DateTimeField(default=timezone.now)
created_date = models.DateTimeField(
default=timezone.now,
editable=False
)
name = models.CharField(max_length=50)
units = models.ForeignKey(Unit, on_delete=models.PROTECT)
unit_cost = models.DecimalField(
@ -93,7 +174,7 @@ class CustomIngredient(CustomModel):
abstract = True
class BatchRecipe(CustomModel):
class Recipe(CustomModel):
""" Recipe to be stored with a batch."""
name = models.CharField(max_length=50)
batch_recipe = models.BooleanField(null=True)
@ -106,25 +187,80 @@ class BatchRecipe(CustomModel):
max_digits=6, decimal_places=2, default=75)
batch_size = models.DecimalField(
max_digits=6, decimal_places=2, default=11)
fermentables = models.ManyToManyField(
'Fermentable', through='RecipeFermentable')
with open(finders.find('bjcp/2021.json'), encoding='utf-8', errors="ignore") as bjcp_file:
bjcp = json.load(bjcp_file)
styles = bjcp['styles']
style_ids = dict([
(x['style_id'], '{}: {}'.format(x['style_id'], x['name']))
for x in styles
])
bjcp_style_id = models.CharField(
max_length=3, choices=style_ids, default='1A')
class Meta:
verbose_name = 'Recipe'
verbose_name_plural = 'Recipes'
@property
def fermentables(self):
return [x for x in list(self.recipefermentable_set.all())]
def batch_size_display(self):
return convert(self.batch_size, 'l', 'gal')
@property
def fermentable_weight(self):
return sum([x.quantity for x in self.fermentables])
def fw_max(self):
potential = 0
weight = 0
for ferm in self.recipefermentable_set.all():
potential += ferm.fermentable.extract_percent*float(ferm.quantity)
weight += float(ferm.quantity)
e_grain = potential / weight
ratio = float(self.equipment.mash_ratio)
return 100*e_grain / (ratio+e_grain)
@property
def hops(self):
return [x for x in list(self.recipehop_set.all())]
def total_fermentable_display(self):
return convert(self.fermentable_weight_kg, 'kg', 'lb')
@property
def fermentable_weight_kg(self):
"""Weight of all fermentables attached to recipe."""
aggregate = self.recipefermentable_set.all().aggregate(Sum('quantity'))
if aggregate['quantity__sum']:
return aggregate['quantity__sum']
else:
return 0
@property
def total_extract_kg(self):
extract = 0
for f in self.recipefermentable_set.all():
extract += f.extract_weight_kg
return extract
@property
def total_hop_display(self):
return convert(self.hop_weight_g, 'g', 'oz')
@property
def hop_weight_g(self):
"""Weight of all fermentables attached to recipe."""
aggregate = self.recipehop_set.all().aggregate(Sum('quantity'))
if aggregate['quantity__sum']:
return aggregate['quantity__sum']
else:
return 0
@property
def final_volume(self):
"""Return final volume (after boil)."""
return (float(self.batch_size)
+ self.hop_water_loss
+ self.net_kettle_deadspace
@ -132,44 +268,50 @@ class BatchRecipe(CustomModel):
@property
def sugar_yield(self):
"""Return point yield of all non-mashed ingredients."""
ferms = self.recipefermentable_set.all().select_related('fermentable')
sugars = ferms.filter(fermentable__fermentable_type=3)
ferm_yield = 0
sugars = (x for x in self.fermentables
if x.fermentable.fermentable_type == 3) # Is sugar
for f in sugars:
ferm_yield += f.quantity * (f.fermentable.potential - 1) * 1000
ferm_yield += f.extract_weight_kg
return float(ferm_yield)
@property
def mash_yield(self):
"""Return point yield of all mashed ingredients."""
ferms = self.recipefermentable_set.all().select_related('fermentable')
mashed = ferms.filter(~Q(fermentable__fermentable_type=3))
mash_yield = 0
mashed = (x for x in self.fermentables
if x.fermentable.fermentable_type != 3) # Is not sugar
for f in mashed:
mash_yield += (f.quantity * (self.efficiency / 100)
* (f.fermentable.potential - 1) * 1000)
mash_yield += f.extract_weight_kg * float(self.efficiency/100)
return float(mash_yield)
@property
def original_sg(self):
total_yield = self.sugar_yield + self.mash_yield
return round(1 + total_yield / self.final_volume / 1000, 3)
"""Return original gravity."""
total_extract = self.sugar_yield + self.mash_yield
plato = 100 * total_extract / (self.final_volume + total_extract)
return round(plato_sg(plato), 3)
@property
def pre_boil_sg(self):
total_yield = self.sugar_yield + self.mash_yield
return round(1 + total_yield / (self.final_volume
+ self.boil_off_gph) / 1000, 3)
"""Return pre-boil gravity."""
total_extract = self.sugar_yield + self.mash_yield
total_water = self.final_volume+self.boil_off_gph
plato = 100 * total_extract / (total_water + total_extract)
return round(plato_sg(plato), 3)
@property
def hop_water_loss(self):
hop_absorption = .025 # gallons per ounce
return sum([float(x.quantity) * hop_absorption for x in self.hops])
return float(sum(
x.quantity*x.trub_volume for x in self.recipehop_set.all()
))
@property
def net_kettle_deadspace(self):
@ -202,13 +344,18 @@ class BatchRecipe(CustomModel):
return 0
return float(self.equipment.kettle_boil_rate)
@property
def ibu(self): # TODO: Multiple IBU formulas
return self.ibu_tinseth
@property
def ibu_tinseth(self):
return sum([x.ibu_tinseth for x in self.hops])
return sum(x.ibu_tinseth for x in self.recipehop_set.all())
@property
def srm(self):
color_total = sum([x.srm for x in self.fermentables])
color_total = sum(x.lovibond_contributed
for x in self.recipefermentable_set.all())
return 1.4922*(color_total**0.6859)
@property
@ -262,7 +409,8 @@ class Fermentable(CustomIngredient):
fermentable_type = models.IntegerField(choices=types, default=1)
diastatic_power = models.DecimalField(
max_digits=6, decimal_places=4, null=True, blank=True)
potential = models.DecimalField(max_digits=6, decimal_places=4)
potential = models.DecimalField(
max_digits=6, decimal_places=4, default=0.80)
protein = models.DecimalField(
max_digits=6, decimal_places=4, null=True, blank=True)
attenuation = models.DecimalField(
@ -272,28 +420,47 @@ class Fermentable(CustomIngredient):
max_in_batch = models.DecimalField(
max_digits=6, decimal_places=4, null=True, blank=True)
moisture = models.DecimalField(
max_digits=6, decimal_places=4, null=True, blank=True)
max_digits=6, decimal_places=4, null=True, blank=True, default=0.04)
non_fermentable = models.BooleanField(null=True, blank=True)
ibu_per_unit = models.DecimalField(
max_digits=6, decimal_places=4, default=0)
@property
def extract_percent(self):
return (((float(self.potential)-1)*1000/46.17)
* (1-float(self.moisture)/100))
def __str__(self):
return self.name
class RecipeFermentable(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
@property
def percent(self):
return float(100 * self.quantity / self.recipe.fermentable_weight)
# Properties Needed:
# - The weight of extract in the grist
# extract in grist in kg = weight of grist in kg * extract potential
@property
def srm(self):
def quantity_display(self):
return convert(self.quantity, 'kg', 'lb')
@property
def extract_weight_kg(self):
return float(self.quantity) * self.fermentable.extract_percent
@property
def percent(self):
return float(100 * self.quantity / self.recipe.fermentable_weight_kg)
@property
def lovibond_contributed(self):
srm_calc = (float(self.fermentable.lovibond)
* float(self.quantity) / self.recipe.final_volume)
* convert(self.quantity, 'kg', 'lb')
/ convert(self.recipe.final_volume, 'l', 'gal'))
return round(srm_calc, 1)
@ -328,12 +495,17 @@ class RecipeHop(CustomModel):
4: 'Mash',
5: 'First Wort'
}
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
hop = models.ForeignKey(Hop, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
time = models.IntegerField(default=60, validators=[MinValueValidator(0)])
use = models.IntegerField(choices=uses, default=1)
@property
def quantity_display(self):
"""Convert grams to ounces."""
return convert(self.quantity, 'g', 'oz')
@property
def ibu_tinseth(self):
type_bonus = {
@ -343,22 +515,32 @@ class RecipeHop(CustomModel):
4: 1.4, # CO2 Extract
}
ibu = 0
hop_bonus = type_bonus[self.hop.hop_type]
average_wort_sg = (((self.recipe.pre_boil_sg - 1)
+ (self.recipe.original_sg - 1)) / 2)
average_wort_sg = (((self.recipe.pre_boil_sg-1)
+ (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))
conc = (float(self.hop.alpha/100)
* convert(self.quantity, 'g', 'oz')
* 7490/convert(self.recipe.final_volume, 'l', 'gal'))
util = (hop_bonus*1.65*0.000125**average_wort_sg
* ((1-2.71828182845904**(-0.04*self.time)) / 4.15))
ibu = conc * util
else:
ibu = 0
return float(ibu)
@property
def trub_volume(self):
if self.hop.hop_type == 1:
return self.recipe.equipment.pellet_hop_trub
elif self.hop.hop_type in [2, 3]:
return self.recipe.equipment.leaf_hop_trub
else:
return 0
class Misc(CustomIngredient):
uses = {
@ -390,7 +572,7 @@ class Misc(CustomIngredient):
class RecipeMisc(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
misc = models.ForeignKey(Misc, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
@ -399,7 +581,7 @@ class RecipeMisc(CustomModel):
class RecipeYeast(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
yeast = models.ForeignKey('yeast.Strain', on_delete=models.CASCADE)
@ -436,29 +618,96 @@ class EquipmentProfile(CustomModel):
# Water managment stuff
hlt_deadspace = models.DecimalField(
max_digits=6, decimal_places=2, default=0.25)
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
mt_deadspace = models.DecimalField(
max_digits=6, decimal_places=2, default=0.25)
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
mt_capacity = models.DecimalField(
max_digits=6, decimal_places=2, default=10)
max_digits=6, decimal_places=2, default=10, help_text='liters')
grain_absorption = models.DecimalField(
max_digits=6, decimal_places=2, default=0.12) # gal/lb
max_digits=6, decimal_places=2,
default=0.12, help_text='liters/kilogram')
kettle_deadspace = models.DecimalField(
max_digits=6, decimal_places=2, default=0.25)
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
kettle_plumbing_loss = models.DecimalField(
max_digits=6, decimal_places=2, default=0.25)
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
kettle_boil_rate = models.DecimalField(
max_digits=6, decimal_places=2, default=0.5) # gal/hr
max_digits=6, decimal_places=2,
default=0.5, help_text='liters/hr')
batch_volume = models.DecimalField(
max_digits=6, decimal_places=2, default=5.5)
max_digits=6, decimal_places=2, default=5.5, help_text='liters')
leaf_hop_trub = models.DecimalField(
max_digits=6, decimal_places=4, default=0.0625)
max_digits=12, decimal_places=10,
default=0.0083454045, help_text='liters/gram')
pellet_hop_trub = models.DecimalField(
max_digits=6, decimal_places=4, default=0.025)
max_digits=12, decimal_places=10,
default=0.0033381620, help_text='liters/gram')
hops_remain_kettle = models.BooleanField(default=True)
mash_ratio = models.DecimalField(
max_digits=6, decimal_places=2,
default=2.6, help_text='liters/kilogram')
# Thermal Properties
mt_initial_hear = models.DecimalField(
max_digits=6, decimal_places=4, default=0.74)
mt_heat_loss_hour = models.DecimalField(
max_digits=6, decimal_places=4, default=2.0)
class Meta:
db_table_comment = 'Volumes in liters and weights in kg.'
class BjcpStyle(models.Model):
name = models.CharField(max_length=50, blank=True, null=True)
category = models.CharField(max_length=50, blank=True, null=True)
category_id = models.CharField(max_length=5, blank=True, null=True)
style_id = models.CharField(max_length=5, blank=True, null=True)
category_description = models.TextField(blank=True, null=True)
overall_impression = models.TextField(blank=True, null=True)
aroma = models.TextField(blank=True, null=True)
appearance = models.TextField(blank=True, null=True)
flavor = models.TextField(blank=True, null=True)
mouthfeel = models.TextField(blank=True, null=True)
comments = models.TextField(blank=True, null=True)
history = models.TextField(blank=True, null=True)
style_comparison = models.TextField(blank=True, null=True)
tags = models.TextField(blank=True, null=True)
original_gravity_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
original_gravity_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
original_gravity_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
original_gravity_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
international_bitterness_units_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
international_bitterness_units_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
international_bitterness_units_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
international_bitterness_units_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
final_gravity_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
final_gravity_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
final_gravity_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
final_gravity_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
alcohol_by_volume_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
alcohol_by_volume_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
alcohol_by_volume_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
alcohol_by_volume_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
color_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
color_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
color_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
color_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
ingredients = models.TextField(blank=True, null=True)
examples = models.TextField(blank=True, null=True)
style_guide = models.CharField(max_length=20, blank=True, null=True)
type = models.TextField(blank=True, null=True)
entry_instructions = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
currently_defined_types = models.TextField(blank=True, null=True)
strength_classifications = models.TextField(blank=True, null=True)
vital_statistics = models.TextField(blank=True, null=True)
profile = models.TextField(blank=True, null=True)
comparison = models.TextField(blank=True, null=True)
def __str__(self):
return '{} {}: {}'.format(
self.category,
self.style_id,
self.name
)
class Meta:
db_table = 'beer_bjcp'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
{% extends "no_jumbo_base.html" %}
{% load mathfilters %}
{% block title %}#{{ batch.brewfather_num }} {{ batch.brewfather_name }}{% endblock %}
{% block style %}
.table td.fit,
.table th.fit {
white-space: nowrap;
width: 1%;
}
{% endblock %}
{% block content %}
<!-- Information Header -->
<div class="container">
<h1>{{ batch.brewfather_name }}</h1>
</div> <!-- /container -->
<!-- End Information Header -->
<!-- Main Data Container -->
<div class="container mt-4 mb-4">
<div class="row">
<!-- Tabbed Pages -->
<!-- Nav tabs -->
<ul class="nav nav-tabs nav-fill" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#planning">Planning</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#brewing">Brewing</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#fermenting">Fermenting</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#complete">Complete</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div id="planning" class="container tab-pane active"><br>
{% include "beer/planning.html" %}
</div>
<div id="brewing" class="container tab-pane fade"><br>
</div>
<div id="fermenting" class="container tab-pane fade"><br>
</div>
<div id="complete" class="container tab-pane fade"><br>
</div>
</div>
<!-- End Tabbed Pages -->
</div>
</div>
<!-- End Main Data Container -->
{% endblock %}

View File

@ -0,0 +1,58 @@
<div class="row">
<div class="col-lg-6">
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Recipe Ingredients
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
{% for f in batch.recipe.recipefermentable_set.all %}
<tr>
<td>{{ f.quantity|floatformat:2 }} lb</td>
<td>{{ f.fermentable.name }}<br>
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.srm }} SRM</span>
</td>
<td>{{ f.percent|floatformat:1 }} %</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-lg-6">
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Batch Recipe
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
<tr onclick="window.location='recipe/{{ batch.recipe.id }}';">
<td><b>{{ batch.recipe.name }}</b><br>
<span class="text-muted"><b>OG:</b> {{ batch.recipe.original_sg }} <b>IBU:</b> {{ batch.recipe.ibu|floatformat:0 }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Yeast
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
{% for y in batch.recipe.recipeyeast_set.all %}
<tr>
<td>{{ y.yeast.manufacturer.name }} {{ y.yeast.name }}<br>
<span class="text-muted">{{ y.yeast.long_name }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -10,6 +10,11 @@ input, label {
.container-beer {
background: {{ recipe.srm_hex }};
}
p.smaller {
font-size: .8em;
}
{% endblock %}
{% block title %}Recipes{% endblock %}
@ -19,7 +24,7 @@ input, label {
{% 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 class="col-lg d-flex justify-content-between">
<div>
<div class="container-beer"><img src="{% static "beer_back.png" %}" alt="" class="img-responsive d-none d-sm-block"></div>
</div>
@ -32,23 +37,27 @@ input, label {
<dd>All Grain</dd>
</dl>
</div>
<div class="col-lg-4" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div class="text-truncate">Equipment: {{ recipe.equipment.name }}</div>
<div class="col-lg-4">
<div class="d-flex justify-content-between bg-dark text-white">
<div class="text-truncate ms-1 my-1">{{ recipe.equipment.name }}</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-scale-unbalanced-flip"></i></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-arrow-right-arrow-left"></i></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-pen-to-square"></i></button>
</div>
</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 }} %
-->
<dl class="row">
<dt>Batch Size</dt>
<dd>{{ recipe.batch_size }} gal</dd>
<dt>Actual Volume</dt>
<dd>{{ recipe.final_volume|floatformat:2 }} gal</dd>
<dd>{{ recipe.batch_size_display|floatformat:2 }} gal</dd>
<dt>Mash Efficiency</dt>
<dd>{{ recipe.efficiency|floatformat:2 }} %</dd>
</dl>
</div>
<div class="col-xl-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div class="col-xl-3">
Style Data
</div>
</div>
@ -64,25 +73,38 @@ input, label {
<!-- 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 ">
<div class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Fermentables ({{ recipe.total_fermentable_display|floatformat:2 }} lbs)</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><b>%</b></button>
<button type="button" class="btn btn-dark btn-sm"><b>OG</b></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fa fa-plus"></i><b>ADD</b></button>
</div>
</div>
<table class="table table-sm table-hover">
<caption>
<p class="text-end smaller">
Pre-Boil Gravity: <b>{{ recipe.pre_boil_sg|floatformat:3 }}</b><br>
Original Gravity: <b>{{ recipe.original_sg|floatformat:3 }}</b><br>
Color: <b>{{ recipe.srm|floatformat:0 }} SRM</b>
</p>
</caption>
<tbody>
{% for f in recipe.recipefermentable_set.all %}
<tr>
<td>{{ f.quantity|floatformat:2 }} lb</td>
<tr onclick="window.location='{% url 'beer:update_fermentable' f.fermentable.id %}';">
<td>{{ f.quantity_display|floatformat:2 }} lb</td>
<td>{{ f.fermentable.name }}<br>
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.srm }} SRM</span>
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.fermentable.lovibond|floatformat:0 }} &deg;L</span>
</td>
<td>{{ f.percent|floatformat:1 }} %</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Pre-Boil Gravity: <b>{{ recipe.pre_boil_sg }}</b><br>
Original Gravity: <b>{{ recipe.original_sg }}</b><br>
Color: <b>{{ recipe.srm|floatformat:0 }} SRM</b>
</p>
</div>
</div>
@ -90,12 +112,25 @@ input, label {
<!-- 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 ">
<div class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Hops ({{ recipe.total_hop_display|floatformat:2 }} oz)</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><b>IBU</b></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fa fa-plus"></i><b>ADD</b></button>
</div>
</div>
<table class="table table-sm table-hover">
<caption>
<p class="text-end smaller">
Total IBU: <b>{{ recipe.ibu_tinseth|floatformat:1 }}</b><br>
BU/GU: <b>{{ recipe.bu_gu|floatformat:2 }}</b><br>
RBR: <b>{{ recipe.rbr|floatformat:2 }}</b>
</p>
</caption>
<tbody>
{% for h in recipe.recipehop_set.all %}
<tr>
<td>{{ h.quantity|floatformat:2 }} oz</td>
<tr onclick="window.location='{% url 'beer:update_hop' h.hop.id %}';">
<td>{{ h.quantity_display|floatformat:2 }} oz</td>
<td>{{ h.hop.name }} {{ h.hop.alpha|floatformat:1 }} %<br>
<span class="text-muted">{{ h.hop.get_hop_type_display }} {{ h.ibu_tinseth|floatformat:1 }} IBU</span>
</td>
@ -103,11 +138,7 @@ input, label {
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Total IBU: <b>{{ recipe.ibu_tinseth|floatformat:1 }}</b><br>
BU/GU: <b>{{ recipe.bu_gu|floatformat:2 }}</b><br>
RBR: <b>{{ recipe.rbr|floatformat:2 }}</b>
</p>
</div>
</div>
@ -118,7 +149,12 @@ input, label {
<!-- Misc -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Misc.</div>
<div class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Misc.</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><i class="fa fa-plus"></i><b>ADD</b></button>
</div>
</div>
<table class="table table-sm">
<tbody>
{% for m in recipe.recipemisc_set.all %}
@ -131,7 +167,12 @@ input, label {
<!-- Yeast -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Yeast</div>
<div class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Yeast</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><i class="fa fa-plus"></i><b>ADD</b></button>
</div>
</div>
<table class="table table-sm">
<tbody>
{% for y in recipe.recipeyeast_set.all %}
@ -149,7 +190,13 @@ input, label {
<!-- Mash -->
<div class="col-md">
<div class="container-fluid">
<div class="container-fluid bg-dark text-white">Mash Profile {{ recipe.mash.name }}</div>
<div class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Mash Profile: {{ recipe.mash.name }}</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-arrow-right-arrow-left"></i></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-pen-to-square"></i></button>
</div>
</div>
<table class="table table-sm ">
<tbody>
{% for step in recipe.mash.mashstep_set.all %}
@ -165,14 +212,13 @@ input, label {
<!-- 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 class="d-flex justify-content-between bg-dark text-white">
<div class="ms-2 my-1">Fermentation Profile</div>
<div>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-arrow-right-arrow-left"></i></button>
<button type="button" class="btn btn-dark btn-sm"><i class="fas fa-pen-to-square"></i></button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load mathfilters %}
{% load funcs %}
{% load static %}
{% block title %}Recipes{% endblock %}
{% block jumbotron %}{{ data.name }}{% endblock %}
{% block jumbotronsub %}
{{ data.potential }}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load mathfilters %}
{% load funcs %}
{% load static %}
{% block title %}Recipes{% endblock %}
{% block jumbotron %}{{ data.name }}{% endblock %}
{% block jumbotronsub %}
{{ data.alpha }} %
{% endblock %}

View File

@ -1,10 +1,15 @@
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from .views import home, view_recipe
from .views import home, view_recipe, view_batch, update_ferm, update_hop
urlpatterns = [
path('', home, name='home'),
path('recipes/<int:recipe_id>/', view_recipe, name='recipe'),
path('batches/<int:batch_id>/recipe/<int:recipe_id>', view_recipe),
path('batches/<int:batch_id>/', view_batch, name='batch'),
path('ingredients/fermentables/<int:ferm_id>',
update_ferm, name='update_fermentable'),
path('ingredients/hops/<int:hop_id>', update_hop, name='update_hop'),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -1,6 +1,6 @@
from django.shortcuts import render, get_object_or_404
from .models import UserProfile, BatchRecipe, Batch
from .models import UserProfile, Recipe, Batch, Fermentable, Hop
from .extras import get_batches
import json
@ -20,7 +20,7 @@ def home(request):
if Batch.objects.filter(brewfather_id=batch['_id']).first() is None:
recipe_name = batch['recipe']['name']
recipe_obj = BatchRecipe(
recipe_obj = Recipe(
name=recipe_name,
batch_recipe=True, recipe_json=json.dumps(batch['recipe'])
)
@ -36,18 +36,39 @@ def home(request):
batch_obj.save()
context = {
'recipes': BatchRecipe.objects.all(),
'recipes': Recipe.objects.all(),
}
return render(request, 'beer/home.html', context)
def view_recipe(request, recipe_id):
recipe = get_object_or_404(BatchRecipe, pk=recipe_id)
def view_recipe(request, recipe_id, batch_id=None):
context = {
'recipe': recipe,
'fermentable_weight': sum([x.quantity for x in recipe.fermentables]),
'hop_weight': sum([x.quantity for x in recipe.hops]),
'recipe': get_object_or_404(Recipe, pk=recipe_id),
}
return render(request, 'beer/recipe.html', context)
def view_batch(request, batch_id):
context = {
'batch': get_object_or_404(Batch, pk=batch_id)
}
return render(request, 'beer/batch.html', context)
def update_ferm(request, ferm_id):
fermentable = get_object_or_404(Fermentable, pk=ferm_id)
context = {
'data': fermentable,
}
return render(request, 'beer/update_fermentable.html', context)
def update_hop(request, hop_id):
hop = get_object_or_404(Hop, pk=hop_id)
context = {
'data': hop,
}
return render(request, 'beer/update_hop.html', context)

View File

@ -37,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.flatpages',
'fontawesomefree',
'mathfilters',
'yeast.apps.YeastLabConfig',
'beer.apps.BeerConfig',

12344
fixtures/bjcp.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ django-cryptography-django5==2.2
django-mathfilters==1.0.0
docutils==0.21.2
environs==11.0.0
fontawesomefree==6.5.1
gunicorn==22.0.0
pip-chill==1.0.3
psycopg2-binary==2.9.9

8
run.sh
View File

@ -1,5 +1,9 @@
#! /usr/bin/env bash
source .env/bin/activate
pip install --upgrade -r requirements.txt
source .env/bin/activate || source .env/Scripts/activate
find -name "*.py" -not -name "manage.py" -not -path "./.env/*" -not -path "*/migrations/*" -exec python -m flake8 {} \;
pip install --upgrade -r requirements.txt > /dev/null
python manage.py makemigrations
python manage.py migrate
python manage.py runserver 0.0.0.0:9595

View File

@ -6,7 +6,9 @@
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{% load static %}
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<!-- JS, Popper.js, and jQuery -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" -->
<!-- integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" -->
@ -15,14 +17,14 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
{% block script %}{% endblock %}
<style>
{% block style %}{% endblock %}
body {
margin-top: 10px;
}
label {
margin-right: 10px;
}
<style>
{% block style %}{% endblock %}
body {
margin-top: 10px;
}
label {
margin-right: 10px;
}
.table-borderless > tbody > tr > td,
.table-borderless > tbody > tr > th,
.table-borderless > tfoot > tr > td,
@ -32,14 +34,14 @@
border: none;
background-color: rgba(0,0,0, 0.0) !important;
}
</style>
</style>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Damn Yankee Brewing</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@ -85,9 +87,9 @@
</div>
</div>
</nav>
</div>
</div>
<main role="main">
<main role="main">
<div class="container my-5">
<div class="p-5 text-left bg-body-tertiary rounded-3">
<h1 class="text-body-emphasis">{% block jumbotron %}{% endblock %}</h1>
@ -96,8 +98,8 @@
</p>
</div>
</div>
{% block content %}{% endblock %}
</main>
{% block content %}{% endblock %}
</main>
</body>
{% block endscript %}{% endblock %}

View File

@ -0,0 +1,98 @@
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{% load static %}
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<!-- JS, Popper.js, and jQuery -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" -->
<!-- integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" -->
<!-- crossorigin="anonymous"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
{% block script %}{% endblock %}
<style>
{% block style %}{% endblock %}
body {
margin-top: 10px;
}
label {
margin-right: 10px;
}
.table-borderless > tbody > tr > td,
.table-borderless > tbody > tr > th,
.table-borderless > tfoot > tr > td,
.table-borderless > tfoot > tr > th,
.table-borderless > thead > tr > td,
.table-borderless > thead > tr > th {
border: none;
background-color: rgba(0,0,0, 0.0) !important;
}
</style>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Damn Yankee Brewing</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'yeast:batches' %}">Batches</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Brewery Operations</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="/yeast/">Yeast</a>
<a class="dropdown-item" href="/equipment/">Equipment</a>
<a class="dropdown-item" href="/beer/">Beer</a>
</div>
</li>
</ul>
{% if user.is_authenticated %}
<div class="dropdown ms-auto">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ user.username }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="document.getElementById('LougoutForm').submit();">Log Out</a></li>
<li><a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a></li>
{% if user.is_superuser %}
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
</ul>
</div>
{% else %}
<a href="{% url 'login' %}">Log In</a>
{% endif %}
<form id="LougoutForm" action="{% url 'logout' %}" method="POST" class="form-inline my-2 my-lg-0">
{% csrf_token %}
</form>
</div>
</div>
</nav>
</div>
<main role="main">
{% block content %}{% endblock %}
</main>
</body>
{% block endscript %}{% endblock %}
</html>

View File

@ -1,36 +1,20 @@
{% extends 'base.html' %}
{% block style %}
form { display: table; }
p { display: table-row; }
label { display: table-cell; }
input { display: table-cell; }
{% endblock %}
{% block jumbotron %}
Add Batch
{% endblock %}
{% block content %}
<div class="container">
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
<!-- <form action="addbatch/" method="post">
{% csrf_token %}
production_date:<br>
<input name="production_date">
<br><br>
strain:<br>
<input name="strain">
<br><br>
source:<br>
<input name="source">
<br><br>
data_web:<br>
<input name="data_web">
<br><br>
<input type="submit" value="Submit">
</form> -->
</div>
{% endblock %}
{% extends 'base.html' %}
{% block style %}
form { display: table; }
p { display: table-row; }
label { display: table-cell; }
input { display: table-cell; }
{% endblock %}
{% block jumbotron %}
Add Batch
{% endblock %}
{% block content %}
<div class="container">
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
</div>
{% endblock %}