Initial commit

This commit is contained in:
Andrew Bryant 2023-09-10 13:04:17 -04:00
commit a0498a3774
72 changed files with 1230 additions and 0 deletions

33
Makefile Normal file
View File

@ -0,0 +1,33 @@
IP_ADDR = allpawcare.com
DOMAIN = allpawcare.com
USER = awkawb
IMG_ORIGINAL = "img_original"
IMG_PUBLIC = "all_paw_care/static/img"
LOCAL_ARCHIVE = $(shell pwd)/public.tar
PUBLIC_DIR = "$(shell pwd)/public"
REMOTE_DIR = /home/awkawb
.PHONY: dev-server
dev-server:
flask run --no-reload
.PHONY: process-images
process-images:
for
.PHONY: push-archive
push-archive: archive
scp $(LOCAL_ARCHIVE) root@$(DOMAIN):/srv/www && rm $(LOCAL_ARCHIVE)
.PHONY: archive
archive:
tar -cf $(LOCAL_ARCHIVE) $(PUBLIC_DIR)
.PHONY: ssh
ssh:
ssh $(USER)@$(DOMAIN)
.PHONY: ssh-root
ssh-root:
ssh root@$(DOMAIN)

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# Andrew's animal caregiving website
## Technologies used
* python
* flask
* Bootstrap
## shell.nix
A nix development environment. Include python3 and flask. To enter
the development environment run `nix-shell`.
## Development server
Development server can be run with `make dev-server`. The server runs
locally on port 5000.

33
all_paw_care/__init__.py Normal file
View File

@ -0,0 +1,33 @@
# App route blueprints
from all_paw_care.forms import forms
from all_paw_care.pages import pages
from all_paw_care.users import users
from all_paw_care.db.actions import ensure_tables
# App config
from config import Config
# Flask
from flask import Flask
# SQLAlchemy
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
def create_app():
# Initialize app
all_paw_care = Flask(__name__)
# Import config
all_paw_care.config.from_object(Config)
# Register app blueprints
all_paw_care.register_blueprint(pages)
all_paw_care.register_blueprint(forms)
all_paw_care.register_blueprint(users)
# Ensure database tables
ensure_tables()
return all_paw_care

View File

@ -0,0 +1,11 @@
from config import Config
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Database engine object
DBEngine = create_engine(Config.SQLALCHEMY_DATABASE_URI, echo=True)
# Database session object
DBSession = sessionmaker(DBEngine)

View File

@ -0,0 +1,65 @@
# Local
from all_paw_care.db.types.user import User
from all_paw_care.db.types.base import Base
from all_paw_care.db import DBEngine
from all_paw_care.db import DBSession
from sqlalchemy.orm import Session
from sqlalchemy import select
def ensure_tables():
Base.metadata.create_all(DBEngine)
def login(username: str):
if get_user(username):
return True
else:
return False
def add_user(username: str):
with DBSession() as session, session.begin():
try:
session.add(User(username=username))
except:
session.rollback()
finally:
session.commit()
return True
def get_users():
with DBSession() as session, session.begin():
users = list()
database_users = session.scalars(select(User).order_by(User.id)).all()
for database_user in database_users:
user = (database_user.id, database_user.username)
users.append(user)
return users
def get_user(username: str = None):
if username:
with DBSession() as session, session.begin():
user = session.scalars(
select(User).where(User.username == username)).all()
if len(user) == 1:
return (user[0].id, user[0].username)
elif len(user) == 0:
return None
else:
return None
def user_exists(username: str):
with DBSession() as session, session.begin():
users = session.execute(
select(User).where(User.username == username))
print(users)

View File

@ -0,0 +1,4 @@
from sqlalchemy.schema import MetaData
DBMetadata = MetaData(schema='all_paw_care')

View File

@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass

View File

@ -0,0 +1,13 @@
from all_paw_care.db.types.base import Base
from sqlalchemy import String
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(
String(40), nullable=False, unique=True)

View File

