Compare commits

...

7 Commits

Author SHA1 Message Date
1e8d79b7cf Merge pull request 'Update from dev branch' (#7) from dev into master
Reviewed-on: #7
2024-06-07 11:44:03 -04:00
8c5896b6b1 Merge branch '3-sample_controls' into dev 2024-06-07 11:41:06 -04:00
3468c38008 Rename yeast batch to propogation.
Refrences to yeast `batches` are now `propogations`.

Hopefully less confusing now...
Closes #6
2024-06-07 11:27:41 -04:00
651cfe9162 Merge branch 'dev' into 3-sample_controls 2024-06-07 11:05:54 -04:00
74e8a3135a Modify admin page a bit. 2024-06-07 08:53:43 -04:00
a6c244756e Merge branch 'dev' into 3-sample_controls 2024-06-07 08:47:04 -04:00
c3634141a3 Basic outline of yeast sample page.
None of the controls do anything yet.
2024-06-07 07:54:23 -04:00
12 changed files with 243 additions and 86 deletions

View File

@ -1,6 +1,8 @@
from django.db import models
from django.utils import timezone
from config.extras import BREWFATHER_APP_ROOT
import logging
logger = logging.getLogger('django')
@ -18,6 +20,10 @@ class Batch(CustomModel):
brewfather_name = models.CharField(max_length=500, default='name')
recipe = models.ForeignKey('BatchRecipe', on_delete=models.CASCADE, default=1)
@property
def brewfather_url(self):
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)

View File

@ -39,8 +39,8 @@ class MaintenanceAdmin(admin.ModelAdmin):
class EquipmentAdmin(admin.ModelAdmin):
readonly_fields = ('id',)
list_display = ['id', 'state', 'equipment_type', 'keg_type', 'state']
list_editable = ['state', 'equipment_type', 'keg_type', 'state']
list_display = ['id', 'equipment_type', 'keg_type', 'state']
list_editable = ['state', 'state']
@admin.register(KegType)
class KegTypeAdmin(admin.ModelAdmin):

View File

