Skip to content

Commit

Permalink
Merge pull request opengisch#324 from AgenciaImplementacion/mssql_sup…
Browse files Browse the repository at this point in the history
…port_300

Add Sql Server support
  • Loading branch information
m-kuhn authored Sep 12, 2019
2 parents e587055 + c112f8a commit 788a607
Show file tree
Hide file tree
Showing 25 changed files with 1,693 additions and 5 deletions.
12 changes: 12 additions & 0 deletions .docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@ RUN apt-get update && \
python3-pip \
&& rm -rf /var/lib/apt/lists/*

# MSSQL: client side
RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | tee /etc/apt/sources.list.d/msprod.list
RUN apt-get update
RUN ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools unixodbc unixodbc-dev libqt5sql5-odbc

COPY ./requirements.txt /tmp/
RUN pip3 install -r /tmp/requirements.txt

# Avoid sqlcmd termination due to locale -- see https://github.com/Microsoft/mssql-docker/issues/163
RUN echo "nb_NO.UTF-8 UTF-8" > /etc/locale.gen
RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen
RUN locale-gen
ENV PATH="/usr/local/bin:${PATH}"

ENV LANG=C.UTF-8

WORKDIR /
9 changes: 9 additions & 0 deletions .docker/docker-compose.travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ services:
environment:
- ALLOW_IP_RANGE="172.0.0.0/8"

mssql:
image: microsoft/mssql-server-linux:2017-latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: <YourStrong!Passw0rd>
ports:
- "1433:1433"

qgis:
build:
context: ..
Expand All @@ -16,3 +24,4 @@ services:
- ${TRAVIS_BUILD_DIR}:/usr/src
links:
- postgres
- mssql
5 changes: 5 additions & 0 deletions .docker/run-docker-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#***************************************************************************

set -e

/usr/src/QgisModelBaker/tests/testdata/mssql/setup-mssql.sh


# rationale: Wait for postgres container to become available
echo "Wait a moment while loading the database."
while ! PGPASSWORD='docker' psql -h postgres -U docker -p 5432 -l &> /dev/null
Expand All @@ -26,6 +30,7 @@ do
done
echo ""


pushd /usr/src
xvfb-run nose2-3
popd
121 changes: 121 additions & 0 deletions QgisModelBaker/gui/panel/mssql_config_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
"""
/***************************************************************************
begin : 10/05/19
git sha : :%H$
copyright : (C) 2019 by Yesid Polania (BSF Swissphoto)
email : yesidpol.3@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
from qgis.PyQt.QtCore import pyqtSignal

from .db_config_panel import DbConfigPanel
from ...utils import get_ui_class
from QgisModelBaker.utils.qt_utils import (
Validators,
NonEmptyStringValidator)
from QgisModelBaker.utils.mssql_utils import get_odbc_drivers
from QgisModelBaker.libili2db.globals import DbActionType

WIDGET_UI = get_ui_class('mssql_settings_panel.ui')

class MssqlConfigPanel(DbConfigPanel, WIDGET_UI):

notify_fields_modified = pyqtSignal(str)

def __init__(self, parent, db_action_type):
DbConfigPanel.__init__(self, parent, db_action_type)
self.setupUi(self)

for item_odbc_driver in get_odbc_drivers():
self.mssql_odbc_driver.addItem(item_odbc_driver)

# define validators
self.validators = Validators()
nonEmptyValidator = NonEmptyStringValidator()

self.mssql_host_line_edit.setValidator(nonEmptyValidator)
self.mssql_database_line_edit.setValidator(nonEmptyValidator)
self.mssql_user_line_edit.setValidator(nonEmptyValidator)

self.mssql_host_line_edit.textChanged.connect(
self.validators.validate_line_edits)
self.mssql_host_line_edit.textChanged.emit(self.mssql_host_line_edit.text())
self.mssql_database_line_edit.textChanged.connect(
self.validators.validate_line_edits)
self.mssql_database_line_edit.textChanged.emit(
self.mssql_database_line_edit.text())
self.mssql_user_line_edit.textChanged.connect(
self.validators.validate_line_edits)
self.mssql_user_line_edit.textChanged.emit(self.mssql_user_line_edit.text())

self.mssql_host_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_instance_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_port_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_database_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_schema_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_user_line_edit.textChanged.connect(self.notify_fields_modified)
self.mssql_password_line_edit.textChanged.connect(self.notify_fields_modified)

def _show_panel(self):
if self.interlis_mode:
self.mssql_schema_line_edit.setPlaceholderText(
self.tr("[Leave empty to create a default schema]"))
else:
if self._db_action_type == DbActionType.IMPORT_DATA:
self.mssql_schema_line_edit.setPlaceholderText(
self.tr("[Leave empty to import data into a default schema]"))
else:
self.mssql_schema_line_edit.setPlaceholderText(
self.tr("[Leave empty to load all schemas in the database]"))

def get_fields(self, configuration):
configuration.dbhost = self.mssql_host_line_edit.text().strip()
configuration.dbinstance = self.mssql_instance_line_edit.text().strip()
configuration.dbport = self.mssql_port_line_edit.text().strip()
configuration.dbusr = self.mssql_user_line_edit.text().strip()
configuration.database = self.mssql_database_line_edit.text().strip()
configuration.dbschema = self.mssql_schema_line_edit.text().strip().lower()
configuration.dbpwd = self.mssql_password_line_edit.text()
configuration.db_odbc_driver = self.mssql_odbc_driver.currentText()

def set_fields(self, configuration):
self.mssql_host_line_edit.setText(configuration.dbhost)
self.mssql_instance_line_edit.setText(configuration.dbinstance)
self.mssql_port_line_edit.setText(configuration.dbport)
self.mssql_user_line_edit.setText(configuration.dbusr)
self.mssql_database_line_edit.setText(configuration.database)
self.mssql_schema_line_edit.setText(configuration.dbschema)
self.mssql_password_line_edit.setText(configuration.dbpwd)

index = self.mssql_odbc_driver.findText(configuration.db_odbc_driver)
if index != -1:
self.mssql_odbc_driver.setCurrentIndex(index)

def is_valid(self):
result = False
message = ''
if not self.mssql_host_line_edit.text().strip():
message = self.tr("Please set a host before creating the project.")
self.mssql_host_line_edit.setFocus()
elif not "{}".format(self.mssql_database_line_edit.text().strip()):
message = self.tr("Please set a database before creating the project.")
self.mssql_database_line_edit.setFocus()
elif not self.mssql_user_line_edit.text().strip():
message = self.tr("Please set a database user before creating the project.")
self.mssql_user_line_edit.setFocus()
elif not self.mssql_odbc_driver.currentText():
message = self.tr("Please set a odbc driver before creating the project.")
else:
result = True

return result, message
2 changes: 2 additions & 0 deletions QgisModelBaker/libili2db/ili2dbconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ def __init__(self):
self.ilifile = ''
self.ilimodels = ''
self.tomlfile = ''
self.dbinstance = ''
self.db_odbc_driver = ''

def to_ili2db_args(self):

Expand Down
17 changes: 14 additions & 3 deletions QgisModelBaker/libqgsprojectgen/dataobjects/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@
QgsVectorLayer,
QgsDataSourceUri,
QgsWkbTypes,
QgsRectangle
QgsRectangle,
QgsCoordinateReferenceSystem
)
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtCore import QCoreApplication, QSettings


class Layer(object):

def __init__(self, provider, uri, name, extent, geometry_column=None, wkb_type=QgsWkbTypes.Unknown, alias=None, is_domain=False, is_structure=False, is_nmrel=False, display_expression=None):
def __init__(self, provider, uri, name, srid, extent, geometry_column=None, wkb_type=QgsWkbTypes.Unknown, alias=None, is_domain=False, is_structure=False, is_nmrel=False, display_expression=None):
self.provider = provider
self.uri = uri
self.name = name
Expand All @@ -55,6 +56,7 @@ def __init__(self, provider, uri, name, extent, geometry_column=None, wkb_type=Q
self.is_structure = is_structure

self.is_nmrel = is_nmrel
self.srid = srid
""" If is_nmrel is set to true it is a junction table in a N:M relation.
Or in ili2db terms, the table is marked as ASSOCIATION in t_ili2db_table_prop.settings. """

Expand Down Expand Up @@ -85,7 +87,16 @@ def load(self, definition):
def create(self):
if self.__layer is None:
layer_name = self.alias or self.name

settings = QSettings()
# Take the "CRS for new layers" config, overwrite it while loading layers and...
old_proj_value = settings.value("/Projections/defaultBehaviour", "prompt", type=str)
settings.setValue("/Projections/defaultBehaviour", "useProject")
self.__layer = QgsVectorLayer(self.uri, layer_name, self.provider)
settings.setValue("/Projections/defaultBehavior", old_proj_value)

if self.srid is not None and not self.__layer.crs().authid() == "EPSG:{}".format(self.srid):
self.__layer.setCrs(QgsCoordinateReferenceSystem().fromEpsgId(self.srid))
if self.is_domain:
self.__layer.setReadOnly()
if self.display_expression:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(self, configuration: Ili2DbCommandConfiguration):
self.configuration = configuration

@abstractmethod
def get_uri(self, su: bool):
def get_uri(self, su: bool = False):
"""Gets database uri (connection string) for db connectors (:class:`DBConnector`).
:param bool su: *True* to use super user credentials, *False* otherwise.
Expand Down
13 changes: 13 additions & 0 deletions QgisModelBaker/libqgsprojectgen/db_factory/db_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .layer_uri import LayerUri
from QgisModelBaker.gui.panel.db_config_panel import DbConfigPanel
from .db_command_config_manager import DbCommandConfigManager
from QgisModelBaker.libqgsprojectgen.dataobjects import Field


class DbFactory(ABC):
Expand Down Expand Up @@ -121,3 +122,15 @@ def get_specific_messages(self):
}

return messages

def customize_widget_editor(self, field: Field, data_type: str):
"""Allows customizing the way a field is shown in the widget editor.
For instance, a boolean field can be shown as a checkbox.
:param field: The field that will be customized
:type field: :class:`Field`
:param data_type: The type of field
:return None
"""
pass
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@
from .db_factory import DbFactory
from .pg_factory import PgFactory
from .gpkg_factory import GpkgFactory
from .mssql_factory import MssqlFactory
from QgisModelBaker.libili2db.globals import DbIliMode


class DbSimpleFactory:
"""Provides a single point (simple factory) to create a database factory (:class:`DbFactory`).
"""

_available_databases = [DbIliMode.pg, DbIliMode.gpkg]
_available_databases = [DbIliMode.pg, DbIliMode.gpkg, DbIliMode.mssql]
_index_default_db = 0

def create_factory(self, ili_mode: DbIliMode) -> DbFactory:
Expand All @@ -45,6 +46,8 @@ def create_factory(self, ili_mode: DbIliMode) -> DbFactory:
result = PgFactory()
elif ili_mode & DbIliMode.gpkg:
result = GpkgFactory()
elif ili_mode & DbIliMode.mssql:
result = MssqlFactory()

return result

Expand Down Expand Up @@ -77,3 +80,4 @@ def default_database(self):
:rtype: :class:`DbIliMode`
"""
return self._available_databases[self._index_default_db]

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""
/***************************************************************************
begin : 09/05/19
git sha : :%H$
copyright : (C) 2019 by Yesid Polania (BSF Swissphoto)
email : yesidpol.3@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
from qgis.PyQt.QtCore import QSettings
from .db_command_config_manager import DbCommandConfigManager


class MssqlCommandConfigManager(DbCommandConfigManager):

_settings_base_path = 'QgisModelBaker/ili2mssql/'

def __init__(self, configuration):
DbCommandConfigManager.__init__(self, configuration)

def get_uri(self, su=False):
separator = ';'
uri = []
uri += ['DRIVER={{{}}}'.format(self.configuration.db_odbc_driver)]
host = self.configuration.dbhost
if self.configuration.dbinstance:
host += '\\' + self.configuration.dbinstance
if self.configuration.dbport:
host += ',' + self.configuration.dbport

uri += ['SERVER={}'.format(host)]
uri += ['DATABASE={}'.format(self.configuration.database)]
uri += ['UID={}'.format(self.configuration.dbusr)]
uri += ['PWD={}'.format(self.configuration.dbpwd)]

return separator.join(uri)

def get_db_args(self, hide_password=False):
db_args = list()
db_args += ["--dbhost", self.configuration.dbhost]
if self.configuration.dbport:
db_args += ["--dbport", self.configuration.dbport]
db_args += ["--dbusr", self.configuration.dbusr]
if self.configuration.dbpwd:
if hide_password:
db_args += ["--dbpwd", '******']
else:
db_args += ["--dbpwd", self.configuration.dbpwd]
db_args += ["--dbdatabase", self.configuration.database]
db_args += ["--dbschema", self.configuration.dbschema or self.configuration.database]
if self.configuration.dbinstance:
db_args += ["--dbinstance", self.configuration.dbinstance]

return db_args

def save_config_in_qsettings(self):
settings = QSettings()

settings.setValue(self._settings_base_path + 'host', self.configuration.dbhost)
settings.setValue(self._settings_base_path + 'instance', self.configuration.dbhost)
settings.setValue(self._settings_base_path + 'port', self.configuration.dbport)
settings.setValue(self._settings_base_path + 'user', self.configuration.dbusr)
settings.setValue(self._settings_base_path + 'database', self.configuration.database)
settings.setValue(self._settings_base_path + 'schema', self.configuration.dbschema)
settings.setValue(self._settings_base_path + 'password', self.configuration.dbpwd)
settings.setValue(self._settings_base_path + 'odbc_driver', self.configuration.db_odbc_driver)

def load_config_from_qsettings(self):
settings = QSettings()

self.configuration.dbhost = settings.value(self._settings_base_path + 'host', 'localhost')
self.configuration.dbport = settings.value(self._settings_base_path + 'instance')
self.configuration.dbport = settings.value(self._settings_base_path + 'port')
self.configuration.dbusr = settings.value(self._settings_base_path + 'user')
self.configuration.database = settings.value(self._settings_base_path + 'database')
self.configuration.dbschema = settings.value(self._settings_base_path + 'schema')
self.configuration.dbpwd = settings.value(self._settings_base_path + 'password')
self.configuration.db_odbc_driver = settings.value(self._settings_base_path + 'odbc_driver')
Loading

0 comments on commit 788a607

Please sign in to comment.