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
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
||||||