From f3d24662b24c3e5bf43404b74da9e72b1f027f92 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Fri, 17 Jun 2016 11:46:33 -0400 Subject: [PATCH 01/28] Add a validator to check for one field if another is false-y. --- app/forms/validators.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/forms/validators.py b/app/forms/validators.py index 7ef79bb..3c2bc05 100644 --- a/app/forms/validators.py +++ b/app/forms/validators.py @@ -2,6 +2,25 @@ import wtforms as wtf +class RequiredIfNot(object): + """A validator indicating that the field is required if another field is + false-y. + """ + def __init__(self, condition_field_name, message=None): + """Initialize the validator with the other field's name.""" + self.condition_field_name = condition_field_name + self.message = ( + message or + 'Field is required if {} is not provided.'.format( + condition_field_name)) + + def __call__(self, form, field): + """Check the value if the other field's value is false-y.""" + condition_field = form._fields.get(self.condition_field_name) + if not condition_field.data and not field.data: + raise wtf.ValidationError(self.message) + + class AnyOf(wtf.validators.AnyOf): """A custom AnyOf validator that accepts null values.""" def __call__(self, form, field): -- GitLab From 470af5dfc416832eb779d41f486bada7fde712a9 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Thu, 2 Feb 2017 16:32:31 -0500 Subject: [PATCH 02/28] Add ability to change number of workers and processes for uwsgi at build time. --- Dockerfile | 2 ++ app/config/uwsgi.ini | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a80685c..c524421 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,8 @@ RUN \ echo "Copying environment variables from $1 to $2."; \ sed -e "s/\$DOMAIN/$DOMAIN/g" \ -e "s@\$CODEROOT@$CODEROOT@g" \ + -e "s@\NUM_PROCESSES@$NUM_PROCESSES@g" \ + -e "s@\NUM_WORKERS@$NUM_WORKERS@g" \ -e "s@\$NGINXERR@$NGINXERR@g" \ -e "s@\$NGINXREQ@$NGINXREQ@g" \ -e "s@\$UWSGIERR@$UWSGIERR@g" \ diff --git a/app/config/uwsgi.ini b/app/config/uwsgi.ini index 08d7693..fe18980 100644 --- a/app/config/uwsgi.ini +++ b/app/config/uwsgi.ini @@ -7,9 +7,10 @@ socket = $CODEROOT/uwsgi.sock chmod-socket = 666 master = true -# This is a best guess. At some point in time, we should run some experiments -# and get something more concrete. -processes = 1 +# Number of processes and workers should be determined at build time +# Should be based on the type of instance that it is running on +processes = $NUM_PROCESSES +workers = $NUM_WORKERS # Set loggers. req-logger = file:$UWSGIREQ -- GitLab From a3c4e104b0ac9b2f1ab902bf84e8bfcf962c02af Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Feb 2017 14:41:59 -0500 Subject: [PATCH 03/28] Take NUM_PROCESSES and NUM_WORKERS as docker build parameters. --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index c524421..0ef553f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,10 @@ ENV UWSGIREQ=/var/log/uwsgi.req.log # Custom environment variables. These change from project to project. ARG DOMAIN ENV DOMAIN=${DOMAIN} +ARG NUM_PROCESSES +ENV NUM_PROCESSES=$(NUM_PROCESSES) +ARG NUM_WORKERS +ENV NUM_WORKERS=$(NUM_WORKERS) # Add the nginx reposoitory. We need 1.8 in order to support adding CORS headers to error responses. RUN apt-get update -- GitLab From 0068002257c72124b49d997fce8cc1c29bf300e4 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Feb 2017 15:17:26 -0500 Subject: [PATCH 04/28] Change workers to threads. --- Dockerfile | 6 +++--- app/config/uwsgi.ini | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0ef553f..b067449 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,8 @@ ARG DOMAIN ENV DOMAIN=${DOMAIN} ARG NUM_PROCESSES ENV NUM_PROCESSES=$(NUM_PROCESSES) -ARG NUM_WORKERS -ENV NUM_WORKERS=$(NUM_WORKERS) +ARG NUM_THREADS +ENV NUM_THREADS=$(NUM_THREADS) # Add the nginx reposoitory. We need 1.8 in order to support adding CORS headers to error responses. RUN apt-get update @@ -59,7 +59,7 @@ RUN \ sed -e "s/\$DOMAIN/$DOMAIN/g" \ -e "s@\$CODEROOT@$CODEROOT@g" \ -e "s@\NUM_PROCESSES@$NUM_PROCESSES@g" \ - -e "s@\NUM_WORKERS@$NUM_WORKERS@g" \ + -e "s@\NUM_THREADS@$NUM_THREADS@g" \ -e "s@\$NGINXERR@$NGINXERR@g" \ -e "s@\$NGINXREQ@$NGINXREQ@g" \ -e "s@\$UWSGIERR@$UWSGIERR@g" \ diff --git a/app/config/uwsgi.ini b/app/config/uwsgi.ini index fe18980..03c1f90 100644 --- a/app/config/uwsgi.ini +++ b/app/config/uwsgi.ini @@ -10,7 +10,7 @@ master = true # Number of processes and workers should be determined at build time # Should be based on the type of instance that it is running on processes = $NUM_PROCESSES -workers = $NUM_WORKERS +threads = $NUM_THREADS # Set loggers. req-logger = file:$UWSGIREQ -- GitLab From 1d1da1f8e0ede3e82146fbb67ae5b9f72604ed5c Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 6 Feb 2017 12:21:19 -0500 Subject: [PATCH 05/28] Add curly braces instead of parantheses. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b067449..2523869 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,9 @@ ENV UWSGIREQ=/var/log/uwsgi.req.log ARG DOMAIN ENV DOMAIN=${DOMAIN} ARG NUM_PROCESSES -ENV NUM_PROCESSES=$(NUM_PROCESSES) +ENV NUM_PROCESSES=${NUM_PROCESSES} ARG NUM_THREADS -ENV NUM_THREADS=$(NUM_THREADS) +ENV NUM_THREADS=${NUM_THREADS} # Add the nginx reposoitory. We need 1.8 in order to support adding CORS headers to error responses. RUN apt-get update -- GitLab From 64992c4d4bab5124bfb71f34ea7c401e46766389 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 6 Feb 2017 14:44:30 -0500 Subject: [PATCH 06/28] String replace the as well --- Dockerfile | 4 ++-- app/config/uwsgi.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2523869..5611b79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,8 +58,8 @@ RUN \ echo "Copying environment variables from $1 to $2."; \ sed -e "s/\$DOMAIN/$DOMAIN/g" \ -e "s@\$CODEROOT@$CODEROOT@g" \ - -e "s@\NUM_PROCESSES@$NUM_PROCESSES@g" \ - -e "s@\NUM_THREADS@$NUM_THREADS@g" \ + -e "s@\$NUM_PROCESSES@$NUM_PROCESSES@g" \ + -e "s@\$NUM_THREADS@$NUM_THREADS@g" \ -e "s@\$NGINXERR@$NGINXERR@g" \ -e "s@\$NGINXREQ@$NGINXREQ@g" \ -e "s@\$UWSGIERR@$UWSGIERR@g" \ diff --git a/app/config/uwsgi.ini b/app/config/uwsgi.ini index 03c1f90..c996d6b 100644 --- a/app/config/uwsgi.ini +++ b/app/config/uwsgi.ini @@ -1,5 +1,5 @@ [uwsgi] -# This is used if no configuration is speicified. +# This is used if no configuration is specified. ini = :base socket = $CODEROOT/uwsgi.sock -- GitLab From 0e74105480526527a94e1b62f5caf2b619e6fb78 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Fri, 10 Feb 2017 17:35:55 -0500 Subject: [PATCH 07/28] Add dev requirements --- requirements-dev.txt | 5 +++++ requirements.txt | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..0814946 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +Flask-Testing==0.4.2 +nose==1.3.7 +pytest==2.9.1 +pylint>=1.6.5 diff --git a/requirements.txt b/requirements.txt index a0f1eac..932bb87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ docutils==0.12 Flask==0.10.1 Flask-Classy==0.6.10 Flask-SQLAlchemy==2.1 -Flask-Testing==0.4.2 GeoAlchemy2==0.3.0 httplib2==0.9.2 itsdangerous==0.24 @@ -18,7 +17,6 @@ Jinja2==2.8 jmespath==0.9.0 MarkupSafe==0.23 needs==1.0.9 -nose==1.3.7 oauth2client==2.0.1 parse==1.6.6 pathspec==0.3.3 @@ -28,7 +26,6 @@ py==1.4.31 pyasn1==0.1.9 pyasn1-modules==0.0.8 pycparser==2.14 -pytest==2.9.1 python-dateutil==2.4.2 PyYAML==3.11 redis==2.10.5 -- GitLab From 94ad65fbc167d96d64a2ed54dbe5a1a8133caf4f Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Sat, 11 Feb 2017 15:35:11 -0500 Subject: [PATCH 08/28] Add default .pylintrc --- .pylintrc | 407 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..f304c2c --- /dev/null +++ b/.pylintrc @@ -0,0 +1,407 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=import-star-module-level,coerce-builtin,unicode-builtin,no-absolute-import,input-builtin,reduce-builtin,cmp-builtin,basestring-builtin,cmp-method,setslice-method,unichr-builtin,backtick,old-octal-literal,old-ne-operator,oct-method,map-builtin-not-iterating,raw_input-builtin,apply-builtin,range-builtin-not-iterating,useless-suppression,dict-view-method,filter-builtin-not-iterating,unpacking-in-except,getslice-method,intern-builtin,raising-string,standarderror-builtin,buffer-builtin,coerce-method,old-raise-syntax,file-builtin,dict-iter-method,metaclass-assignment,long-suffix,next-method-called,suppressed-message,long-builtin,delslice-method,execfile-builtin,old-division,nonzero-method,using-cmp-argument,xrange-builtin,hex-method,indexing-exception,print-statement,round-builtin,reload-builtin,parameter-unpacking,zip-builtin-not-iterating + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception -- GitLab From 8bfaacf84bee43f7c9140bc2a8e390088c3e7456 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Sat, 11 Feb 2017 15:55:55 -0500 Subject: [PATCH 09/28] Add pylint flask plugin --- .pylintrc | 2 +- requirements-dev.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index f304c2c..71e9450 100644 --- a/.pylintrc +++ b/.pylintrc @@ -20,7 +20,7 @@ persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins= +load-plugins=pylint_flask # Use multiple processes to speed up Pylint. jobs=1 diff --git a/requirements-dev.txt b/requirements-dev.txt index 0814946..5660f02 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,3 +3,4 @@ Flask-Testing==0.4.2 nose==1.3.7 pytest==2.9.1 pylint>=1.6.5 +pylint-flask>=0.5 -- GitLab From 0897f2e7abe099f9c70d707332168a111547bf23 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Mon, 13 Feb 2017 17:16:17 -0500 Subject: [PATCH 10/28] Create local config and set debug true on staging and dev --- app/config/development.default.py | 17 ++++++++++------- app/config/local.default.py | 29 +++++++++++++++++++++++++++++ app/config/staging.default.py | 2 +- run.py | 2 +- 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 app/config/local.default.py diff --git a/app/config/development.default.py b/app/config/development.default.py index 24620cb..9a06abb 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -1,20 +1,23 @@ +import os + + SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'sqlite://' +SQLALCHEMY_DATABASE_URI = os.environ['DBURI'] -REDIS_URI = 'redis://127.0.0.1:6379/' +REDIS_URI = os.environ['REDISURI'] -DEBUG = False +DEBUG = True SERVICE_CONFIG = { - 'app_key': '$APP_KEY', - 'app_secret': '$APP_SECRET', + 'app_key': os.environ['APP_KEY'], + 'app_secret': os.environ['APP_SECRET'], 'headers': { 'app_key': 'x-blocpower-app-key', 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://127.0.0.1:5400', - 'user': 'http://127.0.0.1:5401'} + 'app': 'http://staging.app.s.blocpower.us/', + 'user': 'http://staging.user.s.blocpower.us/'} } # AppService diff --git a/app/config/local.default.py b/app/config/local.default.py new file mode 100644 index 0000000..9d611c0 --- /dev/null +++ b/app/config/local.default.py @@ -0,0 +1,29 @@ +SQLALCHEMY_TRACK_MODIFICATIONS = False +SQLALCHEMY_DATABASE_URI = 'sqlite://' + +REDIS_URI = 'redis://127.0.0.1:6379/' + +DEBUG = True + +SERVICE_CONFIG = { + 'app_key': '$APP_KEY', + 'app_secret': '$APP_SECRET', + 'headers': { + 'app_key': 'x-blocpower-app-key', + 'app_token': 'x-blocpower-app-token', + 'app_secret': 'x-blocpower-app-secret'}, + 'urls': { + 'app': 'http://127.0.0.1:5400', + 'user': 'http://127.0.0.1:5401'} +} + +# AppService +APP_CACHE_EXPIRY = 60 * 60 # One hour. + +# UserService +GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' +USER_CACHE_EXPIRY = 60 * 60 # One Hour. + +# Blocpower auth information. +HEADER_AUTH_KEY = 'x-blocpower-auth-key' +HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' diff --git a/app/config/staging.default.py b/app/config/staging.default.py index a4b9ead..9a06abb 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -6,7 +6,7 @@ SQLALCHEMY_DATABASE_URI = os.environ['DBURI'] REDIS_URI = os.environ['REDISURI'] -DEBUG = False +DEBUG = True SERVICE_CONFIG = { 'app_key': os.environ['APP_KEY'], diff --git a/run.py b/run.py index b19744b..1ab56a6 100644 --- a/run.py +++ b/run.py @@ -2,6 +2,6 @@ from app import create_app from app.lib.database import db -app = create_app('config/development.py') +app = create_app('config/local.py') if __name__ == '__main__': app.run(host='0.0.0.0', port=5300) -- GitLab From 1dcb7e452e0c4e445fee9fd5ab699bd74459910d Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Mon, 13 Feb 2017 17:34:15 -0500 Subject: [PATCH 11/28] Add docs folder for api reference --- docs/API/api.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/API/api.md diff --git a/docs/API/api.md b/docs/API/api.md new file mode 100644 index 0000000..1b62768 --- /dev/null +++ b/docs/API/api.md @@ -0,0 +1,56 @@ +**Title** +---- + <_Additional information about your API call. Try to use verbs that match both request type (fetching vs modifying) and plurality (one vs multiple)._> + +* **URL** + + <_The URL Structure (path only, no root url)_> + +* **Method:** + + <_The request type_> + + `GET` | `POST` | `DELETE` | `PUT` + +* **URL Params** + + <_If URL params exist, specify them in accordance with name mentioned in URL section. Separate into optional and required. Document data constraints._> + + **Required:** + + `id=[integer]` + + **Optional:** + + `photo_id=[alphanumeric]` + +* **Data Params** + + <_If making a post request, what should the body payload look like? URL Params rules apply here too._> + +* **Success Response:** + + <_What should the status code be on success and is there any returned data? This is useful when people need to to know what their callbacks should expect!_> + + * **Code:** 200
+ **Content:** `{ id : 12 }` + +* **Error Response:** + + <_Most endpoints will have many ways they can fail. From unauthorized access, to wrongful parameters etc. All of those should be liste d here. It might seem repetitive, but it helps prevent assumptions from being made where they should be._> + + * **Code:** 401 UNAUTHORIZED
+ **Content:** `{ error : "Log in" }` + + OR + + * **Code:** 422 UNPROCESSABLE ENTRY
+ **Content:** `{ error : "Email Invalid" }` + +* **Sample Call:** + + <_Just a sample call to your endpoint in a runnable format ($.ajax call or a curl request) - this makes life easier and more predictable._> + +* **Notes:** + + <_This is where all uncertainties, commentary, discussion etc. can go. I recommend timestamping and identifying oneself when leaving comments here._> -- GitLab From b9747457d87512be138dc0af24ff175f2df26b29 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 14 Feb 2017 15:23:41 -0500 Subject: [PATCH 12/28] Change default URI back to old value. --- app/config/local.default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/local.default.py b/app/config/local.default.py index 9d611c0..5a20208 100644 --- a/app/config/local.default.py +++ b/app/config/local.default.py @@ -1,5 +1,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'sqlite://' +SQLALCHEMY_DATABASE_URI = 'postgres:///projectservice' REDIS_URI = 'redis://127.0.0.1:6379/' -- GitLab From 7057e780b1fbece2466acb28e41d3d4d6173f24b Mon Sep 17 00:00:00 2001 From: Conrad S Date: Tue, 21 Feb 2017 11:48:39 -0500 Subject: [PATCH 13/28] Add default public schema in the base model --- app/models/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/base.py b/app/models/base.py index 835e50e..115fbb9 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -22,6 +22,9 @@ class Model(object): """ return cls.__name__.lower() + __table_args__ = {"schema": "public"} + + def __str__(self): """Provide a sane default for model string representation.""" return '<{} (id={})>'.format(self.__class__.__name__, self.id) -- GitLab From 82152cb930d164caaddbd71cd40332ae3fabca70 Mon Sep 17 00:00:00 2001 From: Conrad S Date: Tue, 21 Feb 2017 11:49:31 -0500 Subject: [PATCH 14/28] Remove whitespace --- app/models/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/base.py b/app/models/base.py index 115fbb9..1d39b5c 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -24,7 +24,6 @@ class Model(object): __table_args__ = {"schema": "public"} - def __str__(self): """Provide a sane default for model string representation.""" return '<{} (id={})>'.format(self.__class__.__name__, self.id) -- GitLab From fcf94c17b13562e5500db1475b094b9bb858f018 Mon Sep 17 00:00:00 2001 From: Conrad S Date: Tue, 21 Feb 2017 15:40:03 -0500 Subject: [PATCH 15/28] Add building_id to model and filter to controller --- app/controllers/base.py | 2 +- app/controllers/project.py | 9 +++++---- app/forms/project.py | 1 + app/models/project.py | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/base.py b/app/controllers/base.py index 9168871..a99b010 100644 --- a/app/controllers/base.py +++ b/app/controllers/base.py @@ -177,5 +177,5 @@ class SalesforceObjectController(RestController): if model: data.update({'id': model.id}) return self.put(model.id, data, filter_data) - + return super(SalesforceObjectController, self).post(data, filter_data) diff --git a/app/controllers/project.py b/app/controllers/project.py index 679b02c..ed407fd 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -26,8 +26,9 @@ class ProjectController(RestController): filters = { 'q': lambda d: and_(*[ Project.name.ilike('%{}%'.format(term)) - for term in d['q'].split(' ') - ])} + for term in d['q'].split(' ')]), + 'building_id': lambda d: Project.building_id == d['building_id'], + } def get_form(self, filter_data): """Return the project form.""" @@ -100,7 +101,7 @@ class ProjectController(RestController): address = AddressController().put(address.id, address_data, {}) else: address = AddressController().post(address_data, {}) - + place_data.update({'address_id': address.id}) if place: @@ -108,7 +109,7 @@ class ProjectController(RestController): place = PlaceController().put(place.id, place_data, {}) else: place = PlaceController().post(place_data, {}) - + data.update({'place_id': place.id}) # Create/modify the project diff --git a/app/forms/project.py b/app/forms/project.py index d69d9bf..a46bb06 100644 --- a/app/forms/project.py +++ b/app/forms/project.py @@ -21,3 +21,4 @@ class ProjectForm(Form): wtf.validators.Required(), wtf.validators.AnyOf(Project.states) ]) + building_id = wtf.IntegerField() diff --git a/app/models/project.py b/app/models/project.py index e1203c7..ee5f92a 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -12,6 +12,7 @@ class Project(Model, Tracked, SalesForce, db.Model): db.Integer, db.ForeignKey('place.id'), nullable=False) name = db.Column(db.Unicode(255), nullable=False) + building_id = db.Column(db.Integer, nullable=False) @property def computed_slug(self): -- GitLab From f1c7d51f8267b21310d518d082deb2692be85565 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Tue, 21 Feb 2017 15:48:54 -0500 Subject: [PATCH 16/28] Add ProcTable, ProcColumn and run stored proc --- app/lib/database.py | 59 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/app/lib/database.py b/app/lib/database.py index 3de6fb0..d42d70e 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -21,7 +21,7 @@ def register(app): def commit(): """A wrapper for db.session.commit(). - + This rolls back the database session after a failed commit so that the app can continue to request future commits on the same thread. """ @@ -30,3 +30,60 @@ def commit(): except Exception as e: db.session.rollback() raise e + +class ProcTable: + """ProcTable represents a simplified class of SQLAlchemy db.model""" + + def __init__(self, name, *columns): + self.name = name + self.columns = [column for column in columns] + + def get_columns(self): + """List all columns""" + return [column.key for column in self.columns] + +class ProcColumn: + """ProcColumn represents a column similar to SQLAlchemy column""" + + def __init__(self, key): + self.key = key + +def proc(model, method, limit=None, offset=None, **kwargs): + """ + Run stored procedure + + Args: + model (class): The class of the db table + method (str): Method name for stored procedure + kwargs: Arguments for stored proc + Returns: + list: Results of the query + """ + + params = "" + cols = ','.join(str(i) for i in model.__table__.get_columns()) + for key, value in kwargs.items(): + params += "in_{} := '{}', ".format(key, value) + params = params[:-2] # remove last comma and space + + query = "select {} from {}.{}({})".format(cols, model.SCHEMA, method, params) + if limit: + query += ' limit {}'.format(limit) + if offset: + query += ' offset {}'.format(offset) + + try: + results = db.session.execute(query) + db.session.commit() + + data = [] + for row in results: + obj = {} + for column, value in row.items(): + obj[column] = value + data.append(model(**obj)) + + except Exception as err: + raise err + + return data -- GitLab From 6f7f0f20b4a550340124ea8a32e3ef8bf30b041b Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Tue, 21 Feb 2017 17:50:34 -0500 Subject: [PATCH 17/28] Update schema in proc --- app/lib/database.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/lib/database.py b/app/lib/database.py index d42d70e..da2e814 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -66,7 +66,12 @@ def proc(model, method, limit=None, offset=None, **kwargs): params += "in_{} := '{}', ".format(key, value) params = params[:-2] # remove last comma and space - query = "select {} from {}.{}({})".format(cols, model.SCHEMA, method, params) + query = "select {} from {}.{}({})".format( + cols, + model.__table_args__['schema'], + method, + params + ) if limit: query += ' limit {}'.format(limit) if offset: -- GitLab From 7468893b728a60f41f7a5e8dde04f43a0ef398ed Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Tue, 21 Feb 2017 18:05:04 -0500 Subject: [PATCH 18/28] Create BaseModel for database --- app/models/base.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/app/models/base.py b/app/models/base.py index 1d39b5c..e304c90 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -13,23 +13,8 @@ from ..lib import geography from . import columns -class Model(object): +class BaseModel: """A base mixin for all models.""" - @declared_attr - def __tablename__(cls): - """Automatically set the database table name to the class name - lower-cased. - """ - return cls.__name__.lower() - - __table_args__ = {"schema": "public"} - - def __str__(self): - """Provide a sane default for model string representation.""" - return '<{} (id={})>'.format(self.__class__.__name__, self.id) - - id = db.Column(db.Integer, primary_key=True) - def get_dictionary(self): """Return a dictionary representation of the model. @@ -56,6 +41,24 @@ class Model(object): return d +class Model(BaseModel): + """A base mixin for SQLAlchemy model.""" + @declared_attr + def __tablename__(cls): + """Automatically set the database table name to the class name + lower-cased. + """ + return cls.__name__.lower() + + __table_args__ = {"schema": "public"} + + def __str__(self): + """Provide a sane default for model string representation.""" + return '<{} (id={})>'.format(self.__class__.__name__, self.id) + + id = db.Column(db.Integer, primary_key=True) + + class Tracked(object): """A mixin to include tracking datetime fields.""" created = db.Column(columns.Arrow, default=func.now()) -- GitLab From 45ad5bae684c74f5123e95bad5a18c9b19c16b43 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Wed, 22 Feb 2017 09:49:03 -0500 Subject: [PATCH 19/28] Simplify result loop in proc --- app/lib/database.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/lib/database.py b/app/lib/database.py index da2e814..b2f8943 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -83,10 +83,7 @@ def proc(model, method, limit=None, offset=None, **kwargs): data = [] for row in results: - obj = {} - for column, value in row.items(): - obj[column] = value - data.append(model(**obj)) + data.append(model(**dict(zip(row.keys(), row)))) except Exception as err: raise err -- GitLab From d5acab84cb25ceb31d9c6260a68b121215eb0431 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Wed, 22 Feb 2017 12:50:35 -0500 Subject: [PATCH 20/28] Fix config to point to new dev application and user service. --- app/config/development.default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/development.default.py b/app/config/development.default.py index 9a06abb..8d06722 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -16,8 +16,8 @@ SERVICE_CONFIG = { 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://staging.app.s.blocpower.us/', - 'user': 'http://staging.user.s.blocpower.us/'} + 'app': 'http://dev.appservice.blocpower.io/', + 'user': 'http://dev.userservice.blocpower.io/'} } # AppService -- GitLab From 3821723004be897a4f4e351c8b70e843a3814ae6 Mon Sep 17 00:00:00 2001 From: Conrad S Date: Thu, 23 Feb 2017 10:12:38 -0500 Subject: [PATCH 21/28] Add public schema to address relationship --- app/models/place.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/place.py b/app/models/place.py index e06c953..cbb4e07 100644 --- a/app/models/place.py +++ b/app/models/place.py @@ -9,7 +9,7 @@ class Place(Model, Tracked, SalesForce, db.Model): """The Place class""" name = db.Column(db.Unicode(255), nullable=False) address_id = db.Column( - db.Integer, db.ForeignKey('address.id'), nullable=False) + db.Integer, db.ForeignKey('public.address.id'), nullable=False) address = relationship('Address', uselist=False) -- GitLab From e3f7c57c3e63b35a5c6c2428df29b6100814cfeb Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Thu, 23 Feb 2017 13:47:22 -0500 Subject: [PATCH 22/28] Add support to use the project schema in the customer database. --- app/__init__.py | 3 --- app/config/local.default.py | 2 +- app/models/base.py | 20 +++++++++++++++++++- app/models/client.py | 4 ++-- app/models/contact.py | 10 +++++----- app/models/document.py | 4 ++-- app/models/note.py | 4 ++-- app/models/place.py | 26 +++++++++++++------------- app/models/project.py | 17 +++++++++-------- app/views/base.py | 4 ++-- 10 files changed, 55 insertions(+), 39 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index cbd49e1..990886a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -24,7 +24,4 @@ def create_app(config): from .lib import session session.register(app) - with app.app_context(): - database.db.create_all() - return app diff --git a/app/config/local.default.py b/app/config/local.default.py index 5a20208..9c69a5f 100644 --- a/app/config/local.default.py +++ b/app/config/local.default.py @@ -1,5 +1,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'postgres:///projectservice' +SQLALCHEMY_DATABASE_URI = 'postgres:///customer' REDIS_URI = 'redis://127.0.0.1:6379/' diff --git a/app/models/base.py b/app/models/base.py index 8a484f1..eb78608 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -58,7 +58,6 @@ class Model(BaseModel): id = db.Column(db.Integer, primary_key=True) - class Tracked(object): """A mixin to include tracking datetime fields.""" created = db.Column(columns.Arrow, default=func.now()) @@ -96,3 +95,22 @@ class External(object): """An external-facing model. These have a UUID key.""" key = db.Column( columns.GUID(), index=True, nullable=False, default=uuid.uuid4) + + +class ProjectSchema(object): + """A mixin that changes the schema to project_service""" + + @declared_attr + def __tablename__(cls): + """Automatically set the database table name to the class name + lower-cased. + """ + return cls.__name__.lower() + + __table_args__= {"schema": "project_service"} + + def __str__(self): + """Provide a sane default for model string representation.""" + return '<{} (id={})>'.format(self.__class__.__name__, self.id) + + id = db.Column(db.Integer, primary_key=True) diff --git a/app/models/client.py b/app/models/client.py index afa2543..3a4ada2 100644 --- a/app/models/client.py +++ b/app/models/client.py @@ -1,8 +1,8 @@ """Models for tracking client information.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Client(Model, Tracked, SalesForce, db.Model): +class Client(ProjectSchema, Tracked, SalesForce, db.Model): """A client.""" name = db.Column(db.Unicode(255), nullable=False) diff --git a/app/models/contact.py b/app/models/contact.py index 0fefe9d..a16d788 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -1,14 +1,14 @@ """Models for dealing with contacts.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Contact(Model, Tracked, SalesForce, db.Model): +class Contact(ProjectSchema, Tracked, SalesForce, db.Model): """A contact (i.e an entity you can reach out to).""" name = db.Column(db.Unicode(255), nullable=False) -class ContactMethod(Model, Tracked, db.Model): +class ContactMethod(ProjectSchema, Tracked, db.Model): """A contact method.""" # TODO SQLAlchemy < 1.1 does not support the use of a python enum in Enum # fields, so for now we have to use a list of strings. When 1.1 leaves @@ -24,12 +24,12 @@ class ContactMethod(Model, Tracked, db.Model): contact_id = db.Column( db.Integer, db.ForeignKey('contact.id'), nullable=False) method = db.Column( - db.Enum(*methods, name='contact_methods'), + db.Enum(*methods, name='contact_methods', inherit_schema=True), nullable=False) value = db.Column(db.Unicode(255), nullable=False) -class ProjectContact(Model, Tracked, db.Model): +class ProjectContact(ProjectSchema, Tracked, db.Model): """A m2m relationship between project and contact.""" project_id = db.Column( db.Integer, db.ForeignKey('project.id'), nullable=False) diff --git a/app/models/document.py b/app/models/document.py index f9fbb2d..5dba758 100644 --- a/app/models/document.py +++ b/app/models/document.py @@ -1,10 +1,10 @@ """Models for handling associated documents.""" from app.lib.database import db -from app.models.base import Model, Tracked +from app.models.base import Model, Tracked, ProjectSchema from app.models.columns import GUID -class DocumentSlot(Model, Tracked, db.Model): +class DocumentSlot(ProjectSchema, Tracked, db.Model): """A m2m relationship between the project model and documents (provided by the document service). """ diff --git a/app/models/note.py b/app/models/note.py index 470c1dc..4385a8e 100644 --- a/app/models/note.py +++ b/app/models/note.py @@ -1,10 +1,10 @@ """Models for making small notes on the project.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema from app.models import columns -class Note(Model, Tracked, SalesForce, db.Model): +class Note(ProjectSchema, Tracked, SalesForce, db.Model): """A note.""" project_id = db.Column( db.Integer, diff --git a/app/models/place.py b/app/models/place.py index cbb4e07..625268a 100644 --- a/app/models/place.py +++ b/app/models/place.py @@ -2,27 +2,27 @@ from sqlalchemy.orm import relationship from app.lib.database import db from app.lib import geography -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Tracked, SalesForce, ProjectSchema -class Place(Model, Tracked, SalesForce, db.Model): - """The Place class""" - name = db.Column(db.Unicode(255), nullable=False) - address_id = db.Column( - db.Integer, db.ForeignKey('public.address.id'), nullable=False) - - address = relationship('Address', uselist=False) - - -class Address(Model, Tracked, db.Model): +class Address(ProjectSchema, Tracked, db.Model): """The Address class""" street_address = db.Column(db.Unicode(255), nullable=False) city = db.Column(db.Unicode(255), nullable=False) county = db.Column(db.Unicode(255)) state = db.Column( - db.Enum(*geography.states, name='geo_states'), + db.Enum(*geography.states, name='geo_states', inherit_schema=True), nullable=False) country = db.Column( - db.Enum(*geography.countries, name='geo_countries'), + db.Enum(*geography.countries, name='geo_countries', inherit_schema=True), nullable=False) postal_code = db.Column(db.Unicode(10)) + + +class Place(ProjectSchema, Tracked, SalesForce, db.Model): + """The Place class""" + name = db.Column(db.Unicode(255), nullable=False) + address_id = db.Column( + db.INTEGER, db.ForeignKey('project_service.address.id'), nullable=False) + + address = relationship('Address', uselist=False) diff --git a/app/models/project.py b/app/models/project.py index ee5f92a..a8ed55f 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -1,15 +1,15 @@ """Models directly-relating to the top-level project.""" import re from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Project(Model, Tracked, SalesForce, db.Model): +class Project(ProjectSchema, Tracked, SalesForce, db.Model): """A project.""" client_id = db.Column( - db.Integer, db.ForeignKey('client.id'), nullable=False) + db.Integer, db.ForeignKey('project_service.client.id'), nullable=False) place_id = db.Column( - db.Integer, db.ForeignKey('place.id'), nullable=False) + db.Integer, db.ForeignKey('project_service.place.id'), nullable=False) name = db.Column(db.Unicode(255), nullable=False) building_id = db.Column(db.Integer, nullable=False) @@ -61,7 +61,7 @@ class Project(Model, Tracked, SalesForce, db.Model): 'constructed', 'verified', 'paid' ] state = db.Column( - db.Enum(*states, name='project_states'), + db.Enum(*states, name='project_states', inherit_schema=True), default='pending', nullable=False) @@ -72,10 +72,11 @@ class Project(Model, Tracked, SalesForce, db.Model): return d -class ProjectStateChange(Model, Tracked, db.Model): +class ProjectStateChange(ProjectSchema, Tracked, db.Model): """A state change of a project.""" - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), + project_id = db.Column( + db.Integer, db.ForeignKey('project.id'), nullable=False) state = db.Column( - db.Enum(*Project.states, name='project_states'), + db.Enum(*Project.states, name='project_states', inherit_schema=True), nullable=False) diff --git a/app/views/base.py b/app/views/base.py index f37f3d1..a508bbc 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -76,11 +76,11 @@ class SalesforceObjectView(UnprotectedRestView): """Return a controller instance to use for database interactions.""" pass - @standard_login_need + # @standard_login_need def index(self): return super(SalesforceObjectView, self).index() - @standard_login_need + # @standard_login_need def get(self, id_): return super(SalesforceObjectView, self).get(id_) -- GitLab From 42ebc37ec5c3d71827425876a30088f078b22e5e Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Thu, 23 Feb 2017 13:54:30 -0500 Subject: [PATCH 23/28] Style change and remove comments. --- app/views/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/base.py b/app/views/base.py index a508bbc..b885c9c 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -70,17 +70,18 @@ class RestView(UnprotectedRestView): """ decorators = (standard_login_need,) + class SalesforceObjectView(UnprotectedRestView): """A view wrapper for Salesforce object that offers API protection""" def get_controller(self): """Return a controller instance to use for database interactions.""" pass - # @standard_login_need + @standard_login_need def index(self): return super(SalesforceObjectView, self).index() - # @standard_login_need + @standard_login_need def get(self, id_): return super(SalesforceObjectView, self).get(id_) -- GitLab From 983744d5284beda722a7839c13b1f3a201b4b6d5 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 27 Feb 2017 13:39:02 -0500 Subject: [PATCH 24/28] Remove create db from wsgi.py. --- wsgi.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/wsgi.py b/wsgi.py index d147c76..294e9eb 100644 --- a/wsgi.py +++ b/wsgi.py @@ -9,6 +9,3 @@ env = os.environ['ENVIRONMENT'] # Correctly raise a file not found if the specified environment does not exist. app = create_app('config/{}.py'.format(env)) -with app.app_context(): - # Build the database models. - db.create_all() -- GitLab From de801f98f18a9e449c761f645ba5d6304057eb6f Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 27 Feb 2017 16:06:36 -0500 Subject: [PATCH 25/28] Add leading schema name to the foreign key in project state change. --- app/models/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.py b/app/models/project.py index a8ed55f..7b71df1 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -75,7 +75,7 @@ class Project(ProjectSchema, Tracked, SalesForce, db.Model): class ProjectStateChange(ProjectSchema, Tracked, db.Model): """A state change of a project.""" project_id = db.Column( - db.Integer, db.ForeignKey('project.id'), + db.Integer, db.ForeignKey('project_service.project.id'), nullable=False) state = db.Column( db.Enum(*Project.states, name='project_states', inherit_schema=True), -- GitLab From 6fdcf7b21d75a9eee0ad75cee71f181378c77653 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 27 Feb 2017 16:54:14 -0500 Subject: [PATCH 26/28] Use basemodel in project schema mixin. --- app/models/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/base.py b/app/models/base.py index eb78608..529e2b8 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -97,7 +97,7 @@ class External(object): columns.GUID(), index=True, nullable=False, default=uuid.uuid4) -class ProjectSchema(object): +class ProjectSchema(BaseModel): """A mixin that changes the schema to project_service""" @declared_attr @@ -107,7 +107,7 @@ class ProjectSchema(object): """ return cls.__name__.lower() - __table_args__= {"schema": "project_service"} + __table_args__ = {"schema": "project_service"} def __str__(self): """Provide a sane default for model string representation.""" -- GitLab From ad59f57c9c016d29ed8b7cbec4250d00afab943c Mon Sep 17 00:00:00 2001 From: Conrad S Date: Tue, 28 Feb 2017 11:46:20 -0500 Subject: [PATCH 27/28] Add project_service schema to document slot model --- app/models/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/document.py b/app/models/document.py index 5dba758..b604dc4 100644 --- a/app/models/document.py +++ b/app/models/document.py @@ -9,7 +9,7 @@ class DocumentSlot(ProjectSchema, Tracked, db.Model): the document service). """ project_id = db.Column( - db.Integer, db.ForeignKey('project.id'), nullable=False) + db.Integer, db.ForeignKey('project_service.project.id'), nullable=False) document_key = db.Column(GUID(), nullable=False) # How the document fits into the project. -- GitLab From d1c8438ce8e008f8e26f9f8ac78f7b6e78cdfa91 Mon Sep 17 00:00:00 2001 From: Conrad S Date: Mon, 6 Mar 2017 14:12:18 -0500 Subject: [PATCH 28/28] Sanitize input and check if input is empty --- app/lib/database.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/lib/database.py b/app/lib/database.py index b2f8943..9bb84f0 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -62,8 +62,14 @@ def proc(model, method, limit=None, offset=None, **kwargs): params = "" cols = ','.join(str(i) for i in model.__table__.get_columns()) + # By seperating the args like this sanatation will happen automatically + input_args = {} for key, value in kwargs.items(): - params += "in_{} := '{}', ".format(key, value) + if value is not None: + params += "in_{} := :{}, ".format(key, key) + input_args[key] = str(value) + else: + params += "in_{} := null, ".format(key) params = params[:-2] # remove last comma and space query = "select {} from {}.{}({})".format( @@ -78,7 +84,7 @@ def proc(model, method, limit=None, offset=None, **kwargs): query += ' offset {}'.format(offset) try: - results = db.session.execute(query) + results = db.session.execute(query, input_args) db.session.commit() data = [] -- GitLab