@ -0,0 +1,9 @@
# Pages
Blueprints for /forms routes
## Routes
- Meet and Greet

View File

@ -0,0 +1,7 @@
from flask import Blueprint
forms = Blueprint('forms', __name__, url_prefix='/forms')
# place here, else circular import errors
from all_paw_care.forms import routes

View File

@ -0,0 +1,17 @@
from all_paw_care.send_mail import SendMail
from all_paw_care.forms import forms
from flask import render_template,request
@forms.route('/meet_and_greet', methods=['GET', 'POST'])
def meet_and_greet():
if request.method == 'POST':
send_client = SendMail()
send_client.send_meet_and_greet_request(request.form['client-name'],
request.form['client-pets'],request.form['visit-type'],
request.form['meet-greet-date'],request.form['meet-greet-time'],
request.form['contact-type'],request.form['contact'],
request.form['notes'])
return render_template("forms/meet-and-greet.html")

View File

@ -0,0 +1,25 @@
# Python Invoice Ninja library
#from all_paw_care.invoice_ninja import swagger_client
#from all_paw_care.swagger_client.rest import ApiException
# Invoice Ninja library dependencies
#from __future__ import print_function
#import time
#from pprint import pprint
BASE_URL='https://invoice.allpawcare.com/api/v1'
API_TOKEN='r4AKEz9xRgk2SA7IotAvLrj64f7s0BczLDVjVwmjiPWeyG0fpu2eyib4VKI23QNO'
INVOICE_NINJA_USERNAME = 'billing@allpawcare.com'
INVOICE_NINJA_PASSWORD = """sw'1eMqN6#9fO!3"RY$L"""
INVOICE_NINJA_LOGIN_DICT = {
'email': INVOICE_NINJA_USERNAME,
'pasword': INVOICE_NINJA_PASSWORD
}
#invoice_ninja = swagger_client()

View File

@ -0,0 +1,94 @@
from all_paw_care.invoice_ninja import API_TOKEN
from all_paw_care.invoice_ninja import BASE_URL
from all_paw_care.invoice_ninja.mappings.client import Client
import requests
class Clients(object):
CLIENTS_URL = '{}/clients'.format(BASE_URL)
HEADERS = {'X-API-TOKEN': API_TOKEN}
def __build_sort_params(self, sort: dict):
sort_params = {'sort': str()}
is_first_entry = True
for option in sort.keys():
if is_first_entry:
sort_params['sort'] += '{}|{}'.format(option, sort[option])
is_first_entry = False
else:
sort_params['sort'] += ' {}|{}'.format(option, sort[option])
return sort_params
def __client_from_dict(self, client: dict):
return Client(client_id=client['id'],
name=client['name'],
address=client['address1'],
city=client['city'],
state=client['state'],
postal_code=client['postal_code'],
phone=client['phone'],
email=client['contacts'][0]['email'],
pets=client['custom_value1'])
def __clients_from_response(self, response: requests.Response):
clients = list()
for client_dict in response.json()['data']:
clients.append(self.__client_from_dict(client_dict))
return clients
def list_client(self, client_id: str = None):
"""
Get client based on client id.
"""
if client_id:
request_url = '{}/{}'.format(self.CLIENTS_URL, client_id)
response = requests.get(url=request_url,
headers=self.HEADERS)
if response.ok:
return self.__client_from_dict(response.json()['data'])
return None
def list_clients(self, include: str = 'activities',
sort: dict = dict(), status: str = 'active',
name: str = None):
"""
Get list of clients.
"""
request_params = dict()
# Add sort parameters to request
if len(sort) > 0:
request_params.update(self.__build_sort_params(sort))
# Add include parameters to request
request_params.update({'include': include})
# Add status parameters to request
request_params.update({'status': status})
# Add name parameters to request
if name:
request_params.update({'name': name})
# Check is request should be sent with parameters
if len(request_params) > 0:
response = requests.get(url=self.CLIENTS_URL,
params=request_params,
headers=self.HEADERS)
else:
response = requests.get(url=self.CLIENTS_URL,
headers=self.HEADERS)
if response.ok:
return self.__clients_from_response(response)
else:
return None

