diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..e2f0b66 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,13 @@ +google_appengine +virtualenv_ +lib +.tox +env +.git +tmp +loveapp/config-example.py +YelpLove.egg-info +*.pyc +tests/ +testing/ +.pytest_cache \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e57d95..76b1f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ tmp import/*.csv import/*.json venv +env .vscode \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e013afd..7d93486 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks.git - sha: v0.7.1 + rev: v0.7.1 hooks: - id: autopep8-wrapper - id: check-added-large-files @@ -15,15 +16,20 @@ - id: double-quote-string-fixer - id: end-of-file-fixer - id: flake8 - language_version: python2.7 + language_version: python3.11 - id: fix-encoding-pragma - id: name-tests-test - id: pretty-format-json args: ['--autofix'] - id: requirements-txt-fixer - id: trailing-whitespace +- repo: https://github.com/asottile/reorder-python-imports + rev: v3.12.0 + hooks: + - id: reorder-python-imports + - repo: https://github.com/Yelp/detect-secrets - sha: 0.9.1 + rev: 0.9.1 hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] diff --git a/.travis.yml b/.travis.yml index 9c13f59..91492f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "2.7" + - "3.11" # command to install dependencies install: "pip install tox" # command to run tests diff --git a/README.md b/README.md index ee13275..fb4d9a3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To get an idea what Yelp Love looks like go and check out the [screenshots](/scr Yelp Love runs on [Google App Engine](https://appengine.google.com/). In order to install Yelp Love you will need a Google account and the -[Google App Engine SDK for Python 2.7](https://cloud.google.com/appengine/docs/standard/python/download). +[Google App Engine SDK for Python](https://cloud.google.com/appengine/docs/standard/python/download). ### Create a new project @@ -78,23 +78,23 @@ fields Yelp Love requires for an employee. The S3 bucket name must be configured in config.py. In order to access the S3 bucket you have to save AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY -using the [Secret](models/secret.py) model. Locally you can open up the -[interactive console](http://localhost:8000/console) and execute the following code: +using the [Secret](models/secret.py) model. Locally, you can temporarily add an endpoint inside loveapp/views/web.py and then navigate to it in your browser (e.g., http://localhost:8080/create_secrets): ```python -from models import Secret - -Secret(id='AWS_ACCESS_KEY_ID', value='change-me').put() -Secret(id='AWS_SECRET_ACCESS_KEY', value='change-me').put() +@web_app.route('/create_secrets') +def create_secrets(): + from loveapp.models import Secret + Secret(id='AWS_ACCESS_KEY_ID', value='change-me').put() + Secret(id='AWS_SECRET_ACCESS_KEY', value='change-me').put() + return "please delete me now" ``` -In production you can either use the [Datastore UI](https://console.cloud.google.com/datastore/entities/) -or the [Remote API](https://cloud.google.com/appengine/docs/python/tools/remoteapi). +In production you can use the [Datastore UI](https://console.cloud.google.com/datastore/entities/). To kick off the final import you have to run: ```python -from logic import employee +from loveapp.logic import employee employee.load_employees() ``` diff --git a/app.yaml b/app.yaml index 540cae7..4b281fc 100644 --- a/app.yaml +++ b/app.yaml @@ -1,24 +1,8 @@ service: default -runtime: python27 -api_version: 1 -threadsafe: true +runtime: python311 +app_engine_apis: true handlers: -- url: /api/.* - script: main.app - secure: always -- url: /keys/?.* - script: main.app - login: admin - secure: always -- url: /subscriptions/?.* - script: main.app - login: admin - secure: always -- url: /aliases/?.* - script: main.app - login: admin - secure: always - url: /robots.txt static_files: static/robots.txt upload: static/robots.txt @@ -26,31 +10,3 @@ handlers: - url: /static static_dir: static secure: always -- url: /_themes/(.*)/img/(.*) - static_files: themes/\1/static/img/\2 - upload: themes/(.*)/static/img/(.*) - secure: always - login: optional -- url: .* # Anything not explicitly listed above - script: main.app - login: required - secure: always - -builtins: -- remote_api: on - -libraries: -- name: ssl - version: latest - -skip_files: -- ^(.*/)?#.*#$ -- ^(.*/)?.*/RCS/.*$ -- ^(.*/)?.*\.py[co]$ -- ^(.*/)?.*~$ -- ^(.*/)?\..*$ -- ^YelpLove.egg-info(/.*)?$ -- ^config-example.py$ -- ^google_appengine(/.*)?$ -- ^tmp(/.*)?$ -- ^virtualenv_.*$ diff --git a/appengine_config.py b/appengine_config.py deleted file mode 100644 index 11ec9e2..0000000 --- a/appengine_config.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import sys - -# Load dependencies in lib -app_root_dir = os.path.dirname(__file__) -server_lib_dir = os.path.join(app_root_dir, 'lib') -if server_lib_dir not in sys.path: - sys.path.insert(0, server_lib_dir) diff --git a/import/employees.csv.example b/import/employees.csv.example deleted file mode 100644 index df29a0f..0000000 --- a/import/employees.csv.example +++ /dev/null @@ -1,3 +0,0 @@ -username,first_name,last_name,department,photo_url -john,John,Doe,,https://placehold.it/100x100 -janet,Janet,Doe,,https://placehold.it/100x100 \ No newline at end of file diff --git a/loveapp/__init__.py b/loveapp/__init__.py new file mode 100644 index 0000000..7637a02 --- /dev/null +++ b/loveapp/__init__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +import flask_themes2 +from flask import Blueprint +from flask import current_app +from flask import Flask +from flask_themes2 import Themes +from flask_themes2 import ThemeTemplateLoader +from google.appengine.api import wrap_wsgi_app + +import loveapp.config as config +from loveapp import views +from loveapp.util.auth import is_admin +from loveapp.util.company_values import linkify_company_values +from loveapp.util.converter import RegexConverter +from loveapp.util.csrf import generate_csrf_token + + +def create_app(theme_loaders=()): + if current_app: + return current_app + app = Flask(__name__.split('.')[0]) + app.wsgi_app = wrap_wsgi_app(app.wsgi_app) + + app.secret_key = config.SECRET_KEY + app.url_map.converters['regex'] = RegexConverter + app.jinja_env.globals['config'] = config + app.jinja_env.globals['csrf_token'] = generate_csrf_token + app.jinja_env.globals['is_admin'] = is_admin + app.jinja_env.filters['linkify_company_values'] = linkify_company_values + + app.register_blueprint(views.web.web_app) + app.register_blueprint(views.api.api_app) + app.register_blueprint(views.tasks.tasks_app) + + # flask_themes2 is storing themes_blueprint at the global level + # https://github.com/sysr-q/flask-themes2/blob/master/flask_themes2/__init__.py#L280C1-L281C58 + # which means on some parametrized test runs, we run into errors re-adding urls on the blueprint. + # Forcing the reset of themes_blueprint here seems to work + flask_themes2.themes_blueprint = Blueprint('_themes', __name__) + flask_themes2.themes_blueprint.jinja_loader = ThemeTemplateLoader(True) + Themes(app, app_identifier='yelplove', loaders=theme_loaders) + + # if debug property is present, let's use it + try: + app.debug = config.DEBUG + except AttributeError: + app.debug = False + + return app diff --git a/config-example.py b/loveapp/config-example.py similarity index 100% rename from config-example.py rename to loveapp/config-example.py diff --git a/loveapp/import/employees.csv b/loveapp/import/employees.csv new file mode 100644 index 0000000..06f2be8 --- /dev/null +++ b/loveapp/import/employees.csv @@ -0,0 +1,4 @@ +username,first_name,last_name,office,department,photo_url +duncan,Duncan,Cook,Awesomeness Office,Department Of Awesome,https://placehold.it/100x100 +hbomb,Harrison,Cook,,,https://placehold.it/100x100 +niffs,Jennifer,Cook,,,https://placehold.it/100x100 \ No newline at end of file diff --git a/loveapp/import/employees.csv.example b/loveapp/import/employees.csv.example new file mode 100644 index 0000000..78b1d4a --- /dev/null +++ b/loveapp/import/employees.csv.example @@ -0,0 +1,3 @@ +username,first_name,last_name,department,office,photo_url +john,John,Doe,,,https://placehold.it/100x100 +janet,Janet,Doe,,,https://placehold.it/100x100 \ No newline at end of file diff --git a/import/employees.json.example b/loveapp/import/employees.json.example similarity index 89% rename from import/employees.json.example rename to loveapp/import/employees.json.example index 4f24fd8..b10ee71 100644 --- a/import/employees.json.example +++ b/loveapp/import/employees.json.example @@ -4,6 +4,7 @@ "first_name": "John", "last_name": "Doe", "department": "", + "office": "", "photo_url": "https://placehold.it/100x100" }, { @@ -11,6 +12,7 @@ "first_name": "Janet", "last_name": "Doe", "department": "", + "office": "", "photo_url": "https://placehold.it/100x100" } ] diff --git a/logic/__init__.py b/loveapp/logic/__init__.py similarity index 92% rename from logic/__init__.py rename to loveapp/logic/__init__.py index 6672ddf..b0d0edf 100644 --- a/logic/__init__.py +++ b/loveapp/logic/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from datetime import timedelta -from itertools import izip_longest +from itertools import zip_longest import pytz from google.appengine.ext import ndb @@ -13,11 +13,11 @@ def chunk(iterable, chunk_size): """Collect data into fixed-length chunks or blocks (http://docs.python.org/2/library/itertools.html#recipes)""" args = [iter(iterable)] * chunk_size - return izip_longest(*args) + return zip_longest(*args) def to_the_future(dict): - for k, v in dict.iteritems(): + for k, v in dict.items(): if issubclass(v.__class__, ndb.Future): dict[k] = v.get_result() diff --git a/logic/alias.py b/loveapp/logic/alias.py similarity index 89% rename from logic/alias.py rename to loveapp/logic/alias.py index cce6ef2..1d066d2 100644 --- a/logic/alias.py +++ b/loveapp/logic/alias.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- - -from models import Alias -from models import Employee +from loveapp.models import Alias +from loveapp.models import Employee def get_alias(alias): diff --git a/logic/department.py b/loveapp/logic/department.py similarity index 84% rename from logic/department.py rename to loveapp/logic/department.py index 84b8afe..3271f8b 100644 --- a/logic/department.py +++ b/loveapp/logic/department.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- - -from models.employee import Employee +from loveapp.models.employee import Employee def get_all_departments(): diff --git a/logic/email.py b/loveapp/logic/email.py similarity index 90% rename from logic/email.py rename to loveapp/logic/email.py index dff6492..c78885c 100644 --- a/logic/email.py +++ b/loveapp/logic/email.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from google.appengine.api.mail import EmailMessage -from util.email import get_name_and_email -import config -import logic.secret +import loveapp.config as config +import loveapp.logic.secret +from loveapp.util.email import get_name_and_email if config.EMAIL_BACKEND == 'sendgrid': # a bit of a hack here so that we can avoid adding dependencies unless @@ -22,7 +22,7 @@ def send_appengine_email(sender, recipient, subject, body_html, body_text): def send_sendgrid_email(sender, recipient, subject, body_html, body_text): - key = logic.secret.get_secret('SENDGRID_API_KEY') + key = loveapp.logic.secret.get_secret('SENDGRID_API_KEY') sg = sendgrid.SendGridAPIClient(apikey=key) from_ = sendgrid.helpers.mail.Email(*get_name_and_email(sender)) diff --git a/logic/employee.py b/loveapp/logic/employee.py similarity index 93% rename from logic/employee.py rename to loveapp/logic/employee.py index 98b9f1b..4a499b7 100644 --- a/logic/employee.py +++ b/loveapp/logic/employee.py @@ -1,23 +1,23 @@ # -*- coding: utf-8 -*- import csv import json -import os.path import logging +import os.path from google.appengine.api import search from google.appengine.api.runtime import memory_usage from google.appengine.ext import ndb -import config +import loveapp.config as config from errors import NoSuchEmployee -from logic import chunk -from logic.secret import get_secret -from logic.toggle import set_toggle_state -from models import Employee -from models import Love -from models import LoveCount -from models.toggle import LOVE_SENDING_ENABLED -from logic.office import OfficeParser +from loveapp.logic import chunk +from loveapp.logic.office import OfficeParser +from loveapp.logic.secret import get_secret +from loveapp.logic.toggle import set_toggle_state +from loveapp.models import Employee +from loveapp.models import Love +from loveapp.models import LoveCount +from loveapp.models.toggle import LOVE_SENDING_ENABLED INDEX_NAME = 'employees' @@ -46,7 +46,7 @@ def _get_employee_info_from_csv(): def _clear_index(): - logging.info('Clearing index... {}MB'.format(memory_usage().current())) + logging.info('Clearing index... {}MB'.format(memory_usage().current)) index = search.Index(name=INDEX_NAME) last_id = None while True: @@ -70,7 +70,7 @@ def _clear_index(): last_id = doc_ids[-1] index.delete(doc_ids) - logging.info('Done clearing index. {}MB'.format(memory_usage().current())) + logging.info('Done clearing index. {}MB'.format(memory_usage().current)) def _generate_substrings(string): @@ -80,14 +80,14 @@ def _generate_substrings(string): Example: _concatenate_substrings('arothman') => 'a ar aro arot aroth arothm arothma' """ - return ' '.join([string[:i] for i in xrange(1, len(string))]) + return ' '.join([string[:i] for i in range(1, len(string))]) def _get_employee_info_from_s3(): from boto import connect_s3 from boto.s3.key import Key - logging.info('Reading employees file from S3... {}MB'.format(memory_usage().current())) + logging.info('Reading employees file from S3... {}MB'.format(memory_usage().current)) key = Key( connect_s3( aws_access_key_id=get_secret('AWS_ACCESS_KEY_ID'), @@ -96,12 +96,12 @@ def _get_employee_info_from_s3(): 'employees.json', ) employee_dicts = json.loads(key.get_contents_as_string()) - logging.info('Done reading employees file from S3. {}MB'.format(memory_usage().current())) + logging.info('Done reading employees file from S3. {}MB'.format(memory_usage().current)) return employee_dicts def _index_employees(employees): - logging.info('Indexing employees... {}MB'.format(memory_usage().current())) + logging.info('Indexing employees... {}MB'.format(memory_usage().current)) index = search.Index(name=INDEX_NAME) # According to appengine, put can handle a maximum of 200 documents, # and apparently batching is more efficient @@ -118,12 +118,12 @@ def _index_employees(employees): doc = search.Document(fields=[ # Full name is already unicode search.TextField(name='full_name', value=employee.full_name), - search.TextField(name='username', value=unicode(employee.username)), + search.TextField(name='username', value=employee.username), search.TextField(name='substrings', value=substrings), ]) documents.append(doc) index.put(documents) - logging.info('Done indexing employees. {}MB'.format(memory_usage().current())) + logging.info('Done indexing employees. {}MB'.format(memory_usage().current)) def _update_employees(employee_dicts): @@ -135,7 +135,7 @@ def _update_employees(employee_dicts): """ employee_dicts = list(employee_dicts) - logging.info('Updating employees... {}MB'.format(memory_usage().current())) + logging.info('Updating employees... {}MB'.format(memory_usage().current)) db_employee_dict = { employee.username: employee @@ -166,7 +166,7 @@ def _update_employees(employee_dicts): current_usernames.add(d['username']) if len(all_employees) % 200 == 0: - logging.info('Processed {} employees, {}MB'.format(len(all_employees), memory_usage().current())) + logging.info('Processed {} employees, {}MB'.format(len(all_employees), memory_usage().current)) ndb.put_multi(all_employees) # Figure out if there are any employees in the DB that aren't in the S3 @@ -181,7 +181,7 @@ def _update_employees(employee_dicts): terminated_employees.append(employee) ndb.put_multi(terminated_employees) - logging.info('Done updating employees. {}MB'.format(memory_usage().current())) + logging.info('Done updating employees. {}MB'.format(memory_usage().current)) def combine_employees(old_username, new_username): diff --git a/logic/event.py b/loveapp/logic/event.py similarity index 100% rename from logic/event.py rename to loveapp/logic/event.py diff --git a/logic/leaderboard.py b/loveapp/logic/leaderboard.py similarity index 75% rename from logic/leaderboard.py rename to loveapp/logic/leaderboard.py index 37cd4b8..bbdcfb6 100644 --- a/logic/leaderboard.py +++ b/loveapp/logic/leaderboard.py @@ -2,10 +2,10 @@ from datetime import datetime from datetime import timedelta -from logic import TIMESPAN_LAST_WEEK -from logic import to_the_future -from logic import utc_week_limits -import logic.love_count +import loveapp.logic.love_count +from loveapp.logic import TIMESPAN_LAST_WEEK +from loveapp.logic import to_the_future +from loveapp.logic import utc_week_limits def get_leaderboard_data(timespan, department, office=None): @@ -16,7 +16,7 @@ def get_leaderboard_data(timespan, department, office=None): utc_now -= timedelta(days=7) utc_week_start, _ = utc_week_limits(utc_now) - top_lovers, top_lovees = logic.love_count.top_lovers_and_lovees( + top_lovers, top_lovees = loveapp.logic.love_count.top_lovers_and_lovees( utc_week_start, dept=department, office=office, @@ -41,6 +41,6 @@ def get_leaderboard_data(timespan, department, office=None): ] # get results for the futures set up previously - map(to_the_future, top_lover_dicts) - map(to_the_future, top_loved_dicts) + list(map(to_the_future, top_lover_dicts)) + list(map(to_the_future, top_loved_dicts)) return (top_lover_dicts, top_loved_dicts) diff --git a/logic/love.py b/loveapp/logic/love.py similarity index 89% rename from logic/love.py rename to loveapp/logic/love.py index e57d2ad..b5db5a2 100644 --- a/logic/love.py +++ b/loveapp/logic/love.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- from datetime import datetime -from google.appengine.api import taskqueue -import config -import logic.alias -import logic.email -import logic.event +from google.appengine.api import taskqueue +import loveapp.config as config +import loveapp.logic.alias +import loveapp.logic.email +import loveapp.logic.event from errors import TaintedLove -from logic.toggle import get_toggle_state -from models import Employee -from models import Love -from models import LoveCount -from models.toggle import LOVE_SENDING_ENABLED -from util.company_values import get_hashtag_value_mapping -from util.render import render_template +from loveapp.logic.toggle import get_toggle_state +from loveapp.models import Employee +from loveapp.models import Love +from loveapp.models import LoveCount +from loveapp.models.toggle import LOVE_SENDING_ENABLED +from loveapp.util.company_values import get_hashtag_value_mapping +from loveapp.util.render import render_template def _love_query(start_dt, end_dt, include_secret): @@ -102,7 +102,7 @@ def send_love_email(l): # noqa recent_love_and_lovers=[(love, love.sender_key.get()) for love in recent_love[:3]] ) - logic.email.send_email(from_, to, subject, body_html, body_text) + loveapp.logic.email.send_email(from_, to, subject, body_html, body_text) def get_love(sender_username=None, recipient_username=None, limit=None): @@ -112,8 +112,8 @@ def get_love(sender_username=None, recipient_username=None, limit=None): :param recipient_username: If present, only return love sent to a particular user. :param limit: If present, only return this many items. """ - sender_username = logic.alias.name_for_alias(sender_username) - recipient_username = logic.alias.name_for_alias(recipient_username) + sender_username = loveapp.logic.alias.name_for_alias(sender_username) + recipient_username = loveapp.logic.alias.name_for_alias(recipient_username) if not (sender_username or recipient_username): raise TaintedLove('Not gonna give you all the love in the world. Sorry.') @@ -150,7 +150,7 @@ def send_loves(recipients, message, sender_username=None, secret=False): if sender_username is None: sender_username = Employee.get_current_employee().username - sender_username = logic.alias.name_for_alias(sender_username) + sender_username = loveapp.logic.alias.name_for_alias(sender_username) sender_key = Employee.query( Employee.username == sender_username, Employee.terminated == False, # noqa @@ -175,7 +175,7 @@ def send_loves(recipients, message, sender_username=None, secret=False): def validate_love_recipients(recipients): - unique_recipients = set([logic.alias.name_for_alias(name) for name in recipients]) + unique_recipients = set([loveapp.logic.alias.name_for_alias(name) for name in recipients]) if len(recipients) != len(unique_recipients): raise TaintedLove(u'Sorry, you are trying to send love to a user multiple times.') @@ -217,8 +217,8 @@ def _send_love(recipient_key, message, sender_key, secret): ) if not secret: - logic.event.add_event( - logic.event.LOVESENT, + loveapp.logic.event.add_event( + loveapp.logic.event.LOVESENT, {'love_id': new_love.key.id()}, ) @@ -228,7 +228,7 @@ def _get_company_values(new_love, message): hashtag_value_mapping = get_hashtag_value_mapping() matched_categories = set() - for hashtag, category in hashtag_value_mapping.iteritems(): + for hashtag, category in hashtag_value_mapping.items(): if hashtag in message.lower(): matched_categories.add(category) diff --git a/logic/love_count.py b/loveapp/logic/love_count.py similarity index 90% rename from logic/love_count.py rename to loveapp/logic/love_count.py index 52f2c9f..23c3d51 100644 --- a/logic/love_count.py +++ b/loveapp/logic/love_count.py @@ -5,12 +5,12 @@ from google.appengine.api.runtime import memory_usage from google.appengine.ext import ndb -from logic import utc_week_limits -from logic.toggle import set_toggle_state -from models import Employee -from models import Love -from models import LoveCount -from models.toggle import LOVE_SENDING_ENABLED +from loveapp.logic import utc_week_limits +from loveapp.logic.toggle import set_toggle_state +from loveapp.models import Employee +from loveapp.models import Love +from loveapp.models import LoveCount +from loveapp.models.toggle import LOVE_SENDING_ENABLED def top_lovers_and_lovees(utc_week_start, dept=None, office=None, limit=20): diff --git a/logic/love_link.py b/loveapp/logic/love_link.py similarity index 91% rename from logic/love_link.py rename to loveapp/logic/love_link.py index c7ff8f3..cdb5015 100644 --- a/logic/love_link.py +++ b/loveapp/logic/love_link.py @@ -4,13 +4,13 @@ import random import string -import logic.alias -from errors import NoSuchLoveLink -from models import LoveLink -from models import Employee - from google.appengine.ext import ndb +import loveapp.logic.alias +from errors import NoSuchLoveLink +from loveapp.models import Employee +from loveapp.models import LoveLink + def get_love_link(hash_key): loveLink = LoveLink.query(LoveLink.hash_key == hash_key).get() @@ -44,7 +44,7 @@ def add_recipient(hash_key, recipient): raise NoSuchLoveLink("Couldn't Love Link with id {}".format(hash_key)) # check that user exists, get_key_for_username throws an exception if not - recipient_username = logic.alias.name_for_alias(recipient) + recipient_username = loveapp.logic.alias.name_for_alias(recipient) Employee.get_key_for_username(recipient_username) loveLink.recipient_list += ', ' + recipient diff --git a/logic/notification_request.py b/loveapp/logic/notification_request.py similarity index 98% rename from logic/notification_request.py rename to loveapp/logic/notification_request.py index 6611304..4953ea6 100644 --- a/logic/notification_request.py +++ b/loveapp/logic/notification_request.py @@ -3,9 +3,10 @@ import hmac import json import logging + import urllib3 -import config +import loveapp.config as config CONTENT_TYPE_JSON = 'application/json' diff --git a/logic/notifier/__init__.py b/loveapp/logic/notifier/__init__.py similarity index 64% rename from logic/notifier/__init__.py rename to loveapp/logic/notifier/__init__.py index 7769020..274ed0f 100644 --- a/logic/notifier/__init__.py +++ b/loveapp/logic/notifier/__init__.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- -import logic.event - +import loveapp.logic.event from errors import UnknownEvent -from logic.notifier.lovesent_notifier import LovesentNotifier +from loveapp.logic.notifier.lovesent_notifier import LovesentNotifier EVENT_TO_NOTIFIER_MAPPING = { - logic.event.LOVESENT: LovesentNotifier, + loveapp.logic.event.LOVESENT: LovesentNotifier, } diff --git a/logic/notifier/lovesent_notifier.py b/loveapp/logic/notifier/lovesent_notifier.py similarity index 83% rename from logic/notifier/lovesent_notifier.py rename to loveapp/logic/notifier/lovesent_notifier.py index 56b8c8b..a5978b2 100644 --- a/logic/notifier/lovesent_notifier.py +++ b/loveapp/logic/notifier/lovesent_notifier.py @@ -1,16 +1,15 @@ # -*- coding: utf-8 -*- from google.appengine.ext import ndb -import logic.event - -from logic.notification_request import NotificationRequest -from models.love import Love -from models.subscription import Subscription +import loveapp.logic.event +from loveapp.logic.notification_request import NotificationRequest +from loveapp.models.love import Love +from loveapp.models.subscription import Subscription class LovesentNotifier(object): - event = logic.event.LOVESENT + event = loveapp.logic.event.LOVESENT def __init__(self, *args, **kwargs): self.love = ndb.Key(Love, kwargs.get('love_id')).get() diff --git a/logic/office.py b/loveapp/logic/office.py similarity index 98% rename from logic/office.py rename to loveapp/logic/office.py index 9347126..85a9e75 100644 --- a/logic/office.py +++ b/loveapp/logic/office.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- -import yaml from collections import Counter from collections import defaultdict +import yaml + REMOTE_OFFICE = 'Remote' OTHER_OFFICE = 'Other' def get_all_offices(): - from models import Employee + from loveapp.models import Employee """ Retrieve all the offices in the database diff --git a/logic/secret.py b/loveapp/logic/secret.py similarity index 85% rename from logic/secret.py rename to loveapp/logic/secret.py index 29c9fb9..b0bba49 100644 --- a/logic/secret.py +++ b/loveapp/logic/secret.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from errors import NoSuchSecret -from models import Secret +from loveapp.models import Secret def get_secret(id): diff --git a/logic/subscription.py b/loveapp/logic/subscription.py similarity index 74% rename from logic/subscription.py rename to loveapp/logic/subscription.py index 513abaf..d5e0cdd 100644 --- a/logic/subscription.py +++ b/loveapp/logic/subscription.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models.subscription import Subscription +from loveapp.models.subscription import Subscription def delete_subscription(subscription_id): diff --git a/logic/toggle.py b/loveapp/logic/toggle.py similarity index 88% rename from logic/toggle.py rename to loveapp/logic/toggle.py index 45bb4a7..46fdc04 100644 --- a/logic/toggle.py +++ b/loveapp/logic/toggle.py @@ -3,9 +3,9 @@ from errors import InvalidToggleName from errors import InvalidToggleState -from models import Toggle -from models.toggle import TOGGLE_NAMES -from models.toggle import TOGGLE_STATES +from loveapp.models import Toggle +from loveapp.models.toggle import TOGGLE_NAMES +from loveapp.models.toggle import TOGGLE_STATES def _validate_and_maybe_create_toggle(name, state): diff --git a/models/__init__.py b/loveapp/models/__init__.py similarity index 99% rename from models/__init__.py rename to loveapp/models/__init__.py index e9f68d9..a3367de 100644 --- a/models/__init__.py +++ b/loveapp/models/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # flake8: noqa - from .access_key import AccessKey from .alias import Alias from .employee import Employee diff --git a/models/access_key.py b/loveapp/models/access_key.py similarity index 100% rename from models/access_key.py rename to loveapp/models/access_key.py diff --git a/models/alias.py b/loveapp/models/alias.py similarity index 86% rename from models/alias.py rename to loveapp/models/alias.py index d554199..da3e81f 100644 --- a/models/alias.py +++ b/loveapp/models/alias.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from google.appengine.ext import ndb -from models.employee import Employee +from loveapp.models.employee import Employee class Alias(ndb.Model): diff --git a/models/employee.py b/loveapp/models/employee.py similarity index 88% rename from models/employee.py rename to loveapp/models/employee.py index bb0ec7a..9ba8fc5 100644 --- a/models/employee.py +++ b/loveapp/models/employee.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- import base64 -import hashlib import functools +import hashlib -from google.appengine.ext import ndb from google.appengine.api import users +from google.appengine.ext import ndb -import config +import loveapp.config from errors import NoSuchEmployee -from util.pagination import Pagination +from loveapp.util.pagination import Pagination def memoized(func): @@ -52,7 +52,10 @@ def get_current_employee(cls): def create_from_dict(cls, d, persist=True): new_employee = cls() new_employee.username = d['username'] - new_employee.user = users.User('{user}@{domain}'.format(user=new_employee.username, domain=config.DOMAIN)) + new_employee.user = users.User( + '{user}@{domain}'.format(user=new_employee.username, domain=loveapp.config.DOMAIN), + _auth_domain=loveapp.config.DOMAIN + ) new_employee.update_from_dict(d) if persist is True: @@ -84,7 +87,7 @@ def update_from_dict(self, d): def get_gravatar(self): """Creates gravatar URL from email address.""" m = hashlib.md5() - m.update(self.user.email()) + m.update(self.user.email().encode()) encoded_hash = base64.b16encode(m.digest()).lower() return 'https://gravatar.com/avatar/{}?s=200'.format(encoded_hash) @@ -92,9 +95,9 @@ def get_photo_url(self): """Return an avatar photo URL (depending on Gravatar config). This still could be empty, in which case the theme needs to provide an alternate photo. """ - if config.GRAVATAR == 'always': + if loveapp.config.GRAVATAR == 'always': return self.get_gravatar() - elif config.GRAVATAR == 'backup' and not self.photo_url: + elif loveapp.config.GRAVATAR == 'backup' and not self.photo_url: return self.get_gravatar() else: return self.photo_url diff --git a/models/love.py b/loveapp/models/love.py similarity index 93% rename from models/love.py rename to loveapp/models/love.py index 4b10588..eafa84a 100644 --- a/models/love.py +++ b/loveapp/models/love.py @@ -3,7 +3,7 @@ from google.appengine.ext import ndb -from models import Employee +from loveapp.models import Employee class Love(ndb.Model): diff --git a/models/love_count.py b/loveapp/models/love_count.py similarity index 98% rename from models/love_count.py rename to loveapp/models/love_count.py index ee6ea1b..49811a7 100644 --- a/models/love_count.py +++ b/loveapp/models/love_count.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from google.appengine.ext import ndb -from logic import utc_week_limits +from loveapp.logic import utc_week_limits class LoveCount(ndb.Model): diff --git a/models/love_link.py b/loveapp/models/love_link.py similarity index 94% rename from models/love_link.py rename to loveapp/models/love_link.py index 5f558fe..072ac08 100644 --- a/models/love_link.py +++ b/loveapp/models/love_link.py @@ -3,7 +3,7 @@ from google.appengine.ext import ndb -import config +import loveapp.config as config class LoveLink(ndb.Model): diff --git a/models/secret.py b/loveapp/models/secret.py similarity index 100% rename from models/secret.py rename to loveapp/models/secret.py diff --git a/models/subscription.py b/loveapp/models/subscription.py similarity index 92% rename from models/subscription.py rename to loveapp/models/subscription.py index c277325..18e6973 100644 --- a/models/subscription.py +++ b/loveapp/models/subscription.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from google.appengine.ext import ndb -from logic.notification_request import CONTENT_TYPE_JSON -from models import Employee +from loveapp.logic.notification_request import CONTENT_TYPE_JSON +from loveapp.models import Employee class Subscription(ndb.Model): diff --git a/models/toggle.py b/loveapp/models/toggle.py similarity index 100% rename from models/toggle.py rename to loveapp/models/toggle.py diff --git a/static/robots.txt b/loveapp/static/robots.txt similarity index 100% rename from static/robots.txt rename to loveapp/static/robots.txt diff --git a/themes/default/info.json b/loveapp/themes/default/info.json similarity index 100% rename from themes/default/info.json rename to loveapp/themes/default/info.json diff --git a/themes/default/static/css/bootstrap.min.css b/loveapp/themes/default/static/css/bootstrap.min.css similarity index 100% rename from themes/default/static/css/bootstrap.min.css rename to loveapp/themes/default/static/css/bootstrap.min.css diff --git a/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css similarity index 100% rename from themes/default/static/css/style.css rename to loveapp/themes/default/static/css/style.css diff --git a/themes/default/static/img/favicon.png b/loveapp/themes/default/static/img/favicon.png similarity index 100% rename from themes/default/static/img/favicon.png rename to loveapp/themes/default/static/img/favicon.png diff --git a/themes/default/static/img/glyphicons-halflings-white.png b/loveapp/themes/default/static/img/glyphicons-halflings-white.png similarity index 100% rename from themes/default/static/img/glyphicons-halflings-white.png rename to loveapp/themes/default/static/img/glyphicons-halflings-white.png diff --git a/themes/default/static/img/glyphicons-halflings.png b/loveapp/themes/default/static/img/glyphicons-halflings.png similarity index 100% rename from themes/default/static/img/glyphicons-halflings.png rename to loveapp/themes/default/static/img/glyphicons-halflings.png diff --git a/themes/default/static/img/logo.png b/loveapp/themes/default/static/img/logo.png similarity index 100% rename from themes/default/static/img/logo.png rename to loveapp/themes/default/static/img/logo.png diff --git a/themes/default/static/img/rocket.png b/loveapp/themes/default/static/img/rocket.png similarity index 100% rename from themes/default/static/img/rocket.png rename to loveapp/themes/default/static/img/rocket.png diff --git a/themes/default/static/img/star_header_bg.png b/loveapp/themes/default/static/img/star_header_bg.png similarity index 100% rename from themes/default/static/img/star_header_bg.png rename to loveapp/themes/default/static/img/star_header_bg.png diff --git a/themes/default/static/img/user_medium_square.png b/loveapp/themes/default/static/img/user_medium_square.png similarity index 100% rename from themes/default/static/img/user_medium_square.png rename to loveapp/themes/default/static/img/user_medium_square.png diff --git a/themes/default/static/js/jquery-ui-1.10.4.custom.min.js b/loveapp/themes/default/static/js/jquery-ui-1.10.4.custom.min.js similarity index 100% rename from themes/default/static/js/jquery-ui-1.10.4.custom.min.js rename to loveapp/themes/default/static/js/jquery-ui-1.10.4.custom.min.js diff --git a/themes/default/static/js/linkify.js b/loveapp/themes/default/static/js/linkify.js similarity index 100% rename from themes/default/static/js/linkify.js rename to loveapp/themes/default/static/js/linkify.js diff --git a/themes/default/static/js/main.js b/loveapp/themes/default/static/js/main.js similarity index 100% rename from themes/default/static/js/main.js rename to loveapp/themes/default/static/js/main.js diff --git a/themes/default/templates/alias_form.html b/loveapp/themes/default/templates/alias_form.html similarity index 100% rename from themes/default/templates/alias_form.html rename to loveapp/themes/default/templates/alias_form.html diff --git a/themes/default/templates/aliases.html b/loveapp/themes/default/templates/aliases.html similarity index 100% rename from themes/default/templates/aliases.html rename to loveapp/themes/default/templates/aliases.html diff --git a/themes/default/templates/email.html b/loveapp/themes/default/templates/email.html similarity index 99% rename from themes/default/templates/email.html rename to loveapp/themes/default/templates/email.html index 1c40d77..d931c57 100644 --- a/themes/default/templates/email.html +++ b/loveapp/themes/default/templates/email.html @@ -48,7 +48,7 @@

