Django testing MS-SQL legacy database with stored procedures - python

In Django I would like to use Unit Test for testing a MS-SQL Server legacy database. The database is using stored procedures for adding data. The situation is as follow:
The MS-SQL database has the following settings in Django:
DATABASES['vadain_import'] = {
'ENGINE': 'sql_server.pyodbc',
'USER': 'xx',
'PASSWORD': 'xxx',
'HOST': '192.168.103.102',
'PORT': '',
'NAME': 'Vadain_import',
'OPTIONS': {
'driver': 'SQL Server Native Client 11.0',
'MARS_Connection': True,
}
}
The models of the database are made with inspectdb, example:
class WaOrders(models.Model):
order_id = models.IntegerField(primary_key=True, db_column='intOrderId')
type = models.TextField(db_column='chvType', blank=True)
class Meta:
db_table = 'WA_ORDERS'
database_name = 'vadain_import'
managed = False
# (There's a lot more of properties and models)
In models is executing the stored procedures. I cann't use the save
functionality of Django, like WAOrders.save(), because in the MS-SQL
database the primary key's are generated in the stored procedure.
#classmethod
def export(cls, **kwargs):
# Stored procedure for adding data into the db
sql = "declare #id int \
set #id=0\
declare #error varchar(1000)\
set #error=''\
exec UspWA_OrderImport\
#intOrderId=#id out\
,#chvType=N'%s'" % kwargs['type'] + " \
,#chvErrorMsg=#error output\
select #id as id, #error as 'error' \
"
# Connection Vadain db
cursor = connections['vadain_import'].cursor()
# Execute sql stored procedure, no_count is needed otherwise it's returning an error
# Return value primary key of WAOrders
try:
cursor.execute(no_count + sql)
result = cursor.fetchone()
# Check if primary key is set and if there are no errors:
if result[0] > 1 and result[1] == '':
# Commit SP
cursor.execute('COMMIT')
return result[0]
There is a mapping for creating the models, because the MS-SQL
database expect different data then the normal objects, like ‘order’.
def adding_data_into_vadain(self, order):
for curtain in order.curtains.all():
order_id = WaOrders.export(
type=format(curtain.type)
)
# relation with default and vadain db.
order.exported_id = order_id
order.save()
The function is working proper by running the program, but by running ‘manage.py test’ will be created a test databases. This is given the following problems:
By creating test database is missing the south tables (this is also not needed in the legacy database)
By changing the SOUTH_TESTS_MIGRATE to False I’m getting the error message that the tables are already exists of the default database.
My test is as follow:
class AddDataServiceTest(TestCase):
fixtures = ['order']
def test_data_service(self):
# add data into vadain_import data
for order in Order.objects.all():
AddingDataService.adding_data_into_vadain(order)
# test the exported values
for order in Order.objects.all():
exported_order = WaOrders.objects.get(order_exported_id=order.exported_id)
self.assertEqual(exported_order.type, 'Pleat curtain')
Can somebody advise me how I can test the situation?

maybe inside your WaOrders you can extend save() method and call there export function.

Related

Populate Specific Database With factory_boy Data for testing

