diff --git a/Planteer/Planteer/__init__.py b/Planteer/Planteer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/Planteer/asgi.py b/Planteer/Planteer/asgi.py new file mode 100644 index 0000000..7693f2d --- /dev/null +++ b/Planteer/Planteer/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for Planteer 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/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Planteer.settings') + +application = get_asgi_application() diff --git a/Planteer/Planteer/settings.py b/Planteer/Planteer/settings.py new file mode 100644 index 0000000..16059fa --- /dev/null +++ b/Planteer/Planteer/settings.py @@ -0,0 +1,127 @@ +""" +Django settings for Planteer project. + +Generated by 'django-admin startproject' using Django 6.0.4. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path +import os + +# 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/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-=bq-yu6kds01c+*nn4^kcixwmy%(0v1r1rd7zk1vjdv3gu@3=r' + +# 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', + 'plants', + 'accounts', +] + +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 = 'Planteer.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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 = 'Planteer.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/6.0/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/6.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Riyadh' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + + diff --git a/Planteer/Planteer/urls.py b/Planteer/Planteer/urls.py new file mode 100644 index 0000000..5817a04 --- /dev/null +++ b/Planteer/Planteer/urls.py @@ -0,0 +1,27 @@ +""" +URL configuration for Planteer project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/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 +from django.conf.urls.static import static +from . import settings + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('main.urls')), + path('plants/', include('plants.urls')), + path('accounts/', include('accounts.urls')) +] + static(settings.MEDIA_URL,document_root =settings.MEDIA_ROOT) diff --git a/Planteer/Planteer/wsgi.py b/Planteer/Planteer/wsgi.py new file mode 100644 index 0000000..f14ac6f --- /dev/null +++ b/Planteer/Planteer/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for Planteer 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/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Planteer.settings') + +application = get_wsgi_application() diff --git a/Planteer/accounts/__init__.py b/Planteer/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/accounts/admin.py b/Planteer/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Planteer/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Planteer/accounts/apps.py b/Planteer/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/Planteer/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/Planteer/accounts/migrations/__init__.py b/Planteer/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/accounts/models.py b/Planteer/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Planteer/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Planteer/accounts/templates/accounts/signin.html b/Planteer/accounts/templates/accounts/signin.html new file mode 100644 index 0000000..c8ba3e2 --- /dev/null +++ b/Planteer/accounts/templates/accounts/signin.html @@ -0,0 +1,30 @@ +{% extends 'main/base.html' %} + +{% block title %}Sign In{% endblock %} + +{% block content %} + +
+

Sign In

+ +
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ + +

Don't have an account? Sign Up

+
+
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/accounts/templates/accounts/signup.html b/Planteer/accounts/templates/accounts/signup.html new file mode 100644 index 0000000..6e559a1 --- /dev/null +++ b/Planteer/accounts/templates/accounts/signup.html @@ -0,0 +1,43 @@ +{% extends 'main/base.html' %} +{% block title %}Sign Up{% endblock %} +{% block content %} + +
+
+

Sign Up

