Compare commits
14 Commits
d31d53ca9b
...
dd9ae6cd21
Author | SHA1 | Date | |
---|---|---|---|
dd9ae6cd21 | |||
0f6bed9707 | |||
14fb791357 | |||
ea4340c8ea | |||
34470cbea7 | |||
9566f203a0 | |||
d6aa8e8d6a | |||
8a3c80b517 | |||
1a4d352d85 | |||
b05f486f19 | |||
cd66ff1f50 | |||
1eab86e5ae | |||
06dfd5a9e9 | |||
e0ac1f96db |
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,10 +1,11 @@
|
||||
# Django #
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__
|
||||
brewery.sqlite
|
||||
media
|
||||
secrets.json
|
||||
.env
|
||||
tags
|
||||
# Django #
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__
|
||||
brewery.sqlite
|
||||
media
|
||||
secrets.json
|
||||
.env
|
||||
tags
|
||||
*.geany
|
||||
|
@ -1,20 +1,49 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.apps import apps
|
||||
|
||||
from beer.models import Batch, BatchRecipe, BatchRecipe
|
||||
from beer.models import Batch, BatchRecipe, Mash, MashStep, \
|
||||
RecipeFermentable, RecipeHop, RecipeMisc, RecipeYeast
|
||||
from yeast.models import Yeast
|
||||
|
||||
from config.extras import BREWFATHER_APP_ROOT
|
||||
|
||||
|
||||
class SampleInline(admin.TabularInline):
|
||||
model = Yeast
|
||||
extra = 0
|
||||
|
||||
|
||||
class FermentableInline(admin.TabularInline):
|
||||
model = RecipeFermentable
|
||||
extra = 1
|
||||
|
||||
|
||||
class HopInline(admin.TabularInline):
|
||||
model = RecipeHop
|
||||
extra = 1
|
||||
|
||||
|
||||
class MiscInline(admin.TabularInline):
|
||||
model = RecipeMisc
|
||||
extra = 1
|
||||
|
||||
|
||||
class StrainInline(admin.TabularInline):
|
||||
model = RecipeYeast
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(BatchRecipe)
|
||||
class BatchRecipeAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
inlines = [
|
||||
FermentableInline,
|
||||
HopInline,
|
||||
MiscInline,
|
||||
StrainInline
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Batch)
|
||||
class BeerBatchAdmin(admin.ModelAdmin):
|
||||
@ -23,11 +52,28 @@ class BeerBatchAdmin(admin.ModelAdmin):
|
||||
SampleInline,
|
||||
]
|
||||
|
||||
url_string = "<a href='{root}/tabs/batches/batch/{batch_id}'>Brewfather Batch ID: {batch_id}</a>"
|
||||
|
||||
def batch_url(self, obj):
|
||||
url_string = ('<a href="{root}/tabs/batches/batch/{batch_id}">'
|
||||
'Brewfather Batch ID: {batch_id}</a>')
|
||||
bf_id = obj.brewfather_id
|
||||
return format_html("<a href='{root}/tabs/batches/batch/{batch_id}'>Brewfather App: {batch_id}</a>", batch_id=bf_id, root=BREWFATHER_APP_ROOT)
|
||||
return format_html(
|
||||
url_string,
|
||||
batch_id=bf_id,
|
||||
root=BREWFATHER_APP_ROOT
|
||||
)
|
||||
|
||||
|
||||
class MashStepInline(admin.TabularInline):
|
||||
model = MashStep
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Mash)
|
||||
class MashAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', ]
|
||||
inlines = [
|
||||
MashStepInline,
|
||||
]
|
||||
|
||||
|
||||
app = apps.get_app_config('beer')
|
||||
|
@ -9,18 +9,67 @@ 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',
|
||||
'unit_cost': 'costPerAmount',
|
||||
'supplier': 'supplier',
|
||||
'notes': 'notes',
|
||||
'user_notes': 'userNotes',
|
||||
},
|
||||
'fermentable': {
|
||||
'grain_category': 'grainCategory',
|
||||
'fermentable_type': 'type',
|
||||
'diastatic_power': 'diastaticPower',
|
||||
'potential': 'potential',
|
||||
'protein': 'protein',
|
||||
'attenuation': 'attenuation',
|
||||
'lovibond': 'lovibond',
|
||||
'max_in_batch': 'maxInBatch',
|
||||
'moisture': 'moisture',
|
||||
'non_fermentable': 'notFermentable',
|
||||
'ibu_per_unit': 'ibuPerAmount',
|
||||
},
|
||||
'hop': {
|
||||
'ibu': 'ibu',
|
||||
'use': 'use',
|
||||
'hop_type': 'type',
|
||||
'alpha': 'alpha',
|
||||
|
||||
},
|
||||
'misc': {
|
||||
'use': 'use',
|
||||
'misc_type': 'type',
|
||||
'water_adjustment': 'waterAdjustment',
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def bf_from_local(category, name):
|
||||
return BREWFATHER_CONVERT_LOOKUP[category][name]
|
||||
|
||||
|
||||
def local_from_bf(category, name):
|
||||
local_keys = list(BREWFATHER_CONVERT_LOOKUP[category].keys())
|
||||
bf_keys = list(BREWFATHER_CONVERT_LOOKUP[category].values())
|
||||
return local_keys[bf_keys.index(name)]
|
||||
|
||||
|
||||
def get_batches(api_user, api_key, batch=''):
|
||||
auth_string = api_user + ':' + api_key
|
||||
|
||||
|
||||
auth64 = base64.b64encode(auth_string.encode("utf-8"))
|
||||
batch_array = []
|
||||
|
||||
if batch != '':
|
||||
lastbatch = '&start_after=' + batch
|
||||
else:
|
||||
lastbatch = ''
|
||||
|
||||
query = '{batch_url}?limit={pull_limit}&complete=True&include=recipe,recipe.batchSize&status=Planning{last_batch}'.format(
|
||||
|
||||
qry_str = ('{batch_url}?limit={pull_limit}'
|
||||
'&complete=True&include=recipe,recipe.batchSize'
|
||||
'&status=Planning{last_batch}')
|
||||
query = qry_str.format(
|
||||
batch_url=BATCH_URL,
|
||||
pull_limit=PULL_LIMIT,
|
||||
last_batch=lastbatch
|
||||
@ -37,45 +86,3 @@ def get_batches(api_user, api_key, batch=''):
|
||||
data = data + get_batches(batch=last_id)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def pull_recipes(api_user, api_key):
|
||||
batch_list = get_batches(api_user, api_key);
|
||||
|
||||
for batch in batch_list:
|
||||
|
||||
batch_id = batch['_id']
|
||||
brewDate = batch['brewDate']
|
||||
recipe = batch['recipe']
|
||||
batch_name = '{} {}'.format(batch['name'], batch['batchNo'])
|
||||
name = recipe['name']
|
||||
batchSize = recipe['batchSize'] * 0.264172
|
||||
strike_water = recipe['data']['mashWaterAmount'] * 0.264172
|
||||
sparge_water = recipe['data']['spargeWaterAmount'] * 0.264172
|
||||
fermentables = recipe['data']['mashFermentables']
|
||||
fermentable_text = ''
|
||||
|
||||
for num, ferm in enumerate(fermentables):
|
||||
logger.critical(ferm)
|
||||
# var this_ferm = recipe.data.mashFermentables[key];
|
||||
# console.log(this_ferm);
|
||||
# var malt_string = this_ferm.name + '@@@' + this_ferm.grainCategory + '@@@' + this_ferm.amount * 2.20462 + '@@@ @@@' + this_ferm.color;
|
||||
# fermentable_text = fermentable_text + "%%%" + malt_string;
|
||||
# }
|
||||
|
||||
# /*for (j=0;j<fermentables.length;j++){
|
||||
# console.log(fermentables[j]);
|
||||
# var malt_string = fermentables[j].name + '@@@' + fermentables[j].grainCategory + '@@@' + fermentables[j].amount * 2.20462 + '@@@ @@@' + fermentables[j].color;
|
||||
# fermentable_text = fermentable_text + "%%%" + malt_string;
|
||||
# }*/
|
||||
# row_data.push([name, batch_name, fermentable_text, id, strike_water, sparge_water, batchSize, brewDate]);
|
||||
# }
|
||||
|
||||
# sheet = SpreadsheetApp.getActive().getSheetByName('Recipes');
|
||||
|
||||
# clearrange = sheet.getRange("A2:H");
|
||||
# clearrange.clear();
|
||||
# range = sheet.getRange(1, 1, row_data.length, row_data[0].length);
|
||||
# range.setValues(row_data);
|
||||
# clearrange.sort(8);
|
||||
# }
|
@ -0,0 +1,217 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-17 17:13
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0003_delete_recipe_batchrecipe_batch_recipe_and_more'),
|
||||
('yeast', '0004_alter_propogation_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fermentable',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||
('notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('grain_category', models.IntegerField(choices=[(1, 'Base'), (2, 'Wheat/Oat'), (3, 'Crystal'), (4, 'Roasted'), (5, 'Acid')], default=1)),
|
||||
('fermentable_type', models.IntegerField(choices=[(1, 'Grain'), (2, 'Adjunct')], default=1)),
|
||||
('diastatic_power', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('potential', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||
('protein', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('attenuation', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('lovibond', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('max_in_batch', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('moisture', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
|
||||
('non_fermentable', models.BooleanField(blank=True, null=True)),
|
||||
('ibu_per_unit', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Hop',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||
('notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('ibu', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
|
||||
('use', models.IntegerField(choices=[(1, 'Bittering'), (2, 'Aroma'), (3, 'Both')], default=1)),
|
||||
('hop_type', models.IntegerField(choices=[(1, 'Pellet'), (2, 'Leaf'), (3, 'Cryo'), (4, 'CO2 Extract')], default=1)),
|
||||
('alpha', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Mash',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Misc',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
|
||||
('notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||
('use', models.IntegerField(choices=[(1, 'Mash'), (2, 'Sparge'), (3, 'Boil'), (4, 'Flamout'), (5, 'Primary'), (6, 'Secondary'), (7, 'Cold Crash'), (8, 'Bottling')], default=1)),
|
||||
('misc_type', models.IntegerField(choices=[(1, 'Spice'), (2, 'Fining'), (3, 'Water Agent'), (4, 'Herb'), (5, 'Flavor'), (6, 'Other')], default=1)),
|
||||
('water_adjustment', models.BooleanField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Supplier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Unit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('unit_type', models.CharField(choices=[('WT', 'Weight'), ('VL', 'Volume')], default='WT', max_length=3)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batchrecipe',
|
||||
name='efficiency',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batchrecipe',
|
||||
name='mash',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.mash'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MashStep',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('step_temp', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||
('ramp_time', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||
('step_time', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||
('step_type', models.IntegerField(choices=[(1, 'infusion'), (2, 'temperature'), (3, 'decoction')], default=1)),
|
||||
('parent_mash', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.mash')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeFermentable',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||
('fermentable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.fermentable')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeHop',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||
('hop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.hop')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeMisc',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||
('misc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.misc')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeYeast',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
|
||||
('yeast', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.strain')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='misc',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hop',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fermentable',
|
||||
name='supplier',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='misc',
|
||||
name='units',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hop',
|
||||
name='units',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fermentable',
|
||||
name='units',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
|
||||
),
|
||||
]
|
24
beer/migrations/0005_recipehop_time_recipehop_use.py
Normal file
24
beer/migrations/0005_recipehop_time_recipehop_use.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-17 18:24
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0004_fermentable_hop_mash_misc_supplier_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recipehop',
|
||||
name='time',
|
||||
field=models.IntegerField(default=60, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='recipehop',
|
||||
name='use',
|
||||
field=models.IntegerField(choices=[(1, 'Boil'), (2, 'Dry Hop'), (3, 'Aroma (Hop Stand)'), (4, 'Mash'), (5, 'First Wort')], default=1),
|
||||
),
|
||||
]
|
@ -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),
|
||||
),
|
||||
]
|
@ -0,0 +1,77 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-18 23:23
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0006_batchrecipe_batch_size_alter_batchrecipe_efficiency'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='fermentable',
|
||||
name='parent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.fermentable'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hop',
|
||||
name='parent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.hop'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mash',
|
||||
name='parent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.mash'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='misc',
|
||||
name='parent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.misc'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fermentable',
|
||||
name='fermentable_type',
|
||||
field=models.IntegerField(choices=[(1, 'Grain'), (2, 'Adjunct'), (3, 'Sugar')], default=1),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EquipmentProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('hlt_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
|
||||
('mt_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
|
||||
('mt_capacity', models.DecimalField(decimal_places=2, default=10, max_digits=6)),
|
||||
('grain_absorption', models.DecimalField(decimal_places=2, default=0.12, max_digits=6)),
|
||||
('kettle_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
|
||||
('kettle_plumbing_loss', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
|
||||
('kettle_boil_rate', models.DecimalField(decimal_places=2, default=0.5, max_digits=6)),
|
||||
('batch_volume', models.DecimalField(decimal_places=2, default=5.5, max_digits=6)),
|
||||
('leaf_hop_trub', models.DecimalField(decimal_places=2, default=0.0625, max_digits=6)),
|
||||
('pellet_hop_trub', models.DecimalField(decimal_places=2, default=0.025, max_digits=6)),
|
||||
('hops_remain_kettle', models.BooleanField(default=True)),
|
||||
('mt_initial_hear', models.DecimalField(decimal_places=2, default=0.74, max_digits=6)),
|
||||
('mt_heat_loss_hour', models.DecimalField(decimal_places=2, default=2.0, max_digits=6)),
|
||||
('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.equipmentprofile')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RecipeEquipment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.equipmentprofile')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-18 23:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0007_fermentable_parent_hop_parent_mash_parent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='equipmentprofile',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.equipmentprofile'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fermentable',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.fermentable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hop',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.hop'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mash',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.mash'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='misc',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.misc'),
|
||||
),
|
||||
]
|
42
beer/migrations/0009_batchrecipe_equipment_and_more.py
Normal file
42
beer/migrations/0009_batchrecipe_equipment_and_more.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-19 00:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0008_alter_equipmentprofile_parent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='batchrecipe',
|
||||
name='equipment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.equipmentprofile'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentprofile',
|
||||
name='leaf_hop_trub',
|
||||
field=models.DecimalField(decimal_places=4, default=0.0625, max_digits=6),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentprofile',
|
||||
name='mt_heat_loss_hour',
|
||||
field=models.DecimalField(decimal_places=4, default=2.0, max_digits=6),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentprofile',
|
||||
name='mt_initial_hear',
|
||||
field=models.DecimalField(decimal_places=4, default=0.74, max_digits=6),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='equipmentprofile',
|
||||
name='pellet_hop_trub',
|
||||
field=models.DecimalField(decimal_places=4, default=0.025, max_digits=6),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='RecipeEquipment',
|
||||
),
|
||||
]
|
429
beer/models.py
429
beer/models.py
@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
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 django.conf import settings
|
||||
@ -8,6 +9,7 @@ from django.conf import settings
|
||||
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)
|
||||
@ -15,31 +17,448 @@ class CustomModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UserProfile(CustomModel):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
brewfather_api_user = encrypt(models.TextField(max_length=128))
|
||||
brewfather_api_key = encrypt(models.TextField(max_length=128))
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class Batch(CustomModel):
|
||||
brewfather_id = models.CharField(max_length=50)
|
||||
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 = models.OneToOneField(
|
||||
'BatchRecipe', on_delete=models.CASCADE, default=1)
|
||||
|
||||
@property
|
||||
def brewfather_url(self):
|
||||
return '{}/tabs/batches/batch/{}'.format(BREWFATHER_APP_ROOT, self.brewfather_id)
|
||||
return '{}/tabs/batches/batch/{}'.format(
|
||||
BREWFATHER_APP_ROOT,
|
||||
self.brewfather_id
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
# Return a string that represents the instance
|
||||
return 'BF #{num}: {name}'.format(name=self.brewfather_name, num=self.brewfather_num)
|
||||
return 'BF #{num}: {name}'.format(
|
||||
name=self.brewfather_name,
|
||||
num=self.brewfather_num
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Recipe Stuff
|
||||
# ----------------------------------------------------------------------
|
||||
class Unit(CustomModel):
|
||||
unit_types = {
|
||||
'WT': 'Weight',
|
||||
'VL': 'Volume',
|
||||
}
|
||||
|
||||
""" Recipe to be stored with a batch."""
|
||||
name = models.CharField(max_length=50)
|
||||
unit_type = models.CharField(
|
||||
max_length=3, choices=unit_types, default='WT')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Supplier(CustomModel):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class CustomIngredient(CustomModel):
|
||||
""" Custom model class with default fields to use. """
|
||||
created_date = models.DateTimeField(default=timezone.now)
|
||||
name = models.CharField(max_length=50)
|
||||
units = models.ForeignKey(Unit, on_delete=models.PROTECT)
|
||||
unit_cost = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, null=True, blank=True)
|
||||
supplier = models.ForeignKey(
|
||||
Supplier, on_delete=models.PROTECT, null=True, blank=True)
|
||||
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
user_notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
parent = models.ForeignKey(
|
||||
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
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)
|
||||
equipment = models.ForeignKey(
|
||||
'EquipmentProfile', on_delete=models.PROTECT, 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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Recipe'
|
||||
verbose_name_plural = 'Recipes'
|
||||
|
||||
@property
|
||||
def fermentables(self):
|
||||
return [x for x in list(self.recipefermentable_set.all())]
|
||||
|
||||
@property
|
||||
def fermentable_weight(self):
|
||||
return sum([x.quantity for x in self.fermentables])
|
||||
|
||||
@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 sugar_yield(self):
|
||||
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
|
||||
|
||||
return float(ferm_yield)
|
||||
|
||||
@property
|
||||
def mash_yield(self):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
@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)
|
||||
|
||||
@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 self.equipment is None:
|
||||
return 0
|
||||
|
||||
# If hops in kettle deadspace
|
||||
# No deadspace if its all filled with hop trub
|
||||
if self.equipment.hops_remain_kettle:
|
||||
result = self.kettle_dead_space - self.hop_water_loss
|
||||
return float(max(0, result))
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def kettle_hose_loss(self):
|
||||
if self.equipment is None:
|
||||
return 0
|
||||
return float(self.equipment.kettle_plumbing_loss)
|
||||
|
||||
@property
|
||||
def kettle_dead_space(self):
|
||||
if self.equipment is None:
|
||||
return 0
|
||||
return float(self.equipment.kettle_deadspace)
|
||||
|
||||
@property
|
||||
def boil_off_gph(self):
|
||||
if self.equipment is None:
|
||||
return 0
|
||||
return float(self.equipment.kettle_boil_rate)
|
||||
|
||||
@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_lookup = {
|
||||
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', 0: 'C1963C'
|
||||
}
|
||||
|
||||
return '#{}'.format(srm_hex_lookup[int(self.srm)])
|
||||
|
||||
@property
|
||||
def bu_gu(self):
|
||||
gu = (self.original_sg - 1) * 1000
|
||||
try:
|
||||
return self.ibu_tinseth / gu
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def rbr(self): # .75 needs to be calculated number...
|
||||
return self.bu_gu * (1 + (.75 - 0.7655))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Fermentable(CustomIngredient):
|
||||
categories = {
|
||||
1: 'Base',
|
||||
2: 'Wheat/Oat',
|
||||
3: 'Crystal',
|
||||
4: 'Roasted',
|
||||
5: 'Acid',
|
||||
}
|
||||
|
||||
types = {
|
||||
1: 'Grain',
|
||||
2: 'Adjunct',
|
||||
3: 'Sugar'
|
||||
}
|
||||
|
||||
grain_category = models.IntegerField(
|
||||
choices=categories, default=1)
|
||||
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)
|
||||
protein = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, null=True, blank=True)
|
||||
attenuation = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, null=True, blank=True)
|
||||
lovibond = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, null=True, blank=True)
|
||||
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)
|
||||
non_fermentable = models.BooleanField(null=True, blank=True)
|
||||
ibu_per_unit = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeFermentable(CustomModel):
|
||||
recipe = models.ForeignKey(BatchRecipe, 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)
|
||||
|
||||
@property
|
||||
def srm(self):
|
||||
srm_calc = (float(self.fermentable.lovibond)
|
||||
* float(self.quantity) / self.recipe.final_volume)
|
||||
return round(srm_calc, 1)
|
||||
|
||||
|
||||
class Hop(CustomIngredient):
|
||||
uses = {
|
||||
1: 'Bittering',
|
||||
2: 'Aroma',
|
||||
3: 'Both',
|
||||
}
|
||||
|
||||
types = {
|
||||
1: 'Pellet',
|
||||
2: 'Leaf',
|
||||
3: 'Cryo',
|
||||
4: 'CO2 Extract',
|
||||
}
|
||||
|
||||
ibu = models.DecimalField(max_digits=6, decimal_places=4, default=0)
|
||||
use = models.IntegerField(choices=uses, default=1)
|
||||
hop_type = models.IntegerField(choices=types, default=1)
|
||||
alpha = models.DecimalField(max_digits=6, decimal_places=2, default=0)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeHop(CustomModel):
|
||||
uses = {
|
||||
1: 'Boil',
|
||||
2: 'Dry Hop',
|
||||
3: 'Aroma (Hop Stand)',
|
||||
4: 'Mash',
|
||||
5: 'First Wort'
|
||||
}
|
||||
recipe = models.ForeignKey(BatchRecipe, 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 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 - 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))
|
||||
ibu = conc * util
|
||||
|
||||
return float(ibu)
|
||||
|
||||
|
||||
class Misc(CustomIngredient):
|
||||
uses = {
|
||||
1: 'Mash',
|
||||
2: 'Sparge',
|
||||
3: 'Boil',
|
||||
4: 'Flamout',
|
||||
5: 'Primary',
|
||||
6: 'Secondary',
|
||||
7: 'Cold Crash',
|
||||
8: 'Bottling',
|
||||
}
|
||||
|
||||
types = {
|
||||
1: 'Spice',
|
||||
2: 'Fining',
|
||||
3: 'Water Agent',
|
||||
4: 'Herb',
|
||||
5: 'Flavor',
|
||||
6: 'Other',
|
||||
}
|
||||
|
||||
use = models.IntegerField(choices=uses, default=1)
|
||||
misc_type = models.IntegerField(choices=types, default=1)
|
||||
water_adjustment = models.BooleanField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeMisc(CustomModel):
|
||||
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
|
||||
misc = models.ForeignKey(Misc, on_delete=models.CASCADE)
|
||||
quantity = models.DecimalField(max_digits=6, decimal_places=4)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecipeYeast(CustomModel):
|
||||
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
|
||||
yeast = models.ForeignKey('yeast.Strain', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Mash(CustomModel):
|
||||
name = models.CharField(max_length=50)
|
||||
parent = models.ForeignKey(
|
||||
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class MashStep(CustomModel):
|
||||
step_types = {
|
||||
1: 'infusion',
|
||||
2: 'temperature',
|
||||
3: 'decoction',
|
||||
}
|
||||
name = models.CharField(max_length=50)
|
||||
step_temp = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
ramp_time = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
step_time = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
step_type = models.IntegerField(choices=step_types, default=1)
|
||||
parent_mash = models.ForeignKey(Mash, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class EquipmentProfile(CustomModel):
|
||||
name = models.CharField(max_length=50)
|
||||
parent = models.ForeignKey(
|
||||
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
# Water managment stuff
|
||||
hlt_deadspace = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.25)
|
||||
mt_deadspace = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.25)
|
||||
mt_capacity = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=10)
|
||||
grain_absorption = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.12) # gal/lb
|
||||
kettle_deadspace = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.25)
|
||||
kettle_plumbing_loss = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.25)
|
||||
kettle_boil_rate = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0.5) # gal/hr
|
||||
batch_volume = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=5.5)
|
||||
leaf_hop_trub = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, default=0.0625)
|
||||
pellet_hop_trub = models.DecimalField(
|
||||
max_digits=6, decimal_places=4, default=0.025)
|
||||
hops_remain_kettle = models.BooleanField(default=True)
|
||||
|
||||
# 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)
|
||||
|
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 |
7964
beer/static/bjcp/2015.json
Normal file
7964
beer/static/bjcp/2015.json
Normal file
File diff suppressed because it is too large
Load Diff
6885
beer/static/bjcp/2021.json
Normal file
6885
beer/static/bjcp/2021.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
||||
input, label {
|
||||
display:block;
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Recipea{% endblock %}
|
||||
@ -13,6 +14,13 @@ input, label {
|
||||
{% block jumbotron %}Recipe Manager {% endblock %}
|
||||
{% block jumbotronsub %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Comming Soon?</h1>
|
||||
|
||||
<div class="container" id="main">
|
||||
<h1>Comming Soon?</h1>
|
||||
|
||||
<ul>
|
||||
{% for recipe in recipes %}
|
||||
<li><a href="{% url 'beer:recipe' recipe.id %}">{{ recipe.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
187
beer/templates/beer/recipe.html
Normal file
187
beer/templates/beer/recipe.html
Normal file
@ -0,0 +1,187 @@
|
||||
{% 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-4" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
|
||||
|
||||
<div class="text-truncate">Equipment: {{ recipe.equipment.name }}</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>
|
||||
<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">
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
||||
<!-- 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>
|
||||
<span class="text-muted">{{ h.hop.get_hop_type_display }} {{ h.ibu_tinseth|floatformat:1 }} IBU</span>
|
||||
</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>{{ recipe.bu_gu|floatformat:2 }}</b><br>
|
||||
RBR: <b>{{ recipe.rbr|floatformat:2 }}</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,38 +1,53 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import ListView
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .models import UserProfile, BatchRecipe, Batch
|
||||
from .extras import get_batches
|
||||
|
||||
import json
|
||||
|
||||
from config.extras import AveryLabel
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
def home(request):
|
||||
profile = get_object_or_404(UserProfile, user=request.user)
|
||||
api_user = profile.brewfather_api_user
|
||||
api_key = profile.brewfather_api_key
|
||||
|
||||
|
||||
batch_list = get_batches(api_user, api_key)
|
||||
|
||||
|
||||
for batch in batch_list:
|
||||
if Batch.objects.filter(brewfather_id=batch['_id']).first() is None:
|
||||
recipe_name = batch['recipe']['name']
|
||||
|
||||
recipe_obj = BatchRecipe(name=recipe_name, batch_recipe=True,recipe_json=json.dumps(batch['recipe']))
|
||||
|
||||
recipe_obj = BatchRecipe(
|
||||
name=recipe_name,
|
||||
batch_recipe=True, recipe_json=json.dumps(batch['recipe'])
|
||||
)
|
||||
recipe_obj.save()
|
||||
|
||||
|
||||
batch_obj = Batch(
|
||||
brewfather_id = batch['_id'],
|
||||
brewfather_num = batch['batchNo'],
|
||||
brewfather_name = batch['recipe']['name'],
|
||||
recipe = recipe_obj,
|
||||
brewfather_id=batch['_id'],
|
||||
brewfather_num=batch['batchNo'],
|
||||
brewfather_name=batch['recipe']['name'],
|
||||
recipe=recipe_obj,
|
||||
)
|
||||
|
||||
batch_obj.save()
|
||||
|
||||
return render(request, 'beer/home.html',{'batches':BatchRecipe.objects.all()})
|
||||
context = {
|
||||
'recipes': BatchRecipe.objects.all(),
|
||||
}
|
||||
return render(request, 'beer/home.html', context)
|
||||
|
||||
|
||||
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)
|
||||
|
126
config/extras.py
126
config/extras.py
@ -7,33 +7,25 @@ from django.db.models import BigIntegerField
|
||||
from django.urls import reverse
|
||||
|
||||
from reportlab.graphics import shapes
|
||||
from reportlab.graphics.barcode import createBarcodeDrawing
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.lib.pagesizes import LETTER
|
||||
from config.settings import DEBUG
|
||||
|
||||
BREWFATHER_APP_ROOT = 'https://web.brewfather.app'
|
||||
|
||||
def random_with_N_digits(n):
|
||||
|
||||
def random_with_n_digits(n):
|
||||
range_start = 10**(n-1)
|
||||
range_end = (10**n)-1
|
||||
return randint(range_start, range_end)
|
||||
|
||||
|
||||
class AveryLabel:
|
||||
"""
|
||||
test_labels = [
|
||||
{'manufacturer':'SafAle', 'name':'S-04', 'date':'2024-05-01', 'id':'12345'},
|
||||
{'manufacturer':'SafAle', 'name':'US-05', 'date':'2024-05-15', 'id':'23456'}
|
||||
]
|
||||
|
||||
labelSheet = AveryLabel(18294, debug=True)
|
||||
labelSheet.render(test_labels, 'filelabels.pdf', 5)
|
||||
"""
|
||||
""" Class to generate page of averty labels. """
|
||||
|
||||
AVERY = {
|
||||
18294: ( 4, 15, LETTER, (44.45, 16.764), (7.62, 13.97)),
|
||||
5263: ( 2, 5, LETTER, (101.6, 50.8), (3.95, 12.7)),
|
||||
18294: (4, 15, LETTER, (44.45, 16.764), (7.62, 13.97)),
|
||||
5263: (2, 5, LETTER, (101.6, 50.8), (3.95, 12.7)),
|
||||
}
|
||||
|
||||
def __init__(self, label, **kwargs):
|
||||
@ -51,70 +43,28 @@ class AveryLabel:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
self.specs = labels.Specification(
|
||||
self.pagesize[0]*0.352778, self.pagesize[1]*0.352778, # Convert from PostScript points to mm
|
||||
self.pagesize[0]*0.352778, self.pagesize[1] *
|
||||
0.352778, # Convert from PostScript points to mm
|
||||
self.across, self.down,
|
||||
self.labelsize[0], self.labelsize[1],
|
||||
corner_radius=self.corner_radius,
|
||||
left_margin=self.margins[0], right_margin=self.margins[0], top_margin=self.margins[1],
|
||||
left_margin=self.margins[0],
|
||||
right_margin=self.margins[0],
|
||||
top_margin=self.margins[1],
|
||||
left_padding=2, right_padding=1, top_padding=1, bottom_padding=0,
|
||||
row_gap=0)
|
||||
|
||||
# def draw_sample(self, label, width, height, obj):
|
||||
# if not obj['blank']:
|
||||
# obj['path'] = reverse("yeast:sample", kwargs={"sample_id": obj['id']})
|
||||
# # Barcode
|
||||
# if DEBUG:
|
||||
# qrw = QrCodeWidget('https://yeast.giacofei.org/{path}'.format(**obj))
|
||||
# else:
|
||||
# qrw = QrCodeWidget('https://{host}/{path}'.format(**obj))
|
||||
|
||||
# b = qrw.getBounds()
|
||||
# w=b[2]-b[0]
|
||||
# h=b[3]-b[1]
|
||||
# psSize = (self.labelsize[1]/0.352778)
|
||||
# d = shapes.Drawing(w,h, transform=[(4.5/6) * (psSize/w),0,0,(4.5/6) * (psSize/h),0,0])
|
||||
# d.add(qrw)
|
||||
# label.add(d)
|
||||
|
||||
# # Title
|
||||
# label.add(shapes.String(
|
||||
# 0, # Left Position
|
||||
# 5 * height / 6, # Bottom Position
|
||||
# '{manufacturer} {name}'.format(**obj), # Text
|
||||
# fontName="Helvetica",
|
||||
# fontSize=10
|
||||
# ))
|
||||
|
||||
# # Line
|
||||
# line_pos = 4.5 * height / 6
|
||||
# label.add(shapes.Line(0, line_pos, width,line_pos))
|
||||
|
||||
# # Metadata
|
||||
# label.add(shapes.String(
|
||||
# height*.75, # Left Position
|
||||
# 3 * height / 6, # Bottom Position
|
||||
# 'ID: {id}'.format(**obj), # Text
|
||||
# fontName="Helvetica",
|
||||
# fontSize=6
|
||||
# ))
|
||||
# label.add(shapes.String(
|
||||
# height*.75, # Left Position
|
||||
# 2 * height / 6, # Bottom Position
|
||||
# 'Packaging Date: {date}'.format(**obj), # Text
|
||||
# fontName="Helvetica",
|
||||
# fontSize=6
|
||||
# ))
|
||||
|
||||
def draw(self, label, width, height, obj):
|
||||
if not obj['blank']:
|
||||
obj['path'] = reverse(
|
||||
"{}:{}".format(obj['ns'],obj['template']),
|
||||
"{}:{}".format(obj['ns'], obj['template']),
|
||||
kwargs={"{}_id".format(obj['template']): obj['id']}
|
||||
)
|
||||
|
||||
# Barcode
|
||||
if DEBUG:
|
||||
qrw = QrCodeWidget('https://brewery.giacofei.org/{path}'.format(**obj))
|
||||
qrw = QrCodeWidget(
|
||||
'https://brewery.giacofei.org/{path}'.format(**obj))
|
||||
else:
|
||||
qrw = QrCodeWidget('https://{host}/{path}'.format(**obj))
|
||||
|
||||
@ -122,53 +72,49 @@ class AveryLabel:
|
||||
font = height/num_lines
|
||||
|
||||
b = qrw.getBounds()
|
||||
w=b[2]-b[0]
|
||||
h=b[3]-b[1]
|
||||
psSize = (self.labelsize[1]/0.352778)
|
||||
d = shapes.Drawing(w,h, transform=[(psSize/w),0,0,(psSize/h),0,0])
|
||||
w = b[2]-b[0]
|
||||
h = b[3]-b[1]
|
||||
ps_size = (self.labelsize[1]/0.352778)
|
||||
d = shapes.Drawing(
|
||||
w, h, transform=[(ps_size/w), 0, 0, (ps_size/h), 0, 0])
|
||||
d.add(qrw)
|
||||
label.add(d)
|
||||
|
||||
# Line
|
||||
line_pos = (num_lines - 1) * height / num_lines
|
||||
label.add(shapes.Line(height, line_pos, width,line_pos))
|
||||
|
||||
label.add(shapes.Line(height, line_pos, width, line_pos))
|
||||
|
||||
# Title
|
||||
label.add(shapes.String(
|
||||
height, # Left Position
|
||||
line_pos * 1.05, # Bottom Position
|
||||
'{title}'.format(**obj), # Text
|
||||
'{title}'.format(**obj), # Text
|
||||
fontName="Helvetica",
|
||||
fontSize=font
|
||||
))
|
||||
|
||||
for x, line in enumerate(obj['data']):
|
||||
x=x+1
|
||||
x = x+1
|
||||
label.add(shapes.String(
|
||||
height, # Left Position
|
||||
line_pos - font * x, # Bottom Position
|
||||
line, # Text
|
||||
line, # Text
|
||||
fontName="Helvetica",
|
||||
fontSize=font
|
||||
))
|
||||
))
|
||||
|
||||
def render(self, objects, render_file, labels_used=0):
|
||||
"""
|
||||
|
||||
# Create the HttpResponse object with the appropriate PDF headers.
|
||||
response = HttpResponse(mimetype='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename=somefilename.pdf'
|
||||
"""
|
||||
""" Create the HttpResponse object with the appropriate PDF headers."""
|
||||
|
||||
# Create the sheet.
|
||||
# if render_type == 'sample':
|
||||
# sheet = labels.Sheet(self.specs, self.draw_sample, border=self.debug)
|
||||
# sheet = labels.Sheet(self.specs, self.draw_sample, border=self.debug)
|
||||
|
||||
sheet = labels.Sheet(self.specs, self.draw, border=self.debug)
|
||||
sheet = labels.Sheet(self.specs, self.draw, border=self.debug)
|
||||
|
||||
# Add a couple of labels.
|
||||
for i in range(int(labels_used)):
|
||||
sheet.add_label({'blank':True})
|
||||
sheet.add_label({'blank': True})
|
||||
|
||||
for entry in objects:
|
||||
sheet.add_label(entry)
|
||||
@ -176,12 +122,14 @@ class AveryLabel:
|
||||
# Save the file and we are done.
|
||||
sheet.save(render_file)
|
||||
|
||||
|
||||
class DateUUIDField(BigIntegerField):
|
||||
"""
|
||||
A field which stores a Short UUID value with prepended date. This may also have
|
||||
the Boolean attribute 'auto' which will set the value on initial save to a
|
||||
new UUID value (calculated using shortuuid's default (uuid4)). Note that while all
|
||||
UUIDs are expected to be unique we enforce this with a DB constraint.
|
||||
A field which stores a Short UUID value with prepended date.
|
||||
This may also have the Boolean attribute 'auto' which will set the value
|
||||
on initial save to a new UUID value (calculated using shortuuid's default
|
||||
(uuid4)). Note that while all UUIDs are expected to be unique we enforce
|
||||
this with a DB constraint.
|
||||
"""
|
||||
|
||||
def __init__(self, auto=True, *args, **kwargs):
|
||||
@ -190,7 +138,7 @@ class DateUUIDField(BigIntegerField):
|
||||
# Do not let the user edit UUIDs if they are auto-assigned.
|
||||
kwargs['editable'] = False
|
||||
kwargs['blank'] = True
|
||||
kwargs['unique'] = True # if you want to be paranoid, set unique=True in your instantiation of the field.
|
||||
kwargs['unique'] = True
|
||||
|
||||
super(DateUUIDField, self).__init__(*args, **kwargs)
|
||||
|
||||
@ -204,11 +152,11 @@ class DateUUIDField(BigIntegerField):
|
||||
# Assign a new value for this attribute if required.
|
||||
x = datetime.datetime.now()
|
||||
front = x.strftime("%Y%m%d")
|
||||
value = int('{}{}'.format(front,random_with_N_digits(6)))
|
||||
value = int('{}{}'.format(front, random_with_n_digits(6)))
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
if self.auto:
|
||||
return None
|
||||
return super(DateUUIDField, self).formfield(**kwargs)
|
||||
return super(DateUUIDField, self).formfield(**kwargs)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
class ChangePasswordForm(forms.Form):
|
||||
old_password = forms.CharField(widget=forms.PasswordInput())
|
||||
new_password = forms.CharField(widget=forms.PasswordInput())
|
||||
confirm_password = forms.CharField(widget=forms.PasswordInput())
|
||||
confirm_password = forms.CharField(widget=forms.PasswordInput())
|
||||
|
@ -1,217 +0,0 @@
|
||||
# Sample Gunicorn configuration file.
|
||||
# https://raw.githubusercontent.com/benoitc/gunicorn/master/examples/example_config.py
|
||||
#
|
||||
# Server socket
|
||||
#
|
||||
# bind - The socket to bind.
|
||||
#
|
||||
# A string of the form: 'HOST', 'HOST:PORT', 'unix:PATH'.
|
||||
# An IP is a valid HOST.
|
||||
#
|
||||
# backlog - The number of pending connections. This refers
|
||||
# to the number of clients that can be waiting to be
|
||||
# served. Exceeding this number results in the client
|
||||
# getting an error when attempting to connect. It should
|
||||
# only affect servers under significant load.
|
||||
#
|
||||
# Must be a positive integer. Generally set in the 64-2048
|
||||
# range.
|
||||
#
|
||||
|
||||
|
||||
bind = 'myserver.com:1234'
|
||||
backlog = 2048
|
||||
|
||||
#
|
||||
# Worker processes
|
||||
#
|
||||
# workers - The number of worker processes that this server
|
||||
# should keep alive for handling requests.
|
||||
#
|
||||
# A positive integer generally in the 2-4 x $(NUM_CORES)
|
||||
# range. You'll want to vary this a bit to find the best
|
||||
# for your particular application's work load.
|
||||
#
|
||||
# worker_class - The type of workers to use. The default
|
||||
# sync class should handle most 'normal' types of work
|
||||
# loads. You'll want to read
|
||||
# http://docs.gunicorn.org/en/latest/design.html#choosing-a-worker-type
|
||||
# for information on when you might want to choose one
|
||||
# of the other worker classes.
|
||||
#
|
||||
# A string referring to a Python path to a subclass of
|
||||
# gunicorn.workers.base.Worker. The default provided values
|
||||
# can be seen at
|
||||
# http://docs.gunicorn.org/en/latest/settings.html#worker-class
|
||||
#
|
||||
# worker_connections - For the eventlet and gevent worker classes
|
||||
# this limits the maximum number of simultaneous clients that
|
||||
# a single process can handle.
|
||||
#
|
||||
# A positive integer generally set to around 1000.
|
||||
#
|
||||
# timeout - If a worker does not notify the master process in this
|
||||
# number of seconds it is killed and a new worker is spawned
|
||||
# to replace it.
|
||||
#
|
||||
# Generally set to thirty seconds. Only set this noticeably
|
||||
# higher if you're sure of the repercussions for sync workers.
|
||||
# For the non sync workers it just means that the worker
|
||||
# process is still communicating and is not tied to the length
|
||||
# of time required to handle a single request.
|
||||
#
|
||||
# keepalive - The number of seconds to wait for the next request
|
||||
# on a Keep-Alive HTTP connection.
|
||||
#
|
||||
# A positive integer. Generally set in the 1-5 seconds range.
|
||||
#
|
||||
|
||||
workers = 1
|
||||
worker_class = 'sync'
|
||||
worker_connections = 1000
|
||||
timeout = 30
|
||||
keepalive = 2
|
||||
|
||||
#
|
||||
# spew - Install a trace function that spews every line of Python
|
||||
# that is executed when running the server. This is the
|
||||
# nuclear option.
|
||||
#
|
||||
# True or False
|
||||
#
|
||||
|
||||
spew = False
|
||||
|
||||
#
|
||||
# Server mechanics
|
||||
#
|
||||
# daemon - Detach the main Gunicorn process from the controlling
|
||||
# terminal with a standard fork/fork sequence.
|
||||
#
|
||||
# True or False
|
||||
#
|
||||
# raw_env - Pass environment variables to the execution environment.
|
||||
#
|
||||
# pidfile - The path to a pid file to write
|
||||
#
|
||||
# A path string or None to not write a pid file.
|
||||
#
|
||||
# user - Switch worker processes to run as this user.
|
||||
#
|
||||
# A valid user id (as an integer) or the name of a user that
|
||||
# can be retrieved with a call to pwd.getpwnam(value) or None
|
||||
# to not change the worker process user.
|
||||
#
|
||||
# group - Switch worker process to run as this group.
|
||||
#
|
||||
# A valid group id (as an integer) or the name of a user that
|
||||
# can be retrieved with a call to pwd.getgrnam(value) or None
|
||||
# to change the worker processes group.
|
||||
#
|
||||
# umask - A mask for file permissions written by Gunicorn. Note that
|
||||
# this affects unix socket permissions.
|
||||
#
|
||||
# A valid value for the os.umask(mode) call or a string
|
||||
# compatible with int(value, 0) (0 means Python guesses
|
||||
# the base, so values like "0", "0xFF", "0022" are valid
|
||||
# for decimal, hex, and octal representations)
|
||||
#
|
||||
# tmp_upload_dir - A directory to store temporary request data when
|
||||
# requests are read. This will most likely be disappearing soon.
|
||||
#
|
||||
# A path to a directory where the process owner can write. Or
|
||||
# None to signal that Python should choose one on its own.
|
||||
#
|
||||
|
||||
daemon = False
|
||||
# raw_env = [
|
||||
# 'DJANGO_SECRET_KEY=something',
|
||||
# 'SPAM=eggs',
|
||||
# ]
|
||||
pidfile = None
|
||||
umask = 0
|
||||
user = None
|
||||
group = None
|
||||
tmp_upload_dir = None
|
||||
|
||||
#
|
||||
# Logging
|
||||
#
|
||||
# logfile - The path to a log file to write to.
|
||||
#
|
||||
# A path string. "-" means log to stdout.
|
||||
#
|
||||
# loglevel - The granularity of log output
|
||||
#
|
||||
# A string of "debug", "info", "warning", "error", "critical"
|
||||
#
|
||||
|
||||
errorlog = '-'
|
||||
loglevel = 'info'
|
||||
accesslog = '-'
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
#
|
||||
# Process naming
|
||||
#
|
||||
# proc_name - A base to use with setproctitle to change the way
|
||||
# that Gunicorn processes are reported in the system process
|
||||
# table. This affects things like 'ps' and 'top'. If you're
|
||||
# going to be running more than one instance of Gunicorn you'll
|
||||
# probably want to set a name to tell them apart. This requires
|
||||
# that you install the setproctitle module.
|
||||
#
|
||||
# A string or None to choose a default of something like 'gunicorn'.
|
||||
#
|
||||
|
||||
proc_name = None
|
||||
|
||||
#
|
||||
# Server hooks
|
||||
#
|
||||
# post_fork - Called just after a worker has been forked.
|
||||
#
|
||||
# A callable that takes a server and worker instance
|
||||
# as arguments.
|
||||
#
|
||||
# pre_fork - Called just prior to forking the worker subprocess.
|
||||
#
|
||||
# A callable that accepts the same arguments as after_fork
|
||||
#
|
||||
# pre_exec - Called just prior to forking off a secondary
|
||||
# master process during things like config reloading.
|
||||
#
|
||||
# A callable that takes a server instance as the sole argument.
|
||||
#
|
||||
|
||||
def post_fork(server, worker):
|
||||
server.log.info("Worker spawned (pid: %s)", worker.pid)
|
||||
|
||||
def pre_fork(server, worker):
|
||||
pass
|
||||
|
||||
def pre_exec(server):
|
||||
server.log.info("Forked child, re-executing.")
|
||||
|
||||
def when_ready(server):
|
||||
server.log.info("Server is ready. Spawning workers")
|
||||
|
||||
def worker_int(worker):
|
||||
worker.log.info("worker received INT or QUIT signal")
|
||||
|
||||
## get traceback info
|
||||
import threading, sys, traceback
|
||||
id2name = {th.ident: th.name for th in threading.enumerate()}
|
||||
code = []
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""),
|
||||
threadId))
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
code.append('File: "%s", line %d, in %s' % (filename,
|
||||
lineno, name))
|
||||
if line:
|
||||
code.append(" %s" % (line.strip()))
|
||||
worker.log.debug("\n".join(code))
|
||||
|
||||
def worker_abort(worker):
|
||||
worker.log.info("worker received SIGABRT signal")
|
@ -1,16 +1,14 @@
|
||||
import os
|
||||
import dj_database_url
|
||||
from environs import Env
|
||||
import json
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
with open(os.path.join(BASE_DIR,'secrets.json')) as f:
|
||||
with open(os.path.join(BASE_DIR, 'secrets.json')) as f:
|
||||
secrets = json.load(f)
|
||||
|
||||
TIME_ZONE = 'America/New_York'
|
||||
|
||||
SECRET_KEY = secrets.get('SECRET_KEY',"default_secret")
|
||||
SECRET_KEY = secrets.get('SECRET_KEY', "default_secret")
|
||||
|
||||
DEBUG = secrets.get('DEBUG', True)
|
||||
SITE_ID = 1
|
||||
@ -62,14 +60,14 @@ MIDDLEWARE = [
|
||||
MEDIA_ROOT = '/tmp/media/'
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
STATIC_ROOT = secrets.get('STATIC_ROOT',os.path.join(BASE_DIR,'sitestatic'))
|
||||
STATIC_ROOT = secrets.get('STATIC_ROOT', os.path.join(BASE_DIR, 'sitestatic'))
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR,'templates'),
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
@ -89,9 +87,8 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
DEFAULT_AUTO_FIELD='django.db.models.AutoField'
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
LOGIN_REDIRECT_URL = 'home'
|
||||
LOGIN_URL = 'login'
|
||||
LOGOUT_REDIRECT_URL = 'home'
|
||||
|
@ -9,6 +9,5 @@ urlpatterns = [
|
||||
path('yeast/', include(('yeast.urls', 'yeast'))),
|
||||
path('beer/', include(('beer.urls', 'beer'))),
|
||||
path('equipment/', include(('equipment.urls', 'equipment'))),
|
||||
#path('change-password/', ChangePasswordView.as_view(), name='change_password'),
|
||||
path('', home, name="home"),
|
||||
]
|
||||
|
@ -1,7 +1,5 @@
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth.views import PasswordChangeView
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, 'home.html',{})
|
||||
return render(request, 'home.html', {})
|
||||
|
@ -1,13 +1,15 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.apps import apps
|
||||
from django import forms
|
||||
|
||||
from equipment.models import Equipment, KegType, State, EquipmentMaintenanceLine, EquipmentMaintenance
|
||||
from equipment.models import Equipment, KegType, State, \
|
||||
EquipmentMaintenanceLine, EquipmentMaintenance
|
||||
|
||||
|
||||
class AdminCreateFormMixin:
|
||||
"""
|
||||
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
|
||||
Mixin to easily use a different form for the create case
|
||||
(in comparison to "edit") in the django admin
|
||||
|
||||
Logic copied from `django.contrib.auth.admin.UserAdmin`
|
||||
"""
|
||||
add_form = None
|
||||
@ -19,22 +21,26 @@ class AdminCreateFormMixin:
|
||||
defaults.update(kwargs)
|
||||
return super().get_form(request, obj, **defaults)
|
||||
|
||||
|
||||
class MainenanceLineInline(admin.TabularInline):
|
||||
model = EquipmentMaintenanceLine
|
||||
extra = 1
|
||||
|
||||
|
||||
class EquipmentInline(admin.TabularInline):
|
||||
model = Equipment
|
||||
extra = 4
|
||||
|
||||
|
||||
@admin.register(EquipmentMaintenance)
|
||||
class MaintenanceAdmin(admin.ModelAdmin):
|
||||
#list_display_links = ['equipment',]
|
||||
list_display = ['created_date','equipment', 'notes',]
|
||||
# list_display_links = ['equipment',]
|
||||
list_display = ['created_date', 'equipment', 'notes',]
|
||||
inlines = [
|
||||
MainenanceLineInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Equipment)
|
||||
class EquipmentAdmin(admin.ModelAdmin):
|
||||
|
||||
@ -42,9 +48,10 @@ class EquipmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'equipment_type', 'keg_type', 'state']
|
||||
list_editable = ['state', 'state']
|
||||
|
||||
|
||||
@admin.register(KegType)
|
||||
class KegTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ['name','manufacturer','size_gal',]
|
||||
list_display = ['name', 'manufacturer', 'size_gal',]
|
||||
inlines = [
|
||||
EquipmentInline,
|
||||
]
|
||||
@ -52,6 +59,7 @@ class KegTypeAdmin(admin.ModelAdmin):
|
||||
# class EquipmentAdmin(admin.ModelAdmin):
|
||||
# list_display = ['name','equipment_type','keg_type','state']
|
||||
|
||||
|
||||
@admin.register(State)
|
||||
class StateAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
|
@ -1,10 +1,10 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
|
||||
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)
|
||||
@ -12,19 +12,23 @@ class CustomModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class EquipmentType(CustomModel):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class State(CustomModel):
|
||||
name = models.CharField(max_length=100)
|
||||
EquipmentType = models.ForeignKey(EquipmentType, on_delete=models.PROTECT, null=True, blank=True)
|
||||
EquipmentType = models.ForeignKey(
|
||||
EquipmentType, on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class KegType(CustomModel):
|
||||
name = models.CharField(max_length=100)
|
||||
manufacturer = models.CharField(max_length=100, blank=True)
|
||||
@ -33,6 +37,7 @@ class KegType(CustomModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Equipment(CustomModel):
|
||||
name = models.CharField(max_length=100)
|
||||
equipment_type = models.ForeignKey(EquipmentType, on_delete=models.PROTECT)
|
||||
@ -61,12 +66,18 @@ class EquipmentMaintenance(CustomModel):
|
||||
equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE)
|
||||
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
|
||||
|
||||
class EquipmentMaintenanceLine(CustomModel):
|
||||
maintenance = models.ForeignKey(EquipmentMaintenance, on_delete=models.CASCADE)
|
||||
maintenance_type = models.ForeignKey(MaintenanceType, on_delete=models.PROTECT)
|
||||
maintenance = models.ForeignKey(
|
||||
EquipmentMaintenance, on_delete=models.CASCADE)
|
||||
maintenance_type = models.ForeignKey(
|
||||
MaintenanceType, on_delete=models.PROTECT)
|
||||
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
|
||||
|
||||
class EquipmentTransactions(CustomModel):
|
||||
equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE)
|
||||
transaction_type = models.ForeignKey(TransactionType, on_delete=models.PROTECT)
|
||||
beer_batch = models.ForeignKey('beer.Batch', on_delete=models.PROTECT, blank=True, null=True)
|
||||
transaction_type = models.ForeignKey(
|
||||
TransactionType, on_delete=models.PROTECT)
|
||||
beer_batch = models.ForeignKey(
|
||||
'beer.Batch', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import equipment_labels, home, EquipmentListView, equipment
|
||||
from .views import equipment_labels, EquipmentListView, equipment
|
||||
|
||||
urlpatterns = [
|
||||
path('labels/', equipment_labels, name='labels'),
|
||||
|
@ -8,40 +8,56 @@ from config.extras import AveryLabel
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, 'equipment/home.html',{'equipment':Equipment.objects.all()})
|
||||
return render(
|
||||
request,
|
||||
'equipment/home.html',
|
||||
{'equipment': Equipment.objects.all()}
|
||||
)
|
||||
|
||||
|
||||
class EquipmentListView(ListView):
|
||||
model = Equipment
|
||||
|
||||
|
||||
def equipment(request, equipment_id):
|
||||
equipment = get_object_or_404(Equipment, pk=equipment_id)
|
||||
return render(request, 'equipment/equipment.html', {'equipment': equipment})
|
||||
return render(
|
||||
request,
|
||||
'equipment/equipment.html',
|
||||
{'equipment': equipment}
|
||||
)
|
||||
|
||||
|
||||
def equipment_labels(request):
|
||||
""" Create label PDF for selected equipment
|
||||
"""
|
||||
skip_count = request.POST.get("skip_count", "")
|
||||
equipment_list = request.POST.getlist("equipment_list", "")
|
||||
to_print = list(filter(lambda d: str(d.id) in equipment_list, Equipment.objects.all()))
|
||||
to_print = list(filter(lambda d: str(
|
||||
d.id) in equipment_list, Equipment.objects.all()))
|
||||
|
||||
# Create the HttpResponse object with the appropriate PDF headers.
|
||||
response = HttpResponse(content_type ='application/pdf')
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename=keglabels.pdf'
|
||||
labelSheet = AveryLabel(5263, debug=False)
|
||||
|
||||
label_sheet = AveryLabel(5263, debug=False)
|
||||
|
||||
labels = []
|
||||
for equipment in to_print:
|
||||
labels.append({
|
||||
'id': equipment.id,
|
||||
'title': equipment.keg_type.name,
|
||||
'data': ['{} gal'.format(equipment.keg_type.size_gal), 'ID: {}'.format(equipment.id)], # TODO: Update this fo rgeneric equipment
|
||||
# TODO: Update this fo rgeneric equipment
|
||||
'data': [
|
||||
'{} gal'.format(equipment.keg_type.size_gal),
|
||||
'ID: {}'.format(equipment.id)],
|
||||
'blank': False,
|
||||
'host': request.get_host(),
|
||||
'template': 'equipment',
|
||||
'ns': 'equipment'
|
||||
})
|
||||
|
||||
labelSheet.render(labels, response, skip_count)
|
||||
|
||||
return response
|
||||
label_sheet.render(labels, response, skip_count)
|
||||
|
||||
return response
|
||||
|
@ -5,40 +5,51 @@ from django.apps import apps
|
||||
from yeast.models import Yeast, Strain, Manufacturer, Storage, Propogation
|
||||
from yeast.forms import YeastModelForm
|
||||
|
||||
import beer
|
||||
|
||||
from config.extras import BREWFATHER_APP_ROOT
|
||||
|
||||
|
||||
class PropogationInline(admin.TabularInline):
|
||||
model = Propogation
|
||||
extra = 0
|
||||
|
||||
|
||||
class SampleInline(admin.TabularInline):
|
||||
model = Yeast
|
||||
extra = 0
|
||||
|
||||
|
||||
class StrainInline(admin.TabularInline):
|
||||
model = Strain
|
||||
extra = 5
|
||||
|
||||
|
||||
class ParentInline(admin.TabularInline):
|
||||
verbose_name = 'Parent Samples'
|
||||
verbose_name = 'Parent Samples'
|
||||
model = Propogation.parent.through
|
||||
|
||||
|
||||
@admin.register(Yeast)
|
||||
class YeastAdmin(admin.ModelAdmin):
|
||||
list_display = [ 'propogation', 'url', 'lot_number', 'age', 'storage', 'viability', 'generation_num', 'cellcount', 'pitched', 'date_pitched', 'pitched_batch']
|
||||
list_display = ['propogation', 'url', 'lot_number', 'age', 'storage',
|
||||
'viability', 'generation_num', 'cellcount', 'pitched',
|
||||
'date_pitched', 'pitched_batch']
|
||||
list_editable = ['pitched', 'date_pitched', 'pitched_batch', 'lot_number']
|
||||
|
||||
def batch_url(self, obj):
|
||||
if obj.pitched_batch:
|
||||
bf_id = obj.pitched_batch.brewfather_id
|
||||
return format_html("<a href='https://web.brewfather.app/tabs/batches/batch/{batch_id}'>{batch_id}</a>", batch_id=bf_id)
|
||||
url_string = ('<a href="{root}/tabs/batches/batch/{batch_id}">'
|
||||
'{batch_id}</a>')
|
||||
return format_html(
|
||||
url_string,
|
||||
batch_id=bf_id,
|
||||
root=BREWFATHER_APP_ROOT)
|
||||
|
||||
def url(self, obj):
|
||||
if obj.data_web:
|
||||
return format_html("<a href='{url}'>{url}</a>", url=obj.data_web)
|
||||
|
||||
|
||||
@admin.register(Strain)
|
||||
class StrainAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'long_name', 'manufacturer', 'avilable_batches']
|
||||
@ -53,13 +64,16 @@ class StrainAdmin(admin.ModelAdmin):
|
||||
urls = []
|
||||
|
||||
for related_obj in related_objs:
|
||||
url_text = '{}: {}'.format(related_obj.get_source_display(), related_obj.production_date.strftime("%Y-%m-%d"))
|
||||
url = reverse('admin:yeast_batch_change', args=[related_obj.id]) # replace 'myapp' with your app name
|
||||
url_text = '{}: {}'.format(related_obj.get_source_display(
|
||||
), related_obj.production_date.strftime("%Y-%m-%d"))
|
||||
# replace 'myapp' with your app name
|
||||
url = reverse('admin:yeast_batch_change', args=[related_obj.id])
|
||||
urls.append('<a href="{}">{}</a>'.format(url, url_text))
|
||||
return format_html(', '.join(urls))
|
||||
|
||||
avilable_batches.short_description = 'Available Propogation'
|
||||
|
||||
|
||||
@admin.register(Storage)
|
||||
class StorageAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'viability_loss', 'viability_interval']
|
||||
@ -67,6 +81,7 @@ class StorageAdmin(admin.ModelAdmin):
|
||||
SampleInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'url']
|
||||
@ -78,20 +93,24 @@ class ManufacturerAdmin(admin.ModelAdmin):
|
||||
if obj.website:
|
||||
return format_html("<a href='{url}'>{url}</a>", url=obj.website)
|
||||
|
||||
|
||||
@admin.register(Propogation)
|
||||
class PropogationAdmin(admin.ModelAdmin):
|
||||
list_display = ['strain', 'consumed', 'source', 'parent_samples', 'production_date', 'avilable_samples', 'used_samples']
|
||||
list_display = ['strain', 'consumed', 'source', 'parent_samples',
|
||||
'production_date', 'avilable_samples', 'used_samples']
|
||||
form = YeastModelForm
|
||||
filter_horizontal = ['parent']
|
||||
filter_horizontal = ['parent']
|
||||
inlines = [
|
||||
#ParentInline,
|
||||
# ParentInline,
|
||||
SampleInline,
|
||||
]
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
super(PropogationAdmin, self).save_related(request, form, formsets, change)
|
||||
super(PropogationAdmin, self).save_related(
|
||||
request, form, formsets, change)
|
||||
if form.instance.source_batch:
|
||||
relate_samples = [x for x in Yeast.objects.all() if x.pitched_batch==form.instance.source_batch]
|
||||
relate_samples = [x for x in Yeast.objects.all(
|
||||
) if x.pitched_batch == form.instance.source_batch]
|
||||
for sample in relate_samples:
|
||||
form.instance.parent.add(sample)
|
||||
|
||||
@ -99,7 +118,8 @@ class PropogationAdmin(admin.ModelAdmin):
|
||||
obj.parent.all()
|
||||
|
||||
def used_samples(self, obj):
|
||||
related_objs = list(set(obj.yeast_set.all()) - set(obj.remaining_samples))
|
||||
related_objs = list(set(obj.yeast_set.all()) -
|
||||
set(obj.remaining_samples))
|
||||
|
||||
urls = []
|
||||
|
||||
@ -116,7 +136,8 @@ class PropogationAdmin(admin.ModelAdmin):
|
||||
urls = []
|
||||
|
||||
for related_obj in related_objs:
|
||||
url = reverse('admin:yeast_yeast_change', args=[related_obj.id]) # replace 'myapp' with your app name
|
||||
# replace 'myapp' with your app name
|
||||
url = reverse('admin:yeast_yeast_change', args=[related_obj.id])
|
||||
urls.append('<a href="{}">{}</a>'.format(url, related_obj))
|
||||
return format_html(', '.join(urls))
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from .models import Yeast, Propogation, Strain, Storage
|
||||
from beer.models import Batch
|
||||
|
||||
@ -11,12 +11,13 @@ class MyModelChoiceField(forms.ModelChoiceField):
|
||||
return obj.name
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
input_type = 'date'
|
||||
|
||||
|
||||
class YeastModelForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Yeast
|
||||
@ -33,7 +34,9 @@ class YeastModelForm(forms.ModelForm):
|
||||
class BatchAddForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BatchAddForm, self).__init__(*args, **kwargs)
|
||||
self.fields['strain'].help_text = '<a href="{}">Add a new strain</a>'.format(reverse('yeast:addstrain'))
|
||||
href_string = '<a href="{}">Add a new strain</a>'
|
||||
self.fields['strain'].help_text = href_string.format(
|
||||
reverse('yeast:addstrain'))
|
||||
|
||||
# create meta class
|
||||
class Meta:
|
||||
@ -53,6 +56,7 @@ class BatchAddForm(forms.ModelForm):
|
||||
|
||||
num_samples = forms.IntegerField()
|
||||
|
||||
|
||||
class StrainAddForm(forms.ModelForm):
|
||||
|
||||
# create meta class
|
||||
@ -67,6 +71,7 @@ class StrainAddForm(forms.ModelForm):
|
||||
'manufacturer',
|
||||
]
|
||||
|
||||
|
||||
class PitchIntoBeerForm(forms.Form):
|
||||
batch = forms.ModelChoiceField(queryset=Batch.objects.all())
|
||||
starter = forms.BooleanField(required=False)
|
||||
@ -79,6 +84,7 @@ class PitchIntoBeerForm(forms.Form):
|
||||
sample.date_pitched = timezone.now()
|
||||
sample.save()
|
||||
|
||||
|
||||
class PropogateSampleForm(forms.Form):
|
||||
num = forms.IntegerField(min_value=1)
|
||||
storage = forms.ModelChoiceField(queryset=Storage.objects.all())
|
||||
@ -94,9 +100,9 @@ class PropogateSampleForm(forms.Form):
|
||||
# send email using the self.cleaned_data dictionary
|
||||
prop_obj = Propogation(
|
||||
production_date=timezone.now(),
|
||||
strain = strain,
|
||||
source = 'PR',
|
||||
notes = 'Auto generated from form.'
|
||||
strain=strain,
|
||||
source='PR',
|
||||
notes='Auto generated from form.'
|
||||
)
|
||||
|
||||
prop_obj.save()
|
||||
@ -109,15 +115,14 @@ class PropogateSampleForm(forms.Form):
|
||||
cells = 100
|
||||
|
||||
yeast = Yeast(
|
||||
propogation = prop_obj,
|
||||
generation_num = parent.generation_num + 1,
|
||||
storage = storage,
|
||||
pitched = False,
|
||||
cellcount = cells,
|
||||
notes = 'Auto generated from form.'
|
||||
propogation=prop_obj,
|
||||
generation_num=parent.generation_num + 1,
|
||||
storage=storage,
|
||||
pitched=False,
|
||||
cellcount=cells,
|
||||
notes='Auto generated from form.'
|
||||
)
|
||||
|
||||
yeast.save()
|
||||
|
||||
return prop_obj
|
||||
|
||||
|
@ -1,24 +1,24 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from datetime import datetime
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Count
|
||||
import math
|
||||
|
||||
from config.extras import DateUUIDField
|
||||
from config.extras import BREWFATHER_APP_ROOT
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
from config.extras import BREWFATHER_APP_ROOT
|
||||
|
||||
def average(lst):
|
||||
return sum(lst) / len(lst)
|
||||
|
||||
|
||||
class AvailableYeastManager(models.Manager):
|
||||
""" Special manager for filtering out pitched yeast."""
|
||||
|
||||
def get_queryset(self):
|
||||
return super(AvailableYeastManager, self).get_queryset().filter(pitched=False)
|
||||
return super(AvailableYeastManager, self)\
|
||||
.get_queryset().filter(pitched=False)
|
||||
|
||||
|
||||
class CustomModel(models.Model):
|
||||
@ -38,6 +38,7 @@ class Manufacturer(CustomModel):
|
||||
# Return a string that represents the instance
|
||||
return self.name
|
||||
|
||||
|
||||
class Strain(CustomModel):
|
||||
"""
|
||||
Store individual yeast strain data. :model:`yeast.Manufacturer`.
|
||||
@ -48,12 +49,14 @@ class Strain(CustomModel):
|
||||
|
||||
@property
|
||||
def batches_available(self):
|
||||
return [x for x in Propogation.objects.all() if not x.consumed and x.strain==self]
|
||||
return [x for x in Propogation.objects.all()
|
||||
if not x.consumed and x.strain == self]
|
||||
|
||||
def __str__(self):
|
||||
# Return a string that represents the instance
|
||||
return '{}: {}'.format(self.manufacturer.name, self.name)
|
||||
|
||||
|
||||
class Storage(CustomModel):
|
||||
"""
|
||||
Data for methods of yeast storage. Used for calculating viability
|
||||
@ -86,31 +89,37 @@ class Propogation(CustomModel):
|
||||
production_date = models.DateField()
|
||||
strain = models.ForeignKey(Strain, on_delete=models.PROTECT, default=0)
|
||||
source = models.CharField(max_length=3, choices=BATCH_TYPES, default='ST')
|
||||
source_batch = models.ForeignKey('beer.Batch', null=True, blank=True, on_delete=models.PROTECT)
|
||||
source_batch = models.ForeignKey(
|
||||
'beer.Batch', null=True, blank=True, on_delete=models.PROTECT)
|
||||
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Propogation, self).save(*args, **kwargs)
|
||||
if self.source_batch:
|
||||
relate_samples = [x for x in Yeast.objects.all() if x.pitched_batch==self.source_batch]
|
||||
relate_samples = [x for x in Yeast.objects.all()
|
||||
if x.pitched_batch == self.source_batch]
|
||||
for sample in relate_samples:
|
||||
self.parent.add(sample)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return int(average([x.age for x in Yeast.available.all() if x.propogation == self]))
|
||||
return int(average([x.age for x in Yeast.available.all()
|
||||
if x.propogation == self]))
|
||||
|
||||
@property
|
||||
def max_viability(self):
|
||||
return int(max([x.viability for x in Yeast.available.all() if x.propogation == self]) * 100)
|
||||
return int(max([x.viability for x in Yeast.available.all()
|
||||
if x.propogation == self]) * 100)
|
||||
|
||||
@property
|
||||
def min_viability(self):
|
||||
return int(min([x.viability for x in Yeast.available.all() if x.propogation == self]) * 100)
|
||||
return int(min([x.viability for x in Yeast.available.all()
|
||||
if x.propogation == self]) * 100)
|
||||
|
||||
@property
|
||||
def generation(self):
|
||||
return int(average([x.generation_num for x in Yeast.available.all() if x.propogation == self]))
|
||||
return int(average([x.generation_num for x in Yeast.available.all()
|
||||
if x.propogation == self]))
|
||||
|
||||
@property
|
||||
def beer_name(self):
|
||||
@ -125,7 +134,10 @@ class Propogation(CustomModel):
|
||||
@property
|
||||
def beer_url(self):
|
||||
if self.source_batch:
|
||||
return '{}/tabs/batches/batch/{}'.format(BREWFATHER_APP_ROOT, self.source_batch.brewfather_id)
|
||||
return '{}/tabs/batches/batch/{}'.format(
|
||||
BREWFATHER_APP_ROOT,
|
||||
self.source_batch.brewfather_id
|
||||
)
|
||||
|
||||
@property
|
||||
def consumed(self):
|
||||
@ -133,15 +145,19 @@ class Propogation(CustomModel):
|
||||
|
||||
@property
|
||||
def remaining_samples(self):
|
||||
return [x for x in Yeast.available.all() if x.propogation==self]
|
||||
return [x for x in Yeast.available.all() if x.propogation == self]
|
||||
|
||||
@property
|
||||
def used_samples(self):
|
||||
return [x for x in Yeast.objects.all() if x.propogation==self and x.pitched]
|
||||
return [x for x in Yeast.objects.all()
|
||||
if x.propogation == self and x.pitched]
|
||||
|
||||
def __str__(self):
|
||||
# Return a string that represents the instance
|
||||
return '{} [{}]'.format(self.strain, self.production_date.strftime("%Y-%m-%d"))
|
||||
return '{} [{}]'.format(
|
||||
self.strain,
|
||||
self.production_date.strftime("%Y-%m-%d")
|
||||
)
|
||||
|
||||
|
||||
class Yeast(CustomModel):
|
||||
@ -155,7 +171,8 @@ class Yeast(CustomModel):
|
||||
cellcount = models.IntegerField(default=100)
|
||||
pitched = models.BooleanField(default=False)
|
||||
date_pitched = models.DateField(blank=True, null=True)
|
||||
pitched_batch = models.ForeignKey('beer.Batch', null=True, blank=True, on_delete=models.CASCADE)
|
||||
pitched_batch = models.ForeignKey(
|
||||
'beer.Batch', null=True, blank=True, on_delete=models.CASCADE)
|
||||
data_web = models.URLField(blank=True, null=True)
|
||||
lot_number = models.CharField(max_length=15, blank=True, null=True)
|
||||
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||
@ -179,8 +196,12 @@ class Yeast(CustomModel):
|
||||
|
||||
@property
|
||||
def viability(self):
|
||||
"""Return the viability based on age and storage method (:model:`yeast.Storage`)."""
|
||||
return 0.97 * math.exp(self.age * math.log(1-self.storage.viability_loss)/self.storage.viability_interval)
|
||||
"""Return the viability based on age and storage method
|
||||
(:model:`yeast.Storage`).
|
||||
"""
|
||||
return 0.97 * math.exp(self.age
|
||||
* math.log(1-self.storage.viability_loss)
|
||||
/ self.storage.viability_interval)
|
||||
|
||||
def __str__(self):
|
||||
# Return a string that represents the instance
|
||||
|
@ -1,36 +1,36 @@
|
||||
{% 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>
|
||||
{% 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 %}
|
@ -1,14 +1,16 @@
|
||||
import logging
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_value_in_qs(queryset, key):
|
||||
batch_list = ','.join([str(x.id) for x in queryset if x.strain.id==key.id])
|
||||
batch_list = ','.join([str(x.id) for x in queryset
|
||||
if x.strain.id == key.id])
|
||||
|
||||
if batch_list:
|
||||
return batch_list
|
||||
return ''
|
||||
return ''
|
||||
|
@ -1,8 +1,8 @@
|
||||
from django.urls import include, path
|
||||
from django.urls import path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import admin
|
||||
|
||||
from yeast.views import YeastListView, BatchListView, home, sample, batch, batch_labels, addBatch, addStrain, get_batch, NewPropogation
|
||||
from yeast.views import YeastListView, BatchListView, home, sample, batch, \
|
||||
batch_labels, AddBatch, AddStrain, get_batch, NewPropogation
|
||||
|
||||
# app_name = 'yeast'
|
||||
|
||||
@ -10,12 +10,17 @@ urlpatterns = [
|
||||
|
||||
path('samples/<int:yeast_id>/', sample, name='yeast'),
|
||||
path('samples/', YeastListView.as_view(), name='yeasts'),
|
||||
path('batches/addbatch/', login_required(addBatch.as_view()), name='addbatch'),
|
||||
path('batches/addstrain/', login_required(addStrain.as_view()), name='addstrain'),
|
||||
path('batches/addbatch/',
|
||||
login_required(AddBatch.as_view()),
|
||||
name='addbatch'),
|
||||
path('batches/addstrain/',
|
||||
login_required(AddStrain.as_view()),
|
||||
name='addstrain'),
|
||||
path('batches/<int:propogation_id>/', batch, name='batch'),
|
||||
path('batches/', BatchListView.as_view(), name='batches'),
|
||||
path('batch_lookup/', get_batch, name='get_batch'),
|
||||
path('batch_labels/<int:propogation_id>/', login_required(batch_labels), name='labels'),
|
||||
path('batch_labels/<int:propogation_id>/',
|
||||
login_required(batch_labels), name='labels'),
|
||||
path('progation/add/', NewPropogation, name='propogate'),
|
||||
path('', home, name='home'),
|
||||
]
|
||||
|
@ -1,26 +1,28 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic.edit import CreateView, FormView
|
||||
from django.views import View
|
||||
from django.http import HttpResponse, HttpRequest, HttpResponseRedirect
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from yeast.models import Yeast, Propogation, Strain, Storage
|
||||
from config.extras import AveryLabel
|
||||
from yeast.forms import BatchAddForm, StrainAddForm, PropogateSampleForm, PitchIntoBeerForm
|
||||
from yeast.forms import BatchAddForm, StrainAddForm, PropogateSampleForm, \
|
||||
PitchIntoBeerForm
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger('django')
|
||||
|
||||
|
||||
class YeastListView(ListView):
|
||||
model = Yeast
|
||||
|
||||
|
||||
class BatchListView(ListView):
|
||||
model = Propogation
|
||||
|
||||
|
||||
def sample(request, yeast_id):
|
||||
if request.method == 'POST':
|
||||
if 'pitch' in request.POST:
|
||||
@ -31,18 +33,23 @@ def sample(request, yeast_id):
|
||||
logger.critical(pitch_form.cleaned_data)
|
||||
sample = get_object_or_404(Yeast, pk=yeast_id)
|
||||
pitch_form.pitch_sample()
|
||||
return HttpResponseRedirect(reverse('yeast:yeast', kwargs={'yeast_id': sample.id}))
|
||||
return HttpResponseRedirect(
|
||||
reverse('yeast:yeast', kwargs={'yeast_id': sample.id})
|
||||
)
|
||||
|
||||
elif 'propogate' in request.POST:
|
||||
propogate_form = PropogateSampleForm(request.POST)
|
||||
if propogate_form.is_valid():
|
||||
new_prop = propogate_form.create_propogation()
|
||||
return HttpResponseRedirect(reverse('yeast:batch', kwargs={'propogation_id': new_prop.id}))
|
||||
return HttpResponseRedirect(reverse(
|
||||
'yeast:batch', kwargs={'propogation_id': new_prop.id}
|
||||
))
|
||||
else:
|
||||
|
||||
return redirect('yeast:samples', kwargs={'yeast_id':yeast_id})
|
||||
return redirect('yeast:samples', kwargs={'yeast_id': yeast_id})
|
||||
else:
|
||||
propogate_form = PropogateSampleForm(initial={'sample': get_object_or_404(Yeast, pk=yeast_id)})
|
||||
propogate_form = PropogateSampleForm(
|
||||
initial={'sample': get_object_or_404(Yeast, pk=yeast_id)})
|
||||
pitch_form = PitchIntoBeerForm()
|
||||
|
||||
sample = get_object_or_404(Yeast, pk=yeast_id)
|
||||
@ -61,8 +68,14 @@ def get_batch(request):
|
||||
re_url = reverse('yeast:batch', kwargs={'propogation_id': propogation_id})
|
||||
return redirect(re_url)
|
||||
|
||||
|
||||
def home(request):
|
||||
return render(request, 'yeast/home.html',{'batches': Propogation.objects.all, 'strains': Strain.objects.all})
|
||||
context = {
|
||||
'batches': Propogation.objects.all,
|
||||
'strains': Strain.objects.all,
|
||||
}
|
||||
return render(request, 'yeast/home.html', context)
|
||||
|
||||
|
||||
def batch(request, propogation_id):
|
||||
"""
|
||||
@ -91,12 +104,13 @@ def batch_labels(request, propogation_id):
|
||||
skip_count = request.POST.get("skip_count", "")
|
||||
samples = request.POST.getlist("samples", "")
|
||||
batch = get_object_or_404(Propogation, pk=propogation_id)
|
||||
to_print = list(filter(lambda d: str(d.id) in samples, batch.yeast_set.all()))
|
||||
to_print = list(filter(lambda d: str(
|
||||
d.id) in samples, batch.yeast_set.all()))
|
||||
|
||||
# Create the HttpResponse object with the appropriate PDF headers.
|
||||
response = HttpResponse(content_type ='application/pdf')
|
||||
response = HttpResponse(content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename=samplelabels.pdf'
|
||||
labelSheet = AveryLabel(18294, debug=False)
|
||||
label_sheet = AveryLabel(18294, debug=False)
|
||||
|
||||
logger.critical(samples)
|
||||
logger.critical(to_print)
|
||||
@ -105,33 +119,38 @@ def batch_labels(request, propogation_id):
|
||||
for sample in to_print:
|
||||
labels.append({
|
||||
'id': sample.id,
|
||||
'title': '{} {}'.format(sample.propogation.strain.manufacturer.name, sample.propogation.strain.name),
|
||||
'data': ['ID: {}'.format(sample.id), 'Date: {}'.format(sample.propogation.production_date)],
|
||||
'title': '{} {}'.format(
|
||||
sample.propogation.strain.manufacturer.name,
|
||||
sample.propogation.strain.name),
|
||||
'data': ['ID: {}'.format(sample.id), 'Date: {}'.format(
|
||||
sample.propogation.production_date)],
|
||||
'blank': False,
|
||||
'host': request.get_host(),
|
||||
'template': 'yeast',
|
||||
'ns': 'yeast'
|
||||
})
|
||||
labelSheet.render(labels, response, skip_count)
|
||||
label_sheet.render(labels, response, skip_count)
|
||||
|
||||
return response
|
||||
|
||||
class addBatch(CreateView):
|
||||
|
||||
class AddBatch(CreateView):
|
||||
model = Propogation
|
||||
form_class = BatchAddForm
|
||||
|
||||
def get_success_url(self):
|
||||
id = self.object.id #gets id from created object
|
||||
id = self.object.id # gets id from created object
|
||||
return reverse('yeast:batch', kwargs={'propogation_id': id})
|
||||
|
||||
class addStrain(CreateView):
|
||||
|
||||
class AddStrain(CreateView):
|
||||
model = Strain
|
||||
form_class = StrainAddForm
|
||||
|
||||
def get_success_url(self):
|
||||
id = self.object.id #gets id from created object
|
||||
return reverse('yeast:batches')
|
||||
|
||||
|
||||
@login_required
|
||||
class NewPropogation(FormView):
|
||||
# ~ template_name = "contact.html"
|
||||
|
Loading…
Reference in New Issue
Block a user