diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..dc608e7
--- /dev/null
+++ b/manage.py
@@ -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', 'verdure.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()
diff --git a/planner/__init__.py b/planner/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/planner/admin.py b/planner/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/planner/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/planner/apps.py b/planner/apps.py
new file mode 100644
index 0000000..63e1fa8
--- /dev/null
+++ b/planner/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PlannerConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'planner'
diff --git a/planner/forms.py b/planner/forms.py
new file mode 100644
index 0000000..eee49c9
--- /dev/null
+++ b/planner/forms.py
@@ -0,0 +1,14 @@
+from django import forms
+from django.contrib.auth.models import User
+from django.contrib.auth.forms import UserCreationForm
+
+
+class LoginForm(forms.Form):
+ username = forms.CharField(max_length=65)
+ password = forms.CharField(max_length=128, widget=forms.PasswordInput)
+
+
+class RegisterForm(UserCreationForm):
+ class Meta:
+ model=User
+ fields = ['username','email','password1','password2']
diff --git a/planner/migrations/__init__.py b/planner/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/planner/models.py b/planner/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/planner/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/planner/templates/planner/index.html b/planner/templates/planner/index.html
new file mode 100644
index 0000000..ebc7832
--- /dev/null
+++ b/planner/templates/planner/index.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+{% block head_title %}Verdure: My Garden Plans{% endblock head_title %}
+{%block head_content%}
Garden Planner
{%endblock head_content%}
+
+
+
+{% block content %}
+
+Green Planner
+TODO
+
+{% endblock content %}
diff --git a/planner/templates/planner/login.html b/planner/templates/planner/login.html
new file mode 100644
index 0000000..caa21a9
--- /dev/null
+++ b/planner/templates/planner/login.html
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+
+{% block head_title %}Verdure: Login{% endblock head_title %}
+{%block head_content%}Login
{%endblock head_content%}
+
+
+
+{% block content %}
+
+
+{% endblock content%}
+
diff --git a/planner/templates/planner/register.html b/planner/templates/planner/register.html
new file mode 100644
index 0000000..42a9a95
--- /dev/null
+++ b/planner/templates/planner/register.html
@@ -0,0 +1,16 @@
+{% extends 'base.html' %}
+
+{% block head_title %}Verdure: Sign Up{% endblock head_title %}
+{%block head_content%}Sign Up
{%endblock head_content%}
+
+
+
+{% block content %}
+
+
+
+{% endblock content%}
diff --git a/planner/tests.py b/planner/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/planner/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/planner/urls.py b/planner/urls.py
new file mode 100644
index 0000000..20c089f
--- /dev/null
+++ b/planner/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+app_name = "planner"
+urlpatterns = [
+ path("", views.index, name="index"),
+ path("login/", views.sign_in, name="login"),
+ path("logout/", views.sign_out, name="logout"),
+ path("register/", views.sign_up, name="register"),
+]
+
diff --git a/planner/views.py b/planner/views.py
new file mode 100644
index 0000000..57e7f2b
--- /dev/null
+++ b/planner/views.py
@@ -0,0 +1,72 @@
+from django.shortcuts import render, redirect
+from django.http import HttpResponse, Http404
+from django.contrib import messages
+from django.contrib.auth import login, authenticate, logout
+from .forms import LoginForm, RegisterForm
+import logging
+
+def index(request):
+ # from django.shortcuts import render
+ # context = { "latest_plant_list": latest_plant_list }
+ # return render(request, "planterteque/index.html", context)
+ #template = loader.get_template("planner/index.html")
+ #context = { }
+ #return HttpResponse( template.render( context, request ) )
+ return render(request, 'planner/index.html')
+
+## Form action to log in
+def sign_in(request):
+ if request.user.is_authenticated and request.method == 'GET':
+ return redirect('planner:index')
+ elif request.method == 'GET':
+ form = LoginForm()
+ return render(request, 'planner/login.html', {'form': form})
+ elif request.method == 'POST':
+ form = LoginForm(request.POST)
+
+ if form.is_valid():
+ username = form.cleaned_data['username']
+ password = form.cleaned_data['password']
+ user = authenticate(request,username=username,password=password)
+ if user:
+ logging.warning('User %s is valid', username.title())
+ login(request, user)
+ messages.success(request,f'Hi {username.title()}, welcome back!')
+ return redirect('planner:index')
+ #return index(request)
+ else:
+ logging.warning('User login failed')
+ else:
+ logging.warning('User form invalid')
+
+ # form is not valid or user is not authenticated
+ messages.error(request,f'Invalid username or password')
+ return render(request,'planner/login.html',{'form': form})
+ return redirect('planner:index')
+
+## Form action to log out
+def sign_out(request):
+ if (request.user.is_authenticated):
+ logout(request)
+ messages.success(request,f'You have been logged out.')
+ return render(request, 'planner/index.html', {})
+ return redirect('planner:index')
+
+def sign_up(request):
+ if request.method == 'GET':
+ form = RegisterForm()
+ return render(request, 'planner/register.html', { 'form': form})
+ if request.method == 'POST':
+ form = RegisterForm(request.POST)
+ if form.is_valid():
+ user = form.save(commit=False)
+ user.username = user.username.lower()
+ user.save()
+ messages.success(request, 'You have signed up successfully.')
+ login(request, user)
+ return redirect('planner:index')
+ else:
+ return render(request, 'planner/register.html', {'form': form})
+
+
+
diff --git a/planterteque/__init__.py b/planterteque/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/planterteque/admin.py b/planterteque/admin.py
new file mode 100644
index 0000000..d032d45
--- /dev/null
+++ b/planterteque/admin.py
@@ -0,0 +1,5 @@
+from django.contrib import admin
+
+from .models import Plant
+
+admin.site.register(Plant)
diff --git a/planterteque/apps.py b/planterteque/apps.py
new file mode 100644
index 0000000..e4c0723
--- /dev/null
+++ b/planterteque/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PlantertequeConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'planterteque'
diff --git a/planterteque/migrations/0001_initial.py b/planterteque/migrations/0001_initial.py
new file mode 100644
index 0000000..240140f
--- /dev/null
+++ b/planterteque/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.2.6 on 2025-12-03 09:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Plant',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=512, unique=True)),
+ ('create_date', models.DateTimeField(auto_now_add=True)),
+ ('modified_date', models.DateTimeField(auto_now=True)),
+ ('botanical_name', models.CharField(max_length=2048)),
+ ('description_short', models.TextField(blank=2048)),
+ ('description_long', models.TextField(blank=True)),
+ ('growing_notes', models.TextField(blank=True)),
+ ('spread', models.IntegerField(default=0)),
+ ('height', models.IntegerField(default=0)),
+ ('sow_depth', models.IntegerField(default=0)),
+ ('spacing', models.IntegerField(default=0)),
+ ('other_notes', models.TextField(blank=True)),
+ ],
+ ),
+ ]
diff --git a/planterteque/migrations/__init__.py b/planterteque/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/planterteque/models.py b/planterteque/models.py
new file mode 100644
index 0000000..4eaa2bf
--- /dev/null
+++ b/planterteque/models.py
@@ -0,0 +1,21 @@
+from django.db import models
+
+class Plant(models.Model):
+ name = models.CharField(max_length=512, unique=True)
+ create_date = models.DateTimeField(auto_now_add=True)
+ modified_date = models.DateTimeField(auto_now=True)
+ botanical_name = models.CharField(max_length=2048)
+ description_short = models.TextField(blank=2048)
+ description_long = models.TextField(blank=True)
+ growing_notes = models.TextField(blank=True)
+ spread = models.IntegerField(default=0)
+ height = models.IntegerField(default=0)
+ sow_depth = models.IntegerField(default=0)
+ spacing = models.IntegerField(default=0)
+ other_notes = models.TextField(blank=True)
+
+ def __str__(self):
+ return self.name
+
+
+
diff --git a/planterteque/templates/planterteque/index.html b/planterteque/templates/planterteque/index.html
new file mode 100644
index 0000000..cb6e9b5
--- /dev/null
+++ b/planterteque/templates/planterteque/index.html
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+
+{% block head_title %}Verdure: Plant Reference{% endblock head_title %}
+{%block head_content%}Plant Reference
{%endblock head_content%}
+
+
+
+{% block content %}
+
+{% if latest_plants_list %}
+
+{% else %}
+No plants. Add some?
+{% endif %}
+
+{% endblock content%}
+
diff --git a/planterteque/templates/planterteque/reference.html b/planterteque/templates/planterteque/reference.html
new file mode 100644
index 0000000..0313cc8
--- /dev/null
+++ b/planterteque/templates/planterteque/reference.html
@@ -0,0 +1,44 @@
+{% extends 'base.html' %}
+
+{% block head_title %}Verdure: Plant Reference{% endblock head_title %}
+{% block head_content %}{{ plant.name }}
{% endblock head_content %}
+
+
+
+{% block content %}
+
+{% if plant.description_short %}
+
+{{ plant.description_short }}
+
+{% endif %}
+
+
+
+{% if plant.height %}
+- Height
-
+{{ plant.height }}
+
+{% endif %}
+
+{% if plant.spread %}
+- Spread
-
+{{ plant.spread }}
+
+{% endif %}
+
+{% if plant.spacing %}
+- Spacing
-
+{{ plant.spacing }}
+
+{% endif %}
+
+{% if plant.description_long %}
+
+{{ plant.description_long }}
+
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/planterteque/tests.py b/planterteque/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/planterteque/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/planterteque/urls.py b/planterteque/urls.py
new file mode 100644
index 0000000..df840e3
--- /dev/null
+++ b/planterteque/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from . import views
+
+app_name = "planterteque"
+urlpatterns = [
+ path("", views.index, name="index"),
+ path("", views.reference, name="reference"),
+]
diff --git a/planterteque/views.py b/planterteque/views.py
new file mode 100644
index 0000000..817d781
--- /dev/null
+++ b/planterteque/views.py
@@ -0,0 +1,21 @@
+from django.shortcuts import render
+
+# Create your views here.
+from django.http import HttpResponse, Http404
+from django.template import loader
+
+from .models import Plant
+
+def index(request):
+ latest_plants_list = Plant.objects.order_by("-create_date")[:5]
+ context = { "latest_plants_list": latest_plants_list }
+ return render( request, "planterteque/index.html", context )
+
+def reference(request, plant_id):
+ try:
+ plant = Plant.objects.get( pk=plant_id )
+ except Plant.DoesNotExist:
+ raise Http404("Plant does not exist")
+ context = { "plant": plant }
+ return render( request, "planterteque/reference.html", context )
+
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..aa8441f
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,26 @@
+
+
+body {
+ background: #fff;
+ color: #333;
+ line-height: 1;
+}
+/* =Global
+----------------------------------------------- */
+
+body, input, textarea {
+ color: #373737;
+ font: 15px "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ line-height: 1.625;
+}
+strong {
+ font-weight: bold;
+}
+blockquote {
+ font-family: Georgia, "Bitstream Charter", serif;
+ font-style: italic;
+ font-weight: normal;
+ margin: 0 3em;
+}
+
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..68d2e21
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,55 @@
+
+
+{% load static %}
+{#
+
+TODO:
+- fix head_content block links
+
+#}
+
+
+
+
+
+ {% block head_title %}Verdure: How Does Your Garden Grow{% endblock head_title %}
+
+
+
+
+ {% block head_content %}
+ Verdure
+ {% endblock head_content %}
+ {% if request.user.is_authenticated %}
+ Hi {{ request.user.username | title }}
+ Logout
+ {% else %}
+ Login
+ Register
+ {% endif %}
+
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
+ {{ message }}
+
+ {% endfor %}
+
+ {% endif %}
+
+{% autoescape off %}
+ {% block content %}
+ {% endblock content %}
+{% endautoescape %}
+
+
+
+
+
+
diff --git a/verdure/__init__.py b/verdure/__init__.py
new file mode 100644
index 0000000..72b22f8
--- /dev/null
+++ b/verdure/__init__.py
@@ -0,0 +1,7 @@
+## Set up the environment to add the path
+
+from os.path import dirname
+from sys import path
+
+path.append(dirname(__file__))
+
diff --git a/verdure/asgi.py b/verdure/asgi.py
new file mode 100644
index 0000000..ce99b3c
--- /dev/null
+++ b/verdure/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for verdure 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.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'verdure.settings')
+
+application = get_asgi_application()
diff --git a/verdure/settings.py b/verdure/settings.py
new file mode 100644
index 0000000..7501b51
--- /dev/null
+++ b/verdure/settings.py
@@ -0,0 +1,149 @@
+"""
+Django settings for verdure project.
+
+Generated by 'django-admin startproject' using Django 5.2.6.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+
+import os
+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.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+#SECRET_KEY = os.getenv('SECRET_KEY')
+SECRET_KEY = 'django-insecure-u!70snw1poh+nz%&yx0-8wo-bfbux^xpopj=6*)d=wcar5bz#i'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# https://pypi.org/project/django-encrypted-model-fields/
+FIELD_ENCRYPTION_KEY = os.getenv('FIELD_ENCRYPTION_KEY')
+##
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'planner.apps.PlannerConfig',
+ 'planterteque.apps.PlantertequeConfig',
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'encrypted_model_fields',
+]
+
+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 = 'verdure.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [
+ BASE_DIR / "templates",
+ ],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'verdure.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/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.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = 'static/'
+
+STATICFILES_DIRS = [
+ BASE_DIR / "static",
+]
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+# encrypted fields
+
+# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model
+#AUTH_USER_MODEL = 'planner.GreenUserModel' #TODO: custom model
+
+
+
+
+# https://pypi.org/project/django-hashid-field/
+HASHID_FIELD_SALT = os.getenv('HASHID_FIELD_SALT')
+HASHID_FIELD_ALLOW_INT_LOOKUP = True
diff --git a/verdure/urls.py b/verdure/urls.py
new file mode 100644
index 0000000..8ee0bf2
--- /dev/null
+++ b/verdure/urls.py
@@ -0,0 +1,24 @@
+"""
+URL configuration for verdure project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/5.2/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 include, path
+
+urlpatterns = [
+ path('planner/', include("planner.urls")),
+ path("plantlibrary/", include("planterteque.urls")),
+ path('admin/', admin.site.urls),
+]
diff --git a/verdure/wsgi.py b/verdure/wsgi.py
new file mode 100644
index 0000000..a1d9456
--- /dev/null
+++ b/verdure/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for verdure 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.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'verdure.settings')
+
+application = get_wsgi_application()