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 %} +
+ {% csrf_token %} +

Login

+ {{form.as_p}} + +
+ +{% 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 %} + +
+ {% csrf_token %} + {{ form.as_p }} + +
+ +{% 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()