From 019f7b5b19184a0f257f08f57ff9b934b7fa8c92 Mon Sep 17 00:00:00 2001 From: Chris GIACOFEI Date: Thu, 30 May 2024 16:26:55 -0400 Subject: [PATCH] initial commit. --- README.md | 52 ++++++ beer/__init__.py | 0 beer/admin.py | 39 ++++ beer/apps.py | 6 + beer/migrations/0001_initial.py | 28 +++ beer/migrations/0002_batchrecipe_recipe.py | 36 ++++ beer/migrations/0003_batch_recipe.py | 19 ++ beer/migrations/__init__.py | 0 beer/models.py | 32 ++++ beer/urls.py | 8 + config/__init__.py | 0 config/extras.py | 166 ++++++++++++++++++ config/settings.py | 127 ++++++++++++++ config/urls.py | 21 +++ config/views.py | 5 + kegs/__init__.py | 0 kegs/admin.py | 29 +++ kegs/apps.py | 6 + kegs/migrations/0001_initial.py | 73 ++++++++ kegs/migrations/__init__.py | 0 kegs/models.py | 38 ++++ kegs/urls.py | 10 ++ kegs/views.py | 47 +++++ manage.py | 23 +++ requirements.txt | 8 + run.sh | 3 + templates/base.html | 74 ++++++++ templates/flatpages/default.html | 25 +++ templates/home.html | 43 +++++ templates/kegs/keg.html | 31 ++++ templates/kegs/keg_list.html | 44 +++++ templates/yeast/batch.html | 56 ++++++ templates/yeast/batch_form.html | 23 +++ templates/yeast/batch_list.html | 40 +++++ templates/yeast/sample.html | 30 ++++ templates/yeast/strain_form.html | 23 +++ templates/yeast/yeast_list.html | 25 +++ ..._yeast_data_web_batch_data_web_and_more.py | 73 ++++++++ .../0012_remove_batch_sample_yeast_batch.py | 23 +++ ..._batch_remove_yeast_parent_yeast_parent.py | 28 +++ ...alter_yeast_managers_alter_yeast_parent.py | 25 +++ ...t_managers_remove_yeast_parent_and_more.py | 32 ++++ ...16_remove_batch_data_web_yeast_data_web.py | 22 +++ .../0017_remove_yeast_packaging_date.py | 17 ++ .../0018_remove_yeast_purchase_date.py | 17 ++ yeast/New folder/0019_yeast_lot_number.py | 18 ++ ...ast_date_pitched_alter_yeast_lot_number.py | 23 +++ .../0021_beerbatch_yeast_pitched_batch.py | 31 ++++ yeast/New folder/0022_batch_source_batch.py | 19 ++ ...rewfather_name_beerbatch_brewfather_num.py | 23 +++ yeast/New folder/0024_strain_long_name.py | 18 ++ ...batch_alter_batch_source_batch_and_more.py | 33 ++++ yeast/New folder/0026_batch_notes.py | 18 ++ yeast/__init__.py | 0 yeast/admin.py | 128 ++++++++++++++ yeast/apps.py | 6 + yeast/forms.py | 60 +++++++ .../0001_squashed_0026_batch_notes.py | 100 +++++++++++ ...ource_batch_alter_batch_strain_and_more.py | 30 ++++ yeast/migrations/__init__.py | 0 yeast/models.py | 152 ++++++++++++++++ yeast/tests.py | 3 + yeast/urls.py | 18 ++ yeast/views.py | 89 ++++++++++ yeast/wsgi.py | 16 ++ 65 files changed, 2212 insertions(+) create mode 100644 README.md create mode 100644 beer/__init__.py create mode 100644 beer/admin.py create mode 100644 beer/apps.py create mode 100644 beer/migrations/0001_initial.py create mode 100644 beer/migrations/0002_batchrecipe_recipe.py create mode 100644 beer/migrations/0003_batch_recipe.py create mode 100644 beer/migrations/__init__.py create mode 100644 beer/models.py create mode 100644 beer/urls.py create mode 100644 config/__init__.py create mode 100644 config/extras.py create mode 100644 config/settings.py create mode 100644 config/urls.py create mode 100644 config/views.py create mode 100644 kegs/__init__.py create mode 100644 kegs/admin.py create mode 100644 kegs/apps.py create mode 100644 kegs/migrations/0001_initial.py create mode 100644 kegs/migrations/__init__.py create mode 100644 kegs/models.py create mode 100644 kegs/urls.py create mode 100644 kegs/views.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 run.sh create mode 100644 templates/base.html create mode 100644 templates/flatpages/default.html create mode 100644 templates/home.html create mode 100644 templates/kegs/keg.html create mode 100644 templates/kegs/keg_list.html create mode 100644 templates/yeast/batch.html create mode 100644 templates/yeast/batch_form.html create mode 100644 templates/yeast/batch_list.html create mode 100644 templates/yeast/sample.html create mode 100644 templates/yeast/strain_form.html create mode 100644 templates/yeast/yeast_list.html create mode 100644 yeast/New folder/0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more.py create mode 100644 yeast/New folder/0012_remove_batch_sample_yeast_batch.py create mode 100644 yeast/New folder/0013_alter_yeast_batch_remove_yeast_parent_yeast_parent.py create mode 100644 yeast/New folder/0014_alter_yeast_managers_alter_yeast_parent.py create mode 100644 yeast/New folder/0015_alter_yeast_managers_remove_yeast_parent_and_more.py create mode 100644 yeast/New folder/0016_remove_batch_data_web_yeast_data_web.py create mode 100644 yeast/New folder/0017_remove_yeast_packaging_date.py create mode 100644 yeast/New folder/0018_remove_yeast_purchase_date.py create mode 100644 yeast/New folder/0019_yeast_lot_number.py create mode 100644 yeast/New folder/0020_yeast_date_pitched_alter_yeast_lot_number.py create mode 100644 yeast/New folder/0021_beerbatch_yeast_pitched_batch.py create mode 100644 yeast/New folder/0022_batch_source_batch.py create mode 100644 yeast/New folder/0023_beerbatch_brewfather_name_beerbatch_brewfather_num.py create mode 100644 yeast/New folder/0024_strain_long_name.py create mode 100644 yeast/New folder/0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more.py create mode 100644 yeast/New folder/0026_batch_notes.py create mode 100644 yeast/__init__.py create mode 100644 yeast/admin.py create mode 100644 yeast/apps.py create mode 100644 yeast/forms.py create mode 100644 yeast/migrations/0001_squashed_0026_batch_notes.py create mode 100644 yeast/migrations/0027_alter_batch_source_batch_alter_batch_strain_and_more.py create mode 100644 yeast/migrations/__init__.py create mode 100644 yeast/models.py create mode 100644 yeast/tests.py create mode 100644 yeast/urls.py create mode 100644 yeast/views.py create mode 100644 yeast/wsgi.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a18733 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/beer/__init__.py b/beer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/beer/admin.py b/beer/admin.py new file mode 100644 index 0000000..1dfd778 --- /dev/null +++ b/beer/admin.py @@ -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 = "Brewfather Batch ID: {batch_id}" + + def batch_url(self, obj): + bf_id = obj.brewfather_id + return format_html("Brewfather App: {batch_id}", batch_id=bf_id, root=BREWFATHER_APP_ROOT) + + +admin.site.register(Batch, BeerBatchAdmin) +admin.site.register(Recipe, RecipeAdmin) +admin.site.register(BatchRecipe, BatchRecipeAdmin) \ No newline at end of file diff --git a/beer/apps.py b/beer/apps.py new file mode 100644 index 0000000..5cf0e16 --- /dev/null +++ b/beer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BeerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'beer' diff --git a/beer/migrations/0001_initial.py b/beer/migrations/0001_initial.py new file mode 100644 index 0000000..8b0ce67 --- /dev/null +++ b/beer/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/beer/migrations/0002_batchrecipe_recipe.py b/beer/migrations/0002_batchrecipe_recipe.py new file mode 100644 index 0000000..86a6f32 --- /dev/null +++ b/beer/migrations/0002_batchrecipe_recipe.py @@ -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, + }, + ), + ] diff --git a/beer/migrations/0003_batch_recipe.py b/beer/migrations/0003_batch_recipe.py new file mode 100644 index 0000000..4722756 --- /dev/null +++ b/beer/migrations/0003_batch_recipe.py @@ -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'), + ), + ] diff --git a/beer/migrations/__init__.py b/beer/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/beer/models.py b/beer/models.py new file mode 100644 index 0000000..4507e1e --- /dev/null +++ b/beer/models.py @@ -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) diff --git a/beer/urls.py b/beer/urls.py new file mode 100644 index 0000000..b6f24d4 --- /dev/null +++ b/beer/urls.py @@ -0,0 +1,8 @@ +from django.urls import include, path +from django.contrib import admin +from django.contrib.flatpages import views + + +urlpatterns = [ + +] diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/extras.py b/config/extras.py new file mode 100644 index 0000000..f922c9b --- /dev/null +++ b/config/extras.py @@ -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) diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..eef5458 --- /dev/null +++ b/config/settings.py @@ -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'] diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..073ac4d --- /dev/null +++ b/config/urls.py @@ -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"), +] diff --git a/config/views.py b/config/views.py new file mode 100644 index 0000000..352303a --- /dev/null +++ b/config/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + + +def home(request): + return render(request, 'home.html',{}) diff --git a/kegs/__init__.py b/kegs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kegs/admin.py b/kegs/admin.py new file mode 100644 index 0000000..2304a5f --- /dev/null +++ b/kegs/admin.py @@ -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) diff --git a/kegs/apps.py b/kegs/apps.py new file mode 100644 index 0000000..236164e --- /dev/null +++ b/kegs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class KegConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'kegs' diff --git a/kegs/migrations/0001_initial.py b/kegs/migrations/0001_initial.py new file mode 100644 index 0000000..674b475 --- /dev/null +++ b/kegs/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/kegs/migrations/__init__.py b/kegs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kegs/models.py b/kegs/models.py new file mode 100644 index 0000000..7128a40 --- /dev/null +++ b/kegs/models.py @@ -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) \ No newline at end of file diff --git a/kegs/urls.py b/kegs/urls.py new file mode 100644 index 0000000..5d681b7 --- /dev/null +++ b/kegs/urls.py @@ -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('/', keg, name='keg'), + path('', KegListView.as_view(), name='home'), +] diff --git a/kegs/views.py b/kegs/views.py new file mode 100644 index 0000000..6dcfaab --- /dev/null +++ b/kegs/views.py @@ -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 \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..4836ad2 --- /dev/null +++ b/manage.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d7581e5 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..a24d170 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +python manage.py runserver 0.0.0.0:9595 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..4ddfc31 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,74 @@ + + + + {% block title %}{% endblock %} + + + + + + + + + + + + +
+ {% block content %} + {% endblock %} +
+
+ + +
+ © Damn yankee Brewing 2010-{% now "Y" %} +
+ +
+ + + diff --git a/templates/flatpages/default.html b/templates/flatpages/default.html new file mode 100644 index 0000000..a36aad8 --- /dev/null +++ b/templates/flatpages/default.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} + +{% block title %}{{ flatpage.title }}{% endblock %} + + + +{% block content %} +
+ + +
+
+

