Skip to content

Commit

Permalink
[#141 , #143] Support Microsoft SQL Server 17 and 19
Browse files Browse the repository at this point in the history
* [#141] Support Microsoft SQL Server 17 and 19
  • Loading branch information
javrasya authored Mar 17, 2020
1 parent 405df9b commit 5b71699
Show file tree
Hide file tree
Showing 63 changed files with 578 additions and 1,104 deletions.
4 changes: 3 additions & 1 deletion .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
exclude_paths:
- 'river/migrations/*'
- '**/tests/**'
- 'settings/**'
- 'settings/**'
- 'river/sql/**/**'
- 'docs/conf.py'
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,20 @@ matrix:
env: TOXENV=py36-dj2.2-mysql8.0
services:
- docker
- python: "3.6"
env: TOXENV=py36-dj2.2-mssql17
services:
- docker
- python: "3.6"
env: TOXENV=py36-dj2.2-mssql19
services:
- docker

install:
- curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
- curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
- sudo apt-get update
- sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 g++ unixodbc-dev
- pip install tox-travis
- pip install tox-docker
- pip install coveralls
Expand Down
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include README.md
include *.txt
include *.txt
include river/sql/mssql/get_available_approvals.sql
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ Supported (Tested) Databases:
| 8.0 |||
+------------+--------+---------+

+------------+--------+---------+
| MSSQL | Tested | Support |
+------------+--------+---------+
| 19 |||
+------------+--------+---------+
| 17 |||
+------------+--------+---------+


Usage
-----
Expand Down Expand Up @@ -242,6 +250,18 @@ out of the box. All you need to do is to run;
python manage.py migrate river
3.1.X to 3.2.X
^^^^^^^^^^^^^^

``django-river`` started to support **Microsoft SQL Server 17 and 19** after version 3.2.0 but the previous migrations didn't get along with it. We needed to reset all
the migrations to have fresh start. If you have already migrated to version `3.1.X` all you need to do is to pull your migrations back to the beginning.


.. code:: bash
python manage.py migrate --fake river zero
python manage.py migrate --fake river
FAQ
---

Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
# built documents.
#
# The short X.Y version.
version = '3.1.4'
version = '3.2.0'
# The full version, including alpha/beta/rc tags.
release = '3.1.4'
release = '3.2.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
1 change: 1 addition & 0 deletions docs/migration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Migration Guide
:maxdepth: 2

migration_2_to_3
migration_31_to_32
hooking

13 changes: 13 additions & 0 deletions docs/migration/migration_31_to_32.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.. _migration_31_to_32:

3.1.X to 3.2.X
==============

``django-river`` started to support **Microsoft SQL Server 17 and 19** after version 3.2.0 but the previous migrations didn't get along with it. We needed to reset all
the migrations to have fresh start. If you have already migrated to version `3.1.X` all you need to do is to pull your migrations back to the beginning.


.. code:: bash
python manage.py migrate --fake river zero
python manage.py migrate --fake river
7 changes: 7 additions & 0 deletions docs/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ Supported (Tested) Databases:
| 8.0 |||
+------------+--------+---------+

+------------+--------+---------+
| MSSQL | Tested | Support |
+------------+--------+---------+
| 19 |||
+------------+--------+---------+
| 17 |||
+------------+--------+---------+

Example Scenarios
-----------------
Expand Down
11 changes: 4 additions & 7 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
__author__ = 'ahmetdal'


#!/usr/bin/env python
import os
# !/usr/bin/env python
import os
import sys

if __name__ == "__main__":
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.with_sqlite3")

from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)
execute_from_command_line(sys.argv)
2 changes: 0 additions & 2 deletions river/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
__author__ = 'ahmetdal'

default_app_config = 'river.apps.RiverApp'
41 changes: 28 additions & 13 deletions river/config.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
from django.contrib.auth.models import Permission, Group
from django.contrib.contenttypes.models import ContentType

__author__ = 'ahmetdal'
from django.db import connection


class RiverConfig(object):
# from settings
prefix = 'RIVER'

def __init__(self):
self.cached_settings = None

@property
def settings(self):
if self.cached_settings:
return self.cached_settings
else:
from django.conf import settings
allowed_configurations = {
'CONTENT_TYPE_CLASS': ContentType,
'USER_CLASS': settings.AUTH_USER_MODEL,
'PERMISSION_CLASS': Permission,
'GROUP_CLASS': Group,
'INJECT_MODEL_ADMIN': False
}
river_settings = {}
for key, default in allowed_configurations.items():
river_settings[key] = getattr(settings, self.get_with_prefix(key), default)

river_settings['IS_MSSQL'] = connection.vendor == 'microsoft'
self.cached_settings = river_settings

return self.cached_settings

def get_with_prefix(self, config):
return '%s_%s' % (self.prefix, config)

def __getattr__(self, item):
from django.conf import settings
allowed_configurations = {
'CONTENT_TYPE_CLASS': ContentType,
'USER_CLASS': settings.AUTH_USER_MODEL,
'PERMISSION_CLASS': Permission,
'GROUP_CLASS': Group,
'INJECT_MODEL_ADMIN': False
}
if item in allowed_configurations.keys():
default_value = allowed_configurations[item]
return getattr(settings, self.get_with_prefix(item), default_value)
if item in self.settings:
return self.settings[item]
else:
raise AttributeError(item)

Expand Down
79 changes: 15 additions & 64 deletions river/core/classworkflowobject.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,36 @@
from django.contrib import auth
from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Q, Min, CharField
from django.db.models.functions import Cast
from django_cte import With

from river.models import State, TransitionApprovalMeta, TransitionApproval, PENDING, Workflow
from river.driver.mssql_driver import MsSqlDriver
from river.driver.orm_driver import OrmDriver
from river.models import State, TransitionApprovalMeta, Workflow, app_config


class ClassWorkflowObject(object):

def __init__(self, wokflow_object_class, field_name):
self.wokflow_object_class = wokflow_object_class
self.field_name = field_name
self._cached_workflow = None
self.workflow = Workflow.objects.filter(field_name=self.field_name, content_type=self._content_type).first()
self._cached_river_driver = None

@property
def workflow(self):
if not self._cached_workflow:
self._cached_workflow = Workflow.objects.filter(field_name=self.field_name, content_type=self._content_type).first()
return self._cached_workflow
def _river_driver(self):
if self._cached_river_driver:
return self._cached_river_driver
else:
if app_config.IS_MSSQL:
self._cached_river_driver = MsSqlDriver(self.workflow, self.wokflow_object_class, self.field_name)
else:
self._cached_river_driver = OrmDriver(self.workflow, self.wokflow_object_class, self.field_name)
return self._cached_river_driver

def get_on_approval_objects(self, as_user):
approvals = self.get_available_approvals(as_user)
object_ids = list(approvals.values_list('object_id', flat=True))
return self.wokflow_object_class.objects.filter(pk__in=object_ids)

def get_available_approvals(self, as_user):
those_with_max_priority = With(
TransitionApproval.objects.filter(
workflow=self.workflow, status=PENDING
).values(
'workflow', 'object_id', 'transition'
).annotate(min_priority=Min('priority'))
)

workflow_objects = With(
self.wokflow_object_class.objects.all(),
name="workflow_object"
)

approvals_with_max_priority = those_with_max_priority.join(
self._authorized_approvals(as_user),
workflow_id=those_with_max_priority.col.workflow_id,
object_id=those_with_max_priority.col.object_id,
transition_id=those_with_max_priority.col.transition_id,
).with_cte(
those_with_max_priority
).annotate(
object_id_as_str=Cast('object_id', CharField(max_length=200)),
min_priority=those_with_max_priority.col.min_priority
).filter(min_priority=F("priority"))

return workflow_objects.join(
approvals_with_max_priority, object_id_as_str=Cast(workflow_objects.col.pk, CharField(max_length=200))
).with_cte(
workflow_objects
).filter(transition__source_state=getattr(workflow_objects.col, self.field_name + "_id"))
return self._river_driver.get_available_approvals(as_user)

@property
def initial_state(self):
Expand All @@ -67,30 +42,6 @@ def final_states(self):
final_approvals = TransitionApprovalMeta.objects.filter(workflow=self.workflow, children__isnull=True)
return State.objects.filter(pk__in=final_approvals.values_list("transition_meta__destination_state", flat=True))

def _authorized_approvals(self, as_user):
group_q = Q()
for g in as_user.groups.all():
group_q = group_q | Q(groups__in=[g])

permissions = []
for backend in auth.get_backends():
permissions.extend(backend.get_all_permissions(as_user))

permission_q = Q()
for p in permissions:
label, codename = p.split('.')
permission_q = permission_q | Q(permissions__content_type__app_label=label,
permissions__codename=codename)

return TransitionApproval.objects.filter(
Q(workflow=self.workflow, status=PENDING) &
(
(Q(transactioner__isnull=True) | Q(transactioner=as_user)) &
(Q(permissions__isnull=True) | permission_q) &
(Q(groups__isnull=True) | group_q)
)
)

@property
def _content_type(self):
return ContentType.objects.get_for_model(self.wokflow_object_class)
2 changes: 1 addition & 1 deletion river/core/instanceworkflowobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def get_available_states(self, as_user=None):
return State.objects.filter(pk__in=all_destination_state_ids)

def get_available_approvals(self, as_user=None, destination_state=None):
qs = self.class_workflow.get_available_approvals(as_user).filter(object_id=self.workflow_object.pk)
qs = self.class_workflow.get_available_approvals(as_user, ).filter(object_id=self.workflow_object.pk)
if destination_state:
qs = qs.filter(transition__destination_state=destination_state)

Expand Down
Empty file added river/driver/__init__.py
Empty file.
52 changes: 52 additions & 0 deletions river/driver/mssql_driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
from os.path import dirname

import six
from django.db import connection
from django.contrib.auth.models import Permission

from river.driver.river_driver import RiverDriver
from river.models import TransitionApproval


class MsSqlDriver(RiverDriver):

def __init__(self, *args, **kwargs):
super(MsSqlDriver, self).__init__(*args, **kwargs)
with open(os.path.join(dirname(dirname(__file__)), "sql", "mssql", "get_available_approvals.sql")) as f:
self.available_approvals_sql_template = f.read().replace("river.dbo.", "")
self.cursor = connection.cursor()

def get_available_approvals(self, as_user):
with connection.cursor() as cursor:
cursor.execute(self._clean_sql % {
"workflow_id": self.workflow.pk,
"transactioner_id": as_user.pk,
"field_name": self.field_name,
"permission_ids": self._permission_ids_str(as_user),
"group_ids": self._group_ids_str(as_user),
"workflow_object_table": self.wokflow_object_class._meta.db_table,
"object_pk_name": self.wokflow_object_class._meta.pk.name
})

return TransitionApproval.objects.filter(pk__in=[row[0] for row in cursor.fetchall()])

@staticmethod
def _permission_ids_str(as_user):
permissions = as_user.user_permissions.all() | Permission.objects.filter(group__user=as_user)
return ",".join(list(six.moves.map(str, permissions.values_list("pk", flat=True))) or ["-1"])

@staticmethod
def _group_ids_str(as_user):
return ",".join(list(six.moves.map(str, as_user.groups.all().values_list("pk", flat=True))) or ["-1"])

@property
def _clean_sql(self):
return self.available_approvals_sql_template \
.replace("'%(workflow_id)s'", "%(workflow_id)s") \
.replace("'%(transactioner_id)s'", "%(transactioner_id)s") \
.replace("'%(field_name)s'", "%(field_name)s") \
.replace("'%(permission_ids)s'", "%(permission_ids)s") \
.replace("'%(group_ids)s'", "%(group_ids)s") \
.replace("'%(workflow_object_table)s'", "%(workflow_object_table)s") \
.replace("'%(object_pk_name)s'", "%(object_pk_name)s")
Loading

0 comments on commit 5b71699

Please sign in to comment.