Skip to content

Commit

Permalink
Merge branch 'master' into design/refinement
Browse files Browse the repository at this point in the history
  • Loading branch information
arikfr authored Nov 22, 2017
2 parents 4eb5005 + 3dad9b6 commit bc1fa00
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 2 deletions.
3 changes: 2 additions & 1 deletion cli/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ def delete(email, organization=None):
models.User.org == org.id,
).delete()
else:
deleted_count = models.User.query.filter(models.User.email == email).delete()
deleted_count = models.User.query.filter(models.User.email == email).delete(
synchronize_session=False)
models.db.session.commit()
print("Deleted %d users." % deleted_count)

Expand Down
25 changes: 24 additions & 1 deletion models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from redash.query_runner import (get_configuration_schema_for_query_runner_type,
get_query_runner)
from redash.utils import generate_token, json_dumps
from redash.utils.comparators import CaseInsensitiveComparator
from redash.utils.configuration import ConfigurationContainer
from sqlalchemy import distinct, or_
from sqlalchemy.dialects import postgresql
Expand Down Expand Up @@ -368,12 +369,32 @@ def __unicode__(self):
return unicode(self.id)


class LowercasedString(TypeDecorator):
"""
A lowercased string
"""
impl = db.String
comparator_factory = CaseInsensitiveComparator

def __init__(self, length=320, *args, **kwargs):
super(LowercasedString, self).__init__(length=length, *args, **kwargs)

def process_bind_param(self, value, dialect):
if value is not None:
return value.lower()
return value

@property
def python_type(self):
return self.impl.type.python_type


class User(TimestampMixin, db.Model, BelongsToOrgMixin, UserMixin, PermissionsCheckMixin):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey('organizations.id'))
org = db.relationship(Organization, backref=db.backref("users", lazy="dynamic"))
name = Column(db.String(320))
email = Column(db.String(320))
email = Column(LowercasedString)
password_hash = Column(db.String(128), nullable=True)
# XXX replace with association table
group_ids = Column('groups', MutableList.as_mutable(postgresql.ARRAY(db.Integer)), nullable=True)
Expand All @@ -385,6 +406,8 @@ class User(TimestampMixin, db.Model, BelongsToOrgMixin, UserMixin, PermissionsCh
__table_args__ = (db.Index('users_org_id_email', 'org_id', 'email', unique=True),)

def __init__(self, *args, **kwargs):
if kwargs.get('email') is not None:
kwargs['email'] = kwargs['email'].lower()
super(User, self).__init__(*args, **kwargs)

def to_dict(self, with_api_key=False):
Expand Down
2 changes: 2 additions & 0 deletions query_runner/elasticsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ def collect_aggregations(mappings, rows, parent_key, data, row, result_columns,
for value in data:
result_row = get_row(rows, row)
collect_aggregations(mappings, rows, parent_key, value, result_row, result_columns, result_columns_index)
if 'doc_count' in value:
collect_value(mappings, result_row, 'doc_count', value['doc_count'], 'integer')
if 'key' in value:
if 'key_as_string' in value:
collect_value(mappings, result_row, parent_key, value['key_as_string'], 'string')
Expand Down
117 changes: 117 additions & 0 deletions query_runner/mapd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import absolute_import
import sys
import json

try:
import pymapd
enabled = True
except ImportError:
enabled = False

from redash.query_runner import BaseSQLQueryRunner, register
from redash.query_runner import TYPE_STRING, TYPE_DATE, TYPE_DATETIME, TYPE_INTEGER, TYPE_FLOAT, TYPE_BOOLEAN
from redash.utils import json_dumps

TYPES_MAP = {
0: TYPE_INTEGER,
1: TYPE_INTEGER,
2: TYPE_INTEGER,
3: TYPE_FLOAT,
4: TYPE_FLOAT,
5: TYPE_FLOAT,
6: TYPE_STRING,
7: TYPE_DATE,
8: TYPE_DATETIME,
9: TYPE_DATE,
10: TYPE_BOOLEAN,
11: TYPE_DATE,
12: TYPE_DATE
}


class Mapd(BaseSQLQueryRunner):

@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"host": {
"type": "string",
"default": "localhost"
},
"port": {
"type": "number",
"default": 9091
},
"user": {
"type": "string",
"default": "mapd",
"title": "username"
},
"password": {
"type": "string",
"default": "HyperInteractive"
},
"database": {
"type": "string",
"default": "mapd"
}
},
"order": ["user", "password", "host", "port", "database"],
"required": ["host", "port", "user", "password", "database"],
"secret": ["password"]
}

@classmethod
def enabled(cls):
return enabled

def connect_database(self):
connection = pymapd.connect(
user=self.configuration['user'],
password=self.configuration['password'],
host=self.configuration['host'],
port=self.configuration['port'],
dbname=self.configuration['database']
)
return connection

def run_query(self, query, user):
connection = self.connect_database()
cursor = connection.cursor()

try:
cursor.execute(query)
columns = self.fetch_columns([(i[0], TYPES_MAP.get(i[1], None)) for i in cursor.description])
rows = [dict(zip((c['name'] for c in columns), row)) for row in cursor]
data = {'columns': columns, 'rows': rows}
error = None
json_data = json_dumps(data)
finally:
cursor.close()
connection.close()

return json_data, error

def _get_tables(self, schema):
connection = self.connect_database()
try:
for table_name in connection.get_tables():
schema[table_name] = {'name': table_name, 'columns': []}
for row_column in connection.get_table_details(table_name):
schema[table_name]['columns'].append(row_column[0])
finally:
connection.close

return schema.values()

def test_connection(self):
connection = self.connect_database()
try:
tables = connection.get_tables()
num_tables = tables.count(tables)
finally:
connection.close

register(Mapd)
1 change: 1 addition & 0 deletions query_runner/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ def run_query(self, query, user):
json_data = None
error = e.args[1]
except KeyboardInterrupt:
cursor.close()
error = "Query cancelled by user."
json_data = None
finally:
Expand Down
2 changes: 2 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def all_settings():
# How many requests are allowed per IP to the login page before
# being throttled?
# See https://flask-limiter.readthedocs.io/en/stable/#rate-limit-string-notation

THROTTLE_LOGIN_PATTERN = os.environ.get('REDASH_THROTTLE_LOGIN_PATTERN', '50/hour')
LIMITER_STORAGE = os.environ.get("REDASH_LIMITER_STORAGE", REDIS_URL)

Expand Down Expand Up @@ -219,6 +220,7 @@ def all_settings():
'redash.query_runner.dynamodb_sql',
'redash.query_runner.mssql',
'redash.query_runner.memsql_ds',
'redash.query_runner.mapd',
'redash.query_runner.jql',
'redash.query_runner.google_analytics',
'redash.query_runner.axibase_tsd',
Expand Down
7 changes: 7 additions & 0 deletions utils/comparators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlalchemy import func
from sqlalchemy.ext.hybrid import Comparator


class CaseInsensitiveComparator(Comparator):
def __eq__(self, other):
return func.lower(self.__clause_element__()) == func.lower(other)

0 comments on commit bc1fa00

Please sign in to comment.