View File

@ -0,0 +1,15 @@
class Sort(object):
def __init__(self, id: str = None,
name: str = None,
balance: str = None):
self.sort_params = {'sort': str()}
is_first_entry = True
if id:
if is_first_entry:
self.sort_params['sort'] += '{}|{}'.format(option, sort[option])
is_first_entry = False
else:
self.sort_params['sort'] += ' {}|{}'.format(option, sort[option])

View File

@ -0,0 +1,54 @@
class Client(object):
def __init__(self, client_id: int = None, name: str = None,
address: str = None, city: str = None, state: str = None,
postal_code: str = None, phone: str = None,
email: str = None, pets: str = None):
self.id = client_id
self.name = name
self.address = address
self.city = city
self.state = state
self.postal_code = postal_code
self.phone = phone
self.email = email
self.pets = pets
def __str__(self):
return 'Client({}, {}, {}, {}, {}, {}, {}, {}, {})'.format(
self.id,
self.name,
self.address,
self.city,
self.state,
self.postal_code,
self.phone,
self.email,
self.pets
)
def get_name(self):
return self.name
def get_id(self):
return self.id
def get_address(self):
return self.address
def get_city(self):
return self.city
def get_state(self):
return self.state
def get_postal_code(self):
return self.postal_code
def get_phone(self):
return self.postal_code
def get_email(self):
return self.email

View File

@ -0,0 +1,11 @@
# Pages
Blueprints for /pages routes
## Routes
- Home
- FAQ
- Services

View File

@ -0,0 +1,7 @@
from flask import Blueprint
pages = Blueprint('pages', __name__, url_prefix='/pages')
# place here, else circular import errors
from all_paw_care.pages import routes

View File

@ -0,0 +1,17 @@
from all_paw_care.pages import pages
from flask import render_template,url_for
@pages.route('/')
@pages.route('/home')
def home():
return render_template("pages/index.html")
@pages.route('/faq')
def faq():
return render_template("pages/faq.html")
@pages.route('/services')
def services():
return render_template("pages/services.html")

100
all_paw_care/send_mail.py Normal file
View File

