Initial commit
33
Makefile
Normal 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
@ -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
@ -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
|
||||
|
||||
11
all_paw_care/db/__init__.py
Normal 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)
|
||||
|
||||
65
all_paw_care/db/actions.py
Normal 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)
|
||||
|
||||
4
all_paw_care/db/metadata.py
Normal file
@ -0,0 +1,4 @@
|
||||
from sqlalchemy.schema import MetaData
|
||||
|
||||
DBMetadata = MetaData(schema='all_paw_care')
|
||||
|
||||
5
all_paw_care/db/types/base.py
Normal file
@ -0,0 +1,5 @@
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
13
all_paw_care/db/types/user.py
Normal 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)
|
||||
|
||||
9
all_paw_care/forms/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Pages
|
||||
|
||||
Blueprints for /forms routes
|
||||
|
||||
## Routes
|
||||
|
||||
- Meet and Greet
|
||||
|
||||
|
||||
7
all_paw_care/forms/__init__.py
Normal 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
|
||||
|
||||
17
all_paw_care/forms/routes.py
Normal 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")
|
||||
|
||||
25
all_paw_care/invoice_ninja/__init__.py
Normal 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()
|
||||
|
||||
|
||||
|
||||
0
all_paw_care/invoice_ninja/endpoints/__init__.py
Normal file
94
all_paw_care/invoice_ninja/endpoints/clients/__init__.py
Normal 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
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
0
all_paw_care/invoice_ninja/mappings/__init__.py
Normal file
54
all_paw_care/invoice_ninja/mappings/client.py
Normal 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
|
||||
|
||||
11
all_paw_care/pages/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Pages
|
||||
|
||||
Blueprints for /pages routes
|
||||
|
||||
## Routes
|
||||
|
||||
- Home
|
||||
- FAQ
|
||||
- Services
|
||||
|
||||
|
||||
7
all_paw_care/pages/__init__.py
Normal 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
|
||||
|
||||
17
all_paw_care/pages/routes.py
Normal 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
@ -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
|
||||
|
||||
BIN
all_paw_care/static/img/about_me/about_me_1_lg.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
all_paw_care/static/img/about_me/about_me_1_md.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
all_paw_care/static/img/about_me/about_me_1_sm.jpg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
all_paw_care/static/img/about_me/about_me_1_xl.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
all_paw_care/static/img/about_me/about_me_1_xxl.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
all_paw_care/static/img/about_me/about_me_2_lg.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
all_paw_care/static/img/about_me/about_me_2_md.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
all_paw_care/static/img/about_me/about_me_2_sm.jpg
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
all_paw_care/static/img/about_me/about_me_2_xl.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
all_paw_care/static/img/about_me/about_me_2_xxl.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
all_paw_care/static/img/about_me/about_me_3_lg.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
all_paw_care/static/img/about_me/about_me_3_md.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
all_paw_care/static/img/about_me/about_me_3_sm.jpg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
all_paw_care/static/img/about_me/about_me_3_xl.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
all_paw_care/static/img/about_me/about_me_3_xxl.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
all_paw_care/static/img/services/dog_walking_card_lg.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
all_paw_care/static/img/services/dog_walking_card_md.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
all_paw_care/static/img/services/dog_walking_card_sm.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
all_paw_care/static/img/services/dog_walking_card_xl.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
all_paw_care/static/img/services/dog_walking_card_xxl.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
all_paw_care/static/img/services/drop_in_card_lg.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
all_paw_care/static/img/services/drop_in_card_md.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
all_paw_care/static/img/services/drop_in_card_sm.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
all_paw_care/static/img/services/drop_in_card_xl.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
all_paw_care/static/img/services/drop_in_card_xxl.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
all_paw_care/static/img/services/house_sitting_card_lg.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
all_paw_care/static/img/services/house_sitting_card_md.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
all_paw_care/static/img/services/house_sitting_card_sm.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
all_paw_care/static/img/services/house_sitting_card_xl.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
all_paw_care/static/img/services/house_sitting_card_xxl.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
102
all_paw_care/templates/forms/meet-and-greet.html
Normal 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 %}
|
||||
|
||||
10
all_paw_care/templates/jinja/footer.html
Normal 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>
|
||||
|
||||
8
all_paw_care/templates/jinja/header.html
Normal 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>
|
||||
|
||||
32
all_paw_care/templates/jinja/menu.html
Normal 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>
|
||||
|
||||
29
all_paw_care/templates/jinja/types/form.html
Normal 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>
|
||||
|
||||
27
all_paw_care/templates/jinja/types/page.html
Normal 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>
|
||||
|
||||
36
all_paw_care/templates/pages/faq.html
Normal 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 %}
|
||||
|
||||
37
all_paw_care/templates/pages/index.html
Normal 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 %}
|
||||
121
all_paw_care/templates/pages/services.html
Normal 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 %}
|
||||
|
||||
29
all_paw_care/templates/user/user_create.html
Normal 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 %}
|
||||
|
||||
29
all_paw_care/templates/user/user_login.html
Normal 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 %}
|
||||
|
||||
9
all_paw_care/users/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Users
|
||||
|
||||
Blueprints for /users routes
|
||||
|
||||
## Routes
|
||||
|
||||
- /login
|
||||
- /<user>
|
||||
|
||||
7
all_paw_care/users/__init__.py
Normal 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
|
||||
|
||||
39
all_paw_care/users/routes.py
Normal 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
@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
BIN
img_original/about_me/about_me_1.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
img_original/about_me/about_me_2.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
img_original/about_me/about_me_3.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
28
shell.nix
Normal 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
|
||||
'';
|
||||
}
|
||||
|
||||