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..3c07e1d --- /dev/null +++ b/Planteer/Planteer/settings.py @@ -0,0 +1,124 @@ +""" +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/ +""" + +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/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-rwq+8)*tp(5d4#=tcxp!emmgh$fe8rz7yx6dy56))%!-g(6l)x' + +# 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': [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 = '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' + +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') \ No newline at end of file diff --git a/Planteer/Planteer/urls.py b/Planteer/Planteer/urls.py new file mode 100644 index 0000000..d227b92 --- /dev/null +++ b/Planteer/Planteer/urls.py @@ -0,0 +1,30 @@ +""" +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 import settings +from django.conf.urls.static import static + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('main.urls')), + path('plants/', include('plants.urls')), + path('accounts/', include('accounts.urls')) +] + +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file 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..d440a20 --- /dev/null +++ b/Planteer/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountConfig(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..9d61c5a --- /dev/null +++ b/Planteer/accounts/models.py @@ -0,0 +1,5 @@ +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..e34080b --- /dev/null +++ b/Planteer/accounts/templates/accounts/signin.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} + +{% block title %} Sign up new user {% endblock %} + +{% block content %} + +
+ +

Sign In

+
+ {% csrf_token %} + + + +
+ + +
+ +
+ + +
+ + +
+
+ +{% 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..1bec02d --- /dev/null +++ b/Planteer/accounts/templates/accounts/signup.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} + +{% block title %} Sign up new user {% endblock %} + +{% block content %} + +
+ +

Sign Up

+
+ {% csrf_token %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +{% 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..df0cf22 --- /dev/null +++ b/Planteer/accounts/urls.py @@ -0,0 +1,11 @@ +from . import views +from django.urls import path + +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..0a8b62d --- /dev/null +++ b/Planteer/accounts/views.py @@ -0,0 +1,44 @@ +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse +from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login, logout +from django.contrib import messages +# Create your views here. + +def sign_up(request:HttpRequest): + + if request.method == "POST": + + try: + new_user = User.objects.create_user(username=request.POST["username"], first_name=request.POST["first_name"], last_name=request.POST["last_name"], email=request.POST["email"], password=request.POST["password"]) + new_user.save() + messages.success(request, "Registered User Successfuly", "alert-success") + return redirect(request.GET.get("next", "/")) + except Exception as e: + print(e) + + + return render(request, 'accounts/signup.html', {}) + + +def sign_in(request:HttpRequest): + + if request.method == "POST": + user = authenticate(request, username=request.POST["username"], password=request.POST["password"]) + + if user: + login(request, user) + messages.success(request, "Logged in successfully", "alert-success") + return redirect("main:home_view") + + 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.success(request, "Logged out successsfully", "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..30ac82a 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..e999cc0 --- /dev/null +++ b/Planteer/main/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.4 on 2026-04-17 20:53 + +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=50)), + ('last_name', models.CharField(max_length=50)), + ('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..2275ef7 --- /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=50) + last_name = models.CharField(max_length=50) + email = models.EmailField() + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) \ No newline at end of file diff --git a/Planteer/main/static/css/style.css b/Planteer/main/static/css/style.css new file mode 100644 index 0000000..3de32e4 --- /dev/null +++ b/Planteer/main/static/css/style.css @@ -0,0 +1,167 @@ +*{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + margin: 0; +} + +body{ + padding: 60px 90px; + display: flex; + flex-direction: column; + margin: 0; +} + +main { + flex: 1; +} + + +header { + display: flex; + flex-direction: row; + position: relative; + align-items: center; + justify-content: space-between; +} + +header a{ + text-decoration: none; + color: black; +} + +.btn-primary{ + background-color: black; + border: none; + padding: 13px 20px; + font-size: 20px; +} + +.nav-links{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; + font-size: 20px; +} + +footer{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 30rem; + border-top: 0.1rem solid black; + padding: 20px; +} + +.nav-footer{ + display: flex; + flex-direction: column; +} + +.logo-footer{ + padding-bottom: 80px; +} + +.fa-brands{ + font-size: 25px; +} + +a{ + text-decoration: none; + color: black; +} + +.hero, .img { + height: 600px; + background-image: url("../images/element5-digital-z6i_UCBuu5Q-unsplash.jpg"); + background-size: cover; + background-position: center; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + color: rgb(0, 0, 0); + margin-top: 60px; + border-radius: 15px; +} + +.hero h1{ + font-size: 80px; +} + +.hero h3{ + font-size: 60px; +} + +.btn-outline-success{ + background-color: black; + color: white; + border:none; +} + +.plant-section{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding-top: 60px; +} + +.all-plants{ + padding-top: 60px; + padding-bottom: 60px; +} + + +.btn-primary:hover{ + background-color: #198754; +} + +.btn-primary a{ + color: white; +} + +.card{ + height: auto; +} + +.card-body{ + height: 450px; + + +} + +.card-body img{ + height: 270px; + +} + +.plant-img{ + display: flex; + flex-direction: column; + align-items: start; + justify-items: start; +} + +.more-plants a{ + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +a:hover{ + color: #000000; +} + +.section-flag{ + background-color: #00000025; + border-radius: 50px; + padding: 4px; +} + diff --git a/Planteer/main/static/images/element5-digital-z6i_UCBuu5Q-unsplash.jpg b/Planteer/main/static/images/element5-digital-z6i_UCBuu5Q-unsplash.jpg new file mode 100644 index 0000000..9e25851 Binary files /dev/null and b/Planteer/main/static/images/element5-digital-z6i_UCBuu5Q-unsplash.jpg differ diff --git a/Planteer/main/static/images/plants-hero.jpg b/Planteer/main/static/images/plants-hero.jpg new file mode 100644 index 0000000..bf45779 Binary files /dev/null and b/Planteer/main/static/images/plants-hero.jpg differ diff --git a/Planteer/main/templates/base.html b/Planteer/main/templates/base.html new file mode 100644 index 0000000..a83d3e7 --- /dev/null +++ b/Planteer/main/templates/base.html @@ -0,0 +1,101 @@ +{% load static %} + + + + + {% block title %}Planteer{% endblock %} + + + + + + + + + +
+ + + + {% if request.user.is_authenticated %} +
Welcome {{ request.user.username }},Log out
+ {% else %} +
+ + + +
+ {% endif %} +
+ + +
+ {% 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..6599535 --- /dev/null +++ b/Planteer/main/templates/main/contact.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} + +{% block content %} +

Contact Us

+

Subheading for description or instructions

+ +
+
+
+ {% csrf_token %} + +
+ + +
+ + + + + +
+
+ +
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Planteer/main/templates/main/home.html b/Planteer/main/templates/main/home.html new file mode 100644 index 0000000..5769448 --- /dev/null +++ b/Planteer/main/templates/main/home.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% load static %} + +{% block content %} +
+
+ +
+
+ +
+
+
+

Plants

+

Learn more about plants

+
+ + + + +
+ + {% include 'plants/card_plants.html' %} + +
+ + +{% endblock %} \ No newline at end of file diff --git a/Planteer/main/templates/main/messages.html b/Planteer/main/templates/main/messages.html new file mode 100644 index 0000000..f6b57ae --- /dev/null +++ b/Planteer/main/templates/main/messages.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +

Messages from Users

+ +{% for msg in messages %} +
+
+
{{ msg.first_name }} {{ msg.last_name }}
+

{{ msg.message }}

+ {{ msg.created_at }} +
+
+{% endfor %} + +{% endblock %} \ No newline at end of file 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..1e8e233 --- /dev/null +++ b/Planteer/main/urls.py @@ -0,0 +1,11 @@ +from . import views +from django.urls import path + +app_name = 'main' + +urlpatterns = [ + path('', views.home_view, name='home_view'), + path('contact/', views.contact_view, name='contact_view'), + path('contact/messages/', views.messages_view, name='messages_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..82b9f86 --- /dev/null +++ b/Planteer/main/views.py @@ -0,0 +1,27 @@ +from django.shortcuts import redirect, render +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()[0:3] + return render(request, 'main/home.html', {'plants': plants}) + + +def contact_view(request:HttpRequest): + if request.method == 'POST': + Contact.objects.create( + first_name=request.POST.get('first_name'), + last_name=request.POST.get('last_name'), + email=request.POST.get('email'), + message=request.POST.get('message'), + ) + return redirect('main:home_view') + + return render(request, 'main/contact.html') + +def messages_view(request:HttpRequest): + msgs = Contact.objects.all().order_by('-created_at') + return render(request, 'main/messages.html', {'messages': msgs}) \ No newline at end of file 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/media/Flag_of_Indonesia.svg.png b/Planteer/media/media/Flag_of_Indonesia.svg.png new file mode 100644 index 0000000..ce76041 Binary files /dev/null and b/Planteer/media/media/Flag_of_Indonesia.svg.png differ diff --git a/Planteer/media/media/Flag_of_Kuwait.svg.png b/Planteer/media/media/Flag_of_Kuwait.svg.png new file mode 100644 index 0000000..2a09678 Binary files /dev/null and b/Planteer/media/media/Flag_of_Kuwait.svg.png differ diff --git a/Planteer/media/media/Flag_of_Saudi_Arabia.svg.webp b/Planteer/media/media/Flag_of_Saudi_Arabia.svg.webp new file mode 100644 index 0000000..11983d2 Binary files /dev/null and b/Planteer/media/media/Flag_of_Saudi_Arabia.svg.webp differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living.jpg b/Planteer/media/media/Lemon_-_Sprout_Living.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_0Uhe75U.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_0Uhe75U.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_0Uhe75U.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_1ev5qbW.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_1ev5qbW.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_1ev5qbW.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_KW8aRHs.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_KW8aRHs.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_KW8aRHs.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_Scg2bEG.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_Scg2bEG.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_Scg2bEG.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_eCNe13O.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_eCNe13O.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_eCNe13O.jpg differ diff --git a/Planteer/media/media/Lemon_-_Sprout_Living_vPkhCOg.jpg b/Planteer/media/media/Lemon_-_Sprout_Living_vPkhCOg.jpg new file mode 100644 index 0000000..0228e56 Binary files /dev/null and b/Planteer/media/media/Lemon_-_Sprout_Living_vPkhCOg.jpg differ diff --git a/Planteer/media/media/Morangos.jpg b/Planteer/media/media/Morangos.jpg new file mode 100644 index 0000000..d67170a Binary files /dev/null and b/Planteer/media/media/Morangos.jpg differ diff --git a/Planteer/media/media/Morangos_fiNDA2u.jpg b/Planteer/media/media/Morangos_fiNDA2u.jpg new file mode 100644 index 0000000..d67170a Binary files /dev/null and b/Planteer/media/media/Morangos_fiNDA2u.jpg differ diff --git a/Planteer/media/media/Morangos_gwAo5KI.jpg b/Planteer/media/media/Morangos_gwAo5KI.jpg new file mode 100644 index 0000000..d67170a Binary files /dev/null and b/Planteer/media/media/Morangos_gwAo5KI.jpg differ diff --git a/Planteer/media/media/Morangos_p4ox3ow.jpg b/Planteer/media/media/Morangos_p4ox3ow.jpg new file mode 100644 index 0000000..d67170a Binary files /dev/null and b/Planteer/media/media/Morangos_p4ox3ow.jpg differ diff --git a/Planteer/media/media/download.jpg b/Planteer/media/media/download.jpg new file mode 100644 index 0000000..b5b66fd Binary files /dev/null and b/Planteer/media/media/download.jpg differ diff --git a/Planteer/media/media/download_LTN85y3.jpg b/Planteer/media/media/download_LTN85y3.jpg new file mode 100644 index 0000000..b5b66fd Binary files /dev/null and b/Planteer/media/media/download_LTN85y3.jpg differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_9DmmWYX.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_9DmmWYX.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_9DmmWYX.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_ByJzIKj.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_ByJzIKj.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_ByJzIKj.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_HemZA7V.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_HemZA7V.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_HemZA7V.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_Ve3329r.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_Ve3329r.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_Ve3329r.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_XF8Rnoc.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_XF8Rnoc.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_XF8Rnoc.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_YyQuwKL.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_YyQuwKL.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_YyQuwKL.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_dMTm2wO.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_dMTm2wO.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_dMTm2wO.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_fBW4rjc.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_fBW4rjc.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_fBW4rjc.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_k9y4HyO.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_k9y4HyO.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_k9y4HyO.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_mh9LeJT.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_mh9LeJT.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_mh9LeJT.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_nBuu8bQ.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_nBuu8bQ.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_nBuu8bQ.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_srRQD2v.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_srRQD2v.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_srRQD2v.jpg" differ diff --git "a/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_vxf8yqM.jpg" "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_vxf8yqM.jpg" new file mode 100644 index 0000000..35a0223 Binary files /dev/null and "b/Planteer/media/media/\330\250\331\206\330\257\331\210\330\261\330\251_vxf8yqM.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..d6035e8 --- /dev/null +++ b/Planteer/plants/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Plant, Review, Country +# Register your models here. + +admin.site.register(Plant) +admin.site.register(Review) +admin.site.register(Country) \ No newline at end of file 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/migrations/0001_initial.py b/Planteer/plants/migrations/0001_initial.py new file mode 100644 index 0000000..be2f3a9 --- /dev/null +++ b/Planteer/plants/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.4 on 2026-04-17 20:53 + +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=100)), + ('about', models.TextField()), + ('used_for', models.TextField()), + ('image', models.ImageField(upload_to='media/')), + ('category', models.CharField(choices=[('Indoor', 'indoor'), ('Outdoor', 'outdoor'), ('Succulent', 'succulent'), ('Flowering', 'flowering'), ('Herb', 'herb'), ('Tree', 'tree'), ('Shrub', 'shrub'), ('Cactus', 'cactus'), ('Fern', 'fern'), ('Grass', 'grass')], max_length=20)), + ('is_edible', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/Planteer/plants/migrations/0002_review.py b/Planteer/plants/migrations/0002_review.py new file mode 100644 index 0000000..8af0743 --- /dev/null +++ b/Planteer/plants/migrations/0002_review.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.4 on 2026-04-20 19:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Review', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=1024)), + ('comment', 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..94fc8cf --- /dev/null +++ b/Planteer/plants/migrations/0003_country_plant_countries.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.4 on 2026-04-21 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plants', '0002_review'), + ] + + 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=32)), + ('flag', models.ImageField(upload_to='media/')), + ], + ), + migrations.AddField( + model_name='plant', + name='countries', + field=models.ManyToManyField(related_name='plants', to='plants.country'), + ), + ] diff --git a/Planteer/plants/migrations/0004_remove_review_name_review_user.py b/Planteer/plants/migrations/0004_remove_review_name_review_user.py new file mode 100644 index 0000000..dd7b2fc --- /dev/null +++ b/Planteer/plants/migrations/0004_remove_review_name_review_user.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.4 on 2026-04-27 06:32 + +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='review', + name='name', + ), + migrations.AddField( + model_name='review', + name='user', + field=models.ForeignKey(default=2, 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..b33089a --- /dev/null +++ b/Planteer/plants/models.py @@ -0,0 +1,48 @@ +from django.db import models +from django.contrib.auth.models import User + +# Create your models here. + +class Plant(models.Model): + + class TextChoices(models.TextChoices): + INDOOR = 'Indoor', 'indoor' + OUTDOOR = 'Outdoor', 'outdoor' + SUCCULENT = 'Succulent', 'succulent' + FLOWERING = 'Flowering', 'flowering' + HERB = 'Herb', 'herb' + TREE = 'Tree', 'tree' + SHRUB = 'Shrub', 'shrub' + CACTUS = 'Cactus', 'cactus' + FERN = 'Fern', 'fern' + GRASS = 'Grass', 'grass' + + name = models.CharField(max_length=100) + about = models.TextField() + used_for = models.TextField() + image = models.ImageField(upload_to='media/') + category = models.CharField(max_length=20, choices=TextChoices.choices) + is_edible = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + countries = models.ManyToManyField('Country', related_name='plants') + + def __str__(self): + return self.name + + +class Review(models.Model): + plant = models.ForeignKey(Plant, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + comment = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.user.username + + +class Country(models.Model): + name = models.CharField(max_length=32) + flag = models.ImageField(upload_to='media/') + + def __str__(self): + return self.name \ No newline at end of file diff --git a/Planteer/plants/templates/plants/all.html b/Planteer/plants/templates/plants/all.html new file mode 100644 index 0000000..5421deb --- /dev/null +++ b/Planteer/plants/templates/plants/all.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} +

All Plants

+ +
+
+ + + + + +
+
+ + {% include 'plants/card_plants.html' %} + + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/card_plants.html b/Planteer/plants/templates/plants/card_plants.html new file mode 100644 index 0000000..63331c4 --- /dev/null +++ b/Planteer/plants/templates/plants/card_plants.html @@ -0,0 +1,29 @@ +
+ {% for plant in plants %} +
+
+
+ +
+ +
+ + +
{{ plant.name }}
+

{{ plant.about }}

+

{{ plant.category }}

+
+ +
+ {% for country in plant.countries.all %} + + {{ country.name }} + + {% endfor %} +
+ +
+
+
+ {% endfor %} +
\ No newline at end of file diff --git a/Planteer/plants/templates/plants/country_plants.html b/Planteer/plants/templates/plants/country_plants.html new file mode 100644 index 0000000..c9f7c25 --- /dev/null +++ b/Planteer/plants/templates/plants/country_plants.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block content %} + +

Plants Native to {{ country.name }}

+ + {% include 'plants/card_plants.html' %} + + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/delete.html b/Planteer/plants/templates/plants/delete.html new file mode 100644 index 0000000..d0bc876 --- /dev/null +++ b/Planteer/plants/templates/plants/delete.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +

Are you sure you want to delete {{ plant.name }}?

+ +
+ {% csrf_token %} + + Cancel +
+ +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/detail.html b/Planteer/plants/templates/plants/detail.html new file mode 100644 index 0000000..2ac991b --- /dev/null +++ b/Planteer/plants/templates/plants/detail.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+ +
+ +
+

{{ plant.name }}

+

{{ plant.category }}

+

{{ plant.about }}

+

Native to:

+ +
+ {% for c in plant.countries.all %} +
+ {% if c.flag %} + + {% endif %} + {{ c.name }} +
+ {% endfor %} +
+ +

Is Edible: + {% if plant.is_edible %} + Yes + {% else %} + No + {% endif %} +

+

Used For:
{{ plant.used_for }}

+ {% if request.user.is_staff %} + +
+ Edit + +
+ {% csrf_token %} + +
+
+ {% endif %} + +
+ +
+ + +
+ +

Comments

+
+ {% for review in reviews %} +
+

{{review.user.first_name}} {{review.user.last_name}}

+

{{review.comment}}

+

{{review.created_at}}

+
+ {% endfor %} +
+ + +{% if request.user.is_authenticated %} +

Add Comment

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

Related Plants

+ {% include 'plants/card_plants.html' %} + +{% endblock %} \ No newline at end of file diff --git a/Planteer/plants/templates/plants/form.html b/Planteer/plants/templates/plants/form.html new file mode 100644 index 0000000..d47275b --- /dev/null +++ b/Planteer/plants/templates/plants/form.html @@ -0,0 +1,55 @@ +{% extends 'base.html' %} + +{% block content %} +

{% if plant %}Update{% else %}Add{% endif %} Plant

+ +
+ {% csrf_token %} + + + + + + + + + + + +
Countries
+ +
+ {% for country in countries %} +
+ + +
+ {% endfor %} +
+ +
Edible
+ +
+ +
+ +{% if error %} +

{{ error }}

+{% endif %} + +{% 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..6fcac71 --- /dev/null +++ b/Planteer/plants/templates/plants/search.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} + +{% block content %} +
+ +

Search Results

+
+ + + +
+
+ +
+ + +
+ +
+ +
+ {% for plant in results %} +
+
+
+ +
+ +
+ + +
{{ plant.name }}
+

{{ plant.about }}

+

{{ plant.category }}

+
+ +
+ {% for country in plant.countries.all %} + + {{ country.name }} + + {% endfor %} +
+ +
+
+
+ {% 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..3329516 --- /dev/null +++ b/Planteer/plants/urls.py @@ -0,0 +1,15 @@ +from . import views +from django.urls import path + +app_name = 'plants' + +urlpatterns = [ + path('all/', views.all_plants_view, name='all_plants_view'), + path('/detail/', views.plant_detail_view, name='plant_detail_view'), + path('new/', views.add_plant_view, name='add_plant_view'), + path('/update/', views.update_plant_view, name='update_plant_view'), + path('/delete/', views.delete_plant_view, name='delete_plant_view'), + path('search/', views.search_plant_view, name='search_plant_view'), + path('reviews/add//', views.add_review_view, name='add_review_view'), + path('country/', views.country_plants_view, name='country_plants_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..0dcc801 --- /dev/null +++ b/Planteer/plants/views.py @@ -0,0 +1,151 @@ +from django.shortcuts import get_object_or_404, redirect, render +from django.http import HttpRequest, HttpResponse +from .models import Plant, Review, Country, User +from django.contrib import messages + +# Create your views here. + +def all_plants_view(request:HttpRequest): + plants = Plant.objects.all() + country_id = request.GET.get('country') + + category = request.GET.get('category') + edible = request.GET.get('is_edible') + + if category: + plants = plants.filter(category = category) + + if edible == 'true': + plants = plants.filter(is_edible = True) + + if country_id: + plants = Plant.objects.filter(countries__id=country_id) + else: + plants = Plant.objects.all() + + countries = Country.objects.all() + + + return render(request, 'plants/all.html', {'plants': plants, 'countries': countries}) + +def plant_detail_view(request: HttpRequest, plant_id): + plant = get_object_or_404(Plant, id=plant_id) + + related = Plant.objects.filter( + category=plant.category + ).exclude(id=plant_id)[0:3] + + reviews = Review.objects.filter(plant=plant) + + return render(request, 'plants/detail.html', { + 'plant': plant, + 'related': related, + 'reviews': reviews + }) + + +def add_plant_view(request): + + if not request.user.is_staff: + messages.warning(request, "only staff can add plants", "alert-warning") + return redirect("main:home_view") + + if request.method == "POST": + name = request.POST.get('name') + about = request.POST.get('about') + used_for = request.POST.get('used_for') + category = request.POST.get('category') + is_edible = request.POST.get('is_edible') == 'on' + image = request.FILES.get('image') + + # 🔥 إنشاء النبات أولاً + plant = Plant.objects.create( + name=name, + about=about, + used_for=used_for, + category=category, + is_edible=is_edible, + image=image + ) + + # 🔥 بعدها تربط الدول + countries_ids = request.POST.getlist('countries') + plant.countries.set(countries_ids) + + return redirect('plants:all_plants_view') + + countries = Country.objects.all() + return render(request, 'plants/form.html', { + 'countries': countries + }) + +def update_plant_view(request:HttpRequest, plant_id): + + if not request.user.is_staff: + messages.warning(request, "only staff can add plants", "alert-warning") + return redirect("main:home_view") + + plant = get_object_or_404(Plant, id=plant_id) + + if request.method == 'POST': + plant.name = request.POST.get('name') + plant.about = request.POST.get('about') + plant.used_for = request.POST.get('used_for') + plant.category = request.POST.get('category') + plant.is_edible = request.POST.get('is_edible') == 'on' + + if request.FILES.get('image'): + plant.image = request.FILES.get('image') + + plant.save() + return redirect('plant_detail', id=plant_id) + + return render(request, 'plants/form.html', {'plant': plant}) + +def delete_plant_view(request:HttpRequest, plant_id): + + if not request.user.is_staff: + messages.warning(request, "only staff can add plants", "alert-warning") + return redirect("main:home_view") + + plant = get_object_or_404(Plant, id=plant_id) + + if request.method == 'POST': + plant.delete() + return redirect('plants:all_plants_view') + + return render(request, 'plants/delete.html', {'plant': plant}) + +def search_plant_view(request:HttpRequest): + query = request.GET.get('q') + results = [] + + if query: + results = Plant.objects.filter(name__icontains=query) + + return render(request, 'plants/search.html', { + 'results': results, + 'query': query + }) + +def add_review_view(request:HttpRequest, plant_id): + + if not request.user.is_authenticated: + messages.error(request, "Only registered user can add review", "alert-danger") + return redirect("accounts:sign_in") + + if request.method == "POST": + plant_object = Plant.objects.get(id=plant_id) + new_review = Review(plant=plant_object,user=request.user, comment=request.POST["comment"]) + new_review.save() + + messages.success(request, "Added Review successfully", "alert-success") + + return redirect("plant:plant_detail_view", id=plant_id) + +def country_plants_view(request, country_id): + country = get_object_or_404(Country, id=country_id) + + plants = Plant.objects.filter(countries=country) + + return render(request, 'plants/country_plants.html', {'country': country, 'plants': plants}) \ No newline at end of file