Compare commits

...

10 Commits

5 changed files with 72 additions and 14 deletions

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# A dog a pet
#### Video Demo: [Video url](https://www.youtube.com/shorts/PRFRn0t684g)
#### Description:
This project is meant to be the starting point of a pet adoptions web site. It allows users to register, register pets and ask for pets adoption. It also has a contact form which is not fully functional. It has been done using python with the Flask framework and a sqlite database.
The purpouse of the project is to build a web site where people looking to adopt a pet can contact with people that are offering pets for adoption, as a registered user you could do both, but this could actually be splitted into separate roles though. To adopt or publish a pet you'll need to fill the registration form which has validation both in the frontend and the backend making sure data is correct. After registration you have access to your profile where you can update some of your details as your email address or phone number, but not your name or lastname. In the pets page you can see all the listed pets for adoption whether you are registered or not, you can filter by attributes as pet type (cat or dog), sex and age range. Every pet has a picture and its details, like name, age, etc, and a link to ask to adopt it. If registered when clicking on the adopt link, it will set the pet as in 'pending' status adoption which is visually noticeable by the picture turning to grayscale, meaning other people can't ask to adopt it untill your request is rejected. If a non user tries to adopt a pet the link will lead to the login section. Only logged in users can see the add pet option in the menu, the add pet section also consists of a form with its validations, after succesfully submitting the pet data the user will be notified that the pets has been added and it will be publicly displayed in the pets section for everyone to see. A user can't adopt a pet that has published himself, it will simply ignore the request and redirect to the pet list without changes.
Beside the adoptions there is information about the site itself and a contact form, it is not fully developed but it would be easy to extend and implement the functionality.
This projects uses a lot of resources from the flask environment as flask sqlalchemy orm, flask session, jinja, etc. It also integrates with cloudinary for the image hosting so having a cloudinary account is required for it to be fully functional as is. The deployed version also uses gunicorn as webserver but it can be run with the flask run command.
### General structure:
This app is divided in modules or "blueprints", it consists of a total of 4 modules: Users, Pets, Us and Main.
#### Users module
The users module resposability is to handle user related actions, as loging in, registering and updating user data, it consists of a couple of views and a User service.
#### Pets module
Pets module takes care of eveything related to the pets, as listing them and adding new ones, is consists of the pets and the registe pet views and a Pet service.
#### Us module
This module is intended for the site owners, it has a contact form and an about us view, basically only static assets a little logic.
#### Home module
Home module holds the entry point to the app, it only consists of a home page view
### Models:
This web app uses sqlalchemy ORM, the most important models are users and pets, then there is the logical model adoptions and lookup models like adoption status and pet kind.
### Services
#### User service
It consists of several user realated static methods (no DI in this project), it holds the logic for creating and updating users and loging in.
#### Pet service
Contains methods for listing and creating new pets, including handling image uploads and usage of Cloudinary sdk.
### Utils
Here we can found custom error classes, validation logic, constants and other necessary resources for the project to work properly.

View File

