From b644603fca0d02c0eb7118c0ac9b43ed90e17a20 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 12:43:20 -0700 Subject: [PATCH 01/17] Initial minor python3 compatability changes. Fix the employees.csv/employees.json example files to incorporate "office". update libraries to latest. --- .gitignore | 1 + .pre-commit-config.yaml | 7 +-- app.yaml | 8 ++-- import/employees.csv.example | 6 +-- import/employees.json.example | 2 + logic/__init__.py | 4 +- logic/employee.py | 22 ++++----- logic/love.py | 2 +- main.py | 10 ++++ requirements.txt | 23 +++++---- tox.ini | 2 +- util/company_values.py | 2 +- views/api.py | 15 +++--- views/common.py | 2 +- views/tasks.py | 21 +++++---- views/web.py | 88 ++++++++++++++++++----------------- worker.yaml | 6 +-- 17 files changed, 121 insertions(+), 100 deletions(-) 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..00e2193 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,7 +16,7 @@ - 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 @@ -23,7 +24,7 @@ - id: requirements-txt-fixer - id: trailing-whitespace - 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/app.yaml b/app.yaml index 540cae7..4d8b2a4 100644 --- a/app.yaml +++ b/app.yaml @@ -1,8 +1,10 @@ service: default -runtime: python27 +runtime: python311 api_version: 1 threadsafe: true +app_engine_apis: true + handlers: - url: /api/.* script: main.app @@ -39,10 +41,6 @@ handlers: builtins: - remote_api: on -libraries: -- name: ssl - version: latest - skip_files: - ^(.*/)?#.*#$ - ^(.*/)?.*/RCS/.*$ diff --git a/import/employees.csv.example b/import/employees.csv.example index df29a0f..78b1d4a 100644 --- a/import/employees.csv.example +++ b/import/employees.csv.example @@ -1,3 +1,3 @@ -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 +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/import/employees.json.example index 4f24fd8..b10ee71 100644 --- a/import/employees.json.example +++ b/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/logic/__init__.py index 6672ddf..19ed31d 100644 --- a/logic/__init__.py +++ b/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,7 +13,7 @@ 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): diff --git a/logic/employee.py b/logic/employee.py index 98b9f1b..26b877d 100644 --- a/logic/employee.py +++ b/logic/employee.py @@ -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/love.py b/logic/love.py index e57d2ad..0d04918 100644 --- a/logic/love.py +++ b/logic/love.py @@ -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/main.py b/main.py index 5c28a5b..974ab38 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # flake8: noqa + +from google.appengine.api import wrap_wsgi_app from flask import Flask from flask_themes2 import Themes @@ -9,8 +11,12 @@ from util.company_values import linkify_company_values from util.csrf import generate_csrf_token +import views + 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 @@ -18,6 +24,10 @@ 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) + Themes(app, app_identifier='yelplove') # if debug property is present, let's use it diff --git a/requirements.txt b/requirements.txt index 8eace35..c8271f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,15 @@ +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 +boto +Flask +Flask-Themes2 +ipdb +itsdangerous +Jinja2 +MarkupSafe +pytz +pyyaml +urllib3 +Werkzeug +wheel diff --git a/tox.ini b/tox.ini index ca5497c..e07f7d5 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +envlist = py311 [testenv] deps = -rrequirements-dev.txt diff --git a/util/company_values.py b/util/company_values.py index 85b3b23..d0ece10 100644 --- a/util/company_values.py +++ b/util/company_values.py @@ -37,7 +37,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/views/api.py b/views/api.py index 14ac2b3..335af5b 100644 --- a/views/api.py +++ b/views/api.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from flask import Blueprint from flask import make_response from flask import request @@ -8,7 +9,6 @@ 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 @@ -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/views/common.py index a9eb8e4..9a07af4 100644 --- a/views/common.py +++ b/views/common.py @@ -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/views/tasks.py index 90135ba..76a547b 100644 --- a/views/tasks.py +++ b/views/tasks.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from flask import Blueprint from flask import request from flask import Response from google.appengine.api import taskqueue @@ -9,13 +10,15 @@ import logic.love import logic.love_count import logic.love_link -from main import app from 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() # we need to rebuild the love count index as the departments may have changed. @@ -24,7 +27,7 @@ def load_employees_from_s3(): # 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() # we need to rebuild the love count index as the departments may have changed. @@ -33,7 +36,7 @@ def load_employees_from_csv(): # 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: @@ -45,13 +48,13 @@ def combine_employees(): 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() 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() @@ -59,20 +62,20 @@ def 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() 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.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() return Response(status=200) diff --git a/views/web.py b/views/web.py index 3aaf9da..287605a 100644 --- a/views/web.py +++ b/views/web.py @@ -6,6 +6,7 @@ from datetime import datetime from flask import abort +from flask import Blueprint from flask import flash from flask import redirect from flask import request @@ -25,11 +26,10 @@ 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 models.access_key import AccessKey from util.decorators import admin_required from util.decorators import csrf_protect from util.decorators import user_required @@ -43,8 +43,10 @@ from logic.office import get_all_offices from logic.department import get_all_departments +web_app = Blueprint('web_app', __name__) -@app.route('/', methods=['GET']) + +@web_app.route('/', methods=['GET']) @user_required def home(): link_id = request.args.get('link_id', None) @@ -61,7 +63,7 @@ def home(): ) -@app.route('/me', methods=['GET']) +@web_app.route('/me', methods=['GET']) @user_required def me(): current_employee = Employee.get_current_employee() @@ -78,7 +80,7 @@ def me(): ) -@app.route('/', methods=['GET']) +@web_app.route('/', methods=['GET']) @user_required def me_or_explore(user): current_employee = Employee.get_current_employee() @@ -90,9 +92,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,12 +111,12 @@ 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() @@ -132,7 +134,7 @@ 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: @@ -154,7 +156,7 @@ def company_values(): ) -@app.route('/l/', methods=['GET']) +@web_app.route('/l/', methods=['GET']) @user_required def love_link(link_id): try: @@ -179,10 +181,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,7 +205,7 @@ 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) @@ -217,7 +219,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 +243,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 +251,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 +269,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 +279,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 +287,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,20 +302,20 @@ 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_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) # actual recipients may have the sender stripped from the list @@ -321,10 +323,10 @@ def love(): 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,23 +334,23 @@ 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( @@ -358,7 +360,7 @@ def subscriptions(): ) -@app.route('/subscriptions/create', methods=['POST']) +@web_app.route('/subscriptions/create', methods=['POST']) @csrf_protect @admin_required def create_subscription(): @@ -375,19 +377,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) 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,7 +398,7 @@ def aliases(): ) -@app.route('/aliases', methods=['POST']) +@web_app.route('/aliases', methods=['POST']) @csrf_protect @admin_required def create_alias(): @@ -409,19 +411,19 @@ def create_alias(): except Exception as e: flash('Something went wrong: {}.'.format(e.message), '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) 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,7 +436,7 @@ 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()) @@ -444,9 +446,9 @@ def import_employees_form(): ) -@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/worker.yaml b/worker.yaml index 1815a00..04e15d1 100644 --- a/worker.yaml +++ b/worker.yaml @@ -1,7 +1,8 @@ service: worker -runtime: python27 +runtime: python311 api_version: 1 threadsafe: true +app_engine_apis: true handlers: - url: /tasks/.* @@ -9,9 +10,6 @@ handlers: login: admin secure: always -libraries: -- name: ssl - version: latest skip_files: - ^(.*/)?#.*#$ From 302db9b3c6a1fd6666060985b3243f022cba6354 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 13:01:59 -0700 Subject: [PATCH 02/17] fixed csrf --- util/csrf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/csrf.py b/util/csrf.py index add91d3..8a7d5a1 100644 --- a/util/csrf.py +++ b/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'] From b2f432f85658a703fe8df8e875f9d92024367a35 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 13:32:41 -0700 Subject: [PATCH 03/17] get leaderboard to work by unrolling the iterators --- logic/__init__.py | 2 +- logic/leaderboard.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/logic/__init__.py b/logic/__init__.py index 19ed31d..b0d0edf 100644 --- a/logic/__init__.py +++ b/logic/__init__.py @@ -17,7 +17,7 @@ def chunk(iterable, chunk_size): 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/leaderboard.py b/logic/leaderboard.py index 37cd4b8..3419c4f 100644 --- a/logic/leaderboard.py +++ b/logic/leaderboard.py @@ -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) From d7b3d7797417172c8f8ef43117e66ec747095928 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 13:38:04 -0700 Subject: [PATCH 04/17] add linkification of company values to the email template --- themes/default/templates/email.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/default/templates/email.html b/themes/default/templates/email.html index 1c40d77..d931c57 100644 --- a/themes/default/templates/email.html +++ b/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 %} From 63508f9764c2b30729c030b7b5fe7ed74d758ec3 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 17:16:39 -0700 Subject: [PATCH 05/17] restructuring of the project to be more flask-y --- README.md | 4 +- loveapp/__init__.py | 39 +++++++++ loveapp/import/employees.csv | 4 + .../import}/employees.csv.example | 0 .../import}/employees.json.example | 0 {logic => loveapp/logic}/__init__.py | 0 {logic => loveapp/logic}/alias.py | 4 +- {logic => loveapp/logic}/department.py | 2 +- {logic => loveapp/logic}/email.py | 6 +- {logic => loveapp/logic}/employee.py | 16 ++-- {logic => loveapp/logic}/event.py | 0 {logic => loveapp/logic}/leaderboard.py | 10 +-- {logic => loveapp/logic}/love.py | 34 ++++---- {logic => loveapp/logic}/love_count.py | 12 +-- {logic => loveapp/logic}/love_link.py | 8 +- .../logic}/notification_request.py | 0 {logic => loveapp/logic}/notifier/__init__.py | 6 +- .../logic}/notifier/lovesent_notifier.py | 10 +-- {logic => loveapp/logic}/office.py | 2 +- {logic => loveapp/logic}/secret.py | 2 +- {logic => loveapp/logic}/subscription.py | 2 +- {logic => loveapp/logic}/toggle.py | 6 +- {models => loveapp/models}/__init__.py | 0 {models => loveapp/models}/access_key.py | 0 {models => loveapp/models}/alias.py | 2 +- {models => loveapp/models}/employee.py | 2 +- {models => loveapp/models}/love.py | 2 +- {models => loveapp/models}/love_count.py | 2 +- {models => loveapp/models}/love_link.py | 0 {models => loveapp/models}/secret.py | 0 {models => loveapp/models}/subscription.py | 4 +- {models => loveapp/models}/toggle.py | 0 {static => loveapp/static}/robots.txt | 0 {themes => loveapp/themes}/default/info.json | 0 .../default/static/css/bootstrap.min.css | 0 .../themes}/default/static/css/style.css | 0 .../themes}/default/static/img/favicon.png | Bin .../static/img/glyphicons-halflings-white.png | Bin .../static/img/glyphicons-halflings.png | Bin .../themes}/default/static/img/logo.png | Bin .../themes}/default/static/img/rocket.png | Bin .../default/static/img/star_header_bg.png | Bin .../default/static/img/user_medium_square.png | Bin .../static/js/jquery-ui-1.10.4.custom.min.js | 0 .../themes}/default/static/js/linkify.js | 0 .../themes}/default/static/js/main.js | 0 .../themes}/default/templates/alias_form.html | 0 .../themes}/default/templates/aliases.html | 0 .../themes}/default/templates/email.html | 0 .../themes}/default/templates/employees.html | 0 .../themes}/default/templates/explore.html | 0 .../themes}/default/templates/home.html | 0 .../themes}/default/templates/import.html | 0 .../templates/import_employees_form.html | 0 .../themes}/default/templates/keys.html | 0 .../themes}/default/templates/layout.html | 0 .../default/templates/leaderboard.html | 0 .../themes}/default/templates/love_form.html | 0 .../themes}/default/templates/love_link.html | 0 .../themes}/default/templates/me.html | 0 .../default/templates/parts/avatar.html | 0 .../default/templates/parts/facetile.html | 0 .../default/templates/parts/flash.html | 0 .../default/templates/parts/love_message.html | 0 .../default/templates/parts/photobox.html | 0 .../themes}/default/templates/sent.html | 0 .../default/templates/subscription_form.html | 0 .../default/templates/subscriptions.html | 0 .../themes}/default/templates/values.html | 0 {util => loveapp/util}/__init__.py | 0 {util => loveapp/util}/auth.py | 0 {util => loveapp/util}/company_values.py | 0 {util => loveapp/util}/converter.py | 0 {util => loveapp/util}/csrf.py | 0 {util => loveapp/util}/decorators.py | 4 +- {util => loveapp/util}/email.py | 0 {util => loveapp/util}/pagination.py | 0 {util => loveapp/util}/recipient.py | 0 {util => loveapp/util}/render.py | 0 {views => loveapp/views}/__init__.py | 0 {views => loveapp/views}/api.py | 20 ++--- {views => loveapp/views}/common.py | 4 +- {views => loveapp/views}/tasks.py | 28 +++--- {views => loveapp/views}/web.py | 82 +++++++++--------- main.py | 40 +-------- requirements-dev.txt | 6 +- testing/factories/alias.py | 4 +- testing/factories/employee.py | 2 +- testing/factories/love.py | 2 +- testing/factories/love_link.py | 2 +- testing/factories/secret.py | 2 +- testing/factories/subscription.py | 2 +- tests/conftest.py | 13 +++ tests/logic/alias_test.py | 18 ++-- tests/logic/department_test.py | 2 +- tests/logic/email_test.py | 10 +-- tests/logic/love_link_test.py | 24 ++--- tests/logic/love_test.py | 34 ++++---- tests/logic/office_test.py | 6 +- tests/logic/secret_test.py | 2 +- tests/logic/subscription_test.py | 6 +- tests/models/access_key_test.py | 2 +- tests/models/employee_test.py | 2 +- tests/util/recipient_test.py | 2 +- tests/views/api_test.py | 12 +-- tests/views/web_test.py | 8 +- 106 files changed, 271 insertions(+), 247 deletions(-) create mode 100644 loveapp/__init__.py create mode 100644 loveapp/import/employees.csv rename {import => loveapp/import}/employees.csv.example (100%) rename {import => loveapp/import}/employees.json.example (100%) rename {logic => loveapp/logic}/__init__.py (100%) rename {logic => loveapp/logic}/alias.py (89%) rename {logic => loveapp/logic}/department.py (84%) rename {logic => loveapp/logic}/email.py (92%) rename {logic => loveapp/logic}/employee.py (96%) rename {logic => loveapp/logic}/event.py (100%) rename {logic => loveapp/logic}/leaderboard.py (82%) rename {logic => loveapp/logic}/love.py (90%) rename {logic => loveapp/logic}/love_count.py (90%) rename {logic => loveapp/logic}/love_link.py (91%) rename {logic => loveapp/logic}/notification_request.py (100%) rename {logic => loveapp/logic}/notifier/__init__.py (64%) rename {logic => loveapp/logic}/notifier/lovesent_notifier.py (83%) rename {logic => loveapp/logic}/office.py (98%) rename {logic => loveapp/logic}/secret.py (85%) rename {logic => loveapp/logic}/subscription.py (74%) rename {logic => loveapp/logic}/toggle.py (88%) rename {models => loveapp/models}/__init__.py (100%) rename {models => loveapp/models}/access_key.py (100%) rename {models => loveapp/models}/alias.py (86%) rename {models => loveapp/models}/employee.py (98%) rename {models => loveapp/models}/love.py (93%) rename {models => loveapp/models}/love_count.py (98%) rename {models => loveapp/models}/love_link.py (100%) rename {models => loveapp/models}/secret.py (100%) rename {models => loveapp/models}/subscription.py (92%) rename {models => loveapp/models}/toggle.py (100%) rename {static => loveapp/static}/robots.txt (100%) rename {themes => loveapp/themes}/default/info.json (100%) rename {themes => loveapp/themes}/default/static/css/bootstrap.min.css (100%) rename {themes => loveapp/themes}/default/static/css/style.css (100%) rename {themes => loveapp/themes}/default/static/img/favicon.png (100%) rename {themes => loveapp/themes}/default/static/img/glyphicons-halflings-white.png (100%) rename {themes => loveapp/themes}/default/static/img/glyphicons-halflings.png (100%) rename {themes => loveapp/themes}/default/static/img/logo.png (100%) rename {themes => loveapp/themes}/default/static/img/rocket.png (100%) rename {themes => loveapp/themes}/default/static/img/star_header_bg.png (100%) rename {themes => loveapp/themes}/default/static/img/user_medium_square.png (100%) rename {themes => loveapp/themes}/default/static/js/jquery-ui-1.10.4.custom.min.js (100%) rename {themes => loveapp/themes}/default/static/js/linkify.js (100%) rename {themes => loveapp/themes}/default/static/js/main.js (100%) rename {themes => loveapp/themes}/default/templates/alias_form.html (100%) rename {themes => loveapp/themes}/default/templates/aliases.html (100%) rename {themes => loveapp/themes}/default/templates/email.html (100%) rename {themes => loveapp/themes}/default/templates/employees.html (100%) rename {themes => loveapp/themes}/default/templates/explore.html (100%) rename {themes => loveapp/themes}/default/templates/home.html (100%) rename {themes => loveapp/themes}/default/templates/import.html (100%) rename {themes => loveapp/themes}/default/templates/import_employees_form.html (100%) rename {themes => loveapp/themes}/default/templates/keys.html (100%) rename {themes => loveapp/themes}/default/templates/layout.html (100%) rename {themes => loveapp/themes}/default/templates/leaderboard.html (100%) rename {themes => loveapp/themes}/default/templates/love_form.html (100%) rename {themes => loveapp/themes}/default/templates/love_link.html (100%) rename {themes => loveapp/themes}/default/templates/me.html (100%) rename {themes => loveapp/themes}/default/templates/parts/avatar.html (100%) rename {themes => loveapp/themes}/default/templates/parts/facetile.html (100%) rename {themes => loveapp/themes}/default/templates/parts/flash.html (100%) rename {themes => loveapp/themes}/default/templates/parts/love_message.html (100%) rename {themes => loveapp/themes}/default/templates/parts/photobox.html (100%) rename {themes => loveapp/themes}/default/templates/sent.html (100%) rename {themes => loveapp/themes}/default/templates/subscription_form.html (100%) rename {themes => loveapp/themes}/default/templates/subscriptions.html (100%) rename {themes => loveapp/themes}/default/templates/values.html (100%) rename {util => loveapp/util}/__init__.py (100%) rename {util => loveapp/util}/auth.py (100%) rename {util => loveapp/util}/company_values.py (100%) rename {util => loveapp/util}/converter.py (100%) rename {util => loveapp/util}/csrf.py (100%) rename {util => loveapp/util}/decorators.py (93%) rename {util => loveapp/util}/email.py (100%) rename {util => loveapp/util}/pagination.py (100%) rename {util => loveapp/util}/recipient.py (100%) rename {util => loveapp/util}/render.py (100%) rename {views => loveapp/views}/__init__.py (100%) rename {views => loveapp/views}/api.py (88%) rename {views => loveapp/views}/common.py (84%) rename {views => loveapp/views}/tasks.py (76%) rename {views => loveapp/views}/web.py (84%) create mode 100644 tests/conftest.py diff --git a/README.md b/README.md index ee13275..35383c6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ using the [Secret](models/secret.py) model. Locally you can open up the [interactive console](http://localhost:8000/console) and execute the following code: ```python -from models import Secret +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() @@ -94,7 +94,7 @@ or the [Remote API](https://cloud.google.com/appengine/docs/python/tools/remotea 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/loveapp/__init__.py b/loveapp/__init__.py new file mode 100644 index 0000000..0f925f9 --- /dev/null +++ b/loveapp/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from google.appengine.api import wrap_wsgi_app +from flask import Flask +from flask_themes2 import Themes + +import config +from loveapp.util.auth import is_admin +from loveapp.util.converter import RegexConverter +from loveapp.util.company_values import linkify_company_values +from loveapp.util.csrf import generate_csrf_token + +from loveapp import views + + +def create_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) + + Themes(app, app_identifier='yelplove') + + # if debug property is present, let's use it + try: + app.debug = config.DEBUG + except AttributeError: + app.debug = False + + return app 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/import/employees.csv.example b/loveapp/import/employees.csv.example similarity index 100% rename from import/employees.csv.example rename to loveapp/import/employees.csv.example diff --git a/import/employees.json.example b/loveapp/import/employees.json.example similarity index 100% rename from import/employees.json.example rename to loveapp/import/employees.json.example diff --git a/logic/__init__.py b/loveapp/logic/__init__.py similarity index 100% rename from logic/__init__.py rename to loveapp/logic/__init__.py 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..d8bd705 100644 --- a/logic/alias.py +++ b/loveapp/logic/alias.py @@ -1,7 +1,7 @@ # -*- 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..91d1057 100644 --- a/logic/department.py +++ b/loveapp/logic/department.py @@ -1,6 +1,6 @@ # -*- 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 92% rename from logic/email.py rename to loveapp/logic/email.py index dff6492..5282d36 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 +from loveapp.util.email import get_name_and_email import config -import logic.secret +import loveapp.logic.secret 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 96% rename from logic/employee.py rename to loveapp/logic/employee.py index 26b877d..b06069a 100644 --- a/logic/employee.py +++ b/loveapp/logic/employee.py @@ -10,14 +10,14 @@ import 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.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 +from loveapp.logic.office import OfficeParser INDEX_NAME = 'employees' 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 82% rename from logic/leaderboard.py rename to loveapp/logic/leaderboard.py index 3419c4f..91ec942 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 +from loveapp.logic import TIMESPAN_LAST_WEEK +from loveapp.logic import to_the_future +from loveapp.logic import utc_week_limits +import loveapp.logic.love_count 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, diff --git a/logic/love.py b/loveapp/logic/love.py similarity index 90% rename from logic/love.py rename to loveapp/logic/love.py index 0d04918..bd13cd0 100644 --- a/logic/love.py +++ b/loveapp/logic/love.py @@ -3,18 +3,18 @@ from google.appengine.api import taskqueue import config -import logic.alias -import logic.email -import logic.event +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()}, ) 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..25cc5c7 100644 --- a/logic/love_link.py +++ b/loveapp/logic/love_link.py @@ -4,10 +4,10 @@ import random import string -import logic.alias +import loveapp.logic.alias from errors import NoSuchLoveLink -from models import LoveLink -from models import Employee +from loveapp.models import LoveLink +from loveapp.models import Employee from google.appengine.ext import ndb @@ -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 100% rename from logic/notification_request.py rename to loveapp/logic/notification_request.py 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..d6b8c7d 100644 --- a/logic/notifier/__init__.py +++ b/loveapp/logic/notifier/__init__.py @@ -1,12 +1,12 @@ # -*- 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..fd6b80d 100644 --- a/logic/notifier/lovesent_notifier.py +++ b/loveapp/logic/notifier/lovesent_notifier.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- from google.appengine.ext import ndb -import logic.event +import loveapp.logic.event -from logic.notification_request import NotificationRequest -from models.love import Love -from models.subscription import Subscription +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..3087fd0 100644 --- a/logic/office.py +++ b/loveapp/logic/office.py @@ -8,7 +8,7 @@ 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 100% rename from models/__init__.py rename to loveapp/models/__init__.py 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 98% rename from models/employee.py rename to loveapp/models/employee.py index bb0ec7a..ac4004d 100644 --- a/models/employee.py +++ b/loveapp/models/employee.py @@ -8,7 +8,7 @@ import config from errors import NoSuchEmployee -from util.pagination import Pagination +from loveapp.util.pagination import Pagination def memoized(func): 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 100% rename from models/love_link.py rename to loveapp/models/love_link.py 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 100% rename from themes/default/templates/email.html rename to loveapp/themes/default/templates/email.html 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 100% rename from util/__init__.py rename to loveapp/util/__init__.py 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 100% rename from util/company_values.py rename to loveapp/util/company_values.py 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 100% rename from util/csrf.py rename to loveapp/util/csrf.py 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 100% rename from util/render.py rename to loveapp/util/render.py diff --git a/views/__init__.py b/loveapp/views/__init__.py similarity index 100% rename from views/__init__.py rename to loveapp/views/__init__.py diff --git a/views/api.py b/loveapp/views/api.py similarity index 88% rename from views/api.py rename to loveapp/views/api.py index 335af5b..59d24b4 100644 --- a/views/api.py +++ b/loveapp/views/api.py @@ -4,16 +4,16 @@ 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 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.love import get_love +from loveapp.logic.love import send_loves +from loveapp.logic.love_link import create_love_link +from loveapp.logic.leaderboard import get_leaderboard_data +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 diff --git a/views/common.py b/loveapp/views/common.py similarity index 84% rename from views/common.py rename to loveapp/views/common.py index 9a07af4..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): diff --git a/views/tasks.py b/loveapp/views/tasks.py similarity index 76% rename from views/tasks.py rename to loveapp/views/tasks.py index 76a547b..1923924 100644 --- a/views/tasks.py +++ b/loveapp/views/tasks.py @@ -5,12 +5,12 @@ 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 models import Love +import loveapp.logic.employee +import loveapp.logic.notifier +import loveapp.logic.love +import loveapp.logic.love_count +import loveapp.logic.love_link +from loveapp.models import Love tasks_app = Blueprint('tasks_app', __name__) @@ -20,7 +20,7 @@ @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) @@ -29,7 +29,7 @@ def load_employees_from_s3(): # This task has a web UI to trigger it, so let's use 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) @@ -44,13 +44,13 @@ 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) @tasks_app.route('/tasks/index/rebuild', methods=['GET']) def rebuild_index(): - logic.employee.rebuild_index() + loveapp.logic.employee.rebuild_index() return Response(status=200) @@ -58,24 +58,24 @@ def rebuild_index(): 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) @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) @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) @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 84% rename from views/web.py rename to loveapp/views/web.py index 287605a..86231ee 100644 --- a/views/web.py +++ b/loveapp/views/web.py @@ -12,36 +12,36 @@ from flask import request from flask import url_for -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.logic.alias +import loveapp.logic.employee +import loveapp.logic.event +import loveapp.logic.love +import loveapp.logic.love_link +import loveapp.logic.love_count +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 models import Alias -from models import Employee -from models import Subscription -from models.access_key import AccessKey -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 +from loveapp.logic import TIMESPAN_THIS_WEEK +from loveapp.logic.love_link import create_love_link +from loveapp.logic.leaderboard import get_leaderboard_data +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.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 render_template +from loveapp.util.render import make_json_response +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.views import common +from loveapp.logic.office import get_all_offices +from loveapp.logic.department import get_all_departments web_app = Blueprint('web_app', __name__) @@ -68,8 +68,8 @@ def home(): 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', @@ -84,7 +84,7 @@ def me(): @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) @@ -120,7 +120,7 @@ def single_company_value(company_value_id): 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( @@ -140,7 +140,7 @@ 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() @@ -160,7 +160,7 @@ def company_values(): @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 @@ -207,8 +207,8 @@ def explore(): flash('Sorry, "{}" is not a valid user.'.format(username), 'error') 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', @@ -312,12 +312,12 @@ def love(): 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('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) @@ -356,7 +356,7 @@ def subscriptions(): return render_template( 'subscriptions.html', subscriptions=Subscription.query().fetch(), - events=logic.event.EVENTS, + events=loveapp.logic.event.EVENTS, ) @@ -384,7 +384,7 @@ def create_subscription(): @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('web_app.subscriptions')) @@ -403,7 +403,7 @@ def aliases(): @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(), ) @@ -418,7 +418,7 @@ def create_alias(): @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('web_app.aliases')) @@ -439,7 +439,7 @@ def employees(): @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, diff --git a/main.py b/main.py index 974ab38..5f490d3 100644 --- a/main.py +++ b/main.py @@ -1,40 +1,8 @@ # -*- coding: utf-8 -*- # flake8: noqa +from loveapp import create_app -from google.appengine.api import wrap_wsgi_app -from flask import Flask -from flask_themes2 import Themes +app = 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 - -import views - - -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) - -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..12804ad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements.txt Flask-WebTest==0.0.8 ipdb -mock==2.0.0 -NoseGAE==0.5.10 -pre-commit==0.13.3 +mock==4.0.3 +#NoseGAE==0.5.10 +pre-commit pyrsistent==0.16.1 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/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..42a157d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import pytest + +from loveapp import create_app + + +@pytest.fixture(autouse=True) +def app(): # noqa + + app = create_app() + + with app.app_context(): + yield app diff --git a/tests/logic/alias_test.py b/tests/logic/alias_test.py index 55e07db..743a9a8 100644 --- a/tests/logic/alias_test.py +++ b/tests/logic/alias_test.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import unittest -import logic.alias +import loveapp.logic.alias -from models import Alias +from loveapp.models import Alias from testing.factories import create_alias_with_employee_username from testing.factories import create_employee @@ -15,13 +15,13 @@ 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 +29,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..ae631c6 100644 --- a/tests/logic/department_test.py +++ b/tests/logic/department_test.py @@ -2,7 +2,7 @@ import unittest -from logic.department import get_all_departments +from loveapp.logic.department import get_all_departments from testing.factories import create_employee DEPARTMENTS = [ diff --git a/tests/logic/email_test.py b/tests/logic/email_test.py index c868c16..5ae0b75 100644 --- a/tests/logic/email_test.py +++ b/tests/logic/email_test.py @@ -2,7 +2,7 @@ import mock import unittest -import logic.email +import loveapp.logic.email class EmailTest(unittest.TestCase): @@ -19,8 +19,8 @@ class EmailTest(unittest.TestCase): 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 ) @@ -30,8 +30,8 @@ def test_send_email_appengine(self, mock_config, mock_backends): 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..6fa1865 100644 --- a/tests/logic/love_link_test.py +++ b/tests/logic/love_link_test.py @@ -2,8 +2,8 @@ import unittest import datetime -import logic.love -import logic.love_link +import loveapp.logic.love +import loveapp.logic.love_link from errors import NoSuchLoveLink from testing.factories import create_love_link @@ -20,34 +20,34 @@ def setUp(self): 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..4b6d4a1 100644 --- a/tests/logic/love_test.py +++ b/tests/logic/love_test.py @@ -3,7 +3,7 @@ import unittest from config import CompanyValue -import logic.love +import loveapp.logic.love from errors import TaintedLove from testing.factories import create_alias_with_employee_username from testing.factories import create_employee @@ -21,47 +21,47 @@ def setUp(self): self.message = 'hallo' def test_send_loves(self): - logic.love.send_loves( + 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( + 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 +69,22 @@ 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): 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,9 +93,9 @@ 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') @@ -105,8 +105,8 @@ def test_send_love_with_value_hashtag(self, mock_config): ] 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..43e6535 100644 --- a/tests/logic/office_test.py +++ b/tests/logic/office_test.py @@ -1,9 +1,9 @@ # -*- 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 +from loveapp.logic.office import REMOTE_OFFICE +from loveapp.logic.office import get_all_offices +from loveapp.logic.office import OfficeParser from testing.factories import create_employee OFFICES = { diff --git a/tests/logic/secret_test.py b/tests/logic/secret_test.py index 23a8425..7e2d1d7 100644 --- a/tests/logic/secret_test.py +++ b/tests/logic/secret_test.py @@ -2,7 +2,7 @@ import unittest from errors import NoSuchSecret -from logic.secret import get_secret +from loveapp.logic.secret import get_secret from testing.factories import create_secret diff --git a/tests/logic/subscription_test.py b/tests/logic/subscription_test.py index 8460add..1e2319a 100644 --- a/tests/logic/subscription_test.py +++ b/tests/logic/subscription_test.py @@ -2,8 +2,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 @@ -18,6 +18,6 @@ def test_delete_subscription(self, mock_model_employee): subscription = create_subscription() self.assertIsNotNone(Subscription.get_by_id(subscription.key.id())) - logic.subscription.delete_subscription(subscription.key.id()) + loveapp.logic.subscription.delete_subscription(subscription.key.id()) self.assertIsNone(Subscription.get_by_id(subscription.key.id())) diff --git a/tests/models/access_key_test.py b/tests/models/access_key_test.py index bbc89ac..a721724 100644 --- a/tests/models/access_key_test.py +++ b/tests/models/access_key_test.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import unittest -from models.access_key import AccessKey +from loveapp.models.access_key import AccessKey class AccessKeyTest(unittest.TestCase): diff --git a/tests/models/employee_test.py b/tests/models/employee_test.py index bc6b096..ff7849b 100644 --- a/tests/models/employee_test.py +++ b/tests/models/employee_test.py @@ -5,7 +5,7 @@ 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 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..c450964 100644 --- a/tests/views/api_test.py +++ b/tests/views/api_test.py @@ -5,9 +5,9 @@ import mock -import logic.employee -import logic.love -from models import AccessKey +import loveapp.logic.employee +import loveapp.logic.love +from loveapp.models import AccessKey from testing.factories import create_alias_with_employee_username from testing.factories import create_employee from testing.util import YelpLoveTestCase @@ -56,7 +56,7 @@ def setUp(self): create_employee(username='bob') create_employee(username='carol') with mock.patch('logic.employee.memory_usage', autospec=True): - logic.employee.rebuild_index() + loveapp.logic.employee.rebuild_index() self.api_key = AccessKey.create('autocomplete key').access_key def test_autocomplete(self): @@ -84,7 +84,7 @@ def setUp(self): super(GetLoveTest, self).setUp() create_employee(username='alice') create_employee(username='bob') - logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') + loveapp.logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') def do_request(self, api_key): query_params = { @@ -156,7 +156,7 @@ def setUp(self): super(GetLeaderboardTest, self).setUp() create_employee(username='alice') create_employee(username='bob') - logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') + loveapp.logic.love.send_loves(['bob', ], 'Care Bear Stare!', 'alice') def do_request(self, api_key): query_params = { diff --git a/tests/views/web_test.py b/tests/views/web_test.py index ef601c4..d8d3770 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -4,7 +4,7 @@ from webtest.app import AppError from config import CompanyValue -import logic +import loveapp.logic from testing.factories import create_alias_with_employee_username from testing.factories import create_employee from testing.factories import create_love @@ -492,7 +492,7 @@ def test_saving_alias_all_empty(self): ) self.assertEqual(response.status_int, 302) - self.assertIsNone(logic.alias.get_alias('foo')) + self.assertIsNone(loveapp.logic.alias.get_alias('foo')) @mock.patch('views.web.logic.alias', autospec=True) def test_deleting_alias(self, mock_logic_alias): @@ -603,8 +603,8 @@ def setUp(self): 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() + with mock.patch('loveapp.logic.employee.memory_usage', autospec=True): + loveapp.logic.employee.rebuild_index() def test_autocomplete(self): self._test_autocomplete('a', ['alice', 'alex']) From 9fb1185a69aeac881982f8acc7566d1343bb6c92 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 18:10:04 -0700 Subject: [PATCH 06/17] fix util tests --- loveapp/util/company_values.py | 4 ++-- tests/util/company_values_test.py | 32 +++++++++++++++---------------- tests/util/email_test.py | 6 +++--- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/loveapp/util/company_values.py b/loveapp/util/company_values.py index d0ece10..b04a363 100644 --- a/loveapp/util/company_values.py +++ b/loveapp/util/company_values.py @@ -20,10 +20,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(): diff --git a/tests/util/company_values_test.py b/tests/util/company_values_test.py index 935d8e3..22a0b26 100644 --- a/tests/util/company_values_test.py +++ b/tests/util/company_values_test.py @@ -3,7 +3,7 @@ import unittest from config import CompanyValue -import util.company_values +import loveapp.util.company_values class CompanyValuesUtilTest(unittest.TestCase): @@ -11,32 +11,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 +49,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 +64,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 +76,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') From 2e4dfe2c0169712870036acd404950256e2df037 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 27 Apr 2024 21:21:36 -0700 Subject: [PATCH 07/17] fix lovelink models test --- testing/factories/love_link.py | 1 - tests/models/love_link_test.py | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/testing/factories/love_link.py b/testing/factories/love_link.py index f9219f7..a5866ff 100644 --- a/testing/factories/love_link.py +++ b/testing/factories/love_link.py @@ -13,6 +13,5 @@ def create_love_link( message=message, recipient_list=recipient_list, ) - new_love_link.put() return new_love_link diff --git a/tests/models/love_link_test.py b/tests/models/love_link_test.py index 3144789..67e9e9d 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): + 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 From 1e9e8d5d18b55233786419969136a63e28353123 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sun, 28 Apr 2024 11:29:07 -0700 Subject: [PATCH 08/17] all tests except views are working --- loveapp/__init__.py | 2 +- .../config-example.py | 0 loveapp/logic/email.py | 2 +- loveapp/logic/employee.py | 2 +- loveapp/logic/love.py | 2 +- loveapp/logic/notification_request.py | 2 +- loveapp/models/employee.py | 13 +- loveapp/models/love_link.py | 2 +- loveapp/util/company_values.py | 2 +- loveapp/util/render.py | 2 +- loveapp/views/web.py | 2 +- tests/conftest.py | 24 +++- tests/logic/love_test.py | 2 +- tests/models/access_key_test.py | 15 +- tests/models/employee_test.py | 130 +++++++++--------- tests/util/company_values_test.py | 2 +- tests/views/web_test.py | 2 +- 17 files changed, 111 insertions(+), 95 deletions(-) rename config-example.py => loveapp/config-example.py (100%) diff --git a/loveapp/__init__.py b/loveapp/__init__.py index 0f925f9..5157488 100644 --- a/loveapp/__init__.py +++ b/loveapp/__init__.py @@ -4,7 +4,7 @@ from flask import Flask from flask_themes2 import Themes -import config +import loveapp.config as config from loveapp.util.auth import is_admin from loveapp.util.converter import RegexConverter from loveapp.util.company_values import linkify_company_values 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/logic/email.py b/loveapp/logic/email.py index 5282d36..5219600 100644 --- a/loveapp/logic/email.py +++ b/loveapp/logic/email.py @@ -2,7 +2,7 @@ from google.appengine.api.mail import EmailMessage from loveapp.util.email import get_name_and_email -import config +import loveapp.config as config import loveapp.logic.secret if config.EMAIL_BACKEND == 'sendgrid': diff --git a/loveapp/logic/employee.py b/loveapp/logic/employee.py index b06069a..a227f1d 100644 --- a/loveapp/logic/employee.py +++ b/loveapp/logic/employee.py @@ -8,7 +8,7 @@ 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 loveapp.logic import chunk from loveapp.logic.secret import get_secret diff --git a/loveapp/logic/love.py b/loveapp/logic/love.py index bd13cd0..5cd25e4 100644 --- a/loveapp/logic/love.py +++ b/loveapp/logic/love.py @@ -2,7 +2,7 @@ from datetime import datetime from google.appengine.api import taskqueue -import config +import loveapp.config as config import loveapp.logic.alias import loveapp.logic.email import loveapp.logic.event diff --git a/loveapp/logic/notification_request.py b/loveapp/logic/notification_request.py index 6611304..4320f72 100644 --- a/loveapp/logic/notification_request.py +++ b/loveapp/logic/notification_request.py @@ -5,7 +5,7 @@ import logging import urllib3 -import config +import loveapp.config as config CONTENT_TYPE_JSON = 'application/json' diff --git a/loveapp/models/employee.py b/loveapp/models/employee.py index ac4004d..8e44ff5 100644 --- a/loveapp/models/employee.py +++ b/loveapp/models/employee.py @@ -6,7 +6,7 @@ from google.appengine.ext import ndb from google.appengine.api import users -import config +import loveapp.config from errors import NoSuchEmployee from loveapp.util.pagination import Pagination @@ -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/loveapp/models/love_link.py b/loveapp/models/love_link.py index 5f558fe..072ac08 100644 --- a/loveapp/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/loveapp/util/company_values.py b/loveapp/util/company_values.py index b04a363..0577d71 100644 --- a/loveapp/util/company_values.py +++ b/loveapp/util/company_values.py @@ -3,7 +3,7 @@ import markupsafe import re -import config +import loveapp.config as config def get_company_value(value_id): diff --git a/loveapp/util/render.py b/loveapp/util/render.py index 6211cc6..9ad1858 100644 --- a/loveapp/util/render.py +++ b/loveapp/util/render.py @@ -4,7 +4,7 @@ from flask_themes2 import render_theme_template from flask.helpers import make_response -import config +import loveapp.config as config def get_current_theme(): diff --git a/loveapp/views/web.py b/loveapp/views/web.py index 86231ee..2286432 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import os.path -import config +import loveapp.config as config from datetime import datetime diff --git a/tests/conftest.py b/tests/conftest.py index 42a157d..ffcce23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,33 @@ # -*- coding: utf-8 -*- +import mock import pytest +from google.appengine.ext import testbed from loveapp import create_app -@pytest.fixture(autouse=True) +@pytest.fixture def app(): # noqa - app = create_app() with app.app_context(): yield 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() + + yield + + tb.deactivate() diff --git a/tests/logic/love_test.py b/tests/logic/love_test.py index 4b6d4a1..fac4c43 100644 --- a/tests/logic/love_test.py +++ b/tests/logic/love_test.py @@ -2,7 +2,7 @@ import mock import unittest -from config import CompanyValue +from loveapp.config import CompanyValue import loveapp.logic.love from errors import TaintedLove from testing.factories import create_alias_with_employee_username diff --git a/tests/models/access_key_test.py b/tests/models/access_key_test.py index a721724..adec119 100644 --- a/tests/models/access_key_test.py +++ b/tests/models/access_key_test.py @@ -1,16 +1,11 @@ # -*- coding: utf-8 -*- -import unittest from loveapp.models.access_key import AccessKey -class AccessKeyTest(unittest.TestCase): - # enable the datastore stub - nosegae_datastore_v3 = True +def test_create(gae_testbed): + key = AccessKey.create('description') - 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 ff7849b..71714d1 100644 --- a/tests/models/employee_test.py +++ b/tests/models/employee_test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import mock -import unittest +import pytest from google.appengine.api import users @@ -9,68 +9,66 @@ 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/util/company_values_test.py b/tests/util/company_values_test.py index 22a0b26..d516ddf 100644 --- a/tests/util/company_values_test.py +++ b/tests/util/company_values_test.py @@ -2,7 +2,7 @@ import mock import unittest -from config import CompanyValue +from loveapp.config import CompanyValue import loveapp.util.company_values diff --git a/tests/views/web_test.py b/tests/views/web_test.py index d8d3770..2b1bd7c 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -3,7 +3,7 @@ from webtest.app import AppError -from config import CompanyValue +from loveapp.config import CompanyValue import loveapp.logic from testing.factories import create_alias_with_employee_username from testing.factories import create_employee From 172632d4ffedb0661fa83185d2583e26833fb07a Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sun, 28 Apr 2024 18:20:21 -0700 Subject: [PATCH 09/17] make api_test work --- loveapp/__init__.py | 16 +++- requirements-dev.txt | 1 + testing/util.py | 18 +--- tests/conftest.py | 33 ++++++- tests/views/api_test.py | 189 ++++++++++++++++++---------------------- tox.ini | 6 +- 6 files changed, 137 insertions(+), 126 deletions(-) diff --git a/loveapp/__init__.py b/loveapp/__init__.py index 5157488..48071ca 100644 --- a/loveapp/__init__.py +++ b/loveapp/__init__.py @@ -2,7 +2,11 @@ from google.appengine.api import wrap_wsgi_app from flask import Flask +from flask import Blueprint +from flask import current_app +import flask_themes2 from flask_themes2 import Themes +from flask_themes2 import ThemeTemplateLoader import loveapp.config as config from loveapp.util.auth import is_admin @@ -13,7 +17,9 @@ from loveapp import views -def create_app(): +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) @@ -28,7 +34,13 @@ def create_app(): app.register_blueprint(views.api.api_app) app.register_blueprint(views.tasks.tasks_app) - Themes(app, app_identifier='yelplove') + # 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: diff --git a/requirements-dev.txt b/requirements-dev.txt index 12804ad..c856fda 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,3 +8,4 @@ mock==4.0.3 #NoseGAE==0.5.10 pre-commit pyrsistent==0.16.1 +pytest==8.1.2 diff --git a/testing/util.py b/testing/util.py index 0ec4599..b697097 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,29 +1,15 @@ # -*- coding: utf-8 -*- import mock -import os import unittest +import pytest -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) - - +@pytest.mark.usefixtures('gae_testbed', 'app') class YelpLoveTestCase(unittest.TestCase): - app = get_test_app() - def assertRequiresLogin(self, response): self.assertEqual(response.status_int, 302) self.assert_( diff --git a/tests/conftest.py b/tests/conftest.py index ffcce23..db9bd7d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,32 @@ # -*- coding: utf-8 -*- +import os + import mock import pytest +from flask_themes2 import load_themes_from + from google.appengine.ext import testbed from loveapp import create_app @pytest.fixture def app(): # noqa - app = create_app() + # do we need this? for what? + def test_loader(app): + return load_themes_from(os.path.join(os.path.dirname(__file__), '../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 mock_config(): with mock.patch('loveapp.config') as mock_config: @@ -27,6 +40,24 @@ def gae_testbed(): tb.activate() tb.init_memcache_stub() tb.init_datastore_v3_stub() + tb.init_search_stub() + tb.init_taskqueue_stub() + + yield + + tb.deactivate() + +# TODO cleanup - this is a nice test-speed optimisation, but not entirely necessary + + +@pytest.fixture(scope='class') +def gae_testbed_class_scope(): + tb = testbed.Testbed() + tb.activate() + tb.init_memcache_stub() + tb.init_datastore_v3_stub() + tb.init_search_stub() + tb.init_taskqueue_stub() yield diff --git a/tests/views/api_test.py b/tests/views/api_test.py index c450964..6ca99bc 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 +import pytest import mock 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_class_scope): + 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) - - self.assert_(caught.exception.message.startswith('Bad response: 401'), - 'Expected request without valid API key to return 401') + response = self.do_request(client, bad_api_key) + assert response.status == '401 UNAUTHORIZED' -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(scope='class', autouse=True) + def create_employees(self, gae_testbed_class_scope): 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): + with mock.patch('loveapp.logic.employee.memory_usage', autospec=True): loveapp.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) + 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_class_scope, 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') - 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 = { '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_class_scope, 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_class_scope, 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_class_scope, 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') - loveapp.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_class_scope, 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/tox.ini b/tox.ini index e07f7d5..5c96156 100644 --- a/tox.ini +++ b/tox.ini @@ -11,11 +11,7 @@ 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 From a791aae1f106bf57da645201e3a39a58b0f8f5b9 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Mon, 29 Apr 2024 19:20:27 -0700 Subject: [PATCH 10/17] web_test works! --- loveapp/views/web.py | 2 +- requirements.txt | 1 + testing/factories/love_link.py | 1 + testing/util.py | 74 ++-- tests/conftest.py | 36 +- tests/models/love_link_test.py | 2 +- tests/views/api_test.py | 16 +- tests/views/web_test.py | 752 ++++++++++++++------------------- 8 files changed, 387 insertions(+), 497 deletions(-) diff --git a/loveapp/views/web.py b/loveapp/views/web.py index 2286432..c0be244 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -409,7 +409,7 @@ def create_alias(): ) 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('web_app.aliases')) diff --git a/requirements.txt b/requirements.txt index c8271f2..69d390e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ 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. +beautifulsoup4 boto Flask Flask-Themes2 diff --git a/testing/factories/love_link.py b/testing/factories/love_link.py index a5866ff..f9219f7 100644 --- a/testing/factories/love_link.py +++ b/testing/factories/love_link.py @@ -13,5 +13,6 @@ def create_love_link( message=message, recipient_list=recipient_list, ) + new_love_link.put() return new_love_link diff --git a/testing/util.py b/testing/util.py index b697097..c358302 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,71 +1,57 @@ # -*- coding: utf-8 -*- +from bs4 import BeautifulSoup import mock -import unittest import pytest - from testing.factories import create_employee -@pytest.mark.usefixtures('gae_testbed', 'app') -class YelpLoveTestCase(unittest.TestCase): +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) + 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) + 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 index db9bd7d..c95a8be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import pytest from flask_themes2 import load_themes_from +from flask import template_rendered from google.appengine.ext import testbed from loveapp import create_app @@ -14,7 +15,7 @@ def app(): # noqa # do we need this? for what? def test_loader(app): - return load_themes_from(os.path.join(os.path.dirname(__file__), '../themes/')) + return load_themes_from(os.path.join(os.path.dirname(__file__), '../loveapp/themes/')) app = create_app(theme_loaders=[test_loader]) with app.app_context(): @@ -27,6 +28,20 @@ def client(app): 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: @@ -42,23 +57,8 @@ def gae_testbed(): tb.init_datastore_v3_stub() tb.init_search_stub() tb.init_taskqueue_stub() + tb.init_user_stub() - yield - - tb.deactivate() - -# TODO cleanup - this is a nice test-speed optimisation, but not entirely necessary - - -@pytest.fixture(scope='class') -def gae_testbed_class_scope(): - tb = testbed.Testbed() - tb.activate() - tb.init_memcache_stub() - tb.init_datastore_v3_stub() - tb.init_search_stub() - tb.init_taskqueue_stub() - - yield + yield tb tb.deactivate() diff --git a/tests/models/love_link_test.py b/tests/models/love_link_test.py index 67e9e9d..775ca48 100644 --- a/tests/models/love_link_test.py +++ b/tests/models/love_link_test.py @@ -5,7 +5,7 @@ @mock.patch('loveapp.models.love_link.config') -def test_url(mock_config): +def test_url(mock_config, gae_testbed): mock_config.APP_BASE_URL = 'http://foo.io/' link = create_love_link(hash_key='lOvEr') diff --git a/tests/views/api_test.py b/tests/views/api_test.py index 6ca99bc..c14ade3 100644 --- a/tests/views/api_test.py +++ b/tests/views/api_test.py @@ -14,7 +14,7 @@ @pytest.fixture -def api_key(gae_testbed_class_scope): +def api_key(gae_testbed): return AccessKey.create('autocomplete key').access_key @@ -36,8 +36,8 @@ def test_without_api_key(self, gae_testbed, client): class TestAutocomplete(_ApiKeyRequiredTestCase): - @pytest.fixture(scope='class', autouse=True) - def create_employees(self, gae_testbed_class_scope): + @pytest.fixture(autouse=True) + def create_employees(self, gae_testbed): create_employee(username='alice') create_employee(username='alex') create_employee(username='bob') @@ -59,7 +59,7 @@ def do_request(self, client, api_key): ('', []), ('stupidprefix', []), ]) - def test_autocomplete(gae_testbed_class_scope, client, api_key, prefix, expected_values): + 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) @@ -84,7 +84,7 @@ def do_request(self, client, api_key): data={'api_key': api_key} ) - def test_get_love(self, gae_testbed_class_scope, client, 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) @@ -113,11 +113,11 @@ def do_request(self, client, api_key): response = client.post('/api/love', data=form_values) return response - def test_send_love(self, gae_testbed_class_scope, client, api_key): + 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() - def test_send_loves_with_alias_and_username_for_same_user(self, gae_testbed_class_scope, client, api_key): + 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') form_values = { 'sender': 'alice', @@ -149,7 +149,7 @@ def do_request(self, client, api_key): data={'api_key': api_key} ) - def test_get_leaderboard(self, gae_testbed_class_scope, client, 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') diff --git a/tests/views/web_test.py b/tests/views/web_test.py index 2b1bd7c..248b526 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import mock - -from webtest.app import AppError +import pytest +from bs4 import BeautifulSoup from loveapp.config import CompanyValue import loveapp.logic @@ -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(YelpLoveTestCase): # 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) + 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,163 +343,158 @@ 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(loveapp.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') @@ -606,41 +502,42 @@ def setUp(self): with mock.patch('loveapp.logic.employee.memory_usage', autospec=True): loveapp.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('', []) + @pytest.mark.parametrize('prefix, expected_values', [ + ('a', ['alice', 'alex']), + ('b', ['bob']), + ('c', ['carol']), + ('stupidprefix', []), + ('', []) - def _test_autocomplete(self, prefix, expected_values): - response = self.app.get('/user/autocomplete', {'term': prefix}) + ]) + 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 From 5a86990810487c3606f670a51ccdfecd87c79f59 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Mon, 29 Apr 2024 19:29:06 -0700 Subject: [PATCH 11/17] add python-commit-redordering --- .pre-commit-config.yaml | 5 +++++ loveapp/__init__.py | 12 +++++------ loveapp/logic/alias.py | 1 - loveapp/logic/department.py | 1 - loveapp/logic/email.py | 2 +- loveapp/logic/employee.py | 4 ++-- loveapp/logic/leaderboard.py | 2 +- loveapp/logic/love.py | 2 +- loveapp/logic/love_link.py | 6 +++--- loveapp/logic/notification_request.py | 1 + loveapp/logic/notifier/__init__.py | 1 - loveapp/logic/notifier/lovesent_notifier.py | 1 - loveapp/logic/office.py | 3 ++- loveapp/models/__init__.py | 1 - loveapp/models/employee.py | 4 ++-- loveapp/util/__init__.py | 1 - loveapp/util/company_values.py | 3 ++- loveapp/util/render.py | 2 +- loveapp/views/__init__.py | 1 - loveapp/views/api.py | 2 +- loveapp/views/tasks.py | 2 +- loveapp/views/web.py | 22 ++++++++++----------- setup.py | 1 - testing/util.py | 2 +- tests/conftest.py | 5 ++--- tests/logic/alias_test.py | 1 - tests/logic/department_test.py | 1 - tests/logic/email_test.py | 3 ++- tests/logic/love_link_test.py | 5 ++--- tests/logic/love_test.py | 5 +++-- tests/logic/office_test.py | 4 +++- tests/logic/subscription_test.py | 3 ++- tests/models/access_key_test.py | 1 - tests/models/employee_test.py | 1 - tests/util/company_values_test.py | 5 +++-- tests/views/api_test.py | 6 +++--- tests/views/web_test.py | 2 +- 37 files changed, 60 insertions(+), 64 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00e2193..7d93486 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,6 +23,11 @@ repos: 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 rev: 0.9.1 hooks: diff --git a/loveapp/__init__.py b/loveapp/__init__.py index 48071ca..7637a02 100644 --- a/loveapp/__init__.py +++ b/loveapp/__init__.py @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- - -from google.appengine.api import wrap_wsgi_app -from flask import Flask +import flask_themes2 from flask import Blueprint from flask import current_app -import flask_themes2 +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.converter import RegexConverter from loveapp.util.company_values import linkify_company_values +from loveapp.util.converter import RegexConverter from loveapp.util.csrf import generate_csrf_token -from loveapp import views - def create_app(theme_loaders=()): if current_app: diff --git a/loveapp/logic/alias.py b/loveapp/logic/alias.py index d8bd705..1d066d2 100644 --- a/loveapp/logic/alias.py +++ b/loveapp/logic/alias.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from loveapp.models import Alias from loveapp.models import Employee diff --git a/loveapp/logic/department.py b/loveapp/logic/department.py index 91d1057..3271f8b 100644 --- a/loveapp/logic/department.py +++ b/loveapp/logic/department.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from loveapp.models.employee import Employee diff --git a/loveapp/logic/email.py b/loveapp/logic/email.py index 5219600..c78885c 100644 --- a/loveapp/logic/email.py +++ b/loveapp/logic/email.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from google.appengine.api.mail import EmailMessage -from loveapp.util.email import get_name_and_email 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 diff --git a/loveapp/logic/employee.py b/loveapp/logic/employee.py index a227f1d..4a499b7 100644 --- a/loveapp/logic/employee.py +++ b/loveapp/logic/employee.py @@ -1,8 +1,8 @@ # -*- 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 @@ -11,13 +11,13 @@ import loveapp.config as config from errors import NoSuchEmployee 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 -from loveapp.logic.office import OfficeParser INDEX_NAME = 'employees' diff --git a/loveapp/logic/leaderboard.py b/loveapp/logic/leaderboard.py index 91ec942..bbdcfb6 100644 --- a/loveapp/logic/leaderboard.py +++ b/loveapp/logic/leaderboard.py @@ -2,10 +2,10 @@ from datetime import datetime from datetime import timedelta +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 -import loveapp.logic.love_count def get_leaderboard_data(timespan, department, office=None): diff --git a/loveapp/logic/love.py b/loveapp/logic/love.py index 5cd25e4..b5db5a2 100644 --- a/loveapp/logic/love.py +++ b/loveapp/logic/love.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- from datetime import datetime + 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 loveapp.logic.toggle import get_toggle_state from loveapp.models import Employee diff --git a/loveapp/logic/love_link.py b/loveapp/logic/love_link.py index 25cc5c7..cdb5015 100644 --- a/loveapp/logic/love_link.py +++ b/loveapp/logic/love_link.py @@ -4,12 +4,12 @@ import random import string +from google.appengine.ext import ndb + import loveapp.logic.alias from errors import NoSuchLoveLink -from loveapp.models import LoveLink from loveapp.models import Employee - -from google.appengine.ext import ndb +from loveapp.models import LoveLink def get_love_link(hash_key): diff --git a/loveapp/logic/notification_request.py b/loveapp/logic/notification_request.py index 4320f72..4953ea6 100644 --- a/loveapp/logic/notification_request.py +++ b/loveapp/logic/notification_request.py @@ -3,6 +3,7 @@ import hmac import json import logging + import urllib3 import loveapp.config as config diff --git a/loveapp/logic/notifier/__init__.py b/loveapp/logic/notifier/__init__.py index d6b8c7d..274ed0f 100644 --- a/loveapp/logic/notifier/__init__.py +++ b/loveapp/logic/notifier/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import loveapp.logic.event - from errors import UnknownEvent from loveapp.logic.notifier.lovesent_notifier import LovesentNotifier diff --git a/loveapp/logic/notifier/lovesent_notifier.py b/loveapp/logic/notifier/lovesent_notifier.py index fd6b80d..a5978b2 100644 --- a/loveapp/logic/notifier/lovesent_notifier.py +++ b/loveapp/logic/notifier/lovesent_notifier.py @@ -2,7 +2,6 @@ from google.appengine.ext import ndb import loveapp.logic.event - from loveapp.logic.notification_request import NotificationRequest from loveapp.models.love import Love from loveapp.models.subscription import Subscription diff --git a/loveapp/logic/office.py b/loveapp/logic/office.py index 3087fd0..85a9e75 100644 --- a/loveapp/logic/office.py +++ b/loveapp/logic/office.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -import yaml from collections import Counter from collections import defaultdict +import yaml + REMOTE_OFFICE = 'Remote' OTHER_OFFICE = 'Other' diff --git a/loveapp/models/__init__.py b/loveapp/models/__init__.py index e9f68d9..a3367de 100644 --- a/loveapp/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/loveapp/models/employee.py b/loveapp/models/employee.py index 8e44ff5..9ba8fc5 100644 --- a/loveapp/models/employee.py +++ b/loveapp/models/employee.py @@ -1,10 +1,10 @@ # -*- 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 loveapp.config from errors import NoSuchEmployee diff --git a/loveapp/util/__init__.py b/loveapp/util/__init__.py index 421f1d6..987db09 100644 --- a/loveapp/util/__init__.py +++ b/loveapp/util/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- # flake8: noqa - from . import recipient diff --git a/loveapp/util/company_values.py b/loveapp/util/company_values.py index 0577d71..3bf7b5e 100644 --- a/loveapp/util/company_values.py +++ b/loveapp/util/company_values.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import itertools -import markupsafe import re +import markupsafe + import loveapp.config as config diff --git a/loveapp/util/render.py b/loveapp/util/render.py index 9ad1858..ec8cdc5 100644 --- a/loveapp/util/render.py +++ b/loveapp/util/render.py @@ -1,8 +1,8 @@ # -*- 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 loveapp.config as config diff --git a/loveapp/views/__init__.py b/loveapp/views/__init__.py index 26142f2..fb96c8f 100644 --- a/loveapp/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/loveapp/views/api.py b/loveapp/views/api.py index 59d24b4..703740c 100644 --- a/loveapp/views/api.py +++ b/loveapp/views/api.py @@ -5,10 +5,10 @@ from errors import TaintedLove 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.logic.leaderboard import get_leaderboard_data from loveapp.models import Employee from loveapp.util.decorators import api_key_required from loveapp.util.recipient import sanitize_recipients diff --git a/loveapp/views/tasks.py b/loveapp/views/tasks.py index 1923924..0af0329 100644 --- a/loveapp/views/tasks.py +++ b/loveapp/views/tasks.py @@ -6,10 +6,10 @@ from google.appengine.ext import ndb import loveapp.logic.employee -import loveapp.logic.notifier 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__) diff --git a/loveapp/views/web.py b/loveapp/views/web.py index c0be244..4ad1990 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- import os.path - -import loveapp.config as config - from datetime import datetime from flask import abort @@ -11,37 +8,38 @@ from flask import redirect from flask import request from flask import url_for +from google.appengine.api import taskqueue +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_link 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 loveapp.logic import TIMESPAN_THIS_WEEK -from loveapp.logic.love_link import create_love_link +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 render_template from loveapp.util.render import make_json_response -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.render import render_template from loveapp.views import common -from loveapp.logic.office import get_all_offices -from loveapp.logic.department import get_all_departments web_app = Blueprint('web_app', __name__) 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/util.py b/testing/util.py index c358302..9ef01f7 100644 --- a/testing/util.py +++ b/testing/util.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -from bs4 import BeautifulSoup import mock import pytest +from bs4 import BeautifulSoup from testing.factories import create_employee diff --git a/tests/conftest.py b/tests/conftest.py index c95a8be..81fe661 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,11 +3,10 @@ import mock import pytest - -from flask_themes2 import load_themes_from from flask import template_rendered - +from flask_themes2 import load_themes_from from google.appengine.ext import testbed + from loveapp import create_app diff --git a/tests/logic/alias_test.py b/tests/logic/alias_test.py index 743a9a8..384dc53 100644 --- a/tests/logic/alias_test.py +++ b/tests/logic/alias_test.py @@ -2,7 +2,6 @@ import unittest import loveapp.logic.alias - from loveapp.models import Alias from testing.factories import create_alias_with_employee_username from testing.factories import create_employee diff --git a/tests/logic/department_test.py b/tests/logic/department_test.py index ae631c6..0b6ea84 100644 --- a/tests/logic/department_test.py +++ b/tests/logic/department_test.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import unittest - from loveapp.logic.department import get_all_departments from testing.factories import create_employee diff --git a/tests/logic/email_test.py b/tests/logic/email_test.py index 5ae0b75..791f91c 100644 --- a/tests/logic/email_test.py +++ b/tests/logic/email_test.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -import mock import unittest +import mock + import loveapp.logic.email diff --git a/tests/logic/love_link_test.py b/tests/logic/love_link_test.py index 6fa1865..6edb0f5 100644 --- a/tests/logic/love_link_test.py +++ b/tests/logic/love_link_test.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -import unittest import datetime +import unittest import loveapp.logic.love import loveapp.logic.love_link from errors import NoSuchLoveLink -from testing.factories import create_love_link - from testing.factories import create_employee +from testing.factories import create_love_link class LoveLinkTest(unittest.TestCase): diff --git a/tests/logic/love_test.py b/tests/logic/love_test.py index fac4c43..3829a03 100644 --- a/tests/logic/love_test.py +++ b/tests/logic/love_test.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -import mock import unittest -from loveapp.config import CompanyValue +import mock + 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 diff --git a/tests/logic/office_test.py b/tests/logic/office_test.py index 43e6535..17fe419 100644 --- a/tests/logic/office_test.py +++ b/tests/logic/office_test.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- import unittest + import mock -from loveapp.logic.office import REMOTE_OFFICE + 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 = { diff --git a/tests/logic/subscription_test.py b/tests/logic/subscription_test.py index 1e2319a..2eff36b 100644 --- a/tests/logic/subscription_test.py +++ b/tests/logic/subscription_test.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -import mock import unittest +import mock + import loveapp.logic.subscription from loveapp.models import Subscription from testing.factories import create_employee diff --git a/tests/models/access_key_test.py b/tests/models/access_key_test.py index adec119..5ea6b70 100644 --- a/tests/models/access_key_test.py +++ b/tests/models/access_key_test.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- - from loveapp.models.access_key import AccessKey diff --git a/tests/models/employee_test.py b/tests/models/employee_test.py index 71714d1..0f4de0f 100644 --- a/tests/models/employee_test.py +++ b/tests/models/employee_test.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import mock import pytest - from google.appengine.api import users from errors import NoSuchEmployee diff --git a/tests/util/company_values_test.py b/tests/util/company_values_test.py index d516ddf..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 loveapp.config import CompanyValue +import mock + import loveapp.util.company_values +from loveapp.config import CompanyValue class CompanyValuesUtilTest(unittest.TestCase): diff --git a/tests/views/api_test.py b/tests/views/api_test.py index c14ade3..abf4083 100644 --- a/tests/views/api_test.py +++ b/tests/views/api_test.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals, print_function - -import pytest +from __future__ import print_function +from __future__ import unicode_literals import mock +import pytest import loveapp.logic.employee import loveapp.logic.love diff --git a/tests/views/web_test.py b/tests/views/web_test.py index 248b526..61df5a6 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -3,8 +3,8 @@ import pytest from bs4 import BeautifulSoup -from loveapp.config import CompanyValue 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 From 03d9cf1bdae3720f0f9040c851fa9f4941087728 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Mon, 29 Apr 2024 20:55:23 -0700 Subject: [PATCH 12/17] all tests pass! --- tests/logic/alias_test.py | 4 +++- tests/logic/department_test.py | 14 ++++---------- tests/logic/email_test.py | 10 ++++++---- tests/logic/love_link_test.py | 6 +++--- tests/logic/love_test.py | 21 ++++++++++++--------- tests/logic/office_test.py | 12 ++++++------ tests/logic/secret_test.py | 4 +++- tests/logic/subscription_test.py | 19 +++++++------------ 8 files changed, 44 insertions(+), 46 deletions(-) diff --git a/tests/logic/alias_test.py b/tests/logic/alias_test.py index 384dc53..b7c6684 100644 --- a/tests/logic/alias_test.py +++ b/tests/logic/alias_test.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- import unittest +import pytest + 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') diff --git a/tests/logic/department_test.py b/tests/logic/department_test.py index 0b6ea84..a4f04e1 100644 --- a/tests/logic/department_test.py +++ b/tests/logic/department_test.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import unittest - from loveapp.logic.department import get_all_departments from testing.factories import create_employee @@ -14,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 791f91c..7c8647b 100644 --- a/tests/logic/email_test.py +++ b/tests/logic/email_test.py @@ -2,10 +2,12 @@ import unittest 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.""" @@ -15,8 +17,8 @@ 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() @@ -26,8 +28,8 @@ def test_send_email_appengine(self, mock_config, mock_backends): 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() diff --git a/tests/logic/love_link_test.py b/tests/logic/love_link_test.py index 6edb0f5..9d2ce58 100644 --- a/tests/logic/love_link_test.py +++ b/tests/logic/love_link_test.py @@ -2,6 +2,8 @@ import datetime import unittest +import pytest + import loveapp.logic.love import loveapp.logic.love_link from errors import NoSuchLoveLink @@ -9,10 +11,8 @@ 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') diff --git a/tests/logic/love_test.py b/tests/logic/love_test.py index 3829a03..8b8aa7e 100644 --- a/tests/logic/love_test.py +++ b/tests/logic/love_test.py @@ -2,6 +2,7 @@ import unittest import mock +import pytest import loveapp.logic.love from errors import TaintedLove @@ -10,10 +11,8 @@ 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') @@ -21,7 +20,8 @@ def setUp(self): self.carol = create_employee(username='carol') self.message = 'hallo' - def test_send_loves(self): + @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, @@ -46,7 +46,8 @@ def test_invalid_sender(self): sender_username='wwu', ) - def test_sender_is_a_recipient(self): + @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, @@ -79,7 +80,8 @@ def test_invalid_recipient(self): 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) @@ -99,8 +101,9 @@ def test_send_loves_with_alias_and_username_for_same_user(self): 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']) ] diff --git a/tests/logic/office_test.py b/tests/logic/office_test.py index 17fe419..8845715 100644 --- a/tests/logic/office_test.py +++ b/tests/logic/office_test.py @@ -2,6 +2,7 @@ import unittest import mock +import pytest from loveapp.logic.office import get_all_offices from loveapp.logic.office import OfficeParser @@ -14,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 = [ @@ -29,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( @@ -56,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 7e2d1d7..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 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 2eff36b..692433e 100644 --- a/tests/logic/subscription_test.py +++ b/tests/logic/subscription_test.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import unittest - import mock import loveapp.logic.subscription @@ -9,16 +7,13 @@ from testing.factories import create_subscription -class SubscriptionTest(unittest.TestCase): - nosegae_datastore_v3 = True - - @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() +@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() - subscription = create_subscription() - self.assertIsNotNone(Subscription.get_by_id(subscription.key.id())) + subscription = create_subscription() + assert Subscription.get_by_id(subscription.key.id()) is not None - loveapp.logic.subscription.delete_subscription(subscription.key.id()) + loveapp.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 From b542aad8ef69f1caa3b8ee7878ce9124330829a7 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Mon, 29 Apr 2024 21:20:40 -0700 Subject: [PATCH 13/17] update yaml files to remove unneeded fields remove appengine_coonfig.py, no longer needed in python3 runtime --- app.yaml | 26 +------------------------- appengine_config.py | 9 --------- worker.yaml | 8 -------- 3 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 appengine_config.py diff --git a/app.yaml b/app.yaml index 4d8b2a4..d96501e 100644 --- a/app.yaml +++ b/app.yaml @@ -1,26 +1,8 @@ service: default runtime: python311 -api_version: 1 -threadsafe: true - 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 @@ -29,17 +11,11 @@ handlers: static_dir: static secure: always - url: /_themes/(.*)/img/(.*) - static_files: themes/\1/static/img/\2 + static_files: loveapp/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 skip_files: - ^(.*/)?#.*#$ 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/worker.yaml b/worker.yaml index 04e15d1..4275312 100644 --- a/worker.yaml +++ b/worker.yaml @@ -1,15 +1,7 @@ service: worker runtime: python311 -api_version: 1 -threadsafe: true app_engine_apis: true -handlers: -- url: /tasks/.* - script: main.app - login: admin - secure: always - skip_files: - ^(.*/)?#.*#$ From dadb7fdbad9f126a5e8afad8fe1da70d20f1dc92 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Mon, 29 Apr 2024 21:44:32 -0700 Subject: [PATCH 14/17] Freezing imports, Fix BeautifulSoup warnings --- requirements-dev.txt | 5 ++--- requirements.txt | 24 +++++++++++------------- testing/util.py | 2 +- tests/views/web_test.py | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c856fda..7852b1c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,10 +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==4.0.3 -#NoseGAE==0.5.10 pre-commit -pyrsistent==0.16.1 pytest==8.1.2 +tox==4.15.0 diff --git a/requirements.txt b/requirements.txt index 69d390e..3916100 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +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. -beautifulsoup4 -boto -Flask -Flask-Themes2 -ipdb -itsdangerous -Jinja2 -MarkupSafe -pytz -pyyaml -urllib3 -Werkzeug -wheel +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/testing/util.py b/testing/util.py index 9ef01f7..eefb93e 100644 --- a/testing/util.py +++ b/testing/util.py @@ -22,7 +22,7 @@ def assertHasCsrf(self, response, form_class, session): :param form: a form entry from response.forms :param session: response.session """ - soup = BeautifulSoup(response.data) + soup = BeautifulSoup(response.data, 'html.parser') csrf_token = soup.find('form', class_=form_class).\ find('input', attrs={'name': '_csrf_token'}).\ get('value') diff --git a/tests/views/web_test.py b/tests/views/web_test.py index 61df5a6..d97b916 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -184,7 +184,7 @@ def test_index_with_recipient_and_message(self, client, recorded_templates): assert response.status_code == 200 template, response_context = recorded_templates[0] - soup = BeautifulSoup(response.data) + soup = BeautifulSoup(response.data, 'html.parser') send_love_form = soup.find('form', class_='send-love-form') assert response_context['recipients'] == 'janedoe' From a7df85d8478e9f34594cc880e342016f959aa004 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sun, 5 May 2024 14:24:00 -0700 Subject: [PATCH 15/17] move skip_files into .gcloudignore --- .gcloudignore | 13 +++++++++++++ .travis.yml | 2 +- app.yaml | 18 ------------------ worker.yaml | 12 ------------ 4 files changed, 14 insertions(+), 31 deletions(-) create mode 100644 .gcloudignore 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/.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/app.yaml b/app.yaml index d96501e..4b281fc 100644 --- a/app.yaml +++ b/app.yaml @@ -10,21 +10,3 @@ handlers: - url: /static static_dir: static secure: always -- url: /_themes/(.*)/img/(.*) - static_files: loveapp/themes/\1/static/img/\2 - upload: themes/(.*)/static/img/(.*) - secure: always - login: optional - - -skip_files: -- ^(.*/)?#.*#$ -- ^(.*/)?.*/RCS/.*$ -- ^(.*/)?.*\.py[co]$ -- ^(.*/)?.*~$ -- ^(.*/)?\..*$ -- ^YelpLove.egg-info(/.*)?$ -- ^config-example.py$ -- ^google_appengine(/.*)?$ -- ^tmp(/.*)?$ -- ^virtualenv_.*$ diff --git a/worker.yaml b/worker.yaml index 4275312..5db07f4 100644 --- a/worker.yaml +++ b/worker.yaml @@ -1,15 +1,3 @@ service: worker runtime: python311 app_engine_apis: true - - -skip_files: -- ^(.*/)?#.*#$ -- ^(.*/)?.*/RCS/.*$ -- ^(.*/)?.*\.py[co]$ -- ^(.*/)?.*~$ -- ^(.*/)?\..*$ -- ^YelpLove.egg-info(/.*)?$ -- ^google_appengine(/.*)?$ -- ^tmp(/.*)?$ -- ^virtualenv_.*$ From 807fe44237ad763ce0e73dccd12ce6aae8a8efa1 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 15 Jun 2024 08:47:49 -0700 Subject: [PATCH 16/17] Updating README.md to account for the lack of the remote api/interactive console --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 35383c6..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,18 +78,18 @@ 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 loveapp.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: From 86bc3bd5f157264a0bd2f9ca9f1d9d978c1f2b20 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Tue, 9 Jul 2024 18:50:57 -0700 Subject: [PATCH 17/17] Add missing assert, plus minor change to make tests pass --- testing/util.py | 2 +- tests/views/web_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/util.py b/testing/util.py index eefb93e..b627456 100644 --- a/testing/util.py +++ b/testing/util.py @@ -14,7 +14,7 @@ def assertRequiresLogin(self, response): 'Unexpected Location header: {0}'.format(response.headers['Location']) def assertRequiresAdmin(self, response): - response.status_code == 401 + assert response.status_code == 401 def assertHasCsrf(self, response, form_class, session): """Make sure the response form contains the correct CSRF token. diff --git a/tests/views/web_test.py b/tests/views/web_test.py index d97b916..e9fe00d 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -65,7 +65,7 @@ def test_post_requires_login(self, client, url, data): ) -class TestAdminResources(YelpLoveTestCase): +class TestAdminResources(LoggedInUserBaseTest): # Managing API Keys def test_keys(self, client):