+ + {% if messages %} + {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Already have an account? Sign In

+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/accounts/tests.py b/Planteer/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/accounts/urls.py b/Planteer/accounts/urls.py new file mode 100644 index 0000000..49447cc --- /dev/null +++ b/Planteer/accounts/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from . import views + +app_name="accounts" + +urlpatterns=[ + + path('signup/', views.sign_up, name="sign_up"), + path('signin/', views.sign_in, name="sign_in"), + path('logout/', views.log_out, name="log_out"), +] \ No newline at end of file diff --git a/Planteer/accounts/views.py b/Planteer/accounts/views.py new file mode 100644 index 0000000..6929287 --- /dev/null +++ b/Planteer/accounts/views.py @@ -0,0 +1,54 @@ +from django.shortcuts import render,redirect +from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login, logout +from django.contrib import messages +from django.http import HttpRequest,HttpResponse + +# Create your views here. + +def sign_up(request: HttpRequest): + + if request.method == "POST": + try: + new_user = User.objects.create_user( + username=request.POST["username"], + password=request.POST["password"], + email=request.POST["email"], + first_name=request.POST["first_name"], + last_name=request.POST["last_name"] + ) + new_user.save() + messages.success(request, "Registered Successfully!", "alert-success") + return redirect("accounts:sign_in") + except Exception as e: + print(e) + messages.error(request, "Username already exists!", "alert-danger") + + return render(request, "accounts/signup.html") + +def sign_in(request:HttpRequest): + + if request.method == "POST": + + #checking user credentials + user = authenticate(request, username=request.POST["username"], password=request.POST["password"]) + print(user) + if user: + #login the user + login(request, user) + messages.success(request, "Logged in successfully", "alert-success") + return redirect(request.GET.get("next", "/")) + else: + messages.error(request, "Please try again. You credentials are wrong", "alert-danger") + + + return render(request, "accounts/signin.html") + + + +def log_out(request: HttpRequest): + + logout(request) + messages.warning(request, "logged out successfully", "alert-warning") + + return redirect(request.GET.get("next", "/")) \ No newline at end of file diff --git a/Planteer/db.sqlite3 b/Planteer/db.sqlite3 new file mode 100644 index 0000000..0cf2b1b Binary files /dev/null and b/Planteer/db.sqlite3 differ diff --git a/Planteer/main/__init__.py b/Planteer/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/admin.py b/Planteer/main/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Planteer/main/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Planteer/main/apps.py b/Planteer/main/apps.py new file mode 100644 index 0000000..833bff6 --- /dev/null +++ b/Planteer/main/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MainConfig(AppConfig): + name = 'main' diff --git a/Planteer/main/migrations/0001_initial.py b/Planteer/main/migrations/0001_initial.py new file mode 100644 index 0000000..dab0471 --- /dev/null +++ b/Planteer/main/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.4 on 2026-04-19 18:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=1024)), + ('last_name', models.CharField(max_length=1024)), + ('email', models.EmailField(max_length=254)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/Planteer/main/migrations/__init__.py b/Planteer/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/models.py b/Planteer/main/models.py new file mode 100644 index 0000000..db34519 --- /dev/null +++ b/Planteer/main/models.py @@ -0,0 +1,10 @@ +from django.db import models + +# Create your models here. + +class Contact (models.Model): + first_name = models.CharField(max_length=1024) + last_name = models.CharField(max_length=1024) + email = models.EmailField() + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) diff --git a/Planteer/main/static/css/style.css b/Planteer/main/static/css/style.css new file mode 100644 index 0000000..33ce4e2 --- /dev/null +++ b/Planteer/main/static/css/style.css @@ -0,0 +1,145 @@ + + +/* ===== General ===== */ +* { + font-family: "Ubuntu", sans-serif; +} + +main { + min-height: 70vh; +} + +a { + text-decoration: none; + transition: all 0.2s ease-in; +} + +/* ===== Navbar ===== */ +.navbar-brand h2 { + color: #4CAF50; + font-size: 1.8rem; +} + +/* ===== Hero Section ===== */ +.hero-section { + background-color: #f0f7f0; + border-radius: 16px; + padding: 3rem 2rem; + margin-bottom: 2rem; +} + +.hero-section h1 { + font-size: 3rem; + color: #2e7d32; + font-weight: 700; +} + +.hero-section h4 { + color: #555; +} + +/* ===== Cards ===== */ +.card { + border: none; + border-radius: 12px; + box-shadow: 0 2px 12px rgba(0,0,0,0.08); + transition: transform 0.2s ease-in; +} + +.card:hover { + transform: translateY(-5px); +} + +.card-img-top { + border-radius: 12px 12px 0 0; +} + +/* ===== Badges ===== */ +.badge.bg-success { + background-color: #4CAF50 !important; + font-size: 0.8rem; + padding: 0.4rem 0.8rem; + border-radius: 20px; +} + +/* ===== Buttons ===== */ +.btn-success { + background-color: #4CAF50; + border: none; + border-radius: 8px; +} + +.btn-success:hover { + background-color: #388e3c; +} + +.btn-outline-success { + border-color: #4CAF50; + color: #4CAF50; + border-radius: 8px; +} + +.btn-outline-success:hover { + background-color: #4CAF50; + color: white; +} + +/* ===== Forms ===== */ +.form-control, .form-select { + border-radius: 8px; + border: 1px solid #ddd; + padding: 0.6rem 1rem; +} + +.form-control:focus, .form-select:focus { + border-color: #4CAF50; + box-shadow: 0 0 0 0.2rem rgba(76,175,80,0.2); +} + +/* ===== HR ===== */ +hr { + border-color: #e0e0e0; + margin: 2rem 0; +} + +/* ===== Footer ===== */ +footer { + background-color: #f9f9f9; + color: #555; +} + +footer h5 { + color: #2e7d32; +} + +.form-check{ + width: 150px; +} + +.border-color{ + background-color: #e1dcdc; + +} +/* footer { + background-color: #212529; + color: #adb5bd; +} + +footer h5 { + color: #4CAF50; +} + +footer a { + color: #adb5bd; +} + +footer a:hover { + color: #4CAF50; +} */ + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .hero-section h1 { + font-size: 2rem; + } +} \ No newline at end of file diff --git a/Planteer/main/static/images/Carrot.jpg b/Planteer/main/static/images/Carrot.jpg new file mode 100644 index 0000000..7a9bcfc Binary files /dev/null and b/Planteer/main/static/images/Carrot.jpg differ diff --git a/Planteer/main/static/images/Cedar-Tree.jpg b/Planteer/main/static/images/Cedar-Tree.jpg new file mode 100644 index 0000000..64287d9 Binary files /dev/null and b/Planteer/main/static/images/Cedar-Tree.jpg differ diff --git a/Planteer/main/static/images/Lavendar.jpg b/Planteer/main/static/images/Lavendar.jpg new file mode 100644 index 0000000..341cd95 Binary files /dev/null and b/Planteer/main/static/images/Lavendar.jpg differ diff --git a/Planteer/main/static/images/Maple-Tree.jpg b/Planteer/main/static/images/Maple-Tree.jpg new file mode 100644 index 0000000..f444356 Binary files /dev/null and b/Planteer/main/static/images/Maple-Tree.jpg differ diff --git a/Planteer/main/static/images/Mint.jpg b/Planteer/main/static/images/Mint.jpg new file mode 100644 index 0000000..2620a51 Binary files /dev/null and b/Planteer/main/static/images/Mint.jpg differ diff --git a/Planteer/main/static/images/Oak-Tree.jpg b/Planteer/main/static/images/Oak-Tree.jpg new file mode 100644 index 0000000..c20f46c Binary files /dev/null and b/Planteer/main/static/images/Oak-Tree.jpg differ diff --git a/Planteer/main/static/images/Sun-Flower.jpg b/Planteer/main/static/images/Sun-Flower.jpg new file mode 100644 index 0000000..82547d6 Binary files /dev/null and b/Planteer/main/static/images/Sun-Flower.jpg differ diff --git a/Planteer/main/static/images/Tomato.jpg b/Planteer/main/static/images/Tomato.jpg new file mode 100644 index 0000000..b45c8ce Binary files /dev/null and b/Planteer/main/static/images/Tomato.jpg differ diff --git a/Planteer/main/static/images/apple_tree.jpg b/Planteer/main/static/images/apple_tree.jpg new file mode 100644 index 0000000..9dc600a Binary files /dev/null and b/Planteer/main/static/images/apple_tree.jpg differ diff --git a/Planteer/main/static/images/pine-tree.jpg b/Planteer/main/static/images/pine-tree.jpg new file mode 100644 index 0000000..675cc4f Binary files /dev/null and b/Planteer/main/static/images/pine-tree.jpg differ diff --git a/Planteer/main/templates/main/base.html b/Planteer/main/templates/main/base.html new file mode 100644 index 0000000..d366da8 --- /dev/null +++ b/Planteer/main/templates/main/base.html @@ -0,0 +1,95 @@ +{% load static %} + + + + + + {% block title %}Planteer{% endblock %} + + + + + + + + + + + +
+ +
+ + +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% block content %} + {% endblock %} +
+ + + + + + + \ No newline at end of file diff --git a/Planteer/main/templates/main/contact.html b/Planteer/main/templates/main/contact.html new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/templates/main/home.html b/Planteer/main/templates/main/home.html new file mode 100644 index 0000000..7c96778 --- /dev/null +++ b/Planteer/main/templates/main/home.html @@ -0,0 +1,45 @@ +{% extends 'main/base.html' %} + +{% block title %}Home Page{% endblock %} + +{% block content %} + + +
+
+