@ -8,12 +8,12 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- JS, Popper.js, and jQuery -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" -->
<!-- integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" -->
<!-- crossorigin="anonymous"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
{% block script %}{% endblock %}
<style>
{% block style %}{% endblock %}
@ -23,6 +23,15 @@
label {
margin-right: 10px;
}
.table-borderless > tbody > tr > td,
.table-borderless > tbody > tr > th,
.table-borderless > tfoot > tr > td,
.table-borderless > tfoot > tr > th,
.table-borderless > thead > tr > td,
.table-borderless > thead > tr > th {
border: none;
background-color: rgba(0,0,0, 0.0) !important;
}
</style>
</head>
<body>
@ -90,8 +99,6 @@
{% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
{% block endscript %}{% endblock %}
</html>

View File

@ -2,15 +2,15 @@ from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.apps import apps
from yeast.models import Yeast, Strain, Manufacturer, Storage, Batch
from yeast.models import Yeast, Strain, Manufacturer, Storage, Propogation
from yeast.forms import YeastModelForm
import beer
from config.extras import BREWFATHER_APP_ROOT
class BatchInline(admin.TabularInline):
model = Batch
class PropogationInline(admin.TabularInline):
model = Propogation
extra = 0
class SampleInline(admin.TabularInline):
@ -23,11 +23,11 @@ class StrainInline(admin.TabularInline):
class ParentInline(admin.TabularInline):
verbose_name = 'Parent Samples'
model = Batch.parent.through
model = Propogation.parent.through
@admin.register(Yeast)
class YeastAdmin(admin.ModelAdmin):
list_display = [ 'batch', '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):
@ -43,7 +43,7 @@ class YeastAdmin(admin.ModelAdmin):
class StrainAdmin(admin.ModelAdmin):
list_display = ['name', 'long_name', 'manufacturer', 'avilable_batches']
inlines = [
BatchInline,
PropogationInline,
]
list_editable = ['long_name', 'manufacturer']
@ -58,7 +58,7 @@ class StrainAdmin(admin.ModelAdmin):
urls.append('<a href="{}">{}</a>'.format(url, url_text))
return format_html(', '.join(urls))
avilable_batches.short_description = 'Available Batches'
avilable_batches.short_description = 'Available Propogation'
@admin.register(Storage)
class StorageAdmin(admin.ModelAdmin):
@ -78,8 +78,8 @@ class ManufacturerAdmin(admin.ModelAdmin):
if obj.website:
return format_html("<a href='{url}'>{url}</a>", url=obj.website)
@admin.register(Batch)
class BatchAdmin(admin.ModelAdmin):
@admin.register(Propogation)
class PropogationAdmin(admin.ModelAdmin):
list_display = ['strain', 'consumed', 'source', 'parent_samples', 'production_date', 'avilable_samples', 'used_samples']
form = YeastModelForm
filter_horizontal = ['parent']
@ -89,7 +89,7 @@ class BatchAdmin(admin.ModelAdmin):
]
def save_related(self, request, form, formsets, change):
super(BatchAdmin, 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]
for sample in relate_samples:

View File

@ -1,7 +1,7 @@
from django import forms
from django.urls import reverse
from django.http import HttpResponse, HttpResponseRedirect
from .models import Yeast, Batch, Strain
from .models import Yeast, Propogation, Strain
import logging
logger = logging.getLogger('django')
@ -30,7 +30,7 @@ class BatchAddForm(forms.ModelForm):
# create meta class
class Meta:
# specify model to be used
model = Batch
model = Propogation
# specify fields to be used
fields = [

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.6 on 2024-06-07 15:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('yeast', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='batch',
options={'verbose_name': 'Propagation', 'verbose_name_plural': 'Propagations'},
),
migrations.RenameField(
model_name='yeast',
old_name='batch',
new_name='propogation',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-06-07 15:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('beer', '0001_initial'),
('yeast', '0002_alter_batch_options_rename_batch_yeast_propogation'),
]
operations = [
migrations.RenameModel(
old_name='Batch',
new_name='Propogation',
),
]

View File

@ -48,7 +48,7 @@ class Strain(CustomModel):
@property
def batches_available(self):
return [x for x in Batch.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
@ -68,13 +68,14 @@ class Storage(CustomModel):
return self.name
class Batch(CustomModel):
class Propogation(CustomModel):
"""
Stores a batch of :model:`yeast.Yeast` of a single :model:`yeast.Strain`.
Can be a single purchased pack, or multiple vials
to be frozen from a starter.
"""
BATCH_TYPES = {
'ST': 'Store',
'PR': 'Propogated',
@ -89,7 +90,7 @@ class Batch(CustomModel):
notes = models.TextField(max_length=500, blank=True, null=True)
def save(self, *args, **kwargs):
super(Batch, self).save(*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]
for sample in relate_samples:
@ -97,19 +98,19 @@ class Batch(CustomModel):
@property
def age(self):
return int(average([x.age for x in Yeast.available.all() if x.batch == 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.batch == 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.batch == 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.batch == self]))
return int(average([x.generation_num for x in Yeast.available.all() if x.propogation == self]))
@property
def beer_name(self):
@ -132,11 +133,11 @@ class Batch(CustomModel):
@property
def remaining_samples(self):
return [x for x in Yeast.available.all() if x.batch==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.batch==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
@ -148,7 +149,7 @@ class Yeast(CustomModel):
Store an individual sample of yeast.
"""
id = DateUUIDField(primary_key=True)
batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
propogation = models.ForeignKey(Propogation, on_delete=models.CASCADE)
generation_num = models.IntegerField(default=0)
storage = models.ForeignKey(Storage, on_delete=models.CASCADE)
cellcount = models.IntegerField(default=100)
@ -163,7 +164,7 @@ class Yeast(CustomModel):
@property
def name(self):
return '{} {}'.format(self.id, self.batch.strain.name)
return '{} {}'.format(self.id, self.propogation.strain.name)
@property
def age(self):
@ -174,7 +175,7 @@ class Yeast(CustomModel):
else:
end_date = timezone.now().date()
return abs((self.batch.production_date-end_date).days)
return abs((self.propogation.production_date-end_date).days)
@property
def viability(self):
@ -183,13 +184,4 @@ class Yeast(CustomModel):
def __str__(self):
# Return a string that represents the instance
return '{} {}'.format(self.id, self.batch.strain.name)
# class BeerBatch(CustomModel):
# brewfather_id = models.CharField(max_length=50)
# brewfather_num = models.IntegerField(default=1)
# brewfather_name = models.CharField(max_length=500, default='name')
# def __str__(self):
# # Return a string that represents the instance
# return 'BF #{num}: {name}'.format(name=self.brewfather_name, num=self.brewfather_num)
return '{} {}'.format(self.id, self.propogation.strain.name)

View File

@ -49,7 +49,7 @@
</fieldset>
</div>
<div class="col-md-6 form-group">
<legend>Print Selected Labels for this Batch</legend>
<legend>Print Selected Labels for this Propogation</legend>
<p><p>
<label for="skip_count">Number of labels already removed from the sheet: </label>
<input id="skip_count" type="number" name="skip_count" value=0 size="4">

View File

@ -16,7 +16,7 @@ input, label {
<div class="container" id="main">
<h3>Batches</h3>
<h3>Yeast Propogations</h3>
<form action="{% url 'yeast:get_batch' %}" method="post">
{% csrf_token %}
@ -37,7 +37,7 @@ input, label {
<div class="container m-2">
<div class="row">
<div class="col-md-4">
<label for="batch-select">Choose a batch:</label>
<label for="batch-select">Choose a propogation:</label>
<select name="batch" id="batch-select" size="10" style="min-width:100%;">
{% for batch in batches %}
{% if not batch.consumed %}
@ -56,7 +56,7 @@ input, label {
{% if batch.source == 'SL' %}
<th>Yeast Source</th><td>{{ batch.get_source_display }} <a href="{{ batch.beer_url }}">#{{ batch.beer_num }} {{ batch.beer_name }}</a></td>
{% elif batch.source == 'ST' %}
<th>Batch Source</th><td>Purchased from Store</td>
<th>Yeast Source</th><td>Purchased from Store</td>
{% endif %}
</tr>
<tr>
@ -86,7 +86,7 @@ input, label {
</div>
</div>
<div class="container m-2">
<input type="submit" value="Go to Batch Page">
<input type="submit" value="Go to Yeast Propogation Page">
</div>
</div>

View File

@ -2,33 +2,143 @@
{% load mathfilters %}
{% block title %}Yeast Samples{% endblock %}
{% block jumbotron %}Yeast Sample:{% endblock %}
{% block jumbotronsub %}<a href="{% url 'admin:yeast_yeast_change' sample.id %}">{{ sample.id }}</a>{% endblock %}
{% block style %}
.table td.fit,
.table th.fit {
white-space: nowrap;
width: 1%;
}
{% endblock %}
{% block jumbotron %}Yeast Sample{% endblock %}
{% block jumbotronsub %}
<table class="table table-borderless">
<tbody>
<tr><th class="fit">{{ batch.strain.manufacturer.name }} {{ batch.strain.name }}</th><td><a href="{% url 'admin:yeast_yeast_change' sample.id %}">{{ sample.id }}</a></td></tr>
<tr><th class="fit">Batch Source</th>
<td>{{ batch.get_source_display }}
{% if batch.source_batch %}
from <a href="{{ batch.beer_url }}" target="_blank" rel="noopener noreferrer">#{{ batch.beer_num }}: {{ batch.beer_name }}</a><br>
{% endif %}
</td>
</tr>
<tr><th class="fit">Production Date</th><td>{{ sample.batch.production_date }}</td></tr>
<tr><th class="fit">Storage</th>
<td>
{{ sample.storage }}
{% if sample.pitched %}
(Pitched <a href="{{ sample.pitched_batch.brewfather_url }}" target="_blank" rel="noopener noreferrer">#{{ sample.pitched_batch.brewfather_num }}: {{ sample.pitched_batch.brewfather_name }}</a>)
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endblock %}
{% block content %}
<div class="container">
<!-- Information Header -->
<div class="container">
<h3>{{ batch.strain.name }}</h3>
<b>Batch Source:</b> {{ batch.get_source_display }}
{% if batch.source_batch %}
from <a href="{{ batch.beer_url }}" target="_blank" rel="noopener noreferrer">#{{ batch.beer_num }}: {{ batch.beer_name }}</a>
{% endif %}<br>
<b>Production Date:</b> {{ sample.batch.production_date }}<br>
</div> <!-- /container -->
<!-- End Information Header -->
<!-- Main Data Container -->
<div class="container mt-4 mb-4">
<div class="row">
<!-- Tabbed Forms -->
<div class="col-lg-6" style="{% if sample.pitched %}display:none{% endif %}">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#propogate">Propogate Sample</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#pitch">Pitch Sample</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div id="propogate" class="container tab-pane active"><br>
<!-- Sample Propogation Form -->
<div class="container" style="border:1px solid #cecece;">
<form >
<legend>Propogate Yeast Sample</legend>
<div class="mb-3">
<label for="new-samples" class="form-label"># New Samples</label>
<input type="number" class="form-control" id="new-samples" aria-describedby="numHelp">
<div id="numHelp" class="form-text">New samples will be automatically created</div>
</div>
<div class="mb-3">
<label for="parent" class="form-label">Default Storage Method</label>
<select class="form-select" id="parent" name="parent" aria-describedby="storageHelp">
<option selected>-- Choose an Option --</option>
{% for method in storage %}
<option value="{{ method.id }}">{{ method }}</option>
{% endfor %}
</select>
<div id="storageHelp" class="form-text">How will samples be stored?</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<!-- End Sample Propogation Form -->
</div>
<div id="pitch" class="container tab-pane fade"><br>
<!-- Sample Pitch Form -->
<div class="container" style="border:1px solid #cecece;">
<form >
<legend>Pitch Yeast Sample</legend>
<div class="mb-3">
<label for="beer-batch" class="form-label">Beer Batch</label>
<select class="form-select" id="beer-batch" name="beer-batch" aria-describedby="batchHelp">
<option selected>Beer Batch</option>
<option value="1">#16 So So Special</option>
<option value="2">#17 Some other beer</option>
<option value="3">#18 Another Thing</option>
</select>
<div id="batchHelp" class="form-text">Select batch of beer sample is pitched into.</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="starterCheck">
<label class="form-check-label" for="starterCheck">Pitched into starter?</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<!-- End Sample Pitch Form -->
</div>
</div>
</div>
<!-- End Tabbed Forms -->
<!-- Remain Sample List -->
<div class="col-lg-6">
{% if batch.remaining_samples %}<h4>Other Remaining Batch Samples</h4>
<ul>
{% for s in batch.remaining_samples|dictsort:"storage.name" %}
{% if s.id != sample.id %}
<li><a href="{% url 'yeast:yeast' s.id %}">{{ s.id }}</a> - {{ s.storage.name }} for {{ s.age }} days, {{ s.viability|mul:100|floatformat:1 }}%</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% if batch.used_samples %}<h4>Other Samples Used</h4>
<ul>
{% for s in batch.used_samples %}
{% if s.id != sample.id %}
<li><a href="{% url 'yeast:yeast' s.id %}">{{ s.id }}</a></li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
<!-- End Remain Sample List -->
</div>
</div>
<!-- End Main Data Container -->
<p><p>
{% if batch.remaining_samples %}<h4>Batch Samples Remaining</h4>
<ul>
{% for sample in batch.remaining_samples %}
<li><a href="{% url 'yeast:yeast' sample.id %}">{{ sample.id }}</a> - Storage method: {{ sample.storage.name }} for {{ sample.age }} days, Viability: {{ sample.viability|mul:100|floatformat:1 }}%</li>
{% endfor %}
</ul>
{% endif %}
{% if batch.used_samples %}<h4>Batch Samples Used</h4>
<ul>
{% for sample in batch.used_samples %}
<li><a href="{% url 'yeast:yeast' sample.id %}">{{ sample.id }}</a></li>
{% endfor %}
</ul>
{% endif %}
</div> <!-- /container -->
{% endblock %}

View File

@ -6,7 +6,7 @@ from django.urls import reverse
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from yeast.models import Yeast, Batch, Strain
from yeast.models import Yeast, Propogation, Strain
from config.extras import AveryLabel
from yeast.forms import BatchAddForm, StrainAddForm
@ -18,13 +18,15 @@ class YeastListView(ListView):
model = Yeast
class BatchListView(ListView):
model = Batch
model = Propogation
def sample(request, yeast_id):
sample = get_object_or_404(Yeast, pk=yeast_id)
sample_batch = get_object_or_404(Batch, pk=sample.batch_id)
return render(request, 'yeast/sample.html', {'sample': sample, 'batch':sample_batch})
return render(request, 'yeast/sample.html', {
'sample': sample,
'batch': get_object_or_404(Propogation, pk=sample.batch_id),
'storage': list(Storage.objects.all()),
})
# @login_required
# def get_batches(request):
@ -43,19 +45,19 @@ def get_batch(request):
return redirect(re_url)
def home(request):
return render(request, 'yeast/home.html',{'batches': Batch.objects.all, 'strains': Strain.objects.all})
return render(request, 'yeast/home.html',{'batches': Propogation.objects.all, 'strains': Strain.objects.all})
def batch(request, batch_id):
"""
Display a batch of yeast samples.
``Batch``
An instance of :model:`yeast.Batch`.
``Propogation``
An instance of :model:`yeast.Propogation`.
``Template``
:template:`yeast/batch.html`
"""
batch = get_object_or_404(Batch, pk=batch_id)
batch = get_object_or_404(Propogation, pk=batch_id)
return render(request, 'yeast/batch.html', {'batch': batch})
def batch_labels(request, batch_id):
@ -65,11 +67,11 @@ def batch_labels(request, batch_id):
**Context**
``Batch``
An instance of :model:`yeast.Batch`.
An instance of :model:`yeast.Propogation`.
"""
skip_count = request.POST.get("skip_count", "")
samples = request.POST.getlist("samples", "")
batch = get_object_or_404(Batch, pk=batch_id)
batch = get_object_or_404(Propogation, pk=batch_id)
to_print = list(filter(lambda d: str(d.id) in samples, batch.yeast_set.all()))
# Create the HttpResponse object with the appropriate PDF headers.
@ -96,7 +98,7 @@ def batch_labels(request, batch_id):
return response
class addBatch(CreateView):
model = Batch
model = Propogation
form_class = BatchAddForm
def get_success_url(self):