@ -0,0 +1,100 @@
from config import Config
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from smtplib import SMTP_SSL
import ssl
class SendMail(object):
def __init__(self):
# SMTP credentials
self.__server = Config.MAIL_SERVER
self.__port = Config.MAIL_PORT
self.__user = Config.MAIL_USER
self.__password = Config.MAIL_PASSWORD
self.__default_sender = Config.MAIL_SENDER
self.__default_recipients = Config.MAIL_RECIPIENTS
# SSL context
self.__default_ssl_context = ssl.create_default_context()
# Setup SMTP client
self.__smtp_client = SMTP_SSL(host=self.__server,port=self.__port,)
self.__smtp_client.login(self.__user,self.__password)
# Set default message
self.set_message_subject()
self.set_message()
def set_message(self, msg: str = '<h1>Test email message.</h1>'):
self.__message_root = MIMEMultipart()
self.__message_alt = MIMEMultipart('alternative')
self.__message_root['Subject'] = self.__message_subject
self.__message_root['From'] = self.__default_sender
self.__message_root['To'] = ', '.join(self.__default_recipients)
self.__message_root.attach(self.__message_alt)
self.__message_alt.attach(MIMEText(msg,'html'))
return True
def set_message_subject(self, subject: str = 'allpawcare.com test email'):
self.__message_subject = subject
return True
def send(self):
self.__smtp_client.sendmail(self.__default_sender,self.__default_recipients,
self.__message_root.as_string())
return True
def send_meet_and_greet_request(self, client_name: str = 'Andrew',
client_pets: str = 'Nabooru', visit_type: str = 'Test',
meeting_date: str = 'Test',
meeting_time: str = '10:00',
contact_type: str = 'Test',
client_contact: str = 'Test',
client_notes: str = 'This is just a test.'):
self.set_message_subject('New meet and greet request')
self.set_message('''
<html>
<h1>A new meet and greet request</h1>
<h2>
{name} has requested a meet and greet.
</h2>
<ul>
<li>
<em>Meeting info:</em> {date} {time}
</li>
<li>
<em>Visit type:</em> {vtype}
</li>
<li>
<em>Pet(s):</em> {pets}
</li>
<li>
<em>{ctype}:</em> {contact}
</li>
<li>
<em>Client notes:</em> {notes}
</li>
</ul>
</html>
'''.format(name = client_name,
pets = client_pets, time = meeting_time,
date = meeting_date, vtype = visit_type,
ctype = contact_type, contact = client_contact,
notes = client_notes))
self.send()
return True

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,102 @@
{% extends "jinja/types/form.html" %}
{% set title %}Request meet and greet{% endset %}
{% block content %}
<form method="POST">
<h1 class="text-success text-center my-4">Meet & greet</h1>
<div class="d-flex flex-row justify-content-center px-4">
<div class="input-group mx-2">
<span class="input-group-text">Pet name(s)</span>
<input class="form-control" type="text"
placeholder="Tabby, Curly" name="client-pets" id="client-pets" required>
</div>
<div class="input-group mx-2">
<span class="input-group-text">Name</span>
<input class="form-control" type="text"
placeholder="Kasey" name="client-name" id="client-name" required>
</div>
</div>
<div class="d-flex flex-row justify-content-center input-group m-2">
<span class="input-group-text">Visit type</span>
<input class="btn-check" type="radio"
name="visit-type" id="visit-type-walk" value="walk" required>
<label class="btn btn-primary" for="visit-type-walk">
Walk
</label>
<input class="btn-check" type="radio"
name="visit-type" id="visit-type-drop-in" value="drop-in" required>
<label class="btn btn-primary" for="visit-type-drop-in">
Drop-in
</label>
<input class="btn-check" type="radio"
name="visit-type" id="visit-type-house-sitting" value="house-sitting" required>
<label class="btn btn-primary" for="visit-type-house-sitting">
House sitting
</label>
</div>
<div class="d-flex flex-row justify-content-center m-2">
<div class="col-4 pe-1">
<div class="input-group">
<span class="input-group-text">Date</span>
<input type="date" id="date"
name="meet-greet-date" class="form-control"
placeholder="mm/dd/yyyy" value="" required>
</div>
</div>
<div class="col-4 ps-1">
<div class="input-group">
<span class="input-group-text" id="time">Time</span>
<input type="time" id="meet-greet-time"
name="meet-greet-time" class="form-control"
min="09:00" max="16:00" value="09:00" required>
</div>
</div>
</div>
<div class="d-flex flex-row justify-content-center mx-4 mb-2">
<div class="input-group">
<span class="input-group-text">Contact</span>
<input class="btn-check" type="radio"
name="contact-type" id="contact-type-email" value="email" required>
<label class="btn btn-primary" for="contact-type-email">
Email
</label>
<input class="btn-check" type="radio"
name="contact-type" id="contact-type-phone" value="phone" required>
<label class="btn btn-primary" for="contact-type-phone">
Phone
</label>
<input type="text" class="form-control" name="contact"
required>
</div>
</div>
<div class="d-flex flex-row justify-content-center mx-4 mb-2">
<span class="input-group-text">Additional notes</span>
<textarea class="form-control" rows="5" for="notes"
id="notes" name="notes">
</textarea>
</div>
<div class="row m-2">
<button class="btn btn-primary" type="submit"
action="{{ url_for('forms.meet_and_greet') }}" method="post">
Book it
</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<p class="col-md-4 mb-0 text-muted">
© {{ current_year }} Andrew Bryant
</p>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
{% include "jinja/menu.html" %}
</nav>
</footer>

View File

@ -0,0 +1,8 @@
<header>
<nav class="navbar navbar-expand-md sticky-top navbar-dark bg-dark">
<div class="container-fluid">
{% include "jinja/menu.html" %}
</div>
</nav>
</header>