Planteer

+

Plant Database For Plant Lovers

+
+ + +
+
+
+ +
+ + +
+

Latest Plants

+ More → +
+ +
+ {% for plant in plants %} +
+
+ +
+ {{ plant.category }} +
{{ plant.name }}
+

{{ plant.about|truncatechars:80 }}

+ View Details +
+
+
+ {% empty %} +

No plants yet. Add one!

+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/main/templates/main/message.html b/Planteer/main/templates/main/message.html new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/main/tests.py b/Planteer/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/main/urls.py b/Planteer/main/urls.py new file mode 100644 index 0000000..076d8ed --- /dev/null +++ b/Planteer/main/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from . import views + +app_name="main" + +urlpatterns=[ + + path('', views.home_view, name="home_view"), + path('contact/', views.contact_view, name="contact_view"), + path('message/', views.message_view, name="message_view") + +] \ No newline at end of file diff --git a/Planteer/main/views.py b/Planteer/main/views.py new file mode 100644 index 0000000..34bf289 --- /dev/null +++ b/Planteer/main/views.py @@ -0,0 +1,21 @@ +from django.shortcuts import render, redirect +from django.http import HttpRequest,HttpResponse +from .models import Contact +from plants.models import Plant + +# Create your views here. + + +def home_view(request:HttpRequest): + plants = Plant.objects.all().order_by("-created_at")[0:3] + return render(request, "main/home.html" , {"plants": plants}) + +def contact_view(request:HttpRequest): + + return render(request, "main/contact.html") + +def message_view(request:HttpRequest): + + return render(request, "main/message.html") + + diff --git a/Planteer/manage.py b/Planteer/manage.py new file mode 100644 index 0000000..bd0a64a --- /dev/null +++ b/Planteer/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', 'Planteer.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/Planteer/media/images/Carrot.jpg b/Planteer/media/images/Carrot.jpg new file mode 100644 index 0000000..7a9bcfc Binary files /dev/null and b/Planteer/media/images/Carrot.jpg differ diff --git a/Planteer/media/images/Cedar-Tree.jpg b/Planteer/media/images/Cedar-Tree.jpg new file mode 100644 index 0000000..64287d9 Binary files /dev/null and b/Planteer/media/images/Cedar-Tree.jpg differ diff --git a/Planteer/media/images/Lavendar.jpg b/Planteer/media/images/Lavendar.jpg new file mode 100644 index 0000000..341cd95 Binary files /dev/null and b/Planteer/media/images/Lavendar.jpg differ diff --git a/Planteer/media/images/Maple-Tree.jpg b/Planteer/media/images/Maple-Tree.jpg new file mode 100644 index 0000000..f444356 Binary files /dev/null and b/Planteer/media/images/Maple-Tree.jpg differ diff --git a/Planteer/media/images/Mint.jpg b/Planteer/media/images/Mint.jpg new file mode 100644 index 0000000..2620a51 Binary files /dev/null and b/Planteer/media/images/Mint.jpg differ diff --git a/Planteer/media/images/Oak-Tree.jpg b/Planteer/media/images/Oak-Tree.jpg new file mode 100644 index 0000000..c20f46c Binary files /dev/null and b/Planteer/media/images/Oak-Tree.jpg differ diff --git a/Planteer/media/images/Sun-Flower.jpg b/Planteer/media/images/Sun-Flower.jpg new file mode 100644 index 0000000..82547d6 Binary files /dev/null and b/Planteer/media/images/Sun-Flower.jpg differ diff --git a/Planteer/media/images/Tomato.jpg b/Planteer/media/images/Tomato.jpg new file mode 100644 index 0000000..b45c8ce Binary files /dev/null and b/Planteer/media/images/Tomato.jpg differ diff --git a/Planteer/media/images/apple_tree.jpg b/Planteer/media/images/apple_tree.jpg new file mode 100644 index 0000000..9dc600a Binary files /dev/null and b/Planteer/media/images/apple_tree.jpg differ diff --git a/Planteer/media/images/pine-tree.jpg b/Planteer/media/images/pine-tree.jpg new file mode 100644 index 0000000..e832480 Binary files /dev/null and b/Planteer/media/images/pine-tree.jpg differ diff --git a/Planteer/media/images/pine-tree_ADdiO9n.jpg b/Planteer/media/images/pine-tree_ADdiO9n.jpg new file mode 100644 index 0000000..ad48726 Binary files /dev/null and b/Planteer/media/images/pine-tree_ADdiO9n.jpg differ diff --git a/Planteer/media/images/pine-tree_IJZnJqV.jpg b/Planteer/media/images/pine-tree_IJZnJqV.jpg new file mode 100644 index 0000000..675cc4f Binary files /dev/null and b/Planteer/media/images/pine-tree_IJZnJqV.jpg differ diff --git a/Planteer/plants/__init__.py b/Planteer/plants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/plants/admin.py b/Planteer/plants/admin.py new file mode 100644 index 0000000..2817e0e --- /dev/null +++ b/Planteer/plants/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import Plant, Comment, Country + +# Register your models here. + +admin.site.register(Plant) +admin.site.register(Comment) +admin.site.register(Country) diff --git a/Planteer/plants/apps.py b/Planteer/plants/apps.py new file mode 100644 index 0000000..478c3ce --- /dev/null +++ b/Planteer/plants/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PlantsConfig(AppConfig): + name = 'plants' diff --git a/Planteer/plants/forms.py b/Planteer/plants/forms.py new file mode 100644 index 0000000..878f4ea --- /dev/null +++ b/Planteer/plants/forms.py @@ -0,0 +1,8 @@ +from django import forms +from .models import Plant + +# Create the form class. +class PlantForm(forms.ModelForm): + class Meta: + model = Plant + fields = "__all__" \ No newline at end of file diff --git a/Planteer/plants/migrations/0001_initial.py b/Planteer/plants/migrations/0001_initial.py new file mode 100644 index 0000000..dd345c4 --- /dev/null +++ b/Planteer/plants/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.4 on 2026-04-19 18:58 + +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=1024)), + ('about', models.TextField()), + ('used_for', models.TextField()), + ('image', models.ImageField(upload_to='images/')), + ('category', models.CharField(choices=[('tree', 'Tree'), ('fruit', 'Fruit'), ('vegetable', 'Vegetable'), ('flower', 'Flower'), ('herb', 'Herb')], max_length=500)), + ('is_edible', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/Planteer/plants/migrations/0002_comment.py b/Planteer/plants/migrations/0002_comment.py new file mode 100644 index 0000000..04a6b23 --- /dev/null +++ b/Planteer/plants/migrations/0002_comment.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.4 on 2026-04-20 22:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=512)), + ('content', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('plant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plants.plant')), + ], + ), + ] diff --git a/Planteer/plants/migrations/0003_country_plant_countries.py b/Planteer/plants/migrations/0003_country_plant_countries.py new file mode 100644 index 0000000..69e6aac --- /dev/null +++ b/Planteer/plants/migrations/0003_country_plant_countries.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.4 on 2026-04-22 08:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0002_comment'), + ] + + operations = [ + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=512, unique=True)), + ('flag', models.CharField(max_length=128)), + ], + ), + migrations.AddField( + model_name='plant', + name='countries', + field=models.ManyToManyField(to='plants.country'), + ), + ] diff --git a/Planteer/plants/migrations/0004_remove_comment_name_comment_user.py b/Planteer/plants/migrations/0004_remove_comment_name_comment_user.py new file mode 100644 index 0000000..2b5f6af --- /dev/null +++ b/Planteer/plants/migrations/0004_remove_comment_name_comment_user.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.4 on 2026-04-27 21:51 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0003_country_plant_countries'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='comment', + name='name', + ), + migrations.AddField( + model_name='comment', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/Planteer/plants/migrations/__init__.py b/Planteer/plants/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Planteer/plants/models.py b/Planteer/plants/models.py new file mode 100644 index 0000000..f9f3f8c --- /dev/null +++ b/Planteer/plants/models.py @@ -0,0 +1,42 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. + +#main app + +class Country (models.Model): + + name= models.CharField(max_length=512 ,unique=True) + flag= models.CharField(max_length=128) + + def __str__(self): + return self.name + +class Plant (models.Model): + + class Category (models.TextChoices): + TREE = 'tree' , 'Tree' + FRUIT = 'fruit' , 'Fruit' + VEGETABLE = 'vegetable' , 'Vegetable' + FLOWER = 'flower' , 'Flower' + HERB = 'herb' , 'Herb' + + + + + name = models.CharField(max_length=1024) + about = models.TextField() + used_for = models.TextField() + image = models.ImageField(upload_to="images/") + category= models.CharField(max_length=500,choices=Category.choices) + is_edible = models.BooleanField(default=True) + countries = models.ManyToManyField(Country) + created_at = models.DateTimeField(auto_now_add=True) + + +class Comment (models.Model): + plant= models.ForeignKey(Plant,on_delete=models.CASCADE) + user=models.ForeignKey(User, on_delete=models.CASCADE) + content=models.TextField() + created_at=models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/Planteer/plants/templates/plants/all_plants.html b/Planteer/plants/templates/plants/all_plants.html new file mode 100644 index 0000000..74615c1 --- /dev/null +++ b/Planteer/plants/templates/plants/all_plants.html @@ -0,0 +1,60 @@ +{% extends 'main/base.html' %} + +{% block title %}All Plants{% endblock %} + +{% block content %} + + +
+