I have an application that has some unmanaged models that pull from a table in a different DB.
I've created Factories for all my models and in most cases factory_boy has worked great!
The problem I'm running into has to do with a specific unmanaged model that uses a manager on one of the fields.
class DailyRoll(StaffModel):
id = models.IntegerField(db_column='ID', primary_key=True)
location_id = models.IntegerField(db_column='locationID')
grade = models.CharField(db_column='grade', max_length=10)
end_year = models.IntegerField(db_column='endYear')
run_date = models.DateField(db_column='runDate')
person_count = models.IntegerField(db_column='personCount')
contained_count = models.IntegerField(db_column='containedCount')
double_count = models.IntegerField(db_column='doubleCount')
aide_count = models.IntegerField(db_column='aideCount')
one_count = models.IntegerField(db_column='oneCount')
bi_count = models.IntegerField(db_column='biCount')
last_run_date = managers.DailyRollManager()
class Meta(object):
managed = False
db_table = '[DATA].[position_dailyRoll]'
The manager looks like this:
class DailyRollManager(models.Manager):
def get_queryset(self):
last_run_date = super().get_queryset().using('staff').last().run_date
return super().get_queryset().using('staff').filter(
run_date=last_run_date
)
My factory_boy setup looks like this (it's very basic at the moment, because I'm just trying to get it to work):
class DailyRollFactory(factory.Factory):
class Meta:
model = staff.DailyRoll
id = 1
location_id = 1
grade = 1
end_year = 2019
run_date = factory.fuzzy.FuzzyDateTime(timezone.now())
person_count = 210
contained_count = 1
double_count = 2
aide_count = 3
one_count = 4
bi_count = 5
last_run_date = managers.DailyRollManager()
I'm sure the last_run_date is setup improperly - but I don't know how best to handle using a manager in factory_boy.
When I run my tests - it creates a default sqlite3 database named 'defaultdb' to run tests against (we use SQL Server in production - but because of security we cannot give django control over master to manage testing). To accomplish this I had to create a workaround, but it works.
My settings look like this:
if 'test' in sys.argv or 'test_coverage' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'defaultdb.sqlite3'),
},
'staff': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'staff.sqlite3'),
},
}
My test is simple like this:
def test_index_returns_200_and_correct_template(self):
"""
index page returns a 200
"""
# Load test data which index needs
# if this is not included the test fails with
# 'no such table: [DATA].[position_dailyRoll]'
f.DailyRollFactory()
response = self.client.get(reverse('position:index'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'position/index.html')
If I don't include f.DailyRollFactory() in the test it fails with this error:
no such table: [DATA].[position_dailyRoll]
When I include f.DailyRollFactory() it gives this error:
TypeError: 'last_run_date' is an invalid keyword argument for this function
I think that ultimately that manager is trying to look at the 'staff' database - which I have being created, but it's empty and contains no data.
I am trying to figure out:
A) How can I pre-populate that staff database with data like what I have in my factory-boy model? I think that may resolve the issue I'm running into. Will it? I am not sure.
B) Is that the right way to handle the manager in a factory_boy Factory? I'm not sure exactly how to handle that.
I ended up using Pytest-Django as that testing framework actually allows for doing exactly what I wanted.
Using the --nomigrations flag, it takes my models which are only managed by django in tests and creates the appropriate table name for them (using their db_table attribute) in the test database. Then I can use factory_boy to create mock data and test it up!

How to programmatically generate the CREATE TABLE SQL statement for a given model in Django?