View File

@ -0,0 +1,32 @@
<ul class="navbar-nav">
<li class="nav-item">
<a href="{{ url_for('pages.home') }}" class="nav-link text-secondary">
Home
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('pages.faq') }}" class="nav-link text-secondary">
FAQ
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('pages.services') }}" class="nav-link text-secondary">
Services
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('users.user_login') }}" class="nav-link text-secondary">
Login
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('users.user_create') }}" class="nav-link text-secondary">
Create account
</a>
</li>
</ul>

View File

@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}{% endblock %}
</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>
<body>
{% include "jinja/header.html" %}
<main>
<div class="container pt-5">
<div class="rounded border b-5">
{% block content %}
{% endblock %}
</div>
</div>
</main>
{% include "jinja/footer.html" %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
<body>
{% include "jinja/header.html" %}
<main>
<hr class="invisible pb-3">
<div class="container">
{% block content %}
{% endblock %}
</div>
</main>
{% include "jinja/footer.html" %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,36 @@
{% extends "jinja/types/page.html" %}
{% set title %}FAQ{% endset %}
{% block content %}
<div class="row text-center mb-2">
<h1 class="text-primary display-1">
Frequently asked questions
</h1>
</div>
{% for faq in config.FAQS.items() %}
<div class="row">
<div class="col-1">
</div>
<a class="btn btn-primary text-center" data-bs-toggle="collapse"
href="#{{ faq[1][0] }}" role="button" aria-expanded="false"
aria-controls="{{ faq[1][0] }}">
<h2>{{ faq[0] }}</h2>
</a>
<div class="collapse" id="{{ faq[1][0] }}">
<div class="card card-body text-center">
{{ faq[1][1] }}
</div>
</div>
<div class="col-1">
</div>
</div>
<hr class="invisible">
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "jinja/types/page.html" %}
{% set title %}About me{% endset %}
{% block content %}
<div class="row text-center">
<h1 class="text-primary display-1">
About me
</h1>
</div>
{% for row in config.ABOUT_ME %}
<div class="row pb-3">
<div class="col-8 text-center {{ loop.cycle('order-first','order-last') }}">
<p class="text-secondary lead">
{{ row[1] }}
</p>
</div>
<div class="col-4 {{ loop.cycle('order-last','order-first') }}">
<picture>
{% for img in row[0] %}
{% if not loop.last %}
<source srcset="{{ img }}"
media="(max-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% else %}
<source srcset="{{ img }}"
media="(min-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% endif %}
{% endfor %}
<img class="img-fluid" src="{{ row[0][1] }}">
</picture>
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,121 @@
{% extends "jinja/types/page.html" %}
{% set title %}Services{% endset %}
{% block content %}
<div class="row text-center pb-2">
<h1 class="text-primary display-1">
Services & Pricing
</h1>
</div>
<div class="row">
<h2 class="text-secondary display-2">
Primary services:
</h2>
</div>
<div class="row pt-2 pb-4">
{% for service in config.SERVICES.items() %}
<div class="col-4">
<div class="card">
<picture>
{% for img in service[1][0] %}
{% if not loop.last %}
<source srcset="{{ img }}"
media="(max-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% else %}
<source srcset="{{ img }}"
media="(min-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% endif %}
{% endfor %}
<img class="img-fluid card-img-top"
src="{{ service[1][0][1] }}">
</picture>
<div class="card-body">
<h1 class="card-title text-center text-primary">{{ service[0] }}</h1>
<hr>
<h2 class="text-success text-center">
{{ service[1][1] }}
</h2>
<hr class="invisible">
<p class="text-secondary">
{{ service[1][2] }}
<ul>
{% for included in service[1][3] %}
<li>{{ included }}</li>
{% endfor %}
</ul>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row">
<h2 class="text-secondary">
Add-on services:
</h2>
</div>
<div class="row pt-2 pb-4">
{% for service in config.SERVICES.items() %}
<div class="col-4">
<div class="card">
<picture>
{% for img in service[1][0] %}
{% if not loop.last %}
<source srcset="{{ img }}"
media="(max-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% else %}
<source srcset="{{ img }}"
media="(min-width: {{ config.IMG_BREAKPOINTS[loop.index] }})" />
{% endif %}
{% endfor %}
<img class="img-fluid" class="card-img-top"
src="{{ service[1][0][1] }}"
alt="Woman walking dog">
</picture>
<div class="card-body">
<h1 class="card-title text-center text-primary">{{ service[0] }}</h1>
<hr>
<h2 class="text-success text-center">
{{ service[1][1] }}
</h2>
<hr class="invisible">
<p class="text-secondary">
{{ service[1][2] }}
<ul>
{% for included in service[1][3] %}
<li>{{ included }}</li>
{% endfor %}
</ul>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row">
<div class="btn btn-primary">
<a class="link-light" href="{{ url_for('forms.meet_and_greet') }}">
Request meet & greet
</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "jinja/types/form.html" %}
{% set title %}Create user account{% endset %}
{% block content %}
<form method="POST">
<div class="row text-center my-4">
<h1 class="text-primary">
Create user account
</h1>
</div>
<div class="row">
<div class="input-group mx-2">
<span class="input-group-text">Username</span>
<input class="form-control" type="text"
name="username" id="username" required>
</div>
</div>
<div class="row m-2">
<button class="btn btn-primary" type="submit"
action="{{ url_for('users.user_create') }}" method="post">
Login
</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "jinja/types/form.html" %}
{% set title %}Login{% endset %}
{% block content %}
<form method="POST">
<div class="row text-center my-4">
<h1 class="text-primary">
User login
</h1>
</div>
<div class="row">
<div class="input-group mx-2">
<span class="input-group-text">Username</span>
<input class="form-control" type="text"
name="username" id="username" required>
</div>
</div>
<div class="row m-2">
<button class="btn btn-primary" type="submit"
action="{{ url_for('users.user_login') }}" method="post">
Login
</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,9 @@
# Users
Blueprints for /users routes
## Routes
- /login
- /<user>