All Plants

+
+ + + + + + + +
+
+ + +
+ {% for plant in plants %} +
+
+ +
+ {{ plant.category }} +
{{ plant.name }}
+

{{ plant.about|truncatechars:80 }}

+ View Details +
+
+
+ {% empty %} +

No plants found.

+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/new_plant.html b/Planteer/plants/templates/plants/new_plant.html new file mode 100644 index 0000000..295c5f9 --- /dev/null +++ b/Planteer/plants/templates/plants/new_plant.html @@ -0,0 +1,68 @@ +{% extends 'main/base.html' %} + +{% block title %}Add New Plant{% endblock %} + +{% block content %} + +

Add New Plant

+ +
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ {% for country in countries %} +
+ + +
+ {% endfor %} +
+
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/plant_detail.html b/Planteer/plants/templates/plants/plant_detail.html new file mode 100644 index 0000000..7551ba8 --- /dev/null +++ b/Planteer/plants/templates/plants/plant_detail.html @@ -0,0 +1,109 @@ +{% extends 'main/base.html' %} + +{% block title %}{{ plant.name }}{% endblock %} + +{% block content %} + +
+ +
+ +
+ + +
+ {{ plant.category }} +

{{ plant.name }}

+

{{ plant.about }}

+
+
Used For:
+

