initial commit
This commit is contained in:
commit
f677973feb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for baby_tracker project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'baby_tracker.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
Django settings for baby_tracker project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.1.4.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-q_$lvms3r!+meum_0o&b^#vkf75+sy!kax9v^pyr@fipj6&2$a'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'main',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'baby_tracker.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'baby_tracker.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'Pacific/Honolulu'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""
|
||||||
|
URL configuration for baby_tracker project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.1/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('main.urls')),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for baby_tracker 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/5.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'baby_tracker.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,23 @@
|
||||||
|
# admin.py
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import Feeding, Potty
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PottyAdmin(admin.ModelAdmin):
|
||||||
|
# Show timestamp in the list view
|
||||||
|
list_display = ('timestamp', 'consistency', 'notes') # Add timestamp here
|
||||||
|
|
||||||
|
# Allow timestamp to be editable in the form view
|
||||||
|
fields = ('consistency', 'notes') # Add timestamp here to the edit form
|
||||||
|
|
||||||
|
admin.site.register(Potty, PottyAdmin)
|
||||||
|
|
||||||
|
class FeedingAdmin(admin.ModelAdmin):
|
||||||
|
# Show timestamp in the list view
|
||||||
|
list_display = ('timestamp', 'feeding_type', 'amount', 'duration' , 'notes') # Add timestamp here
|
||||||
|
|
||||||
|
# Allow timestamp to be editable in the form view
|
||||||
|
fields = ('feeding_type', 'amount', 'duration' , 'notes') # Add timestamp here to the edit form
|
||||||
|
|
||||||
|
admin.site.register(Feeding, FeedingAdmin)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MainConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'main'
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from django import forms
|
||||||
|
from .models import Feeding, Potty
|
||||||
|
|
||||||
|
class FeedingForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Feeding
|
||||||
|
fields = ['feeding_type', 'amount', 'duration', 'notes']
|
||||||
|
widgets = {
|
||||||
|
'feeding_type': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'amount': forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Enter amount (if bottle) in ounces'
|
||||||
|
}),
|
||||||
|
'duration': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Enter duration (if breast) in minutes'
|
||||||
|
}),
|
||||||
|
'notes': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Additional notes (optional)',
|
||||||
|
'rows': 3
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
class PottyForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Potty
|
||||||
|
fields = ['consistency', 'notes']
|
||||||
|
widgets = {
|
||||||
|
'consistency': forms.Select(attrs={'class': 'form-control'}),
|
||||||
|
'notes': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Additional notes (optional)',
|
||||||
|
'rows': 3
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 5.1.4 on 2024-12-07 10:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Feeding',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('feeding_type', models.CharField(choices=[('bottle', 'Bottle'), ('breast', 'Breast')], max_length=10)),
|
||||||
|
('amount', models.FloatField(blank=True, null=True)),
|
||||||
|
('duration', models.FloatField(blank=True, null=True)),
|
||||||
|
('notes', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Potty',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('consistency', models.CharField(choices=[('solid', 'Solid'), ('soft', 'Soft'), ('watery', 'Watery'), ('pee', 'Pee'), ('other', 'Other')], max_length=50)),
|
||||||
|
('notes', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.4 on 2024-12-07 18:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feeding',
|
||||||
|
name='amount',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.1.4 on 2024-12-07 18:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0002_alter_feeding_amount'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feeding',
|
||||||
|
name='amount',
|
||||||
|
field=models.FloatField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feeding',
|
||||||
|
name='duration',
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,34 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Feeding(models.Model):
|
||||||
|
FEEDING_TYPES = [
|
||||||
|
('bottle', 'Bottle'),
|
||||||
|
('breast', 'Breast'),
|
||||||
|
]
|
||||||
|
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
feeding_type = models.CharField(max_length=10, choices=FEEDING_TYPES)
|
||||||
|
amount = models.FloatField(null=True, blank=True) # Store ounces for bottle feed
|
||||||
|
duration = models.IntegerField(null=True, blank=True) # Store duration in minutes for breastfeed
|
||||||
|
notes = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_feeding_type_display()} - {self.timestamp}"
|
||||||
|
|
||||||
|
|
||||||
|
class Potty(models.Model):
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
consistency = models.CharField(max_length=50, choices=[
|
||||||
|
('solid', 'Solid'),
|
||||||
|
('soft', 'Soft'),
|
||||||
|
('watery', 'Watery'),
|
||||||
|
('pee', 'Pee'),
|
||||||
|
('other', 'Other'),
|
||||||
|
])
|
||||||
|
notes = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.get_potty_type_display()} - {self.timestamp}"
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Baby Tracker</title>
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<!-- HTMX -->
|
||||||
|
<script src="https://unpkg.com/htmx.org"></script>
|
||||||
|
|
||||||
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="p-3 bg-secondary text-white">
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 class="text-center">Caelum's Poop and Feed Tracker</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||||
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'main/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
{% include 'main/partials/feed_section.html' %}
|
||||||
|
{% include 'main/partials/potty_section.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<form method="post" action="{% url 'add_feed' %}" hx-post="{% url 'add_feed' %}" hx-target="#feed-section" hx-swap="outerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{ form.feeding_type.label_tag }}
|
||||||
|
{{ form.feeding_type }}
|
||||||
|
</div>
|
||||||
|
<div id="bottle-fields" class="mb-3" style="display: none;">
|
||||||
|
{{ form.amount.label_tag }}
|
||||||
|
{{ form.amount }}
|
||||||
|
</div>
|
||||||
|
<div id="breast-fields" class="mb-3" style="display: none;">
|
||||||
|
{{ form.duration.label_tag }}
|
||||||
|
{{ form.duration }}
|
||||||
|
</div>
|
||||||
|
<div id="notes-fields" class="mb-3" >
|
||||||
|
{{ form.notes.label_tag }}
|
||||||
|
{{ form.notes }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function toggleFields() {
|
||||||
|
if (document.getElementById("id_feeding_type").value === "bottle") {
|
||||||
|
document.getElementById("bottle-fields").style.display = "block";
|
||||||
|
document.getElementById("breast-fields").style.display = "none";
|
||||||
|
} else if (document.getElementById("id_feeding_type").value === "breast") {
|
||||||
|
document.getElementById("bottle-fields").style.display = "none";
|
||||||
|
document.getElementById("breast-fields").style.display = "block";
|
||||||
|
} else {
|
||||||
|
document.getElementById("bottle-fields").style.display = "none";
|
||||||
|
document.getElementById("breast-fields").style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("id_feeding_type").addEventListener("change", toggleFields);
|
||||||
|
toggleFields(); // Initial toggle
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<form method="post" action="{% url 'add_potty' %}" hx-post="{% url 'add_potty' %}" hx-target="#potty-section" hx-swap="outerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{{form.consistency.label_tag}}
|
||||||
|
{{form.consistency}}
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
{{form.notes.label_tag}}
|
||||||
|
{{form.notes}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<tr id="feeding-{{ feeding.id }}">
|
||||||
|
<td>
|
||||||
|
<input type="datetime-local" name="timestamp" value="{{ feeding.timestamp|date:'Y-m-d\TH:i' }}" class="form-control">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if feeding.feeding_type == 'bottle' %}
|
||||||
|
<input type="number" name="amount_or_duration" value="{{ feeding.amount }}"
|
||||||
|
class="form-control" placeholder="Enter amount (in oz)">
|
||||||
|
{% elif feeding.feeding_type == 'breast' %}
|
||||||
|
<input type="number" name="amount_or_duration" value="{{ feeding.duration }}"
|
||||||
|
class="form-control" placeholder="Enter duration (in min)">
|
||||||
|
{% else %}
|
||||||
|
<input type="number" name="amount_or_duration" class="form-control"
|
||||||
|
placeholder="Enter value">
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<input type="text" name="notes" value="{{ feeding.notes }}" class="form-control">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-success"
|
||||||
|
hx-post="{% url 'update_feed' feeding.id %}"
|
||||||
|
hx-target="#feeding-{{ feeding.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-include="[name='timestamp'], [name='amount_or_duration'], [name='notes']">
|
||||||
|
<i class="fas fa-save"></i> Save
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-get="{% url 'cancel_edit_feed' feeding.id %}"
|
||||||
|
hx-target="#feeding-{{ feeding.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<tr id="potty-{{ potty.id }}">
|
||||||
|
<td>
|
||||||
|
<input type="datetime-local" name="timestamp" value="{{ potty.timestamp|date:'Y-m-d\\TH:i' }}" class="form-control">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="consistency" class="form-select">
|
||||||
|
{% for value, label in form.consistency.field.choices %}
|
||||||
|
{% if potty.consistency == value %}
|
||||||
|
<option value="{{ value }}" selected>{{label}}</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="{{ value }}">{{label}}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<textarea name="notes" rows="3" class="form-control form-control-lg">{{ potty.notes }}</textarea>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-success"
|
||||||
|
hx-post="{% url 'update_potty' potty.id %}"
|
||||||
|
hx-target="#potty-{{ potty.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-include="[name='timestamp'], [name='consistency'], [name='notes']">
|
||||||
|
<i class="fas fa-save"></i> Save
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-get="{% url 'cancel_edit_potty' potty.id %}"
|
||||||
|
hx-target="#potty-{{ potty.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<tr id="feeding-{{ feeding.id }}">
|
||||||
|
<th scope="row">{{ feeding.timestamp }}</th>
|
||||||
|
<td>
|
||||||
|
{% if feeding.feeding_type == 'breast' %}
|
||||||
|
{{ feeding.duration }} min
|
||||||
|
{% else %}
|
||||||
|
{{ feeding.amount }} oz
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ feeding.notes }}</td>
|
||||||
|
<td>
|
||||||
|
<!-- Edit Button -->
|
||||||
|
<button class="btn btn-sm btn-warning"
|
||||||
|
hx-get="{% url 'edit_feed' feeding.id %}"
|
||||||
|
hx-target="#feeding-{{ feeding.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-edit"></i> <!-- Font Awesome edit icon -->
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-delete="{% url 'delete_feed' feeding.id %}"
|
||||||
|
hx-target="#feed-section"
|
||||||
|
hx-confirm="Are you sure you want to delete this feed?"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-trash"></i> <!-- Font Awesome trash icon -->
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<section id="feed-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Feed Records</h2>
|
||||||
|
<button class="btn btn-primary my-2"
|
||||||
|
hx-get="{% url 'add_feed' %}"
|
||||||
|
hx-target="#feed-form-container"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
Add Feed
|
||||||
|
</button>
|
||||||
|
<div id="feed-form-container"></div>
|
||||||
|
|
||||||
|
<div class="row table-responsive rounded overflow-y-auto" style="max-height: 40rem;">
|
||||||
|
|
||||||
|
<table class="table table-primary align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Amount or Duration</th>
|
||||||
|
<th scope="col">Notes</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for feeding in feedings %}
|
||||||
|
<tr id="feeding-{{ feeding.id }}">
|
||||||
|
<th scope="row">{{ feeding.timestamp }}</th>
|
||||||
|
<td>{% if feeding.feeding_type == 'breast' %}{{feeding.duration}} min{% else %}{{feeding.amount}} oz{% endif %}</td>
|
||||||
|
<td>{{feeding.notes}}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-warning"
|
||||||
|
hx-get="{% url 'edit_feed' feeding.id %}"
|
||||||
|
hx-target="#feeding-{{ feeding.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-edit"></i> <!-- Font Awesome edit icon -->
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-delete="{% url 'delete_feed' feeding.id %}"
|
||||||
|
hx-target="#feed-section"
|
||||||
|
hx-confirm="Are you sure you want to delete this feed?"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-trash"></i> <!-- Font Awesome trash icon -->
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'export_feedings_csv' %}">
|
||||||
|
<button class="btn btn-info my-2" >
|
||||||
|
Export Feed Data
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<tr id="potty-{{potty.id}}">
|
||||||
|
<th scope="row">{{ potty.timestamp }}</th>
|
||||||
|
<td>{{ potty.consistency }}</td>
|
||||||
|
<td>{{ potty.notes }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-warning"
|
||||||
|
hx-get="{% url 'edit_potty' potty.id %}"
|
||||||
|
hx-target="#potty-{{potty.id}}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-delete="{% url 'delete_potty' potty.id %}"
|
||||||
|
hx-target="#potty-section"
|
||||||
|
hx-confirm="Are you sure you want to delete this potty?"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-trash" ></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
<section id="potty-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Potty Records</h2>
|
||||||
|
<button class="btn btn-primary my-2" hx-get="{% url 'add_potty' %}" hx-target="#potty-form-container" hx-swap="innerHTML">Add Potty</button>
|
||||||
|
<div id="potty-form-container"></div>
|
||||||
|
<div class="row table-responsive rounded overflow-y-auto" style="max-height: 40rem;">
|
||||||
|
|
||||||
|
<table class="table table-primary align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Type</th>
|
||||||
|
<th scope="col">Notes</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for potty in potties %}
|
||||||
|
<tr id="potty-{{potty.id}}">
|
||||||
|
<th scope="row">{{ potty.timestamp }}</th>
|
||||||
|
<td>{{ potty.consistency }}</td>
|
||||||
|
<td>{{ potty.notes }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-warning"
|
||||||
|
hx-get="{% url 'edit_potty' potty.id %}"
|
||||||
|
hx-target="#potty-{{potty.id}}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
hx-delete="{% url 'delete_potty' potty.id %}"
|
||||||
|
hx-target="#potty-section"
|
||||||
|
hx-confirm="Are you sure you want to delete this potty?"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fas fa-trash" ></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a href="{% url 'export_potties_csv' %}">
|
||||||
|
<button class="btn btn-info my-2" >
|
||||||
|
Export Potty Data
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.index, name='feedings_list'),
|
||||||
|
path('add_feed', views.add_feed, name='add_feed'),
|
||||||
|
path('delete_feed/<int:feed_id>', views.delete_feed, name='delete_feed'),
|
||||||
|
path('edit_feed/<int:feed_id>', views.edit_feed, name='edit_feed'),
|
||||||
|
path('update_feed/<int:feed_id>', views.update_feed, name='update_feed'),
|
||||||
|
path('cancel_edit_feed/<int:feed_id>', views.cancel_edit_feed, name='cancel_edit_feed'),
|
||||||
|
path('add_potty', views.add_potty, name='add_potty'),
|
||||||
|
path('delete_potty/<int:potty_id>', views.delete_potty, name='delete_potty'),
|
||||||
|
path('edit_potty/<int:potty_id>', views.edit_potty, name='edit_potty'),
|
||||||
|
path('update_potty/<int:potty_id>', views.update_potty, name='update_potty'),
|
||||||
|
path('cancel_edit_potty/<int:potty_id>', views.cancel_edit_potty, name='cancel_edit_potty'),
|
||||||
|
path('export_feedings_csv', views.export_feedings_csv, name='export_feedings_csv'),
|
||||||
|
path('export_potties_csv', views.export_potties_csv, name='export_potties_csv'),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from .models import Feeding, Potty
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from .forms import FeedingForm, PottyForm
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
feedings = Feeding.objects.all().order_by('-timestamp')
|
||||||
|
|
||||||
|
context['feedings'] = feedings
|
||||||
|
|
||||||
|
# Grab all objects sorted by timestamp (most recent first)
|
||||||
|
potties = Potty.objects.all().order_by('-timestamp')
|
||||||
|
|
||||||
|
|
||||||
|
context['potties'] = potties
|
||||||
|
|
||||||
|
return render(request, 'main/index.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def add_feed(request):
|
||||||
|
context = {}
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = FeedingForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
feeding_type = form.cleaned_data['feeding_type']
|
||||||
|
amount = form.cleaned_data['amount']
|
||||||
|
duration = form.cleaned_data['duration']
|
||||||
|
# Validate based on feeding type
|
||||||
|
if feeding_type == 'bottle' and not amount:
|
||||||
|
form.add_error('amount', 'Amount is required for bottle feeding.')
|
||||||
|
elif feeding_type == 'breast' and not duration:
|
||||||
|
form.add_error('duration', 'Duration is required for breast feeding.')
|
||||||
|
else:
|
||||||
|
form.save()
|
||||||
|
feedings = Feeding.objects.all().order_by('-timestamp')
|
||||||
|
context['feedings'] = feedings
|
||||||
|
return render(request, 'main/partials/feed_section.html', context)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = FeedingForm()
|
||||||
|
|
||||||
|
return render(request, 'main/partials/add_feed.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def delete_feed(request, feed_id):
|
||||||
|
context = {}
|
||||||
|
if request.method == "DELETE":
|
||||||
|
feed = get_object_or_404(Feeding, id=int(feed_id))
|
||||||
|
feed.delete()
|
||||||
|
feedings = Feeding.objects.all().order_by('-timestamp')
|
||||||
|
context['feedings'] = feedings
|
||||||
|
return render(request, 'main/partials/feed_section.html', context) # Return a success response
|
||||||
|
return HttpResponse(status=405) # Method not allowed
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def edit_feed(request, feed_id):
|
||||||
|
feed = get_object_or_404(Feeding, id=feed_id)
|
||||||
|
return render(request, 'main/partials/edit_feed_row.html', {'feeding': feed})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def update_feed(request, feed_id):
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
feed = get_object_or_404(Feeding, id=feed_id)
|
||||||
|
feed.timestamp = request.POST['timestamp']
|
||||||
|
if feed.feeding_type == 'bottle':
|
||||||
|
feed.amount = float(request.POST['amount_or_duration'])
|
||||||
|
else:
|
||||||
|
feed.duration = int(float(request.POST['amount_or_duration']))
|
||||||
|
feed.notes = request.POST['notes']
|
||||||
|
feed.save()
|
||||||
|
return render(request, 'main/partials/feed_row.html', {'feeding': feed})
|
||||||
|
|
||||||
|
def cancel_edit_feed(request, feed_id):
|
||||||
|
feed = get_object_or_404(Feeding, id=feed_id)
|
||||||
|
return render(request, 'main/partials/feed_row.html', {'feeding': feed})
|
||||||
|
|
||||||
|
def add_potty(request):
|
||||||
|
context = {}
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = PottyForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
potties = Potty.objects.all().order_by('-timestamp')
|
||||||
|
context['potties'] = potties
|
||||||
|
return render(request, 'main/partials/potty_section.html', context)
|
||||||
|
else:
|
||||||
|
form = PottyForm()
|
||||||
|
|
||||||
|
return render(request, 'main/partials/add_potty.html', {'form': form})
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def delete_potty(request, potty_id):
|
||||||
|
context = {}
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
potty = get_object_or_404(Potty, id=int(potty_id))
|
||||||
|
potty.delete()
|
||||||
|
potties = Potty.objects.all().order_by('-timestamp')
|
||||||
|
context['potties'] = potties
|
||||||
|
return render(request, 'main/partials/potty_section.html', context)
|
||||||
|
return HttpResponse(status=405)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def edit_potty(request, potty_id):
|
||||||
|
context = {}
|
||||||
|
potty = get_object_or_404(Potty, id=potty_id)
|
||||||
|
context['potty'] = potty
|
||||||
|
form = PottyForm(potty)
|
||||||
|
context['form'] = form
|
||||||
|
return render(request, 'main/partials/edit_potty_row.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def update_potty(request, potty_id):
|
||||||
|
context = {}
|
||||||
|
if request.method == 'POST':
|
||||||
|
potty = get_object_or_404(Potty, id=potty_id)
|
||||||
|
potty.timestamp = request.POST['timestamp']
|
||||||
|
potty.consistency = request.POST['consistency']
|
||||||
|
potty.notes = request.POST['notes']
|
||||||
|
potty.save()
|
||||||
|
return render(request, 'main/partials/potty_row.html', {'potty': potty})
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_edit_potty(request, potty_id):
|
||||||
|
potty = get_object_or_404(Potty, id=potty_id)
|
||||||
|
return render(request, 'main/partials/potty_row.html', {'potty': potty})
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def export_feedings_csv(request):
|
||||||
|
# Create the HTTP response with CSV headers
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="feedings.csv"'
|
||||||
|
|
||||||
|
# Create a CSV writer
|
||||||
|
writer = csv.writer(response)
|
||||||
|
# Write the header row
|
||||||
|
writer.writerow(['Timestamp', 'Feeding Type', 'Amount', 'Duration', 'Notes'])
|
||||||
|
|
||||||
|
# Fetch the data from the database
|
||||||
|
feedings = Feeding.objects.all().order_by('-timestamp')
|
||||||
|
for feeding in feedings:
|
||||||
|
writer.writerow([
|
||||||
|
feeding.timestamp,
|
||||||
|
feeding.get_feeding_type_display(), # Use readable choices if applicable
|
||||||
|
feeding.amount if feeding.amount else "",
|
||||||
|
feeding.duration if feeding.duration else "",
|
||||||
|
feeding.notes,
|
||||||
|
])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def export_potties_csv(request):
|
||||||
|
# Create the HTTP response with CSV headers
|
||||||
|
response = HttpResponse(content_type='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="potties.csv"'
|
||||||
|
|
||||||
|
# Create a CSV writer
|
||||||
|
writer = csv.writer(response)
|
||||||
|
# Write the header row
|
||||||
|
writer.writerow(['Timestamp', 'Consistency', 'Notes'])
|
||||||
|
|
||||||
|
# Fetch the potty data from the database
|
||||||
|
potties = Potty.objects.all().order_by('-timestamp')
|
||||||
|
for potty in potties:
|
||||||
|
writer.writerow([
|
||||||
|
potty.timestamp,
|
||||||
|
potty.get_consistency_display() if potty.consistency else "",
|
||||||
|
potty.notes,
|
||||||
|
])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'baby_tracker.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
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?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
asgiref==3.8.1
|
||||||
|
Django==5.1.4
|
||||||
|
sqlparse==0.5.2
|
||||||
|
tzdata==2024.2
|
||||||
Loading…
Reference in New Issue