View File

@ -0,0 +1,7 @@
from flask import Blueprint
users = Blueprint('users', __name__, url_prefix='/users')
# place here, else circular import errors
from all_paw_care.users import routes

View File

@ -0,0 +1,39 @@
# Blueprint import
from all_paw_care.users import users
# TODO uncomment database imports
# Database imports
from all_paw_care.db.actions import add_user
from all_paw_care.db.actions import login
from all_paw_care.db.types.user import User
# Flask imports
from flask import render_template
from flask import request
# SQLAlchemy imports
from sqlalchemy import select
@users.route('/login', methods=['GET', 'POST'])
def user_login():
if request.method == 'POST':
if login(request.form['username']):
return 'Your now logged in.'
else:
return 'User not found.'
return render_template("user/user_login.html")
@users.route('/create', methods=['GET', 'POST'])
def user_create():
if request.method == 'POST':
try:
add_user(request.form['username'])
return 'User created sucessfully.'
except:
return 'User exists'
return render_template("user/user_create.html")

150
config.py Normal file
View File

@ -0,0 +1,150 @@
import os
class Config(object):
# App base directory
BASE_DIR = os.path.dirname(os.path.realpath(__file__))
# Mail client config
MAIL_SERVER = 'smtp.fastmail.com'
MAIL_PORT = 465
MAIL_USER = 'awkawb@awkawb.cloud'
MAIL_PASSWORD = 'm3vaxbmp3tx9fqx4'
MAIL_SENDER = 'support@allpawcare.com'
MAIL_RECIPIENTS = ['support@allpawcare.com']
# Database config
DB_FILE = 'all_paw_care.db'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + BASE_DIR + '/' + DB_FILE
# Image breakpoints
IMG_BREAKPOINTS = [
'576px',
'768px',
'992px',
'1200px',
'1400px'
]
# About me page content
ABOUT_ME = [
(
[
'../static/img/about_me/about_me_1_sm.jpg',
'../static/img/about_me/about_me_1_md.jpg',
'../static/img/about_me/about_me_1_lg.jpg',
'../static/img/about_me/about_me_1_xl.jpg',
'../static/img/about_me/about_me_1_xxl.jpg'
],
'''
Before I provided animal caretaking services, I focused
my time on computers. I designed and coded this website!
'''
),
(
[
'../static/img/about_me/about_me_2_sm.jpg',
'../static/img/about_me/about_me_2_md.jpg',
'../static/img/about_me/about_me_2_lg.jpg',
'../static/img/about_me/about_me_2_xl.jpg',
'../static/img/about_me/about_me_2_xxl.jpg'
],
'''
My passion for animals and plants are what I spend
most my time on currently. I love providing quality
animal care.
'''
),
(
[
'../static/img/about_me/about_me_3_sm.jpg',
'../static/img/about_me/about_me_3_md.jpg',
'../static/img/about_me/about_me_3_lg.jpg',
'../static/img/about_me/about_me_3_xl.jpg',
'../static/img/about_me/about_me_3_xxl.jpg'
],
'''
For the past 5 years, my interest in plants and
horticulture have expanded exponentially. As a kid,
my mother always expressed interest in plants. Plants
are something, for me, that took patients and I
had little patients as a child.
'''
)
]
# Service page content
SERVICES = {
'Walking': [
[
'../static/img/services/dog_walking_card_sm.png',
'../static/img/services/dog_walking_card_md.png',
'../static/img/services/dog_walking_card_lg.png',
'../static/img/services/dog_walking_card_xl.png',
'../static/img/services/dog_walking_card_xxl.png'
],
'$20/visit',
'Service includes:',
[
'30 minute walk.',
'Bags for poop clean-up.'
]
],
'Drop-in': [
[
'../static/img/services/drop_in_card_sm.png',
'../static/img/services/drop_in_card_md.png',
'../static/img/services/drop_in_card_lg.png',
'../static/img/services/drop_in_card_xl.png',
'../static/img/services/drop_in_card_xxl.png'
],
'$20/visit',
'Service includes:',
[
'30 minute in home visit.',
'Bags for poop clean-up.'
]
],
'House sitting': [
[
'../static/img/services/house_sitting_card_sm.png',
'../static/img/services/house_sitting_card_md.png',
'../static/img/services/house_sitting_card_lg.png',
'../static/img/services/house_sitting_card_xl.png',
'../static/img/services/house_sitting_card_xxl.png'
],
'$38/visit',
'Service includes:',
[
'Animal care.',
'In-home overnight stays.'
]
]
}
# FAQ page content
FAQS = {
'What kind of animals do you caretake?': (
'animal-types-faq',
'I have experience careing for many types of animals. '
+ 'I provide care for reptiles, mammals, etc.'
),
'Can you care for special needs pets?': (
'special-needs-faq',
'''I have experience care for special needs, including older
animals'''
),
'Can you stay at my house?': (
'house-sitting-faq',
'I offer house sitting services. This includes in home stays.'
),
'Can you take care of my plants?': (
'plant-faq',
'I love plants and have a garden of my own. I\'ll gladly '
+ 'remove the mess for you.'
),
'My yard is a mess, can you clean up the dog poop?': (
'poop-scoop-faq',
'I do offer animal poop clean up add-on yard'
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

28
shell.nix Normal file
View File

@ -0,0 +1,28 @@
{ pkgs ? import <nixpkgs> {} }:
with pkgs;
pkgs.mkShell {
nativeBuildInputs = [
# Python development environment
(python3.withPackages(ps: with ps; [
flask
markdown
requests
sqlalchemy
certifi
six
python-dateutil
urllib3
]))
imagemagick
sqlite
];
shellHook = ''
export FLASK_APP=all_paw_care
export FLASK_DEBUG=1
'';
}