{{ plant.used_for }}

+
Edible: {% if plant.is_edible %} Yes {% else %} No {% endif %}
+ +
Native to:
+
+ {% for country in plant.countries.all %} + {{ country.name }} + {% empty %} +

No countries added yet.

+ {% endfor %} +
+ +
+ {% if request.user.is_staff %} + Update + + {% endif %} +
+
+
+ + +
+

Comments

+ +{% if request.user.is_authenticated %} +
+ {% csrf_token %} + + +
+{% else %} +
+ Sign in to add a comment. +
+{% endif %} + +
+ {% for comment in comments %} +
+
{{ comment.user.username }}
+

{{ comment.content }}

+ {{ comment.created_at|date:"Y-m-d" }} +
+ {% empty %} +

No comments yet. Be the first!

+ {% endfor %} +
+ + +
+

Related Plants

+
+ {% for related in related_plants %} +
+
+ +
+ {{ related.category }} +
{{ related.name }}
+

{{ related.about|truncatechars:80 }}

+ View Details +
+
+
+ {% empty %} +

No related plants found.

+ {% endfor %} +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/search.html b/Planteer/plants/templates/plants/search.html new file mode 100644 index 0000000..4f3f72c --- /dev/null +++ b/Planteer/plants/templates/plants/search.html @@ -0,0 +1,39 @@ +{% extends 'main/base.html' %} + +{% block title %}Search Plants{% endblock %} + +{% block content %} + +