{{ sender.full_name }}

{{ sender.department }}
-

{{ love.message }}

+

{{ love.message|linkify_company_values }}

{% if love.secret %}

Shh... sent secretly!

{% endif %} diff --git a/themes/default/templates/employees.html b/loveapp/themes/default/templates/employees.html similarity index 100% rename from themes/default/templates/employees.html rename to loveapp/themes/default/templates/employees.html diff --git a/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html similarity index 100% rename from themes/default/templates/explore.html rename to loveapp/themes/default/templates/explore.html diff --git a/themes/default/templates/home.html b/loveapp/themes/default/templates/home.html similarity index 100% rename from themes/default/templates/home.html rename to loveapp/themes/default/templates/home.html diff --git a/themes/default/templates/import.html b/loveapp/themes/default/templates/import.html similarity index 100% rename from themes/default/templates/import.html rename to loveapp/themes/default/templates/import.html diff --git a/themes/default/templates/import_employees_form.html b/loveapp/themes/default/templates/import_employees_form.html similarity index 100% rename from themes/default/templates/import_employees_form.html rename to loveapp/themes/default/templates/import_employees_form.html diff --git a/themes/default/templates/keys.html b/loveapp/themes/default/templates/keys.html similarity index 100% rename from themes/default/templates/keys.html rename to loveapp/themes/default/templates/keys.html diff --git a/themes/default/templates/layout.html b/loveapp/themes/default/templates/layout.html similarity index 100% rename from themes/default/templates/layout.html rename to loveapp/themes/default/templates/layout.html diff --git a/themes/default/templates/leaderboard.html b/loveapp/themes/default/templates/leaderboard.html similarity index 100% rename from themes/default/templates/leaderboard.html rename to loveapp/themes/default/templates/leaderboard.html diff --git a/themes/default/templates/love_form.html b/loveapp/themes/default/templates/love_form.html similarity index 100% rename from themes/default/templates/love_form.html rename to loveapp/themes/default/templates/love_form.html diff --git a/themes/default/templates/love_link.html b/loveapp/themes/default/templates/love_link.html similarity index 100% rename from themes/default/templates/love_link.html rename to loveapp/themes/default/templates/love_link.html diff --git a/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html similarity index 100% rename from themes/default/templates/me.html rename to loveapp/themes/default/templates/me.html diff --git a/themes/default/templates/parts/avatar.html b/loveapp/themes/default/templates/parts/avatar.html similarity index 100% rename from themes/default/templates/parts/avatar.html rename to loveapp/themes/default/templates/parts/avatar.html diff --git a/themes/default/templates/parts/facetile.html b/loveapp/themes/default/templates/parts/facetile.html similarity index 100% rename from themes/default/templates/parts/facetile.html rename to loveapp/themes/default/templates/parts/facetile.html diff --git a/themes/default/templates/parts/flash.html b/loveapp/themes/default/templates/parts/flash.html similarity index 100% rename from themes/default/templates/parts/flash.html rename to loveapp/themes/default/templates/parts/flash.html diff --git a/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html similarity index 100% rename from themes/default/templates/parts/love_message.html rename to loveapp/themes/default/templates/parts/love_message.html diff --git a/themes/default/templates/parts/photobox.html b/loveapp/themes/default/templates/parts/photobox.html similarity index 100% rename from themes/default/templates/parts/photobox.html rename to loveapp/themes/default/templates/parts/photobox.html diff --git a/themes/default/templates/sent.html b/loveapp/themes/default/templates/sent.html similarity index 100% rename from themes/default/templates/sent.html rename to loveapp/themes/default/templates/sent.html diff --git a/themes/default/templates/subscription_form.html b/loveapp/themes/default/templates/subscription_form.html similarity index 100% rename from themes/default/templates/subscription_form.html rename to loveapp/themes/default/templates/subscription_form.html diff --git a/themes/default/templates/subscriptions.html b/loveapp/themes/default/templates/subscriptions.html similarity index 100% rename from themes/default/templates/subscriptions.html rename to loveapp/themes/default/templates/subscriptions.html diff --git a/themes/default/templates/values.html b/loveapp/themes/default/templates/values.html similarity index 100% rename from themes/default/templates/values.html rename to loveapp/themes/default/templates/values.html diff --git a/util/__init__.py b/loveapp/util/__init__.py similarity index 98% rename from util/__init__.py rename to loveapp/util/__init__.py index 421f1d6..987db09 100644 --- a/util/__init__.py +++ b/loveapp/util/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- # flake8: noqa - from . import recipient diff --git a/util/auth.py b/loveapp/util/auth.py similarity index 100% rename from util/auth.py rename to loveapp/util/auth.py diff --git a/util/company_values.py b/loveapp/util/company_values.py similarity index 94% rename from util/company_values.py rename to loveapp/util/company_values.py index 85b3b23..3bf7b5e 100644 --- a/util/company_values.py +++ b/loveapp/util/company_values.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- import itertools -import markupsafe import re -import config +import markupsafe + +import loveapp.config as config def get_company_value(value_id): @@ -20,10 +21,10 @@ def get_company_value_link_pairs(): def supported_hashtags(): # Returns all supported hashtags - return map( + return list(map( lambda x: '#' + x, itertools.chain(*[value.hashtags for value in config.COMPANY_VALUES]) - ) + )) def get_hashtag_value_mapping(): @@ -37,7 +38,7 @@ def get_hashtag_value_mapping(): def linkify_company_values(love): # escape the input before we add our own safe links - escaped_love = unicode(markupsafe.escape(love)) + escaped_love = str(markupsafe.escape(love)) hashtag_value_mapping = get_hashtag_value_mapping() # find all the hashtags. diff --git a/util/converter.py b/loveapp/util/converter.py similarity index 100% rename from util/converter.py rename to loveapp/util/converter.py diff --git a/util/csrf.py b/loveapp/util/csrf.py similarity index 90% rename from util/csrf.py rename to loveapp/util/csrf.py index add91d3..8a7d5a1 100644 --- a/util/csrf.py +++ b/loveapp/util/csrf.py @@ -20,5 +20,5 @@ def check_csrf_protection(): def generate_csrf_token(): if '_csrf_token' not in session: - session['_csrf_token'] = binascii.hexlify(os.urandom(16)) + session['_csrf_token'] = str(binascii.hexlify(os.urandom(16))) return session['_csrf_token'] diff --git a/util/decorators.py b/loveapp/util/decorators.py similarity index 93% rename from util/decorators.py rename to loveapp/util/decorators.py index d4f2e7c..6c34686 100644 --- a/util/decorators.py +++ b/loveapp/util/decorators.py @@ -7,8 +7,8 @@ from flask.helpers import make_response from google.appengine.api import users -from models.access_key import AccessKey -from util.csrf import check_csrf_protection +from loveapp.models.access_key import AccessKey +from loveapp.util.csrf import check_csrf_protection def user_required(func): diff --git a/util/email.py b/loveapp/util/email.py similarity index 100% rename from util/email.py rename to loveapp/util/email.py diff --git a/util/pagination.py b/loveapp/util/pagination.py similarity index 100% rename from util/pagination.py rename to loveapp/util/pagination.py diff --git a/util/recipient.py b/loveapp/util/recipient.py similarity index 100% rename from util/recipient.py rename to loveapp/util/recipient.py diff --git a/util/render.py b/loveapp/util/render.py similarity index 93% rename from util/render.py rename to loveapp/util/render.py index 6211cc6..ec8cdc5 100644 --- a/util/render.py +++ b/loveapp/util/render.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import json -from flask_themes2 import render_theme_template from flask.helpers import make_response +from flask_themes2 import render_theme_template -import config +import loveapp.config as config def get_current_theme(): diff --git a/views/__init__.py b/loveapp/views/__init__.py similarity index 98% rename from views/__init__.py rename to loveapp/views/__init__.py index 26142f2..fb96c8f 100644 --- a/views/__init__.py +++ b/loveapp/views/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # flake8: noqa - from . import api from . import tasks from . import web diff --git a/views/api.py b/loveapp/views/api.py similarity index 80% rename from views/api.py rename to loveapp/views/api.py index 14ac2b3..703740c 100644 --- a/views/api.py +++ b/loveapp/views/api.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- +from flask import Blueprint from flask import make_response from flask import request from errors import TaintedLove -from logic import TIMESPAN_THIS_WEEK -from logic.love import get_love -from logic.love import send_loves -from logic.love_link import create_love_link -from logic.leaderboard import get_leaderboard_data -from main import app -from models import Employee -from util.decorators import api_key_required -from util.recipient import sanitize_recipients -from util.render import make_json_response -from views import common +from loveapp.logic import TIMESPAN_THIS_WEEK +from loveapp.logic.leaderboard import get_leaderboard_data +from loveapp.logic.love import get_love +from loveapp.logic.love import send_loves +from loveapp.logic.love_link import create_love_link +from loveapp.models import Employee +from loveapp.util.decorators import api_key_required +from loveapp.util.recipient import sanitize_recipients +from loveapp.util.render import make_json_response +from loveapp.views import common LOVE_CREATED_STATUS_CODE = 201 # Created @@ -21,9 +21,12 @@ LOVE_BAD_PARAMS_STATUS_CODE = 422 # Unprocessable Entity LOVE_NOT_FOUND_STATUS_CODE = 404 # Not Found +api_app = Blueprint('api_app', __name__) # GET /api/love -@app.route('/api/love', methods=['GET']) + + +@api_app.route('/api/love', methods=['GET']) @api_key_required def api_get_love(): sender = request.args.get('sender') @@ -62,7 +65,7 @@ def api_get_love(): # POST /api/love -@app.route('/api/love', methods=['POST']) +@api_app.route('/api/love', methods=['POST']) @api_key_required def api_send_loves(): sender = request.form.get('sender') @@ -88,7 +91,7 @@ def api_send_loves(): # GET /api/leaderboard -@app.route('/api/leaderboard', methods=['GET']) +@api_app.route('/api/leaderboard', methods=['GET']) @api_key_required def api_get_leaderboard(): department = request.args.get('department', None) @@ -114,7 +117,7 @@ def api_get_leaderboard(): 'username': loved['employee'].username, 'department': loved['employee'].department, 'love_count': loved['num_received'], - 'photo_url': lover['employee'].photo_url, + 'photo_url': loved['employee'].photo_url, } for loved in top_loved_dicts ] @@ -122,7 +125,7 @@ def api_get_leaderboard(): return make_json_response(final_result) -@app.route('/api/autocomplete', methods=['GET']) +@api_app.route('/api/autocomplete', methods=['GET']) @api_key_required def autocomplete(): return common.autocomplete(request) diff --git a/views/common.py b/loveapp/views/common.py similarity index 79% rename from views/common.py rename to loveapp/views/common.py index a9eb8e4..c746f3a 100644 --- a/views/common.py +++ b/loveapp/views/common.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from logic.employee import employees_matching_prefix -from util.render import make_json_response +from loveapp.logic.employee import employees_matching_prefix +from loveapp.util.render import make_json_response def autocomplete(request): @@ -12,7 +12,7 @@ def autocomplete(request): users = [ { 'label': u'{} ({})'.format(full_name, username), - 'value': unicode(username), + 'value': username, 'avatar_url': photo_url, } for full_name, username, photo_url diff --git a/views/tasks.py b/loveapp/views/tasks.py similarity index 56% rename from views/tasks.py rename to loveapp/views/tasks.py index 90135ba..0af0329 100644 --- a/views/tasks.py +++ b/loveapp/views/tasks.py @@ -1,39 +1,42 @@ # -*- coding: utf-8 -*- +from flask import Blueprint from flask import request from flask import Response from google.appengine.api import taskqueue from google.appengine.ext import ndb -import logic.employee -import logic.notifier -import logic.love -import logic.love_count -import logic.love_link -from main import app -from models import Love +import loveapp.logic.employee +import loveapp.logic.love +import loveapp.logic.love_count +import loveapp.logic.love_link +import loveapp.logic.notifier +from loveapp.models import Love +tasks_app = Blueprint('tasks_app', __name__) # All tasks that are to be executed by cron need to use HTTP GET # see https://cloud.google.com/appengine/docs/python/config/cron -@app.route('/tasks/employees/load/s3', methods=['GET']) + + +@tasks_app.route('/tasks/employees/load/s3', methods=['GET']) def load_employees_from_s3(): - logic.employee.load_employees() + loveapp.logic.employee.load_employees() # we need to rebuild the love count index as the departments may have changed. taskqueue.add(url='/tasks/love_count/rebuild') return Response(status=200) # This task has a web UI to trigger it, so let's use POST -@app.route('/tasks/employees/load/csv', methods=['POST']) +@tasks_app.route('/tasks/employees/load/csv', methods=['POST']) def load_employees_from_csv(): - logic.employee.load_employees_from_csv() + loveapp.logic.employee.load_employees_from_csv() # we need to rebuild the love count index as the departments may have changed. taskqueue.add(url='/tasks/love_count/rebuild') return Response(status=200) # One-off tasks are much easier to trigger using GET -@app.route('/tasks/employees/combine', methods=['GET']) +@tasks_app.route('/tasks/employees/combine', methods=['GET']) def combine_employees(): old_username, new_username = request.args['old'], request.args['new'] if not old_username: @@ -41,38 +44,38 @@ def combine_employees(): elif not new_username: return Response(response='{} is not a valid username'.format(new_username), status=400) - logic.employee.combine_employees(old_username, new_username) + loveapp.logic.employee.combine_employees(old_username, new_username) return Response(status=200) -@app.route('/tasks/index/rebuild', methods=['GET']) +@tasks_app.route('/tasks/index/rebuild', methods=['GET']) def rebuild_index(): - logic.employee.rebuild_index() + loveapp.logic.employee.rebuild_index() return Response(status=200) -@app.route('/tasks/love/email', methods=['POST']) +@tasks_app.route('/tasks/love/email', methods=['POST']) def email_love(): love_id = int(request.form['id']) love = ndb.Key(Love, love_id).get() - logic.love.send_love_email(love) + loveapp.logic.love.send_love_email(love) return Response(status=200) -@app.route('/tasks/love_count/rebuild', methods=['GET']) +@tasks_app.route('/tasks/love_count/rebuild', methods=['GET']) def rebuild_love_count(): - logic.love_count.rebuild_love_count() + loveapp.logic.love_count.rebuild_love_count() return Response(status=200) -@app.route('/tasks/subscribers/notify', methods=['POST']) +@tasks_app.route('/tasks/subscribers/notify', methods=['POST']) def notify_subscribers(): - notifier = logic.notifier.notifier_for_event(request.json['event'])(**request.json['options']) + notifier = loveapp.logic.notifier.notifier_for_event(request.json['event'])(**request.json['options']) notifier.notify() return Response(status=200) -@app.route('/tasks/lovelinks/cleanup', methods=['GET']) +@tasks_app.route('/tasks/lovelinks/cleanup', methods=['GET']) def lovelinks_cleanup(): - logic.love_link.love_links_cleanup() + loveapp.logic.love_link.love_links_cleanup() return Response(status=200) diff --git a/views/web.py b/loveapp/views/web.py similarity index 67% rename from views/web.py rename to loveapp/views/web.py index 3aaf9da..4ad1990 100644 --- a/views/web.py +++ b/loveapp/views/web.py @@ -1,50 +1,50 @@ # -*- coding: utf-8 -*- import os.path - -import config - from datetime import datetime from flask import abort +from flask import Blueprint from flask import flash from flask import redirect from flask import request from flask import url_for +from google.appengine.api import taskqueue -import logic.alias -import logic.employee -import logic.event -import logic.love -import logic.love_link -import logic.love_count -import logic.subscription +import loveapp.config as config +import loveapp.logic.alias +import loveapp.logic.employee +import loveapp.logic.event +import loveapp.logic.love +import loveapp.logic.love_count +import loveapp.logic.love_link +import loveapp.logic.subscription from errors import NoSuchEmployee from errors import NoSuchLoveLink from errors import TaintedLove -from google.appengine.api import taskqueue -from logic import TIMESPAN_THIS_WEEK -from logic.love_link import create_love_link -from logic.leaderboard import get_leaderboard_data -from main import app -from models import AccessKey -from models import Alias -from models import Employee -from models import Subscription -from util.decorators import admin_required -from util.decorators import csrf_protect -from util.decorators import user_required -from util.recipient import sanitize_recipients -from util.render import render_template -from util.render import make_json_response -from util.company_values import get_company_value -from util.company_values import get_company_value_link_pairs -from util.company_values import values_matching_prefix -from views import common -from logic.office import get_all_offices -from logic.department import get_all_departments - - -@app.route('/', methods=['GET']) +from loveapp.logic import TIMESPAN_THIS_WEEK +from loveapp.logic.department import get_all_departments +from loveapp.logic.leaderboard import get_leaderboard_data +from loveapp.logic.love_link import create_love_link +from loveapp.logic.office import get_all_offices +from loveapp.models import Alias +from loveapp.models import Employee +from loveapp.models import Subscription +from loveapp.models.access_key import AccessKey +from loveapp.util.company_values import get_company_value +from loveapp.util.company_values import get_company_value_link_pairs +from loveapp.util.company_values import values_matching_prefix +from loveapp.util.decorators import admin_required +from loveapp.util.decorators import csrf_protect +from loveapp.util.decorators import user_required +from loveapp.util.recipient import sanitize_recipients +from loveapp.util.render import make_json_response +from loveapp.util.render import render_template +from loveapp.views import common + +web_app = Blueprint('web_app', __name__) + + +@web_app.route('/', methods=['GET']) @user_required def home(): link_id = request.args.get('link_id', None) @@ -61,13 +61,13 @@ def home(): ) -@app.route('/me', methods=['GET']) +@web_app.route('/me', methods=['GET']) @user_required def me(): current_employee = Employee.get_current_employee() - sent_love = logic.love.recent_sent_love(current_employee.key, limit=20) - received_love = logic.love.recent_received_love(current_employee.key, limit=20) + sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=20) + received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=20) return render_template( 'me.html', @@ -78,11 +78,11 @@ def me(): ) -@app.route('/', methods=['GET']) +@web_app.route('/', methods=['GET']) @user_required def me_or_explore(user): current_employee = Employee.get_current_employee() - username = logic.alias.name_for_alias(user.lower().strip()) + username = loveapp.logic.alias.name_for_alias(user.lower().strip()) try: user_key = Employee.get_key_for_username(username) @@ -90,9 +90,9 @@ def me_or_explore(user): abort(404) if current_employee.key == user_key: - return redirect(url_for('me')) + return redirect(url_for('web_app.me')) else: - return redirect(url_for('explore', user=username)) + return redirect(url_for('web_app.explore', user=username)) def format_loves(loves): @@ -109,16 +109,16 @@ def format_loves(loves): return loves_list_one, loves_list_two -@app.route('/value/', methods=['GET']) +@web_app.route('/value/', methods=['GET']) @user_required def single_company_value(company_value_id): company_value = get_company_value(company_value_id.upper()) if not company_value: - return redirect(url_for('company_values')) + return redirect(url_for('web_app.company_values')) current_employee = Employee.get_current_employee() - loves = logic.love.recent_loves_by_company_value(None, company_value.id, limit=100).get_result() + loves = loveapp.logic.love.recent_loves_by_company_value(None, company_value.id, limit=100).get_result() loves_list_one, loves_list_two = format_loves(loves) return render_template( @@ -132,13 +132,13 @@ def single_company_value(company_value_id): ) -@app.route('/values', methods=['GET']) +@web_app.route('/values', methods=['GET']) @user_required def company_values(): if not config.COMPANY_VALUES: abort(404) - loves = logic.love.recent_loves_with_any_company_value(None, limit=100).get_result() + loves = loveapp.logic.love.recent_loves_with_any_company_value(None, limit=100).get_result() loves_list_one, loves_list_two = format_loves(loves) current_employee = Employee.get_current_employee() @@ -154,11 +154,11 @@ def company_values(): ) -@app.route('/l/', methods=['GET']) +@web_app.route('/l/', methods=['GET']) @user_required def love_link(link_id): try: - loveLink = logic.love_link.get_love_link(link_id) + loveLink = loveapp.logic.love_link.get_love_link(link_id) recipients_str = loveLink.recipient_list message = loveLink.message @@ -179,10 +179,10 @@ def love_link(link_id): ) except (NoSuchLoveLink, NoSuchEmployee): flash('Sorry, that link ({}) is no longer valid.'.format(link_id), 'error') - return redirect(url_for('home')) + return redirect(url_for('web_app.home')) -@app.route('/explore', methods=['GET']) +@web_app.route('/explore', methods=['GET']) @user_required def explore(): username = request.args.get('user', None) @@ -203,10 +203,10 @@ def explore(): if not user_key: flash('Sorry, "{}" is not a valid user.'.format(username), 'error') - return redirect(url_for('explore')) + return redirect(url_for('web_app.explore')) - sent_love = logic.love.recent_sent_love(user_key, include_secret=False, limit=20) - received_love = logic.love.recent_received_love(user_key, include_secret=False, limit=20) + sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20) + received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20) return render_template( 'explore.html', @@ -217,7 +217,7 @@ def explore(): ) -@app.route('/leaderboard', methods=['GET']) +@web_app.route('/leaderboard', methods=['GET']) @user_required def leaderboard(): timespan = request.args.get('timespan', TIMESPAN_THIS_WEEK) @@ -241,7 +241,7 @@ def leaderboard(): ) -@app.route('/sent', methods=['GET']) +@web_app.route('/sent', methods=['GET']) @user_required def sent(): link_id = request.args.get('link_id', None) @@ -249,7 +249,7 @@ def sent(): message = request.args.get('message', None) if not link_id or not recipients_str or not message: - return redirect(url_for('home')) + return redirect(url_for('web_app.home')) recipients = sanitize_recipients(recipients_str) loved = [ @@ -267,7 +267,7 @@ def sent(): ) -@app.route('/keys', methods=['GET']) +@web_app.route('/keys', methods=['GET']) @admin_required def keys(): api_keys = AccessKey.query().fetch() @@ -277,7 +277,7 @@ def keys(): ) -@app.route('/keys/create', methods=['POST']) +@web_app.route('/keys/create', methods=['POST']) @csrf_protect @admin_required def create_key(): @@ -285,10 +285,10 @@ def create_key(): new_key = AccessKey.create(description) flash('Your API key {} has been created. Refresh the page to see it below.'.format(new_key.access_key)) - return redirect(url_for('keys')) + return redirect(url_for('web_app.keys')) -@app.route('/love', methods=['POST']) +@web_app.route('/love', methods=['POST']) @csrf_protect @user_required def love(): @@ -300,31 +300,31 @@ def love(): if not recipients: flash('Enter a name, lover.', 'error') - return redirect(url_for('home')) + return redirect(url_for('web_app.home')) recipients_display_str = ', '.join(recipients) if not message: flash('Enter a message, lover.', 'error') - return redirect(url_for('home', recipients=recipients_display_str)) + return redirect(url_for('web_app.home', recipients=recipients_display_str)) try: if action == 'create_link': - _, real_recipients = logic.love.validate_love_recipients(recipients) + _, real_recipients = loveapp.logic.love.validate_love_recipients(recipients) real_display_str = ', '.join(real_recipients) hash_key = create_love_link(real_display_str, message).hash_key - return redirect(url_for('home', recipients=real_display_str, link_id=hash_key, message=message)) + return redirect(url_for('web_app.home', recipients=real_display_str, link_id=hash_key, message=message)) else: - real_recipients = logic.love.send_loves(recipients, message, secret=secret) + real_recipients = loveapp.logic.love.send_loves(recipients, message, secret=secret) # actual recipients may have the sender stripped from the list real_display_str = ', '.join(real_recipients) if secret: flash('Secret love sent to {}!'.format(real_display_str)) - return redirect(url_for('home')) + return redirect(url_for('web_app.home')) else: hash_key = link_id if link_id else create_love_link(real_display_str, message).hash_key - return redirect(url_for('sent', message=message, recipients=real_display_str, link_id=hash_key)) + return redirect(url_for('web_app.sent', message=message, recipients=real_display_str, link_id=hash_key)) except TaintedLove as exc: if exc.is_error: @@ -332,33 +332,33 @@ def love(): else: flash(exc.user_message) - return redirect(url_for('home', recipients=recipients_display_str, message=message)) + return redirect(url_for('web_app.home', recipients=recipients_display_str, message=message)) -@app.route('/user/autocomplete', methods=['GET']) +@web_app.route('/user/autocomplete', methods=['GET']) @user_required def autocomplete_web(): return common.autocomplete(request) -@app.route('/values/autocomplete', methods=['GET']) +@web_app.route('/values/autocomplete', methods=['GET']) @user_required def autocomplete_company_values_web(): matching_prefixes = values_matching_prefix(request.args.get('term', None)) return make_json_response(matching_prefixes) -@app.route('/subscriptions', methods=['GET']) +@web_app.route('/subscriptions', methods=['GET']) @admin_required def subscriptions(): return render_template( 'subscriptions.html', subscriptions=Subscription.query().fetch(), - events=logic.event.EVENTS, + events=loveapp.logic.event.EVENTS, ) -@app.route('/subscriptions/create', methods=['POST']) +@web_app.route('/subscriptions/create', methods=['POST']) @csrf_protect @admin_required def create_subscription(): @@ -375,19 +375,19 @@ def create_subscription(): except ValueError: flash('Something went wrong. Please check your input.', 'error') - return redirect(url_for('subscriptions')) + return redirect(url_for('web_app.subscriptions')) -@app.route('/subscriptions//delete', methods=['POST']) +@web_app.route('/subscriptions//delete', methods=['POST']) @csrf_protect @admin_required def delete_subscription(subscription_id): - logic.subscription.delete_subscription(subscription_id) + loveapp.logic.subscription.delete_subscription(subscription_id) flash('Subscription deleted. Refresh the page to see it\'s gone.', 'info') - return redirect(url_for('subscriptions')) + return redirect(url_for('web_app.subscriptions')) -@app.route('/aliases', methods=['GET']) +@web_app.route('/aliases', methods=['GET']) @admin_required def aliases(): return render_template( @@ -396,32 +396,32 @@ def aliases(): ) -@app.route('/aliases', methods=['POST']) +@web_app.route('/aliases', methods=['POST']) @csrf_protect @admin_required def create_alias(): try: - logic.alias.save_alias( + loveapp.logic.alias.save_alias( request.form.get('alias').strip(), request.form.get('username').strip(), ) flash('New alias successfully saved. Refresh the page to see it.') except Exception as e: - flash('Something went wrong: {}.'.format(e.message), 'error') + flash('Something went wrong: {}.'.format(str(e)), 'error') - return redirect(url_for('aliases')) + return redirect(url_for('web_app.aliases')) -@app.route('/aliases//delete', methods=['POST']) +@web_app.route('/aliases//delete', methods=['POST']) @csrf_protect @admin_required def delete_alias(alias_id): - logic.alias.delete_alias(alias_id) + loveapp.logic.alias.delete_alias(alias_id) flash('Alias successfully deleted. Refresh the page to see it\'s gone.', 'info') - return redirect(url_for('aliases')) + return redirect(url_for('web_app.aliases')) -@app.route('/employees', methods=['GET']) +@web_app.route('/employees', methods=['GET']) @admin_required def employees(): return render_template( @@ -434,19 +434,19 @@ def employees(): ) -@app.route('/employees/import', methods=['GET']) +@web_app.route('/employees/import', methods=['GET']) @admin_required def import_employees_form(): - import_file_exists = os.path.isfile(logic.employee.csv_import_file()) + import_file_exists = os.path.isfile(loveapp.logic.employee.csv_import_file()) return render_template( 'import.html', import_file_exists=import_file_exists, ) -@app.route('/employees/import', methods=['POST']) +@web_app.route('/employees/import', methods=['POST']) @admin_required def import_employees(): flash('We started importing employee data in the background. Refresh the page to see it.', 'info') taskqueue.add(url='/tasks/employees/load/csv') - return redirect(url_for('employees')) + return redirect(url_for('web_app.employees')) diff --git a/main.py b/main.py index 5c28a5b..5f490d3 100644 --- a/main.py +++ b/main.py @@ -1,30 +1,8 @@ # -*- coding: utf-8 -*- # flake8: noqa -from flask import Flask -from flask_themes2 import Themes +from loveapp import create_app -import config -from util.auth import is_admin -from util.converter import RegexConverter -from util.company_values import linkify_company_values -from util.csrf import generate_csrf_token +app = create_app() - -app = Flask(__name__.split('.')[0]) -app.secret_key = config.SECRET_KEY -app.url_map.converters['regex'] = RegexConverter -app.jinja_env.globals['config'] = config -app.jinja_env.globals['csrf_token'] = generate_csrf_token -app.jinja_env.globals['is_admin'] = is_admin -app.jinja_env.filters['linkify_company_values'] = linkify_company_values - -Themes(app, app_identifier='yelplove') - -# if debug property is present, let's use it -try: - app.debug = config.DEBUG -except AttributeError: - app.debug = False - -# This import needs to stay down here, otherwise we'll get ImportErrors when running tests -import views # noqa +if __name__ == '__main__': + app.run() diff --git a/requirements-dev.txt b/requirements-dev.txt index 719fbb6..7852b1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,9 +2,9 @@ # Unit testing tools, mocking frameworks, debuggers, proxies, ... # These will be used for tox and other testing environments. -r requirements.txt -Flask-WebTest==0.0.8 +Flask-WebTest==0.1.4 ipdb -mock==2.0.0 -NoseGAE==0.5.10 -pre-commit==0.13.3 -pyrsistent==0.16.1 +mock==4.0.3 +pre-commit +pytest==8.1.2 +tox==4.15.0 diff --git a/requirements.txt b/requirements.txt index 8eace35..3916100 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ +appengine-python-standard>=1.0.0 # This requirements file lists all dependecies for this project. # Run 'make lib' to install these dependencies in this project's lib directory. -boto==2.46.1 -Flask==1.0 -Flask-Themes2==0.1.4 -itsdangerous==0.24 -Jinja2==2.11.3 -MarkupSafe==1.0 -pytz==2016.10 -urllib3==1.26.5 -Werkzeug==0.15.5 -wheel==0.29.0 +beautifulsoup4==4.12.3 +boto==2.49.0 +Flask==3.0.3 +Flask-Themes2==1.0.1 +itsdangerous==2.2.0 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +pytz==2024.1 +pyyaml==6.0.1 +urllib3==1.26.18 +Werkzeug==3.0.0 diff --git a/setup.py b/setup.py index 73cb5c9..fe1cca7 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from setuptools import setup diff --git a/testing/factories/alias.py b/testing/factories/alias.py index 4ae1c64..0f4673b 100644 --- a/testing/factories/alias.py +++ b/testing/factories/alias.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from models import Alias -from models import Employee +from loveapp.models import Alias +from loveapp.models import Employee def create_alias_with_employee_username( diff --git a/testing/factories/employee.py b/testing/factories/employee.py index 6d56113..1edf8bc 100644 --- a/testing/factories/employee.py +++ b/testing/factories/employee.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models import Employee +from loveapp.models import Employee def create_employee( diff --git a/testing/factories/love.py b/testing/factories/love.py index aa18c1e..7681c47 100644 --- a/testing/factories/love.py +++ b/testing/factories/love.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models import Love +from loveapp.models import Love DEFAULT_LOVE_MESSAGE = 'So Long, and Thanks For All the Fish' diff --git a/testing/factories/love_link.py b/testing/factories/love_link.py index 3e336f9..f9219f7 100644 --- a/testing/factories/love_link.py +++ b/testing/factories/love_link.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models import LoveLink +from loveapp.models import LoveLink def create_love_link( diff --git a/testing/factories/secret.py b/testing/factories/secret.py index 3b65033..84fe3f8 100644 --- a/testing/factories/secret.py +++ b/testing/factories/secret.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models import Secret +from loveapp.models import Secret def create_secret(id, value='secret'): diff --git a/testing/factories/subscription.py b/testing/factories/subscription.py index 4533a4c..851f3f9 100644 --- a/testing/factories/subscription.py +++ b/testing/factories/subscription.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from models import Subscription +from loveapp.models import Subscription def create_subscription( diff --git a/testing/util.py b/testing/util.py index 0ec4599..b627456 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,85 +1,57 @@ # -*- coding: utf-8 -*- import mock -import os -import unittest +import pytest +from bs4 import BeautifulSoup -from flask_themes2 import Themes, load_themes_from -from flask_webtest import TestApp - -import main from testing.factories import create_employee -def get_test_app(): - - def test_loader(app): - return load_themes_from(os.path.join(os.path.dirname(__file__), '../themes/')) - - Themes(main.app, app_identifier='yelplove', loaders=[test_loader]) - - return TestApp(main.app) - - -class YelpLoveTestCase(unittest.TestCase): - - app = get_test_app() +class YelpLoveTestCase(): def assertRequiresLogin(self, response): - self.assertEqual(response.status_int, 302) - self.assert_( - response.headers['Location'].startswith('https://www.google.com/accounts/Login'), - 'Unexpected Location header: {0}'.format(response.headers['Location']), - ) + assert response.status_code == 302 + assert response.headers['Location'].startswith('https://www.google.com/accounts/Login'), \ + 'Unexpected Location header: {0}'.format(response.headers['Location']) def assertRequiresAdmin(self, response): - self.assertEqual(response.status_int, 401) + assert response.status_code == 401 - def assertHasCsrf(self, form, session): + def assertHasCsrf(self, response, form_class, session): """Make sure the response form contains the correct CSRF token. :param form: a form entry from response.forms :param session: response.session """ - self.assertIsNotNone(form.get('_csrf_token')) - self.assertEqual( - form['_csrf_token'].value, session['_csrf_token'], - ) - - def addCsrfTokenToSession(self): + soup = BeautifulSoup(response.data, 'html.parser') + csrf_token = soup.find('form', class_=form_class).\ + find('input', attrs={'name': '_csrf_token'}).\ + get('value') + assert csrf_token is not None + assert csrf_token == session['_csrf_token'] + + def addCsrfTokenToSession(self, client): csrf_token = 'MY_TOKEN' - with self.app.session_transaction() as session: + with client.session_transaction() as session: session['_csrf_token'] = csrf_token return csrf_token class LoggedInUserBaseTest(YelpLoveTestCase): - nosegae_datastore_v3 = True - nosegae_datastore_v3_kwargs = { - 'datastore_file': '/tmp/nosegae.sqlite3', - 'use_sqlite': True - } - - nosegae_user = True - nosegae_user_kwargs = dict( - USER_ID='johndoe', - USER_EMAIL='johndoe@example.com', - USER_IS_ADMIN='0', - ) - - @mock.patch('models.employee.config') - def setUp(self, mock_config): - mock_config.DOMAIN = 'example.com' - self.current_user = create_employee(username='johndoe') - - def tearDown(self): - self.current_user.key.delete() + @pytest.fixture(autouse=True) + def logged_in_user(self, gae_testbed): + self.logged_in_employee = create_employee(username='johndoe') + with mock.patch('loveapp.util.decorators.users.get_current_user') as mock_get_current_user: + mock_get_current_user.return_value = self.logged_in_employee.user + yield self.logged_in_employee + self.logged_in_employee.key.delete() class LoggedInAdminBaseTest(LoggedInUserBaseTest): - - nosegae_user_kwargs = dict( - USER_ID='johndoe', - USER_EMAIL='johndoe@example.com', - USER_IS_ADMIN='1', - ) + @pytest.fixture(autouse=True) + def logged_in_admin(self, gae_testbed): + self.logged_in_employee = create_employee(username='johndoe') + with mock.patch('loveapp.util.decorators.users.is_current_user_admin') as mock_is_current_user_admin: + mock_is_current_user_admin.return_value = True + yield self.logged_in_employee + self.logged_in_employee.key.delete() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..81fe661 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import os + +import mock +import pytest +from flask import template_rendered +from flask_themes2 import load_themes_from +from google.appengine.ext import testbed + +from loveapp import create_app + + +@pytest.fixture +def app(): # noqa + # do we need this? for what? + def test_loader(app): + return load_themes_from(os.path.join(os.path.dirname(__file__), '../loveapp/themes/')) + app = create_app(theme_loaders=[test_loader]) + + with app.app_context(): + yield app + + +@pytest.fixture +def client(app): + with app.test_client() as test_client: + yield test_client + + +@pytest.fixture +def recorded_templates(app): + recorded = [] + + def record(sender, template, context, **extra): + recorded.append((template, context)) + + template_rendered.connect(record, app) + try: + yield recorded + finally: + template_rendered.disconnect(record, app) + + +@pytest.fixture +def mock_config(): + with mock.patch('loveapp.config') as mock_config: + mock_config.DOMAIN = 'example.com' + yield mock_config + + +@pytest.fixture(scope='function') +def gae_testbed(): + tb = testbed.Testbed() + tb.activate() + tb.init_memcache_stub() + tb.init_datastore_v3_stub() + tb.init_search_stub() + tb.init_taskqueue_stub() + tb.init_user_stub() + + yield tb + + tb.deactivate() diff --git a/tests/logic/alias_test.py b/tests/logic/alias_test.py index 55e07db..b7c6684 100644 --- a/tests/logic/alias_test.py +++ b/tests/logic/alias_test.py @@ -1,27 +1,28 @@ # -*- coding: utf-8 -*- import unittest -import logic.alias +import pytest -from models import Alias +import loveapp.logic.alias +from loveapp.models import Alias from testing.factories import create_alias_with_employee_username from testing.factories import create_employee +@pytest.mark.usefixtures('gae_testbed') class AliasTest(unittest.TestCase): - nosegae_datastore_v3 = True def test_get_alias(self): create_employee(username='fuz') create_alias_with_employee_username(name='fuzzi', username='fuz') - self.assertIsNotNone(logic.alias.get_alias('fuzzi')) + self.assertIsNotNone(loveapp.logic.alias.get_alias('fuzzi')) def test_save_alias(self): johnd = create_employee(username='johnd') self.assertEqual(Alias.query().count(), 0) - alias = logic.alias.save_alias('johnny', 'johnd') + alias = loveapp.logic.alias.save_alias('johnny', 'johnd') self.assertEqual(Alias.query().count(), 1) self.assertEqual(alias.name, 'johnny') @@ -29,18 +30,18 @@ def test_save_alias(self): def test_delete_alias(self): create_employee(username='janed') - alias = logic.alias.save_alias('jane', 'janed') + alias = loveapp.logic.alias.save_alias('jane', 'janed') self.assertEqual(Alias.query().count(), 1) - logic.alias.delete_alias(alias.key.id()) + loveapp.logic.alias.delete_alias(alias.key.id()) self.assertEqual(Alias.query().count(), 0) def test_name_for_alias_with_alias(self): create_employee(username='janed') - logic.alias.save_alias('jane', 'janed') + loveapp.logic.alias.save_alias('jane', 'janed') - self.assertEqual(logic.alias.name_for_alias('jane'), 'janed') + self.assertEqual(loveapp.logic.alias.name_for_alias('jane'), 'janed') def test_name_for_alias_with_employee_name(self): - self.assertEqual(logic.alias.name_for_alias('janed'), 'janed') + self.assertEqual(loveapp.logic.alias.name_for_alias('janed'), 'janed') diff --git a/tests/logic/department_test.py b/tests/logic/department_test.py index 1f4506c..a4f04e1 100644 --- a/tests/logic/department_test.py +++ b/tests/logic/department_test.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -import unittest - - -from logic.department import get_all_departments +from loveapp.logic.department import get_all_departments from testing.factories import create_employee DEPARTMENTS = [ @@ -15,12 +12,8 @@ ] -class DepartmentTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True - - def test_get_all_departments(self): - for department in DEPARTMENTS: - create_employee(department=department, username='{}-{}'.format('username', department)) +def test_get_all_departments(gae_testbed): + for department in DEPARTMENTS: + create_employee(department=department, username='{}-{}'.format('username', department)) - self.assertEqual(set(DEPARTMENTS), set(get_all_departments())) + assert set(DEPARTMENTS) == set(get_all_departments()) diff --git a/tests/logic/email_test.py b/tests/logic/email_test.py index c868c16..7c8647b 100644 --- a/tests/logic/email_test.py +++ b/tests/logic/email_test.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- -import mock import unittest -import logic.email +import mock +import pytest + +import loveapp.logic.email +@pytest.mark.usefixtures('gae_testbed') class EmailTest(unittest.TestCase): """We really just want to test that configuration is honored here.""" @@ -14,24 +17,24 @@ class EmailTest(unittest.TestCase): html = '