{{ flatpage.title }}

+ +
+
+ +
+ {{ flatpage.content }} + +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..3c509b6 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% block title %} Home {% endblock %} + +{% block content %} + + +
+ + +
+
+

Brewing Tools

+
+
+ +
+ +
+
+

Yeast Lab

+

All that fun sciency stuff.

+

Go »

+
+
+

Kegs

+

Maintenance stuff.

+

Go »

+
+
+

Recipes

+

My own personal Brewfather?

+

Go »

+
+
+ +
+ +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/kegs/keg.html b/templates/kegs/keg.html new file mode 100644 index 0000000..a50df0f --- /dev/null +++ b/templates/kegs/keg.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load mathfilters %} +{% block title %}Keg Information{% endblock %} +{% block content %} +
+ + +
+
+

Keg: {{ keg.id }}

+ +
+
+ +
+ +

Attributes

+
    +
  • Type: {{ keg.kegtype.name }}
  • +
  • Size: {{ keg.kegtype.size_gal }} Gallons
  • +
  • Status: {{ keg.kegstate.name }}
  • +
+ + + +
+ + +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/kegs/keg_list.html b/templates/kegs/keg_list.html new file mode 100644 index 0000000..eea04a8 --- /dev/null +++ b/templates/kegs/keg_list.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% load mathfilters %} +{% block title %}Kegging Dashboard{% endblock %} +{% block content %} +
+ + +
+
+