Search Plants

+ + +
+ + +
+ + +{% if plants %} +
Results for: "{{ request.GET.search }}"
+
+ {% for plant in plants %} +
+
+ +
+ {{ plant.category }} +
{{ plant.name }}
+

{{ plant.about|truncatechars:80 }}

+ View Details +
+
+
+ {% endfor %} +
+ +{% elif request.GET.search %} +

No results found for "{{ request.GET.search }}"

+ +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/update_plant.html b/Planteer/plants/templates/plants/update_plant.html new file mode 100644 index 0000000..452fbe4 --- /dev/null +++ b/Planteer/plants/templates/plants/update_plant.html @@ -0,0 +1,74 @@ +{% extends 'main/base.html' %} + +{% block title %}Update {{ plant.name }}{% endblock %} + +{% block content %} + +

Update {{ plant.name }}

+ +
+
+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ +
+ {% for country in countries %} +
+ + + +
+ {% endfor %} +
+
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/tests.py b/Planteer/plants/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Planteer/plants/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Planteer/plants/urls.py b/Planteer/plants/urls.py new file mode 100644 index 0000000..faea55d --- /dev/null +++ b/Planteer/plants/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from . import views + +app_name="plants" + +urlpatterns=[ + + path("all/", views.all_view , name="all_view"), + path("new/", views.new_view , name= "new_view"), + path("/detail/", views.detail_view, name= "detail_view"), + path("/update/", views.update_view, name= "update_view"), + path("/delete/", views.delete_view, name="delete_view"), + path("search/", views.search_view, name="search_view"), + path("comment/add/",views.add_comment_view,name="add_comment_view"), +] \ No newline at end of file diff --git a/Planteer/plants/views.py b/Planteer/plants/views.py new file mode 100644 index 0000000..e6a8d83 --- /dev/null +++ b/Planteer/plants/views.py @@ -0,0 +1,118 @@ +from django.shortcuts import render, redirect +from django.http import HttpRequest,HttpResponse +from .models import Plant , Comment , Country +from .forms import PlantForm + +# Create your views here. + +def all_view(request: HttpRequest): + plants = Plant.objects.all() + + if "category" in request.GET and request.GET["category"] != "": + plants = plants.filter(category=request.GET["category"]) + + if "is_edible" in request.GET and request.GET["is_edible"] != "": + plants = plants.filter(is_edible=request.GET["is_edible"]) + + if "country" in request.GET and request.GET["country"] != "": + plants = plants.filter(countries__id=request.GET["country"]) + + return render(request, "plants/all_plants.html", { + "plants": plants, + "categories": Plant.Category.choices, + "countries": Country.objects.all() + + }) + +# def new_view(request: HttpRequest): +# form = PlantForm() + +# if request.method == "POST": +# form = PlantForm(request.POST, request.FILES) +# if form.is_valid(): +# form.save() +# return redirect("plants:all_view") + +# return render(request, "plants/new_plant.html", { +# "form": form, +# "countries":Country.objects.all() +# }) +def new_view(request: HttpRequest): + if not request.user.is_staff: + return redirect("main:home_view") + + form = PlantForm() + if request.method == "POST": + form = PlantForm(request.POST, request.FILES) + if form.is_valid(): + plant = form.save() + plant.countries.set(request.POST.getlist("countries")) + return redirect("plants:all_view") + + return render(request, "plants/new_plant.html", { + "form": form, + "countries": Country.objects.all() + }) + +def detail_view(request: HttpRequest, plant_id: int): + plant = Plant.objects.get(pk=plant_id) + comments=Comment.objects.filter(plant=plant) + + related_plants = Plant.objects.filter(category=plant.category).exclude(pk=plant_id)[:3] + return render(request, "plants/plant_detail.html", { + "plant": plant, + "comments": comments, + "related_plants": related_plants + }) + + + +def update_view(request: HttpRequest, plant_id: int): + if not request.user.is_staff: + return redirect("main:home_view") + + plant = Plant.objects.get(pk=plant_id) + form = PlantForm(instance=plant) + + if request.method == "POST": + form = PlantForm(request.POST, request.FILES, instance=plant) + if form.is_valid(): + plant=form.save() + plant.countries.set(request.POST.getlist("countries")) + return redirect("plants:detail_view", plant_id=plant.id) + + return render(request, "plants/update_plant.html", { + "form": form, + "plant": plant, + "countries":Country.objects.all() + }) + + + +# def delete_view(request: HttpRequest, plant_id: int): +# return render(request, "plants/plant_detail.html") +def delete_view(request:HttpRequest, plant_id:int): + if not request.user.is_staff: + return redirect("main:home_view") + + plant = Plant.objects.get(pk=plant_id) + plant.delete() + return redirect("main:home_view") + +def search_view(request: HttpRequest): + + # the user have to search with more than 2 letters 3 and on... + if "search" in request.GET and len(request.GET["search"]) >= 3: + plants = Plant.objects.filter(name__contains=request.GET["search"]) + + else: + plants = [] + return render(request, "plants/search.html", {"plants": plants}) + +def add_comment_view(request:HttpRequest,plant_id:int): + if request.method== "POST": + plant_ob = Plant.objects.get(pk=plant_id) + new_comment=Comment(plant=plant_ob,user=request.user,content=request.POST['content']) + new_comment.save() + + return redirect ("plants:detail_view", plant_id=plant_id) \ No newline at end of file