I need to programmatically generate the CREATE TABLE statement for a given unmanaged model in my Django app (managed = False)
Since i'm working on a legacy database, i don't want to create a migration and use sqlmigrate.
The ./manage.py sql command was useful for this purpose but it has been removed in Django 1.8
Do you know about any alternatives?
As suggested, I post a complete answer for the case, that the question might imply.
Suppose you have an external DB table, that you decided to access as a Django model and therefore have described it as an unmanaged model (Meta: managed = False).
Later you need to be able to create it in your code, e.g for some tests using your local DB. Obviously, Django doesn't make migrations for unmanaged models and therefore won't create it in your test DB.
This can be solved using Django APIs without resorting to raw SQL - SchemaEditor. See a more complete example below, but as a short answer you would use it like this:
from django.db import connections
with connections['db_to_create_a_table_in'].schema_editor() as schema_editor:
schema_editor.create_model(YourUnmanagedModelClass)
A practical example:
# your_app/models/your_model.py
from django.db import models
class IntegrationView(models.Model):
"""A read-only model to access a view in some external DB."""
class Meta:
managed = False
db_table = 'integration_view'
name = models.CharField(
db_column='object_name',
max_length=255,
primaty_key=True,
verbose_name='Object Name',
)
some_value = models.CharField(
db_column='some_object_value',
max_length=255,
blank=True,
null=True,
verbose_name='Some Object Value',
)
# Depending on the situation it might be a good idea to redefine
# some methods as a NOOP as a safety-net.
# Note, that it's not completely safe this way, but might help with some
# silly mistakes in user code
def save(self, *args, **kwargs):
"""Preventing data modification."""
pass
def delete(self, *args, **kwargs):
"""Preventing data deletion."""
pass
Now, suppose you need to be able to create this model via Django, e.g. for some tests.
# your_app/tests/some_test.py
# This will allow to access the `SchemaEditor` for the DB
from django.db import connections
from django.test import TestCase
from your_app.models.your_model import IntegrationView
class SomeLogicTestCase(TestCase):
"""Tests some logic, that uses `IntegrationView`."""
# Since it is assumed, that the `IntegrationView` is read-only for the
# the case being described it's a good idea to put setup logic in class
# setup fixture, that will run only once for the whole test case
#classmethod
def setUpClass(cls):
"""Prepares `IntegrationView` mock data for the test case."""
# This is the actual part, that will create the table in the DB
# for the unmanaged model (Any model in fact, but managed models will
# have their tables created already by the Django testing framework)
# Note: Here we're able to choose which DB, defined in your settings,
# will be used to create the table
with connections['external_db'].schema_editor() as schema_editor:
schema_editor.create_model(IntegrationView)
# That's all you need, after the execution of this statements
# a DB table for `IntegrationView` will be created in the DB
# defined as `external_db`.
# Now suppose we need to add some mock data...
# Again, if we consider the table to be read-only, the data can be
# defined here, otherwise it's better to do it in `setUp()` method.
# Remember `IntegrationView.save()` is overridden as a NOOP, so simple
# calls to `IntegrationView.save()` or `IntegrationView.objects.create()`
# won't do anything, so we need to "Improvise. Adapt. Overcome."
# One way is to use the `save()` method of the base class,
# but provide the instance of our class
integration_view = IntegrationView(
name='Biggus Dickus',
some_value='Something really important.',
)
super(IntegrationView, integration_view).save(using='external_db')
# Another one is to use the `bulk_create()`, which doesn't use
# `save()` internally, and in fact is a better solution
# if we're creating many records
IntegrationView.objects.using('external_db').bulk_create([
IntegrationView(
name='Sillius Soddus',
some_value='Something important',
),
IntegrationView(
name='Naughtius Maximus',
some_value='Whatever',
),
])
# Don't forget to clean after
#classmethod
def tearDownClass(cls):
with connections['external_db'].schema_editor() as schema_editor:
schema_editor.delete_model(IntegrationView)
def test_some_logic_using_data_from_integration_view(self):
self.assertTrue(IntegrationView.objects.using('external_db').filter(
name='Biggus Dickus',
))
To make the example more complete... Since we're using multiple DB (default and external_db) Django will try to run migrations on both of them for the tests and as of now there's no option in DB settings to prevent this. So we have to use a custom DB router for testing.
# your_app/tests/base.py
class PreventMigrationsDBRouter:
"""DB router to prevent migrations for specific DBs during tests."""
_NO_MIGRATION_DBS = {'external_db', }
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""Actually disallows migrations for specific DBs."""
return db not in self._NO_MIGRATION_DBS
And a test settings file example for the described case:
# settings/test.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.oracle',
'NAME': 'db_name',
'USER': 'username',
'HOST': 'localhost',
'PASSWORD': 'password',
'PORT': '1521',
},
# For production here we would have settings to connect to the external DB,
# but for testing purposes we could get by with an SQLite DB
'external_db': {
'ENGINE': 'django.db.backends.sqlite3',
},
}
# Not necessary to use a router in production config, since if the DB
# is unspecified explicitly for some action Django will use the `default` DB
DATABASE_ROUTERS = ['your_app.tests.base.PreventMigrationsDBRouter', ]
Hope this detailed new Django user user-friendly example will help someone and save their time.
unfortunately there seems to be no easy way to do this, but for your luck I have just succeeded in producing a working snippet for you digging in the internals of the django migrations jungle.
Just:
save the code to get_sql_create_table.py (in example)
do $ export DJANGO_SETTINGS_MODULE=yourproject.settings
launch the script with python get_sql_create_table.py yourapp.yourmodel
and it should output what you need.
Hope it helps!
import django
django.setup()
from django.db.migrations.state import ModelState
from django.db.migrations import operations
from django.db.migrations.migration import Migration
from django.db import connections
from django.db.migrations.state import ProjectState
def get_create_sql_for_model(model):
model_state = ModelState.from_model(model)
# Create a fake migration with the CreateModel operation
cm = operations.CreateModel(name=model_state.name, fields=model_state.fields)
migration = Migration("fake_migration", "app")
migration.operations.append(cm)
# Let the migration framework think that the project is in an initial state
state = ProjectState()
# Get the SQL through the schema_editor bound to the connection
connection = connections['default']
with connection.schema_editor(collect_sql=True, atomic=migration.atomic) as schema_editor:
state = migration.apply(state, schema_editor, collect_sql=True)
# return the CREATE TABLE statement
return "\n".join(schema_editor.collected_sql)
if __name__ == "__main__":
import importlib
import sys
if len(sys.argv) < 2:
print("Usage: {} <app.model>".format(sys.argv[0]))
sys.exit(100)
app, model_name = sys.argv[1].split('.')
models = importlib.import_module("{}.models".format(app))
model = getattr(models, model_name)
rv = get_create_sql_for_model(model)
print(rv)
For Django v4.1.3, the above get_create_sql_for_model soruce code changed like this:
from django.db.migrations.state import ModelState
from django.db.migrations import operations
from django.db.migrations.migration import Migration
from django.db import connections
from django.db.migrations.state import ProjectState
def get_create_sql_for_model(model):
model_state = ModelState.from_model(model)
table_name = model_state.options['db_table']
# Create a fake migration with the CreateModel operation
cm = operations.CreateModel(name=model_state.name, fields=model_state.fields.items())
migration = Migration("fake_migration", "app")
migration.operations.append(cm)
# Let the migration framework think that the project is in an initial state
state = ProjectState()
# Get the SQL through the schema_editor bound to the connection
connection = connections['default']
with connection.schema_editor(collect_sql=True, atomic=migration.atomic) as schema_editor:
state = migration.apply(state, schema_editor, collect_sql=True)
sqls = schema_editor.collected_sql
items = []
for sql in sqls:
if sql.startswith('--'):
continue
items.append(sql)
return table_name,items
#EOP
I used it to create all tables (like the command syncdb of old Django version):
for app in settings.INSTALLED_APPS:
app_name = app.split('.')[0]
app_models = apps.get_app_config(app_name).get_models()
for model in app_models:
table_name,sqls = get_create_sql_for_model(model)
if settings.DEBUG:
s = "SELECT COUNT(*) AS c FROM sqlite_master WHERE name = '%s'" % table_name
else:
s = "SELECT COUNT(*) AS c FROM information_schema.TABLES WHERE table_name='%s'" % table_name
rs = select_by_raw_sql(s)
if not rs[0]['c']:
for sql in sqls:
exec_by_raw_sql(sql)
print('CREATE TABLE DONE:%s' % table_name)
The full soure code can be found at Django syncdb command came back for v4.1.3 version

Django: How do I perform database introspection with multiple databases?

I have code that works for getting models and fields from a Django database. However, it only works on the default database.
This function wants a database name, and I'd like to get the tables and fields for that database.
def browse_datasource(request, dbname):
table_info = []
# This is what I'd /like/ to be able to do, but it doesn't work:
# tables = connections[dbname].introspection.table_names()
tables = connection.introspection.table_names()
found_models = connection.introspection.installed_models(tables)
for model in found_models:
tablemeta = model._meta
columns = [field.column for field in model._meta.fields]
table_info.append([model.__name__, columns])
How can I perform introspection on the non-default databases? Is there a correct way to get connection.introspection for a database with the name "example", for example?
I found the solution. The trick is getting the database connection from the connections list, then getting a cursor and passing that to introspection.table_names, like so:
table_info = []
conn = connections[dbname]
cursor = conn.cursor()
tables = conn.introspection.table_names(cursor)
found_models = conn.introspection.installed_models(tables)
for model in found_models:
tablemeta = model._meta

Refresh mongodb collection structure through python mongoengine

I'm writing a simple Flask app, with the sole purpose to learn Python and MongoDB.
I've managed to reach to the point where all the collections are defined, and CRUD operations work in general. Now, one thing that I really want to understand, is how to refresh the collection, after updating its structure. For example, say that I have the following model:
user.py
class User(db.Document, UserMixin):
email = db.StringField(required=True, unique=True)
password = db.StringField(required=True)
active = db.BooleanField()
first_name = db.StringField(max_length=64, required=True)
last_name = db.StringField(max_length=64, required=True)
registered_at = db.DateTimeField(default=datetime.datetime.utcnow())
confirmed = db.BooleanField()
confirmed_at = db.DateTimeField()
last_login_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_ip = db.StringField(max_length=45)
current_login_ip = db.StringField(max_length=45)
login_count = db.IntField()
companies = db.ListField(db.ReferenceField('Company'), default=[])
roles = db.ListField(db.ReferenceField(Role), default=[])
meta = {
'indexes': [
{'fields': ['email'], 'unique': True}
]
}
Now, I already have entries in my user collection, but I want to change companies to:
company = db.ReferenceField('Company')
How can I refresh the collection's structure, without having to bring the whole database down?
I do have a manage.py script that helps me and also provides a shell:
#!/usr/bin/python
from flask.ext.script import Manager
from flask.ext.script.commands import Shell
from app import factory
app = factory.create_app()
manager = Manager(app)
manager.add_command("shell", Shell(use_ipython=True))
# manager.add_command('run_tests', RunTests())
if __name__ == "__main__":
manager.run()
and I have tried a couple of commands, from information that I could recompile and out of my basic knowledge:
>>> from app.models import db, User
>>> import mongoengine
>>> mongoengine.Document(User)
field = iter(self._fields_ordered)
AttributeError: 'Document' object has no attribute '_fields_ordered'
>>> mongoengine.Document(User).modify() # well, same result as above
Any pointers on how to achieve this?
Update
I am asking all of this, because I have updated my user.py to match my new requests, but anytime I interact with the db its self, since the table's structure was not refreshed, I get the following error:
FieldDoesNotExist: The field 'companies' does not exist on the
document 'User', referer: http://local.faqcolab.com/company
Solution is easier then I expected:
db.getCollection('user').update(
// query
{},
// update
{
$rename: {
'companies': 'company'
}
},
// options
{
"multi" : true, // update all documents
"upsert" : false // insert a new document, if no existing document match the query
}
);
Explanation for each of the {}:
First is empty because I want to update all documents in user collection.
Second contains $rename which is the invoking action to rename the fields I want.
Last contains aditional settings for the query to be executed.
I have updated my user.py to match my new requests, but anytime I interact with the db its self, since the table's structure was not refreshed, I get the following error
MongoDB does not have a "table structure" like relational databases do. After a document has been inserted, you can't change it's schema by changing the document model.
I don't want to sound like I'm telling you that the answer is to use different tools, but seeing things like db.ListField(db.ReferenceField('Company')) makes me think you'd be much better off with a relational database (Postgres is well supported in the Flask ecosystem).
Mongo works best for storing schema-less documents (you don't know before hand how your data is structured, or it varies significantly between documents). Unless you have data like that, it's worth looking at other options. Especially since you're just getting started with Python and Flask, there's no point in making things harder than they are.

Using the SQL initialization hook with ManytoManyField

I'm fairly new to Django and I'm trying to add some 'host' data to 'record' using django's hook for using SQL to initialise (a SQL file in lowercase in the app folder & sql subfolder)
Here's the models:
class Record(models.Model):
species = models.TextField(max_length = 80)
data=models.TextField(max_length = 700)
hosts = models.ManyToManyField('Host')
class Host(models.Model):
hostname = models.TextField()
I've used a ManyToManyField as each record should be able to have multiple hosts, and hosts should be 'reusable': ie be able to appear in many records.
When I'm trying to insert via SQL I have
INSERT INTO myapp_record VALUES ('Species name', 'data1', XYZ);
I'm not sure what to put for XYZ (the ManytoMany) if I wanted hosts 1, 2 and 3 for example
Separating them by commas doesn't work obviously, and I tried a tuple and neither did that.
Should I be trying to insert into the intermediary table Django makes? Does that have a similar hook to the one I'm using? If not, how can I execute SQL inserts on this table?
The use of initial SQL data files is deprecated. Instead, you should be using a data migration, which might look something like this:
from django.db import models, migrations
def create_records(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Record = apps.get_model("yourappname", "Record")
Host = apps.get_model("yourappname", "Host")
host1 = Host.objects.get(hostname='host1')
record = Record.objects.create(name='Species name', data='Data')
record.hosts.add(host1)
...etc...
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(create_records),
]

Categories