Kegging Dashboard

+ +
+
+ +
+ +
+
+
+
+ Existing Kegs + +
+

+

Print Labels for selected kegs

+

+ + +

+ +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/yeast/batch.html b/templates/yeast/batch.html new file mode 100644 index 0000000..575b41b --- /dev/null +++ b/templates/yeast/batch.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load mathfilters %} +{% block title %}Sample Batch{% endblock %} +{% block content %} +
+ + +
+
+

Batch Number: {{ batch.id }}

+ +
+
+ +
+ +

{{ batch.strain.name }}

+ Source: {{ batch.get_source_display }} +

+ +

+ {% url 'yeast:labels' batch.id %} +
+
+
+ Batch Samples + +
+

+

Print Labels for this Batch

+

+ + +

+ +

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/yeast/batch_form.html b/templates/yeast/batch_form.html new file mode 100644 index 0000000..34d529e --- /dev/null +++ b/templates/yeast/batch_form.html @@ -0,0 +1,23 @@ +

Add New Batch

+ +
{% csrf_token %} + {{ form.as_p }} + +
+ + \ No newline at end of file diff --git a/templates/yeast/batch_list.html b/templates/yeast/batch_list.html new file mode 100644 index 0000000..ba50943 --- /dev/null +++ b/templates/yeast/batch_list.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% load mathfilters %} +{% block title %}Sample Batches{% endblock %} +{% block content %} +
+ + +
+
+

