diff --git a/Chapter08_User/Projects/Readme.md b/Chapter08_User/Projects/Readme.md
index d36b8ae..49c69dc 100644
--- a/Chapter08_User/Projects/Readme.md
+++ b/Chapter08_User/Projects/Readme.md
@@ -2,5 +2,5 @@
მაგალითი:
- სახელი გვარი | [პროექტი](/მისამართი)
-
+- giorgi kelenjeridze | [Food Order App](/Chapter08_User/Projects/giorgikelenjeridze/app.py)
### 2025 ზაფხული
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/app.py b/Chapter08_User/Projects/giorgikelenjeridze/app.py
new file mode 100644
index 0000000..ce3460f
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/app.py
@@ -0,0 +1,6 @@
+from src import create_app
+
+app = create_app()
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/README b/Chapter08_User/Projects/giorgikelenjeridze/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py
new file mode 100644
index 0000000..4c97092
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
new file mode 100644
index 0000000..4476bfd
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
@@ -0,0 +1,32 @@
+"""Added description to Product
+
+Revision ID: 2be0fc404953
+Revises:
+Create Date: 2025-05-31 19:42:43.908055
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2be0fc404953'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('description', sa.String(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.drop_column('description')
+
+ # ### end Alembic commands ###
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
new file mode 100644
index 0000000..5ab938d
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
@@ -0,0 +1,38 @@
+"""Changed field
+
+Revision ID: 9b4f3a94e264
+Revises: 2be0fc404953
+Create Date: 2025-06-02 15:49:00.933969
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '9b4f3a94e264'
+down_revision = '2be0fc404953'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('_password', sa.String(), nullable=True))
+ batch_op.create_unique_constraint(None, ['email'])
+ batch_op.create_unique_constraint(None, ['username'])
+ batch_op.drop_column('password')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True))
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_column('_password')
+
+ # ### end Alembic commands ###
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py
new file mode 100644
index 0000000..93fd1c1
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/__init__.py
@@ -0,0 +1,50 @@
+from flask import Flask
+from src.config import Config
+from src.ext import db, migrate, login_manager
+from src.view import product_blueprint, user_blueprint, main_blueprint
+from src.commands import init_db, populate_db
+from src.models import User
+
+Blueprints = [product_blueprint, user_blueprint, main_blueprint]
+Commands = [init_db, populate_db]
+
+
+def create_app():
+
+ app = Flask(__name__)
+ app.config.from_object(Config)
+
+ register_extensions(app)
+ register_blueprints(app)
+ register_commands(app)
+
+ return app
+
+
+def register_extensions(app):
+
+ # Flask-SQLAlchemy
+ db.init_app(app)
+
+ # Flask-Migrate
+ migrate.init_app(app, db)
+
+ # Flask-LoginManager
+ login_manager.init_app(app)
+ login_manager.login_view = 'user.login'
+
+ @login_manager.user_loader
+ def load_user(_id):
+ return User.query.get(_id)
+
+
+def register_blueprints(app):
+ for blueprint in Blueprints:
+ app.register_blueprint(blueprint)
+
+
+def register_commands(app):
+ for command in Commands:
+ app.cli.add_command(command)
+
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py b/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py
new file mode 100644
index 0000000..8cfb5fa
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/commands.py
@@ -0,0 +1,80 @@
+from flask.cli import with_appcontext
+import datetime
+import click
+from src.ext import db
+from src.models import Product, Person, IDcard
+
+
+@click.command("init_db")
+@with_appcontext
+def init_db():
+ click.echo("Initializing db...")
+
+ db.drop_all()
+ db.create_all()
+
+ click.echo("Initializing is over!")
+
+
+@click.command("populate_db")
+@with_appcontext
+def populate_db():
+
+
+ products = [
+ {
+ "id": 0,
+ "name": "შაურმა პატარა",
+ "price": 11,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 1,
+ "name": "შაურმა სტანდარტი",
+ "price": 13,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 2,
+ "name": "შაურმა დიდი",
+ "price": 18,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 3,
+ "name": "კოლა",
+ "price": 3,
+ "image": "cola.jfif",
+ "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ"
+ },
+ {
+ "id": 4,
+ "name": "სპრაიტი",
+ "price": 3,
+ "image": "sprite.jfif",
+ "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ"
+ },
+ {
+ "id": 5,
+ "name": "ბურგერი",
+ "price": 10,
+ "image": "burger.png",
+ "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი"
+ }
+ ]
+
+ for product in products:
+ new_product = Product(name=product["name"], price=product["price"], description=product["description"],
+ image=product["image"])
+ db.session.add(new_product)
+
+ idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now())
+ db.session.add(idcard)
+ db.session.commit()
+ person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id)
+ db.session.add(person)
+ db.session.commit()
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/config.py b/Chapter08_User/Projects/giorgikelenjeridze/src/config.py
new file mode 100644
index 0000000..abbf13b
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/config.py
@@ -0,0 +1,14 @@
+from os import environ
+
+
+class Config:
+
+ DB_USER = environ.get("DB_USER")
+ DB_PASSWORD = environ.get("DB_PASSWORD")
+ DB_DATABASE = environ.get("DB_DATABASE")
+
+ print(DB_USER)
+ print(DB_PASSWORD)
+ print(DB_DATABASE)
+ SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}"
+ SECRET_KEY = environ.get("SECRET_KEY")
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py b/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py
new file mode 100644
index 0000000..6377be4
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/ext.py
@@ -0,0 +1,7 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager
+
+db = SQLAlchemy()
+migrate = Migrate()
+login_manager = LoginManager()
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py
new file mode 100644
index 0000000..9e8a271
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/__init__.py
@@ -0,0 +1,4 @@
+from src.models.base import BaseModel
+from src.models.person import Person, IDcard
+from src.models.product import Product
+from src.models.user import User
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py
new file mode 100644
index 0000000..6f33fa7
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/base.py
@@ -0,0 +1,19 @@
+from src.ext import db
+
+
+class BaseModel(db.Model):
+
+ __abstract__ = True
+
+ def create(self, commit=True):
+ db.session.add(self)
+ if commit:
+ self.save()
+
+ def save(self):
+ db.session.commit()
+
+ def delete(self, commit=True):
+ db.session.delete(self)
+ if commit:
+ self.save()
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py
new file mode 100644
index 0000000..93875a4
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/person.py
@@ -0,0 +1,26 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Person(BaseModel):
+
+ __tablename__ = "persons"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ surname = db.Column(db.String)
+ birthday = db.Column(db.Date)
+
+ idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id"))
+
+ idcard = db.relationship("IDcard", back_populates="person")
+
+
+class IDcard(BaseModel):
+
+ __tablename__ = "idcards"
+ id = db.Column(db.Integer, primary_key=True)
+ serial_number = db.Column(db.String)
+ expiry_data = db.Column(db.Date)
+
+ person = db.relationship("Person", back_populates="idcard", uselist=False)
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py
new file mode 100644
index 0000000..da5ae3c
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/product.py
@@ -0,0 +1,22 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Product(BaseModel):
+
+ __tablename__ = "products"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ price = db.Column(db.Float)
+ description = db.Column(db.String)
+ image = db.Column(db.String)
+
+ def __init__(self, name, price, description, image):
+ self.name = name
+ self.price = price
+ self.description = description
+ self.image = image
+
+ def __repr__(self):
+ return f"This is {self.name} and costs {self.price}$"
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py b/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py
new file mode 100644
index 0000000..1b43670
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/models/user.py
@@ -0,0 +1,38 @@
+from src.ext import db
+from src.models import BaseModel
+from flask_login import UserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+
+
+class User(BaseModel, UserMixin):
+
+ __tablename__ = "users"
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String, unique=True)
+ email = db.Column(db.String, unique=True)
+ _password = db.Column(db.String)
+
+ def __init__(self, username, email, password):
+ self.username = username
+ self.email = email
+ self.password = password
+
+ def __repr__(self):
+ return f"User name: {self.username}"
+
+ def edit(self, form):
+ self.username = form.username.data
+ self.password = form.password.data
+ db.session.commit()
+
+ def check_password(self, password):
+ return check_password_hash(self.password, password)
+
+ @property
+ def password(self):
+ return self._password
+
+ @password.setter
+ def password(self, password):
+ self._password = generate_password_hash(password)
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png
new file mode 100644
index 0000000..c5bb973
Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/burger.png differ
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif
new file mode 100644
index 0000000..4d83f30
Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif
new file mode 100644
index 0000000..9cc4780
Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif
new file mode 100644
index 0000000..e775e42
Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif
new file mode 100644
index 0000000..128f4c4
Binary files /dev/null and b/Chapter08_User/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html
new file mode 100644
index 0000000..4674ec6
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/macros.html
@@ -0,0 +1,5 @@
+{% macro is_active(endpoint) %}
+ {% if endpoint==request.endpoint %}
+ active
+ {% endif %}
+{% endmacro %}
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html
new file mode 100644
index 0000000..792387f
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/feedback.html
@@ -0,0 +1,72 @@
+{% extends "partials/base.html" %}
+{% block title %}feedback{% endblock %}
+{% block body %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html
new file mode 100644
index 0000000..cd35a37
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/main/index.html
@@ -0,0 +1,48 @@
+{% extends "partials/base.html" %}
+{% block title %}Home{% endblock %}
+{% block body %}
+
+
+
My Simple "Food Order" Web Application
+
+

+
+
+
+
+
+
+
+
+
+
ჩვენი ფილიალები
+
+ - გლდანი, ა მიკრო
+ - გლანი, I მიკრო
+ - საბურთალო
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html
new file mode 100644
index 0000000..6821ae2
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/base.html
@@ -0,0 +1,33 @@
+
+
+
+
+ Kelenjo {% block title %} {% endblock %}
+
+
+
+
+ {% include "partials/navbar.html" %}
+{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #}
+
+{% if get_flashed_messages(with_categories=True) %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+
+
+ {{message}}
+
+{% endfor %}
+{% endif %}
+ {% block body %}
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
new file mode 100644
index 0000000..0bccff3
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
@@ -0,0 +1,23 @@
+{% from "macros.html" import is_active %}
+
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html
new file mode 100644
index 0000000..d696014
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/product/view_product.html
@@ -0,0 +1,23 @@
+{% extends "partials/base.html" %}
+
+{% block title %}{{product['name']}}{% endblock %}
+
+{% block body %}
+
+
+
+
+

+
+
{{ product.name }}
+
{{ product.description }}
+
+
${{ product.price }}
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html
new file mode 100644
index 0000000..6454ecc
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/login.html
@@ -0,0 +1,30 @@
+{% extends "partials/base.html" %}
+{% block title%}Log In{% endblock %}
+
+{% block body %}
+
+{% endblock %}
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html
new file mode 100644
index 0000000..d611686
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/templates/user/register.html
@@ -0,0 +1,46 @@
+{% extends "partials/base.html" %}
+{% block title %}Register{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
Create Your Account
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py
new file mode 100644
index 0000000..fc5db26
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/__init__.py
@@ -0,0 +1,3 @@
+from src.view.product.routes import product_blueprint
+from src.view.user.routes import user_blueprint
+from src.view.main.routes import main_blueprint
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py
new file mode 100644
index 0000000..79835cc
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/main/routes.py
@@ -0,0 +1,15 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+main_blueprint = Blueprint("main", __name__)
+
+
+@main_blueprint.route("/")
+def index():
+ products = Product.query.all()
+ return render_template("main/index.html", products=products)
+
+
+@main_blueprint.route("/feedback")
+def feedback():
+ return render_template("main/feedback.html")
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py
new file mode 100644
index 0000000..a2f8d83
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/product/routes.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+product_blueprint = Blueprint("product", __name__)
+
+
+@product_blueprint.route("/view/")
+def view(product_id):
+ product = Product.query.get(product_id)
+ print(product)
+ return render_template("product/view_product.html", product=product)
\ No newline at end of file
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py
new file mode 100644
index 0000000..eda7766
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/forms.py
@@ -0,0 +1,39 @@
+from flask_wtf import FlaskForm
+from wtforms.fields import StringField, PasswordField, EmailField, SubmitField
+from wtforms.validators import DataRequired, length, ValidationError
+from string import ascii_lowercase, ascii_uppercase, digits
+
+
+class RegisterForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)])
+ email = EmailField("Enter email", validators=[DataRequired()])
+ password = PasswordField("Enter password", validators=[DataRequired()])
+
+ submit = SubmitField("Register")
+
+ def validate_password(self, field):
+ contains_upcase = False
+ contains_lowcase = False
+ contains_digits = False
+
+ for char in field.data:
+ if char in ascii_uppercase:
+ contains_upcase=True
+ if char in ascii_lowercase:
+ contains_lowcase=True
+ if char in digits:
+ contains_digits=True
+
+ if contains_upcase == False:
+ raise ValidationError("Password must contain UpperCase Letter")
+ if contains_lowcase == False:
+ raise ValidationError("Password must contain LowerCase Letter")
+ if contains_digits == False:
+ raise ValidationError("Password must contain Digits Letter")
+
+
+class LoginForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired()])
+ password = PasswordField("Enter Password", validators=[DataRequired()])
+
+ submit = SubmitField("Log in")
diff --git a/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py
new file mode 100644
index 0000000..c62d8ce
--- /dev/null
+++ b/Chapter08_User/Projects/giorgikelenjeridze/src/view/user/routes.py
@@ -0,0 +1,78 @@
+from flask import Blueprint, render_template, flash, redirect, url_for, request
+from src.view.user.forms import RegisterForm, LoginForm
+from src.models.user import User
+from flask_login import login_user, logout_user, login_required
+
+
+user_blueprint = Blueprint("user", __name__)
+
+
+@user_blueprint.route("/register", methods=['GET', 'POST'])
+def register():
+ form = RegisterForm()
+ if form.validate_on_submit():
+ print(form.username.data)
+ print(form.email.data)
+ print(form.password.data)
+ flash("Sagol shen daregistrirdi", "info")
+ print('ar xar')
+ user = User(form.username.data, form.email.data, form.password.data)
+ user.create()
+ # return render_template('index.html')
+ else:
+ print('bad xar1')
+ print(form.errors)
+ print('bad xaar2')
+ return render_template("user/register.html", form=form)
+
+
+@user_blueprint.route("/login", methods=['GET', 'POST'])
+def login():
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter(User.username == form.username.data).first()
+ if user and user.check_password(form.password.data):
+ login_user(user)
+
+ next = request.args.get("next")
+
+ if next:
+ return redirect(next)
+ flash("Logged in!", "success")
+ return redirect(url_for("main.index"))
+ else:
+ flash("Username or Password is incorrect", "danger")
+
+ return render_template("user/login.html", form=form)
+
+
+@user_blueprint.route("/logout")
+@login_required
+def logout():
+ logout_user()
+ return redirect(url_for("main.index"))
+
+
+@user_blueprint.route("/edit/", methods=['GET', 'POST'])
+@login_required
+def edit(user_id):
+ user = User.query.get(user_id)
+ oldname = user.username
+ form = RegisterForm(username=user.username, email=user.email, password=user.password)
+ if form.validate_on_submit():
+ user.edit(form)
+ print(f"{oldname} has changed it own name to {user.username}")
+ else:
+ print(form.errors)
+ return render_template("user/register.html", form=form)
+
+
+@user_blueprint.route("/delete/")
+@login_required
+def delete(user_id):
+ usertodel = User.query.get(user_id)
+ User.query.filter_by()
+ print(usertodel)
+ usertodel.delete()
+ print(url_for("index"))
+ return redirect(url_for("main.index"))
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/Readme.md b/Chapter09_Admin/Projects/Readme.md
index b684ad0..b3bc3f4 100644
--- a/Chapter09_Admin/Projects/Readme.md
+++ b/Chapter09_Admin/Projects/Readme.md
@@ -2,5 +2,5 @@
მაგალითი:
- სახელი გვარი | [პროექტი](/მისამართი)
-
+- giorgi kelenjeridze | [Food Order app](/Chapter09_Admin/Projects/giorgikelenjeridze/app.py)
### 2025 ზაფხული
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/app.py b/Chapter09_Admin/Projects/giorgikelenjeridze/app.py
new file mode 100644
index 0000000..ce3460f
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/app.py
@@ -0,0 +1,6 @@
+from src import create_app
+
+app = create_app()
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt
new file mode 100644
index 0000000..a37387c
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/dev-requirements.txt differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py
new file mode 100644
index 0000000..4c97092
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
new file mode 100644
index 0000000..4476bfd
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
@@ -0,0 +1,32 @@
+"""Added description to Product
+
+Revision ID: 2be0fc404953
+Revises:
+Create Date: 2025-05-31 19:42:43.908055
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2be0fc404953'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('description', sa.String(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.drop_column('description')
+
+ # ### end Alembic commands ###
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
new file mode 100644
index 0000000..5ab938d
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
@@ -0,0 +1,38 @@
+"""Changed field
+
+Revision ID: 9b4f3a94e264
+Revises: 2be0fc404953
+Create Date: 2025-06-02 15:49:00.933969
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '9b4f3a94e264'
+down_revision = '2be0fc404953'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('_password', sa.String(), nullable=True))
+ batch_op.create_unique_constraint(None, ['email'])
+ batch_op.create_unique_constraint(None, ['username'])
+ batch_op.drop_column('password')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True))
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_column('_password')
+
+ # ### end Alembic commands ###
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt
new file mode 100644
index 0000000..3bc5b0f
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/prod-requirements.txt differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py
new file mode 100644
index 0000000..23d4bda
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/__init__.py
@@ -0,0 +1,60 @@
+from flask import Flask
+from src.admin_views.base import SecureModelView
+from src.admin_views import UserView, ProductView
+from src.config import Config
+from src.ext import db, migrate, login_manager, admin
+from src.view import product_blueprint, auth_blueprint, main_blueprint
+from src.commands import init_db, populate_db
+from src.models import User, Product
+from flask_admin.menu import MenuLink
+
+Blueprints = [product_blueprint, auth_blueprint, main_blueprint]
+Commands = [init_db, populate_db]
+
+
+def create_app():
+
+ app = Flask(__name__)
+ app.config.from_object(Config)
+
+ register_extensions(app)
+ register_blueprints(app)
+ register_commands(app)
+
+ return app
+
+
+def register_extensions(app):
+
+ # Flask-SQLAlchemy
+ db.init_app(app)
+
+ # Flask-Migrate
+ migrate.init_app(app, db)
+
+ # Flask-LoginManager
+ login_manager.init_app(app)
+ login_manager.login_view = 'user.login'
+
+ @login_manager.user_loader
+ def load_user(_id):
+ return User.query.get(_id)
+
+ # Flask-Admin
+ admin.init_app(app)
+ admin.add_view(UserView(User, db.session))
+ admin.add_view(ProductView(Product, db.session, endpoint="product_admin"))
+
+ admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out"))
+
+
+def register_blueprints(app):
+ for blueprint in Blueprints:
+ app.register_blueprint(blueprint)
+
+
+def register_commands(app):
+ for command in Commands:
+ app.cli.add_command(command)
+
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py
new file mode 100644
index 0000000..8e52cd1
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/__init__.py
@@ -0,0 +1,2 @@
+from src.admin_views.user import UserView
+from src.admin_views.product import ProductView
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py
new file mode 100644
index 0000000..4f3c2d9
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/base.py
@@ -0,0 +1,24 @@
+from flask_admin.contrib.sqla import ModelView
+from flask_admin import AdminIndexView
+from flask_login import current_user
+from flask import redirect, url_for
+
+
+class SecureModelView(ModelView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
+class SecureIndexView(AdminIndexView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py
new file mode 100644
index 0000000..950ef52
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/product.py
@@ -0,0 +1,33 @@
+from src.admin_views.base import SecureModelView
+from src.config import Config
+
+from flask_admin.form import ImageUploadField
+from os import path
+from uuid import uuid4
+from markupsafe import Markup
+
+
+def generate_filename(obj, file):
+ name, extension = path.splitext(file.filename)
+ return f"{uuid4()}{extension}"
+
+
+class ProductView(SecureModelView):
+ create_modal = True
+ edit_modal = True
+
+ column_editable_list = ("name", "price", "description")
+ column_filters = ["price",]
+
+ column_formatters = {
+ "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...",
+ "image": lambda v,c,m,n: Markup(f"
")
+ }
+
+ form_overrides = {"image": ImageUploadField}
+ form_args = {
+ "image": {
+ "base_path": Config.UPLOAD_PATH,
+ "namegen": generate_filename
+ }
+ }
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py
new file mode 100644
index 0000000..22dbec3
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/admin_views/user.py
@@ -0,0 +1,10 @@
+from src.admin_views.base import SecureModelView
+
+
+class UserView(SecureModelView):
+ can_view_details = True
+ can_delete = False
+
+ column_list = ("username", "email")
+ column_details_list = ("username", "email", "_password")
+ column_searchable_list = ["username"]
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py
new file mode 100644
index 0000000..8cfb5fa
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/commands.py
@@ -0,0 +1,80 @@
+from flask.cli import with_appcontext
+import datetime
+import click
+from src.ext import db
+from src.models import Product, Person, IDcard
+
+
+@click.command("init_db")
+@with_appcontext
+def init_db():
+ click.echo("Initializing db...")
+
+ db.drop_all()
+ db.create_all()
+
+ click.echo("Initializing is over!")
+
+
+@click.command("populate_db")
+@with_appcontext
+def populate_db():
+
+
+ products = [
+ {
+ "id": 0,
+ "name": "შაურმა პატარა",
+ "price": 11,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 1,
+ "name": "შაურმა სტანდარტი",
+ "price": 13,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 2,
+ "name": "შაურმა დიდი",
+ "price": 18,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 3,
+ "name": "კოლა",
+ "price": 3,
+ "image": "cola.jfif",
+ "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ"
+ },
+ {
+ "id": 4,
+ "name": "სპრაიტი",
+ "price": 3,
+ "image": "sprite.jfif",
+ "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ"
+ },
+ {
+ "id": 5,
+ "name": "ბურგერი",
+ "price": 10,
+ "image": "burger.png",
+ "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი"
+ }
+ ]
+
+ for product in products:
+ new_product = Product(name=product["name"], price=product["price"], description=product["description"],
+ image=product["image"])
+ db.session.add(new_product)
+
+ idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now())
+ db.session.add(idcard)
+ db.session.commit()
+ person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id)
+ db.session.add(person)
+ db.session.commit()
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py
new file mode 100644
index 0000000..8b1ac4e
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/config.py
@@ -0,0 +1,17 @@
+from os import environ, path
+
+
+class Config:
+
+ DB_USER = environ.get("DB_USER")
+ DB_PASSWORD = environ.get("DB_PASSWORD")
+ DB_DATABASE = environ.get("DB_DATABASE")
+
+ # print(DB_USER)
+ # print(DB_PASSWORD)
+ # print(DB_DATABASE)
+ BASE_DIRECTORY = path.abspath(path.dirname(__file__))
+ UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets")
+ SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}"
+ SECRET_KEY = environ.get("SECRET_KEY")
+ FLASK_ADMIN_SWATCH="Flatly"
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py
new file mode 100644
index 0000000..b1e63af
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/ext.py
@@ -0,0 +1,10 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager
+from flask_admin import Admin
+from src.admin_views.base import SecureIndexView
+
+db = SQLAlchemy()
+migrate = Migrate()
+login_manager = LoginManager()
+admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html")
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py
new file mode 100644
index 0000000..9e8a271
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/__init__.py
@@ -0,0 +1,4 @@
+from src.models.base import BaseModel
+from src.models.person import Person, IDcard
+from src.models.product import Product
+from src.models.user import User
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py
new file mode 100644
index 0000000..6f33fa7
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/base.py
@@ -0,0 +1,19 @@
+from src.ext import db
+
+
+class BaseModel(db.Model):
+
+ __abstract__ = True
+
+ def create(self, commit=True):
+ db.session.add(self)
+ if commit:
+ self.save()
+
+ def save(self):
+ db.session.commit()
+
+ def delete(self, commit=True):
+ db.session.delete(self)
+ if commit:
+ self.save()
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py
new file mode 100644
index 0000000..93875a4
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/person.py
@@ -0,0 +1,26 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Person(BaseModel):
+
+ __tablename__ = "persons"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ surname = db.Column(db.String)
+ birthday = db.Column(db.Date)
+
+ idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id"))
+
+ idcard = db.relationship("IDcard", back_populates="person")
+
+
+class IDcard(BaseModel):
+
+ __tablename__ = "idcards"
+ id = db.Column(db.Integer, primary_key=True)
+ serial_number = db.Column(db.String)
+ expiry_data = db.Column(db.Date)
+
+ person = db.relationship("Person", back_populates="idcard", uselist=False)
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py
new file mode 100644
index 0000000..da5ae3c
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/product.py
@@ -0,0 +1,22 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Product(BaseModel):
+
+ __tablename__ = "products"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ price = db.Column(db.Float)
+ description = db.Column(db.String)
+ image = db.Column(db.String)
+
+ def __init__(self, name, price, description, image):
+ self.name = name
+ self.price = price
+ self.description = description
+ self.image = image
+
+ def __repr__(self):
+ return f"This is {self.name} and costs {self.price}$"
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py
new file mode 100644
index 0000000..476a7c0
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/models/user.py
@@ -0,0 +1,38 @@
+from src.ext import db
+from src.models import BaseModel
+from flask_login import UserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+
+
+class User(BaseModel, UserMixin):
+
+ __tablename__ = "users"
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String, unique=True)
+ email = db.Column(db.String, unique=True)
+ _password = db.Column(db.String)
+
+ def __init__(self, username, email, password):
+ self.username = username
+ self.email = email
+ self._password = password
+
+ def __repr__(self):
+ return f"User name: {self.username}"
+
+ def edit(self, form):
+ self.username = form.username.data
+ self.password = form.password.data
+ db.session.commit()
+
+ def check_password(self, password):
+ return check_password_hash(self.password, password)
+
+ @property
+ def password(self):
+ return self._password
+
+ @password.setter
+ def password(self, password):
+ self._password = generate_password_hash(password)
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png
new file mode 100644
index 0000000..61f8ccd
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png
new file mode 100644
index 0000000..c5bb973
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/burger.png differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif
new file mode 100644
index 0000000..4d83f30
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif
new file mode 100644
index 0000000..9cc4780
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif
new file mode 100644
index 0000000..e775e42
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif
new file mode 100644
index 0000000..128f4c4
Binary files /dev/null and b/Chapter09_Admin/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
new file mode 100644
index 0000000..7e73a33
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
@@ -0,0 +1,42 @@
+{% extends "admin/base.html" %}
+
+{% block page_body %}
+
+
+
+
+ {% block messages %}
+ {{ layout.messages() }}
+ {% endblock %}
+
+ {# store the jinja2 context for form_rules rendering logic #}
+ {% set render_ctx = h.resolve_ctx() %}
+
+ {% block body %}{% endblock %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html
new file mode 100644
index 0000000..6454ecc
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/login.html
@@ -0,0 +1,30 @@
+{% extends "partials/base.html" %}
+{% block title%}Log In{% endblock %}
+
+{% block body %}
+
+{% endblock %}
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html
new file mode 100644
index 0000000..d611686
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/auth/register.html
@@ -0,0 +1,46 @@
+{% extends "partials/base.html" %}
+{% block title %}Register{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
Create Your Account
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html
new file mode 100644
index 0000000..4674ec6
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/macros.html
@@ -0,0 +1,5 @@
+{% macro is_active(endpoint) %}
+ {% if endpoint==request.endpoint %}
+ active
+ {% endif %}
+{% endmacro %}
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html
new file mode 100644
index 0000000..792387f
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/feedback.html
@@ -0,0 +1,72 @@
+{% extends "partials/base.html" %}
+{% block title %}feedback{% endblock %}
+{% block body %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html
new file mode 100644
index 0000000..cd35a37
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/main/index.html
@@ -0,0 +1,48 @@
+{% extends "partials/base.html" %}
+{% block title %}Home{% endblock %}
+{% block body %}
+
+
+
My Simple "Food Order" Web Application
+
+

+
+
+
+
+
+
+
+
+
+
ჩვენი ფილიალები
+
+ - გლდანი, ა მიკრო
+ - გლანი, I მიკრო
+ - საბურთალო
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html
new file mode 100644
index 0000000..6821ae2
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/base.html
@@ -0,0 +1,33 @@
+
+
+
+
+ Kelenjo {% block title %} {% endblock %}
+
+
+
+
+ {% include "partials/navbar.html" %}
+{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #}
+
+{% if get_flashed_messages(with_categories=True) %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+
+
+ {{message}}
+
+{% endfor %}
+{% endif %}
+ {% block body %}
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
new file mode 100644
index 0000000..4e06a3e
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
@@ -0,0 +1,23 @@
+{% from "macros.html" import is_active %}
+
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html
new file mode 100644
index 0000000..d696014
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/templates/product/view_product.html
@@ -0,0 +1,23 @@
+{% extends "partials/base.html" %}
+
+{% block title %}{{product['name']}}{% endblock %}
+
+{% block body %}
+
+
+
+
+

+
+
{{ product.name }}
+
{{ product.description }}
+
+
${{ product.price }}
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py
new file mode 100644
index 0000000..7e31f71
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/__init__.py
@@ -0,0 +1,3 @@
+from src.view.product.routes import product_blueprint
+from src.view.auth.routes import auth_blueprint
+from src.view.main.routes import main_blueprint
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py
new file mode 100644
index 0000000..eda7766
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/forms.py
@@ -0,0 +1,39 @@
+from flask_wtf import FlaskForm
+from wtforms.fields import StringField, PasswordField, EmailField, SubmitField
+from wtforms.validators import DataRequired, length, ValidationError
+from string import ascii_lowercase, ascii_uppercase, digits
+
+
+class RegisterForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)])
+ email = EmailField("Enter email", validators=[DataRequired()])
+ password = PasswordField("Enter password", validators=[DataRequired()])
+
+ submit = SubmitField("Register")
+
+ def validate_password(self, field):
+ contains_upcase = False
+ contains_lowcase = False
+ contains_digits = False
+
+ for char in field.data:
+ if char in ascii_uppercase:
+ contains_upcase=True
+ if char in ascii_lowercase:
+ contains_lowcase=True
+ if char in digits:
+ contains_digits=True
+
+ if contains_upcase == False:
+ raise ValidationError("Password must contain UpperCase Letter")
+ if contains_lowcase == False:
+ raise ValidationError("Password must contain LowerCase Letter")
+ if contains_digits == False:
+ raise ValidationError("Password must contain Digits Letter")
+
+
+class LoginForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired()])
+ password = PasswordField("Enter Password", validators=[DataRequired()])
+
+ submit = SubmitField("Log in")
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py
new file mode 100644
index 0000000..a14c092
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/auth/routes.py
@@ -0,0 +1,78 @@
+from flask import Blueprint, render_template, flash, redirect, url_for, request
+from src.view.auth.forms import RegisterForm, LoginForm
+from src.models import User
+from flask_login import login_user, logout_user, login_required
+
+
+auth_blueprint = Blueprint("auth", __name__)
+
+
+@auth_blueprint.route("/register", methods=['GET', 'POST'])
+def register():
+ form = RegisterForm()
+ if form.validate_on_submit():
+ print(form.username.data)
+ print(form.email.data)
+ print(form.password.data)
+ flash("Sagol shen daregistrirdi", "info")
+ print('ar xar')
+ user = User(form.username.data, form.email.data, form.password.data)
+ user.create()
+ # return render_template('index.html')
+ else:
+ print('bad xar1')
+ print(form.errors)
+ print('bad xaar2')
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/login", methods=['GET', 'POST'])
+def login():
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter(User.username == form.username.data).first()
+ if user and user.check_password(form.password.data):
+ login_user(user)
+
+ next = request.args.get("next")
+
+ if next:
+ return redirect(next)
+ flash("Logged in!", "success")
+ return redirect(url_for("main.index"))
+ else:
+ flash("Username or Password is incorrect", "danger")
+
+ return render_template("auth/login.html", form=form)
+
+
+@auth_blueprint.route("/logout")
+@login_required
+def logout():
+ logout_user()
+ return redirect(url_for("main.index"))
+
+
+@auth_blueprint.route("/edit/", methods=['GET', 'POST'])
+@login_required
+def edit(user_id):
+ user = User.query.get(user_id)
+ oldname = user.username
+ form = RegisterForm(username=user.username, email=user.email, password=user.password)
+ if form.validate_on_submit():
+ user.edit(form)
+ print(f"{oldname} has changed it own name to {user.username}")
+ else:
+ print(form.errors)
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/delete/")
+@login_required
+def delete(user_id):
+ usertodel = User.query.get(user_id)
+ User.query.filter_by()
+ print(usertodel)
+ usertodel.delete()
+ print(url_for("index"))
+ return redirect(url_for("main.index"))
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py
new file mode 100644
index 0000000..79835cc
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/main/routes.py
@@ -0,0 +1,15 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+main_blueprint = Blueprint("main", __name__)
+
+
+@main_blueprint.route("/")
+def index():
+ products = Product.query.all()
+ return render_template("main/index.html", products=products)
+
+
+@main_blueprint.route("/feedback")
+def feedback():
+ return render_template("main/feedback.html")
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py
new file mode 100644
index 0000000..a2f8d83
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/src/view/product/routes.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+product_blueprint = Blueprint("product", __name__)
+
+
+@product_blueprint.route("/view/")
+def view(product_id):
+ product = Product.query.get(product_id)
+ print(product)
+ return render_template("product/view_product.html", product=product)
\ No newline at end of file
diff --git a/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf b/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf
new file mode 100644
index 0000000..13cb465
--- /dev/null
+++ b/Chapter09_Admin/Projects/giorgikelenjeridze/uwsgi.conf
@@ -0,0 +1,15 @@
+[uwsgi]
+
+master = true
+module = src:create_app()
+processes = 2
+threads = 4
+socket = :5000
+chmod-socket = 600
+die-on-term = true
+vacuum = true
+
+env = DB_DATABASE=python_intern-db
+env = DB_PASSWORD=Wyali!12345
+env = DB_USER=python_intern-user
+env = SECRET_KEY=MYS!GMASECRETKEY!@
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py
new file mode 100644
index 0000000..ce3460f
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py
@@ -0,0 +1,6 @@
+from src import create_app
+
+app = create_app()
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt
new file mode 100644
index 0000000..a37387c
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/dev-requirements.txt differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py
new file mode 100644
index 0000000..4c97092
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
new file mode 100644
index 0000000..4476bfd
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
@@ -0,0 +1,32 @@
+"""Added description to Product
+
+Revision ID: 2be0fc404953
+Revises:
+Create Date: 2025-05-31 19:42:43.908055
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2be0fc404953'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('description', sa.String(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.drop_column('description')
+
+ # ### end Alembic commands ###
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
new file mode 100644
index 0000000..5ab938d
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
@@ -0,0 +1,38 @@
+"""Changed field
+
+Revision ID: 9b4f3a94e264
+Revises: 2be0fc404953
+Create Date: 2025-06-02 15:49:00.933969
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '9b4f3a94e264'
+down_revision = '2be0fc404953'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('_password', sa.String(), nullable=True))
+ batch_op.create_unique_constraint(None, ['email'])
+ batch_op.create_unique_constraint(None, ['username'])
+ batch_op.drop_column('password')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True))
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_column('_password')
+
+ # ### end Alembic commands ###
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt
new file mode 100644
index 0000000..d79aec7
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/prod-requirements.txt differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py
new file mode 100644
index 0000000..1b7a8cf
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/__init__.py
@@ -0,0 +1,64 @@
+from flask import Flask
+from src.admin_views.base import SecureModelView
+from src.admin_views import UserView, ProductView
+from src.config import Config
+from src.ext import db, migrate, login_manager, admin
+from src.view import product_blueprint, auth_blueprint, main_blueprint
+from src.commands import init_db_command, populate_db_command
+from src.models import User, Product
+from flask_admin.menu import MenuLink
+
+Blueprints = [product_blueprint, auth_blueprint, main_blueprint]
+Commands = [init_db_command, populate_db_command]
+
+
+def create_app(test_config=None):
+
+ app = Flask(__name__)
+ app.config.from_object(Config)
+
+ # Then override with test config if passed
+ if test_config:
+ app.config.update(test_config)
+
+ register_extensions(app)
+ register_blueprints(app)
+ register_commands(app)
+
+ return app
+
+
+def register_extensions(app):
+
+ # Flask-SQLAlchemy
+ db.init_app(app)
+
+ # Flask-Migrate
+ migrate.init_app(app, db)
+
+ # Flask-LoginManager
+ login_manager.init_app(app)
+ login_manager.login_view = 'user.login'
+
+ @login_manager.user_loader
+ def load_user(_id):
+ return User.query.get(_id)
+
+ # Flask-Admin
+ admin.init_app(app)
+ admin.add_view(UserView(User, db.session))
+ admin.add_view(ProductView(Product, db.session, endpoint="product_admin"))
+
+ admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out"))
+
+
+def register_blueprints(app):
+ for blueprint in Blueprints:
+ app.register_blueprint(blueprint)
+
+
+def register_commands(app):
+ for command in Commands:
+ app.cli.add_command(command)
+
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py
new file mode 100644
index 0000000..8e52cd1
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/__init__.py
@@ -0,0 +1,2 @@
+from src.admin_views.user import UserView
+from src.admin_views.product import ProductView
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py
new file mode 100644
index 0000000..4f3c2d9
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/base.py
@@ -0,0 +1,24 @@
+from flask_admin.contrib.sqla import ModelView
+from flask_admin import AdminIndexView
+from flask_login import current_user
+from flask import redirect, url_for
+
+
+class SecureModelView(ModelView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
+class SecureIndexView(AdminIndexView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py
new file mode 100644
index 0000000..950ef52
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/product.py
@@ -0,0 +1,33 @@
+from src.admin_views.base import SecureModelView
+from src.config import Config
+
+from flask_admin.form import ImageUploadField
+from os import path
+from uuid import uuid4
+from markupsafe import Markup
+
+
+def generate_filename(obj, file):
+ name, extension = path.splitext(file.filename)
+ return f"{uuid4()}{extension}"
+
+
+class ProductView(SecureModelView):
+ create_modal = True
+ edit_modal = True
+
+ column_editable_list = ("name", "price", "description")
+ column_filters = ["price",]
+
+ column_formatters = {
+ "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...",
+ "image": lambda v,c,m,n: Markup(f"
")
+ }
+
+ form_overrides = {"image": ImageUploadField}
+ form_args = {
+ "image": {
+ "base_path": Config.UPLOAD_PATH,
+ "namegen": generate_filename
+ }
+ }
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py
new file mode 100644
index 0000000..22dbec3
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/admin_views/user.py
@@ -0,0 +1,10 @@
+from src.admin_views.base import SecureModelView
+
+
+class UserView(SecureModelView):
+ can_view_details = True
+ can_delete = False
+
+ column_list = ("username", "email")
+ column_details_list = ("username", "email", "_password")
+ column_searchable_list = ["username"]
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py
new file mode 100644
index 0000000..721340b
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/commands.py
@@ -0,0 +1,93 @@
+from flask.cli import with_appcontext
+import datetime
+import click
+from src.ext import db
+from src.models import Product, Person, IDcard, User
+
+
+def init_db():
+
+ db.drop_all()
+ db.create_all()
+
+
+@click.command("init_db")
+@with_appcontext
+def init_db_command():
+ click.echo("Initializing db...")
+
+ init_db()
+
+ click.echo("Initializing is over!")
+
+
+def populate_db():
+ products = [
+ {
+ "id": 0,
+ "name": "შაურმა პატარა",
+ "price": 11,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 1,
+ "name": "შაურმა სტანდარტი",
+ "price": 13,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 2,
+ "name": "შაურმა დიდი",
+ "price": 18,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 3,
+ "name": "კოლა",
+ "price": 3,
+ "image": "cola.jfif",
+ "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ"
+ },
+ {
+ "id": 4,
+ "name": "სპრაიტი",
+ "price": 3,
+ "image": "sprite.jfif",
+ "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ"
+ },
+ {
+ "id": 5,
+ "name": "ბურგერი",
+ "price": 10,
+ "image": "burger.png",
+ "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი"
+ }
+ ]
+
+ for product in products:
+ new_product = Product(name=product["name"], price=product["price"], description=product["description"],
+ image=product["image"])
+ db.session.add(new_product)
+
+ idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now())
+ db.session.add(idcard)
+ db.session.commit()
+ person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id)
+ db.session.add(person)
+ user = User("mari", "marikuna@gmail.com", "Mari123")
+ db.session.add(user)
+ db.session.commit()
+
+
+@click.command("populate_db")
+@with_appcontext
+def populate_db_command():
+
+ populate_db()
+
+
+
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py
new file mode 100644
index 0000000..dcb42c2
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/config.py
@@ -0,0 +1,17 @@
+from os import environ, path
+
+
+class Config:
+
+ DB_USER = environ.get("DB_USER")
+ DB_PASSWORD = environ.get("DB_PASSWORD")
+ DB_DATABASE = environ.get("DB_DATABASE")
+
+ # print(DB_USER)
+ # print(DB_PASSWORD)
+ # print(DB_DATABASE)
+ BASE_DIRECTORY = path.abspath(path.dirname(__file__))
+ UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets")
+ SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@localhost:5432/{DB_DATABASE}"
+ SECRET_KEY = environ.get("SECRET_KEY")
+ FLASK_ADMIN_SWATCH="Flatly"
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py
new file mode 100644
index 0000000..b1e63af
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/ext.py
@@ -0,0 +1,10 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager
+from flask_admin import Admin
+from src.admin_views.base import SecureIndexView
+
+db = SQLAlchemy()
+migrate = Migrate()
+login_manager = LoginManager()
+admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html")
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py
new file mode 100644
index 0000000..9e8a271
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/__init__.py
@@ -0,0 +1,4 @@
+from src.models.base import BaseModel
+from src.models.person import Person, IDcard
+from src.models.product import Product
+from src.models.user import User
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py
new file mode 100644
index 0000000..6f33fa7
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/base.py
@@ -0,0 +1,19 @@
+from src.ext import db
+
+
+class BaseModel(db.Model):
+
+ __abstract__ = True
+
+ def create(self, commit=True):
+ db.session.add(self)
+ if commit:
+ self.save()
+
+ def save(self):
+ db.session.commit()
+
+ def delete(self, commit=True):
+ db.session.delete(self)
+ if commit:
+ self.save()
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py
new file mode 100644
index 0000000..93875a4
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/person.py
@@ -0,0 +1,26 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Person(BaseModel):
+
+ __tablename__ = "persons"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ surname = db.Column(db.String)
+ birthday = db.Column(db.Date)
+
+ idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id"))
+
+ idcard = db.relationship("IDcard", back_populates="person")
+
+
+class IDcard(BaseModel):
+
+ __tablename__ = "idcards"
+ id = db.Column(db.Integer, primary_key=True)
+ serial_number = db.Column(db.String)
+ expiry_data = db.Column(db.Date)
+
+ person = db.relationship("Person", back_populates="idcard", uselist=False)
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py
new file mode 100644
index 0000000..da5ae3c
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/product.py
@@ -0,0 +1,22 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Product(BaseModel):
+
+ __tablename__ = "products"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ price = db.Column(db.Float)
+ description = db.Column(db.String)
+ image = db.Column(db.String)
+
+ def __init__(self, name, price, description, image):
+ self.name = name
+ self.price = price
+ self.description = description
+ self.image = image
+
+ def __repr__(self):
+ return f"This is {self.name} and costs {self.price}$"
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py
new file mode 100644
index 0000000..3a3129f
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/models/user.py
@@ -0,0 +1,38 @@
+from src.ext import db
+from src.models import BaseModel
+from flask_login import UserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+
+
+class User(BaseModel, UserMixin):
+
+ __tablename__ = "users"
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String, unique=True)
+ email = db.Column(db.String, unique=True)
+ _password = db.Column(db.String)
+
+ def __init__(self, username, email, password):
+ self.username = username
+ self.email = email
+ self._password = generate_password_hash(password)
+
+ def __repr__(self):
+ return f"User name: {self.username}"
+
+ def edit(self, form):
+ self.username = form.username.data
+ self.password = form.password.data
+ db.session.commit()
+
+ def check_password(self, password):
+ return check_password_hash(self.password, password)
+
+ @property
+ def password(self):
+ return self._password
+
+ @password.setter
+ def password(self, password):
+ self._password = generate_password_hash(password)
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png
new file mode 100644
index 0000000..61f8ccd
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png
new file mode 100644
index 0000000..c5bb973
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/burger.png differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif
new file mode 100644
index 0000000..4d83f30
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif
new file mode 100644
index 0000000..9cc4780
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif
new file mode 100644
index 0000000..e775e42
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif
new file mode 100644
index 0000000..128f4c4
Binary files /dev/null and b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
new file mode 100644
index 0000000..7e73a33
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
@@ -0,0 +1,42 @@
+{% extends "admin/base.html" %}
+
+{% block page_body %}
+
+
+
+
+ {% block messages %}
+ {{ layout.messages() }}
+ {% endblock %}
+
+ {# store the jinja2 context for form_rules rendering logic #}
+ {% set render_ctx = h.resolve_ctx() %}
+
+ {% block body %}{% endblock %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html
new file mode 100644
index 0000000..6454ecc
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/login.html
@@ -0,0 +1,30 @@
+{% extends "partials/base.html" %}
+{% block title%}Log In{% endblock %}
+
+{% block body %}
+
+{% endblock %}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html
new file mode 100644
index 0000000..d611686
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/auth/register.html
@@ -0,0 +1,46 @@
+{% extends "partials/base.html" %}
+{% block title %}Register{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
Create Your Account
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html
new file mode 100644
index 0000000..4674ec6
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/macros.html
@@ -0,0 +1,5 @@
+{% macro is_active(endpoint) %}
+ {% if endpoint==request.endpoint %}
+ active
+ {% endif %}
+{% endmacro %}
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html
new file mode 100644
index 0000000..792387f
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/feedback.html
@@ -0,0 +1,72 @@
+{% extends "partials/base.html" %}
+{% block title %}feedback{% endblock %}
+{% block body %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html
new file mode 100644
index 0000000..cd35a37
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/main/index.html
@@ -0,0 +1,48 @@
+{% extends "partials/base.html" %}
+{% block title %}Home{% endblock %}
+{% block body %}
+
+
+
My Simple "Food Order" Web Application
+
+

+
+
+
+
+
+
+
+
+
+
ჩვენი ფილიალები
+
+ - გლდანი, ა მიკრო
+ - გლანი, I მიკრო
+ - საბურთალო
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html
new file mode 100644
index 0000000..6821ae2
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/base.html
@@ -0,0 +1,33 @@
+
+
+
+
+ Kelenjo {% block title %} {% endblock %}
+
+
+
+
+ {% include "partials/navbar.html" %}
+{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #}
+
+{% if get_flashed_messages(with_categories=True) %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+
+
+ {{message}}
+
+{% endfor %}
+{% endif %}
+ {% block body %}
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
new file mode 100644
index 0000000..4e06a3e
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
@@ -0,0 +1,23 @@
+{% from "macros.html" import is_active %}
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html
new file mode 100644
index 0000000..57051ab
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/templates/product/view_product.html
@@ -0,0 +1,29 @@
+{% extends "partials/base.html" %}
+
+{% block title %}{{ product.name if product is not none else "Product Not Found" }}{% endblock %}
+
+{% block body %}
+
+
+
+
+ {% if product is not none %}
+

+
+
{{ product.name }}
+
{{ product.description }}
+
+
${{ product.price }}
+
+
+ {% else %}
+
+
Product not found
+
The product you are looking for does not exist.
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py
new file mode 100644
index 0000000..7e31f71
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/__init__.py
@@ -0,0 +1,3 @@
+from src.view.product.routes import product_blueprint
+from src.view.auth.routes import auth_blueprint
+from src.view.main.routes import main_blueprint
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py
new file mode 100644
index 0000000..eda7766
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/forms.py
@@ -0,0 +1,39 @@
+from flask_wtf import FlaskForm
+from wtforms.fields import StringField, PasswordField, EmailField, SubmitField
+from wtforms.validators import DataRequired, length, ValidationError
+from string import ascii_lowercase, ascii_uppercase, digits
+
+
+class RegisterForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)])
+ email = EmailField("Enter email", validators=[DataRequired()])
+ password = PasswordField("Enter password", validators=[DataRequired()])
+
+ submit = SubmitField("Register")
+
+ def validate_password(self, field):
+ contains_upcase = False
+ contains_lowcase = False
+ contains_digits = False
+
+ for char in field.data:
+ if char in ascii_uppercase:
+ contains_upcase=True
+ if char in ascii_lowercase:
+ contains_lowcase=True
+ if char in digits:
+ contains_digits=True
+
+ if contains_upcase == False:
+ raise ValidationError("Password must contain UpperCase Letter")
+ if contains_lowcase == False:
+ raise ValidationError("Password must contain LowerCase Letter")
+ if contains_digits == False:
+ raise ValidationError("Password must contain Digits Letter")
+
+
+class LoginForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired()])
+ password = PasswordField("Enter Password", validators=[DataRequired()])
+
+ submit = SubmitField("Log in")
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py
new file mode 100644
index 0000000..ec57fcb
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/auth/routes.py
@@ -0,0 +1,78 @@
+from flask import Blueprint, render_template, flash, redirect, url_for, request
+from src.view.auth.forms import RegisterForm, LoginForm
+from src.models import User
+from flask_login import login_user, logout_user, login_required
+
+
+auth_blueprint = Blueprint("auth", __name__)
+
+
+@auth_blueprint.route("/register", methods=['GET', 'POST'])
+def register():
+ form = RegisterForm()
+ if form.validate_on_submit():
+ print(form.username.data)
+ print(form.email.data)
+ print(form.password.data)
+ flash("Sagol shen daregistrirdi", "info")
+ print('ar xar')
+ user = User(form.username.data, form.email.data, form.password.data)
+ user.create()
+ # return render_template('index.html')
+ else:
+ print('bad xar1')
+ print(form.errors)
+ print('bad xaar2')
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/login", methods=['GET', 'POST'])
+def login():
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter(User.username == form.username.data).first()
+ if user and user.check_password(form.password.data):
+ login_user(user)
+
+ next = request.args.get("next")
+
+ if next:
+ return redirect(next)
+ flash("Logged in!", "success")
+ return redirect(url_for("main.index"))
+ else:
+ flash("Username or Password is incorrect", "danger")
+
+ return render_template("auth/login.html", form=form)
+
+
+@auth_blueprint.route("/logout")
+@login_required
+def logout():
+ logout_user()
+ return redirect(url_for("main.index"))
+
+
+@auth_blueprint.route("/edit/", methods=['GET', 'POST'])
+@login_required
+def edit(user_id):
+ user = User.query.get(user_id)
+ oldname = user.username
+ form = RegisterForm(username=user.username, email=user.email, password=user.password)
+ if form.validate_on_submit():
+ user.edit(form)
+ print(f"{oldname} has changed it own name to {user.username}")
+ else:
+ print(form.errors)
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/delete/")
+@login_required
+def delete(user_id):
+ usertodel = User.query.get(user_id)
+ # User.query.filter_by()
+ print(usertodel)
+ usertodel.delete()
+ print(url_for("main.index"))
+ return redirect(url_for("main.index"))
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py
new file mode 100644
index 0000000..79835cc
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/main/routes.py
@@ -0,0 +1,15 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+main_blueprint = Blueprint("main", __name__)
+
+
+@main_blueprint.route("/")
+def index():
+ products = Product.query.all()
+ return render_template("main/index.html", products=products)
+
+
+@main_blueprint.route("/feedback")
+def feedback():
+ return render_template("main/feedback.html")
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py
new file mode 100644
index 0000000..a2f8d83
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/src/view/product/routes.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+product_blueprint = Blueprint("product", __name__)
+
+
+@product_blueprint.route("/view/")
+def view(product_id):
+ product = Product.query.get(product_id)
+ print(product)
+ return render_template("product/view_product.html", product=product)
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py
new file mode 100644
index 0000000..d4526e9
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/auth/test_auth.py
@@ -0,0 +1,76 @@
+from flask_login import current_user
+from src.models import User
+
+
+def test_login(client):
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ print(client.get().data)
+ assert current_user.is_authenticated == True
+
+ client.get('/logout')
+ assert current_user.is_authenticated == False
+
+ client.post("/login", data={"username": "mari", "password": "Mari13"})
+ print(client.get().data)
+ assert current_user.is_authenticated == False
+
+
+def test_register_get(client):
+ response = client.get("/register")
+ assert response.status_code == 200
+ assert b"Register" in response.data or b"username" in response.data
+
+
+def test_register_post_valid(client, app):
+ response = client.post("/register", data={
+ "username": "gio",
+ "email": "gio@gmail.com",
+ "password": "Gio123"
+ }, follow_redirects=True)
+
+ assert b"Sagol shen daregistrirdi" in response.data
+
+ with app.app_context():
+ user = User.query.filter_by(username="gio").first()
+ assert user is not None
+ assert user.email == "gio@gmail.com"
+
+
+def test_logout(client, app):
+ # with app.app_context():
+ # user = User("mari", "mari@gmail.com", "Mari123")
+ # user.create()
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ assert current_user.is_authenticated == True
+
+ response = client.get("/logout", follow_redirects=True)
+ assert not current_user.is_authenticated
+
+
+def test_edit(client, app):
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+
+ client.post("/edit/1", data={"username": "marikuna", "email": "mari@gmail.com", "password": "Mari1234"})
+
+ with app.app_context():
+ user = User.query.get(1)
+ assert user.username=="marikuna"
+
+
+def test_delete(client, app):
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ response = client.get(f'/delete/{1}', follow_redirects=True)
+ assert response.status_code == 200
+
+ with app.app_context():
+ user = User.query.get(1)
+ assert user is None
+
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py
new file mode 100644
index 0000000..30593c7
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/conftest.py
@@ -0,0 +1,43 @@
+import tempfile
+import os
+import pytest
+from src import create_app
+from src.ext import admin
+from src.commands import init_db, populate_db
+
+
+@pytest.fixture
+def app():
+ # Create a temporary file
+ db_fd, db_path = tempfile.mkstemp()
+
+ test_config = {
+ "TESTING": True,
+ "WTF_CSRF_ENABLED": False,
+ "DEBUG": False,
+ "SECRET_KEY": "test-secret-key",
+ "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}.sqlite", # Use temporary SQLite file
+ }
+
+ app = create_app(test_config=test_config)
+ admin._views = []
+
+ with app.app_context():
+ init_db()
+ populate_db()
+
+ yield app
+
+ # Cleanup
+ os.close(db_fd)
+ os.unlink(db_path)
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
+
+
+@pytest.fixture
+def server(app):
+ return app.test_cli_runner()
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py
new file mode 100644
index 0000000..1ae9a2a
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/main/test_index.py
@@ -0,0 +1,12 @@
+# from tests.conftest import client, app, server
+
+
+def test_main(client):
+ response = client.get('/')
+ assert response.status_code == 200
+
+
+def test_feedback(client):
+ response = client.get('/feedback')
+ assert response.status_code == 200
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py
new file mode 100644
index 0000000..96bce4e
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/product/test_product.py
@@ -0,0 +1,15 @@
+
+def test_view(client):
+ response = client.get('/view/1')
+
+ assert response.status_code == 200
+
+ assert b"11.0" in response.data
+
+ response = client.get('/view/7')
+
+ assert response.status_code == 200
+
+ assert b"Product not found" in response.data
+
+
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py
new file mode 100644
index 0000000..545f28b
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/tests/test_python.py
@@ -0,0 +1,59 @@
+# import pytest
+#
+#
+# def sum(a, b):
+# return a+b
+#
+#
+# def test_sum():
+# assert sum(5, 5) == 10
+# assert sum(5, 0) == 5
+#
+#
+# class User:
+# real_person = True
+#
+# def __init__(self, name, age):
+# self.name = name
+# self.age = age
+#
+# def sum(self, x):
+# return self.age + x
+#
+# @classmethod
+# def is_real_person(cls):
+# return cls.real_person
+#
+# @classmethod
+# def get_class(cls):
+# return cls
+#
+#
+# def test_class():
+# user = User("gio", 15)
+# assert user.sum(-2) == 13
+# assert user.is_real_person() == True
+# assert User.is_real_person() == True
+# assert User.get_class().real_person == True
+# assert user.get_class().real_person == True
+#
+#
+# @pytest.fixture(autouse=True)
+# def ini(request):
+# if request.cls is not None: # Only for class-based tests
+# request.cls.user = User("mari", 15)
+# request.cls.p = 3.14159
+#
+#
+# @pytest.mark.usefixtures("ini")
+# class Testsmt:
+# def test_user(self):
+# assert self.user.name=="mari"
+#
+# @pytest.mark.skip
+# def test_pruwi(self):
+# assert self.p*5==3.14159*0
+#
+#
+#
+#
diff --git a/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf
new file mode 100644
index 0000000..9ca0486
--- /dev/null
+++ b/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/uwsgi.conf
@@ -0,0 +1,10 @@
+[uwsgi]
+
+master = true
+module = src:create_app()
+processes = 2
+threads = 4
+socket = :5000
+chmod-socket = 600
+die-on-term = true
+vacuum = true
\ No newline at end of file
diff --git a/Chapter10_TestDrivenDevelopment/Projects/readme.md b/Chapter10_TestDrivenDevelopment/Projects/readme.md
index 99fb713..0bd7f37 100644
--- a/Chapter10_TestDrivenDevelopment/Projects/readme.md
+++ b/Chapter10_TestDrivenDevelopment/Projects/readme.md
@@ -1,6 +1,6 @@
# Unit ტესტინგის დირექტორია
- სახელი გვარი | [პროექტი](/მისამართი)
-
+- giorgi kelenjeridze | [Food order app](/Chapter10_TestDrivenDevelopment/Projects/giorgikelenjeridze/app.py)
### 2025 ზაფხული
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile b/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile
new file mode 100644
index 0000000..a2bbee7
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/Dockerfile
@@ -0,0 +1,16 @@
+FROM python:3.10-slim-bookworm
+
+WORKDIR /giorgikelenjeridze
+
+COPY . .
+
+RUN apt-get update && \
+ apt-get -y install gcc build-essential python3-dev postgresql-client && \
+ pip install -r prod-requirements.txt
+
+COPY ./entrypoint.sh ./entrypoint.sh
+RUN chmod +x ./entrypoint.sh
+
+EXPOSE 5000
+
+CMD ["sh", "./entrypoint.sh"]
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/app.py b/Chapter11_Docker/Projects/giorgikelenjeridze/app.py
new file mode 100644
index 0000000..ce3460f
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/app.py
@@ -0,0 +1,6 @@
+from src import create_app
+
+app = create_app()
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt b/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt
new file mode 100644
index 0000000..a37387c
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/dev-requirements.txt differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml b/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml
new file mode 100644
index 0000000..92b0953
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/docker-compose.yml
@@ -0,0 +1,40 @@
+
+services:
+ nginx:
+ build: ./nginx
+ container_name: nginx
+ ports:
+ - "80:80"
+ restart: always
+
+ flask:
+ build: .
+ container_name: flask_docker
+ expose:
+ - 5000
+ environment:
+ SECRET_KEY: Real_SecreT-S!gma-Key_!23
+ DB_USER: python_intern-user
+ DB_PASSWORD: Wyali!12345
+ DB_DATABASE: python_intern-db
+ DB_HOST: postgres_db
+ DB_PORT: 5432
+ depends_on:
+ - postgres_db
+ restart: always
+
+ postgres_db:
+ image: postgres:15
+ container_name: postgres_db
+ environment:
+ POSTGRES_USER: python_intern-user
+ POSTGRES_PASSWORD: Wyali!12345
+ POSTGRES_DB: python_intern-db
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ restart: always
+
+volumes:
+ pgdata:
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh b/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh
new file mode 100644
index 0000000..f0a6d19
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/entrypoint.sh
@@ -0,0 +1,19 @@
+echo "Waiting for Postgres to be ready..."
+while ! pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER"; do
+ sleep 1
+done
+echo "Postgres is ready."
+
+# Check if 'products' table exists
+TABLE_EXISTS=$(psql "postgresql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_DATABASE" -tAc "SELECT to_regclass('public.products');")
+
+if [ "$TABLE_EXISTS" = "" ]; then
+ echo "Database not initialized. Running migrations and populating data..."
+ flask init_db
+ flask populate_db
+else
+ echo "Database already initialized. Skipping init."
+fi
+
+# Start Flask app with uwsgi
+exec uwsgi uwsgi.ini
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README
new file mode 100644
index 0000000..0e04844
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini
new file mode 100644
index 0000000..ec9d45c
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py
new file mode 100644
index 0000000..4c97092
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
new file mode 100644
index 0000000..4476bfd
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/2be0fc404953_added_description_to_product.py
@@ -0,0 +1,32 @@
+"""Added description to Product
+
+Revision ID: 2be0fc404953
+Revises:
+Create Date: 2025-05-31 19:42:43.908055
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2be0fc404953'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('description', sa.String(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('products', schema=None) as batch_op:
+ batch_op.drop_column('description')
+
+ # ### end Alembic commands ###
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
new file mode 100644
index 0000000..5ab938d
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/migrations/versions/9b4f3a94e264_changed_field.py
@@ -0,0 +1,38 @@
+"""Changed field
+
+Revision ID: 9b4f3a94e264
+Revises: 2be0fc404953
+Create Date: 2025-06-02 15:49:00.933969
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '9b4f3a94e264'
+down_revision = '2be0fc404953'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('_password', sa.String(), nullable=True))
+ batch_op.create_unique_constraint(None, ['email'])
+ batch_op.create_unique_constraint(None, ['username'])
+ batch_op.drop_column('password')
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=True))
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_constraint(None, type_='unique')
+ batch_op.drop_column('_password')
+
+ # ### end Alembic commands ###
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile
new file mode 100644
index 0000000..19306bf
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/Dockerfile
@@ -0,0 +1,4 @@
+FROM nginx:1.29.0
+
+RUN rm /etc/nginx/conf.d/default.conf
+COPY default.conf /etc/nginx/conf.d/
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf
new file mode 100644
index 0000000..19267d2
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/nginx/default.conf
@@ -0,0 +1,12 @@
+server {
+ listen 80;
+ server_name 127.0.0.1;
+
+ location / {
+ include uwsgi_params;
+ uwsgi_pass flask_docker:5000;
+
+ }
+
+
+}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt b/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt
new file mode 100644
index 0000000..233b715
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/prod-requirements.txt differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py
new file mode 100644
index 0000000..1b7a8cf
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/__init__.py
@@ -0,0 +1,64 @@
+from flask import Flask
+from src.admin_views.base import SecureModelView
+from src.admin_views import UserView, ProductView
+from src.config import Config
+from src.ext import db, migrate, login_manager, admin
+from src.view import product_blueprint, auth_blueprint, main_blueprint
+from src.commands import init_db_command, populate_db_command
+from src.models import User, Product
+from flask_admin.menu import MenuLink
+
+Blueprints = [product_blueprint, auth_blueprint, main_blueprint]
+Commands = [init_db_command, populate_db_command]
+
+
+def create_app(test_config=None):
+
+ app = Flask(__name__)
+ app.config.from_object(Config)
+
+ # Then override with test config if passed
+ if test_config:
+ app.config.update(test_config)
+
+ register_extensions(app)
+ register_blueprints(app)
+ register_commands(app)
+
+ return app
+
+
+def register_extensions(app):
+
+ # Flask-SQLAlchemy
+ db.init_app(app)
+
+ # Flask-Migrate
+ migrate.init_app(app, db)
+
+ # Flask-LoginManager
+ login_manager.init_app(app)
+ login_manager.login_view = 'user.login'
+
+ @login_manager.user_loader
+ def load_user(_id):
+ return User.query.get(_id)
+
+ # Flask-Admin
+ admin.init_app(app)
+ admin.add_view(UserView(User, db.session))
+ admin.add_view(ProductView(Product, db.session, endpoint="product_admin"))
+
+ admin.add_link(MenuLink("Back To Site", url="/", icon_type="fa", icon_value="fa-sing-out"))
+
+
+def register_blueprints(app):
+ for blueprint in Blueprints:
+ app.register_blueprint(blueprint)
+
+
+def register_commands(app):
+ for command in Commands:
+ app.cli.add_command(command)
+
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py
new file mode 100644
index 0000000..8e52cd1
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/__init__.py
@@ -0,0 +1,2 @@
+from src.admin_views.user import UserView
+from src.admin_views.product import ProductView
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py
new file mode 100644
index 0000000..4f3c2d9
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/base.py
@@ -0,0 +1,24 @@
+from flask_admin.contrib.sqla import ModelView
+from flask_admin import AdminIndexView
+from flask_login import current_user
+from flask import redirect, url_for
+
+
+class SecureModelView(ModelView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
+class SecureIndexView(AdminIndexView):
+ def is_accessible(self):
+ return current_user.is_authenticated
+
+ def inaccessible_callback(self, name, **kwargs):
+ if not self.is_accessible():
+ return redirect(url_for("auth.login"))
+
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py
new file mode 100644
index 0000000..950ef52
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/product.py
@@ -0,0 +1,33 @@
+from src.admin_views.base import SecureModelView
+from src.config import Config
+
+from flask_admin.form import ImageUploadField
+from os import path
+from uuid import uuid4
+from markupsafe import Markup
+
+
+def generate_filename(obj, file):
+ name, extension = path.splitext(file.filename)
+ return f"{uuid4()}{extension}"
+
+
+class ProductView(SecureModelView):
+ create_modal = True
+ edit_modal = True
+
+ column_editable_list = ("name", "price", "description")
+ column_filters = ["price",]
+
+ column_formatters = {
+ "name": lambda v,c,m,n: m.name if len(m.name) < 24 else m.name[0:24] + "...",
+ "image": lambda v,c,m,n: Markup(f"
")
+ }
+
+ form_overrides = {"image": ImageUploadField}
+ form_args = {
+ "image": {
+ "base_path": Config.UPLOAD_PATH,
+ "namegen": generate_filename
+ }
+ }
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py
new file mode 100644
index 0000000..22dbec3
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/admin_views/user.py
@@ -0,0 +1,10 @@
+from src.admin_views.base import SecureModelView
+
+
+class UserView(SecureModelView):
+ can_view_details = True
+ can_delete = False
+
+ column_list = ("username", "email")
+ column_details_list = ("username", "email", "_password")
+ column_searchable_list = ["username"]
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py
new file mode 100644
index 0000000..721340b
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/commands.py
@@ -0,0 +1,93 @@
+from flask.cli import with_appcontext
+import datetime
+import click
+from src.ext import db
+from src.models import Product, Person, IDcard, User
+
+
+def init_db():
+
+ db.drop_all()
+ db.create_all()
+
+
+@click.command("init_db")
+@with_appcontext
+def init_db_command():
+ click.echo("Initializing db...")
+
+ init_db()
+
+ click.echo("Initializing is over!")
+
+
+def populate_db():
+ products = [
+ {
+ "id": 0,
+ "name": "შაურმა პატარა",
+ "price": 11,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 200გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 1,
+ "name": "შაურმა სტანდარტი",
+ "price": 13,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 250გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 2,
+ "name": "შაურმა დიდი",
+ "price": 18,
+ "image": "shaurma.jfif",
+ "description": "ლავაში, ღორის ხორცი 300გ, პომიდორი, ხახვი, სალათის ფურცელი(აისბერგი), წიწაკა, მაიონეზი, კეტჩუპი"
+ },
+ {
+ "id": 3,
+ "name": "კოლა",
+ "price": 3,
+ "image": "cola.jfif",
+ "description": "ცივი გაზიანი სასმელი, კოკა-კოლა 500მლ"
+ },
+ {
+ "id": 4,
+ "name": "სპრაიტი",
+ "price": 3,
+ "image": "sprite.jfif",
+ "description": "ცივი გაზიანი სასმელი, სპრაიტი 500მლ"
+ },
+ {
+ "id": 5,
+ "name": "ბურგერი",
+ "price": 10,
+ "image": "burger.png",
+ "description": "ბურგერის ბული, ღორის ხორცი 200გ, ყველი, პომიდორი, სალათის ფურცელი, კეტჩუპი, მაიონეზი"
+ }
+ ]
+
+ for product in products:
+ new_product = Product(name=product["name"], price=product["price"], description=product["description"],
+ image=product["image"])
+ db.session.add(new_product)
+
+ idcard = IDcard(serial_number="01201115242", expiry_data=datetime.datetime.now())
+ db.session.add(idcard)
+ db.session.commit()
+ person = Person(name="Giorgi", surname="Kelenjeridze", birthday=datetime.datetime.now(), idcard_id=idcard.id)
+ db.session.add(person)
+ user = User("mari", "marikuna@gmail.com", "Mari123")
+ db.session.add(user)
+ db.session.commit()
+
+
+@click.command("populate_db")
+@with_appcontext
+def populate_db_command():
+
+ populate_db()
+
+
+
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py
new file mode 100644
index 0000000..3c26a29
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/config.py
@@ -0,0 +1,17 @@
+from os import environ, path
+
+
+class Config:
+
+ DB_USER = environ.get("DB_USER")
+ DB_PASSWORD = environ.get("DB_PASSWORD")
+ DB_DATABASE = environ.get("DB_DATABASE")
+
+ # print(DB_USER)
+ # print(DB_PASSWORD)
+ # print(DB_DATABASE)
+ BASE_DIRECTORY = path.abspath(path.dirname(__file__))
+ UPLOAD_PATH = path.join(BASE_DIRECTORY, "static", "assets")
+ SQLALCHEMY_DATABASE_URI = f"postgresql://{DB_USER}:{DB_PASSWORD}@postgres_db:5432/{DB_DATABASE}"
+ SECRET_KEY = environ.get("SECRET_KEY")
+ FLASK_ADMIN_SWATCH="Flatly"
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py
new file mode 100644
index 0000000..b1e63af
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/ext.py
@@ -0,0 +1,10 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_login import LoginManager
+from flask_admin import Admin
+from src.admin_views.base import SecureIndexView
+
+db = SQLAlchemy()
+migrate = Migrate()
+login_manager = LoginManager()
+admin = Admin(name="Kelenjo's Admin", template_mode="bootstrap4", index_view=SecureIndexView(), base_template="admin/admin_base.html")
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py
new file mode 100644
index 0000000..9e8a271
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/__init__.py
@@ -0,0 +1,4 @@
+from src.models.base import BaseModel
+from src.models.person import Person, IDcard
+from src.models.product import Product
+from src.models.user import User
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py
new file mode 100644
index 0000000..6f33fa7
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/base.py
@@ -0,0 +1,19 @@
+from src.ext import db
+
+
+class BaseModel(db.Model):
+
+ __abstract__ = True
+
+ def create(self, commit=True):
+ db.session.add(self)
+ if commit:
+ self.save()
+
+ def save(self):
+ db.session.commit()
+
+ def delete(self, commit=True):
+ db.session.delete(self)
+ if commit:
+ self.save()
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py
new file mode 100644
index 0000000..93875a4
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/person.py
@@ -0,0 +1,26 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Person(BaseModel):
+
+ __tablename__ = "persons"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ surname = db.Column(db.String)
+ birthday = db.Column(db.Date)
+
+ idcard_id = db.Column(db.Integer, db.ForeignKey("idcards.id"))
+
+ idcard = db.relationship("IDcard", back_populates="person")
+
+
+class IDcard(BaseModel):
+
+ __tablename__ = "idcards"
+ id = db.Column(db.Integer, primary_key=True)
+ serial_number = db.Column(db.String)
+ expiry_data = db.Column(db.Date)
+
+ person = db.relationship("Person", back_populates="idcard", uselist=False)
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py
new file mode 100644
index 0000000..da5ae3c
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/product.py
@@ -0,0 +1,22 @@
+from src.ext import db
+from src.models import BaseModel
+
+
+class Product(BaseModel):
+
+ __tablename__ = "products"
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+ price = db.Column(db.Float)
+ description = db.Column(db.String)
+ image = db.Column(db.String)
+
+ def __init__(self, name, price, description, image):
+ self.name = name
+ self.price = price
+ self.description = description
+ self.image = image
+
+ def __repr__(self):
+ return f"This is {self.name} and costs {self.price}$"
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py
new file mode 100644
index 0000000..3a3129f
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/models/user.py
@@ -0,0 +1,38 @@
+from src.ext import db
+from src.models import BaseModel
+from flask_login import UserMixin
+from werkzeug.security import generate_password_hash, check_password_hash
+
+
+class User(BaseModel, UserMixin):
+
+ __tablename__ = "users"
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String, unique=True)
+ email = db.Column(db.String, unique=True)
+ _password = db.Column(db.String)
+
+ def __init__(self, username, email, password):
+ self.username = username
+ self.email = email
+ self._password = generate_password_hash(password)
+
+ def __repr__(self):
+ return f"User name: {self.username}"
+
+ def edit(self, form):
+ self.username = form.username.data
+ self.password = form.password.data
+ db.session.commit()
+
+ def check_password(self, password):
+ return check_password_hash(self.password, password)
+
+ @property
+ def password(self):
+ return self._password
+
+ @password.setter
+ def password(self, password):
+ self._password = generate_password_hash(password)
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png
new file mode 100644
index 0000000..61f8ccd
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/2c363098-6067-4a42-aad2-8ee2085cc401.png differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png
new file mode 100644
index 0000000..c5bb973
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/burger.png differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif
new file mode 100644
index 0000000..4d83f30
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/cola.jfif differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif
new file mode 100644
index 0000000..9cc4780
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurma.jfif differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif
new file mode 100644
index 0000000..e775e42
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/shaurmisxorci.jfif differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif
new file mode 100644
index 0000000..128f4c4
Binary files /dev/null and b/Chapter11_Docker/Projects/giorgikelenjeridze/src/static/assets/sprite.jfif differ
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
new file mode 100644
index 0000000..7e73a33
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/admin/admin_base.html
@@ -0,0 +1,42 @@
+{% extends "admin/base.html" %}
+
+{% block page_body %}
+
+
+
+
+ {% block messages %}
+ {{ layout.messages() }}
+ {% endblock %}
+
+ {# store the jinja2 context for form_rules rendering logic #}
+ {% set render_ctx = h.resolve_ctx() %}
+
+ {% block body %}{% endblock %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html
new file mode 100644
index 0000000..6454ecc
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/login.html
@@ -0,0 +1,30 @@
+{% extends "partials/base.html" %}
+{% block title%}Log In{% endblock %}
+
+{% block body %}
+
+{% endblock %}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html
new file mode 100644
index 0000000..d611686
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/auth/register.html
@@ -0,0 +1,46 @@
+{% extends "partials/base.html" %}
+{% block title %}Register{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
Create Your Account
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html
new file mode 100644
index 0000000..4674ec6
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/macros.html
@@ -0,0 +1,5 @@
+{% macro is_active(endpoint) %}
+ {% if endpoint==request.endpoint %}
+ active
+ {% endif %}
+{% endmacro %}
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html
new file mode 100644
index 0000000..792387f
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/feedback.html
@@ -0,0 +1,72 @@
+{% extends "partials/base.html" %}
+{% block title %}feedback{% endblock %}
+{% block body %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html
new file mode 100644
index 0000000..cd35a37
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/main/index.html
@@ -0,0 +1,48 @@
+{% extends "partials/base.html" %}
+{% block title %}Home{% endblock %}
+{% block body %}
+
+
+
My Simple "Food Order" Web Application
+
+

+
+
+
+
+
+
+
+
+
+
ჩვენი ფილიალები
+
+ - გლდანი, ა მიკრო
+ - გლანი, I მიკრო
+ - საბურთალო
+
+
+
+
+
+{% endblock %}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html
new file mode 100644
index 0000000..6821ae2
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/base.html
@@ -0,0 +1,33 @@
+
+
+
+
+ Kelenjo {% block title %} {% endblock %}
+
+
+
+
+ {% include "partials/navbar.html" %}
+{# get_flashed_messages() ავტომატურად იღებს flash()-ის პარამეტრებს #}
+
+{% if get_flashed_messages(with_categories=True) %}
+{% for category, message in get_flashed_messages(with_categories=True) %}
+
+
+ {{message}}
+
+{% endfor %}
+{% endif %}
+ {% block body %}
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
new file mode 100644
index 0000000..4e06a3e
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/partials/navbar.html
@@ -0,0 +1,23 @@
+{% from "macros.html" import is_active %}
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html
new file mode 100644
index 0000000..57051ab
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/templates/product/view_product.html
@@ -0,0 +1,29 @@
+{% extends "partials/base.html" %}
+
+{% block title %}{{ product.name if product is not none else "Product Not Found" }}{% endblock %}
+
+{% block body %}
+
+
+
+
+ {% if product is not none %}
+

+
+
{{ product.name }}
+
{{ product.description }}
+
+
${{ product.price }}
+
+
+ {% else %}
+
+
Product not found
+
The product you are looking for does not exist.
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py
new file mode 100644
index 0000000..7e31f71
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/__init__.py
@@ -0,0 +1,3 @@
+from src.view.product.routes import product_blueprint
+from src.view.auth.routes import auth_blueprint
+from src.view.main.routes import main_blueprint
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py
new file mode 100644
index 0000000..eda7766
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/forms.py
@@ -0,0 +1,39 @@
+from flask_wtf import FlaskForm
+from wtforms.fields import StringField, PasswordField, EmailField, SubmitField
+from wtforms.validators import DataRequired, length, ValidationError
+from string import ascii_lowercase, ascii_uppercase, digits
+
+
+class RegisterForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired(), length(min=2, max=30)])
+ email = EmailField("Enter email", validators=[DataRequired()])
+ password = PasswordField("Enter password", validators=[DataRequired()])
+
+ submit = SubmitField("Register")
+
+ def validate_password(self, field):
+ contains_upcase = False
+ contains_lowcase = False
+ contains_digits = False
+
+ for char in field.data:
+ if char in ascii_uppercase:
+ contains_upcase=True
+ if char in ascii_lowercase:
+ contains_lowcase=True
+ if char in digits:
+ contains_digits=True
+
+ if contains_upcase == False:
+ raise ValidationError("Password must contain UpperCase Letter")
+ if contains_lowcase == False:
+ raise ValidationError("Password must contain LowerCase Letter")
+ if contains_digits == False:
+ raise ValidationError("Password must contain Digits Letter")
+
+
+class LoginForm(FlaskForm):
+ username = StringField("Enter Username", validators=[DataRequired()])
+ password = PasswordField("Enter Password", validators=[DataRequired()])
+
+ submit = SubmitField("Log in")
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py
new file mode 100644
index 0000000..ec57fcb
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/auth/routes.py
@@ -0,0 +1,78 @@
+from flask import Blueprint, render_template, flash, redirect, url_for, request
+from src.view.auth.forms import RegisterForm, LoginForm
+from src.models import User
+from flask_login import login_user, logout_user, login_required
+
+
+auth_blueprint = Blueprint("auth", __name__)
+
+
+@auth_blueprint.route("/register", methods=['GET', 'POST'])
+def register():
+ form = RegisterForm()
+ if form.validate_on_submit():
+ print(form.username.data)
+ print(form.email.data)
+ print(form.password.data)
+ flash("Sagol shen daregistrirdi", "info")
+ print('ar xar')
+ user = User(form.username.data, form.email.data, form.password.data)
+ user.create()
+ # return render_template('index.html')
+ else:
+ print('bad xar1')
+ print(form.errors)
+ print('bad xaar2')
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/login", methods=['GET', 'POST'])
+def login():
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter(User.username == form.username.data).first()
+ if user and user.check_password(form.password.data):
+ login_user(user)
+
+ next = request.args.get("next")
+
+ if next:
+ return redirect(next)
+ flash("Logged in!", "success")
+ return redirect(url_for("main.index"))
+ else:
+ flash("Username or Password is incorrect", "danger")
+
+ return render_template("auth/login.html", form=form)
+
+
+@auth_blueprint.route("/logout")
+@login_required
+def logout():
+ logout_user()
+ return redirect(url_for("main.index"))
+
+
+@auth_blueprint.route("/edit/", methods=['GET', 'POST'])
+@login_required
+def edit(user_id):
+ user = User.query.get(user_id)
+ oldname = user.username
+ form = RegisterForm(username=user.username, email=user.email, password=user.password)
+ if form.validate_on_submit():
+ user.edit(form)
+ print(f"{oldname} has changed it own name to {user.username}")
+ else:
+ print(form.errors)
+ return render_template("auth/register.html", form=form)
+
+
+@auth_blueprint.route("/delete/")
+@login_required
+def delete(user_id):
+ usertodel = User.query.get(user_id)
+ # User.query.filter_by()
+ print(usertodel)
+ usertodel.delete()
+ print(url_for("main.index"))
+ return redirect(url_for("main.index"))
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py
new file mode 100644
index 0000000..79835cc
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/main/routes.py
@@ -0,0 +1,15 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+main_blueprint = Blueprint("main", __name__)
+
+
+@main_blueprint.route("/")
+def index():
+ products = Product.query.all()
+ return render_template("main/index.html", products=products)
+
+
+@main_blueprint.route("/feedback")
+def feedback():
+ return render_template("main/feedback.html")
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py
new file mode 100644
index 0000000..a2f8d83
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/src/view/product/routes.py
@@ -0,0 +1,11 @@
+from flask import Blueprint, render_template
+from src.models.product import Product
+
+product_blueprint = Blueprint("product", __name__)
+
+
+@product_blueprint.route("/view/")
+def view(product_id):
+ product = Product.query.get(product_id)
+ print(product)
+ return render_template("product/view_product.html", product=product)
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py
new file mode 100644
index 0000000..d4526e9
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/auth/test_auth.py
@@ -0,0 +1,76 @@
+from flask_login import current_user
+from src.models import User
+
+
+def test_login(client):
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ print(client.get().data)
+ assert current_user.is_authenticated == True
+
+ client.get('/logout')
+ assert current_user.is_authenticated == False
+
+ client.post("/login", data={"username": "mari", "password": "Mari13"})
+ print(client.get().data)
+ assert current_user.is_authenticated == False
+
+
+def test_register_get(client):
+ response = client.get("/register")
+ assert response.status_code == 200
+ assert b"Register" in response.data or b"username" in response.data
+
+
+def test_register_post_valid(client, app):
+ response = client.post("/register", data={
+ "username": "gio",
+ "email": "gio@gmail.com",
+ "password": "Gio123"
+ }, follow_redirects=True)
+
+ assert b"Sagol shen daregistrirdi" in response.data
+
+ with app.app_context():
+ user = User.query.filter_by(username="gio").first()
+ assert user is not None
+ assert user.email == "gio@gmail.com"
+
+
+def test_logout(client, app):
+ # with app.app_context():
+ # user = User("mari", "mari@gmail.com", "Mari123")
+ # user.create()
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ assert current_user.is_authenticated == True
+
+ response = client.get("/logout", follow_redirects=True)
+ assert not current_user.is_authenticated
+
+
+def test_edit(client, app):
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+
+ client.post("/edit/1", data={"username": "marikuna", "email": "mari@gmail.com", "password": "Mari1234"})
+
+ with app.app_context():
+ user = User.query.get(1)
+ assert user.username=="marikuna"
+
+
+def test_delete(client, app):
+
+ with client:
+ client.post("/login", data={"username": "mari", "password": "Mari123"})
+ response = client.get(f'/delete/{1}', follow_redirects=True)
+ assert response.status_code == 200
+
+ with app.app_context():
+ user = User.query.get(1)
+ assert user is None
+
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py
new file mode 100644
index 0000000..30593c7
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/conftest.py
@@ -0,0 +1,43 @@
+import tempfile
+import os
+import pytest
+from src import create_app
+from src.ext import admin
+from src.commands import init_db, populate_db
+
+
+@pytest.fixture
+def app():
+ # Create a temporary file
+ db_fd, db_path = tempfile.mkstemp()
+
+ test_config = {
+ "TESTING": True,
+ "WTF_CSRF_ENABLED": False,
+ "DEBUG": False,
+ "SECRET_KEY": "test-secret-key",
+ "SQLALCHEMY_DATABASE_URI": f"sqlite:///{db_path}.sqlite", # Use temporary SQLite file
+ }
+
+ app = create_app(test_config=test_config)
+ admin._views = []
+
+ with app.app_context():
+ init_db()
+ populate_db()
+
+ yield app
+
+ # Cleanup
+ os.close(db_fd)
+ os.unlink(db_path)
+
+
+@pytest.fixture
+def client(app):
+ return app.test_client()
+
+
+@pytest.fixture
+def server(app):
+ return app.test_cli_runner()
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py
new file mode 100644
index 0000000..1ae9a2a
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/main/test_index.py
@@ -0,0 +1,12 @@
+# from tests.conftest import client, app, server
+
+
+def test_main(client):
+ response = client.get('/')
+ assert response.status_code == 200
+
+
+def test_feedback(client):
+ response = client.get('/feedback')
+ assert response.status_code == 200
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py
new file mode 100644
index 0000000..96bce4e
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/product/test_product.py
@@ -0,0 +1,15 @@
+
+def test_view(client):
+ response = client.get('/view/1')
+
+ assert response.status_code == 200
+
+ assert b"11.0" in response.data
+
+ response = client.get('/view/7')
+
+ assert response.status_code == 200
+
+ assert b"Product not found" in response.data
+
+
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py
new file mode 100644
index 0000000..545f28b
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/tests/test_python.py
@@ -0,0 +1,59 @@
+# import pytest
+#
+#
+# def sum(a, b):
+# return a+b
+#
+#
+# def test_sum():
+# assert sum(5, 5) == 10
+# assert sum(5, 0) == 5
+#
+#
+# class User:
+# real_person = True
+#
+# def __init__(self, name, age):
+# self.name = name
+# self.age = age
+#
+# def sum(self, x):
+# return self.age + x
+#
+# @classmethod
+# def is_real_person(cls):
+# return cls.real_person
+#
+# @classmethod
+# def get_class(cls):
+# return cls
+#
+#
+# def test_class():
+# user = User("gio", 15)
+# assert user.sum(-2) == 13
+# assert user.is_real_person() == True
+# assert User.is_real_person() == True
+# assert User.get_class().real_person == True
+# assert user.get_class().real_person == True
+#
+#
+# @pytest.fixture(autouse=True)
+# def ini(request):
+# if request.cls is not None: # Only for class-based tests
+# request.cls.user = User("mari", 15)
+# request.cls.p = 3.14159
+#
+#
+# @pytest.mark.usefixtures("ini")
+# class Testsmt:
+# def test_user(self):
+# assert self.user.name=="mari"
+#
+# @pytest.mark.skip
+# def test_pruwi(self):
+# assert self.p*5==3.14159*0
+#
+#
+#
+#
diff --git a/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini b/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini
new file mode 100644
index 0000000..9ca0486
--- /dev/null
+++ b/Chapter11_Docker/Projects/giorgikelenjeridze/uwsgi.ini
@@ -0,0 +1,10 @@
+[uwsgi]
+
+master = true
+module = src:create_app()
+processes = 2
+threads = 4
+socket = :5000
+chmod-socket = 600
+die-on-term = true
+vacuum = true
\ No newline at end of file
diff --git a/Chapter11_Docker/Projects/readme.md b/Chapter11_Docker/Projects/readme.md
index f5ad8b1..7bd71a0 100644
--- a/Chapter11_Docker/Projects/readme.md
+++ b/Chapter11_Docker/Projects/readme.md
@@ -1,5 +1,5 @@
# დოკერის დირექტორია
- სახელი გვარი | [პროექტი](/მისამართი)
-
+- giorgi kelenjeridze | [Food order App](https://hub.docker.com/r/kelenjo/flask_app)
### 2025 ზაფხული
\ No newline at end of file