initial creation
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
from flask import render_template
|
||||
|
||||
from snakeeyes.extensions import mail
|
||||
|
||||
|
||||
def send_template_message(template=None, ctx=None, *args, **kwargs):
|
||||
"""
|
||||
Send a templated e-mail using a similar signature as Flask-Mail:
|
||||
http://pythonhosted.org/Flask-Mail/
|
||||
|
||||
Except, it also supports template rendering. If you want to use a template
|
||||
then just omit the body and html kwargs to Flask-Mail and instead supply
|
||||
a path to a template. It will auto-lookup and render text/html messages.
|
||||
|
||||
Example:
|
||||
ctx = {'user': current_user, 'reset_token': token}
|
||||
send_template_message('Password reset from Foo', ['you@example.com'],
|
||||
template='user/mail/password_reset', ctx=ctx)
|
||||
|
||||
:param subject:
|
||||
:param recipients:
|
||||
:param body:
|
||||
:param html:
|
||||
:param sender:
|
||||
:param cc:
|
||||
:param bcc:
|
||||
:param attachments:
|
||||
:param reply_to:
|
||||
:param date:
|
||||
:param charset:
|
||||
:param extra_headers:
|
||||
:param mail_options:
|
||||
:param rcpt_options:
|
||||
:param template: Path to a template without the extension
|
||||
:param context: Dictionary of anything you want in the template context
|
||||
:return: None
|
||||
"""
|
||||
if ctx is None:
|
||||
ctx = {}
|
||||
|
||||
if template is not None:
|
||||
if 'body' in kwargs:
|
||||
raise Exception('You cannot have both a template and body arg.')
|
||||
elif 'html' in kwargs:
|
||||
raise Exception('You cannot have both a template and body arg.')
|
||||
|
||||
kwargs['body'] = _try_renderer_template(template, **ctx)
|
||||
kwargs['html'] = _try_renderer_template(template, ext='html', **ctx)
|
||||
|
||||
mail.send_message(*args, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _try_renderer_template(template_path, ext='txt', **kwargs):
|
||||
"""
|
||||
Attempt to render a template. We use a try/catch here to avoid having to
|
||||
do a path exists based on a relative path to the template.
|
||||
|
||||
:param template_path: Template path
|
||||
:type template_path: str
|
||||
:param ext: File extension
|
||||
:type ext: str
|
||||
:return: str
|
||||
"""
|
||||
try:
|
||||
return render_template('{0}.{1}'.format(template_path, ext), **kwargs)
|
||||
except IOError:
|
||||
pass
|
||||
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class Currency(object):
|
||||
TYPES = OrderedDict([
|
||||
('usd', u'United States Dollar'),
|
||||
('aed', u'United Arab Emirates Dirham'),
|
||||
('afn', u'Afghan Afghani'),
|
||||
('all', u'Albanian Lek'),
|
||||
('amd', u'Armenian Dram'),
|
||||
('ang', u'Netherlands Antillean Gulden'),
|
||||
('aoa', u'Angolan Kwanza'),
|
||||
('ars', u'Argentine Peso'),
|
||||
('aud', u'Australian Dollar'),
|
||||
('awg', u'Aruban Florin'),
|
||||
('azn', u'Azerbaijani Manat'),
|
||||
('bam', u'Bosnia & Herzegovina Convertible Mark'),
|
||||
('bbd', u'Barbadian Dollar'),
|
||||
('bdt', u'Bangladeshi Taka'),
|
||||
('bgn', u'Bulgarian Lev'),
|
||||
('bif', u'Burundian Franc'),
|
||||
('bmd', u'Bermudian Dollar'),
|
||||
('bnd', u'Brunei Dollar'),
|
||||
('bob', u'Bolivian Boliviano'),
|
||||
('brl', u'Brazilian Real'),
|
||||
('bsd', u'Bahamian Dollar'),
|
||||
('bwp', u'Botswana Pula'),
|
||||
('bzd', u'Belize Dollar'),
|
||||
('cad', u'Canadian Dollar'),
|
||||
('cdf', u'Congolese Franc'),
|
||||
('chf', u'Swiss Franc'),
|
||||
('clp', u'Chilean Peso'),
|
||||
('cny', u'Chinese Renminbi Yuan'),
|
||||
('cop', u'Colombian Peso'),
|
||||
('crc', u'Costa Rican Colón'),
|
||||
('cve', u'Cape Verdean Escudo'),
|
||||
('czk', u'Czech Koruna'),
|
||||
('djf', u'Djiboutian Franc'),
|
||||
('dkk', u'Danish Krone'),
|
||||
('dop', u'Dominican Peso'),
|
||||
('dzd', u'Algerian Dinar'),
|
||||
('eek', u'Estonian Kroon'),
|
||||
('egp', u'Egyptian Pound'),
|
||||
('etb', u'Ethiopian Birr'),
|
||||
('eur', u'Euro'),
|
||||
('fjd', u'Fijian Dollar'),
|
||||
('fkp', u'Falkland Islands Pound'),
|
||||
('gbp', u'British Pound'),
|
||||
('gel', u'Georgian Lari'),
|
||||
('gip', u'Gibraltar Pound'),
|
||||
('gmd', u'Gambian Dalasi'),
|
||||
('gnf', u'Guinean Franc'),
|
||||
('gtq', u'Guatemalan Quetzal'),
|
||||
('gyd', u'Guyanese Dollar'),
|
||||
('hkd', u'Hong Kong Dollar'),
|
||||
('hnl', u'Honduran Lempira'),
|
||||
('hrk', u'Croatian Kuna'),
|
||||
('htg', u'Haitian Gourde'),
|
||||
('huf', u'Hungarian Forint'),
|
||||
('idr', u'Indonesian Rupiah'),
|
||||
('ils', u'Israeli New Sheqel'),
|
||||
('inr', u'Indian Rupee'),
|
||||
('isk', u'Icelandic Króna'),
|
||||
('jmd', u'Jamaican Dollar'),
|
||||
('jpy', u'Japanese Yen'),
|
||||
('kes', u'Kenyan Shilling'),
|
||||
('kgs', u'Kyrgyzstani Som'),
|
||||
('khr', u'Cambodian Riel'),
|
||||
('kmf', u'Comorian Franc'),
|
||||
('krw', u'South Korean Won'),
|
||||
('kyd', u'Cayman Islands Dollar'),
|
||||
('kzt', u'Kazakhstani Tenge'),
|
||||
('lak', u'Lao Kip'),
|
||||
('lbp', u'Lebanese Pound'),
|
||||
('lkr', u'Sri Lankan Rupee'),
|
||||
('lrd', u'Liberian Dollar'),
|
||||
('lsl', u'Lesotho Loti'),
|
||||
('ltl', u'Lithuanian Litas'),
|
||||
('lvl', u'Latvian Lats'),
|
||||
('mad', u'Moroccan Dirham'),
|
||||
('mdl', u'Moldovan Leu'),
|
||||
('mga', u'Malagasy Ariary'),
|
||||
('mkd', u'Macedonian Denar'),
|
||||
('mnt', u'Mongolian Tögrög'),
|
||||
('mop', u'Macanese Pataca'),
|
||||
('mro', u'Mauritanian Ouguiya'),
|
||||
('mur', u'Mauritian Rupee'),
|
||||
('mvr', u'Maldivian Rufiyaa'),
|
||||
('mwk', u'Malawian Kwacha'),
|
||||
('mxn', u'Mexican Peso'),
|
||||
('myr', u'Malaysian Ringgit'),
|
||||
('mzn', u'Mozambican Metical'),
|
||||
('nad', u'Namibian Dollar'),
|
||||
('ngn', u'Nigerian Naira'),
|
||||
('nio', u'Nicaraguan Córdoba'),
|
||||
('nok', u'Norwegian Krone'),
|
||||
('npr', u'Nepalese Rupee'),
|
||||
('nzd', u'New Zealand Dollar'),
|
||||
('pab', u'Panamanian Balboa'),
|
||||
('pen', u'Peruvian Nuevo Sol'),
|
||||
('pgk', u'Papua New Guinean Kina'),
|
||||
('php', u'Philippine Peso'),
|
||||
('pkr', u'Pakistani Rupee'),
|
||||
('pln', u'Polish Złoty'),
|
||||
('pyg', u'Paraguayan Guaraní'),
|
||||
('qar', u'Qatari Riyal'),
|
||||
('ron', u'Romanian Leu'),
|
||||
('rsd', u'Serbian Dinar'),
|
||||
('rub', u'Russian Ruble'),
|
||||
('rwf', u'Rwandan Franc'),
|
||||
('sar', u'Saudi Riyal'),
|
||||
('sbd', u'Solomon Islands Dollar'),
|
||||
('scr', u'Seychellois Rupee'),
|
||||
('sek', u'Swedish Krona'),
|
||||
('sgd', u'Singapore Dollar'),
|
||||
('shp', u'Saint Helenian Pound'),
|
||||
('sll', u'Sierra Leonean Leone'),
|
||||
('sos', u'Somali Shilling'),
|
||||
('srd', u'Surinamese Dollar'),
|
||||
('std', u'São Tomé and Príncipe Dobra'),
|
||||
('svc', u'Salvadoran Colón'),
|
||||
('szl', u'Swazi Lilangeni'),
|
||||
('thb', u'Thai Baht'),
|
||||
('tjs', u'Tajikistani Somoni'),
|
||||
('top', u'Tongan Paʻanga'),
|
||||
('try', u'Turkish Lira'),
|
||||
('ttd', u'Trinidad and Tobago Dollar'),
|
||||
('twd', u'New Taiwan Dollar'),
|
||||
('tzs', u'Tanzanian Shilling'),
|
||||
('uah', u'Ukrainian Hryvnia'),
|
||||
('ugx', u'Ugandan Shilling'),
|
||||
('uyu', u'Uruguayan Peso'),
|
||||
('uzs', u'Uzbekistani Som'),
|
||||
('vef', u'Venezuelan Bolívar'),
|
||||
('vnd', u'Vietnamese Đồng'),
|
||||
('vuv', u'Vanuatu Vatu'),
|
||||
('wst', u'Samoan Tala'),
|
||||
('xaf', u'Central African Cfa Franc'),
|
||||
('xcd', u'East Caribbean Dollar'),
|
||||
('xof', u'West African Cfa Franc'),
|
||||
('xpf', u'Cfp Franc'),
|
||||
('yer', u'Yemeni Rial'),
|
||||
('zar', u'South African Rand'),
|
||||
('zmw', u'Zambian Kwacha')
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def lookup(cls, currency_code):
|
||||
"""
|
||||
Return the full currency name.
|
||||
|
||||
:param currency_code: Currency abbreviation
|
||||
:type currency_code: str
|
||||
:return: str
|
||||
"""
|
||||
return Currency.TYPES[currency_code]
|
||||
@@ -0,0 +1,20 @@
|
||||
def cents_to_dollars(cents):
|
||||
"""
|
||||
Convert cents to dollars.
|
||||
|
||||
:param cents: Amount in cents
|
||||
:type cents: int
|
||||
:return: float
|
||||
"""
|
||||
return round(cents / 100.0, 2)
|
||||
|
||||
|
||||
def dollars_to_cents(dollars):
|
||||
"""
|
||||
Convert dollars to cents.
|
||||
|
||||
:param dollars: Amount in dollars
|
||||
:type dollars: float
|
||||
:return: int
|
||||
"""
|
||||
return int(dollars * 100)
|
||||
@@ -0,0 +1,19 @@
|
||||
try:
|
||||
from urlparse import urljoin
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
||||
from flask import request
|
||||
|
||||
|
||||
def safe_next_url(target):
|
||||
"""
|
||||
Ensure a relative URL path is on the same domain as this host.
|
||||
This protects against the 'Open redirect vulnerability'.
|
||||
|
||||
:param target: Relative url (typically supplied by Flask-Login)
|
||||
:type target: str
|
||||
:return: str
|
||||
"""
|
||||
return urljoin(request.host_url, target)
|
||||
@@ -0,0 +1,75 @@
|
||||
import pytest
|
||||
from flask import url_for
|
||||
|
||||
|
||||
def assert_status_with_message(status_code=200, response=None, message=None):
|
||||
"""
|
||||
Check to see if a message is contained within a response.
|
||||
|
||||
:param status_code: Status code that defaults to 200
|
||||
:type status_code: int
|
||||
:param response: Flask response
|
||||
:type response: str
|
||||
:param message: String to check for
|
||||
:type message: str
|
||||
:return: None
|
||||
"""
|
||||
assert response.status_code == status_code
|
||||
assert message in str(response.data)
|
||||
|
||||
|
||||
class ViewTestMixin(object):
|
||||
"""
|
||||
Automatically load in a session and client, this is common for a lot of
|
||||
tests that work with views.
|
||||
"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_common_fixtures(self, session, client):
|
||||
self.session = session
|
||||
self.client = client
|
||||
|
||||
def login(self, identity='admin@local.host', password='password'):
|
||||
"""
|
||||
Login a specific user.
|
||||
|
||||
:return: Flask response
|
||||
"""
|
||||
return login(self.client, identity, password)
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
Logout a specific user.
|
||||
|
||||
:return: Flask response
|
||||
"""
|
||||
return logout(self.client)
|
||||
|
||||
|
||||
def login(client, username='', password=''):
|
||||
"""
|
||||
Log a specific user in.
|
||||
|
||||
:param client: Flask client
|
||||
:param username: The username
|
||||
:type username: str
|
||||
:param password: The password
|
||||
:type password: str
|
||||
:return: Flask response
|
||||
"""
|
||||
user = dict(identity=username, password=password)
|
||||
|
||||
response = client.post(url_for('user.login'), data=user,
|
||||
follow_redirects=True)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def logout(client):
|
||||
"""
|
||||
Log a specific user out.
|
||||
|
||||
:param client: Flask client
|
||||
:return: Flask response
|
||||
"""
|
||||
return client.get(url_for('user.logout'), follow_redirects=True)
|
||||
@@ -0,0 +1,30 @@
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
|
||||
def tzware_datetime():
|
||||
"""
|
||||
Return a timezone aware datetime.
|
||||
|
||||
:return: Datetime
|
||||
"""
|
||||
return datetime.datetime.now(pytz.utc)
|
||||
|
||||
|
||||
def timedelta_months(months, compare_date=None):
|
||||
"""
|
||||
Return a new datetime with a month offset applied.
|
||||
|
||||
:param months: Amount of months to offset
|
||||
:type months: int
|
||||
:param compare_date: Date to compare at
|
||||
:type compare_date: date
|
||||
:return: datetime
|
||||
"""
|
||||
if compare_date is None:
|
||||
compare_date = datetime.date.today()
|
||||
|
||||
delta = months * 365 / 12
|
||||
compare_date_with_delta = compare_date + datetime.timedelta(delta)
|
||||
|
||||
return compare_date_with_delta
|
||||
@@ -0,0 +1,21 @@
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
def render_json(status, *args, **kwargs):
|
||||
"""
|
||||
Return a JSON response.
|
||||
|
||||
Example usage:
|
||||
render_json(404, {'error': 'Discount code not found.'})
|
||||
render_json(200, {'data': coupon.to_json()})
|
||||
|
||||
:param status: HTTP status code
|
||||
:type status: int
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return: Flask response
|
||||
"""
|
||||
response = jsonify(*args, **kwargs)
|
||||
response.status_code = status
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,139 @@
|
||||
import datetime
|
||||
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
from lib.util_datetime import tzware_datetime
|
||||
from snakeeyes.extensions import db
|
||||
|
||||
|
||||
class AwareDateTime(TypeDecorator):
|
||||
"""
|
||||
A DateTime type which can only store tz-aware DateTimes.
|
||||
|
||||
Source:
|
||||
https://gist.github.com/inklesspen/90b554c864b99340747e
|
||||
"""
|
||||
impl = DateTime(timezone=True)
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if isinstance(value, datetime.datetime) and value.tzinfo is None:
|
||||
raise ValueError('{!r} must be TZ-aware'.format(value))
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
return 'AwareDateTime()'
|
||||
|
||||
|
||||
class ResourceMixin(object):
|
||||
# Keep track when records are created and updated.
|
||||
created_on = db.Column(AwareDateTime(),
|
||||
default=tzware_datetime)
|
||||
updated_on = db.Column(AwareDateTime(),
|
||||
default=tzware_datetime,
|
||||
onupdate=tzware_datetime)
|
||||
|
||||
@classmethod
|
||||
def sort_by(cls, field, direction):
|
||||
"""
|
||||
Validate the sort field and direction.
|
||||
|
||||
:param field: Field name
|
||||
:type field: str
|
||||
:param direction: Direction
|
||||
:type direction: str
|
||||
:return: tuple
|
||||
"""
|
||||
if field not in cls.__table__.columns:
|
||||
field = 'created_on'
|
||||
|
||||
if direction not in ('asc', 'desc'):
|
||||
direction = 'asc'
|
||||
|
||||
return field, direction
|
||||
|
||||
@classmethod
|
||||
def get_bulk_action_ids(cls, scope, ids, omit_ids=[], query=''):
|
||||
"""
|
||||
Determine which IDs are to be modified.
|
||||
|
||||
:param scope: Affect all or only a subset of items
|
||||
:type scope: str
|
||||
:param ids: List of ids to be modified
|
||||
:type ids: list
|
||||
:param omit_ids: Remove 1 or more IDs from the list
|
||||
:type omit_ids: list
|
||||
:param query: Search query (if applicable)
|
||||
:type query: str
|
||||
:return: list
|
||||
"""
|
||||
# Hello. This is Nick from the future (July 2020 to be exact). This
|
||||
# needed to be patched to include a call to list(), otherwise it was
|
||||
# deleting every user instead of skipping the current_user.
|
||||
omit_ids = list(map(str, omit_ids))
|
||||
|
||||
# Hello. This is Nick from the future (July 2022 to be exact). I added
|
||||
# the query condition below to make this a bit more intuitive so that
|
||||
# an empty search query won't delete everything. Prior to this an empty
|
||||
# query would have been technically all of the results.
|
||||
if query and scope == 'all_search_results':
|
||||
# Change the scope to go from selected ids to all search results.
|
||||
ids = cls.query.with_entities(cls.id).filter(cls.search(query))
|
||||
|
||||
# SQLAlchemy returns back a list of tuples, we want a list of strs.
|
||||
ids = [str(item[0]) for item in ids]
|
||||
|
||||
# Remove 1 or more items from the list, this could be useful in spots
|
||||
# where you may want to protect the current user from deleting themself
|
||||
# when bulk deleting user accounts.
|
||||
if omit_ids:
|
||||
ids = [id for id in ids if id not in omit_ids]
|
||||
|
||||
return ids
|
||||
|
||||
@classmethod
|
||||
def bulk_delete(cls, ids):
|
||||
"""
|
||||
Delete 1 or more model instances.
|
||||
|
||||
:param ids: List of ids to be deleted
|
||||
:type ids: list
|
||||
:return: Number of deleted instances
|
||||
"""
|
||||
delete_count = cls.query.filter(cls.id.in_(ids)).delete(
|
||||
synchronize_session=False)
|
||||
db.session.commit()
|
||||
|
||||
return delete_count
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save a model instance.
|
||||
|
||||
:return: Model instance
|
||||
"""
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
return self
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Delete a model instance.
|
||||
|
||||
:return: db.session.commit()'s result
|
||||
"""
|
||||
db.session.delete(self)
|
||||
return db.session.commit()
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Create a human readable version of a class instance.
|
||||
|
||||
:return: self
|
||||
"""
|
||||
obj_id = hex(id(self))
|
||||
columns = self.__table__.c.keys()
|
||||
|
||||
values = ', '.join("%s=%r" % (n, getattr(self, n)) for n in columns)
|
||||
return '<%s %s(%s)>' % (obj_id, self.__class__.__name__, values)
|
||||
@@ -0,0 +1,89 @@
|
||||
from flask_wtf import Form
|
||||
|
||||
|
||||
class ModelForm(Form):
|
||||
"""
|
||||
wtforms_components exposes ModelForm but their ModelForm does not inherit
|
||||
from flask_wtf's Form, but instead WTForm's Form.
|
||||
|
||||
However, in order to get CSRF protection handled by default we need to
|
||||
inherit from flask_wtf's Form. So let's just copy his class directly.
|
||||
|
||||
We modified it by removing the format argument so that wtforms_component
|
||||
uses its own default which is to pass in request.form automatically.
|
||||
"""
|
||||
def __init__(self, obj=None, prefix='', **kwargs):
|
||||
Form.__init__(
|
||||
self, obj=obj, prefix=prefix, **kwargs
|
||||
)
|
||||
self._obj = obj
|
||||
|
||||
|
||||
def choices_from_dict(source, prepend_blank=True):
|
||||
"""
|
||||
Convert a dict to a format that's compatible with WTForm's choices. It also
|
||||
optionally prepends a "Please select one..." value.
|
||||
|
||||
Example:
|
||||
# Convert this data structure:
|
||||
STATUS = OrderedDict([
|
||||
('unread', 'Unread'),
|
||||
('open', 'Open'),
|
||||
('contacted', 'Contacted'),
|
||||
('closed', 'Closed')
|
||||
])
|
||||
|
||||
# Into this:
|
||||
choices = [('', 'Please select one...'), ('unread', 'Unread) ...]
|
||||
|
||||
:param source: Input source
|
||||
:type source: dict
|
||||
:param prepend_blank: An optional blank item
|
||||
:type prepend_blank: bool
|
||||
:return: list
|
||||
"""
|
||||
choices = []
|
||||
|
||||
if prepend_blank:
|
||||
choices.append(('', 'Please select one...'))
|
||||
|
||||
for key, value in source.items():
|
||||
pair = (key, value)
|
||||
choices.append(pair)
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
def choices_from_list(source, prepend_blank=True):
|
||||
"""
|
||||
Convert a list to a format that's compatible with WTForm's choices. It also
|
||||
optionally prepends a "Please select one..." value.
|
||||
|
||||
Example:
|
||||
# Convert this data structure:
|
||||
TIMEZONES = (
|
||||
'Africa/Abidjan',
|
||||
'Africa/Accra',
|
||||
'Africa/Addis_Ababa'
|
||||
)
|
||||
|
||||
# Into this:
|
||||
choices = [('', 'Please select one...'),
|
||||
('Africa/Abidjan', 'Africa/Abidjan) ...]
|
||||
|
||||
:param source: Input source
|
||||
:type source: list or tuple
|
||||
:param prepend_blank: An optional blank item
|
||||
:type prepend_blank: bool
|
||||
:return: list
|
||||
"""
|
||||
choices = []
|
||||
|
||||
if prepend_blank:
|
||||
choices.append(('', 'Please select one...'))
|
||||
|
||||
for item in source:
|
||||
pair = (item, item)
|
||||
choices.append(pair)
|
||||
|
||||
return choices
|
||||
Reference in New Issue
Block a user