Yeast Sample Batches

+ +
+
+ +
+
    + {% for batch in object_list %} + {% if batch.remaining_samples %} +
  • {{ batch }}
  • +
      + {% for sample in batch.yeast_set.all %} + {% if sample.pitched %}{% endif %} +
    • Sample #{{ sample.id }} Age: {{ sample.age }} days, Viability: {{ sample.viability|mul:100|floatformat:1 }}%
    • + {% if sample.pitched %}
      {% endif %} + {% endfor %} +
    + {% endif %} + + {% endfor %} +
+

+ Test + Add Batch + Add Yeast Strain +

+ + +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/yeast/sample.html b/templates/yeast/sample.html new file mode 100644 index 0000000..fa97b94 --- /dev/null +++ b/templates/yeast/sample.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% load mathfilters %} +{% block title %}Yeast Samples{% endblock %} +{% block content %} +
+ + +
+
+

Yeast Sample: {{ sample.id }}

+ +
+
+ +
+ +

{{ batch.strain.name }}

+ Batch Source: {{ batch.source }} + + +
+ + {% for sample in batch.remaining_samples %} + + {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/yeast/strain_form.html b/templates/yeast/strain_form.html new file mode 100644 index 0000000..70827bf --- /dev/null +++ b/templates/yeast/strain_form.html @@ -0,0 +1,23 @@ +

Add New Strain

+ +
{% csrf_token %} + {{ form.as_p }} + +
+ + \ No newline at end of file diff --git a/templates/yeast/yeast_list.html b/templates/yeast/yeast_list.html new file mode 100644 index 0000000..1e7593d --- /dev/null +++ b/templates/yeast/yeast_list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}Yeast Samples{% endblock %} +{% block content %} +
+ + +
+
+

Yeast Samples