hello test

' text = 'hello test' - @mock.patch('logic.email.EMAIL_BACKENDS') - @mock.patch('logic.email.config') + @mock.patch('loveapp.logic.email.EMAIL_BACKENDS') + @mock.patch('loveapp.logic.email.config') def test_send_email_appengine(self, mock_config, mock_backends): mock_config.EMAIL_BACKEND = 'appengine' mock_backends['appengine'] = mock.Mock() - logic.email.send_email(self.sender, self.recipient, self.subject, - self.html, self.text) + loveapp.logic.email.send_email(self.sender, self.recipient, self.subject, + self.html, self.text) mock_backends['appengine'].assert_called_once_with( self.sender, self.recipient, self.subject, self.html, self.text ) - @mock.patch('logic.email.EMAIL_BACKENDS') - @mock.patch('logic.email.config') + @mock.patch('loveapp.logic.email.EMAIL_BACKENDS') + @mock.patch('loveapp.logic.email.config') def test_send_email_sendgrid(self, mock_config, mock_backends): mock_config.EMAIL_BACKEND = 'sendgrid' mock_backends['sendgrid'] = mock.Mock() - logic.email.send_email(self.sender, self.recipient, self.subject, - self.html, self.text) + loveapp.logic.email.send_email(self.sender, self.recipient, self.subject, + self.html, self.text) mock_backends['sendgrid'].assert_called_once_with( self.sender, self.recipient, self.subject, self.html, self.text ) diff --git a/tests/logic/love_link_test.py b/tests/logic/love_link_test.py index d7fabfb..9d2ce58 100644 --- a/tests/logic/love_link_test.py +++ b/tests/logic/love_link_test.py @@ -1,53 +1,52 @@ # -*- coding: utf-8 -*- -import unittest import datetime +import unittest -import logic.love -import logic.love_link -from errors import NoSuchLoveLink -from testing.factories import create_love_link +import pytest +import loveapp.logic.love +import loveapp.logic.love_link +from errors import NoSuchLoveLink from testing.factories import create_employee +from testing.factories import create_love_link +@pytest.mark.usefixtures('gae_testbed') class LoveLinkTest(unittest.TestCase): - nosegae_taskqueue = True - nosegae_memcache = True - nosegae_datastore_v3 = True def setUp(self): self.link = create_love_link(hash_key='HeLLo', recipient_list='johndoe,janedoe', message='well hello there') self.princessbubblegum = create_employee(username='princessbubblegum') def test_get_love_link(self): - link = logic.love_link.get_love_link('HeLLo') + link = loveapp.logic.love_link.get_love_link('HeLLo') self.assertEqual(link.hash_key, 'HeLLo') self.assertEqual(link.recipient_list, 'johndoe,janedoe') self.assertEqual(link.message, 'well hello there') def test_create_love_link(self): - link = logic.love_link.create_love_link('jake', "it's adventure time!") + link = loveapp.logic.love_link.create_love_link('jake', "it's adventure time!") self.assertEqual(link.recipient_list, 'jake') self.assertEqual(link.message, "it's adventure time!") def test_add_recipient(self): - link = logic.love_link.create_love_link('finn', 'Mathematical!') + link = loveapp.logic.love_link.create_love_link('finn', 'Mathematical!') - logic.love_link.add_recipient(link.hash_key, 'princessbubblegum') - new_link = logic.love_link.get_love_link(link.hash_key) + loveapp.logic.love_link.add_recipient(link.hash_key, 'princessbubblegum') + new_link = loveapp.logic.love_link.get_love_link(link.hash_key) self.assertEqual(new_link.recipient_list, 'finn, princessbubblegum') def test_love_links_cleanup(self): - new_love = logic.love_link.create_love_link('jake', "I'm new love!") - old_love = logic.love_link.create_love_link('finn', "I'm old love :(") + new_love = loveapp.logic.love_link.create_love_link('jake', "I'm new love!") + old_love = loveapp.logic.love_link.create_love_link('finn', "I'm old love :(") old_love.timestamp = datetime.datetime.now() - datetime.timedelta(days=31) old_love.put() - logic.love_link.love_links_cleanup() - db_love = logic.love_link.get_love_link(new_love.hash_key) + loveapp.logic.love_link.love_links_cleanup() + db_love = loveapp.logic.love_link.get_love_link(new_love.hash_key) self.assertEqual(db_love.hash_key, new_love.hash_key) with self.assertRaises(NoSuchLoveLink): - logic.love_link.get_love_link(old_love.hash_key) + loveapp.logic.love_link.get_love_link(old_love.hash_key) diff --git a/tests/logic/love_test.py b/tests/logic/love_test.py index b20f81a..8b8aa7e 100644 --- a/tests/logic/love_test.py +++ b/tests/logic/love_test.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- -import mock import unittest -from config import CompanyValue -import logic.love +import mock +import pytest + +import loveapp.logic.love from errors import TaintedLove +from loveapp.config import CompanyValue from testing.factories import create_alias_with_employee_username from testing.factories import create_employee -class SendLovesTest(unittest.TestCase): - nosegae_taskqueue = True - nosegae_memcache = True - nosegae_datastore_v3 = True +@pytest.mark.usefixtures('gae_testbed') +class TestSendLoves(unittest.TestCase): def setUp(self): self.alice = create_employee(username='alice') @@ -20,48 +20,50 @@ def setUp(self): self.carol = create_employee(username='carol') self.message = 'hallo' - def test_send_loves(self): - logic.love.send_loves( + @mock.patch('google.appengine.api.taskqueue.add', autospec=True) + def test_send_loves(self, mock_taskqueue_add): + loveapp.logic.love.send_loves( set(['bob', 'carol']), self.message, sender_username='alice', ) - loves_for_bob = logic.love.get_love(None, 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love(None, 'bob').get_result() self.assertEqual(len(loves_for_bob), 1) self.assertEqual(loves_for_bob[0].sender_key, self.alice.key) self.assertEqual(loves_for_bob[0].message, self.message) - loves_for_carol = logic.love.get_love(None, 'carol').get_result() + loves_for_carol = loveapp.logic.love.get_love(None, 'carol').get_result() self.assertEqual(len(loves_for_carol), 1) self.assertEqual(loves_for_carol[0].sender_key, self.alice.key) self.assertEqual(loves_for_carol[0].message, self.message) def test_invalid_sender(self): with self.assertRaises(TaintedLove): - logic.love.send_loves( + loveapp.logic.love.send_loves( set(['alice']), 'hallo', sender_username='wwu', ) - def test_sender_is_a_recipient(self): - logic.love.send_loves( + @mock.patch('google.appengine.api.taskqueue.add', autospec=True) + def test_sender_is_a_recipient(self, mock_taskqueue_add): + loveapp.logic.love.send_loves( set(['bob', 'alice']), self.message, sender_username='alice', ) - loves_for_bob = logic.love.get_love('alice', 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love('alice', 'bob').get_result() self.assertEqual(len(loves_for_bob), 1) self.assertEqual(loves_for_bob[0].message, self.message) - loves_for_alice = logic.love.get_love(None, 'alice').get_result() + loves_for_alice = loveapp.logic.love.get_love(None, 'alice').get_result() self.assertEqual(loves_for_alice, []) def test_sender_is_only_recipient(self): with self.assertRaises(TaintedLove): - logic.love.send_loves( + loveapp.logic.love.send_loves( set(['alice']), self.message, sender_username='alice', @@ -69,22 +71,23 @@ def test_sender_is_only_recipient(self): def test_invalid_recipient(self): with self.assertRaises(TaintedLove): - logic.love.send_loves( + loveapp.logic.love.send_loves( set(['bob', 'dean']), 'hallo', sender_username='alice', ) - loves_for_bob = logic.love.get_love('alice', 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love('alice', 'bob').get_result() self.assertEqual(loves_for_bob, []) - def test_send_loves_with_alias(self): + @mock.patch('google.appengine.api.taskqueue.add', autospec=True) + def test_send_loves_with_alias(self, mock_taskqueue_add): message = 'Loving your alias' create_alias_with_employee_username(name='bobby', username=self.bob.username) - logic.love.send_loves(['bobby'], message, sender_username=self.carol.username) + loveapp.logic.love.send_loves(['bobby'], message, sender_username=self.carol.username) - loves_for_bob = logic.love.get_love('carol', 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love('carol', 'bob').get_result() self.assertEqual(len(loves_for_bob), 1) self.assertEqual(loves_for_bob[0].sender_key, self.carol.key) self.assertEqual(loves_for_bob[0].message, message) @@ -93,20 +96,21 @@ def test_send_loves_with_alias_and_username_for_same_user(self): create_alias_with_employee_username(name='bobby', username=self.bob.username) with self.assertRaises(TaintedLove): - logic.love.send_loves(['bob', 'bobby'], 'hallo', sender_username='alice') + loveapp.logic.love.send_loves(['bob', 'bobby'], 'hallo', sender_username='alice') - loves_for_bob = logic.love.get_love('alice', 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love('alice', 'bob').get_result() self.assertEqual(loves_for_bob, []) - @mock.patch('util.company_values.config') - def test_send_love_with_value_hashtag(self, mock_config): + @mock.patch('loveapp.util.company_values.config') + @mock.patch('google.appengine.api.taskqueue.add', autospec=True) + def test_send_love_with_value_hashtag(self, mock_taskqueue_add, mock_config): mock_config.COMPANY_VALUES = [ CompanyValue('AWESOME', 'awesome', ['awesome']) ] message = 'Loving your alias #Awesome' create_alias_with_employee_username(name='bobby', username=self.bob.username) - logic.love.send_loves(['bobby'], message, sender_username=self.carol.username) + loveapp.logic.love.send_loves(['bobby'], message, sender_username=self.carol.username) - loves_for_bob = logic.love.get_love('carol', 'bob').get_result() + loves_for_bob = loveapp.logic.love.get_love('carol', 'bob').get_result() self.assertEqual(len(loves_for_bob), 1) self.assertEqual(loves_for_bob[0].company_values, ['AWESOME']) diff --git a/tests/logic/office_test.py b/tests/logic/office_test.py index 48bc77f..8845715 100644 --- a/tests/logic/office_test.py +++ b/tests/logic/office_test.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- import unittest + import mock -from logic.office import REMOTE_OFFICE -from logic.office import get_all_offices -from logic.office import OfficeParser +import pytest + +from loveapp.logic.office import get_all_offices +from loveapp.logic.office import OfficeParser +from loveapp.logic.office import REMOTE_OFFICE from testing.factories import create_employee OFFICES = { @@ -12,9 +15,7 @@ } -class OfficeTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True +class TestOffice(unittest.TestCase): def setUp(self): self.employee_dicts = [ @@ -27,11 +28,12 @@ def _create_employees(self): for office in OFFICES: create_employee(office=office, username='{}-{}'.format('username', office)) + @pytest.mark.usefixtures('gae_testbed') def test_get_all_offices(self): self._create_employees() - self.assertEqual(OFFICES, set(get_all_offices())) + assert OFFICES == set(get_all_offices()) - @mock.patch('logic.office.yaml.safe_load', return_value=OFFICES) + @mock.patch('loveapp.logic.office.yaml.safe_load', return_value=OFFICES) def test_employee_parser_no_team_match(self, mock_offices): office_parser = OfficeParser() self.assertEqual( @@ -54,7 +56,7 @@ def test_employee_parser_no_team_match(self, mock_offices): ) mock_offices.assert_called_once() - @mock.patch('logic.office.yaml.safe_load', return_value=OFFICES) + @mock.patch('loveapp.logic.office.yaml.safe_load', return_value=OFFICES) def test_employee_parser_with_team_match(self, mock_offices): office_parser = OfficeParser(self.employee_dicts) for employee in self.employee_dicts: diff --git a/tests/logic/secret_test.py b/tests/logic/secret_test.py index 23a8425..37ca965 100644 --- a/tests/logic/secret_test.py +++ b/tests/logic/secret_test.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- import unittest +import pytest + from errors import NoSuchSecret -from logic.secret import get_secret +from loveapp.logic.secret import get_secret from testing.factories import create_secret +@pytest.mark.usefixtures('gae_testbed') class SecretTest(unittest.TestCase): - nosegae_datastore_v3 = True def test_existing_secret(self): create_secret('FOO', value='bar') diff --git a/tests/logic/subscription_test.py b/tests/logic/subscription_test.py index 8460add..692433e 100644 --- a/tests/logic/subscription_test.py +++ b/tests/logic/subscription_test.py @@ -1,23 +1,19 @@ # -*- coding: utf-8 -*- import mock -import unittest -import logic.subscription -from models import Subscription +import loveapp.logic.subscription +from loveapp.models import Subscription from testing.factories import create_employee from testing.factories import create_subscription -class SubscriptionTest(unittest.TestCase): - nosegae_datastore_v3 = True +@mock.patch('loveapp.models.subscription.Employee', autospec=True) +def test_delete_subscription(mock_model_employee, gae_testbed): + mock_model_employee.get_current_employee.return_value = create_employee() - @mock.patch('models.subscription.Employee', autospec=True) - def test_delete_subscription(self, mock_model_employee): - mock_model_employee.get_current_employee.return_value = create_employee() + subscription = create_subscription() + assert Subscription.get_by_id(subscription.key.id()) is not None - subscription = create_subscription() - self.assertIsNotNone(Subscription.get_by_id(subscription.key.id())) + loveapp.logic.subscription.delete_subscription(subscription.key.id()) - logic.subscription.delete_subscription(subscription.key.id()) - - self.assertIsNone(Subscription.get_by_id(subscription.key.id())) + assert Subscription.get_by_id(subscription.key.id()) is None diff --git a/tests/models/access_key_test.py b/tests/models/access_key_test.py index bbc89ac..5ea6b70 100644 --- a/tests/models/access_key_test.py +++ b/tests/models/access_key_test.py @@ -1,16 +1,10 @@ # -*- coding: utf-8 -*- -import unittest +from loveapp.models.access_key import AccessKey -from models.access_key import AccessKey +def test_create(gae_testbed): + key = AccessKey.create('description') -class AccessKeyTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True - - def test_create(self): - key = AccessKey.create('description') - - self.assertIsNotNone(key) - self.assertEqual('description', key.description) - self.assertIsNotNone(key.access_key) + assert key is not None + assert 'description' == key.description + assert key.access_key is not None diff --git a/tests/models/employee_test.py b/tests/models/employee_test.py index bc6b096..0f4de0f 100644 --- a/tests/models/employee_test.py +++ b/tests/models/employee_test.py @@ -1,76 +1,73 @@ # -*- coding: utf-8 -*- import mock -import unittest - +import pytest from google.appengine.api import users from errors import NoSuchEmployee -from models.employee import Employee +from loveapp.models.employee import Employee from testing.factories import create_employee -class EmployeeTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True - - @mock.patch('models.employee.config') - def test_create_from_dict(self, mock_config): - mock_config.DOMAIN = 'foo.io' - - employee_dict = dict( - username='john.d', - first_name='John', - last_name='Doe', - department='Accounting', - office='USA CA SF New Montgomery', - photos=[], - ) - employee = Employee.create_from_dict(employee_dict) - - self.assertIsNotNone(employee) - self.assertIsNotNone(employee.user) - self.assertEqual('john.d@foo.io', employee.user.email()) - - @mock.patch('models.employee.users.get_current_user') - def test_get_current_employee(self, mock_get_current_user): - employee = create_employee(username='john.d') - mock_get_current_user.return_value = employee.user - current_employee = Employee.get_current_employee() - - self.assertIsNotNone(current_employee) - self.assertEqual('john.d', current_employee.username) - - @mock.patch('models.employee.users.get_current_user') - def test_get_current_employee_raises(self, mock_get_current_user): - mock_get_current_user.return_value = users.User('foo@bar.io') - - with self.assertRaises(NoSuchEmployee): - Employee.get_current_employee() - - def test_full_name(self): - employee = create_employee(first_name='Foo', last_name='Bar') - self.assertEqual('Foo Bar', employee.full_name) - - @mock.patch('models.employee.config') - def test_gravatar_backup(self, mock_config): - mock_config.GRAVATAR = 'backup' - employee = create_employee(photo_url='') - self.assertEqual(employee.get_gravatar(), employee.get_photo_url()) - employee = create_employee(photo_url='http://example.com/example.jpg') - self.assertEqual(employee.photo_url, employee.get_photo_url()) - - @mock.patch('models.employee.config') - def test_gravatar_always(self, mock_config): - mock_config.GRAVATAR = 'always' - employee = create_employee(photo_url='') - self.assertEqual(employee.get_gravatar(), employee.get_photo_url()) - employee = create_employee(photo_url='http://example.com/example.jpg') - self.assertEqual(employee.get_gravatar(), employee.get_photo_url()) - - @mock.patch('models.employee.config') - def test_gravatar_disabled(self, mock_config): - mock_config.GRAVATAR = 'disabled' - employee = create_employee(photo_url='') - self.assertEqual(employee.photo_url, employee.get_photo_url()) - employee = create_employee(photo_url='http://example.com/example.jpg') - self.assertEqual(employee.photo_url, employee.get_photo_url()) +def test_create_from_dict(mock_config, gae_testbed): + mock_config.DOMAIN = 'foo.io' + + employee_dict = dict( + username='john.d', + first_name='John', + last_name='Doe', + department='Accounting', + office='USA CA SF New Montgomery', + photos=[], + ) + employee = Employee.create_from_dict(employee_dict) + + assert employee is not None + assert employee.user is not None + assert 'john.d@foo.io' == employee.user.email() + + +@mock.patch('loveapp.models.employee.users.get_current_user') +def test_get_current_employee(mock_get_current_user, gae_testbed): + employee = create_employee(username='john.d') + mock_get_current_user.return_value = employee.user + current_employee = Employee.get_current_employee() + + assert current_employee is not None + assert 'john.d' == current_employee.username + + +@mock.patch('loveapp.models.employee.users.get_current_user') +def test_get_current_employee_raises(mock_get_current_user, gae_testbed): + mock_get_current_user.return_value = users.User('foo@bar.io') + + with pytest.raises(NoSuchEmployee): + Employee.get_current_employee() + + +def test_full_name(gae_testbed): + employee = create_employee(first_name='Foo', last_name='Bar') + assert 'Foo Bar' == employee.full_name + + +def test_gravatar_backup(mock_config, gae_testbed): + mock_config.GRAVATAR = 'backup' + employee = create_employee(photo_url='') + assert employee.get_gravatar() == employee.get_photo_url() + employee = create_employee(photo_url='http://example.com/example.jpg') + assert employee.photo_url == employee.get_photo_url() + + +def test_gravatar_always(mock_config, gae_testbed): + mock_config.GRAVATAR = 'always' + employee = create_employee(photo_url='') + assert employee.get_gravatar() == employee.get_photo_url() + employee = create_employee(photo_url='http://example.com/example.jpg') + assert employee.get_gravatar() == employee.get_photo_url() + + +def test_gravatar_disabled(mock_config, gae_testbed): + mock_config.GRAVATAR = 'disabled' + employee = create_employee(photo_url='') + assert employee.photo_url == employee.get_photo_url() + employee = create_employee(photo_url='http://example.com/example.jpg') + assert employee.photo_url == employee.get_photo_url() diff --git a/tests/models/love_link_test.py b/tests/models/love_link_test.py index 3144789..775ca48 100644 --- a/tests/models/love_link_test.py +++ b/tests/models/love_link_test.py @@ -1,17 +1,12 @@ # -*- coding: utf-8 -*- import mock -import unittest from testing.factories import create_love_link -class LoveLinkTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True +@mock.patch('loveapp.models.love_link.config') +def test_url(mock_config, gae_testbed): + mock_config.APP_BASE_URL = 'http://foo.io/' - @mock.patch('models.love_link.config') - def test_url(self, mock_config): - mock_config.APP_BASE_URL = 'http://foo.io/' - - link = create_love_link(hash_key='lOvEr') - self.assertEqual('http://foo.io/l/lOvEr', link.url) + link = create_love_link(hash_key='lOvEr') + assert 'http://foo.io/l/lOvEr' == link.url diff --git a/tests/util/company_values_test.py b/tests/util/company_values_test.py index 935d8e3..1f4260b 100644 --- a/tests/util/company_values_test.py +++ b/tests/util/company_values_test.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -import mock import unittest -from config import CompanyValue -import util.company_values +import mock + +import loveapp.util.company_values +from loveapp.config import CompanyValue class CompanyValuesUtilTest(unittest.TestCase): @@ -11,32 +12,32 @@ class CompanyValuesUtilTest(unittest.TestCase): COMPANY_VALUE_ONE = CompanyValue('FAKE_VALUE_ONE', 'Fake Value 1', ['fakevalue1']) COMPANY_VALUE_TWO = CompanyValue('FAKE_VALUE_TWO', 'Fake Value 2', ['fakevalue2', 'otherhashtag']) - @mock.patch('util.company_values.config') + @mock.patch('loveapp.util.company_values.config') def test_get_company_value(self, mock_config): mock_config.COMPANY_VALUES = [ self.COMPANY_VALUE_ONE, self.COMPANY_VALUE_TWO ] - company_value = util.company_values.get_company_value(self.COMPANY_VALUE_ONE.id) + company_value = loveapp.util.company_values.get_company_value(self.COMPANY_VALUE_ONE.id) self.assertEqual(company_value, self.COMPANY_VALUE_ONE) - probably_None = util.company_values.get_company_value('fake_value') + probably_None = loveapp.util.company_values.get_company_value('fake_value') self.assertEqual(None, probably_None) - @mock.patch('util.company_values.config') + @mock.patch('loveapp.util.company_values.config') def test_supported_hashtags(self, mock_config): mock_config.COMPANY_VALUES = [] - supported_hashtags = util.company_values.supported_hashtags() + supported_hashtags = loveapp.util.company_values.supported_hashtags() self.assertEqual(supported_hashtags, []) mock_config.COMPANY_VALUES = [self.COMPANY_VALUE_TWO] - supported_hashtags = util.company_values.supported_hashtags() + supported_hashtags = loveapp.util.company_values.supported_hashtags() self.assertEqual(supported_hashtags, ['#fakevalue2', '#otherhashtag']) - @mock.patch('util.company_values.config') + @mock.patch('loveapp.util.company_values.config') def test_get_hashtag_value_mapping(self, mock_config): mock_config.COMPANY_VALUES = [] - hashtag_mapping = util.company_values.get_hashtag_value_mapping() + hashtag_mapping = loveapp.util.company_values.get_hashtag_value_mapping() self.assertEqual({}, hashtag_mapping) mock_config.COMPANY_VALUES = [ @@ -49,14 +50,14 @@ def test_get_hashtag_value_mapping(self, mock_config): '#' + self.COMPANY_VALUE_TWO.hashtags[0]: self.COMPANY_VALUE_TWO.id, '#' + self.COMPANY_VALUE_TWO.hashtags[1]: self.COMPANY_VALUE_TWO.id, } - hashtag_mapping = util.company_values.get_hashtag_value_mapping() + hashtag_mapping = loveapp.util.company_values.get_hashtag_value_mapping() self.assertEqual(expected_mapping, hashtag_mapping) - @mock.patch('util.company_values.config') + @mock.patch('loveapp.util.company_values.config') def test_linkify_company_values(self, mock_config): mock_config.COMPANY_VALUES = [] love_text = u'who wants to #liveForever? 😭' - linkified_value = util.company_values.linkify_company_values(love_text) + linkified_value = loveapp.util.company_values.linkify_company_values(love_text) # should be the same, because there's no hashtags. self.assertEqual(love_text, linkified_value) @@ -64,11 +65,11 @@ def test_linkify_company_values(self, mock_config): CompanyValue('FREDDIE', 'Mercury', ('liveForever',)) ] love_text = 'who wants to #liveForever?' - linkified_value = util.company_values.linkify_company_values(love_text) + linkified_value = loveapp.util.company_values.linkify_company_values(love_text) # there should be a link in here now self.assertIn('href', linkified_value) - @mock.patch('util.company_values.config') + @mock.patch('loveapp.util.company_values.config') def test_values_matching_prefix(self, mock_config): mock_config.COMPANY_VALUES = [ CompanyValue('TEST', 'test', ['abseil', 'absolute', 'abrasion']) @@ -76,10 +77,10 @@ def test_values_matching_prefix(self, mock_config): self.assertEqual( set(['#abseil', '#absolute', '#abrasion']), - set(util.company_values.values_matching_prefix('#a')) + set(loveapp.util.company_values.values_matching_prefix('#a')) ) self.assertEqual( set(['#abseil', '#absolute']), - set(util.company_values.values_matching_prefix('#abs')) + set(loveapp.util.company_values.values_matching_prefix('#abs')) ) diff --git a/tests/util/email_test.py b/tests/util/email_test.py index f825f15..cf5a16e 100644 --- a/tests/util/email_test.py +++ b/tests/util/email_test.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import unittest -import util.email +import loveapp.util.email class GetNameAndEmailTest(unittest.TestCase): @@ -9,12 +9,12 @@ class GetNameAndEmailTest(unittest.TestCase): def test_bare_email(self): email_string = 'darwin@example.com' - email, name = util.email.get_name_and_email(email_string) + email, name = loveapp.util.email.get_name_and_email(email_string) self.assertEqual(email, email_string) self.assertIsNone(name) def test_name_and_email(self): email_string = 'Darwin Stoppelman ' - email, name = util.email.get_name_and_email(email_string) + email, name = loveapp.util.email.get_name_and_email(email_string) self.assertEqual(email, 'darwin@example.com') self.assertEqual(name, 'Darwin Stoppelman') diff --git a/tests/util/recipient_test.py b/tests/util/recipient_test.py index f6effae..bd6ffcf 100644 --- a/tests/util/recipient_test.py +++ b/tests/util/recipient_test.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import unittest -from util.recipient import sanitize_recipients +from loveapp.util.recipient import sanitize_recipients class SanitizeRecipientsTest(unittest.TestCase): diff --git a/tests/views/api_test.py b/tests/views/api_test.py index 86a0e48..abf4083 100644 --- a/tests/views/api_test.py +++ b/tests/views/api_test.py @@ -1,175 +1,160 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function - -import unittest +from __future__ import print_function +from __future__ import unicode_literals import mock +import pytest -import logic.employee -import logic.love -from models import AccessKey +import loveapp.logic.employee +import loveapp.logic.love +from loveapp.models import AccessKey +from loveapp.views.api import LOVE_FAILED_STATUS_CODE from testing.factories import create_alias_with_employee_username from testing.factories import create_employee -from testing.util import YelpLoveTestCase -from webtest.app import AppError -class _ApiKeyRequiredTestCase(YelpLoveTestCase): - nosegae_datastore_v3 = True - nosegae_memcache = True - nosegae_taskqueue = True - successful_response_code = 200 +@pytest.fixture +def api_key(gae_testbed): + return AccessKey.create('autocomplete key').access_key - @classmethod - def setUpClass(cls): - if cls is _ApiKeyRequiredTestCase: - raise unittest.SkipTest('_ApiKeyRequiredTestCase is a base class') - super(_ApiKeyRequiredTestCase, cls).setUpClass() + +class _ApiKeyRequiredTestCase(): + successful_response_code = 200 def do_request(self, api_key): raise NotImplementedError('Implement this method with behavior which' ' requires an API key and returns the response') - def test_with_api_key(self): - api_key = AccessKey.create('test key').access_key - response = self.do_request(api_key) - self.assertEqual(response.status_int, self.successful_response_code) + def test_with_api_key(self, gae_testbed, client, api_key): + response = self.do_request(client, api_key) + assert response.status_code == self.successful_response_code - def test_without_api_key(self): + def test_without_api_key(self, gae_testbed, client): bad_api_key = AccessKey.generate_uuid() - with self.assertRaises(AppError) as caught: - self.do_request(bad_api_key) + response = self.do_request(client, bad_api_key) + assert response.status == '401 UNAUTHORIZED' - self.assert_(caught.exception.message.startswith('Bad response: 401'), - 'Expected request without valid API key to return 401') - -class AutocompleteTest(_ApiKeyRequiredTestCase): - nosegae_memcache = True - nosegae_datastore_v3 = True - nosegae_search = True - - def setUp(self): - super(AutocompleteTest, self).setUp() +class TestAutocomplete(_ApiKeyRequiredTestCase): + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='alex') create_employee(username='bob') create_employee(username='carol') - with mock.patch('logic.employee.memory_usage', autospec=True): - logic.employee.rebuild_index() - self.api_key = AccessKey.create('autocomplete key').access_key - - def test_autocomplete(self): - self._test_autocomplete('a', ['alice', 'alex']) - self._test_autocomplete('b', ['bob']) - self._test_autocomplete('c', ['carol']) - self._test_autocomplete('stupidprefix', []) - self._test_autocomplete('', []) - - def _test_autocomplete(self, prefix, expected_values, api_key=None): - if api_key is None: - api_key = self.api_key - response = self.app.get('/api/autocomplete', {'term': prefix, 'api_key': api_key}) - received_values = set(item['value'] for item in response.json) - self.assertEqual(set(expected_values), received_values) - return response - - def do_request(self, api_key): - return self._test_autocomplete('test', [], api_key) + with mock.patch('loveapp.logic.employee.memory_usage', autospec=True): + loveapp.logic.employee.rebuild_index() + + def do_request(self, client, api_key): + return client.get( + 'api/autocomplete', + query_string={'term': ''}, + data={'api_key': api_key} + ) + @pytest.mark.parametrize('prefix, expected_values', [ + ('a', ['alice', 'alex']), + ('b', ['bob']), + ('c', ['carol']), + ('', []), + ('stupidprefix', []), + ]) + def test_autocomplete(gae_testbed, client, api_key, prefix, expected_values): + api_key = AccessKey.create('autocomplete key').access_key + response = client.get('/api/autocomplete', query_string={'term': prefix}, data={'api_key': api_key}) + received_values = set(item['value'] for item in response.json) + assert set(expected_values) == received_values -class GetLoveTest(_ApiKeyRequiredTestCase): - def setUp(self): - super(GetLoveTest, self).setUp() +class TestGetLove(_ApiKeyRequiredTestCase): + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='bob') - logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') - def do_request(self, api_key): + def do_request(self, client, api_key): query_params = { 'sender': 'alice', 'recipient': 'bob', - 'limit': 1, - 'api_key': api_key, + 'limit': 1 } - response = self.app.get('/api/love', query_params) + return client.get( + '/api/love', + query_string=query_params, + data={'api_key': api_key} + ) + + def test_get_love(self, gae_testbed, client, api_key): + with mock.patch('loveapp.logic.event.add_event'): + loveapp.logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') + response = self.do_request(client, api_key) response_data = response.json - self.assertEqual(len(response_data), 1) - self.assertEqual(response_data[0]['sender'], 'alice') - self.assertEqual(response_data[0]['recipient'], 'bob') - return response + assert len(response_data) == 1 + assert response_data[0]['sender'] == 'alice' + assert response_data[0]['recipient'] == 'bob' -class SendLoveTest(_ApiKeyRequiredTestCase): +class TestSendLove(_ApiKeyRequiredTestCase): successful_response_code = 201 - def setUp(self): - super(SendLoveTest, self).setUp() + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='bob') - def do_request(self, api_key): + def do_request(self, client, api_key): form_values = { 'sender': 'alice', 'recipient': 'bob', 'message': 'Care Bear Stare!', 'api_key': api_key, } - response = self.app.post('/api/love', form_values) - self.assertTrue('Love sent to bob! Share:' in response.body) + with mock.patch('loveapp.logic.event.add_event'): + response = client.post('/api/love', data=form_values) return response + def test_send_love(self, gae_testbed, client, api_key): + response = self.do_request(client, api_key) + assert 'Love sent to bob! Share:' in response.data.decode() -class SendLoveFailTest(YelpLoveTestCase): - nosegae_datastore_v3 = True - nosegae_memcache = True - nosegae_taskqueue = True - - def setUp(self): - self.api_key = AccessKey.create('test key').access_key - create_employee(username='bob') + def test_send_loves_with_alias_and_username_for_same_user(self, gae_testbed, client, api_key): create_alias_with_employee_username(name='bobby', username='bob') - create_employee(username='alice') - - def test_send_loves_with_alias_and_username_for_same_user(self): form_values = { 'sender': 'alice', 'recipient': 'bob,bobby', 'message': 'Alias', - 'api_key': self.api_key, + 'api_key': api_key, } - with self.assertRaises(AppError) as caught: - self.app.post('/api/love', form_values) - - self.assert_( - caught.exception.message.startswith('Bad response: 418'), - 'Expected request to return 418', - ) - self.assertIn('send love to a user multiple times', caught.exception.message) - + response = client.post('/api/love', data=form_values) + assert response.status_code == LOVE_FAILED_STATUS_CODE + assert 'send love to a user multiple times' in response.data.decode() -class GetLeaderboardTest(_ApiKeyRequiredTestCase): - def setUp(self): - super(GetLeaderboardTest, self).setUp() +class TestGetLeaderboard(_ApiKeyRequiredTestCase): + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='bob') - logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') + with mock.patch('loveapp.logic.event.add_event'): + loveapp.logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') - def do_request(self, api_key): + def do_request(self, client, api_key): query_params = { - 'api_key': api_key, 'department': 'Engineering', } - response = self.app.get('/api/leaderboard', query_params) - response_data = response.json + return client.get( + '/api/leaderboard', + query_string=query_params, + data={'api_key': api_key} + ) + + def test_get_leaderboard(self, gae_testbed, client, api_key): + response_data = self.do_request(client, api_key).json top_loved = response_data.get('top_loved') top_lover = response_data.get('top_lover') - self.assertEqual(len(response_data), 2) - self.assertEqual(len(top_loved), 1) - self.assertEqual(len(top_lover), 1) - self.assertEqual(top_loved[0].get('username'), 'bob') - self.assertEqual(top_lover[0].get('username'), 'alice') - return response + assert len(response_data) == 2 + assert len(top_loved) == 1 + assert len(top_lover) == 1 + assert top_loved[0].get('username') == 'bob' + assert top_lover[0].get('username') == 'alice' diff --git a/tests/views/web_test.py b/tests/views/web_test.py index ef601c4..e9fe00d 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import mock +import pytest +from bs4 import BeautifulSoup -from webtest.app import AppError - -from config import CompanyValue -import logic +import loveapp.logic +from loveapp.config import CompanyValue from testing.factories import create_alias_with_employee_username from testing.factories import create_employee from testing.factories import create_love @@ -15,415 +15,316 @@ from testing.util import YelpLoveTestCase -class LoggedOutTest(YelpLoveTestCase): +@pytest.mark.usefixtures('gae_testbed') +class TestLoggedOut(YelpLoveTestCase): """ Testing access to pages when no user is logged in. """ - nosegae_user = True - nosegae_user_kwargs = dict(USER_EMAIL='') - - def test_homepage(self): - self.assertRequiresLogin(self.app.get('/')) - - def test_me(self): - self.assertRequiresLogin(self.app.get('/me')) - - def test_explore(self): - self.assertRequiresLogin(self.app.get('/explore')) - - def test_user_shortcut(self): - self.assertRequiresLogin(self.app.get('/johnd')) - - def test_leaderboard(self): - self.assertRequiresLogin(self.app.get('/leaderboard')) - - def test_keys(self): - self.assertRequiresLogin(self.app.get('/keys')) - - def test_autocomplete(self): - self.assertRequiresLogin(self.app.get('/user/autocomplete')) - - def test_values_autocomplete(self): - self.assertRequiresLogin(self.app.get('/values/autocomplete')) - - def test_single_company_value(self): - self.assertRequiresLogin(self.app.get('/value/test')) - def test_company_values(self): - self.assertRequiresLogin(self.app.get('/values')) - - def test_sent(self): - self.assertRequiresLogin(self.app.get('/sent')) - - def test_create_key(self): - csrf_token = self.addCsrfTokenToSession() + @pytest.mark.parametrize('url', [ + '/', + '/me', + '/explore', + '/johnd', + '/leaderboard', + '/keys', + '/user/autocomplete', + '/values/autocomplete', + '/value/test', + '/values', + '/sent', + '/subscriptions', + '/aliases', + '/employees', + '/employees/import' + ]) + def test_get_requires_login(self, client, url): + self.assertRequiresLogin(client.get(url)) + + @pytest.mark.parametrize('url, data', [ + ('/keys/create', dict(description='My Api Key')), + ('/love', dict(recipients='jenny', message='Love')), + ('/subscriptions/create', dict( + request_url='http://localhost.com/foo', + event='lovesent', + active='true', + secret='mysecret') + ), + ('/subscriptions/1/delete', dict()), + ('/aliases', dict(alias='johnny', username='john')), + ('/aliases/1/delete', dict()), + ('/employees/import', dict()) + ]) + def test_post_requires_login(self, client, url, data): + data['_csrf_token'] = self.addCsrfTokenToSession(client) self.assertRequiresLogin( - self.app.post( - '/keys/create', - dict( - description='My API Key', - _csrf_token=csrf_token, - ), - ), - ) - - def test_post_love(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/love', - dict( - recipients='jenny', - message='Love', - _csrf_token=csrf_token, - ), - ), - ) - - def test_subscriptions(self): - self.assertRequiresLogin(self.app.get('/subscriptions')) - - def test_create_subscription(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/subscriptions/create', - dict( - request_url='http://localhost.com/foo', - event='lovesent', - active='true', - secret='mysecret', - _csrf_token=csrf_token, - ), - ), - ) - - def test_delete_subscription(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/subscriptions/1/delete', - dict(_csrf_token=csrf_token), - ), - ) - - def test_listing_aliases(self): - self.assertRequiresLogin(self.app.get('/aliases')) - - def test_create_alias(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/aliases', - dict( - alias='johnny', - username='john', - _csrf_token=csrf_token, - ), - ), - ) - - def test_delete_alias(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/aliases/1/delete', - dict(_csrf_token=csrf_token), - ), - ) - - def test_employees(self): - self.assertRequiresLogin(self.app.get('/employees')) - - def test_import_employees_form(self): - self.assertRequiresLogin(self.app.get('/employees/import')) - - def test_import_employees(self): - csrf_token = self.addCsrfTokenToSession() - self.assertRequiresLogin( - self.app.post( - '/employees/import', - dict(_csrf_token=csrf_token), + client.post( + url, + data=data ) ) -class AdminResourcesTest(LoggedInUserBaseTest): +class TestAdminResources(LoggedInUserBaseTest): # Managing API Keys - def test_keys(self): + def test_keys(self, client): self.assertRequiresAdmin( - self.app.get('/keys', expect_errors=True), + client.get('/keys') ) - def test_create_key(self): - csrf_token = self.addCsrfTokenToSession() + def test_create_key(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/keys/create', - dict( + data=dict( description='My API Key', - _csrf_token=csrf_token, - ), - expect_errors=True, + _csrf_token=csrf_token + ) ), ) # Managing Webhook Subscriptions - def test_subscriptions(self): + def test_subscriptions(self, client): self.assertRequiresAdmin( - self.app.get('/subscriptions', expect_errors=True), + client.get('/subscriptions') ) - def test_create_subscription(self): - csrf_token = self.addCsrfTokenToSession() + def test_create_subscription(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/subscriptions/create', - dict( + data=dict( request_url='http://localhost.com/foo', event='lovesent', active='true', secret='mysecret', - _csrf_token=csrf_token, - ), - expect_errors=True, + _csrf_token=csrf_token + ) ), ) - def test_delete_subscription(self): - csrf_token = self.addCsrfTokenToSession() + def test_delete_subscription(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/subscriptions/1/delete', - dict(_csrf_token=csrf_token), - expect_errors=True, + data=dict(_csrf_token=csrf_token) ), ) # Managing Aliases - def test_aliases(self): + def test_aliases(self, client): self.assertRequiresAdmin( - self.app.get('/aliases', expect_errors=True), + client.get('/aliases'), ) - def test_create_alias(self): - csrf_token = self.addCsrfTokenToSession() + def test_create_alias(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/aliases', - dict( + data=dict( alias='johnny', username='john', - _csrf_token=csrf_token, - ), - expect_errors=True, - ), + _csrf_token=csrf_token + ) + ) ) - def test_delete_alias(self): - csrf_token = self.addCsrfTokenToSession() + def test_delete_alias(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/aliases/1/delete', - dict(_csrf_token=csrf_token), - expect_errors=True, - ), + data=dict(_csrf_token=csrf_token) + ) ) - def test_employees(self): + def test_employees(self, client): self.assertRequiresAdmin( - self.app.get('/employees', expect_errors=True) + client.get('/employees') ) - def test_import_employees_form(self): + def test_import_employees_form(self, client): self.assertRequiresAdmin( - self.app.get('/employees/import', expect_errors=True) + client.get('/employees/import') ) - def test_import_employees(self): - csrf_token = self.addCsrfTokenToSession() + def test_import_employees(self, client): + csrf_token = self.addCsrfTokenToSession(client) self.assertRequiresAdmin( - self.app.post( + client.post( '/employees/import', - dict(_csrf_token=csrf_token), - expect_errors=True, + data=dict(_csrf_token=csrf_token) ) ) -class HomepageTest(LoggedInUserBaseTest): +class TestHomepage(LoggedInUserBaseTest): """ Testing the homepage """ - def test_index(self): - response = self.app.get('/') + def test_index(self, client, recorded_templates): + response = client.get('/') + assert response.status_code == 200 + template, response_context = recorded_templates[0] + assert 'home.html' in template.name - self.assertEqual(response.status_int, 200) - self.assertIn('home.html', response.template) + assert response_context['current_time'] is not None + assert response_context['current_user'] == self.logged_in_employee + assert response_context['recipients'] is None + self.assertHasCsrf(response, 'send-love-form', response_context['session']) - self.assertIsNotNone(response.context['current_time']) - self.assertEqual(response.context['current_user'], self.current_user) - self.assertIsNone(response.context['recipients']) - self.assertHasCsrf(response.forms['send-love-form'], response.session) + def test_index_with_recipient_and_message(self, client, recorded_templates): + response = client.get('/', query_string=dict(recipients='janedoe', message='hi')) + assert response.status_code == 200 - def test_index_with_recipient_and_message(self): - response = self.app.get('/', dict(recipients='janedoe', message='hi')) + template, response_context = recorded_templates[0] + soup = BeautifulSoup(response.data, 'html.parser') + send_love_form = soup.find('form', class_='send-love-form') + assert response_context['recipients'] == 'janedoe' - self.assertEqual(response.context['recipients'], 'janedoe') - self.assertEqual( - response.forms['send-love-form'].get('recipients').value, - 'janedoe', - ) - self.assertEqual( - response.forms['send-love-form'].get('message').value, - 'hi', - ) - self.assertHasCsrf(response.forms['send-love-form'], response.session) + assert send_love_form.find('input', {'name': 'recipients'}).get('value') == 'janedoe' + assert send_love_form.textarea.text == 'hi' + self.assertHasCsrf(response, 'send-love-form', response_context['session']) -class SentTest(LoggedInUserBaseTest): +class TestSent(LoggedInUserBaseTest): """ Testing the sent page """ - def setUp(self): - super(SentTest, self).setUp() - self.recipient = create_employee(username='janedoe') - - def tearDown(self): - self.recipient.key.delete() - super(SentTest, self).tearDown() + def test_missing_args_is_redirect(self, client): + response = client.get('/sent') + assert response.status_code == 302 - def test_missing_args_is_redirect(self): - response = self.app.get('/sent') - - self.assertEqual(response.status_int, 302) - - @mock.patch('views.web.config') - def test_sent_with_args(self, mock_config): + @mock.patch('loveapp.config') + def test_sent_with_args(self, mock_config, client, recorded_templates): mock_config.APP_BASE_URL = 'http://foo.io/' + mock_config.DOMAIN = 'example.com' - response = self.app.get('/sent', dict(recipients='janedoe', message='hi', link_id='cn23sx')) - self.assertIsNotNone(response.context['current_time']) - self.assertEqual(response.context['current_user'], self.current_user) - self.assertIsNotNone(response.context['loved']) - self.assertEqual(response.context['url'], 'http://foo.io/l/cn23sx') + create_employee('janedoe') + client.get('/sent', query_string=dict(recipients='janedoe', message='hi', link_id='cn23sx')) + _, response_context = recorded_templates[0] + assert response_context['current_time'] is not None + response_context['current_user'] == self.logged_in_employee + assert response_context['loved'] is not None + response_context['url'] == 'http://foo.io/l/cn23sx' -class LoveLinkTest(LoggedInUserBaseTest): + +class TestLoveLink(LoggedInUserBaseTest): """ Testing the sent page """ - def setUp(self): - super(LoveLinkTest, self).setUp() - self.recipient = create_employee(username='janedoe') - self.link = create_love_link('lOvEr', 'i love you!', 'janedoe') - - def tearDown(self): - self.recipient.key.delete() - super(LoveLinkTest, self).tearDown() + def test_bad_hash(self, client): + response = client.get('/l/badId') + assert response.status_code == 302 - def test_bad_hash(self): - response = self.app.get('/l/badId') + def test_good_hash(self, client, recorded_templates): + create_employee(username='janedoe') + create_love_link('lOvEr', 'i love you!', 'janedoe') + client.get('/l/lOvEr') - self.assertEqual(response.status_int, 302) + _, response_context = recorded_templates[0] - def test_good_hash(self): - response = self.app.get('/l/lOvEr') - self.assertIsNotNone(response.context['current_time']) - self.assertEqual(response.context['current_user'], self.current_user) - self.assertIsNotNone(response.context['loved']) - self.assertEqual(response.context['recipients'], 'janedoe') - self.assertEqual(response.context['message'], 'i love you!') - self.assertEqual(response.context['link_id'], 'lOvEr') + assert response_context['current_time'] is not None + assert response_context['current_user'] == self.logged_in_employee + assert response_context['loved'] is not None + assert response_context['recipients'] == 'janedoe' + assert response_context['message'] == 'i love you!' + assert response_context['link_id'] == 'lOvEr' -class SendLoveTest(LoggedInUserBaseTest): +class TestSendLove(LoggedInUserBaseTest): - def setUp(self): - super(SendLoveTest, self).setUp() - self.recipient = create_employee(username='jenny') + @pytest.fixture + def jenny(self): + jenny = create_employee(username='jenny') + yield jenny + jenny.key.delete() - def tearDown(self): - self.recipient.key.delete() - super(SendLoveTest, self).tearDown() + @mock.patch('loveapp.logic.love.send_loves', autospec=True) + def test_send_love_without_csrf(self, mock_send_loves, client, jenny): + response = client.post('/love', data={'recipients': 'jenny', 'message': 'Love'}, ) - @mock.patch('logic.love.send_loves', autospec=True) - def test_send_love_without_csrf(self, mock_send_loves): - response = self.app.post('/love', {'recipients': 'jenny', 'message': 'Love'}, expect_errors=True) + assert response.status_code == 403 + assert mock_send_loves.called is False - self.assertEqual(response.status_int, 403) - self.assertFalse(mock_send_loves.called) + @mock.patch('loveapp.logic.love.send_loves', autospec=True) + def test_send_love(self, mock_send_loves, client, jenny): + csrf_token = self.addCsrfTokenToSession(client) + response = client.post('/love', data={'recipients': 'jenny', 'message': 'Love', '_csrf_token': csrf_token}) - @mock.patch('logic.love.send_loves', autospec=True) - def test_send_love(self, mock_send_loves): - csrf_token = self.addCsrfTokenToSession() - response = self.app.post('/love', {'recipients': 'jenny', 'message': 'Love', '_csrf_token': csrf_token}) - - self.assertEqual(response.status_int, 302) + assert response.status_code == 302 mock_send_loves.assert_called_once_with(set([u'jenny']), u'Love', secret=False) -class MeTest(LoggedInUserBaseTest): +class TestMe(LoggedInUserBaseTest): """ Testing /me """ - def test_me(self): - response = self.app.get('/me') + def test_me(self, client, recorded_templates): + response = client.get('/me') + template, response_context = recorded_templates[0] - self.assertEqual(response.status_int, 200) - self.assertIn('me.html', response.template) + assert response.status_code == 200 + assert 'me.html' in template.name - self.assertIsNotNone(response.context['current_time']) - self.assertEqual(response.context['current_user'], self.current_user) - self.assertEqual(response.context['sent_loves'], []) - self.assertIn('Give and ye shall receive!', response.body) - self.assertEqual(response.context['received_loves'], []) - self.assertIn('You haven\'t sent any love yet.', response.body) + assert response_context['current_time'] is not None + assert response_context['current_user'] == self.logged_in_employee + assert response_context['sent_loves'] == [] + assert 'Give and ye shall receive!' in response.data.decode() + assert response_context['received_loves'] == [] + assert 'You haven\'t sent any love yet.' in response.data.decode() - def test_me_with_loves(self): + def test_me_with_loves(self, client, recorded_templates): dude = create_employee(username='dude') sent_love = create_love( - sender_key=self.current_user.key, + sender_key=self.logged_in_employee.key, recipient_key=dude.key, message='Well done.' ) received_love = create_love( sender_key=dude.key, - recipient_key=self.current_user.key, + recipient_key=self.logged_in_employee.key, message='Awesome work.' ) - response = self.app.get('/me') + response = client.get('/me') + _, response_context = recorded_templates[0] - self.assertEqual(response.context['sent_loves'], [sent_love]) - self.assertIn('Well done.', response.body) - self.assertEqual(response.context['received_loves'], [received_love]) - self.assertIn('Awesome work.', response.body) + assert response_context['sent_loves'] == [sent_love] + assert 'Well done.' in response.data.decode() + assert response_context['received_loves'] == [received_love] + assert 'Awesome work.' in response.data.decode() dude.key.delete() -class SubscriptionsTestCase(LoggedInAdminBaseTest): +class TestSubscriptions(LoggedInAdminBaseTest): """ Testing /subscriptions """ - def test_subscriptions(self): - response = self.app.get('/subscriptions') + def test_subscriptions(self, client, recorded_templates): + response = client.get('/subscriptions') + template, response_context = recorded_templates[0] - self.assertEqual(response.status_int, 200) - self.assertIn('subscriptions.html', response.template) + assert response.status_code == 200 + assert 'subscriptions.html' in template.name - @mock.patch('views.web.Subscription', autospec=True) - def test_create_subscription(self, mock_model_subscription): - csrf_token = self.addCsrfTokenToSession() - response = self.app.post( + @mock.patch('loveapp.views.web.Subscription', autospec=True) + def test_create_subscription(self, mock_model_subscription, client): + csrf_token = self.addCsrfTokenToSession(client) + response = client.post( '/subscriptions/create', - dict( + data=dict( request_url='http://example.org', event='lovesent', active='true', @@ -432,7 +333,7 @@ def test_create_subscription(self, mock_model_subscription): ) ) - self.assertEqual(response.status_int, 302) + assert response.status_code == 302 mock_model_subscription.create_from_dict.assert_called_once_with( dict( request_url='http://example.org', @@ -442,205 +343,201 @@ def test_create_subscription(self, mock_model_subscription): ) ) - @mock.patch('views.web.logic.subscription', autospec=True) - def test_deleting_alias(self, mock_logic_subscription): - csrf_token = self.addCsrfTokenToSession() + @mock.patch('loveapp.logic.subscription', autospec=True) + def test_deleting_alias(self, mock_logic_subscription, client): + csrf_token = self.addCsrfTokenToSession(client) subscription = create_subscription() - response = self.app.post( + response = client.post( '/subscriptions/{id}/delete'.format(id=subscription.key.id()), - dict(_csrf_token=csrf_token), + data=dict(_csrf_token=csrf_token), ) - self.assertEqual(response.status_int, 302) + assert response.status_code == 302 mock_logic_subscription.delete_subscription.assert_called_once_with(subscription.key.id()) -class AliasesTestCase(LoggedInAdminBaseTest): +class TestAliases(LoggedInAdminBaseTest): """ Testing /aliases """ - def test_listing_aliases(self): - response = self.app.get('/aliases') - - self.assertEqual(response.status_int, 200) - self.assertIn('aliases.html', response.template) - self.assertHasCsrf(response.forms['alias-form'], response.session) + def test_listing_aliases(self, client, recorded_templates): + response = client.get('/aliases') + assert response.status_code == 200 + template, response_context = recorded_templates[0] + assert 'aliases.html' in template.name + self.assertHasCsrf(response, 'alias-form', response_context['session']) - @mock.patch('views.web.logic.alias', autospec=True) - def test_saving_alias(self, mock_logic_alias): + @mock.patch('loveapp.logic.alias', autospec=True) + def test_saving_alias(self, mock_logic_alias, client): create_employee(username='dude') - csrf_token = self.addCsrfTokenToSession() + csrf_token = self.addCsrfTokenToSession(client) - response = self.app.post( + response = client.post( '/aliases', - {'alias': 'duden', 'username': 'dude', '_csrf_token': csrf_token}, + data={'alias': 'duden', 'username': 'dude', '_csrf_token': csrf_token}, ) - self.assertEqual(response.status_int, 302) + assert response.status_code == 302 mock_logic_alias.save_alias.assert_called_once_with( 'duden', 'dude', ) - def test_saving_alias_all_empty(self): - csrf_token = self.addCsrfTokenToSession() + def test_saving_alias_all_empty(self, client): + csrf_token = self.addCsrfTokenToSession(client) - response = self.app.post( + response = client.post( '/aliases', - {'alias': '', 'username': '', '_csrf_token': csrf_token}, + data={'alias': '', 'username': '', '_csrf_token': csrf_token} ) - self.assertEqual(response.status_int, 302) - self.assertIsNone(logic.alias.get_alias('foo')) + assert response.status_code == 302 + assert loveapp.logic.alias.get_alias('foo') is None - @mock.patch('views.web.logic.alias', autospec=True) - def test_deleting_alias(self, mock_logic_alias): + @mock.patch('loveapp.logic.alias', autospec=True) + def test_deleting_alias(self, mock_logic_alias, client): create_employee(username='man') - csrf_token = self.addCsrfTokenToSession() + csrf_token = self.addCsrfTokenToSession(client) alias = create_alias_with_employee_username(name='mano', username='man') - response = self.app.post( + response = client.post( '/aliases/{id}/delete'.format(id=alias.key.id()), - {'_csrf_token': csrf_token}, + data={'_csrf_token': csrf_token}, ) - self.assertEqual(response.status_int, 302) + assert response.status_code == 302 mock_logic_alias.delete_alias.assert_called_once_with(alias.key.id()) -class MeOrExploreTest(LoggedInUserBaseTest): +class TestMeOrExplore(LoggedInUserBaseTest): """ Testing redirect to /me or /explore?user=johnd """ - def test_no_such_employee(self): - with self.assertRaises(AppError) as caught: - self.app.get('/panda') - - self.assert_( - caught.exception.message.startswith('Bad response: 404'), - 'Expected request for unknown employee to return 404', - ) + def test_no_such_employee(self, client): + response = client.get('/panda') + assert response.status_code == 404 - def test_redirect_to_me(self): - response = self.app.get('/{username}'.format(username=self.current_user.username)) + def test_redirect_to_me(self, client): + response = client.get('/{username}'.format(username=self.logged_in_employee.username)) + assert response.status_code == 302 + assert '/me' in response.headers.get('location') - self.assertEqual(response.status_int, 302) - self.assertIn('/me', response.location) - - def test_redirect_to_explore(self): + def test_redirect_to_explore(self, client): create_employee(username='buddy') - response = self.app.get('/buddy') + response = client.get('/buddy') - self.assertEqual(response.status_int, 302) - self.assertIn('/explore?user=buddy', response.location) + assert response.status_code == 302 + assert '/explore?user=buddy' in response.headers.get('location') - def test_with_alias(self): + def test_with_alias(self, client): create_employee(username='buddy') create_alias_with_employee_username(name='buddyalias', username='buddy') - response = self.app.get('/buddyalias') + response = client.get('/buddyalias') - self.assertEqual(response.status_int, 302) - self.assertIn('/explore?user=buddy', response.location) + assert response.status_code == 302 + assert '/explore?user=buddy' in response.headers.get('location') -class LeaderboardTest(LoggedInUserBaseTest): +class TestLeaderboard(LoggedInUserBaseTest): """ Testing /leaderboard """ - def test_leaderboard(self): - response = self.app.get('/leaderboard') + def test_leaderboard(self, client, recorded_templates): + response = client.get('/leaderboard') + template, response_context = recorded_templates[0] - self.assertEqual(response.status_int, 200) - self.assertIn('leaderboard.html', response.template) - self.assertIsNotNone(response.context['top_loved']) - self.assertIsNotNone(response.context['top_lovers']) - self.assertIsNotNone(response.context['departments']) - self.assertIsNotNone(response.context['offices']) - self.assertIsNone(response.context['selected_dept']) - self.assertIsNotNone(response.context['selected_timespan']) - self.assertIsNone(response.context['selected_office']) + assert response.status_code == 200 + assert 'leaderboard.html' in template.name + assert response_context['top_loved'] is not None + assert response_context['top_lovers'] is not None + assert response_context['departments'] is not None + assert response_context['offices'] is not None + assert response_context['selected_dept'] is None + assert response_context['selected_timespan'] is not None + assert response_context['selected_office'] is None -class ExploreTest(LoggedInUserBaseTest): +class TestExplore(LoggedInUserBaseTest): """ Testing /explore """ - def test_explore(self): - response = self.app.get('/explore') + def test_explore(self, client, recorded_templates): + response = client.get('/explore') + template, response_context = recorded_templates[0] - self.assertEqual(response.status_int, 200) - self.assertIn('explore.html', response.template) - self.assertIsNotNone(response.context['current_time']) - self.assertIsNone(response.context['user']) + assert response.status_code == 200 + assert 'explore.html' in template.name + assert response_context['current_time'] is not None + assert response_context['user'] is None - def test_explore_with_user(self): + def test_explore_with_user(self, client, recorded_templates): create_employee(username='buddy') - response = self.app.get('/explore?user=buddy') + response = client.get('/explore?user=buddy') + template, response_context = recorded_templates[0] - self.assertEqual(response.status_int, 200) - self.assertIn('explore.html', response.template) - self.assertIsNotNone(response.context['current_time']) - self.assertEqual('buddy', response.context['user'].username) + assert response.status_code == 200 + assert 'explore.html' in template.name + assert response_context['current_time'] is not None + assert 'buddy' == response_context['user'].username - def test_explore_with_unkown_user(self): - response = self.app.get('/explore?user=noone') + def test_explore_with_unkown_user(self, client): + response = client.get('/explore?user=noone') - self.assertEqual(response.status_int, 302) - self.assertIn('/explore', response.location) + assert response.status_code == 302 + assert '/explore' in response.headers.get('location') -class AutocompleteTest(LoggedInUserBaseTest): - nosegae_memcache = True - nosegae_search = True +class TestAutocomplete(LoggedInUserBaseTest): - def setUp(self): - super(AutocompleteTest, self).setUp() + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='alex') create_employee(username='bob') create_employee(username='carol') - with mock.patch('logic.employee.memory_usage', autospec=True): - logic.employee.rebuild_index() - - def test_autocomplete(self): - self._test_autocomplete('a', ['alice', 'alex']) - self._test_autocomplete('b', ['bob']) - self._test_autocomplete('c', ['carol']) - self._test_autocomplete('stupidprefix', []) - self._test_autocomplete('', []) - - def _test_autocomplete(self, prefix, expected_values): - response = self.app.get('/user/autocomplete', {'term': prefix}) + with mock.patch('loveapp.logic.employee.memory_usage', autospec=True): + loveapp.logic.employee.rebuild_index() + + @pytest.mark.parametrize('prefix, expected_values', [ + ('a', ['alice', 'alex']), + ('b', ['bob']), + ('c', ['carol']), + ('stupidprefix', []), + ('', []) + + ]) + def test_autocomplete(self, client, prefix, expected_values): + response = client.get('/user/autocomplete', query_string={'term': prefix}) received_values = set(item['value'] for item in response.json) - self.assertEqual(set(expected_values), received_values) - + assert set(expected_values) == received_values -class ValuesAutocompleteTest(LoggedInUserBaseTest): - @mock.patch('util.company_values.config') - def test_autocomplete(self, mock_config): - mock_config.COMPANY_VALUES = [ - CompanyValue('AWESOME', 'awesome', ['awesome', 'awesometacular', 'superAwesome']), - ] - self._test_autocomplete('#aw', ['#awesome', '#awesometacular']) - self._test_autocomplete('#su', ['#superAwesome']) - self._test_autocomplete('#derp', []) +class TestValuesAutocomplete(LoggedInUserBaseTest): - def _test_autocomplete(self, prefix, expected_values): - response = self.app.get('/values/autocomplete', {'term': prefix}) - received_values = set(item for item in response.json) - self.assertEqual(set(expected_values), received_values) + @pytest.mark.parametrize('prefix, expected_values', [ + ('#aw', ['#awesome', '#awesometacular']), + ('#su', ['#superAwesome']), + ('#derp', []) + ]) + def test_autocomplete(self, client, prefix, expected_values): + with mock.patch('loveapp.util.company_values.config') as mock_config: + mock_config.COMPANY_VALUES = [ + CompanyValue('AWESOME', 'awesome', ['awesome', 'awesometacular', 'superAwesome']), + ] + response = client.get('/values/autocomplete', query_string={'term': prefix}) + received_values = set(item for item in response.json) + assert set(expected_values) == received_values -class ValuesTest(LoggedInUserBaseTest): - def setUp(self): - super(ValuesTest, self).setUp() +class TestValues(LoggedInUserBaseTest): + @pytest.fixture(autouse=True) + def create_loves(self, gae_testbed): receiver = create_employee(username='receiver') sender = create_employee(username='sender') @@ -679,57 +576,62 @@ def setUp(self): company_values=[] ) - @mock.patch('util.company_values.config') - @mock.patch('logic.love.config') - def test_single_value_page(self, mock_util_config, mock_logic_config): + @mock.patch('loveapp.util.company_values.config') + @mock.patch('loveapp.logic.love.config') + def test_single_value_page(self, mock_util_config, mock_logic_config, client, recorded_templates): mock_util_config.COMPANY_VALUES = mock_logic_config.COMPANY_VALUES = [ CompanyValue('AWESOME', 'awesome', ['awesome']), CompanyValue('COOL', 'cool', ['cool']) ] - response = self.app.get('/value/cool') - self.assertIn('really cool', response.body) - self.assertIn('really quite cool', response.body) + response = client.get('/value/cool') + template, response_context = recorded_templates[0] + assert 'really cool' in response.data.decode() + assert 'really quite cool' in response.data.decode() # check linkification of hashtags - self.assertIn('#cool', response.body) + assert '#cool' in response.data.decode() # check only relevant hashtags are linkified - self.assertIn('#notcool', response.body) - self.assertNotIn('#notcool', response.body) + assert '#notcool' in response.data.decode() + assert '#notcool' not in response.data.decode() - self.assertNotIn('jk really awesome', response.body) + assert 'jk really awesome' not in response.data.decode() - @mock.patch('util.company_values.config') - @mock.patch('logic.love.config') - def test_all_values_page(self, mock_util_config, mock_logic_config): + @mock.patch('loveapp.util.company_values.config') + @mock.patch('loveapp.logic.love.config') + def test_all_values_page(self, mock_util_config, mock_logic_config, client, recorded_templates): mock_util_config.COMPANY_VALUES = mock_logic_config.COMPANY_VALUES = [ CompanyValue('AWESOME', 'awesome', ['awesome']), CompanyValue('COOL', 'cool', ['cool']) ] - response = self.app.get('/values') - self.assertIn('really cool', response.body) - self.assertIn('jk really awesome', response.body) - self.assertNotIn('bogus', response.body) + response = client.get('/values') + template, response_context = recorded_templates[0] + + assert 'really cool' in response.data.decode() + assert 'jk really awesome' in response.data.decode() + assert 'bogus' not in response.data.decode() -class EmployeeTestCase(LoggedInAdminBaseTest): +class TestEmployee(LoggedInAdminBaseTest): """ Testing /employees """ - def test_employees(self): + def test_employees(self, client, recorded_templates): create_employee(username='buddy') - response = self.app.get('/employees') + response = client.get('/employees') - self.assertEqual(response.status_int, 200) - self.assertIn('employees.html', response.template) - self.assertIsNotNone(response.context['pagination_result']) + assert response.status_code == 200 + template, response_context = recorded_templates[0] + assert 'employees.html' in template.name + assert response_context['pagination_result'] is not None - def test_employees_import_form(self): - response = self.app.get('/employees/import') + def test_employees_import_form(self, client, recorded_templates): + response = client.get('/employees/import') - self.assertEqual(response.status_int, 200) - self.assertIn('import.html', response.template) - self.assertIsNotNone(response.context['import_file_exists']) + assert response.status_code == 200 + template, response_context = recorded_templates[0] + assert 'import.html' in template.name + assert response_context['import_file_exists'] is not None diff --git a/tox.ini b/tox.ini index ca5497c..5c96156 100644 --- a/tox.ini +++ b/tox.ini @@ -4,18 +4,14 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +envlist = py311 [testenv] deps = -rrequirements-dev.txt commands = pre-commit install -f --install-hooks pre-commit run --all-files - nosetests --with-gae \ - --gae-application='app.yaml,dispatch.yaml,worker.yaml' \ - --gae-lib-root={toxinidir}/google_appengine \ - --nologcapture \ - {posargs:tests} + pytest tests [flake8] max-line-length = 120 diff --git a/worker.yaml b/worker.yaml index 1815a00..5db07f4 100644 --- a/worker.yaml +++ b/worker.yaml @@ -1,25 +1,3 @@ service: worker -runtime: python27 -api_version: 1 -threadsafe: true - -handlers: -- url: /tasks/.* - script: main.app - login: admin - secure: always - -libraries: -- name: ssl - version: latest - -skip_files: -- ^(.*/)?#.*#$ -- ^(.*/)?.*/RCS/.*$ -- ^(.*/)?.*\.py[co]$ -- ^(.*/)?.*~$ -- ^(.*/)?\..*$ -- ^YelpLove.egg-info(/.*)?$ -- ^google_appengine(/.*)?$ -- ^tmp(/.*)?$ -- ^virtualenv_.*$ +runtime: python311 +app_engine_apis: true