@@ -14,6 +14,7 @@ from app.utils.errors.pets.pet_register_errors import PetRegisterError
from app.utils.flash_message import FlashMessage from app.utils.flash_message import FlashMessage
from app.utils.helpers import pet_sex_id_to_str from app.utils.helpers import pet_sex_id_to_str
from app.utils.validators.pet_validators import PetValidators from app.utils.validators.pet_validators import PetValidators
from app.utils.validators.validators import Validators
class PetService: class PetService:
@staticmethod @staticmethod
@@ -54,14 +55,18 @@ class PetService:
img = request.files['img'] img = request.files['img']
img_url = None img_url = None
if(request.files['img']): if(img):
try: try:
if not Validators.allowed_file_img(img.filename):
raise(PetRegisterError("Invalid image format"))
cloudinary.config(cloud_name = os.environ.get('CLOUD_NAME'), api_key=os.getenv('API_KEY'), cloudinary.config(cloud_name = os.environ.get('CLOUD_NAME'), api_key=os.getenv('API_KEY'),
api_secret=os.getenv('API_SECRET')) api_secret=os.getenv('API_SECRET'))
upload_result = cloudinary.uploader.upload(img) upload_result = cloudinary.uploader.upload(img)
img_url = upload_result['secure_url'] img_url = upload_result['secure_url']
except PetRegisterError as e:
flash(FlashMessage(e.message, AlertType.DANGER.value ))
except: except:
print("err!") print("error")
try: try:
name = PetValidators.is_valid_name(request.form.get('name')) name = PetValidators.is_valid_name(request.form.get('name'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -1,20 +1,21 @@
{% macro menu(ROUTES, menu_type) %} {% macro menu(ROUTES, menu_type) %}
<ul class="navbar-nav me-auto mb-2 mb-lg-0 w-100 justify-content-center"> <ul class="navbar-nav me-auto mb-2 mb-lg-0 w-100 justify-content-center">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ROUTES['home']}}">Home</a> <a class="nav-link {% if request.path == ROUTES['home'] %} active {% endif %} " aria-current="page" href="{{ROUTES['home']}}">Home</a>
</li> </li>
{% if menu_type == "movil" %} {% if menu_type == "movil" %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['pets']['index']}}">Pets</a> <a class="nav-link {% if request.path == ROUTES['pets']['index'] %} active {% endif %}" href="{{ROUTES['pets']['index']}}">Pets</a>
</li> </li>
{% if session["id"] %} {% if session["id"] %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['pets']['register']}}">Add pet</a> <a class="nav-link {% if request.path == ROUTES['pets']['register'] %} active {% endif %}" href="{{ROUTES['pets']['register']}}">Add pet</a>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link {% if 'pets' in request.path %} active {% endif %} dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Pets Pets
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -23,29 +24,34 @@
<li><a class="dropdown-item" href="{{ROUTES['pets']['index']}}/?type=1">Dogs</a></li> <li><a class="dropdown-item" href="{{ROUTES['pets']['index']}}/?type=1">Dogs</a></li>
{% if session["id"] %} {% if session["id"] %}
<li> <li>
<a class="dropdown-item" href="{{ROUTES['pets']['register']}}">Add pet</a> <a class="dropdown-item {% if request.path == ROUTES['pets']['register'] %} active {% endif %}" href="{{ROUTES['pets']['register']}}">Add pet</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['about-us']}}">About Us</a> <a class="nav-link {% if request.path == ROUTES['about-us'] %} active {% endif %}" href="{{ROUTES['about-us']}}">About Us</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['contact-us']}}">Contact Us</a> <a class="nav-link {% if request.path == ROUTES['contact-us'] %} active {% endif %}" href="{{ROUTES['contact-us']}}">Contact Us</a>
</li> </li>
{% if session["id"] %} {% if session["id"] %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['users']['index']}}">My Profile</a> <a class="nav-link {% if request.path == ROUTES['users']['index'] %} active {% endif %}" href="{{ROUTES['users']['index']}}">My Profile</a>
</li> </li>
{% endif %} {% endif %}
{% if not session["id"] %} {% if not session["id"] %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ROUTES['users']['register']}}">Register</a> <a class="nav-link {% if request.path == ROUTES['users']['register'] %} active {% endif %}" href="{{ROUTES['users']['register']}}">Register</a>
</li> </li>
<li class="nav-item d-block d-lg-none"> <li class="nav-item d-block d-lg-none">
<a class="nav-link" href="{{ROUTES['users']['login']}}">Log In</a> <a class="nav-link {% if request.path == ROUTES['users']['login'] %} active {% endif %}" href="{{ROUTES['users']['login']}}">Log In</a>
</li>
{% endif %}
{% if session["id"] %}
<li class="nav-item d-block d-lg-none">
<a class="nav-link" href="{{ROUTES['users']['logout']}}">Logout</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@@ -1,6 +1,5 @@
import re import re
from typing import Optional from typing import Optional
class Validators: class Validators:
@staticmethod @staticmethod
@@ -18,3 +17,9 @@ class Validators:
return True return True
except: except:
return False return False
@staticmethod
def allowed_file_img(filename):
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp'}
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS