diff options
author | zsloan | 2018-03-29 10:36:12 -0500 |
---|---|---|
committer | GitHub | 2018-03-29 10:36:12 -0500 |
commit | b215b5fe5c6d13f0ed445106230e1e38db71c918 (patch) | |
tree | 97f1f47b092bef38856ac34c1bd745e471c38311 | |
parent | e67e3a76fca0bad4796853eb58140a412922bc9c (diff) | |
parent | e0c706c51c834caa836ecffd27a5d18fc23178ff (diff) | |
download | genenetwork2-b215b5fe5c6d13f0ed445106230e1e38db71c918.tar.gz |
Merge pull request #297 from pjotrp/testing
Testing
70 files changed, 1211 insertions, 2865 deletions
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md new file mode 100644 index 00000000..d15cad1a --- /dev/null +++ b/RELEASE-NOTES.md @@ -0,0 +1,17 @@ +## ChangeLog v2.11 (date unknown) + +This is a massive bug fix release with many improvements. For contributions +see +[contributors](https://github.com/genenetwork/genenetwork2/contributors) +and +[commits](https://github.com/genenetwork/genenetwork2/commits/master). + +### Added GEMMA support + +* Front-end support + +### Added test framework and unit tests + +* Added python integration and unit tests + + diff --git a/bin/genenetwork2 b/bin/genenetwork2 index 8886e4bc..3f06e7f9 100755 --- a/bin/genenetwork2 +++ b/bin/genenetwork2 @@ -68,32 +68,36 @@ if [ "$1" = "-c" -o "$1" = "-gunicorn" ]; then echo "Can not use $1 switch without default settings file" exit 1 fi -# Handle settings parameter (can be .py or .json) -if [ ! -z $1 ]; then - settings=$(realpath "$1") - if [ ! -e $settings ]; then - settings=$GN2_BASE_DIR/etc/default_settings.py - else - shift - fi -fi -ext="${settings##*.}" -if [ "$ext" = "json" -o "$ext" = "JSON" ]; then - overrides=$settings +settings=$1 +if [ -z $settings ]; then + settings=$GN2_BASE_DIR/etc/default_settings.py else - echo $settings + shift fi +settings=$(realpath $settings) + +# ext="${settings##*.}" +# if [ "$ext" = "json" -o "$ext" = "JSON" ]; then +# overrides=$settings +# else +# echo $settings +# fi if [ ! -e $settings ]; then echo "ERROR: can not locate settings file - pass it in the command line" exit 1 fi -export WQFLASK_SETTINGS=$settings # Python -export WQFLASK_OVERRIDES=$overrides # JSON -echo WQFLASK_SETTINGS=$settings -echo WQFLASK_OVERRIDES=$overrides +export GN2_SETTINGS=$settings # Python +echo GN2_SETTINGS=$settings + +# This is a temporary hack to inject ES - should have added python2-elasticsearch package to guix instead +# if [ -z $ELASTICSEARCH_PROFILE ]; then +# echo -e "WARNING: Elastic Search profile has not been set - use ELASTICSEARCH_PROFILE"; +# else +# PYTHONPATH="$PYTHONPATH${PYTHONPATH:+:}$ELASTICSEARCH_PROFILE/lib/python2.7/site-packages" +# fi if [ -z $GN2_PROFILE ] ; then echo "WARNING: GN2_PROFILE has not been set - you need the environment, so I hope you know what you are doing!" @@ -108,7 +112,7 @@ if [ -z $GN2_PROFILE ]; then read -p "PRESS [ENTER] TO CONTINUE..." else export PATH=$GN2_PROFILE/bin:$PATH - export PYTHONPATH=$GN2_PROFILE/lib/python2.7/site-packages + export PYTHONPATH="$GN2_PROFILE/lib/python2.7/site-packages" # never inject another PYTHONPATH!! export R_LIBS_SITE=$GN2_PROFILE/site-library export GEM_PATH=$GN2_PROFILE/lib/ruby/gems/2.4.0 export JS_GUIX_PATH=$GN2_PROFILE/share/genenetwork2/javascript @@ -124,7 +128,11 @@ else if [ -z $GEMMA_WRAPPER_COMMAND ]; then export GEMMA_WRAPPER_COMMAND="$GN2_PROFILE/bin/gemma-wrapper" fi - if [ ! -d $PYTHONPATH ] ; then echo "PYTHONPATH not valid "$PYTHONPATH ; exit 1 ; fi + while IFS=":" read -ra PPATH; do + for PPART in "${PPATH[@]}"; do + if [ ! -d $PPART ] ; then echo "$PPART in PYTHONPATH not valid $PYTHONPATH" ; exit 1 ; fi + done + done <<< "$PYTHONPATH" if [ ! -d $R_LIBS_SITE ] ; then echo "R_LIBS_SITE not valid "$R_LIBS_SITE ; exit 1 ; fi if [ ! -d $GEM_PATH ] ; then echo "GEM_PATH not valid "$GEM_PATH ; exit 1 ; fi fi @@ -157,8 +165,9 @@ if [ "$1" = '-c' ] ; then cd $GN2_BASE_DIR/wqflask cmd=${2#wqflask/} echo PYTHONPATH=$PYTHONPATH - echo RUNNING COMMAND $cmd - python $cmd + shift ; shift + echo RUNNING COMMAND $cmd $* + python $cmd $* exit $? fi # Now handle command parameter -cli which runs in bash @@ -167,8 +176,9 @@ if [ "$1" = "-cli" ] ; then cd $GN2_BASE_DIR/wqflask cmd=$2 echo PYTHONPATH=$PYTHONPATH - echo RUNNING COMMAND $cmd - $cmd + shift ; shift + echo RUNNING COMMAND $cmd $* + $cmd $* exit $? fi if [ "$1" = '-gunicorn' ] ; then diff --git a/bin/mechnical-rob b/bin/mechnical-rob new file mode 100755 index 00000000..be223d94 --- /dev/null +++ b/bin/mechnical-rob @@ -0,0 +1,111 @@ +#!/usr/bin/env ruby + + +USAGE = <<EOT +This is Mechanical-Rob - an automated web server tester for + Genenetwork.org that uses the brilliant + mechanize gem with minitest. + +To use this software you need to install mechanize. Run it from +the root of the genenetwork2 source tree as, for example, + + ./bin/test-website http://localhost:5003/ (default) + +If you are using the small deployment database you can use + + ./bin/test-website --skip -n + +To run all tests + + ./bin/test-website --all + +To run individual tests on localhost you can do + + ruby -Itest -Itest/lib test/lib/mapping.rb --name="/Mapping/" + +For more information see http://genenetwork.org/ + +EOT +$stderr.print USAGE + +require 'optparse' + +options = { database: :small, link_checker: false} +opts = OptionParser.new do |o| + o.banner = "Usage: #{File.basename($0)} [options] URL" + + o.on('-d','--database', String, 'Use database (default db_webqtl_s)') do |s| + options[:database] = + case s + when 'xx' + :unknown + else + :small + end + end + + o.on('--all', 'Run all tests') do + options[:all] = true + end + + o.on('-l','--link-checker', 'Check for dead links') do + options[:link_checker] = true + end + + o.on('--navigation', 'Check for navigation') do + options[:navigation] = true + end + + o.on('--mapping', 'Check for mapping') do + options[:mapping] = true + end + + o.on('--skip-broken', 'Skip tests that are known to be broken') do + options[:skip_broken] = true + end + + o.separator "" + o.on_tail('-h', '--help', 'display this help and exit') do + options[:show_help] = true + end +end + +opts.parse!(ARGV) + +if options[:show_help] + print opts + exit 1 +end + +$options = options # we are using a global here +$host = + if ARGV.size>0 + ARGV.shift + else + "http://localhost:5003" + end + +$stderr.print "Testing <",$host,">\n" + +require 'mechanize' +require 'minitest/spec' +require 'minitest/autorun' + +# These are the actual testing modules + +libpath = File.dirname(File.dirname(__FILE__)) +$: << File.join(libpath,'test/lib') + +require 'main_web_functionality' + +if options[:all] or options[:mapping] + require 'mapping' +end + +if options[:all] or options[:link_checker] + require 'link_checker' +end + +if options[:all] or options[:navigation] + require 'navigation' +end diff --git a/bin/test-website b/bin/test-website index be223d94..5935f016 100755 --- a/bin/test-website +++ b/bin/test-website @@ -1,111 +1,7 @@ -#!/usr/bin/env ruby +#! /bin/bash - -USAGE = <<EOT -This is Mechanical-Rob - an automated web server tester for - Genenetwork.org that uses the brilliant - mechanize gem with minitest. - -To use this software you need to install mechanize. Run it from -the root of the genenetwork2 source tree as, for example, - - ./bin/test-website http://localhost:5003/ (default) - -If you are using the small deployment database you can use - - ./bin/test-website --skip -n - -To run all tests - - ./bin/test-website --all - -To run individual tests on localhost you can do - - ruby -Itest -Itest/lib test/lib/mapping.rb --name="/Mapping/" - -For more information see http://genenetwork.org/ - -EOT -$stderr.print USAGE - -require 'optparse' - -options = { database: :small, link_checker: false} -opts = OptionParser.new do |o| - o.banner = "Usage: #{File.basename($0)} [options] URL" - - o.on('-d','--database', String, 'Use database (default db_webqtl_s)') do |s| - options[:database] = - case s - when 'xx' - :unknown - else - :small - end - end - - o.on('--all', 'Run all tests') do - options[:all] = true - end - - o.on('-l','--link-checker', 'Check for dead links') do - options[:link_checker] = true - end - - o.on('--navigation', 'Check for navigation') do - options[:navigation] = true - end - - o.on('--mapping', 'Check for mapping') do - options[:mapping] = true - end - - o.on('--skip-broken', 'Skip tests that are known to be broken') do - options[:skip_broken] = true - end - - o.separator "" - o.on_tail('-h', '--help', 'display this help and exit') do - options[:show_help] = true - end -end - -opts.parse!(ARGV) - -if options[:show_help] - print opts +if [ -z $GN2_PROFILE ]; then + echo "Run request tests with something like" + echo env GN2_PROFILE=/home/wrk/opt/gn-latest ./bin/genenetwork2 ./etc/default_settings.py -c ../test/requests/test-website.py http://localhost:5003 exit 1 -end - -$options = options # we are using a global here -$host = - if ARGV.size>0 - ARGV.shift - else - "http://localhost:5003" - end - -$stderr.print "Testing <",$host,">\n" - -require 'mechanize' -require 'minitest/spec' -require 'minitest/autorun' - -# These are the actual testing modules - -libpath = File.dirname(File.dirname(__FILE__)) -$: << File.join(libpath,'test/lib') - -require 'main_web_functionality' - -if options[:all] or options[:mapping] - require 'mapping' -end - -if options[:all] or options[:link_checker] - require 'link_checker' -end - -if options[:all] or options[:navigation] - require 'navigation' -end +fi diff --git a/etc/default_settings.py b/etc/default_settings.py index 699d21f1..a70d8aec 100644 --- a/etc/default_settings.py +++ b/etc/default_settings.py @@ -41,6 +41,24 @@ SECURITY_POST_LOGIN_VIEW = "/thank_you" SERVER_PORT = 5003 # running on localhost SECRET_HMAC_CODE = '\x08\xdf\xfa\x93N\x80\xd9\\H@\\\x9f`\x98d^\xb4a;\xc6OM\x946a\xbc\xfc\x80:*\xebc' +GITHUB_CLIENT_ID = "UNKNOWN" +GITHUB_CLIENT_SECRET = "UNKNOWN" +GITHUB_AUTH_URL = "UNKNOWN" +GITHUB_API_URL = "UNKNOWN" + +ORCID_CLIENT_ID = "UNKNOWN" +ORCID_CLIENT_SECRET = "UNKNOWN" +ORCID_AUTH_URL = "UNKNOWN" +ORCID_TOKEN_URL = "UNKNOWN" + +ELASTICSEARCH_HOST = "localhost" +ELASTICSEARCH_PORT = '9200' + +SMTP_CONNECT = "UNKNOWN" +SMTP_USERNAME = "UNKNOWN" +SMTP_PASSWORD = "UNKNOWN" + + # ---- Behavioural settings (defaults) note that logger and log levels can # be overridden at the module level and with enviroment settings WEBSERVER_MODE = 'DEV' # Python webserver mode (DEBUG|DEV|PROD) diff --git a/test/requests/link_checker.py b/test/requests/link_checker.py new file mode 100644 index 00000000..256bf6ef --- /dev/null +++ b/test/requests/link_checker.py @@ -0,0 +1,63 @@ +from __future__ import print_function +import re +import requests +from lxml.html import parse +from requests.exceptions import ConnectionError + +def is_root_link(link): + pattern = re.compile("^/$") + return pattern.match(link) + +def is_mailto_link(link): + pattern = re.compile("^mailto:.*") + return pattern.match(link) + +def is_internal_link(link): + pattern = re.compile("^/.*") + return pattern.match(link) + +def get_links(doc): + return filter( + lambda x: not ( + is_root_link(x) + or is_mailto_link(x)) + , map(lambda y: y.get("href") + , doc.cssselect("a"))) + +def verify_link(link): + try: + result = requests.get(link, timeout=20) + if result.status_code == 200: + print(link+" ==> OK") + else: + print("ERROR: link `"+link+"` failed with status " + , result.status_code) + except ConnectionError as ex: + print("ERROR: ", link, ex) + +def check_page(host, start_url): + print("") + print("Checking links in page `"+start_url+"`") + doc = parse(start_url).getroot() + links = get_links(doc) + internal_links = filter(is_internal_link, links) + external_links = filter(lambda x: not is_internal_link(x), links) + external_links.append("http://somenon-existentsite.brr") + for link in internal_links: + verify_link(host+link) + + for link in external_links: + verify_link(link) + +def check_links(args_obj, parser): + print("") + print("Checking links") + host = args_obj.host + + # Check the home page + check_page(host, host) + + # Check traits page + check_page( + host, + host+"/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P") diff --git a/test/requests/main_web_functionality.py b/test/requests/main_web_functionality.py new file mode 100644 index 00000000..7b89b833 --- /dev/null +++ b/test/requests/main_web_functionality.py @@ -0,0 +1,40 @@ +from __future__ import print_function +import re +import requests +from lxml.html import parse +from link_checker import check_page +from requests.exceptions import ConnectionError + +def check_home(url): + doc = parse(url).getroot() + search_button = doc.cssselect("#btsearch") + assert(search_button[0].value == "Search") + print("OK") + +def check_search_page(host): + data = dict( + species="mouse" + , group="BXD" + , type="Hippocampus mRNA" + , dataset="HC_M2_0606_P" + , search_terms_or="" + , search_terms_and="MEAN=(15 16) LRS=(23 46)") + result = requests.get(host+"/search", params=data) + found = result.text.find("/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P") + assert(found >= 0) + print("OK") + check_traits_page(host, "/show_trait?trait_id=1435395_s_at&dataset=HC_M2_0606_P") + +def check_traits_page(host, traits_url): + doc = parse(host+traits_url).getroot() + traits_form = doc.forms[1] + assert(traits_form.fields["corr_dataset"] == "HC_M2_0606_P") + print("OK") + check_page(host, host+traits_url) + +def check_main_web_functionality(args_obj, parser): + print("") + print("Checking main web functionality...") + host = args_obj.host + check_home(host) + check_search_page(host) diff --git a/test/requests/mapping_tests.py b/test/requests/mapping_tests.py new file mode 100644 index 00000000..fd20df11 --- /dev/null +++ b/test/requests/mapping_tests.py @@ -0,0 +1,43 @@ +from __future__ import print_function +import re +import json +import requests +from lxml.html import fromstring + +def get_data(list_item): + try: + value = list_item[1] + except: + value = None + #print("list_item:", list_item, "==>", value) + return value + +def load_data_from_file(): + filename = "../test/data/input/mapping/1435395_s_at_HC_M2_0606_P.json" + file_handle = open(filename, "r") + file_data = json.loads(file_handle.read().encode("utf-8")) + return file_data + +def check_pylmm_tool_selection(host, data): + data["method"] = "pylmm" + page = requests.post(host+"/marker_regression", data=data) + doc = fromstring(page.text) + form = doc.forms[1] + assert form.fields["dataset"] == "HC_M2_0606_P" + assert form.fields["value:BXD1"] == "15.034" # Check value in the file + +def check_R_qtl_tool_selection(host, data): + pass + +def check_CIM_tool_selection(host, data): + pass + +def check_mapping(args_obj, parser): + print("") + print("Checking mapping") + + host = args_obj.host + data = load_data_from_file() + check_pylmm_tool_selection(host, data) + check_R_qtl_tool_selection(host, data) + check_CIM_tool_selection(host, data) diff --git a/test/requests/parametrized_test.py b/test/requests/parametrized_test.py new file mode 100644 index 00000000..abf98fce --- /dev/null +++ b/test/requests/parametrized_test.py @@ -0,0 +1,27 @@ +import logging +import unittest +from elasticsearch import Elasticsearch, TransportError + +class ParametrizedTest(unittest.TestCase): + + def __init__(self, methodName='runTest', gn2_url="http://localhost:5003", es_url="localhost:9200"): + super(ParametrizedTest, self).__init__(methodName=methodName) + self.gn2_url = gn2_url + self.es_url = es_url + + def setUp(self): + self.es = Elasticsearch([self.es_url]) + self.es_cleanup = [] + + es_logger = logging.getLogger("elasticsearch") + es_logger.addHandler( + logging.FileHandler("/tmp/es_TestRegistrationInfo.log")) + es_trace_logger = logging.getLogger("elasticsearch.trace") + es_trace_logger.addHandler( + logging.FileHandler("/tmp/es_TestRegistrationTrace.log")) + + def tearDown(self): + self.es.delete_by_query( + index="users" + , doc_type="local" + , body={"query":{"match":{"email_address":"test@user.com"}}}) diff --git a/test/requests/run-integration-tests.py b/test/requests/run-integration-tests.py new file mode 100644 index 00000000..5e816549 --- /dev/null +++ b/test/requests/run-integration-tests.py @@ -0,0 +1,34 @@ +import sys +from test_login_local import TestLoginLocal +from test_login_orcid import TestLoginOrcid +from test_login_github import TestLoginGithub +from test_registration import TestRegistration +from unittest import TestSuite, TextTestRunner, TestLoader + +test_cases = [ + TestRegistration + , TestLoginLocal + , TestLoginGithub + , TestLoginOrcid +] + +def suite(gn2_url, es_url): + the_suite = TestSuite() + for case in test_cases: + the_suite.addTests(initTest(case, gn2_url, es_url)) + return the_suite + +def initTest(klass, gn2_url, es_url): + loader = TestLoader() + methodNames = loader.getTestCaseNames(klass) + return [klass(mname, gn2_url, es_url) for mname in methodNames] + +def main(gn2_url, es_url): + runner = TextTestRunner() + runner.run(suite(gn2_url, es_url)) + +if __name__ == "__main__": + if len(sys.argv) < 3: + raise Exception("Required arguments missing:\n\tTry running `run-integration-test.py <gn2-url> <es-url>`") + else: + main(sys.argv[1], sys.argv[2]) diff --git a/test/requests/test-website.py b/test/requests/test-website.py new file mode 100755 index 00000000..2bef6eb1 --- /dev/null +++ b/test/requests/test-website.py @@ -0,0 +1,71 @@ +# Run with something like +# +# env GN2_PROFILE=/home/wrk/opt/gn-latest ./bin/genenetwork2 ./etc/default_settings.py -c ../test/requests/test-website.py http://localhost:5003 +# +# Mostly to pick up the Guix GN2_PROFILE and python modules +from __future__ import print_function +import argparse +from link_checker import check_links +from mapping_tests import check_mapping +from main_web_functionality import check_main_web_functionality + +print("Mechanical Rob firing up...") + +def run_all(args_obj, parser): + print("") + print("Running all tests.") + check_main_web_functionality(args_obj, parser) + check_links(args_obj, parser) + check_mapping(args_obj, parser) + # TODO: Add other functions as they are created. + +def print_help(args_obj, parser): + print(parser.format_help()) + +def dummy(args_obj, parser): + print("Not implemented yet.") + + +desc = """ +This is Mechanical-Rob - an automated web server tester for + Genenetwork.org +""" +parser = argparse.ArgumentParser(description=desc) + +parser.add_argument("-d", "--database", metavar="DB", type=str + , default="db_webqtl_s" + , help="Use database (default db_webqtl_s)") + +parser.add_argument("host", metavar="HOST", type=str + , default="http://localhost:5003" + , help="The url to the web server") + +parser.add_argument("-a", "--all", dest="accumulate", action="store_const" + , const=run_all, default=print_help + , help="Runs all tests.") + +parser.add_argument("-l", "--link-checker", dest="accumulate" + , action='store_const', const=check_links, default=print_help + , help="Checks for dead links.") + +parser.add_argument("-f", "--main-functionality", dest="accumulate" + , action='store_const', const=check_main_web_functionality + , default=print_help + , help="Checks for main web functionality.") + +parser.add_argument("-m", "--mapping", dest="accumulate" + , action="store_const", const=check_mapping, default=print_help + , help="Checks for mapping.") + +# parser.add_argument("-n", "--navigation", dest="accumulate" +# , action="store_const", const=check_navigation, default=print_help +# , help="Checks for navigation.") + +# parser.add_argument("-s", "--skip-broken", dest="accumulate" +# , action="store_const", const=dummy, default=print_help +# , help="Skip tests that are known to be broken.") + +args = parser.parse_args() +# print("The arguments object: ", args) + +args.accumulate(args, parser) diff --git a/test/requests/test_login_github.py b/test/requests/test_login_github.py new file mode 100644 index 00000000..1bf4f695 --- /dev/null +++ b/test/requests/test_login_github.py @@ -0,0 +1,47 @@ +import uuid +import requests +from time import sleep +from wqflask import app +from parameterized import parameterized +from parametrized_test import ParametrizedTest + +login_link_text = '<a id="login_in" href="/n/login">Sign in</a>' +logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>' +uid = str(uuid.uuid4()) + +class TestLoginGithub(ParametrizedTest): + + def setUp(self): + super(TestLoginGithub, self).setUp() + data = { + "user_id": uid + , "name": "A. T. Est User" + , "github_id": 693024 + , "user_url": "https://fake-github.com/atestuser" + , "login_type": "github" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + self.es.create(index="users", doc_type="local", body=data, id=uid) + sleep(1) + + def tearDown(self): + super(TestLoginGithub, self).tearDown() + self.es.delete(index="users", doc_type="local", id=uid) + + def testLoginUrl(self): + login_button_text = '<a href="https://github.com/login/oauth/authorize?client_id=' + app.config.get("GITHUB_CLIENT_ID") + '&client_secret=' + app.config.get("GITHUB_CLIENT_SECRET") + '" title="Login with GitHub" class="btn btn-info btn-group">Login with Github</a>' + result = requests.get(self.gn2_url+"/n/login") + index = result.content.find(login_button_text) + self.assertTrue(index >= 0, "Should have found `Login with Github` button") + + @parameterized.expand([ + ("1234", login_link_text, "Login should have failed with non-existing user") + , (uid, logout_link_text, "Login should have been successful with existing user") + ]) + def testLogin(self, test_uid, expected, message): + url = self.gn2_url+"/n/login?type=github&uid="+test_uid + result = requests.get(url) + index = result.content.find(expected) + self.assertTrue(index >= 0, message) diff --git a/test/requests/test_login_local.py b/test/requests/test_login_local.py new file mode 100644 index 00000000..808649ca --- /dev/null +++ b/test/requests/test_login_local.py @@ -0,0 +1,60 @@ +import requests +from wqflask import user_manager +from parameterized import parameterized +from parametrized_test import ParametrizedTest + +login_link_text = '<a id="login_in" href="/n/login">Sign in</a>' +logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>' + +class TestLoginLocal(ParametrizedTest): + + def setUp(self): + super(TestLoginLocal, self).setUp() + self.login_url = self.gn2_url +"/n/login" + data = { + "es_connection": self.es, + "email_address": "test@user.com", + "full_name": "Test User", + "organization": "Test Organisation", + "password": "test_password", + "password_confirm": "test_password" + } + user_manager.basic_info = lambda : { "basic_info": "basic" } + user_manager.RegisterUser(data) + + + @parameterized.expand([ + ( + { + "email_address": "non@existent.email", + "password": "doesitmatter?" + }, login_link_text, "Login should have failed with the wrong user details."), + ( + { + "email_address": "test@user.com", + "password": "test_password" + }, logout_link_text, "Login should have been successful with correct user details and neither import_collections nor remember_me set"), + ( + { + "email_address": "test@user.com", + "password": "test_password", + "import_collections": "y" + }, logout_link_text, "Login should have been successful with correct user details and only import_collections set"), + ( + { + "email_address": "test@user.com", + "password": "test_password", + "remember_me": "y" + }, logout_link_text, "Login should have been successful with correct user details and only remember_me set"), + ( + { + "email_address": "test@user.com", + "password": "test_password", + "remember_me": "y", + "import_collections": "y" + }, logout_link_text, "Login should have been successful with correct user details, and both remember_me, and import_collections set") + ]) + def testLogin(self, data, expected, message): + result = requests.post(self.login_url, data=data) + index = result.content.find(expected) + self.assertTrue(index >= 0, message) diff --git a/test/requests/test_login_orcid.py b/test/requests/test_login_orcid.py new file mode 100644 index 00000000..ea15642e --- /dev/null +++ b/test/requests/test_login_orcid.py @@ -0,0 +1,47 @@ +import uuid +import requests +from time import sleep +from wqflask import app +from parameterized import parameterized +from parametrized_test import ParametrizedTest + +login_link_text = '<a id="login_in" href="/n/login">Sign in</a>' +logout_link_text = '<a id="login_out" title="Signed in as ." href="/n/logout">Sign out</a>' +uid = str(uuid.uuid4()) + +class TestLoginOrcid(ParametrizedTest): + + def setUp(self): + super(TestLoginOrcid, self).setUp() + data = { + "user_id": uid + , "name": "A. T. Est User" + , "orcid": 345872 + , "user_url": "https://fake-orcid.org/atestuser" + , "login_type": "orcid" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + self.es.create(index="users", doc_type="local", body=data, id=uid) + sleep(1) + + def tearDown(self): + super(TestLoginOrcid, self).tearDown() + self.es.delete(index="users", doc_type="local", id=uid) + + def testLoginUrl(self): + login_button_text = 'a href="https://sandbox.orcid.org/oauth/authorize?response_type=code&scope=/authenticate&show_login=true&client_id=' + app.config.get("ORCID_CLIENT_ID") + '&client_secret=' + app.config.get("ORCID_CLIENT_SECRET") + '" title="Login with ORCID" class="btn btn-info btn-group">Login with ORCID</a>' + result = requests.get(self.gn2_url+"/n/login") + index = result.content.find(login_button_text) + self.assertTrue(index >= 0, "Should have found `Login with ORCID` button") + + @parameterized.expand([ + ("1234", login_link_text, "Login should have failed with non-existing user") + , (uid, logout_link_text, "Login should have been successful with existing user") + ]) + def testLogin(self, test_uid, expected, message): + url = self.gn2_url+"/n/login?type=orcid&uid="+test_uid + result = requests.get(url) + index = result.content.find(expected) + self.assertTrue(index >= 0, message) diff --git a/test/requests/test_registration.py b/test/requests/test_registration.py new file mode 100644 index 00000000..0047e8a6 --- /dev/null +++ b/test/requests/test_registration.py @@ -0,0 +1,41 @@ +import sys +import requests +from parametrized_test import ParametrizedTest + +class TestRegistration(ParametrizedTest): + + def tearDown(self): + for item in self.es_cleanup: + self.es.delete(index="users", doc_type="local", id=item["_id"]) + + def testRegistrationPage(self): + if self.es.ping(): + data = { + "email_address": "test@user.com", + "full_name": "Test User", + "organization": "Test Organisation", + "password": "test_password", + "password_confirm": "test_password" + } + requests.post(self.gn2_url+"/n/register", data) + response = self.es.search( + index="users" + , doc_type="local" + , body={ + "query": {"match": {"email_address": "test@user.com"}}}) + self.assertEqual(len(response["hits"]["hits"]), 1) + else: + self.skipTest("The elasticsearch server is down") + +def main(gn2, es): + import unittest + suite = unittest.TestSuite() + suite.addTest(TestRegistration(methodName="testRegistrationPage", gn2_url=gn2, es_url=es)) + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == "__main__": + if len(sys.argv) < 3: + raise Exception("Required arguments missing") + else: + main(sys.argv[1], sys.argv[2]) diff --git a/test/unittest/test_registration.py b/test/unittest/test_registration.py new file mode 100644 index 00000000..98d0cdff --- /dev/null +++ b/test/unittest/test_registration.py @@ -0,0 +1,27 @@ +# Run test with something like +# +# env GN2_PROFILE=~/opt/gn-latest GENENETWORK_FILES=$HOME/gn2_data ./bin/genenetwork2 ./etc/default_settings.py -c ../test/unittest/test_registration.py +# + +import unittest +import mock.es_double as es +from wqflask.user_manager import RegisterUser + +class TestRegisterUser(unittest.TestCase): + def setUp(self): + self.es = es.ESDouble() + + def testRegisterUserWithCorrectData(self): + data = { + "email_address": "user@example.com" + , "full_name": "A.N. Other" + , "organization": "Some Organisation" + , "password": "testing" + , "password_confirm": "testing" + , "es_connection": self.es + } + result = RegisterUser(data) + self.assertEqual(len(result.errors), 0, "Errors were not expected") + +if __name__ == "__main__": + unittest.main() diff --git a/wqflask/flask_security/__init__.py b/wqflask/flask_security/__init__.py deleted file mode 100644 index 81e6c89e..00000000 --- a/wqflask/flask_security/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security - ~~~~~~~~~~~~~~~~~~ - - Flask-Security is a Flask extension that aims to add quick and simple - security via Flask-Login, Flask-Principal, Flask-WTF, and passlib. - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -__version__ = '1.6.0' - -from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user -from .datastore import SQLAlchemyUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore -from .decorators import auth_token_required, http_auth_required, \ - login_required, roles_accepted, roles_required -from .forms import ForgotPasswordForm, LoginForm, RegisterForm, \ - ResetPasswordForm, PasswordlessLoginForm, ConfirmRegisterForm -from .signals import confirm_instructions_sent, password_reset, \ - reset_password_instructions_sent, user_confirmed, user_registered -from .utils import login_user, logout_user, url_for_security - -print "Using our own flask.ext.security"
\ No newline at end of file diff --git a/wqflask/flask_security/changeable.py b/wqflask/flask_security/changeable.py deleted file mode 100644 index 4447b655..00000000 --- a/wqflask/flask_security/changeable.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.changeable - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security recoverable module - - :copyright: (c) 2012 by Matt Wright. - :author: Eskil Heyn Olsen - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app as app, request -from werkzeug.local import LocalProxy - -from .signals import password_changed -from .utils import send_mail, encrypt_password, url_for_security, \ - config_value - - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def send_password_changed_notice(user): - """Sends the password changed notice email for the specified user. - - :param user: The user to send the notice to - """ - send_mail(config_value('EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE'), user.email, - 'change_notice', user=user) - - -def change_user_password(user, password): - """Change the specified user's password - - :param user: The user to change_password - :param password: The unencrypted new password - """ - user.password = encrypt_password(password) - _datastore.put(user) - send_password_changed_notice(user) - password_changed.send(user, app=app._get_current_object()) diff --git a/wqflask/flask_security/confirmable.py b/wqflask/flask_security/confirmable.py deleted file mode 100644 index a7caf6cd..00000000 --- a/wqflask/flask_security/confirmable.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.confirmable - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security confirmable module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from datetime import datetime - -from flask import current_app as app, request -from werkzeug.local import LocalProxy - -from .utils import send_mail, md5, url_for_security, get_token_status,\ - config_value -from .signals import user_confirmed, confirm_instructions_sent - - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def generate_confirmation_link(user): - token = generate_confirmation_token(user) - url = url_for_security('confirm_email', token=token) - return request.url_root[:-1] + url, token - - -def send_confirmation_instructions(user): - """Sends the confirmation instructions email for the specified user. - - :param user: The user to send the instructions to - :param token: The confirmation token - """ - - confirmation_link, token = generate_confirmation_link(user) - - send_mail(config_value('EMAIL_SUBJECT_CONFIRM'), user.email, - 'confirmation_instructions', user=user, - confirmation_link=confirmation_link) - - confirm_instructions_sent.send(user, app=app._get_current_object()) - return token - - -def generate_confirmation_token(user): - """Generates a unique confirmation token for the specified user. - - :param user: The user to work with - """ - data = [str(user.id), md5(user.email)] - return _security.confirm_serializer.dumps(data) - - -def requires_confirmation(user): - """Returns `True` if the user requires confirmation.""" - return _security.confirmable and user.confirmed_at == None - - -def confirm_email_token_status(token): - """Returns the expired status, invalid status, and user of a confirmation - token. For example:: - - expired, invalid, user = confirm_email_token_status('...') - - :param token: The confirmation token - """ - return get_token_status(token, 'confirm', 'CONFIRM_EMAIL') - - -def confirm_user(user): - """Confirms the specified user - - :param user: The user to confirm - """ - user.confirmed_at = datetime.utcnow() - _datastore.put(user) - user_confirmed.send(user, app=app._get_current_object()) diff --git a/wqflask/flask_security/core.py b/wqflask/flask_security/core.py deleted file mode 100644 index 0f3a231f..00000000 --- a/wqflask/flask_security/core.py +++ /dev/null @@ -1,382 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.core - ~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security core module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app -from flask.ext.login import AnonymousUser as AnonymousUserBase, \ - UserMixin as BaseUserMixin, LoginManager, current_user -from flask.ext.principal import Principal, RoleNeed, UserNeed, Identity, \ - identity_loaded -from itsdangerous import URLSafeTimedSerializer -from passlib.context import CryptContext -from werkzeug.datastructures import ImmutableList -from werkzeug.local import LocalProxy - -from .utils import config_value as cv, get_config, md5, url_for_security -from .views import create_blueprint -from .forms import LoginForm, ConfirmRegisterForm, RegisterForm, \ - ForgotPasswordForm, ChangePasswordForm, ResetPasswordForm, \ - SendConfirmationForm, PasswordlessLoginForm - -# Convenient references -_security = LocalProxy(lambda: current_app.extensions['security']) - - -#: Default Flask-Security configuration -_default_config = { - 'BLUEPRINT_NAME': 'security', - 'URL_PREFIX': None, - 'SUBDOMAIN': None, - 'FLASH_MESSAGES': True, - 'PASSWORD_HASH': 'plaintext', - 'PASSWORD_SALT': None, - 'LOGIN_URL': '/login', - 'LOGOUT_URL': '/logout', - 'REGISTER_URL': '/register', - 'RESET_URL': '/reset', - 'CHANGE_URL': '/change', - 'CONFIRM_URL': '/confirm', - 'POST_LOGIN_VIEW': '/', - 'POST_LOGOUT_VIEW': '/', - 'CONFIRM_ERROR_VIEW': None, - 'POST_REGISTER_VIEW': None, - 'POST_CONFIRM_VIEW': None, - 'POST_RESET_VIEW': None, - 'POST_CHANGE_VIEW': None, - 'UNAUTHORIZED_VIEW': None, - 'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html', - 'LOGIN_USER_TEMPLATE': 'security/login_user.html', - 'REGISTER_USER_TEMPLATE': 'security/register_user.html', - 'RESET_PASSWORD_TEMPLATE': 'security/reset_password.html', - 'SEND_CONFIRMATION_TEMPLATE': 'security/send_confirmation.html', - 'SEND_LOGIN_TEMPLATE': 'security/send_login.html', - 'CONFIRMABLE': False, - 'REGISTERABLE': False, - 'RECOVERABLE': False, - 'TRACKABLE': False, - 'PASSWORDLESS': False, - 'CHANGEABLE': False, - 'SEND_REGISTER_EMAIL': True, - 'LOGIN_WITHIN': '1 days', - 'CONFIRM_EMAIL_WITHIN': '5 days', - 'RESET_PASSWORD_WITHIN': '5 days', - 'LOGIN_WITHOUT_CONFIRMATION': False, - 'EMAIL_SENDER': 'no-reply@localhost', - 'TOKEN_AUTHENTICATION_KEY': 'auth_token', - 'TOKEN_AUTHENTICATION_HEADER': 'Authentication-Token', - 'CONFIRM_SALT': 'confirm-salt', - 'RESET_SALT': 'reset-salt', - 'LOGIN_SALT': 'login-salt', - 'CHANGE_SALT': 'change-salt', - 'REMEMBER_SALT': 'remember-salt', - 'DEFAULT_HTTP_AUTH_REALM': 'Login Required', - 'EMAIL_SUBJECT_REGISTER': 'Welcome', - 'EMAIL_SUBJECT_CONFIRM': 'Please confirm your email', - 'EMAIL_SUBJECT_PASSWORDLESS': 'Login instructions', - 'EMAIL_SUBJECT_PASSWORD_NOTICE': 'Your password has been reset', - 'EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE': 'Your password has been changed', - 'EMAIL_SUBJECT_PASSWORD_RESET': 'Password reset instructions' -} - -#: Default Flask-Security messages -_default_messages = { - 'UNAUTHORIZED': ('You do not have permission to view this resource.', 'error'), - 'CONFIRM_REGISTRATION': ('Thank you. Confirmation instructions have been sent to %(email)s.', 'success'), - 'EMAIL_CONFIRMED': ('Thank you. Your email has been confirmed.', 'success'), - 'ALREADY_CONFIRMED': ('Your email has already been confirmed.', 'info'), - 'INVALID_CONFIRMATION_TOKEN': ('Invalid confirmation token.', 'error'), - 'EMAIL_ALREADY_ASSOCIATED': ('%(email)s is already associated with an account.', 'error'), - 'PASSWORD_MISMATCH': ('Password does not match', 'error'), - 'RETYPE_PASSWORD_MISMATCH': ('Passwords do not match', 'error'), - 'INVALID_REDIRECT': ('Redirections outside the domain are forbidden', 'error'), - 'PASSWORD_RESET_REQUEST': ('Instructions to reset your password have been sent to %(email)s.', 'info'), - 'PASSWORD_RESET_EXPIRED': ('You did not reset your password within %(within)s. New instructions have been sent to %(email)s.', 'error'), - 'INVALID_RESET_PASSWORD_TOKEN': ('Invalid reset password token.', 'error'), - 'CONFIRMATION_REQUIRED': ('Email requires confirmation.', 'error'), - 'CONFIRMATION_REQUEST': ('Confirmation instructions have been sent to %(email)s.', 'info'), - 'CONFIRMATION_EXPIRED': ('You did not confirm your email within %(within)s. New instructions to confirm your email have been sent to %(email)s.', 'error'), - 'LOGIN_EXPIRED': ('You did not login within %(within)s. New instructions to login have been sent to %(email)s.', 'error'), - 'LOGIN_EMAIL_SENT': ('Instructions to login have been sent to %(email)s.', 'success'), - 'INVALID_LOGIN_TOKEN': ('Invalid login token.', 'error'), - 'DISABLED_ACCOUNT': ('Account is disabled.', 'error'), - 'EMAIL_NOT_PROVIDED': ('Email not provided', 'error'), - 'INVALID_EMAIL_ADDRESS': ('Invalid email address', 'error'), - 'PASSWORD_NOT_PROVIDED': ('Password not provided', 'error'), - 'USER_DOES_NOT_EXIST': ('Specified user does not exist', 'error'), - 'INVALID_PASSWORD': ('Invalid password', 'error'), - 'PASSWORDLESS_LOGIN_SUCCESSFUL': ('You have successfuly logged in.', 'success'), - 'PASSWORD_RESET': ('You successfully reset your password and you have been logged in automatically.', 'success'), - 'PASSWORD_CHANGE': ('You successfully changed your password.', 'success'), - 'LOGIN': ('Please log in to access this page.', 'info'), - 'REFRESH': ('Please reauthenticate to access this page.', 'info'), -} - -_allowed_password_hash_schemes = [ - 'bcrypt', - 'des_crypt', - 'pbkdf2_sha256', - 'pbkdf2_sha512', - 'sha256_crypt', - 'sha512_crypt', - # And always last one... - 'plaintext' -] - -_default_forms = { - 'login_form': LoginForm, - 'confirm_register_form': ConfirmRegisterForm, - 'register_form': RegisterForm, - 'forgot_password_form': ForgotPasswordForm, - 'reset_password_form': ResetPasswordForm, - 'change_password_form': ChangePasswordForm, - 'send_confirmation_form': SendConfirmationForm, - 'passwordless_login_form': PasswordlessLoginForm, -} - - -def _user_loader(user_id): - return _security.datastore.find_user(id=user_id) - - -def _token_loader(token): - try: - data = _security.remember_token_serializer.loads(token) - user = _security.datastore.find_user(id=data[0]) - if user and md5(user.password) == data[1]: - return user - except: - pass - - return None - - -def _identity_loader(): - if not isinstance(current_user._get_current_object(), AnonymousUser): - identity = Identity(current_user.id) - return identity - - -def _on_identity_loaded(sender, identity): - if hasattr(current_user, 'id'): - identity.provides.add(UserNeed(current_user.id)) - - for role in current_user.roles: - identity.provides.add(RoleNeed(role.name)) - - identity.user = current_user - - -def _get_login_manager(app): - lm = LoginManager() - lm.anonymous_user = AnonymousUser - lm.login_view = '%s.login' % cv('BLUEPRINT_NAME', app=app) - lm.user_loader(_user_loader) - lm.token_loader(_token_loader) - lm.login_message, lm.login_message_category = cv('MSG_LOGIN', app=app) - lm.needs_refresh_message, lm.needs_refresh_message_category = cv('MSG_REFRESH', app=app) - lm.init_app(app) - return lm - - -def _get_principal(app): - p = Principal(app, use_sessions=False) - p.identity_loader(_identity_loader) - return p - - -def _get_pwd_context(app): - pw_hash = cv('PASSWORD_HASH', app=app) - if pw_hash not in _allowed_password_hash_schemes: - allowed = ', '.join(_allowed_password_hash_schemes[:-1]) + ' and ' + _allowed_password_hash_schemes[-1] - raise ValueError("Invalid hash scheme %r. Allowed values are %s" % (pw_hash, allowed)) - return CryptContext(schemes=_allowed_password_hash_schemes, default=pw_hash) - - -def _get_serializer(app, name): - secret_key = app.config.get('SECRET_KEY') - salt = app.config.get('SECURITY_%s_SALT' % name.upper()) - return URLSafeTimedSerializer(secret_key=secret_key, salt=salt) - - -def _get_state(app, datastore, **kwargs): - for key, value in get_config(app).items(): - print "in _get_state [{}]: {}".format(key, value) - kwargs[key.lower()] = value - - kwargs.update(dict( - app=app, - datastore=datastore, - login_manager=_get_login_manager(app), - principal=_get_principal(app), - pwd_context=_get_pwd_context(app), - remember_token_serializer=_get_serializer(app, 'remember'), - login_serializer=_get_serializer(app, 'login'), - reset_serializer=_get_serializer(app, 'reset'), - confirm_serializer=_get_serializer(app, 'confirm'), - _context_processors={}, - _send_mail_task=None - )) - - for key, value in _default_forms.items(): - if key not in kwargs or not kwargs[key]: - kwargs[key] = value - - return _SecurityState(**kwargs) - - -def _context_processor(): - return dict(url_for_security=url_for_security, security=_security) - - -class RoleMixin(object): - """Mixin for `Role` model definitions""" - def __eq__(self, other): - return (self.name == other or \ - self.name == getattr(other, 'name', None)) - - def __ne__(self, other): - return (self.name != other and - self.name != getattr(other, 'name', None)) - - -class UserMixin(BaseUserMixin): - """Mixin for `User` model definitions""" - - def is_active(self): - """Returns `True` if the user is active.""" - return self.active - - def get_auth_token(self): - """Returns the user's authentication token.""" - data = [str(self.id), md5(self.password)] - return _security.remember_token_serializer.dumps(data) - - def has_role(self, role): - """Returns `True` if the user identifies with the specified role. - - :param role: A role name or `Role` instance""" - return role in self.roles - - -class AnonymousUser(AnonymousUserBase): - """AnonymousUser definition""" - - def __init__(self): - super(AnonymousUser, self).__init__() - self.roles = ImmutableList() - - def has_role(self, *args): - """Returns `False`""" - return False - - -class _SecurityState(object): - - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key.lower(), value) - - def _add_ctx_processor(self, endpoint, fn): - group = self._context_processors.setdefault(endpoint, []) - fn not in group and group.append(fn) - - def _run_ctx_processor(self, endpoint): - rv, fns = {}, [] - for g in [None, endpoint]: - for fn in self._context_processors.setdefault(g, []): - rv.update(fn()) - return rv - - def context_processor(self, fn): - self._add_ctx_processor(None, fn) - - def forgot_password_context_processor(self, fn): - self._add_ctx_processor('forgot_password', fn) - - def login_context_processor(self, fn): - self._add_ctx_processor('login', fn) - - def register_context_processor(self, fn): - self._add_ctx_processor('register', fn) - - def reset_password_context_processor(self, fn): - self._add_ctx_processor('reset_password', fn) - - def change_password_context_processor(self, fn): - self._add_ctx_processor('change_password', fn) - - def send_confirmation_context_processor(self, fn): - self._add_ctx_processor('send_confirmation', fn) - - def send_login_context_processor(self, fn): - self._add_ctx_processor('send_login', fn) - - def mail_context_processor(self, fn): - self._add_ctx_processor('mail', fn) - - def send_mail_task(self, fn): - self._send_mail_task = fn - - -class Security(object): - """The :class:`Security` class initializes the Flask-Security extension. - - :param app: The application. - :param datastore: An instance of a user datastore. - """ - def __init__(self, app=None, datastore=None, **kwargs): - self.app = app - self.datastore = datastore - - if app is not None and datastore is not None: - self._state = self.init_app(app, datastore, **kwargs) - - def init_app(self, app, datastore=None, register_blueprint=True, - login_form=None, confirm_register_form=None, - register_form=None, forgot_password_form=None, - reset_password_form=None, change_password_form=None, - send_confirmation_form=None, passwordless_login_form=None): - """Initializes the Flask-Security extension for the specified - application and datastore implentation. - - :param app: The application. - :param datastore: An instance of a user datastore. - :param register_blueprint: to register the Security blueprint or not. - """ - datastore = datastore or self.datastore - - for key, value in _default_config.items(): - app.config.setdefault('SECURITY_' + key, value) - - for key, value in _default_messages.items(): - app.config.setdefault('SECURITY_MSG_' + key, value) - - identity_loaded.connect_via(app)(_on_identity_loaded) - - state = _get_state(app, datastore, - login_form=login_form, - confirm_register_form=confirm_register_form, - register_form=register_form, - forgot_password_form=forgot_password_form, - reset_password_form=reset_password_form, - change_password_form=change_password_form, - send_confirmation_form=send_confirmation_form, - passwordless_login_form=passwordless_login_form) - - if register_blueprint: - app.register_blueprint(create_blueprint(state, __name__)) - app.context_processor(_context_processor) - - app.extensions['security'] = state - - return state - - def __getattr__(self, name): - return getattr(self._state, name, None) diff --git a/wqflask/flask_security/datastore.py b/wqflask/flask_security/datastore.py deleted file mode 100644 index 634399d9..00000000 --- a/wqflask/flask_security/datastore.py +++ /dev/null @@ -1,261 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.datastore - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - This module contains an user datastore classes. - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -class Datastore(object): - def __init__(self, db): - self.db = db - - def commit(self): - pass - - def put(self, model): - raise NotImplementedError - - def delete(self, model): - raise NotImplementedError - - -class SQLAlchemyDatastore(Datastore): - def commit(self): - self.db.session.commit() - - def put(self, model): - self.db.session.add(model) - return model - - def delete(self, model): - self.db.session.delete(model) - - -class MongoEngineDatastore(Datastore): - def put(self, model): - model.save() - return model - - def delete(self, model): - model.delete() - - -class PeeweeDatastore(Datastore): - def put(self, model): - model.save() - return model - - def delete(self, model): - model.delete_instance() - - -class UserDatastore(object): - """Abstracted user datastore. - - :param user_model: A user model class definition - :param role_model: A role model class definition - """ - - def __init__(self, user_model, role_model): - self.user_model = user_model - self.role_model = role_model - - def _prepare_role_modify_args(self, user, role): - if isinstance(user, basestring): - user = self.find_user(email=user) - if isinstance(role, basestring): - role = self.find_role(role) - return user, role - - def _prepare_create_user_args(self, **kwargs): - kwargs.setdefault('active', True) - roles = kwargs.get('roles', []) - for i, role in enumerate(roles): - rn = role.name if isinstance(role, self.role_model) else role - # see if the role exists - roles[i] = self.find_role(rn) - kwargs['roles'] = roles - return kwargs - - def find_user(self, *args, **kwargs): - """Returns a user matching the provided parameters.""" - raise NotImplementedError - - def find_role(self, *args, **kwargs): - """Returns a role matching the provided name.""" - raise NotImplementedError - - def add_role_to_user(self, user, role): - """Adds a role tp a user - - :param user: The user to manipulate - :param role: The role to add to the user - """ - rv = False - user, role = self._prepare_role_modify_args(user, role) - if role not in user.roles: - rv = True - user.roles.append(role) - return rv - - def remove_role_from_user(self, user, role): - """Removes a role from a user - - :param user: The user to manipulate - :param role: The role to remove from the user - """ - rv = False - user, role = self._prepare_role_modify_args(user, role) - if role in user.roles: - rv = True - user.roles.remove(role) - return rv - - def toggle_active(self, user): - """Toggles a user's active status. Always returns True.""" - user.active = not user.active - return True - - def deactivate_user(self, user): - """Deactivates a specified user. Returns `True` if a change was made. - - :param user: The user to deactivate - """ - if user.active: - user.active = False - return True - return False - - def activate_user(self, user): - """Activates a specified user. Returns `True` if a change was made. - - :param user: The user to activate - """ - if not user.active: - user.active = True - return True - return False - - def create_role(self, **kwargs): - """Creates and returns a new role from the given parameters.""" - - role = self.role_model(**kwargs) - return self.put(role) - - def find_or_create_role(self, name, **kwargs): - """Returns a role matching the given name or creates it with any - additionally provided parameters - """ - kwargs["name"] = name - return self.find_role(name) or self.create_role(**kwargs) - - def create_user(self, **kwargs): - """Creates and returns a new user from the given parameters.""" - - user = self.user_model(**self._prepare_create_user_args(**kwargs)) - print "in abstraced create_user, user is:", user - return self.put(user) - - def delete_user(self, user): - """Delete the specified user - - :param user: The user to delete - """ - self.delete(user) - - -class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore): - """A SQLAlchemy datastore implementation for Flask-Security that assumes the - use of the Flask-SQLAlchemy extension. - """ - def __init__(self, db, user_model, role_model): - SQLAlchemyDatastore.__init__(self, db) - UserDatastore.__init__(self, user_model, role_model) - - def find_user(self, **kwargs): - return self.user_model.query.filter_by(**kwargs).first() - - def find_role(self, role): - return self.role_model.query.filter_by(name=role).first() - - -class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): - """A MongoEngine datastore implementation for Flask-Security that assumes - the use of the Flask-MongoEngine extension. - """ - def __init__(self, db, user_model, role_model): - MongoEngineDatastore.__init__(self, db) - UserDatastore.__init__(self, user_model, role_model) - - def find_user(self, **kwargs): - return self.user_model.objects(**kwargs).first() - - def find_role(self, role): - return self.role_model.objects(name=role).first() - - -class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): - """A PeeweeD datastore implementation for Flask-Security that assumes - the use of the Flask-Peewee extension. - - :param user_model: A user model class definition - :param role_model: A role model class definition - :param role_link: A model implementing the many-to-many user-role relation - """ - def __init__(self, db, user_model, role_model, role_link): - PeeweeDatastore.__init__(self, db) - UserDatastore.__init__(self, user_model, role_model) - self.UserRole = role_link - - def find_user(self, **kwargs): - try: - return self.user_model.filter(**kwargs).get() - except self.user_model.DoesNotExist: - return None - - def find_role(self, role): - try: - return self.role_model.filter(name=role).get() - except self.role_model.DoesNotExist: - return None - - def create_user(self, **kwargs): - """Creates and returns a new user from the given parameters.""" - roles = kwargs.pop('roles', []) - user = self.user_model(**self._prepare_create_user_args(**kwargs)) - user = self.put(user) - for role in roles: - self.add_role_to_user(user, role) - return user - - - def add_role_to_user(self, user, role): - """Adds a role tp a user - - :param user: The user to manipulate - :param role: The role to add to the user - """ - user, role = self._prepare_role_modify_args(user, role) - if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): - return False - else: - self.UserRole.create(user=user, role=role) - return True - - def remove_role_from_user(self, user, role): - """Removes a role from a user - - :param user: The user to manipulate - :param role: The role to remove from the user - """ - user, role = self._prepare_role_modify_args(user, role) - if self.UserRole.select().where(self.UserRole.user==user, self.UserRole.role==role).count(): - self.UserRole.delete().where(self.UserRole.user==user, self.UserRole.role==role) - return True - else: - return False - diff --git a/wqflask/flask_security/decorators.py b/wqflask/flask_security/decorators.py deleted file mode 100644 index 0ea1105c..00000000 --- a/wqflask/flask_security/decorators.py +++ /dev/null @@ -1,207 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.decorators - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security decorators module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from functools import wraps - -from flask import current_app, Response, request, redirect, _request_ctx_stack -from flask.ext.login import current_user, login_required -from flask.ext.principal import RoleNeed, Permission, Identity, identity_changed -from werkzeug.local import LocalProxy - -from . import utils - - -# Convenient references -_security = LocalProxy(lambda: current_app.extensions['security']) - - -_default_unauthorized_html = """ - <h1>Unauthorized</h1> - <p>The server could not verify that you are authorized to access the URL - requested. You either supplied the wrong credentials (e.g. a bad password), - or your browser doesn't understand how to supply the credentials required.</p> - """ - - -def _get_unauthorized_response(text=None, headers=None): - text = text or _default_unauthorized_html - headers = headers or {} - return Response(text, 401, headers) - - -def _get_unauthorized_view(): - cv = utils.get_url(utils.config_value('UNAUTHORIZED_VIEW')) - utils.do_flash(*utils.get_message('UNAUTHORIZED')) - return redirect(cv or request.referrer or '/') - - -def _check_token(): - header_key = _security.token_authentication_header - args_key = _security.token_authentication_key - header_token = request.headers.get(header_key, None) - token = request.args.get(args_key, header_token) - if request.json: - token = request.json.get(args_key, token) - serializer = _security.remember_token_serializer - - try: - data = serializer.loads(token) - except: - return False - - user = _security.datastore.find_user(id=data[0]) - - if utils.md5(user.password) == data[1]: - app = current_app._get_current_object() - _request_ctx_stack.top.user = user - identity_changed.send(app, identity=Identity(user.id)) - return True - - -def _check_http_auth(): - auth = request.authorization or dict(username=None, password=None) - user = _security.datastore.find_user(email=auth.username) - - if user and utils.verify_and_update_password(auth.password, user): - _security.datastore.commit() - app = current_app._get_current_object() - _request_ctx_stack.top.user = user - identity_changed.send(app, identity=Identity(user.id)) - return True - - return False - - -def http_auth_required(realm): - """Decorator that protects endpoints using Basic HTTP authentication. - The username should be set to the user's email address. - - :param realm: optional realm name""" - - def decorator(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - if _check_http_auth(): - return fn(*args, **kwargs) - r = _security.default_http_auth_realm if callable(realm) else realm - h = {'WWW-Authenticate': 'Basic realm="%s"' % r} - return _get_unauthorized_response(headers=h) - return wrapper - - if callable(realm): - return decorator(realm) - return decorator - - -def auth_token_required(fn): - """Decorator that protects endpoints using token authentication. The token - should be added to the request by the client by using a query string - variable with a name equal to the configuration value of - `SECURITY_TOKEN_AUTHENTICATION_KEY` or in a request header named that of - the configuration value of `SECURITY_TOKEN_AUTHENTICATION_HEADER` - """ - - @wraps(fn) - def decorated(*args, **kwargs): - if _check_token(): - return fn(*args, **kwargs) - return _get_unauthorized_response() - return decorated - - -def auth_required(*auth_methods): - """ - Decorator that protects enpoints through multiple mechanisms - Example:: - - @app.route('/dashboard') - @auth_required('token', 'session') - def dashboard(): - return 'Dashboard' - - :param auth_methods: Specified mechanisms. - """ - login_mechanisms = { - 'token': lambda: _check_token(), - 'basic': lambda: _check_http_auth(), - 'session': lambda: current_user.is_authenticated() - } - - def wrapper(fn): - @wraps(fn) - def decorated_view(*args, **kwargs): - mechanisms = [login_mechanisms.get(method) for method in auth_methods] - for mechanism in mechanisms: - if mechanism and mechanism(): - return fn(*args, **kwargs) - return _get_unauthorized_response() - return decorated_view - return wrapper - - -def roles_required(*roles): - """Decorator which specifies that a user must have all the specified roles. - Example:: - - @app.route('/dashboard') - @roles_required('admin', 'editor') - def dashboard(): - return 'Dashboard' - - The current user must have both the `admin` role and `editor` role in order - to view the page. - - :param args: The required roles. - """ - def wrapper(fn): - @wraps(fn) - def decorated_view(*args, **kwargs): - perms = [Permission(RoleNeed(role)) for role in roles] - for perm in perms: - if not perm.can(): - return _get_unauthorized_view() - return fn(*args, **kwargs) - return decorated_view - return wrapper - - -def roles_accepted(*roles): - """Decorator which specifies that a user must have at least one of the - specified roles. Example:: - - @app.route('/create_post') - @roles_accepted('editor', 'author') - def create_post(): - return 'Create Post' - - The current user must have either the `editor` role or `author` role in - order to view the page. - - :param args: The possible roles. - """ - def wrapper(fn): - @wraps(fn) - def decorated_view(*args, **kwargs): - perm = Permission(*[RoleNeed(role) for role in roles]) - if perm.can(): - return fn(*args, **kwargs) - return _get_unauthorized_view() - return decorated_view - return wrapper - - -def anonymous_user_required(f): - @wraps(f) - def wrapper(*args, **kwargs): - if current_user.is_authenticated(): - return redirect(utils.get_url(_security.post_login_view)) - return f(*args, **kwargs) - return wrapper diff --git a/wqflask/flask_security/forms.py b/wqflask/flask_security/forms.py deleted file mode 100644 index 54677e77..00000000 --- a/wqflask/flask_security/forms.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.forms - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security forms module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -import inspect -import urlparse - -import flask_wtf as wtf - -from flask import request, current_app -from flask_wtf import Form as BaseForm, TextField, PasswordField, \ - SubmitField, HiddenField, BooleanField, ValidationError, Field -from flask_login import current_user -from werkzeug.local import LocalProxy - -from .confirmable import requires_confirmation -from .utils import verify_and_update_password, get_message - -# Convenient reference -_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) - -_default_field_labels = { - 'email': 'Email Address', - 'password': 'Password', - 'remember_me': 'Remember Me', - 'login': 'Login', - 'retype_password': 'Retype Password', - 'register': 'Register', - 'send_confirmation': 'Resend Confirmation Instructions', - 'recover_password': 'Recover Password', - 'reset_password': 'Reset Password', - 'retype_password': 'Retype Password', - 'new_password': 'New Password', - 'change_password': 'Change Password', - 'send_login_link': 'Send Login Link' -} - - -class ValidatorMixin(object): - def __call__(self, form, field): - if self.message and self.message.isupper(): - self.message = get_message(self.message)[0] - return super(ValidatorMixin, self).__call__(form, field) - - -class EqualTo(ValidatorMixin, wtf.EqualTo): - pass - - -class Required(ValidatorMixin, wtf.Required): - pass - - -class Email(ValidatorMixin, wtf.Email): - pass - - -class Length(ValidatorMixin, wtf.Length): - pass - - -email_required = Required(message='EMAIL_NOT_PROVIDED') -email_validator = Email(message='INVALID_EMAIL_ADDRESS') -password_required = Required(message='PASSWORD_NOT_PROVIDED') - - -def get_form_field_label(key): - return _default_field_labels.get(key, '') - - -def unique_user_email(form, field): - if _datastore.find_user(email=field.data) is not None: - msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0] - raise ValidationError(msg) - - -def valid_user_email(form, field): - form.user = _datastore.find_user(email=field.data) - if form.user is None: - raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0]) - - -class Form(BaseForm): - def __init__(self, *args, **kwargs): - #print "importing tracer" - #from wqflask import tracer - #tracer.turn_on() - #print "imported tracer" - print "in Form, args:", args - print "in Form, kwargs:", kwargs - if current_app.testing: - self.TIME_LIMIT = None - super(Form, self).__init__(*args, **kwargs) - - -class EmailFormMixin(): - email = TextField(get_form_field_label('email'), - validators=[email_required, - email_validator]) - - -class UserEmailFormMixin(): - user = None - email = TextField(get_form_field_label('email'), - validators=[email_required, - email_validator, - valid_user_email]) - - -class UniqueEmailFormMixin(): - email = TextField(get_form_field_label('email'), - validators=[email_required, - email_validator, - unique_user_email]) - - -class PasswordFormMixin(): - password = PasswordField(get_form_field_label('password'), - validators=[password_required]) - - -class NewPasswordFormMixin(): - password = PasswordField(get_form_field_label('password'), - validators=[password_required, - Length(min=6, max=128)]) - - -class PasswordConfirmFormMixin(): - password_confirm = PasswordField( - get_form_field_label('retype_password'), - validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH')]) - - -class NextFormMixin(): - next = HiddenField() - - def validate_next(self, field): - url_next = urlparse.urlsplit(field.data) - url_base = urlparse.urlsplit(request.host_url) - if url_next.netloc and url_next.netloc != url_base.netloc: - field.data = '' - raise ValidationError(get_message('INVALID_REDIRECT')[0]) - - -class RegisterFormMixin(): - submit = SubmitField(get_form_field_label('register')) - - def to_dict(form): - def is_field_and_user_attr(member): - print "in ifaua:", member - return isinstance(member, Field) and \ - hasattr(_datastore.user_model, member.name) - - print("green:", vars(form)) - fields = inspect.getmembers(form, is_field_and_user_attr) - print("fields:" ,vars(form)) - return dict((key, value.data) for key, value in fields) - - -class SendConfirmationForm(Form, UserEmailFormMixin): - """The default forgot password form""" - - submit = SubmitField(get_form_field_label('send_confirmation')) - - def __init__(self, *args, **kwargs): - super(SendConfirmationForm, self).__init__(*args, **kwargs) - if request.method == 'GET': - self.email.data = request.args.get('email', None) - - def validate(self): - if not super(SendConfirmationForm, self).validate(): - return False - if self.user.confirmed_at is not None: - self.email.errors.append(get_message('ALREADY_CONFIRMED')[0]) - return False - return True - - -class ForgotPasswordForm(Form, UserEmailFormMixin): - """The default forgot password form""" - - submit = SubmitField(get_form_field_label('recover_password')) - - -class PasswordlessLoginForm(Form, UserEmailFormMixin): - """The passwordless login form""" - - submit = SubmitField(get_form_field_label('send_login_link')) - - def __init__(self, *args, **kwargs): - super(PasswordlessLoginForm, self).__init__(*args, **kwargs) - - def validate(self): - if not super(PasswordlessLoginForm, self).validate(): - return False - if not self.user.is_active(): - self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) - return False - return True - - -class LoginForm(Form, NextFormMixin): - """The default login form""" - - email = TextField(get_form_field_label('email')) - password = PasswordField(get_form_field_label('password')) - remember = BooleanField(get_form_field_label('remember_me')) - submit = SubmitField(get_form_field_label('login')) - - def __init__(self, *args, **kwargs): - super(LoginForm, self).__init__(*args, **kwargs) - - def validate(self): - if not super(LoginForm, self).validate(): - return False - - if self.email.data.strip() == '': - self.email.errors.append(get_message('EMAIL_NOT_PROVIDED')[0]) - return False - - if self.password.data.strip() == '': - self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) - return False - - self.user = _datastore.find_user(email=self.email.data) - - if self.user is None: - self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) - return False - if not verify_and_update_password(self.password.data, self.user): - self.password.errors.append(get_message('INVALID_PASSWORD')[0]) - return False - if requires_confirmation(self.user): - self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) - return False - if not self.user.is_active(): - self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) - return False - return True - - -class ConfirmRegisterForm(Form, RegisterFormMixin, - UniqueEmailFormMixin, NewPasswordFormMixin): - pass - - -class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin): - pass - - -class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): - """The default reset password form""" - - submit = SubmitField(get_form_field_label('reset_password')) - - -class ChangePasswordForm(Form, PasswordFormMixin): - """The default change password form""" - - new_password = PasswordField(get_form_field_label('new_password'), - validators=[password_required, - Length(min=6, max=128)]) - - new_password_confirm = PasswordField(get_form_field_label('retype_password'), - validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH')]) - - submit = SubmitField(get_form_field_label('change_password')) - - def validate(self): - if not super(ChangePasswordForm, self).validate(): - return False - - if self.password.data.strip() == '': - self.password.errors.append(get_message('PASSWORD_NOT_PROVIDED')[0]) - return False - if not verify_and_update_password(self.password.data, current_user): - self.password.errors.append(get_message('INVALID_PASSWORD')[0]) - return False - return True diff --git a/wqflask/flask_security/passwordless.py b/wqflask/flask_security/passwordless.py deleted file mode 100644 index b0accb2c..00000000 --- a/wqflask/flask_security/passwordless.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.passwordless - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security passwordless module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import request, current_app as app -from werkzeug.local import LocalProxy - -from .signals import login_instructions_sent -from .utils import send_mail, url_for_security, get_token_status, \ - config_value - - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def send_login_instructions(user): - """Sends the login instructions email for the specified user. - - :param user: The user to send the instructions to - :param token: The login token - """ - token = generate_login_token(user) - url = url_for_security('token_login', token=token) - login_link = request.url_root[:-1] + url - - send_mail(config_value('EMAIL_SUBJECT_PASSWORDLESS'), user.email, - 'login_instructions', user=user, login_link=login_link) - - login_instructions_sent.send(dict(user=user, login_token=token), - app=app._get_current_object()) - - -def generate_login_token(user): - """Generates a unique login token for the specified user. - - :param user: The user the token belongs to - """ - return _security.login_serializer.dumps([str(user.id)]) - - -def login_token_status(token): - """Returns the expired status, invalid status, and user of a login token. - For example:: - - expired, invalid, user = login_token_status('...') - - :param token: The login token - """ - return get_token_status(token, 'login', 'LOGIN') diff --git a/wqflask/flask_security/recoverable.py b/wqflask/flask_security/recoverable.py deleted file mode 100644 index 6aafc111..00000000 --- a/wqflask/flask_security/recoverable.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.recoverable - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security recoverable module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app as app, request -from werkzeug.local import LocalProxy - -from .signals import password_reset, reset_password_instructions_sent -from .utils import send_mail, md5, encrypt_password, url_for_security, \ - get_token_status, config_value - - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def send_reset_password_instructions(user): - """Sends the reset password instructions email for the specified user. - - :param user: The user to send the instructions to - """ - token = generate_reset_password_token(user) - url = url_for_security('reset_password', token=token) - reset_link = request.url_root[:-1] + url - - send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email, - 'reset_instructions', - user=user, reset_link=reset_link) - - reset_password_instructions_sent.send(dict(user=user, token=token), - app=app._get_current_object()) - - -def send_password_reset_notice(user): - """Sends the password reset notice email for the specified user. - - :param user: The user to send the notice to - """ - send_mail(config_value('EMAIL_SUBJECT_PASSWORD_NOTICE'), user.email, - 'reset_notice', user=user) - - -def generate_reset_password_token(user): - """Generates a unique reset password token for the specified user. - - :param user: The user to work with - """ - data = [str(user.id), md5(user.password)] - return _security.reset_serializer.dumps(data) - - -def reset_password_token_status(token): - """Returns the expired status, invalid status, and user of a password reset - token. For example:: - - expired, invalid, user = reset_password_token_status('...') - - :param token: The password reset token - """ - return get_token_status(token, 'reset', 'RESET_PASSWORD') - -def update_password(user, password): - """Update the specified user's password - - :param user: The user to update_password - :param password: The unencrypted new password - """ - user.password = encrypt_password(password) - _datastore.put(user) - send_password_reset_notice(user) - password_reset.send(user, app=app._get_current_object()) diff --git a/wqflask/flask_security/registerable.py b/wqflask/flask_security/registerable.py deleted file mode 100644 index 4606c7c6..00000000 --- a/wqflask/flask_security/registerable.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.registerable - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security registerable module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app as app -from werkzeug.local import LocalProxy - -from .confirmable import generate_confirmation_link -from .signals import user_registered -from .utils import do_flash, get_message, send_mail, encrypt_password, \ - config_value - -# Convenient references -_security = LocalProxy(lambda: app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def register_user(**kwargs): - print "in register_user kwargs:", kwargs - confirmation_link, token = None, None - kwargs['password'] = encrypt_password(kwargs['password']) - user = _datastore.create_user(**kwargs) - _datastore.commit() - - if _security.confirmable: - confirmation_link, token = generate_confirmation_link(user) - do_flash(*get_message('CONFIRM_REGISTRATION', email=user.email)) - - user_registered.send(dict(user=user, confirm_token=token), - app=app._get_current_object()) - - if config_value('SEND_REGISTER_EMAIL'): - send_mail(config_value('EMAIL_SUBJECT_REGISTER'), user.email, 'welcome', - user=user, confirmation_link=confirmation_link) - - return user diff --git a/wqflask/flask_security/script.py b/wqflask/flask_security/script.py deleted file mode 100644 index 9c9a2469..00000000 --- a/wqflask/flask_security/script.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.script - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security script module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" -try: - import simplejson as json -except ImportError: - import json - -import re - -from flask import current_app -from flask.ext.script import Command, Option -from werkzeug.local import LocalProxy - -from .utils import encrypt_password - - -_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) - - -def pprint(obj): - print json.dumps(obj, sort_keys=True, indent=4) - - -def commit(fn): - def wrapper(*args, **kwargs): - fn(*args, **kwargs) - _datastore.commit() - return wrapper - - -class CreateUserCommand(Command): - """Create a user""" - - option_list = ( - Option('-e', '--email', dest='email', default=None), - Option('-p', '--password', dest='password', default=None), - Option('-a', '--active', dest='active', default=''), - ) - - @commit - def run(self, **kwargs): - # sanitize active input - ai = re.sub(r'\s', '', str(kwargs['active'])) - kwargs['active'] = ai.lower() in ['', 'y', 'yes', '1', 'active'] - - from flask_security.forms import ConfirmRegisterForm - from werkzeug.datastructures import MultiDict - - form = ConfirmRegisterForm(MultiDict(kwargs), csrf_enabled=False) - - if form.validate(): - kwargs['password'] = encrypt_password(kwargs['password']) - _datastore.create_user(**kwargs) - print 'User created successfully.' - kwargs['password'] = '****' - pprint(kwargs) - else: - print 'Error creating user' - pprint(form.errors) - - -class CreateRoleCommand(Command): - """Create a role""" - - option_list = ( - Option('-n', '--name', dest='name', default=None), - Option('-d', '--desc', dest='description', default=None), - ) - - @commit - def run(self, **kwargs): - _datastore.create_role(**kwargs) - print 'Role "%(name)s" created successfully.' % kwargs - - -class _RoleCommand(Command): - option_list = ( - Option('-u', '--user', dest='user_identifier'), - Option('-r', '--role', dest='role_name'), - ) - - -class AddRoleCommand(_RoleCommand): - """Add a role to a user""" - - @commit - def run(self, user_identifier, role_name): - _datastore.add_role_to_user(user_identifier, role_name) - print "Role '%s' added to user '%s' successfully" % (role_name, user_identifier) - - -class RemoveRoleCommand(_RoleCommand): - """Add a role to a user""" - - @commit - def run(self, user_identifier, role_name): - _datastore.remove_role_from_user(user_identifier, role_name) - print "Role '%s' removed from user '%s' successfully" % (role_name, user_identifier) - - -class _ToggleActiveCommand(Command): - option_list = ( - Option('-u', '--user', dest='user_identifier'), - ) - - -class DeactivateUserCommand(_ToggleActiveCommand): - """Deactive a user""" - - @commit - def run(self, user_identifier): - _datastore.deactivate_user(user_identifier) - print "User '%s' has been deactivated" % user_identifier - - -class ActivateUserCommand(_ToggleActiveCommand): - """Deactive a user""" - - @commit - def run(self, user_identifier): - _datastore.activate_user(user_identifier) - print "User '%s' has been activated" % user_identifier diff --git a/wqflask/flask_security/signals.py b/wqflask/flask_security/signals.py deleted file mode 100644 index e1c29548..00000000 --- a/wqflask/flask_security/signals.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.signals - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security signals module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -import blinker - - -signals = blinker.Namespace() - -user_registered = signals.signal("user-registered") - -user_confirmed = signals.signal("user-confirmed") - -confirm_instructions_sent = signals.signal("confirm-instructions-sent") - -login_instructions_sent = signals.signal("login-instructions-sent") - -password_reset = signals.signal("password-reset") - -password_changed = signals.signal("password-changed") - -reset_password_instructions_sent = signals.signal("password-reset-instructions-sent") diff --git a/wqflask/flask_security/templates/.DS_Store b/wqflask/flask_security/templates/.DS_Store Binary files differdeleted file mode 100644 index b72f1d98..00000000 --- a/wqflask/flask_security/templates/.DS_Store +++ /dev/null diff --git a/wqflask/flask_security/templates/security/.DS_Store b/wqflask/flask_security/templates/security/.DS_Store Binary files differdeleted file mode 100644 index 5008ddfc..00000000 --- a/wqflask/flask_security/templates/security/.DS_Store +++ /dev/null diff --git a/wqflask/flask_security/templates/security/_macros.html b/wqflask/flask_security/templates/security/_macros.html deleted file mode 100644 index 8575f3db..00000000 --- a/wqflask/flask_security/templates/security/_macros.html +++ /dev/null @@ -1,16 +0,0 @@ -{% macro render_field_with_errors(field) %} - <p> - {{ field.label }} {{ field(**kwargs)|safe }} - {% if field.errors %} - <ul> - {% for error in field.errors %} - <li>{{ error }}</li> - {% endfor %} - </ul> - {% endif %} - </p> -{% endmacro %} - -{% macro render_field(field) %} - <p>{{ field(**kwargs)|safe }}</p> -{% endmacro %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/_menu.html b/wqflask/flask_security/templates/security/_menu.html deleted file mode 100644 index 5291f809..00000000 --- a/wqflask/flask_security/templates/security/_menu.html +++ /dev/null @@ -1,15 +0,0 @@ -{% if security.registerable or security.recoverable or security.confirmabled %} -<h2>Menu</h2> -<ul> - <li><a href="{{ url_for_security('login') }}">Login</a></li> - {% if security.registerable %} - <li><a href="{{ url_for_security('register') }}">Register</a><br/></li> - {% endif %} - {% if security.recoverable %} - <li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li> - {% endif %} - {% if security.confirmable %} - <li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li> - {% endif %} -</ul> -{% endif %} diff --git a/wqflask/flask_security/templates/security/_messages.html b/wqflask/flask_security/templates/security/_messages.html deleted file mode 100644 index 179d0636..00000000 --- a/wqflask/flask_security/templates/security/_messages.html +++ /dev/null @@ -1,9 +0,0 @@ -{%- with messages = get_flashed_messages(with_categories=true) -%} - {% if messages %} - <ul class="flashes"> - {% for category, message in messages %} - <li class="{{ category }}">{{ message }}</li> - {% endfor %} - </ul> - {% endif %} -{%- endwith %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/change_password.html b/wqflask/flask_security/templates/security/change_password.html deleted file mode 100644 index 8ee3eb73..00000000 --- a/wqflask/flask_security/templates/security/change_password.html +++ /dev/null @@ -1,11 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Change password</h1> -<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form"> - {{ change_password_form.hidden_tag() }} - {{ render_field_with_errors(change_password_form.password) }} - {{ render_field_with_errors(change_password_form.new_password) }} - {{ render_field_with_errors(change_password_form.new_password_confirm) }} - {{ render_field(change_password_form.submit) }} -</form> - diff --git a/wqflask/flask_security/templates/security/email/change_notice.html b/wqflask/flask_security/templates/security/email/change_notice.html deleted file mode 100644 index d1224cf5..00000000 --- a/wqflask/flask_security/templates/security/email/change_notice.html +++ /dev/null @@ -1,4 +0,0 @@ -<p>Your password has been changed.</p> -{% if security.recoverable %} -<p>If you did not change your password, <a href="{{ url_for_security('forgot_password', _external=True) }}">click here to reset it</a>.</p> -{% endif %} diff --git a/wqflask/flask_security/templates/security/email/change_notice.txt b/wqflask/flask_security/templates/security/email/change_notice.txt deleted file mode 100644 index e74bd80d..00000000 --- a/wqflask/flask_security/templates/security/email/change_notice.txt +++ /dev/null @@ -1,5 +0,0 @@ -Your password has been changed -{% if security.recoverable %} -If you did not change your password, click the link below to reset it. -{{ url_for_security('forgot_password', _external=True) }} -{% endif %} diff --git a/wqflask/flask_security/templates/security/email/confirmation_instructions.html b/wqflask/flask_security/templates/security/email/confirmation_instructions.html deleted file mode 100644 index 5082a9a8..00000000 --- a/wqflask/flask_security/templates/security/email/confirmation_instructions.html +++ /dev/null @@ -1,3 +0,0 @@ -<p>Please confirm your email through the link below:</p> - -<p><a href="{{ confirmation_link }}">Confirm my account</a></p>
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/confirmation_instructions.txt b/wqflask/flask_security/templates/security/email/confirmation_instructions.txt deleted file mode 100644 index fb435b55..00000000 --- a/wqflask/flask_security/templates/security/email/confirmation_instructions.txt +++ /dev/null @@ -1,3 +0,0 @@ -Please confirm your email through the link below: - -{{ confirmation_link }}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/login_instructions.html b/wqflask/flask_security/templates/security/email/login_instructions.html deleted file mode 100644 index 45a7cb57..00000000 --- a/wqflask/flask_security/templates/security/email/login_instructions.html +++ /dev/null @@ -1,5 +0,0 @@ -<p>Welcome {{ user.email }}!</p> - -<p>You can log into your through the link below:</p> - -<p><a href="{{ login_link }}">Login now</a></p>
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/login_instructions.txt b/wqflask/flask_security/templates/security/email/login_instructions.txt deleted file mode 100644 index 1364ed65..00000000 --- a/wqflask/flask_security/templates/security/email/login_instructions.txt +++ /dev/null @@ -1,5 +0,0 @@ -Welcome {{ user.email }}! - -You can log into your through the link below: - -{{ login_link }}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/reset_instructions.html b/wqflask/flask_security/templates/security/email/reset_instructions.html deleted file mode 100644 index fd0b48d8..00000000 --- a/wqflask/flask_security/templates/security/email/reset_instructions.html +++ /dev/null @@ -1 +0,0 @@ -<p><a href="{{ reset_link }}">Click here to reset your password</a></p>
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/reset_instructions.txt b/wqflask/flask_security/templates/security/email/reset_instructions.txt deleted file mode 100644 index 91ac288e..00000000 --- a/wqflask/flask_security/templates/security/email/reset_instructions.txt +++ /dev/null @@ -1,3 +0,0 @@ -Click the link below to reset your password: - -{{ reset_link }}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/reset_notice.html b/wqflask/flask_security/templates/security/email/reset_notice.html deleted file mode 100644 index 536e2961..00000000 --- a/wqflask/flask_security/templates/security/email/reset_notice.html +++ /dev/null @@ -1 +0,0 @@ -<p>Your password has been reset</p>
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/reset_notice.txt b/wqflask/flask_security/templates/security/email/reset_notice.txt deleted file mode 100644 index a3fa0b4b..00000000 --- a/wqflask/flask_security/templates/security/email/reset_notice.txt +++ /dev/null @@ -1 +0,0 @@ -Your password has been reset
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/welcome.html b/wqflask/flask_security/templates/security/email/welcome.html deleted file mode 100644 index 55eaed61..00000000 --- a/wqflask/flask_security/templates/security/email/welcome.html +++ /dev/null @@ -1,7 +0,0 @@ -<p>Welcome {{ user.email }}!</p> - -{% if security.confirmable %} -<p>You can confirm your email through the link below:</p> - -<p><a href="{{ confirmation_link }}">Confirm my account</a></p> -{% endif %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/email/welcome.txt b/wqflask/flask_security/templates/security/email/welcome.txt deleted file mode 100644 index fb6ee5b5..00000000 --- a/wqflask/flask_security/templates/security/email/welcome.txt +++ /dev/null @@ -1,7 +0,0 @@ -Welcome {{ user.email }}! - -{% if security.confirmable %} -You can confirm your email through the link below: - -{{ confirmation_link }} -{% endif %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/forgot_password.html b/wqflask/flask_security/templates/security/forgot_password.html deleted file mode 100644 index 90fcaf66..00000000 --- a/wqflask/flask_security/templates/security/forgot_password.html +++ /dev/null @@ -1,9 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Send password reset instructions</h1> -<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form"> - {{ forgot_password_form.hidden_tag() }} - {{ render_field_with_errors(forgot_password_form.email) }} - {{ render_field(forgot_password_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/login_user.html b/wqflask/flask_security/templates/security/login_user.html deleted file mode 100644 index d781ce08..00000000 --- a/wqflask/flask_security/templates/security/login_user.html +++ /dev/null @@ -1,12 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Login</h1> -<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> - {{ login_user_form.hidden_tag() }} - {{ render_field_with_errors(login_user_form.email) }} - {{ render_field_with_errors(login_user_form.password) }} - {{ render_field_with_errors(login_user_form.remember) }} - {{ render_field(login_user_form.next) }} - {{ render_field(login_user_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/register_user.html b/wqflask/flask_security/templates/security/register_user.html deleted file mode 100644 index 87cf9b1d..00000000 --- a/wqflask/flask_security/templates/security/register_user.html +++ /dev/null @@ -1,13 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Register</h1> -<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form"> - {{ register_user_form.hidden_tag() }} - {{ render_field_with_errors(register_user_form.email) }} - {{ render_field_with_errors(register_user_form.password) }} - {% if register_user_form.password_confirm %} - {{ render_field_with_errors(register_user_form.password_confirm) }} - {% endif %} - {{ render_field(register_user_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/reset_password.html b/wqflask/flask_security/templates/security/reset_password.html deleted file mode 100644 index e6fc3f58..00000000 --- a/wqflask/flask_security/templates/security/reset_password.html +++ /dev/null @@ -1,10 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Reset password</h1> -<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form"> - {{ reset_password_form.hidden_tag() }} - {{ render_field_with_errors(reset_password_form.password) }} - {{ render_field_with_errors(reset_password_form.password_confirm) }} - {{ render_field(reset_password_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/send_confirmation.html b/wqflask/flask_security/templates/security/send_confirmation.html deleted file mode 100644 index 3e828407..00000000 --- a/wqflask/flask_security/templates/security/send_confirmation.html +++ /dev/null @@ -1,9 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Resend confirmation instructions</h1> -<form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form"> - {{ send_confirmation_form.hidden_tag() }} - {{ render_field_with_errors(send_confirmation_form.email) }} - {{ render_field(send_confirmation_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/templates/security/send_login.html b/wqflask/flask_security/templates/security/send_login.html deleted file mode 100644 index 15611c57..00000000 --- a/wqflask/flask_security/templates/security/send_login.html +++ /dev/null @@ -1,9 +0,0 @@ -{% from "security/_macros.html" import render_field_with_errors, render_field %} -{% include "security/_messages.html" %} -<h1>Login</h1> -<form action="{{ url_for_security('login') }}" method="POST" name="send_login_form"> - {{ send_login_form.hidden_tag() }} - {{ render_field_with_errors(send_login_form.email) }} - {{ render_field(send_login_form.submit) }} -</form> -{% include "security/_menu.html" %}
\ No newline at end of file diff --git a/wqflask/flask_security/utils.py b/wqflask/flask_security/utils.py deleted file mode 100644 index 7397ab4f..00000000 --- a/wqflask/flask_security/utils.py +++ /dev/null @@ -1,379 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.utils - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security utils module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -import base64 -import blinker -import functools -import hashlib -import hmac -from contextlib import contextmanager -from datetime import datetime, timedelta - -from flask import url_for, flash, current_app, request, session, render_template -from flask.ext.login import login_user as _login_user, \ - logout_user as _logout_user -from flask.ext.mail import Message -from flask.ext.principal import Identity, AnonymousIdentity, identity_changed -from itsdangerous import BadSignature, SignatureExpired -from werkzeug.local import LocalProxy - -from .signals import user_registered, user_confirmed, \ - confirm_instructions_sent, login_instructions_sent, \ - password_reset, password_changed, reset_password_instructions_sent - -# Convenient references -_security = LocalProxy(lambda: current_app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - -_pwd_context = LocalProxy(lambda: _security.pwd_context) - - -def login_user(user, remember=True): - """Performs the login and sends the appropriate signal.""" - - if not _login_user(user, remember): - return False - - if _security.trackable: - old_current_login, new_current_login = user.current_login_at, datetime.utcnow() - remote_addr = request.remote_addr or 'untrackable' - old_current_ip, new_current_ip = user.current_login_ip, remote_addr - - user.last_login_at = old_current_login or new_current_login - user.current_login_at = new_current_login - user.last_login_ip = old_current_ip or new_current_ip - user.current_login_ip = new_current_ip - user.login_count = user.login_count + 1 if user.login_count else 1 - - _datastore.put(user) - - identity_changed.send(current_app._get_current_object(), - identity=Identity(user.id)) - return True - - -def logout_user(): - for key in ('identity.name', 'identity.auth_type'): - session.pop(key, None) - identity_changed.send(current_app._get_current_object(), - identity=AnonymousIdentity()) - _logout_user() - - -def get_hmac(password): - if _security.password_hash == 'plaintext': - return password - - if _security.password_salt is None: - raise RuntimeError('The configuration value `SECURITY_PASSWORD_SALT` ' - 'must not be None when the value of `SECURITY_PASSWORD_HASH` is ' - 'set to "%s"' % _security.password_hash) - - h = hmac.new(_security.password_salt, password.encode('utf-8'), hashlib.sha512) - return base64.b64encode(h.digest()) - - -def verify_password(password, password_hash): - return _pwd_context.verify(get_hmac(password), password_hash) - - -def verify_and_update_password(password, user): - verified, new_password = _pwd_context.verify_and_update(get_hmac(password), user.password) - if verified and new_password: - user.password = new_password - _datastore.put(user) - return verified - - -def encrypt_password(password): - return _pwd_context.encrypt(get_hmac(password)) - - -def md5(data): - return hashlib.md5(data).hexdigest() - - -def do_flash(message, category=None): - """Flash a message depending on if the `FLASH_MESSAGES` configuration - value is set. - - :param message: The flash message - :param category: The flash message category - """ - if config_value('FLASH_MESSAGES'): - flash(message, category) - - -def get_url(endpoint_or_url): - """Returns a URL if a valid endpoint is found. Otherwise, returns the - provided value. - - :param endpoint_or_url: The endpoint name or URL to default to - """ - try: - return url_for(endpoint_or_url) - except: - return endpoint_or_url - - -def get_security_endpoint_name(endpoint): - return '%s.%s' % (_security.blueprint_name, endpoint) - - -def url_for_security(endpoint, **values): - """Return a URL for the security blueprint - - :param endpoint: the endpoint of the URL (name of the function) - :param values: the variable arguments of the URL rule - :param _external: if set to `True`, an absolute URL is generated. Server - address can be changed via `SERVER_NAME` configuration variable which - defaults to `localhost`. - :param _anchor: if provided this is added as anchor to the URL. - :param _method: if provided this explicitly specifies an HTTP method. - """ - endpoint = get_security_endpoint_name(endpoint) - return url_for(endpoint, **values) - - -def get_post_login_redirect(): - """Returns the URL to redirect to after a user logs in successfully.""" - return (get_url(request.args.get('next')) or - get_url(request.form.get('next')) or - find_redirect('SECURITY_POST_LOGIN_VIEW')) - - -def find_redirect(key): - """Returns the URL to redirect to after a user logs in successfully. - - :param key: The session or application configuration key to search for - """ - rv = (get_url(session.pop(key.lower(), None)) or - get_url(current_app.config[key.upper()] or None) or '/') - return rv - - -def get_config(app): - """Conveniently get the security configuration for the specified - application without the annoying 'SECURITY_' prefix. - - :param app: The application to inspect - """ - items = app.config.items() - prefix = 'SECURITY_' - - def strip_prefix(tup): - return (tup[0].replace('SECURITY_', ''), tup[1]) - - return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)]) - - -def get_message(key, **kwargs): - rv = config_value('MSG_' + key) - return rv[0] % kwargs, rv[1] - - -def config_value(key, app=None, default=None): - """Get a Flask-Security configuration value. - - :param key: The configuration key without the prefix `SECURITY_` - :param app: An optional specific application to inspect. Defaults to Flask's - `current_app` - :param default: An optional default value if the value is not set - """ - app = app or current_app - return get_config(app).get(key.upper(), default) - - -def get_max_age(key, app=None): - now = datetime.utcnow() - expires = now + get_within_delta(key + '_WITHIN', app) - td = (expires - now) - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 - - -def get_within_delta(key, app=None): - """Get a timedelta object from the application configuration following - the internal convention of:: - - <Amount of Units> <Type of Units> - - Examples of valid config values:: - - 5 days - 10 minutes - - :param key: The config value key without the 'SECURITY_' prefix - :param app: Optional application to inspect. Defaults to Flask's - `current_app` - """ - txt = config_value(key, app=app) - values = txt.split() - return timedelta(**{values[1]: int(values[0])}) - - -def send_mail(subject, recipient, template, **context): - """Send an email via the Flask-Mail extension. - - :param subject: Email subject - :param recipient: Email recipient - :param template: The name of the email template - :param context: The context to render the template with - """ - - context.setdefault('security', _security) - context.update(_security._run_ctx_processor('mail')) - - msg = Message(subject, - sender=_security.email_sender, - recipients=[recipient]) - - ctx = ('security/email', template) - msg.body = render_template('%s/%s.txt' % ctx, **context) - msg.html = render_template('%s/%s.html' % ctx, **context) - - if _security._send_mail_task: - _security._send_mail_task(msg) - return - - mail = current_app.extensions.get('mail') - mail.send(msg) - - -def get_token_status(token, serializer, max_age=None): - serializer = getattr(_security, serializer + '_serializer') - max_age = get_max_age(max_age) - user, data = None, None - expired, invalid = False, False - - try: - data = serializer.loads(token, max_age=max_age) - except SignatureExpired: - d, data = serializer.loads_unsafe(token) - expired = True - except BadSignature: - invalid = True - - if data: - user = _datastore.find_user(id=data[0]) - - expired = expired and (user is not None) - return expired, invalid, user - - -@contextmanager -def capture_passwordless_login_requests(): - login_requests = [] - - def _on(data, app): - login_requests.append(data) - - login_instructions_sent.connect(_on) - - try: - yield login_requests - finally: - login_instructions_sent.disconnect(_on) - - -@contextmanager -def capture_registrations(): - """Testing utility for capturing registrations. - - :param confirmation_sent_at: An optional datetime object to set the - user's `confirmation_sent_at` to - """ - registrations = [] - - def _on(data, app): - registrations.append(data) - - user_registered.connect(_on) - - try: - yield registrations - finally: - user_registered.disconnect(_on) - - -@contextmanager -def capture_reset_password_requests(reset_password_sent_at=None): - """Testing utility for capturing password reset requests. - - :param reset_password_sent_at: An optional datetime object to set the - user's `reset_password_sent_at` to - """ - reset_requests = [] - - def _on(request, app): - reset_requests.append(request) - - reset_password_instructions_sent.connect(_on) - - try: - yield reset_requests - finally: - reset_password_instructions_sent.disconnect(_on) - - -class CaptureSignals(object): - """Testing utility for capturing blinker signals. - - Context manager which mocks out selected signals and registers which are `sent` on and what - arguments were sent. Instantiate with a list of blinker `NamedSignals` to patch. Each signal - has it's `send` mocked out. - """ - def __init__(self, signals): - """Patch all given signals and make them available as attributes. - - :param signals: list of signals - """ - self._records = {} - self._receivers = {} - for signal in signals: - self._records[signal] = [] - self._receivers[signal] = functools.partial(self._record, signal) - - def __getitem__(self, signal): - """All captured signals are available via `ctxt[signal]`. - """ - if isinstance(signal, blinker.base.NamedSignal): - return self._records[signal] - else: - super(CaptureSignals, self).__setitem__(signal) - - def _record(self, signal, *args, **kwargs): - self._records[signal].append((args, kwargs)) - - def __enter__(self): - for signal, receiver in self._receivers.iteritems(): - signal.connect(receiver) - return self - - def __exit__(self, type, value, traceback): - for signal, receiver in self._receivers.iteritems(): - signal.disconnect(receiver) - - def signals_sent(self): - """Return a set of the signals sent. - :rtype: list of blinker `NamedSignals`. - """ - return set([signal for signal, _ in self._records.iteritems() if self._records[signal]]) - - -def capture_signals(): - """Factory method that creates a `CaptureSignals` with all the flask_security signals.""" - return CaptureSignals([user_registered, user_confirmed, - confirm_instructions_sent, login_instructions_sent, - password_reset, password_changed, - reset_password_instructions_sent]) - - diff --git a/wqflask/flask_security/views.py b/wqflask/flask_security/views.py deleted file mode 100644 index 1b8488d8..00000000 --- a/wqflask/flask_security/views.py +++ /dev/null @@ -1,359 +0,0 @@ -# -*- coding: utf-8 -*- -""" - flask.ext.security.views - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Flask-Security views module - - :copyright: (c) 2012 by Matt Wright. - :license: MIT, see LICENSE for more details. -""" - -from flask import current_app, redirect, request, render_template, jsonify, \ - after_this_request, Blueprint -from flask_login import current_user -from werkzeug.datastructures import MultiDict -from werkzeug.local import LocalProxy - -from .confirmable import send_confirmation_instructions, \ - confirm_user, confirm_email_token_status -from .decorators import login_required, anonymous_user_required -from .passwordless import send_login_instructions, \ - login_token_status -from .recoverable import reset_password_token_status, \ - send_reset_password_instructions, update_password -from .changeable import change_user_password -from .registerable import register_user -from .utils import get_url, get_post_login_redirect, do_flash, \ - get_message, login_user, logout_user, url_for_security as url_for, \ - config_value - - -# Convenient references -_security = LocalProxy(lambda: current_app.extensions['security']) - -_datastore = LocalProxy(lambda: _security.datastore) - - -def _render_json(form, include_auth_token=False): - has_errors = len(form.errors) > 0 - - if has_errors: - code = 400 - response = dict(errors=form.errors) - else: - code = 200 - response = dict(user=dict(id=str(form.user.id))) - if include_auth_token: - token = form.user.get_auth_token() - response['user']['authentication_token'] = token - - return jsonify(dict(meta=dict(code=code), response=response)) - - -def _commit(response=None): - _datastore.commit() - return response - - -def _ctx(endpoint): - return _security._run_ctx_processor(endpoint) - - -@anonymous_user_required -def login(): - """View function for login view""" - - form_class = _security.login_form - - if request.json: - form = form_class(MultiDict(request.json)) - else: - form = form_class() - - if form.validate_on_submit(): - login_user(form.user, remember=form.remember.data) - after_this_request(_commit) - - if not request.json: - return redirect(get_post_login_redirect()) - - form.next.data = get_url(request.args.get('next')) \ - or get_url(request.form.get('next')) or '' - - if request.json: - return _render_json(form, True) - - return render_template(config_value('LOGIN_USER_TEMPLATE'), - login_user_form=form, - **_ctx('login')) - - -@login_required -def logout(): - """View function which handles a logout request.""" - - logout_user() - - return redirect(request.args.get('next', None) or - get_url(_security.post_logout_view)) - - -def register(): - """View function which handles a registration request.""" - - if _security.confirmable or request.json: - form_class = _security.confirm_register_form - else: - form_class = _security.register_form - - if request.json: - form_data = MultiDict(request.json) - else: - form_data = request.form - - form = form_class(form_data) - - if form.validate_on_submit(): - user = register_user(**form.to_dict()) - form.user = user - - if not _security.confirmable or _security.login_without_confirmation: - after_this_request(_commit) - login_user(user) - - if not request.json: - post_register_url = get_url(_security.post_register_view) - post_login_url = get_url(_security.post_login_view) - return redirect(post_register_url or post_login_url) - - if request.json: - return _render_json(form) - - return render_template(config_value('REGISTER_USER_TEMPLATE'), - register_user_form=form, - **_ctx('register')) - - -def send_login(): - """View function that sends login instructions for passwordless login""" - - form_class = _security.passwordless_login_form - - if request.json: - form = form_class(MultiDict(request.json)) - else: - form = form_class() - - if form.validate_on_submit(): - send_login_instructions(form.user) - if request.json is None: - do_flash(*get_message('LOGIN_EMAIL_SENT', email=form.user.email)) - - if request.json: - return _render_json(form) - - return render_template(config_value('SEND_LOGIN_TEMPLATE'), - send_login_form=form, - **_ctx('send_login')) - - -@anonymous_user_required -def token_login(token): - """View function that handles passwordless login via a token""" - - expired, invalid, user = login_token_status(token) - - if invalid: - do_flash(*get_message('INVALID_LOGIN_TOKEN')) - if expired: - send_login_instructions(user) - do_flash(*get_message('LOGIN_EXPIRED', email=user.email, - within=_security.login_within)) - if invalid or expired: - return redirect(url_for('login')) - - login_user(user, True) - after_this_request(_commit) - do_flash(*get_message('PASSWORDLESS_LOGIN_SUCCESSFUL')) - - return redirect(get_post_login_redirect()) - - -def send_confirmation(): - """View function which sends confirmation instructions.""" - - form_class = _security.send_confirmation_form - - if request.json: - form = form_class(MultiDict(request.json)) - else: - form = form_class() - - if form.validate_on_submit(): - send_confirmation_instructions(form.user) - if request.json is None: - do_flash(*get_message('CONFIRMATION_REQUEST', email=form.user.email)) - - if request.json: - return _render_json(form) - - return render_template(config_value('SEND_CONFIRMATION_TEMPLATE'), - send_confirmation_form=form, - **_ctx('send_confirmation')) - - -@anonymous_user_required -def confirm_email(token): - """View function which handles a email confirmation request.""" - - expired, invalid, user = confirm_email_token_status(token) - - if not user or invalid: - invalid = True - do_flash(*get_message('INVALID_CONFIRMATION_TOKEN')) - if expired: - send_confirmation_instructions(user) - do_flash(*get_message('CONFIRMATION_EXPIRED', email=user.email, - within=_security.confirm_email_within)) - if invalid or expired: - return redirect(get_url(_security.confirm_error_view) or - url_for('send_confirmation')) - - confirm_user(user) - login_user(user, True) - after_this_request(_commit) - do_flash(*get_message('EMAIL_CONFIRMED')) - - return redirect(get_url(_security.post_confirm_view) or - get_url(_security.post_login_view)) - - -def forgot_password(): - """View function that handles a forgotten password request.""" - - form_class = _security.forgot_password_form - - if request.json: - form = form_class(MultiDict(request.json)) - else: - form = form_class() - - if form.validate_on_submit(): - send_reset_password_instructions(form.user) - if request.json is None: - do_flash(*get_message('PASSWORD_RESET_REQUEST', email=form.user.email)) - - if request.json: - return _render_json(form) - - return render_template(config_value('FORGOT_PASSWORD_TEMPLATE'), - forgot_password_form=form, - **_ctx('forgot_password')) - - -@anonymous_user_required -def reset_password(token): - """View function that handles a reset password request.""" - - expired, invalid, user = reset_password_token_status(token) - - if invalid: - do_flash(*get_message('INVALID_RESET_PASSWORD_TOKEN')) - if expired: - do_flash(*get_message('PASSWORD_RESET_EXPIRED', email=user.email, - within=_security.reset_password_within)) - if invalid or expired: - return redirect(url_for('forgot_password')) - - form = _security.reset_password_form() - - if form.validate_on_submit(): - after_this_request(_commit) - update_password(user, form.password.data) - do_flash(*get_message('PASSWORD_RESET')) - login_user(user, True) - return redirect(get_url(_security.post_reset_view) or - get_url(_security.post_login_view)) - - return render_template(config_value('RESET_PASSWORD_TEMPLATE'), - reset_password_form=form, - reset_password_token=token, - **_ctx('reset_password')) - - -@login_required -def change_password(): - """View function which handles a change password request.""" - - form_class = _security.change_password_form - - if request.json: - form = form_class(MultiDict(request.json)) - else: - form = form_class() - - if form.validate_on_submit(): - after_this_request(_commit) - change_user_password(current_user, form.new_password.data) - if request.json is None: - do_flash(*get_message('PASSWORD_CHANGE')) - return redirect(get_url(_security.post_change_view) or - get_url(_security.post_login_view)) - - if request.json: - return _render_json(form) - - return render_template('security/change_password.html', - change_password_form=form, - **_ctx('change_password')) - - -def create_blueprint(state, import_name): - """Creates the security extension blueprint""" - - bp = Blueprint(state.blueprint_name, import_name, - url_prefix=state.url_prefix, - subdomain=state.subdomain, - template_folder='templates') - - bp.route(state.logout_url, endpoint='logout')(logout) - - if state.passwordless: - bp.route(state.login_url, - methods=['GET', 'POST'], - endpoint='login')(send_login) - bp.route(state.login_url + '/<token>', - endpoint='token_login')(token_login) - else: - bp.route(state.login_url, - methods=['GET', 'POST'], - endpoint='login')(login) - - if state.registerable: - bp.route(state.register_url, - methods=['GET', 'POST'], - endpoint='register')(register) - - if state.recoverable: - bp.route(state.reset_url, - methods=['GET', 'POST'], - endpoint='forgot_password')(forgot_password) - bp.route(state.reset_url + '/<token>', - methods=['GET', 'POST'], - endpoint='reset_password')(reset_password) - - if state.changeable: - bp.route(state.change_url, - methods=['GET', 'POST'], - endpoint='change_password')(change_password) - - if state.confirmable: - bp.route(state.confirm_url, - methods=['GET', 'POST'], - endpoint='send_confirmation')(send_confirmation) - bp.route(state.confirm_url + '/<token>', - methods=['GET', 'POST'], - endpoint='confirm_email')(confirm_email) - - return bp diff --git a/wqflask/mock/__init__.py b/wqflask/mock/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/wqflask/mock/__init__.py diff --git a/wqflask/mock/es_double.py b/wqflask/mock/es_double.py new file mode 100644 index 00000000..6ef8a1b9 --- /dev/null +++ b/wqflask/mock/es_double.py @@ -0,0 +1,15 @@ +class ESDouble(object): + def __init__(self): + self.items = {} + + def ping(self): + return true + + def create(self, index, doc_type, body, id): + self.items["index"] = {doc_type: {"id": id, "_source": data}} + + def search(self, index, doc_type, body): + return { + "hits": { + "hits": self.items[index][doc_type][body] + }} diff --git a/wqflask/run_gunicorn.py b/wqflask/run_gunicorn.py index 14a2d689..adffdca3 100644 --- a/wqflask/run_gunicorn.py +++ b/wqflask/run_gunicorn.py @@ -7,9 +7,12 @@ # from flask import Flask # application = Flask(__name__) -print "Starting up Gunicorn process" +print "===> Starting up Gunicorn process" from wqflask import app +from utility.startup_config import app_config + +app_config() @app.route("/gunicorn") def hello(): diff --git a/wqflask/runserver.py b/wqflask/runserver.py index a0c76e51..5f41d04d 100644 --- a/wqflask/runserver.py +++ b/wqflask/runserver.py @@ -21,22 +21,9 @@ GREEN = '\033[92m' BOLD = '\033[1m' ENDC = '\033[0m' -import os -app.config['SECRET_KEY'] = os.urandom(24) +from utility.startup_config import app_config -from utility.tools import WEBSERVER_MODE,get_setting_int,get_setting,get_setting_bool - -port = get_setting_int("SERVER_PORT") - -print("GN2 API server URL is ["+BLUE+get_setting("GN_SERVER_URL")+ENDC+"]") - -if get_setting_bool("USE_GN_SERVER"): - import requests - page = requests.get(get_setting("GN_SERVER_URL")) - if page.status_code != 200: - raise Exception("API server not found!") - -print("GN2 is running. Visit %s[http://localhost:%s/%s](%s)" % (BLUE,str(port),ENDC,get_setting("WEBSERVER_URL"))) +app_config() werkzeug_logger = logging.getLogger('werkzeug') diff --git a/wqflask/utility/elasticsearch_tools.py b/wqflask/utility/elasticsearch_tools.py new file mode 100644 index 00000000..734379f7 --- /dev/null +++ b/wqflask/utility/elasticsearch_tools.py @@ -0,0 +1,59 @@ +from elasticsearch import Elasticsearch, TransportError +import logging + +from utility.logger import getLogger +logger = getLogger(__name__) + +from utility.tools import ELASTICSEARCH_HOST, ELASTICSEARCH_PORT + +def test_elasticsearch_connection(): + es = Elasticsearch(['http://'+ELASTICSEARCH_HOST+":"+ELASTICSEARCH_PORT+'/'], verify_certs=True) + if not es.ping(): + logger.warning("Elasticsearch is DOWN") + +def get_elasticsearch_connection(): + logger.info("get_elasticsearch_connection") + es = None + try: + assert(ELASTICSEARCH_HOST) + assert(ELASTICSEARCH_PORT) + logger.info("ES HOST",ELASTICSEARCH_HOST) + + es = Elasticsearch([{ + "host": ELASTICSEARCH_HOST + , "port": ELASTICSEARCH_PORT + }]) if (ELASTICSEARCH_HOST and ELASTICSEARCH_PORT) else None + + es_logger = logging.getLogger("elasticsearch") + es_logger.setLevel(logging.INFO) + es_logger.addHandler(logging.NullHandler()) + except: + es = None + + return es + +def get_user_by_unique_column(es, column_name, column_value, index="users", doc_type="local"): + return get_item_by_unique_column(es, column_name, column_value, index=index, doc_type=doc_type) + +def save_user(es, user, user_id): + es_save_data(es, "users", "local", user, user_id) + +def get_item_by_unique_column(es, column_name, column_value, index, doc_type): + item_details = None + try: + response = es.search( + index = index + , doc_type = doc_type + , body = { + "query": { "match": { column_name: column_value } } + }) + if len(response["hits"]["hits"]) > 0: + item_details = response["hits"]["hits"][0]["_source"] + except TransportError as te: + pass + return item_details + +def es_save_data(es, index, doc_type, data_item, data_id,): + from time import sleep + es.create(index, doc_type, body=data_item, id=data_id) + sleep(1) # Delay 1 second to allow indexing diff --git a/wqflask/utility/startup_config.py b/wqflask/utility/startup_config.py new file mode 100644 index 00000000..5a62cc50 --- /dev/null +++ b/wqflask/utility/startup_config.py @@ -0,0 +1,39 @@ + +from wqflask import app +from utility.tools import WEBSERVER_MODE, show_settings, get_setting_int, get_setting, get_setting_bool + +import utility.logger +logger = utility.logger.getLogger(__name__ ) + +BLUE = '\033[94m' +GREEN = '\033[92m' +BOLD = '\033[1m' +ENDC = '\033[0m' + +def app_config(): + app.config['SESSION_TYPE'] = 'filesystem' + if not app.config.get('SECRET_KEY'): + import os + app.config['SECRET_KEY'] = str(os.urandom(24)) + + mode = WEBSERVER_MODE + if mode == "DEV" or mode == "DEBUG": + app.config['TEMPLATES_AUTO_RELOAD'] = True + # if mode == "DEBUG": + # app.config['EXPLAIN_TEMPLATE_LOADING'] = True <--- use overriding app param instead + print("==========================================") + show_settings() + + port = get_setting_int("SERVER_PORT") + + if get_setting_bool("USE_GN_SERVER"): + print("GN2 API server URL is ["+BLUE+get_setting("GN_SERVER_URL")+ENDC+"]") + import requests + page = requests.get(get_setting("GN_SERVER_URL")) + if page.status_code != 200: + raise Exception("API server not found!") + + import utility.elasticsearch_tools as es + es.test_elasticsearch_connection() + + print("GN2 is running. Visit %s[http://localhost:%s/%s](%s)" % (BLUE,str(port),ENDC,get_setting("WEBSERVER_URL"))) diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py index d3113302..59bb49d8 100644 --- a/wqflask/utility/tools.py +++ b/wqflask/utility/tools.py @@ -54,7 +54,7 @@ def get_setting(command_id,guess=None): # print("Looking for "+command_id+"\n") command = value(os.environ.get(command_id)) if command is None or command == "": - command = OVERRIDES.get(command_id) + command = OVERRIDES.get(command_id) # currently not in use if command is None: # ---- Check whether setting exists in app command = value(app.config.get(command_id)) @@ -220,7 +220,7 @@ def show_settings(): logger.info(OVERRIDES) logger.info(BLUE+"Mr. Mojo Risin 2"+ENDC) - print "runserver.py: ****** Webserver configuration ******" + print "runserver.py: ****** Webserver configuration - k,v pairs from app.config ******" keylist = app.config.keys() keylist.sort() for k in keylist: @@ -251,6 +251,31 @@ assert_dir(JS_GUIX_PATH) JS_GN_PATH = get_setting('JS_GN_PATH') # assert_dir(JS_GN_PATH) +GITHUB_CLIENT_ID = get_setting('GITHUB_CLIENT_ID') +GITHUB_CLIENT_SECRET = get_setting('GITHUB_CLIENT_SECRET') +GITHUB_AUTH_URL = None +if GITHUB_CLIENT_ID != 'UNKNOWN' and GITHUB_CLIENT_SECRET: + GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize?client_id=" + \ + GITHUB_CLIENT_ID+"&client_secret="+GITHUB_CLIENT_SECRET + GITHUB_API_URL = get_setting('GITHUB_API_URL') + +ORCID_CLIENT_ID = get_setting('ORCID_CLIENT_ID') +ORCID_CLIENT_SECRET = get_setting('ORCID_CLIENT_SECRET') +ORCID_AUTH_URL = None +if ORCID_CLIENT_ID != 'UNKNOWN' and ORCID_CLIENT_SECRET: + ORCID_AUTH_URL = "https://sandbox.orcid.org/oauth/authorize?response_type=code&scope=/authenticate&show_login=true&client_id=" + \ + ORCID_CLIENT_ID+"&client_secret="+ORCID_CLIENT_SECRET + ORCID_TOKEN_URL = get_setting('ORCID_TOKEN_URL') + +ELASTICSEARCH_HOST = get_setting('ELASTICSEARCH_HOST') +ELASTICSEARCH_PORT = get_setting('ELASTICSEARCH_PORT') +import utility.elasticsearch_tools as es +es.test_elasticsearch_connection() + +SMTP_CONNECT = get_setting('SMTP_CONNECT') +SMTP_USERNAME = get_setting('SMTP_USERNAME') +SMTP_PASSWORD = get_setting('SMTP_PASSWORD') + PYLMM_COMMAND = app_set("PYLMM_COMMAND",pylmm_command()) GEMMA_COMMAND = app_set("GEMMA_COMMAND",gemma_command()) assert(GEMMA_COMMAND is not None) @@ -265,18 +290,5 @@ assert_dir(JS_TWITTER_POST_FETCHER_PATH) from six import string_types -if os.environ.get('WQFLASK_OVERRIDES'): - jsonfn = get_setting('WQFLASK_OVERRIDES') - logger.info("WQFLASK_OVERRIDES: %s" % jsonfn) - with open(jsonfn) as data_file: - overrides = json.load(data_file) - for k in overrides: - cmd = overrides[k] - if isinstance(cmd, string_types): - OVERRIDES[k] = eval(cmd) - else: - OVERRIDES[k] = cmd - logger.debug(OVERRIDES) - # assert_file(PHEWAS_FILES+"/auwerx/PheWAS_pval_EMMA_norm.RData") assert_file(JS_TWITTER_POST_FETCHER_PATH+"/js/twitterFetcher_min.js") diff --git a/wqflask/utility/type_checking.py b/wqflask/utility/type_checking.py new file mode 100644 index 00000000..220e5f62 --- /dev/null +++ b/wqflask/utility/type_checking.py @@ -0,0 +1,42 @@ +# Type checking functions + +def is_float(value): + try: + float(value) + return True + except: + return False + +def is_int(value): + try: + int(value) + return True + except: + return False + +def is_str(value): + if value is None: + return False + try: + str(value) + return True + except: + return False + +def get_float(vars,name,default=None): + if name in vars: + if is_float(vars[name]): + return float(vars[name]) + return None + +def get_int(vars,name,default=None): + if name in vars: + if is_int(vars[name]): + return float(vars[name]) + return default + +def get_string(vars,name,default=None): + if name in vars: + if not vars[name] is None: + return str(vars[name]) + return default diff --git a/wqflask/wqflask/__init__.py b/wqflask/wqflask/__init__.py index 2188ce17..bc8e9900 100644 --- a/wqflask/wqflask/__init__.py +++ b/wqflask/wqflask/__init__.py @@ -13,8 +13,8 @@ logging.basicConfig(level=logging.INFO) app = Flask(__name__) app.config.from_object('cfg.default_settings') # Get the defaults from cfg.default_settings -app.config.from_envvar('WQFLASK_SETTINGS') # See http://flask.pocoo.org/docs/config/#configuring-from-files -# Note we also use WQFLASK_OVERRIDES +app.config.from_envvar('GN2_SETTINGS') # See http://flask.pocoo.org/docs/config/#configuring-from-files +# Note no longer use the badly named WQFLASK_OVERRIDES (nyi) app.jinja_env.globals.update( undefined = jinja2.StrictUndefined, diff --git a/wqflask/wqflask/correlation/show_corr_results.py b/wqflask/wqflask/correlation/show_corr_results.py index 9b048346..2c6c3a14 100644 --- a/wqflask/wqflask/correlation/show_corr_results.py +++ b/wqflask/wqflask/correlation/show_corr_results.py @@ -52,6 +52,7 @@ import utility.webqtlUtil #this is for parallel computing only. from wqflask.correlation import correlation_functions from utility.benchmark import Bench import utility.webqtlUtil +from utility.type_checking import is_float, is_int, is_str, get_float, get_int, get_string from wqflask import user_manager from MySQLdb import escape_string as escape @@ -77,47 +78,6 @@ def print_mem(stage=""): mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss #print("{}: {}".format(stage, mem/1024)) -def is_float(value): - try: - float(value) - return True - except: - return False - -def is_int(value): - try: - int(value) - return True - except: - return False - -def is_str(value): - if value is None: - return False - try: - str(value) - return True - except: - return False - -def get_float(vars,name,default=None): - if name in vars: - if is_float(vars[name]): - return float(vars[name]) - return None - -def get_int(vars,name,default=None): - if name in vars: - if is_int(vars[name]): - return float(vars[name]) - return default - -def get_string(vars,name,default=None): - if name in vars: - if not vars[name] is None: - return str(vars[name]) - return default - class AuthException(Exception): pass diff --git a/wqflask/wqflask/gsearch.py b/wqflask/wqflask/gsearch.py index e33e04e1..fe1e17d2 100644 --- a/wqflask/wqflask/gsearch.py +++ b/wqflask/wqflask/gsearch.py @@ -5,6 +5,7 @@ from base.data_set import create_dataset from base.trait import GeneralTrait from db import webqtlDatabaseFunction +from utility.type_checking import is_float, is_int, is_str, get_float, get_int, get_string from utility.benchmark import Bench from utility.logger import getLogger @@ -13,8 +14,13 @@ logger = getLogger(__name__) class GSearch(object): def __init__(self, kw): + assert('type' in kw) + assert('terms' in kw) + self.type = kw['type'] self.terms = kw['terms'] + assert(is_str(self.type)) + if self.type == "gene": sql = """ SELECT diff --git a/wqflask/wqflask/marker_regression/marker_regression.py b/wqflask/wqflask/marker_regression/marker_regression.py index db4f8f46..3ec61e55 100644 --- a/wqflask/wqflask/marker_regression/marker_regression.py +++ b/wqflask/wqflask/marker_regression/marker_regression.py @@ -114,7 +114,7 @@ class MarkerRegression(object): self.num_perm = 0 self.perm_output = [] self.bootstrap_results = [] - self.covariates = start_vars['covariates'] + self.covariates = start_vars['covariates'] if "covariates" in start_vars else None #ZS: This is passed to GN1 code for single chr mapping self.selected_chr = -1 diff --git a/wqflask/wqflask/search_results.py b/wqflask/wqflask/search_results.py index 59e100d8..53c96591 100644 --- a/wqflask/wqflask/search_results.py +++ b/wqflask/wqflask/search_results.py @@ -10,6 +10,7 @@ import time import math import datetime import collections +import re from pprint import pformat as pf @@ -25,6 +26,7 @@ from db import webqtlDatabaseFunction from flask import render_template from utility import formatting +from utility.type_checking import is_float, is_int, is_str, get_float, get_int, get_string from utility.logger import getLogger logger = getLogger(__name__ ) @@ -64,14 +66,26 @@ views.py). else: self.and_or = "and" self.search_terms = kw['search_terms_and'] - self.search_term_exists = True + search = self.search_terms + # check for dodgy search terms + rx = re.compile(r'.*\W(href|http|sql|select|update)\W.*',re.IGNORECASE) + if rx.match(search): + logger.info("Regex failed search") + self.search_term_exists = False + return + else: + self.search_term_exists = True + self.results = [] - if kw['type'] == "Phenotypes": # split datatype on type field + type = kw.get('type') + if type == "Phenotypes": # split datatype on type field dataset_type = "Publish" - elif kw['type'] == "Genotypes": + elif type == "Genotypes": dataset_type = "Geno" else: dataset_type = "ProbeSet" # ProbeSet is default + + assert(is_str(kw.get('dataset'))) self.dataset = create_dataset(kw['dataset'], dataset_type) logger.debug("search_terms:", self.search_terms) self.search() @@ -145,6 +159,7 @@ statement and executes else: combined_where_clause += "OR" else: + logger.debug("Search failed 1") self.search_term_exists = False if self.search_term_exists: combined_where_clause = "(" + combined_where_clause + ")" @@ -155,6 +170,7 @@ statement and executes else: logger.debug("len(search_terms)<=1") if self.search_terms == []: + logger.debug("Search failed 2") self.search_term_exists = False else: for a_search in self.search_terms: @@ -162,6 +178,7 @@ statement and executes if the_search != None: self.results.extend(the_search.run()) else: + logger.debug("Search failed 3") self.search_term_exists = False if self.search_term_exists: diff --git a/wqflask/wqflask/templates/new_security/login_user.html b/wqflask/wqflask/templates/new_security/login_user.html index b9f49a61..949760b6 100644 --- a/wqflask/wqflask/templates/new_security/login_user.html +++ b/wqflask/wqflask/templates/new_security/login_user.html @@ -15,15 +15,39 @@ <h4>Don't have an account?</h4> - - <a href="/n/register" class="btn btn-primary modalize">Create a new account</a> - - - <hr /> + {% if es_server: %} + <a href="/n/register" class="btn btn-primary modalize">Create a new account</a> + {% else: %} + <div class="alert alert-warning"> + <p>You cannot create an account at this moment.<br /> + Please try again later.</p> + </div> + {% endif %} + + <hr /> + <h4>Login with external services</h4> + + {% if external_login: %} + <div> + {% if external_login["github"]: %} + <a href="{{external_login['github']}}" title="Login with GitHub" class="btn btn-info btn-group">Login with Github</a> + {% endif %} + + {% if external_login["orcid"]: %} + <a href="{{external_login['orcid']}}" title="Login with ORCID" class="btn btn-info btn-group">Login with ORCID</a> + {% endif %} + </div> + {% else: %} + <div class="alert alert-warning"> + <p>You cannot login with external services at this time.<br /> + Please try again later.</p> + </div> + {% endif %} + <hr /> <h4>Already have an account? Sign in here.</h4> - + {% if es_server: %} <form class="form-horizontal" action="/n/login" method="POST" name="login_user_form" id="loginUserForm"> <fieldset> <div class="form-group"> @@ -61,6 +85,19 @@ </fieldset> </form> + {% else: %} + <div class="alert alert-warning"> + <p>You cannot login at this moment using your GeneNetwork account (the authentication service is down).<br /> + Please try again later.</p> + </div> + {% endif %} + {% if not es_server and not external_login: %} + <hr> + <div class="alert alert-warning"> + Note: it is safe to use GeneNetwork without a login. Login is only required for keeping track of + collections and getting access to some types of restricted data. + </div> + {% endif %} </div> </div> diff --git a/wqflask/wqflask/user_manager.py b/wqflask/wqflask/user_manager.py index f7fcd2d0..c55649f3 100644 --- a/wqflask/wqflask/user_manager.py +++ b/wqflask/wqflask/user_manager.py @@ -54,6 +54,9 @@ logger = getLogger(__name__) from base.data_set import create_datasets_list +import requests +from utility.elasticsearch_tools import get_elasticsearch_connection, get_user_by_unique_column, save_user + THREE_DAYS = 60 * 60 * 24 * 3 #THREE_DAYS = 45 @@ -119,7 +122,8 @@ class AnonUser(object): return collections def import_traits_to_user(self): - collections_list = json.loads(Redis.get(self.key)) + result = Redis.get(self.key) + collections_list = json.loads(result if result else "[]") for collection in collections_list: uc = model.UserCollection() uc.name = collection['name'] @@ -268,16 +272,24 @@ class RegisterUser(object): self.thank_you_mode = False self.errors = [] self.user = Bunch() + es = kw.get('es_connection', None) + + if not es: + self.errors.append("Missing connection object") - self.user.email_address = kw.get('email_address', '').strip() + self.user.email_address = kw.get('email_address', '').encode("utf-8").strip() if not (5 <= len(self.user.email_address) <= 50): self.errors.append('Email Address needs to be between 5 and 50 characters.') + else: + email_exists = get_user_by_unique_column(es, "email_address", self.user.email_address) + if email_exists: + self.errors.append('User already exists with that email') - self.user.full_name = kw.get('full_name', '').strip() + self.user.full_name = kw.get('full_name', '').encode("utf-8").strip() if not (5 <= len(self.user.full_name) <= 50): self.errors.append('Full Name needs to be between 5 and 50 characters.') - self.user.organization = kw.get('organization', '').strip() + self.user.organization = kw.get('organization', '').encode("utf-8").strip() if self.user.organization and not (5 <= len(self.user.organization) <= 50): self.errors.append('Organization needs to be empty or between 5 and 50 characters.') @@ -294,28 +306,11 @@ class RegisterUser(object): logger.debug("No errors!") set_password(password, self.user) + self.user.user_id = str(uuid.uuid4()) + self.user.confirmed = 1 self.user.registration_info = json.dumps(basic_info(), sort_keys=True) - - self.new_user = model.User(**self.user.__dict__) - db_session.add(self.new_user) - - try: - db_session.commit() - except sqlalchemy.exc.IntegrityError: - # This exception is thrown if the email address is already in the database - # To do: Perhaps put a link to sign in using an existing account here - self.errors.append("An account with this email address already exists. " - "Click the button above to sign in using an existing account.") - return - - logger.debug("Adding verification email to queue") - #self.send_email_verification() - VerificationEmail(self.new_user) - logger.debug("Added verification email to queue") - - self.thank_you_mode = True - + save_user(es, self.user.__dict__, self.user.user_id) def set_password(password, user): pwfields = Bunch() @@ -361,7 +356,7 @@ class VerificationEmail(object): verification_code = str(uuid.uuid4()) key = self.key_prefix + ":" + verification_code - data = json.dumps(dict(id=user.id, + data = json.dumps(dict(id=user.user_id, timestamp=timestamp()) ) @@ -378,23 +373,33 @@ class ForgotPasswordEmail(VerificationEmail): template_name = "email/forgot_password.txt" key_prefix = "forgot_password_code" subject = "GeneNetwork password reset" + fromaddr = "no-reply@genenetwork.org" - def __init__(self, user): + def __init__(self, toaddr): + from email.MIMEMultipart import MIMEMultipart + from email.MIMEText import MIMEText verification_code = str(uuid.uuid4()) key = self.key_prefix + ":" + verification_code - data = json.dumps(dict(id=user.id, - timestamp=timestamp()) - ) + data = { + "verification_code": verification_code, + "email_address": toaddr, + "timestamp": timestamp() + } + es_save_data(es, self.key_prefix, "local", data, verification_code) - Redis.set(key, data) - #two_days = 60 * 60 * 24 * 2 - Redis.expire(key, THREE_DAYS) - to = user.email_address subject = self.subject - body = render_template(self.template_name, - verification_code = verification_code) - send_email(to, subject, body) + body = render_template( + self.template_name, + verification_code = verification_code) + + msg = MIMEMultipart() + msg["To"] = toaddr + msg["Subject"] = self.subject + msg["From"] = self.fromaddr + msg.attach(MIMEText(body, "plain")) + + send_email(toaddr, msg.as_string()) class Password(object): @@ -429,38 +434,61 @@ def verify_email(): response.set_cookie(UserSession.cookie_name, session_id_signed) return response -@app.route("/n/password_reset") +@app.route("/n/password_reset", methods=['GET']) def password_reset(): logger.debug("in password_reset request.url is:", request.url) # We do this mainly just to assert that it's in proper form for displaying next page # Really not necessary but doesn't hurt - user_encode = DecodeUser(ForgotPasswordEmail.key_prefix).reencode_standalone() - - return render_template("new_security/password_reset.html", user_encode=user_encode) + # user_encode = DecodeUser(ForgotPasswordEmail.key_prefix).reencode_standalone() + verification_code = request.args.get('code') + hmac = request.args.get('hm') + if verification_code: + code_details = get_item_by_unique_column( + es + , "verification_code" + , verification_code + , ForgotPasswordEmail.key_prefix + , "local") + if code_details: + user_details = get_user_by_unique_column( + es + , "email_address" + , code_details["email_address"]) + if user_details: + return render_template( + "new_security/password_reset.html", user_encode=user_details["user_id"]) + else: + flash("Invalid code: User no longer exists!", "error") + else: + flash("Invalid code: Password reset code does not exist or might have expired!", "error") + return redirect(url_for("login"))#render_template("new_security/login_user.html", error=error) @app.route("/n/password_reset_step2", methods=('POST',)) def password_reset_step2(): + from utility.elasticsearch_tools import es logger.debug("in password_reset request.url is:", request.url) errors = [] + user_id = request.form['user_encode'] - user_encode = request.form['user_encode'] - verification_code, separator, hmac = user_encode.partition(':') - - hmac_verified = actual_hmac_creation(verification_code) logger.debug("locals are:", locals()) - assert hmac == hmac_verified, "Someone has been naughty" - - user = DecodeUser.actual_get_user(ForgotPasswordEmail.key_prefix, verification_code) - logger.debug("user is:", user) - + user = Bunch() password = request.form['password'] - set_password(password, user) - db_session.commit() + + es = get_elasticsearch_connection() + es.update( + index = "users" + , doc_type = "local" + , id = user_id + , body = { + "doc": { + "password": user.__dict__.get("password") + } + }) flash("Password changed successfully. You can now sign in.", "alert-info") response = make_response(redirect(url_for('login'))) @@ -492,8 +520,85 @@ class DecodeUser(object): @app.route("/n/login", methods=('GET', 'POST')) def login(): lu = LoginUser() - return lu.standard_login() + login_type = request.args.get("type") + if login_type: + uid = request.args.get("uid") + return lu.oauth2_login(login_type, uid) + else: + return lu.standard_login() + +@app.route("/n/login/github_oauth2", methods=('GET', 'POST')) +def github_oauth2(): + from utility.tools import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET + code = request.args.get("code") + data = { + "client_id": GITHUB_CLIENT_ID, + "client_secret": GITHUB_CLIENT_SECRET, + "code": code + } + result = requests.post("https://github.com/login/oauth/access_token", json=data) + result_dict = {arr[0]:arr[1] for arr in [tok.split("=") for tok in [token.encode("utf-8") for token in result.text.split("&")]]} + + github_user = get_github_user_details(result_dict["access_token"]) + es = get_elasticsearch_connection() + user_details = get_user_by_unique_column(es, "github_id", github_user["id"]) + if user_details == None: + user_details = { + "user_id": str(uuid.uuid4()) + , "name": github_user["name"].encode("utf-8") + , "github_id": github_user["id"] + , "user_url": github_user["html_url"].encode("utf-8") + , "login_type": "github" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + save_user(es, user_details, user_details["user_id"]) + url = "/n/login?type=github&uid="+user_details["user_id"] + return redirect(url) + +@app.route("/n/login/orcid_oauth2", methods=('GET', 'POST')) +def orcid_oauth2(): + from uuid import uuid4 + from utility.tools import ORCID_CLIENT_ID, ORCID_CLIENT_SECRET, ORCID_TOKEN_URL, ORCID_AUTH_URL + code = request.args.get("code") + error = request.args.get("error") + url = "/n/login" + if code: + data = { + "client_id": ORCID_CLIENT_ID + , "client_secret": ORCID_CLIENT_SECRET + , "grant_type": "authorization_code" + , "code": code + } + result = requests.post(ORCID_TOKEN_URL, data=data) + result_dict = json.loads(result.text.encode("utf-8")) + + es = get_elasticsearch_connection() + user_details = get_user_by_unique_column(es, "orcid", result_dict["orcid"]) + if user_details == None: + user_details = { + "user_id": str(uuid4()) + , "name": result_dict["name"] + , "orcid": result_dict["orcid"] + , "user_url": "%s/%s" % ( + "/".join(ORCID_AUTH_URL.split("/")[:-2]), + result_dict["orcid"]) + , "login_type": "orcid" + , "organization": "" + , "active": 1 + , "confirmed": 1 + } + save_user(es, user_details, user_details["user_id"]) + url = "/n/login?type=orcid&uid="+user_details["user_id"] + else: + flash("There was an error getting code from ORCID") + return redirect(url) +def get_github_user_details(access_token): + from utility.tools import GITHUB_API_URL + result = requests.get(GITHUB_API_URL, params={"access_token":access_token}) + return result.json() class LoginUser(object): remember_time = 60 * 60 * 24 * 30 # One month in seconds @@ -501,27 +606,55 @@ class LoginUser(object): def __init__(self): self.remember_me = False + def oauth2_login(self, login_type, user_id): + """Login via an OAuth2 provider""" + es = get_elasticsearch_connection() + user_details = get_user_by_unique_column(es, "user_id", user_id) + if user_details: + user = model.User() + user.id = user_details["user_id"] + user.full_name = user_details["name"] + user.login_type = user_details["login_type"] + return self.actual_login(user) + else: + flash("Error logging in via OAuth2") + return make_response(redirect(url_for('login'))) + def standard_login(self): """Login through the normal form""" params = request.form if request.form else request.args logger.debug("in login params are:", params) + es = get_elasticsearch_connection() if not params: - return render_template("new_security/login_user.html") + from utility.tools import GITHUB_AUTH_URL, GITHUB_CLIENT_ID, ORCID_AUTH_URL, ORCID_CLIENT_ID + external_login = {} + if GITHUB_AUTH_URL and GITHUB_CLIENT_ID != 'UNKNOWN': + external_login["github"] = GITHUB_AUTH_URL + if ORCID_AUTH_URL and ORCID_CLIENT_ID != 'UNKNOWN': + external_login["orcid"] = ORCID_AUTH_URL + assert(es is not None) + return render_template( + "new_security/login_user.html" + , external_login=external_login + , es_server=es.ping()) else: - try: - user = model.User.query.filter_by(email_address=params['email_address']).one() - except sqlalchemy.orm.exc.NoResultFound: - logger.debug("No account exists for that email address") - valid = False - user = None - else: + user_details = get_user_by_unique_column(es, "email_address", params["email_address"]) + user = None + valid = None + if user_details: + user = model.User(); + for key in user_details: + user.__dict__[key] = user_details[key] + valid = False; + submitted_password = params['password'] pwfields = Struct(json.loads(user.password)) - encrypted = Password(submitted_password, - pwfields.salt, - pwfields.iterations, - pwfields.keylength, - pwfields.hashfunc) + encrypted = Password( + submitted_password, + pwfields.salt, + pwfields.iterations, + pwfields.keylength, + pwfields.hashfunc) logger.debug("\n\nComparing:\n{}\n{}\n".format(encrypted.password, pwfields.password)) valid = pbkdf2.safe_str_cmp(encrypted.password, pwfields.password) logger.debug("valid is:", valid) @@ -549,7 +682,7 @@ class LoginUser(object): else: if user: self.unsuccessful_login(user) - flash("Invalid email-address or password. Please try again.", "alert-error") + flash("Invalid email-address or password. Please try again.", "alert-danger") response = make_response(redirect(url_for('login'))) return response @@ -558,7 +691,6 @@ class LoginUser(object): """The meat of the logging in process""" session_id_signed = self.successful_login(user, assumed_by) flash("Thank you for logging in {}.".format(user.full_name), "alert-success") - print("IMPORT1:", import_collections) response = make_response(redirect(url_for('index_page', import_collections=import_collections))) if self.remember_me: max_age = self.remember_time @@ -589,8 +721,6 @@ class LoginUser(object): else: expire_time = THREE_DAYS Redis.expire(key, expire_time) - db_session.add(login_rec) - db_session.commit() return session_id_signed def unsuccessful_login(self, user): @@ -618,13 +748,16 @@ def forgot_password(): def forgot_password_submit(): params = request.form email_address = params['email_address'] - try: - user = model.User.query.filter_by(email_address=email_address).one() - except orm.exc.NoResultFound: - flash("Couldn't find a user associated with the email address {}. Sorry.".format( - email_address)) - return redirect(url_for("login")) - ForgotPasswordEmail(user) + user_details = get_user_by_unique_column(es, "email_address", email_address) + if user_details: + ForgotPasswordEmail(user_details["email_address"]) + # try: + # user = model.User.query.filter_by(email_address=email_address).one() + # except orm.exc.NoResultFound: + # flash("Couldn't find a user associated with the email address {}. Sorry.".format( + # email_address)) + # return redirect(url_for("login")) + # ForgotPasswordEmail(user) return render_template("new_security/forgot_password_step2.html", subject=ForgotPasswordEmail.subject) @@ -691,16 +824,18 @@ def register(): params = request.form if request.form else request.args + params = params.to_dict(flat=True) + es = get_elasticsearch_connection() + params["es_connection"] = es if params: logger.debug("Attempting to register the user...") result = RegisterUser(params) errors = result.errors - if result.thank_you_mode: - assert not errors, "Errors while in thank you mode? That seems wrong..." - return render_template("new_security/registered.html", - subject=VerificationEmail.subject) + if len(errors) == 0: + flash("Registration successful. You may login with your new account", "alert-info") + return redirect(url_for("login")) return render_template("new_security/register_user.html", values=params, errors=errors) @@ -771,13 +906,21 @@ app.jinja_env.globals.update(url_for_hmac=url_for_hmac, ####################################################################################### -def send_email(to, subject, body): - msg = json.dumps(dict(From="no-reply@genenetwork.org", - To=to, - Subject=subject, - Body=body)) - Redis.rpush("mail_queue", msg) - +# def send_email(to, subject, body): +# msg = json.dumps(dict(From="no-reply@genenetwork.org", +# To=to, +# Subject=subject, +# Body=body)) +# Redis.rpush("mail_queue", msg) + +def send_email(toaddr, msg, fromaddr="no-reply@genenetwork.org"): + from smtplib import SMTP + from utility.tools import SMTP_CONNECT, SMTP_USERNAME, SMTP_PASSWORD + server = SMTP(SMTP_CONNECT) + server.starttls() + server.login(SMTP_USERNAME, SMTP_PASSWORD) + server.sendmail(fromaddr, toaddr, msg) + server.quit() diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 17a2d762..a65924d8 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -123,7 +123,7 @@ def handle_bad_request(e): @app.route("/") def index_page(): logger.info("Sending index_page") - logger.error(request.url) + logger.info(request.url) params = request.args if 'import_collections' in params: import_collections = params['import_collections'] @@ -141,7 +141,7 @@ def index_page(): def tmp_page(img_path): logger.info("In tmp_page") logger.info("img_path:", img_path) - logger.error(request.url) + logger.info(request.url) initial_start_vars = request.form logger.info("initial_start_vars:", initial_start_vars) imgfile = open(GENERATED_IMAGE_DIR + img_path, 'rb') @@ -174,7 +174,7 @@ def twitter(filename): @app.route("/search", methods=('GET',)) def search_page(): logger.info("in search_page") - logger.error(request.url) + logger.info(request.url) if 'info_database' in request.args: logger.info("Going to sharing_info_page") template_vars = sharing_info_page() @@ -199,21 +199,22 @@ def search_page(): logger.info("request.args is", request.args) the_search = search_results.SearchResultPage(request.args) result = the_search.__dict__ + valid_search = result['search_term_exists'] logger.debugf("result", result) - if USE_REDIS: + if USE_REDIS and valid_search: Redis.set(key, pickle.dumps(result, pickle.HIGHEST_PROTOCOL)) Redis.expire(key, 60*60) - if result['search_term_exists']: + if valid_search: return render_template("search_result_page.html", **result) else: return render_template("search_error.html") @app.route("/gsearch", methods=('GET',)) def gsearchact(): - logger.error(request.url) + logger.info(request.url) result = gsearch.GSearch(request.args).__dict__ type = request.args['type'] if type == "gene": @@ -224,7 +225,7 @@ def gsearchact(): @app.route("/gsearch_updating", methods=('POST',)) def gsearch_updating(): logger.info("REQUEST ARGS:", request.values) - logger.error(request.url) + logger.info(request.url) result = update_search_results.GSearch(request.args).__dict__ return result['results'] # type = request.args['type'] @@ -235,31 +236,31 @@ def gsearch_updating(): @app.route("/docedit") def docedit(): - logger.error(request.url) + logger.info(request.url) doc = docs.Docs(request.args['entry']) return render_template("docedit.html", **doc.__dict__) @app.route('/generated/<filename>') def generated_file(filename): - logger.error(request.url) + logger.info(request.url) return send_from_directory(GENERATED_IMAGE_DIR,filename) @app.route("/help") def help(): - logger.error(request.url) + logger.info(request.url) doc = docs.Docs("help") return render_template("docs.html", **doc.__dict__) @app.route("/wgcna_setup", methods=('POST',)) def wcgna_setup(): logger.info("In wgcna, request.form is:", request.form) # We are going to get additional user input for the analysis - logger.error(request.url) + logger.info(request.url) return render_template("wgcna_setup.html", **request.form) # Display them using the template @app.route("/wgcna_results", methods=('POST',)) def wcgna_results(): logger.info("In wgcna, request.form is:", request.form) - logger.error(request.url) + logger.info(request.url) wgcna = wgcna_analysis.WGCNA() # Start R, load the package and pointers and create the analysis wgcnaA = wgcna.run_analysis(request.form) # Start the analysis, a wgcnaA object should be a separate long running thread result = wgcna.process_results(wgcnaA) # After the analysis is finished store the result @@ -268,13 +269,13 @@ def wcgna_results(): @app.route("/ctl_setup", methods=('POST',)) def ctl_setup(): logger.info("In ctl, request.form is:", request.form) # We are going to get additional user input for the analysis - logger.error(request.url) + logger.info(request.url) return render_template("ctl_setup.html", **request.form) # Display them using the template @app.route("/ctl_results", methods=('POST',)) def ctl_results(): logger.info("In ctl, request.form is:", request.form) - logger.error(request.url) + logger.info(request.url) ctl = ctl_analysis.CTL() # Start R, load the package and pointers and create the analysis ctlA = ctl.run_analysis(request.form) # Start the analysis, a ctlA object should be a separate long running thread result = ctl.process_results(ctlA) # After the analysis is finished store the result @@ -313,13 +314,13 @@ def environments(): @app.route("/submit_trait") def submit_trait_form(): - logger.error(request.url) + logger.info(request.url) species_and_groups = get_species_groups() return render_template("submit_trait.html", **{'species_and_groups' : species_and_groups, 'gn_server_url' : GN_SERVER_URL, 'version' : GN_VERSION}) @app.route("/create_temp_trait", methods=('POST',)) def create_temp_trait(): - logger.error(request.url) + logger.info(request.url) print("REQUEST.FORM:", request.form) #template_vars = submit_trait.SubmitTrait(request.form) @@ -332,7 +333,7 @@ def export_trait_excel(): """Excel file consisting of the sample data from the trait data and analysis page""" logger.info("In export_trait_excel") logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) sample_data = export_trait_data.export_sample_table(request.form) logger.info("sample_data - type: %s -- size: %s" % (type(sample_data), len(sample_data))) @@ -358,7 +359,7 @@ def export_trait_csv(): """CSV file consisting of the sample data from the trait data and analysis page""" logger.info("In export_trait_csv") logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) sample_data = export_trait_data.export_sample_table(request.form) logger.info("sample_data - type: %s -- size: %s" % (type(sample_data), len(sample_data))) @@ -379,7 +380,7 @@ def export_traits_csv(): """CSV file consisting of the traits from the search result page""" logger.info("In export_traits_csv") logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) csv_data = export_traits.export_search_results_csv(request.form) return Response(csv_data, @@ -389,7 +390,7 @@ def export_traits_csv(): @app.route('/export_perm_data', methods=('POST',)) def export_perm_data(): """CSV file consisting of the permutation data for the mapping results""" - logger.error(request.url) + logger.info(request.url) num_perm = float(request.form['num_perm']) perm_data = json.loads(request.form['perm_results']) @@ -412,7 +413,7 @@ def export_perm_data(): @app.route("/show_temp_trait", methods=('POST',)) def show_temp_trait_page(): - logger.error(request.url) + logger.info(request.url) template_vars = show_trait.ShowTrait(request.form) #logger.info("js_data before dump:", template_vars.js_data) template_vars.js_data = json.dumps(template_vars.js_data, @@ -427,7 +428,7 @@ def show_temp_trait_page(): @app.route("/show_trait") def show_trait_page(): - logger.error(request.url) + logger.info(request.url) template_vars = show_trait.ShowTrait(request.args) #logger.info("js_data before dump:", template_vars.js_data) template_vars.js_data = json.dumps(template_vars.js_data, @@ -443,7 +444,7 @@ def show_trait_page(): @app.route("/heatmap", methods=('POST',)) def heatmap_page(): logger.info("In heatmap, request.form is:", pf(request.form)) - logger.error(request.url) + logger.info(request.url) start_vars = request.form temp_uuid = uuid.uuid4() @@ -493,7 +494,7 @@ def mapping_results_container_page(): @app.route("/loading", methods=('POST',)) def loading_page(): - logger.error(request.url) + logger.info(request.url) initial_start_vars = request.form logger.debug("Marker regression called with initial_start_vars:", initial_start_vars.items()) #temp_uuid = initial_start_vars['temp_uuid'] @@ -552,7 +553,7 @@ def loading_page(): def marker_regression_page(): initial_start_vars = request.form logger.debug("Marker regression called with initial_start_vars:", initial_start_vars.items()) - logger.error(request.url) + logger.info(request.url) temp_uuid = initial_start_vars['temp_uuid'] wanted = ( 'trait_id', @@ -678,7 +679,7 @@ def marker_regression_page(): @app.route("/export_mapping_results", methods = ('POST',)) def export_mapping_results(): logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) file_path = request.form.get("results_path") results_csv = open(file_path, "r").read() response = Response(results_csv, @@ -691,7 +692,7 @@ def export_mapping_results(): @app.route("/export", methods = ('POST',)) def export(): logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) svg_xml = request.form.get("data", "Invalid data") filename = request.form.get("filename", "manhattan_plot_snp") response = Response(svg_xml, mimetype="image/svg+xml") @@ -702,7 +703,7 @@ def export(): def export_pdf(): import cairosvg logger.info("request.form:", request.form) - logger.error(request.url) + logger.info(request.url) svg_xml = request.form.get("data", "Invalid data") logger.info("svg_xml:", svg_xml) filename = request.form.get("filename", "interval_map_pdf") @@ -715,7 +716,7 @@ def export_pdf(): @app.route("/network_graph", methods=('POST',)) def network_graph_page(): logger.info("In network_graph, request.form is:", pf(request.form)) - logger.error(request.url) + logger.info(request.url) start_vars = request.form traits = [trait.strip() for trait in start_vars['trait_list'].split(',')] if traits[0] != "": @@ -731,7 +732,7 @@ def network_graph_page(): @app.route("/corr_compute", methods=('POST',)) def corr_compute_page(): logger.info("In corr_compute, request.form is:", pf(request.form)) - logger.error(request.url) + logger.info(request.url) #fd = webqtlFormData.webqtlFormData(request.form) template_vars = show_corr_results.CorrelationResults(request.form) return render_template("correlation_page.html", **template_vars.__dict__) @@ -739,7 +740,7 @@ def corr_compute_page(): @app.route("/corr_matrix", methods=('POST',)) def corr_matrix_page(): logger.info("In corr_matrix, request.form is:", pf(request.form)) - logger.error(request.url) + logger.info(request.url) start_vars = request.form traits = [trait.strip() for trait in start_vars['trait_list'].split(',')] @@ -755,7 +756,7 @@ def corr_matrix_page(): @app.route("/corr_scatter_plot") def corr_scatter_plot_page(): - logger.error(request.url) + logger.info(request.url) template_vars = corr_scatter_plot.CorrScatterPlot(request.args) template_vars.js_data = json.dumps(template_vars.js_data, default=json_default_handler, @@ -764,7 +765,7 @@ def corr_scatter_plot_page(): @app.route("/submit_bnw", methods=('POST',)) def submit_bnw(): - logger.error(request.url) + logger.info(request.url) template_vars = get_bnw_input(request.form) return render_template("empty_collection.html", **{'tool':'Correlation Matrix'}) @@ -772,7 +773,7 @@ def submit_bnw(): def sharing_info_page(): """Info page displayed when the user clicks the "Info" button next to the dataset selection""" logger.info("In sharing_info_page") - logger.error(request.url) + logger.info(request.url) fd = webqtlFormData.webqtlFormData(request.args) template_vars = SharingInfoPage.SharingInfoPage(fd) return template_vars @@ -780,7 +781,7 @@ def sharing_info_page(): # Take this out or secure it before putting into production @app.route("/get_temp_data") def get_temp_data(): - logger.error(request.url) + logger.info(request.url) temp_uuid = request.args['key'] return flask.jsonify(temp_data.TempData(temp_uuid).get_all()) |