+ +
+
+ +
+
    + {% for sample in object_list %} +
  • {{ sample.batch }}
  • + {% endfor %} +
+ +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/yeast/New folder/0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more.py b/yeast/New folder/0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more.py new file mode 100644 index 0000000..fd8cef3 --- /dev/null +++ b/yeast/New folder/0001_squashed_0011_remove_yeast_data_web_batch_data_web_and_more.py @@ -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)), + ], + ), + ] diff --git a/yeast/New folder/0012_remove_batch_sample_yeast_batch.py b/yeast/New folder/0012_remove_batch_sample_yeast_batch.py new file mode 100644 index 0000000..5984d3c --- /dev/null +++ b/yeast/New folder/0012_remove_batch_sample_yeast_batch.py @@ -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'), + ), + ] diff --git a/yeast/New folder/0013_alter_yeast_batch_remove_yeast_parent_yeast_parent.py b/yeast/New folder/0013_alter_yeast_batch_remove_yeast_parent_yeast_parent.py new file mode 100644 index 0000000..23ee8e2 --- /dev/null +++ b/yeast/New folder/0013_alter_yeast_batch_remove_yeast_parent_yeast_parent.py @@ -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'), + ), + ] diff --git a/yeast/New folder/0014_alter_yeast_managers_alter_yeast_parent.py b/yeast/New folder/0014_alter_yeast_managers_alter_yeast_parent.py new file mode 100644 index 0000000..4ee0ecd --- /dev/null +++ b/yeast/New folder/0014_alter_yeast_managers_alter_yeast_parent.py @@ -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'), + ), + ] diff --git a/yeast/New folder/0015_alter_yeast_managers_remove_yeast_parent_and_more.py b/yeast/New folder/0015_alter_yeast_managers_remove_yeast_parent_and_more.py new file mode 100644 index 0000000..5258fc1 --- /dev/null +++ b/yeast/New folder/0015_alter_yeast_managers_remove_yeast_parent_and_more.py @@ -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), + ), + ] diff --git a/yeast/New folder/0016_remove_batch_data_web_yeast_data_web.py b/yeast/New folder/0016_remove_batch_data_web_yeast_data_web.py new file mode 100644 index 0000000..d6768bc --- /dev/null +++ b/yeast/New folder/0016_remove_batch_data_web_yeast_data_web.py @@ -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), + ), + ] diff --git a/yeast/New folder/0017_remove_yeast_packaging_date.py b/yeast/New folder/0017_remove_yeast_packaging_date.py new file mode 100644 index 0000000..bec95db --- /dev/null +++ b/yeast/New folder/0017_remove_yeast_packaging_date.py @@ -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', + ), + ] diff --git a/yeast/New folder/0018_remove_yeast_purchase_date.py b/yeast/New folder/0018_remove_yeast_purchase_date.py new file mode 100644 index 0000000..f96cc86 --- /dev/null +++ b/yeast/New folder/0018_remove_yeast_purchase_date.py @@ -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', + ), + ] diff --git a/yeast/New folder/0019_yeast_lot_number.py b/yeast/New folder/0019_yeast_lot_number.py new file mode 100644 index 0000000..55e5620 --- /dev/null +++ b/yeast/New folder/0019_yeast_lot_number.py @@ -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), + ), + ] diff --git a/yeast/New folder/0020_yeast_date_pitched_alter_yeast_lot_number.py b/yeast/New folder/0020_yeast_date_pitched_alter_yeast_lot_number.py new file mode 100644 index 0000000..b3e5916 --- /dev/null +++ b/yeast/New folder/0020_yeast_date_pitched_alter_yeast_lot_number.py @@ -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), + ), + ] diff --git a/yeast/New folder/0021_beerbatch_yeast_pitched_batch.py b/yeast/New folder/0021_beerbatch_yeast_pitched_batch.py new file mode 100644 index 0000000..5fdfe1c --- /dev/null +++ b/yeast/New folder/0021_beerbatch_yeast_pitched_batch.py @@ -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'), + ), + ] diff --git a/yeast/New folder/0022_batch_source_batch.py b/yeast/New folder/0022_batch_source_batch.py new file mode 100644 index 0000000..5c571a4 --- /dev/null +++ b/yeast/New folder/0022_batch_source_batch.py @@ -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'), + ), + ] diff --git a/yeast/New folder/0023_beerbatch_brewfather_name_beerbatch_brewfather_num.py b/yeast/New folder/0023_beerbatch_brewfather_name_beerbatch_brewfather_num.py new file mode 100644 index 0000000..e3afad4 --- /dev/null +++ b/yeast/New folder/0023_beerbatch_brewfather_name_beerbatch_brewfather_num.py @@ -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), + ), + ] diff --git a/yeast/New folder/0024_strain_long_name.py b/yeast/New folder/0024_strain_long_name.py new file mode 100644 index 0000000..6efa68a --- /dev/null +++ b/yeast/New folder/0024_strain_long_name.py @@ -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), + ), + ] diff --git a/yeast/New folder/0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more.py b/yeast/New folder/0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more.py new file mode 100644 index 0000000..dc2e7d6 --- /dev/null +++ b/yeast/New folder/0025_alter_yeast_pitched_batch_alter_batch_source_batch_and_more.py @@ -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', + ), + ] diff --git a/yeast/New folder/0026_batch_notes.py b/yeast/New folder/0026_batch_notes.py new file mode 100644 index 0000000..2ae00ed --- /dev/null +++ b/yeast/New folder/0026_batch_notes.py @@ -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), + ), + ] diff --git a/yeast/__init__.py b/yeast/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yeast/admin.py b/yeast/admin.py new file mode 100644 index 0000000..79a42f6 --- /dev/null +++ b/yeast/admin.py @@ -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("{batch_id}", batch_id=bf_id) + + def url(self, obj): + if obj.data_web: + return format_html("{url}", 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('{}'.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("{url}", 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('{}'.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('{}'.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) diff --git a/yeast/apps.py b/yeast/apps.py new file mode 100644 index 0000000..7b7f9cf --- /dev/null +++ b/yeast/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class YeastLabConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'yeast' diff --git a/yeast/forms.py b/yeast/forms.py new file mode 100644 index 0000000..a25ffa4 --- /dev/null +++ b/yeast/forms.py @@ -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 = 'Add a new strain'.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', + ] diff --git a/yeast/migrations/0001_squashed_0026_batch_notes.py b/yeast/migrations/0001_squashed_0026_batch_notes.py new file mode 100644 index 0000000..3c480d4 --- /dev/null +++ b/yeast/migrations/0001_squashed_0026_batch_notes.py @@ -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), + ), + ] diff --git a/yeast/migrations/0027_alter_batch_source_batch_alter_batch_strain_and_more.py b/yeast/migrations/0027_alter_batch_source_batch_alter_batch_strain_and_more.py new file mode 100644 index 0000000..4b5940f --- /dev/null +++ b/yeast/migrations/0027_alter_batch_source_batch_alter_batch_strain_and_more.py @@ -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'), + ), + ] diff --git a/yeast/migrations/__init__.py b/yeast/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yeast/models.py b/yeast/models.py new file mode 100644 index 0000000..358a9da --- /dev/null +++ b/yeast/models.py @@ -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) diff --git a/yeast/tests.py b/yeast/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/yeast/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/yeast/urls.py b/yeast/urls.py new file mode 100644 index 0000000..c3c903b --- /dev/null +++ b/yeast/urls.py @@ -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//', 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//', batch, name='batch'), + path('batches/', BatchListView.as_view(), name='batches'), + path('batch_labels//', batch_labels, name='labels'), +] diff --git a/yeast/views.py b/yeast/views.py new file mode 100644 index 0000000..5eeea95 --- /dev/null +++ b/yeast/views.py @@ -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') \ No newline at end of file diff --git a/yeast/wsgi.py b/yeast/wsgi.py new file mode 100644 index 0000000..3c5eae3 --- /dev/null +++ b/yeast/wsgi.py @@ -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()