initial commit.
This commit is contained in:
commit
019f7b5b19
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
## Compose sample application
|
||||||
|
### Django application in dev mode
|
||||||
|
|
||||||
|
Project structure:
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── compose.yaml
|
||||||
|
├── app
|
||||||
|
├── Dockerfile
|
||||||
|
├── requirements.txt
|
||||||
|
└── manage.py
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
[_compose.yaml_](compose.yaml)
|
||||||
|
```
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: app
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy with docker compose
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker compose up -d
|
||||||
|
Creating network "django_default" with the default driver
|
||||||
|
Building web
|
||||||
|
Step 1/6 : FROM python:3.7-alpine
|
||||||
|
...
|
||||||
|
...
|
||||||
|
Status: Downloaded newer image for python:3.7-alpine
|
||||||
|
Creating django_web_1 ... done
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected result
|
||||||
|
|
||||||
|
Listing containers must show one container running and the port mapping as below:
|
||||||
|
```
|
||||||
|
$ docker ps
|
||||||
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
|
3adaea94142d django_web "python3 manage.py r…" About a minute ago Up About a minute 0.0.0.0:8000->8000/tcp django_web_1
|
||||||
|
```
|
||||||
|
|
||||||
|
After the application starts, navigate to `http://localhost:8000` in your web browser:
|
||||||
|
|
||||||
|
Stop and remove the containers
|
||||||
|
```
|
||||||
|
$ docker compose down
|
||||||
|
```
|
0
beer/__init__.py
Normal file
0
beer/__init__.py
Normal file
39
beer/admin.py
Normal file
39
beer/admin.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
from beer.models import Batch, Recipe, BatchRecipe
|
||||||
|
from yeast.models import Yeast
|
||||||
|
|
||||||
|
from config.extras import BREWFATHER_APP_ROOT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class SampleInline(admin.TabularInline):
|
||||||
|
model = Yeast
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
class RecipeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name']
|
||||||
|
|
||||||
|
class BatchRecipeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name']
|
||||||
|
|
||||||
|
class BeerBatchAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['brewfather_id', 'batch_url']
|
||||||
|
inlines = [
|
||||||
|
SampleInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
url_string = "<a href='{root}/tabs/batches/batch/{batch_id}'>Brewfather Batch ID: {batch_id}</a>"
|
||||||
|
|
||||||
|
def batch_url(self, obj):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Batch, BeerBatchAdmin)
|
||||||
|
admin.site.register(Recipe, RecipeAdmin)
|
||||||
|
admin.site.register(BatchRecipe, BatchRecipeAdmin)
|
6
beer/apps.py
Normal file
6
beer/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BeerConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'beer'
|
28
beer/migrations/0001_initial.py
Normal file
28
beer/migrations/0001_initial.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 12:01
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Batch',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('brewfather_id', models.CharField(max_length=50)),
|
||||||
|
('brewfather_num', models.IntegerField(default=1)),
|
||||||
|
('brewfather_name', models.CharField(default='name', max_length=500)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
36
beer/migrations/0002_batchrecipe_recipe.py
Normal file
36
beer/migrations/0002_batchrecipe_recipe.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 12:26
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('beer', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BatchRecipe',
|
||||||
|
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='Recipe',
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
19
beer/migrations/0003_batch_recipe.py
Normal file
19
beer/migrations/0003_batch_recipe.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 12:30
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('beer', '0002_batchrecipe_recipe'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='recipe',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe'),
|
||||||
|
),
|
||||||
|
]
|
0
beer/migrations/__init__.py
Normal file
0
beer/migrations/__init__.py
Normal file
32
beer/models.py
Normal file
32
beer/models.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
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.ForeignKey('BatchRecipe', on_delete=models.CASCADE, default=1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Return a string that represents the instance
|
||||||
|
return 'BF #{num}: {name}'.format(name=self.brewfather_name, num=self.brewfather_num)
|
||||||
|
|
||||||
|
class BatchRecipe(CustomModel):
|
||||||
|
""" Recipe to be stored with a batch."""
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(CustomModel):
|
||||||
|
""" Recipes not attched to batches."""
|
||||||
|
name = models.CharField(max_length=50)
|
8
beer/urls.py
Normal file
8
beer/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.flatpages import views
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
]
|
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
166
config/extras.py
Normal file
166
config/extras.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import labels
|
||||||
|
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'
|
||||||
|
|
||||||
|
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)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, label, **kwargs):
|
||||||
|
data = self.AVERY[label]
|
||||||
|
self.across = data[0]
|
||||||
|
self.down = data[1]
|
||||||
|
self.pagesize = data[2]
|
||||||
|
self.labelsize = data[3]
|
||||||
|
self.margins = data[4]
|
||||||
|
self.topDown = True
|
||||||
|
self.debug = False
|
||||||
|
self.position = 0
|
||||||
|
self.previous_used = 0
|
||||||
|
self.corner_radius = 2
|
||||||
|
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.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_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']),
|
||||||
|
kwargs={"{}_id".format(obj['template']): obj['id']}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Barcode
|
||||||
|
if DEBUG:
|
||||||
|
qrw = QrCodeWidget('https://brewery.giacofei.org/{path}'.format(**obj))
|
||||||
|
else:
|
||||||
|
qrw = QrCodeWidget('https://{host}/{path}'.format(**obj))
|
||||||
|
|
||||||
|
num_lines = max(len(obj['data']) + 1, 6)
|
||||||
|
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])
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Title
|
||||||
|
label.add(shapes.String(
|
||||||
|
height, # Left Position
|
||||||
|
line_pos * 1.05, # Bottom Position
|
||||||
|
'{title}'.format(**obj), # Text
|
||||||
|
fontName="Helvetica",
|
||||||
|
fontSize=font
|
||||||
|
))
|
||||||
|
|
||||||
|
for x, line in enumerate(obj['data']):
|
||||||
|
x=x+1
|
||||||
|
label.add(shapes.String(
|
||||||
|
height, # Left Position
|
||||||
|
line_pos - font * x, # Bottom Position
|
||||||
|
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 sheet.
|
||||||
|
# if render_type == 'sample':
|
||||||
|
# sheet = labels.Sheet(self.specs, self.draw_sample, 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})
|
||||||
|
|
||||||
|
for entry in objects:
|
||||||
|
sheet.add_label(entry)
|
||||||
|
|
||||||
|
# Save the file and we are done.
|
||||||
|
sheet.save(render_file)
|
127
config/settings.py
Normal file
127
config/settings.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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:
|
||||||
|
secrets = json.load(f)
|
||||||
|
|
||||||
|
TIME_ZONE = 'America/New_York'
|
||||||
|
|
||||||
|
SECRET_KEY = secrets.get('SECRET_KEY',"default_secret")
|
||||||
|
|
||||||
|
DEBUG = secrets.get('DEBUG', True)
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'brewery.sqlite')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = secrets.get('ALLOWED_HOSTS', ['localhost', '127.0.0.1'])
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.admindocs',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.flatpages',
|
||||||
|
'mathfilters',
|
||||||
|
'yeast.apps.YeastLabConfig',
|
||||||
|
'beer.apps.BeerConfig',
|
||||||
|
'kegs.apps.KegConfig',
|
||||||
|
#'django.contrib.sites.apps.SitesConfig',
|
||||||
|
'django.contrib.humanize.apps.HumanizeConfig',
|
||||||
|
'django_nyt.apps.DjangoNytConfig',
|
||||||
|
'mptt',
|
||||||
|
'sekizai',
|
||||||
|
'sorl.thumbnail',
|
||||||
|
'wiki.apps.WikiConfig',
|
||||||
|
'wiki.plugins.attachments.apps.AttachmentsConfig',
|
||||||
|
'wiki.plugins.notifications.apps.NotificationsConfig',
|
||||||
|
'wiki.plugins.images.apps.ImagesConfig',
|
||||||
|
'wiki.plugins.macros.apps.MacrosConfig',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
# 'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
# 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
|
||||||
|
# 'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
# 'django.middleware.security.SecurityMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
MEDIA_ROOT = '/tmp/media/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
STATIC_ROOT = '/tmp/static/'
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [
|
||||||
|
os.path.join(BASE_DIR,'templates'),
|
||||||
|
],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
# Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
|
||||||
|
# list if you haven't customized them:
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.i18n',
|
||||||
|
'django.template.context_processors.media',
|
||||||
|
'django.template.context_processors.static',
|
||||||
|
'django.template.context_processors.tz',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
'sekizai.context_processors.sekizai', # WIKI Stuff
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WIKI_ACCOUNT_HANDLING = True
|
||||||
|
WIKI_ACCOUNT_SIGNUP_ALLOWED = True
|
||||||
|
DEFAULT_AUTO_FIELD='django.db.models.AutoField'
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
LOGIN_REDIRECT_URL = reverse_lazy('wiki:get', kwargs={'path': ''})
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {"class": "logging.StreamHandler"},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
# make all loggers use the console.
|
||||||
|
for logger in LOGGING['loggers']:
|
||||||
|
LOGGING['loggers'][logger]['handlers'] = ['console']
|
21
config/urls.py
Normal file
21
config/urls.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.flatpages import views
|
||||||
|
from .views import home
|
||||||
|
|
||||||
|
#from django.contrib.sites.models import Site
|
||||||
|
|
||||||
|
#admin.site.unregister(Site)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('about/', views.flatpage, {'url': '/about/'}, name='about'),
|
||||||
|
path("pages/", include('django.contrib.flatpages.urls')),
|
||||||
|
path('yeast/', include(('yeast.urls', 'yeast'))),
|
||||||
|
path('beer/', include(('beer.urls', 'beer'))),
|
||||||
|
path('kegs/', include(('kegs.urls', 'kegs'))),
|
||||||
|
path('notifications/', include('django_nyt.urls')),
|
||||||
|
path('wiki/', include('wiki.urls')),
|
||||||
|
path('', home, name="home"),
|
||||||
|
]
|
5
config/views.py
Normal file
5
config/views.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
|
def home(request):
|
||||||
|
return render(request, 'home.html',{})
|
0
kegs/__init__.py
Normal file
0
kegs/__init__.py
Normal file
29
kegs/admin.py
Normal file
29
kegs/admin.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
from kegs.models import Keg, KegType, State
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class KegInline(admin.TabularInline):
|
||||||
|
model = Keg
|
||||||
|
extra = 4
|
||||||
|
|
||||||
|
class KegAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ('id',)
|
||||||
|
list_display = ['id', 'kegstate', 'kegtype']
|
||||||
|
list_editable = ['kegstate', 'kegtype']
|
||||||
|
|
||||||
|
class KegTypeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name','manufacturer','size_gal',]
|
||||||
|
inlines = [
|
||||||
|
KegInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
class StateAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name']
|
||||||
|
|
||||||
|
admin.site.register(Keg, KegAdmin)
|
||||||
|
admin.site.register(KegType, KegTypeAdmin)
|
||||||
|
admin.site.register(State, StateAdmin)
|
6
kegs/apps.py
Normal file
6
kegs/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class KegConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'kegs'
|
73
kegs/migrations/0001_initial.py
Normal file
73
kegs/migrations/0001_initial.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 17:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BatchTransactions',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='KegType',
|
||||||
|
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=100)),
|
||||||
|
('manufacturer', models.CharField(blank=True, max_length=100)),
|
||||||
|
('size_gal', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='State',
|
||||||
|
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=100)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TransactionType',
|
||||||
|
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=100)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Keg',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('kegtype', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kegs.kegtype')),
|
||||||
|
('kegstate', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='kegs.state')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
kegs/migrations/__init__.py
Normal file
0
kegs/migrations/__init__.py
Normal file
38
kegs/models.py
Normal file
38
kegs/models.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class State(CustomModel):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class KegType(CustomModel):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
manufacturer = models.CharField(max_length=100, blank=True)
|
||||||
|
size_gal = models.DecimalField(max_digits=6, decimal_places=2)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Keg(CustomModel):
|
||||||
|
kegtype = models.ForeignKey(KegType, on_delete=models.PROTECT)
|
||||||
|
kegstate = models.ForeignKey(State, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
class TransactionType(CustomModel):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class BatchTransactions(CustomModel):
|
||||||
|
models.ForeignKey(Keg, on_delete=models.CASCADE)
|
||||||
|
models.ForeignKey(TransactionType, on_delete=models.PROTECT)
|
10
kegs/urls.py
Normal file
10
kegs/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .views import keg_labels, home, KegListView, keg
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('labels/', keg_labels, name='labels'),
|
||||||
|
path('<int:keg_id>/', keg, name='keg'),
|
||||||
|
path('', KegListView.as_view(), name='home'),
|
||||||
|
]
|
47
kegs/views.py
Normal file
47
kegs/views.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from kegs.models import Keg
|
||||||
|
from config.extras import AveryLabel
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
def home(request):
|
||||||
|
return render(request, 'kegs/home.html',{'kegs':Keg.objects.all()})
|
||||||
|
|
||||||
|
class KegListView(ListView):
|
||||||
|
model = Keg
|
||||||
|
|
||||||
|
def keg(request, keg_id):
|
||||||
|
keg = get_object_or_404(Keg, pk=keg_id)
|
||||||
|
return render(request, 'kegs/keg.html', {'keg': keg})
|
||||||
|
|
||||||
|
def keg_labels(request):
|
||||||
|
""" Create label PDF for selected kegs
|
||||||
|
"""
|
||||||
|
skip_count = request.POST.get("skip_count", "")
|
||||||
|
kegs_list = request.POST.getlist("kegs", "")
|
||||||
|
to_print = list(filter(lambda d: str(d.id) in kegs_list, Keg.objects.all()))
|
||||||
|
|
||||||
|
# Create the HttpResponse object with the appropriate PDF headers.
|
||||||
|
response = HttpResponse(content_type ='application/pdf')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename=keglabels.pdf'
|
||||||
|
labelSheet = AveryLabel(5263, debug=False)
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
for keg in to_print:
|
||||||
|
labels.append({
|
||||||
|
'id': keg.id,
|
||||||
|
'title': keg.kegtype.name,
|
||||||
|
'data': ['{} gal'.format(keg.kegtype.size_gal), 'ID: {}'.format(keg.id)],
|
||||||
|
'blank': False,
|
||||||
|
'host': request.get_host(),
|
||||||
|
'template': 'keg',
|
||||||
|
'ns': 'kegs'
|
||||||
|
})
|
||||||
|
|
||||||
|
labelSheet.render(labels, response, skip_count)
|
||||||
|
|
||||||
|
return response
|
23
manage.py
Normal file
23
manage.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError:
|
||||||
|
# The above import may fail for some other reason. Ensure that the
|
||||||
|
# issue is really that Django is missing to avoid masking other
|
||||||
|
# exceptions on Python 2.
|
||||||
|
try:
|
||||||
|
import django
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
execute_from_command_line(sys.argv)
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Django==5.0.6
|
||||||
|
dj-database-url==2.1.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
environs==11.0.0
|
||||||
|
docutils==0.21.2
|
||||||
|
django-mathfilters==1.0.0
|
||||||
|
pylabels==1.2.1
|
||||||
|
reportlab==4.2.0
|
3
run.sh
Normal file
3
run.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
python manage.py runserver 0.0.0.0:9595
|
74
templates/base.html
Normal file
74
templates/base.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- CSS only -->
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<!-- JS, Popper.js, and jQuery -->
|
||||||
|
<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/popper.js@1.16.0/dist/umd/popper.min.js"
|
||||||
|
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
|
||||||
|
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||||
|
<a class="navbar-brand" href="/">Damn Yankee Brewing</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
|
||||||
|
aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'yeast:batches' %}">Batches</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true"
|
||||||
|
aria-expanded="false">Operations</a>
|
||||||
|
<div class="dropdown-menu" aria-labelledby="dropdown01">
|
||||||
|
<a class="dropdown-item" href="/yeast/">Yeast</a>
|
||||||
|
<a class="dropdown-item" href="/kegs/">Kegging</a>
|
||||||
|
<a class="dropdown-item" href="/beer/">Beer</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/wiki/">Wiki</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/about/">About</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<!-- <form class="form-inline my-2 my-lg-0">
|
||||||
|
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
|
||||||
|
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
|
||||||
|
</form> -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<footer class="text-left fixed-bottom">
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<div class="text-center p-1" style="background-color: rgba(0, 0, 0, 0.1);">
|
||||||
|
© Damn yankee Brewing 2010-{% now "Y" %}
|
||||||
|
</div>
|
||||||
|
<!-- Copyright -->
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
25
templates/flatpages/default.html
Normal file
25
templates/flatpages/default.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ flatpage.title }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">{{ flatpage.title }}</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{{ flatpage.content }}
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
43
templates/home.html
Normal file
43
templates/home.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %} Home {% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Brewing Tools</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- Example row of columns -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Yeast Lab</h2>
|
||||||
|
<p>All that fun sciency stuff. </p>
|
||||||
|
<p><a class="btn btn-secondary" href="/yeast/" role="button">Go »</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Kegs</h2>
|
||||||
|
<p>Maintenance stuff. </p>
|
||||||
|
<p><a class="btn btn-secondary" href="/kegs/" role="button">Go »</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Recipes</h2>
|
||||||
|
<p>My own personal Brewfather?</p>
|
||||||
|
<p><a class="btn btn-secondary" href="/beer/" role="button">Go »</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
31
templates/kegs/keg.html
Normal file
31
templates/kegs/keg.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% block title %}Keg Information{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Keg: {{ keg.id }}</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h3>Attributes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Type: {{ keg.kegtype.name }}</li>
|
||||||
|
<li>Size: {{ keg.kegtype.size_gal }} Gallons</li>
|
||||||
|
<li>Status: {{ keg.kegstate.name }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
44
templates/kegs/keg_list.html
Normal file
44
templates/kegs/keg_list.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% block title %}Kegging Dashboard{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Kegging Dashboard</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
<div class="container">
|
||||||
|
<form action="{% url 'kegs:labels' %}" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Existing Kegs</legend>
|
||||||
|
<ul>
|
||||||
|
{% for keg in object_list %}
|
||||||
|
<li>
|
||||||
|
<label for="{{ keg.id }}">
|
||||||
|
<input type="checkbox" id="{{ keg.id }}" name="kegs" value="{{ keg.id }}"/>
|
||||||
|
<a href="{% url 'kegs:keg' keg.id %}">{{ keg.kegtype.name }} ID: {{ keg.id }}</a> Currently: {{ keg.kegstate.name }}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
<p><p>
|
||||||
|
<h1>Print Labels for selected kegs</h1>
|
||||||
|
<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">
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Print Labels">
|
||||||
|
</form>
|
||||||
|
</div> <!-- /container -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
56
templates/yeast/batch.html
Normal file
56
templates/yeast/batch.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% block title %}Sample Batch{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Batch Number: {{ batch.id }}</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1>{{ batch.strain.name }}</h1>
|
||||||
|
<b>Source: {{ batch.get_source_display }}</b>
|
||||||
|
<p><p>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
{% url 'yeast:labels' batch.id %}
|
||||||
|
<div class="container">
|
||||||
|
<form action="{% url 'yeast:labels' batch.id %}" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Batch Samples</legend>
|
||||||
|
<ul>
|
||||||
|
{% for sample in batch.yeast_set.all %}
|
||||||
|
<li>
|
||||||
|
<label for="{{ sample.id }}">
|
||||||
|
{% if sample.pitched %}
|
||||||
|
<input type="checkbox" id="{{ sample.id }}" name="samples" value="{{ sample.id }}"/>
|
||||||
|
<strike>
|
||||||
|
<a href="{% url 'yeast:yeast' sample.id %}">{{ sample.id }}: Sample no longer available</a>
|
||||||
|
</strike>
|
||||||
|
{% else %}
|
||||||
|
<input type="checkbox" id="{{ sample.id }}" name="samples" value="{{ sample.id }}" checked/>
|
||||||
|
<a href="{% url 'yeast:yeast' sample.id %}">{{ sample.id }}: {{ sample.packaging_date }}</a> {{ sample.age }} days old with estimated {{ sample.viability|mul:100|floatformat:1 }}% viability
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
<p><p>
|
||||||
|
<h1>Print Labels for this Batch</h1>
|
||||||
|
<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">
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Print selected sample labels">
|
||||||
|
</form>
|
||||||
|
</div> <!-- /container -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
23
templates/yeast/batch_form.html
Normal file
23
templates/yeast/batch_form.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<h1>Add New Batch</h1>
|
||||||
|
|
||||||
|
<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> -->
|
40
templates/yeast/batch_list.html
Normal file
40
templates/yeast/batch_list.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% block title %}Sample Batches{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Yeast Sample Batches</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<ul>
|
||||||
|
{% for batch in object_list %}
|
||||||
|
{% if batch.remaining_samples %}
|
||||||
|
<li><a href="{% url 'yeast:batch' batch.id %}">{{ batch }}</a></li>
|
||||||
|
<ul>
|
||||||
|
{% for sample in batch.yeast_set.all %}
|
||||||
|
{% if sample.pitched %}<strike>{% endif %}
|
||||||
|
<li><a href="{% url 'yeast:yeast' sample.id %}">Sample #{{ sample.id }}</a> Age: {{ sample.age }} days, Viability: {{ sample.viability|mul:100|floatformat:1 }}%</li>
|
||||||
|
{% if sample.pitched %}</strike>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p><p>
|
||||||
|
Test
|
||||||
|
<a href="{% url 'yeast:addbatch' %}">Add Batch</a>
|
||||||
|
<a href="{% url 'yeast:addstrain' %}">Add Yeast Strain</a>
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
30
templates/yeast/sample.html
Normal file
30
templates/yeast/sample.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load mathfilters %}
|
||||||
|
{% block title %}Yeast Samples{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Yeast Sample: {{ sample.id }}</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h3>{{ batch.strain.name }}</h3>
|
||||||
|
Batch Source: {{ batch.source }}
|
||||||
|
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
{% for sample in batch.remaining_samples %}
|
||||||
|
<ul>
|
||||||
|
<li><a href="/samples/{{ sample.id }}">{{ sample.packaging_date }}</a> Strain: {{ sample.age }} days, Viability:{{ sample.viability|mul:100|floatformat:1 }}%</li>
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
23
templates/yeast/strain_form.html
Normal file
23
templates/yeast/strain_form.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<h1>Add New Strain</h1>
|
||||||
|
|
||||||
|
<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> -->
|
25
templates/yeast/yeast_list.html
Normal file
25
templates/yeast/yeast_list.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Yeast Samples{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-3">Yeast Samples</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<ul>
|
||||||
|
{% for sample in object_list %}
|
||||||
|
<li>{{ sample.batch }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -0,0 +1,73 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-23 13:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
#replaces = [('yeast', '0001_initial'), ('yeast', '0002_remove_yeast_strain_batch_source'), ('yeast', '0003_alter_yeast_parent_id'), ('yeast', '0004_alter_yeast_parent_id'), ('yeast', '0005_alter_yeast_parent_id'), ('yeast', '0006_remove_yeast_parent_id_yeast_parent'), ('yeast', '0007_batch_created_date_manufacturer_created_date_and_more'), ('yeast', '0008_yeast_notes'), ('yeast', '0009_manufacturer_website_yeast_data_web_and_more'), ('yeast', '0010_alter_manufacturer_website_alter_yeast_data_web'), ('yeast', '0011_remove_yeast_data_web_batch_data_web_and_more')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Manufacturer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('website', models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Storage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('viability_loss', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||||
|
('viability_interval', models.IntegerField(default=30)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Strain',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.manufacturer')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Yeast',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('generation_num', models.IntegerField(default=0)),
|
||||||
|
('cellcount', models.IntegerField(default=100)),
|
||||||
|
('pitched', models.BooleanField(default=False)),
|
||||||
|
('purchase_date', models.DateField(default=django.utils.timezone.now)),
|
||||||
|
('packaging_date', models.DateField(default=django.utils.timezone.now)),
|
||||||
|
('storage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.storage')),
|
||||||
|
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yeast.yeast')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('notes', models.CharField(blank=True, max_length=500, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Batch',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('production_date', models.DateField()),
|
||||||
|
('strain', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='yeast.strain')),
|
||||||
|
('sample', models.ManyToManyField(to='yeast.yeast')),
|
||||||
|
('source', models.CharField(choices=[('ST', 'Purchased'), ('PR', 'Propogated'), ('SL', 'Fermenter Slurry')], default='ST', max_length=3)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('data_web', models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
23
yeast/New folder/0012_remove_batch_sample_yeast_batch.py
Normal file
23
yeast/New folder/0012_remove_batch_sample_yeast_batch.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-23 13:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='batch',
|
||||||
|
name='sample',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='batch',
|
||||||
|
field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='yeast.batch'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-23 15:16
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0012_remove_batch_sample_yeast_batch'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='batch',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.batch'),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='parent',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='parent',
|
||||||
|
field=models.ManyToManyField(to='yeast.yeast'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-23 15:35
|
||||||
|
|
||||||
|
import django.db.models.manager
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0013_alter_yeast_batch_remove_yeast_parent_yeast_parent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='yeast',
|
||||||
|
managers=[
|
||||||
|
('avail', django.db.models.manager.Manager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='parent',
|
||||||
|
field=models.ManyToManyField(blank=True, to='yeast.yeast'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0014_alter_yeast_managers_alter_yeast_parent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelManagers(
|
||||||
|
name='yeast',
|
||||||
|
managers=[
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='parent',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='parent',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='+', to='yeast.yeast'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(choices=[('ST', 'Store'), ('PR', 'Propogated'), ('SL', 'Slurry')], default='ST', max_length=3),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0015_alter_yeast_managers_remove_yeast_parent_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='batch',
|
||||||
|
name='data_web',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='data_web',
|
||||||
|
field=models.URLField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
17
yeast/New folder/0017_remove_yeast_packaging_date.py
Normal file
17
yeast/New folder/0017_remove_yeast_packaging_date.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0016_remove_batch_data_web_yeast_data_web'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='packaging_date',
|
||||||
|
),
|
||||||
|
]
|
17
yeast/New folder/0018_remove_yeast_purchase_date.py
Normal file
17
yeast/New folder/0018_remove_yeast_purchase_date.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0017_remove_yeast_packaging_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='purchase_date',
|
||||||
|
),
|
||||||
|
]
|
18
yeast/New folder/0019_yeast_lot_number.py
Normal file
18
yeast/New folder/0019_yeast_lot_number.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0018_remove_yeast_purchase_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='lot_number',
|
||||||
|
field=models.CharField(blank=True, max_length=30, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 17:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0019_yeast_lot_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='date_pitched',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='lot_number',
|
||||||
|
field=models.CharField(blank=True, max_length=15, null=True),
|
||||||
|
),
|
||||||
|
]
|
31
yeast/New folder/0021_beerbatch_yeast_pitched_batch.py
Normal file
31
yeast/New folder/0021_beerbatch_yeast_pitched_batch.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 18:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0020_yeast_date_pitched_alter_yeast_lot_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='BeerBatch',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('brewfather_id', models.CharField(max_length=50)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='pitched_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yeast.beerbatch'),
|
||||||
|
),
|
||||||
|
]
|
19
yeast/New folder/0022_batch_source_batch.py
Normal file
19
yeast/New folder/0022_batch_source_batch.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 18:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0021_beerbatch_yeast_pitched_batch'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yeast.beerbatch'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 19:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0022_batch_source_batch'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='beerbatch',
|
||||||
|
name='brewfather_name',
|
||||||
|
field=models.CharField(default='name', max_length=500),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='beerbatch',
|
||||||
|
name='brewfather_num',
|
||||||
|
field=models.IntegerField(default=1),
|
||||||
|
),
|
||||||
|
]
|
18
yeast/New folder/0024_strain_long_name.py
Normal file
18
yeast/New folder/0024_strain_long_name.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-29 20:17
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0023_beerbatch_brewfather_name_beerbatch_brewfather_num'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='strain',
|
||||||
|
name='long_name',
|
||||||
|
field=models.CharField(blank=True, max_length=100),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 12:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('beer', '0001_initial'),
|
||||||
|
('yeast', '0024_strain_long_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='pitched_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='beer.batch'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='beer.batch'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='yeast',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='BeerBatch',
|
||||||
|
),
|
||||||
|
]
|
18
yeast/New folder/0026_batch_notes.py
Normal file
18
yeast/New folder/0026_batch_notes.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 13:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('yeast', '0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
0
yeast/__init__.py
Normal file
0
yeast/__init__.py
Normal file
128
yeast/admin.py
Normal file
128
yeast/admin.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from yeast.models import Yeast, Strain, Manufacturer, Storage, Batch
|
||||||
|
from yeast.forms import YeastModelForm
|
||||||
|
|
||||||
|
import beer
|
||||||
|
|
||||||
|
from config.extras import BREWFATHER_APP_ROOT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class BatchInline(admin.TabularInline):
|
||||||
|
model = Batch
|
||||||
|
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'
|
||||||
|
model = Batch.parent.through
|
||||||
|
|
||||||
|
class YeastAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [ 'batch', '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)
|
||||||
|
|
||||||
|
def url(self, obj):
|
||||||
|
if obj.data_web:
|
||||||
|
return format_html("<a href='{url}'>{url}</a>", url=obj.data_web)
|
||||||
|
|
||||||
|
class StrainAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'long_name', 'manufacturer', 'avilable_batches']
|
||||||
|
inlines = [
|
||||||
|
BatchInline,
|
||||||
|
]
|
||||||
|
list_editable = ['long_name', 'manufacturer']
|
||||||
|
|
||||||
|
def avilable_batches(self, obj):
|
||||||
|
related_objs = [x for x in obj.batch_set.all() if not x.consumed]
|
||||||
|
|
||||||
|
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
|
||||||
|
urls.append('<a href="{}">{}</a>'.format(url, url_text))
|
||||||
|
return format_html(', '.join(urls))
|
||||||
|
|
||||||
|
avilable_batches.short_description = 'Available Batches'
|
||||||
|
|
||||||
|
class StorageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'viability_loss', 'viability_interval']
|
||||||
|
inlines = [
|
||||||
|
SampleInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
class ManufacturerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['name', 'url']
|
||||||
|
inlines = [
|
||||||
|
StrainInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
def url(self, obj):
|
||||||
|
if obj.website:
|
||||||
|
return format_html("<a href='{url}'>{url}</a>", url=obj.website)
|
||||||
|
|
||||||
|
|
||||||
|
class BatchAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['strain', 'consumed', 'source', 'parent_samples', 'production_date', 'avilable_samples', 'used_samples']
|
||||||
|
form = YeastModelForm
|
||||||
|
filter_horizontal = ['parent']
|
||||||
|
inlines = [
|
||||||
|
#ParentInline,
|
||||||
|
SampleInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
def save_related(self, request, form, formsets, change):
|
||||||
|
super(BatchAdmin, 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:
|
||||||
|
logger.critical(sample)
|
||||||
|
form.instance.parent.add(sample)
|
||||||
|
|
||||||
|
def parent_samples(self, obj):
|
||||||
|
obj.parent.all()
|
||||||
|
|
||||||
|
def used_samples(self, obj):
|
||||||
|
related_objs = list(set(obj.yeast_set.all()) - set(obj.remaining_samples))
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
for related_obj in related_objs:
|
||||||
|
url = reverse('admin:yeast_yeast_change', args=[related_obj.id])
|
||||||
|
urls.append('<a href="{}">{}</a>'.format(url, related_obj))
|
||||||
|
return format_html(', '.join(urls))
|
||||||
|
|
||||||
|
used_samples.short_description = 'Used Samples'
|
||||||
|
|
||||||
|
def avilable_samples(self, obj):
|
||||||
|
related_objs = obj.remaining_samples
|
||||||
|
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
for related_obj in related_objs:
|
||||||
|
url = reverse('admin:yeast_yeast_change', args=[related_obj.id]) # replace 'myapp' with your app name
|
||||||
|
urls.append('<a href="{}">{}</a>'.format(url, related_obj))
|
||||||
|
return format_html(', '.join(urls))
|
||||||
|
|
||||||
|
avilable_samples.short_description = 'Available Samples'
|
||||||
|
|
||||||
|
admin.site.register(Yeast, YeastAdmin)
|
||||||
|
admin.site.register(Strain, StrainAdmin)
|
||||||
|
admin.site.register(Manufacturer, ManufacturerAdmin)
|
||||||
|
admin.site.register(Storage, StorageAdmin)
|
||||||
|
admin.site.register(Batch, BatchAdmin)
|
6
yeast/apps.py
Normal file
6
yeast/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class YeastLabConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'yeast'
|
60
yeast/forms.py
Normal file
60
yeast/forms.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from .models import Yeast, Batch, Strain
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class DateInput(forms.DateInput):
|
||||||
|
input_type = 'date'
|
||||||
|
|
||||||
|
class YeastModelForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Yeast
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
forms.ModelForm.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['parent'].queryset = Yeast.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
# creating a form
|
||||||
|
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'))
|
||||||
|
|
||||||
|
# create meta class
|
||||||
|
class Meta:
|
||||||
|
# specify model to be used
|
||||||
|
model = Batch
|
||||||
|
|
||||||
|
# specify fields to be used
|
||||||
|
fields = [
|
||||||
|
'production_date',
|
||||||
|
'strain',
|
||||||
|
'source',
|
||||||
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'production_date': DateInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
num_samples = forms.IntegerField()
|
||||||
|
|
||||||
|
class StrainAddForm(forms.ModelForm):
|
||||||
|
|
||||||
|
# create meta class
|
||||||
|
class Meta:
|
||||||
|
# specify model to be used
|
||||||
|
model = Strain
|
||||||
|
|
||||||
|
# specify fields to be used
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'long_name',
|
||||||
|
'manufacturer',
|
||||||
|
]
|
100
yeast/migrations/0001_squashed_0026_batch_notes.py
Normal file
100
yeast/migrations/0001_squashed_0026_batch_notes.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 13:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [('yeast', '0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more'), ('yeast', '0012_remove_batch_sample_yeast_batch'), ('yeast', '0013_alter_yeast_batch_remove_yeast_parent_yeast_parent'), ('yeast', '0014_alter_yeast_managers_alter_yeast_parent'), ('yeast', '0015_alter_yeast_managers_remove_yeast_parent_and_more'), ('yeast', '0016_remove_batch_data_web_yeast_data_web'), ('yeast', '0017_remove_yeast_packaging_date'), ('yeast', '0018_remove_yeast_purchase_date'), ('yeast', '0019_yeast_lot_number'), ('yeast', '0020_yeast_date_pitched_alter_yeast_lot_number'), ('yeast', '0021_beerbatch_yeast_pitched_batch'), ('yeast', '0022_batch_source_batch'), ('yeast', '0023_beerbatch_brewfather_name_beerbatch_brewfather_num'), ('yeast', '0024_strain_long_name'), ('yeast', '0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more'), ('yeast', '0026_batch_notes')]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('beer', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Manufacturer',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('website', models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Storage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('viability_loss', models.DecimalField(decimal_places=4, max_digits=6)),
|
||||||
|
('viability_interval', models.IntegerField(default=30)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Strain',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('manufacturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.manufacturer')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('long_name', models.CharField(blank=True, max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Batch',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('production_date', models.DateField()),
|
||||||
|
('strain', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='yeast.strain')),
|
||||||
|
('source', models.CharField(choices=[('ST', 'Purchased'), ('PR', 'Propogated'), ('SL', 'Fermenter Slurry')], default='ST', max_length=3)),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('data_web', models.URLField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Yeast',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('generation_num', models.IntegerField(default=0)),
|
||||||
|
('cellcount', models.IntegerField(default=100)),
|
||||||
|
('pitched', models.BooleanField(default=False)),
|
||||||
|
('storage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.storage')),
|
||||||
|
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
('notes', models.TextField(blank=True, max_length=500, null=True)),
|
||||||
|
('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.batch')),
|
||||||
|
('data_web', models.URLField(blank=True, null=True)),
|
||||||
|
('lot_number', models.CharField(blank=True, max_length=15, null=True)),
|
||||||
|
('date_pitched', models.DateField(blank=True, null=True)),
|
||||||
|
('pitched_batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='beer.batch')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='parent',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='+', to='yeast.yeast'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source',
|
||||||
|
field=models.CharField(choices=[('ST', 'Store'), ('PR', 'Propogated'), ('SL', 'Slurry')], default='ST', max_length=3),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='batch',
|
||||||
|
name='data_web',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='beer.batch'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='batch',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-30 17:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('beer', '0003_batch_recipe'),
|
||||||
|
('yeast', '0001_squashed_0026_batch_notes'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='batch',
|
||||||
|
name='source_batch',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.batch'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='batch',
|
||||||
|
name='strain',
|
||||||
|
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.PROTECT, to='yeast.strain'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='strain',
|
||||||
|
name='manufacturer',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='yeast.manufacturer'),
|
||||||
|
),
|
||||||
|
]
|
0
yeast/migrations/__init__.py
Normal file
0
yeast/migrations/__init__.py
Normal file
152
yeast/models.py
Normal file
152
yeast/models.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
|
||||||
|
class AvailableYeastManager(models.Manager):
|
||||||
|
""" Special manager for filtering out pitched yeast."""
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(AvailableYeastManager, self).get_queryset().filter(pitched=False)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomModel(models.Model):
|
||||||
|
""" Custom model class with default fields to use. """
|
||||||
|
created_date = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class Manufacturer(CustomModel):
|
||||||
|
""" Store manufacturer data for various yeast strains."""
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
website = models.URLField(max_length=200, blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Return a string that represents the instance
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Strain(CustomModel):
|
||||||
|
"""
|
||||||
|
Store individual yeast strain data. :model:`yeast.Manufacturer`.
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
long_name = models.CharField(max_length=100, blank=True)
|
||||||
|
manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
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
|
||||||
|
in :model:`yeast.Yeast`.
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
viability_loss = models.DecimalField(max_digits=6, decimal_places=4)
|
||||||
|
viability_interval = models.IntegerField(default=30)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Return a string that represents the instance
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Batch(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',
|
||||||
|
'SL': 'Slurry',
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = models.ManyToManyField('Yeast', related_name='+', blank=True)
|
||||||
|
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)
|
||||||
|
notes = models.TextField(max_length=500, blank=True, null=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super(Batch, 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:
|
||||||
|
logger.critical(sample)
|
||||||
|
self.parent.add(sample)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def consumed(self):
|
||||||
|
return not len(self.remaining_samples) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def remaining_samples(self):
|
||||||
|
return [x for x in Yeast.available.all() if x.batch==self]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Return a string that represents the instance
|
||||||
|
return '{} [{}]'.format(self.strain, self.production_date.strftime("%Y-%m-%d"))
|
||||||
|
|
||||||
|
|
||||||
|
class Yeast(CustomModel):
|
||||||
|
"""
|
||||||
|
Store an individual sample of yeast.
|
||||||
|
"""
|
||||||
|
batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
|
||||||
|
generation_num = models.IntegerField(default=0)
|
||||||
|
storage = models.ForeignKey(Storage, on_delete=models.CASCADE)
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
objects = models.Manager()
|
||||||
|
available = AvailableYeastManager()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return '{} {}'.format(self.id, self.batch.strain.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def age(self):
|
||||||
|
"""Return the age in days since the sample was propogated."""
|
||||||
|
|
||||||
|
if self.pitched:
|
||||||
|
end_date = self.date_pitched
|
||||||
|
else:
|
||||||
|
end_date = timezone.now().date()
|
||||||
|
|
||||||
|
return abs((self.batch.production_date-end_date).days)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
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)
|
3
yeast/tests.py
Normal file
3
yeast/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
18
yeast/urls.py
Normal file
18
yeast/urls.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.flatpages import views
|
||||||
|
|
||||||
|
from yeast.views import YeastListView, BatchListView, home, sample, batch, batch_labels, addBatch, addStrain
|
||||||
|
|
||||||
|
# app_name = 'yeast'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
path('samples/<int:yeast_id>/', sample, name='yeast'),
|
||||||
|
path('samples/', YeastListView.as_view(), name='yeasts'),
|
||||||
|
path('batches/addbatch/', addBatch.as_view(), name='addbatch'),
|
||||||
|
path('batches/addstrain/', addStrain.as_view(), name='addstrain'),
|
||||||
|
path('batches/<int:batch_id>/', batch, name='batch'),
|
||||||
|
path('batches/', BatchListView.as_view(), name='batches'),
|
||||||
|
path('batch_labels/<int:batch_id>/', batch_labels, name='labels'),
|
||||||
|
]
|
89
yeast/views.py
Normal file
89
yeast/views.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.views.generic import ListView
|
||||||
|
from django.views.generic.edit import CreateView
|
||||||
|
from django.http import HttpResponse, HttpRequest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from yeast.models import Yeast, Batch, Strain
|
||||||
|
from config.extras import AveryLabel
|
||||||
|
from yeast.forms import BatchAddForm, StrainAddForm
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('django')
|
||||||
|
|
||||||
|
class YeastListView(ListView):
|
||||||
|
model = Yeast
|
||||||
|
|
||||||
|
class BatchListView(ListView):
|
||||||
|
model = Batch
|
||||||
|
|
||||||
|
def sample(request, sample_id):
|
||||||
|
|
||||||
|
sample = get_object_or_404(Yeast, pk=sample_id)
|
||||||
|
sample_batch = get_object_or_404(Batch, pk=sample.batch_id)
|
||||||
|
return render(request, 'yeast/sample.html', {'sample': sample, 'batch':sample_batch})
|
||||||
|
|
||||||
|
def home(request):
|
||||||
|
return render(request, 'home.html',{})
|
||||||
|
|
||||||
|
def batch(request, batch_id):
|
||||||
|
batch = get_object_or_404(Batch, pk=batch_id)
|
||||||
|
return render(request, 'yeast/batch.html', {'batch': batch})
|
||||||
|
|
||||||
|
def batch_labels(request, batch_id):
|
||||||
|
""" Create label PDF for samples in a batch
|
||||||
|
"""
|
||||||
|
skip_count = request.POST.get("skip_count", "")
|
||||||
|
samples = request.POST.getlist("samples", "")
|
||||||
|
|
||||||
|
batch = get_object_or_404(Batch, 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.
|
||||||
|
response = HttpResponse(content_type ='application/pdf')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename=samplelabels.pdf'
|
||||||
|
labelSheet = AveryLabel(18294, debug=False)
|
||||||
|
|
||||||
|
logger.critical(samples)
|
||||||
|
logger.critical(to_print)
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
for sample in to_print:
|
||||||
|
# labels.append({
|
||||||
|
# 'date': sample.batch.production_date,
|
||||||
|
# 'name': sample.name,
|
||||||
|
# 'manufacturer': sample.batch.strain.manufacturer.name,
|
||||||
|
# 'id': sample.id,
|
||||||
|
# 'blank': False,
|
||||||
|
# 'host': request.get_host(),
|
||||||
|
# })
|
||||||
|
labels.append({
|
||||||
|
'id': sample.id,
|
||||||
|
'title': '{} {}'.format(sample.batch.strain.manufacturer.name, sample.name),
|
||||||
|
'data': ['ID: {}'.format(sample.id), 'Date: {}'.format(sample.batch.production_date)],
|
||||||
|
'blank': False,
|
||||||
|
'host': request.get_host(),
|
||||||
|
'template': 'yeast',
|
||||||
|
'ns': 'yeast'
|
||||||
|
})
|
||||||
|
labelSheet.render(labels, response, skip_count)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
class addBatch(CreateView):
|
||||||
|
model = Batch
|
||||||
|
form_class = BatchAddForm
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
id = self.object.id #gets id from created object
|
||||||
|
return reverse('yeast:batches', kwargs={'batch_id': id})
|
||||||
|
|
||||||
|
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')
|
16
yeast/wsgi.py
Normal file
16
yeast/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for django_sqlite project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_sqlite.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
Loading…
Reference in New Issue
Block a user