diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a470e9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Compiled python files +*.pyc + +# Vim files +*.swp +*.swo + +# Coverage files +.coverage + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info +/*.egg diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..83874d8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python +python: + - '2.7' +env: + - DJANGO=1.6.1 DB=postgres +install: + - pip install -q Django==$DJANGO + - pip install -r requirements.txt +before_script: + - find . | grep .py$ | grep -v /migrations | xargs pep8 --max-line-length=120 + - find . | grep .py$ | grep -v /migrations | grep -v __init__.py | xargs pyflakes + - psql -c 'CREATE DATABASE manager_utils;' -U postgres +script: + - coverage run --source='manager_utils' --branch manage.py test + - coverage report --fail-under=100 diff --git a/LICENSE b/LICENSE index a466289..9875435 100644 --- a/LICENSE +++ b/LICENSE @@ -2,20 +2,19 @@ The MIT License (MIT) Copyright (c) 2014 Ambition -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..57acd2a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include manager_utils/VERSION +include README.md +include LICENSE diff --git a/README.md b/README.md index 60fee80..eb8a8ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,134 @@ +[![Build Status](https://travis-ci.org/ambitioninc/django-manager-utils.png)](https://travis-ci.org/ambitioninc/django-manager-utils) django-manager-utils -==================== +===================== -Model manager utils for Django +Additional utilities for Django model managers. + +## A Brief Overview +Django manager utils allows a user to perform various functions not natively supported by Django's model managers. To use the manager in your Django models, do: + + from manager_utils import ManagerUtilsManager + + class MyModel(Model): + objects = ManagerUtilsManager() + +If you want to extend an existing manager to use the manager utils, include mixin provided first (since it overrides the get_queryset function) as follows: + + from manager_utils import ManagerUtilsMixin + + class MyManager(ManagerUtilsMixin, Manager): + pass + +An overview of each util is below with links to more in-depth documentation and examples for each function. + +- [single](#single): Grabs a single element from table and verifies it is the only element. +- [get_or_none](#get_or_none): Performs a get on a queryset and returns None if the object does not exist. +- [upsert](#upsert): Performs an upsert (update or insert) to a model. +- [bulk_update](#bulk_update): Bulk updates a list of models and the fields that have been updated. + + +## single() +Assumes that the model only has one element in the table or queryset and returns that value. If the table has more than one or no value, an exception is raised. + +**Returns**: The only model object in the queryset. + +**Raises**: DoesNotExist error when the object does not exist or a MultipleObjectsReturned error when there is more than one object. + +**Examples**: + + TestModel.objects.create(int_field=1) + model_obj = TestModel.objects.single() + print model_obj.int_field + 1 + +## get_or_none(\*\*query_params) +Get an object or return None if it doesn't exist. + +**Args**: +- \*\*query_params: The query parameters used in the lookup. + +**Returns**: A model object if one exists with the query params, None otherwise. + +**Examples**: + + model_obj = TestModel.objects.get_or_none(int_field=1) + print model_obj + None + + TestModel.objects.create(int_field=1) + model_obj = TestModel.objects.get_or_none(int_field=1) + print model_obj.int_field + 1 + +## upsert(defaults=None, updates=None, \*\*kwargs) +Performs an update on an object or an insert if the object does not exist. + +**Args**: +- defaults: These values are set when the object is inserted, but are irrelevant when the object already exists. This field should only be used when values only need to be set during creation. +- updates: These values are updated when the object is updated. They also override any values provided in the defaults when inserting the object. +- \*\*kwargs: These values provide the arguments used when checking for the existence of the object. They are used in a similar manner to Django's get_or_create function and are set in a created object. + +**Returns**: A tuple of the upserted object and a Boolean that is True if it was created (False otherwise) + +**Examples**: + + # Upsert a test model with an int value of 1. Use default values that will be given to it when created + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 2.0}) + print created + True + print model_obj.int_field, model_obj.float_field + 1, 2.0 + + # Do an upsert on that same model with different default fields. Since it already exists, the defaults + # are not used + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 3.0}) + print created + False + print model_obj.int_field, model_obj.float_field + 1, 2.0 + + # In order to update the float field in an existing object, use the updates dictionary + model_obj, created = TestModel.objects.upsert(int_field=1, updates={'float_field': 3.0}) + print created + False + print model_obj.int_field, model_obj.float_field + 1, 3.0 + + # You can use updates on a newly created object that will also be used as initial values. + model_obj, created = TestModel.objects.upsert(int_field=2, updates={'float_field': 4.0}) + print created + True + print model_obj.int_field, model_obj.float_field + 2, 4.0 + +## bulk_update(model_objs, fields_to_update) +Performs an bulk update on an list of objects. Any fields listed in the fields_to_update array will be updated in the database. + +**Args**: +- model_objs: A list of model objects that are already stored in the database. +- fields_to_update: A list of fields to update in the models. Only these fields will be updated in the database. The 'id' field is included by default. + +**Examples**: + + # Create a couple test models + model_obj1 = TestModel.objects.create(int_field=1, float_field=2.0, char_field='Hi') + model_obj2 = TestModel.objects.create(int_field=3, float_field=4.0, char_field='Hello') + + # Change their fields and do a bulk update + model_obj1.int_field = 10 + model_obj1.float_field = 20.0 + model_obj2.int_field = 30 + model_obj2.float_field = 40.0 + TestModel.objects.bulk_update([model_obj1, model_obj2], ['int_field', 'float_field']) + + # Reload the models and view their changes + model_obj1 = TestModel.objects.get(id=model_obj1.id) + print model_obj1.int_field, model_obj1.float_field + 10, 20.0 + + model_obj2 = TestModel.objects.get(id=model_obj2.id) + print model_obj2.int_field, model_obj2.float_field + 10, 20.0 + +## License +MIT License (See the LICENSE file included in this repository) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..1013983 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/manager_utils/VERSION b/manager_utils/VERSION new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/manager_utils/VERSION @@ -0,0 +1 @@ +0.1 diff --git a/manager_utils/__init__.py b/manager_utils/__init__.py new file mode 100644 index 0000000..c4f3e4f --- /dev/null +++ b/manager_utils/__init__.py @@ -0,0 +1 @@ +from .manager_utils import ManagerUtilsMixin, ManagerUtilsManager diff --git a/manager_utils/manager_utils.py b/manager_utils/manager_utils.py new file mode 100644 index 0000000..7104094 --- /dev/null +++ b/manager_utils/manager_utils.py @@ -0,0 +1,181 @@ +from itertools import chain + +from django.db.models import Manager +from django.db.models.query import QuerySet +from querybuilder.query import Query + + +class ManagerUtilsQuerySet(QuerySet): + """ + Defines the methods in the manager utils that can also be applied to querysets. + """ + def get_or_none(self, **query_params): + """ + Get an object or return None if it doesn't exist. + + Returns: + A model object if one exists with the query params, None otherwise. + """ + try: + obj = self.get(**query_params) + except self.model.DoesNotExist: + obj = None + return obj + + def single(self): + """ + Assumes that this model only has one element in the table and returns it. If the table has more + than one or no value, an exception is raised. + """ + return self.get(id__gte=0) + + +class ManagerUtilsMixin(object): + """ + A mixin that can be used by django model managers. It provides additional functionality on top + of the regular Django Manager class. + """ + def get_queryset(self): + return ManagerUtilsQuerySet(self.model) + + def bulk_update(self, model_objs, fields_to_update): + """ + Bulk updates a list of model objects that are already saved. + + Args: + model_objs: A list of model objects that have been updated. + fields_to_update: A list of fields to be updated. Only these fields will be updated + + Examples: + # Create a couple test models + model_obj1 = TestModel.objects.create(int_field=1, float_field=2.0, char_field='Hi') + model_obj2 = TestModel.objects.create(int_field=3, float_field=4.0, char_field='Hello') + + # Change their fields and do a bulk update + model_obj1.int_field = 10 + model_obj1.float_field = 20.0 + model_obj2.int_field = 30 + model_obj2.float_field = 40.0 + TestModel.objects.bulk_update([model_obj1, model_obj2], ['int_field', 'float_field']) + + # Reload the models and view their changes + model_obj1 = TestModel.objects.get(id=model_obj1.id) + print model_obj1.int_field, model_obj1.float_field + 10, 20.0 + + model_obj2 = TestModel.objects.get(id=model_obj2.id) + print model_obj2.int_field, model_obj2.float_field + 10, 20.0 + """ + updated_rows = [ + [model_obj.id] + [getattr(model_obj, field_name) for field_name in fields_to_update] + for model_obj in model_objs + ] + if len(updated_rows) == 0 or len(fields_to_update) == 0: + return + + # Execute the bulk update + Query().from_table( + table=self.model, + fields=chain(['id'] + fields_to_update), + ).update(updated_rows) + + def upsert(self, defaults=None, updates=None, **kwargs): + """ + Performs an update on an object or an insert if the object does not exist. + Args: + defaults: These values are set when the object is inserted, but are irrelevant + when the object already exists. This field should only be used when values only need to + be set during creation. + updates: These values are updated when the object is updated. They also override any + values provided in the defaults when inserting the object. + **kwargs: These values provide the arguments used when checking for the existence of + the object. They are used in a similar manner to Django's get_or_create function. + + Returns: A tuple of the upserted object and a Boolean that is True if it was created (False otherwise) + + Examples: + # Upsert a test model with an int value of 1. Use default values that will be given to it when created + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 2.0}) + print created + True + print model_obj.int_field, model_obj.float_field + 1, 2.0 + + # Do an upsert on that same model with different default fields. Since it already exists, the defaults + # are not used + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 3.0}) + print created + False + print model_obj.int_field, model_obj.float_field + 1, 2.0 + + # In order to update the float field in an existing object, use the updates dictionary + model_obj, created = TestModel.objects.upsert(int_field=1, updates={'float_field': 3.0}) + print created + False + print model_obj.int_field, model_obj.float_field + 1, 3.0 + + # You can use updates on a newly created object that will also be used as initial values. + model_obj, created = TestModel.objects.upsert(int_field=2, updates={'float_field': 4.0}) + print created + True + print model_obj.int_field, model_obj.float_field + 2, 4.0 + """ + obj, created = self.model.objects.get_or_create(defaults=defaults or {}, **kwargs) + + if updates is not None: + for k, v in updates.iteritems(): + setattr(obj, k, v) + obj.save(update_fields=updates) + + return obj, created + + def get_or_none(self, **query_params): + """ + Get an object or return None if it doesn't exist. + + Args: + **query_params: The query parameters used in the lookup. + + Returns: A model object if one exists with the query params, None otherwise. + + Examples: + model_obj = TestModel.objects.get_or_none(int_field=1) + print model_obj + None + + TestModel.objects.create(int_field=1) + model_obj = TestModel.objects.get_or_none(int_field=1) + print model_obj.int_field + 1 + """ + return self.get_queryset().get_or_none(**query_params) + + def single(self): + """ + Assumes that this model only has one element in the table and returns it. If the table has more + than one or no value, an exception is raised. + + Returns: The only model object in the queryset. + + Raises: DoesNotExist error when the object does not exist or a MultipleObjectsReturned error when there + is more than one object. + + Examples: + TestModel.objects.create(int_field=1) + model_obj = TestModel.objects.single() + print model_obj.int_field + 1 + """ + return self.get_queryset().single() + + +class ManagerUtilsManager(ManagerUtilsMixin, Manager): + """ + A class that can be used as a manager. It already inherits the Django Manager class and adds + the mixin. + """ + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6b0cf2b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +coverage +django-dynamic-fixture==1.6.5 +django-nose==1.1 +pep8 +psycopg2==2.4.5 +pyflakes +south==0.7.6 + +git+https://github.com/wesokes/django-query-builder.git@0.5.2 +# Note that Django is a requirement, but it is installed in the .travis.yml file in order to test against different versions diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..020a435 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +import os +from setuptools import setup + + +setup( + name='django-manager-utils', + version=open(os.path.join(os.path.dirname(__file__), 'manager_utils', 'VERSION')).read().strip(), + description='Model manager utilities for Django', + long_description=open('README.md').read(), + url='http://github.com/ambitioninc/django-manager-utils/', + author='Wes Kendall', + author_email='wesleykendall@gmail.com', + packages=[ + 'manager_utils', + ], + classifiers=[ + 'Programming Language :: Python', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Framework :: Django', + ], + dependency_links=[ + 'git+https://github.com/wesokes/django-query-builder.git@0.5.2', + ], + install_requires=[ + 'django>=1.6', + ], + include_package_data=True, +) diff --git a/test_project/__init__.py b/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_project/models.py b/test_project/models.py new file mode 100644 index 0000000..a4acdda --- /dev/null +++ b/test_project/models.py @@ -0,0 +1,13 @@ +from django.db import models +from manager_utils import ManagerUtilsManager + + +class TestModel(models.Model): + """ + A model for testing manager utils. + """ + int_field = models.IntegerField() + char_field = models.CharField(max_length=128, null=True) + float_field = models.FloatField(null=True) + + objects = ManagerUtilsManager() diff --git a/test_project/settings.py b/test_project/settings.py new file mode 100644 index 0000000..0d27616 --- /dev/null +++ b/test_project/settings.py @@ -0,0 +1,174 @@ +import os + + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +# Use the nose tests runner +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +test_db = os.environ.get('DB', None) +if test_db is not None: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'USER': 'postgres', + 'NAME': 'manager_utils', + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'ambition_dev', + 'USER': 'ambition_dev', + 'PASSWORD': 'ambition_dev', + 'HOST': 'localhost' + } + } + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'UTC' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = False + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '(v9+!w*uv$%+0ozjkp%9%_^r#et3du+(v(t*w(j55fbzhi@e*7' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + # 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'test_project.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'test_project.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + # 'django.contrib.messages', + # 'django.contrib.staticfiles', + 'django.contrib.admin', + 'django_nose', + 'south', + 'test_project', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake' + } +} diff --git a/test_project/tests/__init__.py b/test_project/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_project/tests/manager_utils_tests.py b/test_project/tests/manager_utils_tests.py new file mode 100644 index 0000000..ab67e77 --- /dev/null +++ b/test_project/tests/manager_utils_tests.py @@ -0,0 +1,318 @@ +from django.test import TestCase +from django_dynamic_fixture import G + +from test_project.models import TestModel + + +class GetOrNoneTests(TestCase): + """ + Tests the get_or_none function in the manager utils + """ + def test_existing_using_objects(self): + """ + Tests get_or_none on an existing object from Model.objects. + """ + # Create an existing model + model_obj = G(TestModel) + # Verify that get_or_none on objects returns the test model + self.assertEquals(model_obj, TestModel.objects.get_or_none(id=model_obj.id)) + + def test_multiple_error_using_objects(self): + """ + Tests get_or_none on multiple existing objects from Model.objects. + """ + # Create an existing model + model_obj = G(TestModel, char_field='hi') + model_obj = G(TestModel, char_field='hi') + # Verify that get_or_none on objects returns the test model + with self.assertRaises(TestModel.MultipleObjectsReturned): + self.assertEquals(model_obj, TestModel.objects.get_or_none(char_field='hi')) + + def test_existing_using_queryset(self): + """ + Tests get_or_none on an existing object from a queryst. + """ + # Create an existing model + model_obj = G(TestModel) + # Verify that get_or_none on objects returns the test model + self.assertEquals(model_obj, TestModel.objects.filter(id=model_obj.id).get_or_none(id=model_obj.id)) + + def test_none_using_objects(self): + """ + Tests when no object exists when using Model.objects. + """ + # Verify that get_or_none on objects returns the test model + self.assertIsNone(TestModel.objects.get_or_none(id=1)) + + def test_none_using_queryset(self): + """ + Tests when no object exists when using a queryset. + """ + # Verify that get_or_none on objects returns the test model + self.assertIsNone(TestModel.objects.filter(id=1).get_or_none(id=1)) + + +class SingleTests(TestCase): + """ + Tests the single function in the manager utils. + """ + def test_none_using_objects(self): + """ + Tests when there are no objects using Model.objects. + """ + with self.assertRaises(TestModel.DoesNotExist): + TestModel.objects.single() + + def test_multiple_using_objects(self): + """ + Tests when there are multiple objects using Model.objects. + """ + G(TestModel) + G(TestModel) + with self.assertRaises(TestModel.MultipleObjectsReturned): + TestModel.objects.single() + + def test_none_using_queryset(self): + """ + Tests when there are no objects using a queryset. + """ + with self.assertRaises(TestModel.DoesNotExist): + TestModel.objects.filter(id__gte=0).single() + + def test_multiple_using_queryset(self): + """ + Tests when there are multiple objects using a queryset. + """ + G(TestModel) + G(TestModel) + with self.assertRaises(TestModel.MultipleObjectsReturned): + TestModel.objects.filter(id__gte=0).single() + + def test_single_using_objects(self): + """ + Tests accessing a single object using Model.objects. + """ + model_obj = G(TestModel) + self.assertEquals(model_obj, TestModel.objects.single()) + + def test_single_using_queryset(self): + """ + Tests accessing a single object using a queryset. + """ + model_obj = G(TestModel) + self.assertEquals(model_obj, TestModel.objects.filter(id__gte=0).single()) + + def test_mutliple_to_single_using_queryset(self): + """ + Tests accessing a single object using a queryset. The queryset is what filters it + down to a single object. + """ + model_obj = G(TestModel) + G(TestModel) + self.assertEquals(model_obj, TestModel.objects.filter(id=model_obj.id).single()) + + +class TestBulkUpdate(TestCase): + """ + Tests the bulk_update function. + """ + def test_none(self): + """ + Tests when no values are provided to bulk update. + """ + TestModel.objects.bulk_update([], []) + + def test_objs_no_fields_to_update(self): + """ + Tests when objects are given to bulk update with no fields to update. Nothing should change in + the objects. + """ + test_obj_1 = G(TestModel, int_field=1) + test_obj_2 = G(TestModel, int_field=2) + # Change the int fields on the models + test_obj_1.int_field = 3 + test_obj_2.int_field = 4 + # Do a bulk update with no update fields + TestModel.objects.bulk_update([test_obj_1, test_obj_2], []) + # The test objects int fields should be untouched + test_obj_1 = TestModel.objects.get(id=test_obj_1.id) + test_obj_2 = TestModel.objects.get(id=test_obj_2.id) + self.assertEquals(test_obj_1.int_field, 1) + self.assertEquals(test_obj_2.int_field, 2) + + def test_objs_one_field_to_update(self): + """ + Tests when objects are given to bulk update with one field to update. + """ + test_obj_1 = G(TestModel, int_field=1) + test_obj_2 = G(TestModel, int_field=2) + # Change the int fields on the models + test_obj_1.int_field = 3 + test_obj_2.int_field = 4 + # Do a bulk update with the int fields + TestModel.objects.bulk_update([test_obj_1, test_obj_2], ['int_field']) + # The test objects int fields should be untouched + test_obj_1 = TestModel.objects.get(id=test_obj_1.id) + test_obj_2 = TestModel.objects.get(id=test_obj_2.id) + self.assertEquals(test_obj_1.int_field, 3) + self.assertEquals(test_obj_2.int_field, 4) + + def test_objs_one_field_to_update_ignore_other_field(self): + """ + Tests when objects are given to bulk update with one field to update. This test changes another field + not included in the update and verifies it is not updated. + """ + test_obj_1 = G(TestModel, int_field=1, float_field=1.0) + test_obj_2 = G(TestModel, int_field=2, float_field=2.0) + # Change the int and float fields on the models + test_obj_1.int_field = 3 + test_obj_2.int_field = 4 + test_obj_1.float_field = 3.0 + test_obj_2.float_field = 4.0 + # Do a bulk update with the int fields + TestModel.objects.bulk_update([test_obj_1, test_obj_2], ['int_field']) + # The test objects int fields should be untouched + test_obj_1 = TestModel.objects.get(id=test_obj_1.id) + test_obj_2 = TestModel.objects.get(id=test_obj_2.id) + self.assertEquals(test_obj_1.int_field, 3) + self.assertEquals(test_obj_2.int_field, 4) + # The float fields should not be updated + self.assertEquals(test_obj_1.float_field, 1.0) + self.assertEquals(test_obj_2.float_field, 2.0) + + def test_objs_two_fields_to_update(self): + """ + Tests when objects are given to bulk update with two fields to update. + """ + test_obj_1 = G(TestModel, int_field=1, float_field=1.0) + test_obj_2 = G(TestModel, int_field=2, float_field=2.0) + # Change the int and float fields on the models + test_obj_1.int_field = 3 + test_obj_2.int_field = 4 + test_obj_1.float_field = 3.0 + test_obj_2.float_field = 4.0 + # Do a bulk update with the int fields + TestModel.objects.bulk_update([test_obj_1, test_obj_2], ['int_field', 'float_field']) + # The test objects int fields should be untouched + test_obj_1 = TestModel.objects.get(id=test_obj_1.id) + test_obj_2 = TestModel.objects.get(id=test_obj_2.id) + self.assertEquals(test_obj_1.int_field, 3) + self.assertEquals(test_obj_2.int_field, 4) + # The float fields should be updated + self.assertEquals(test_obj_1.float_field, 3.0) + self.assertEquals(test_obj_2.float_field, 4.0) + + +class TestUpsert(TestCase): + """ + Tests the upsert method in the manager utils. + """ + def test_upsert_creation_no_defaults(self): + """ + Tests an upsert that results in a created object. Don't use defaults + """ + model_obj, created = TestModel.objects.upsert(int_field=1) + self.assertTrue(created) + self.assertEquals(model_obj.int_field, 1) + self.assertIsNone(model_obj.float_field) + self.assertIsNone(model_obj.char_field) + + def test_upsert_creation_defaults(self): + """ + Tests an upsert that results in a created object. Defaults are used. + """ + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 1.0}) + self.assertTrue(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 1.0) + self.assertIsNone(model_obj.char_field) + + def test_upsert_creation_updates(self): + """ + Tests an upsert that results in a created object. Updates are used. + """ + model_obj, created = TestModel.objects.upsert(int_field=1, updates={'float_field': 1.0}) + self.assertTrue(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 1.0) + self.assertIsNone(model_obj.char_field) + + def test_upsert_creation_defaults_updates(self): + """ + Tests an upsert that results in a created object. Defaults are used and so are updates. + """ + model_obj, created = TestModel.objects.upsert( + int_field=1, defaults={'float_field': 1.0}, updates={'char_field': 'Hello'}) + self.assertTrue(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 1.0) + self.assertEquals(model_obj.char_field, 'Hello') + + def test_upsert_creation_defaults_updates_override(self): + """ + Tests an upsert that results in a created object. Defaults are used and so are updates. Updates + override the defaults. + """ + model_obj, created = TestModel.objects.upsert( + int_field=1, defaults={'float_field': 1.0}, updates={'char_field': 'Hello', 'float_field': 2.0}) + self.assertTrue(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 2.0) + self.assertEquals(model_obj.char_field, 'Hello') + + def test_upsert_no_creation_no_defaults(self): + """ + Tests an upsert that already exists. Don't use defaults + """ + G(TestModel, int_field=1, float_field=None, char_field=None) + model_obj, created = TestModel.objects.upsert(int_field=1) + self.assertFalse(created) + self.assertEquals(model_obj.int_field, 1) + self.assertIsNone(model_obj.float_field) + self.assertIsNone(model_obj.char_field) + + def test_upsert_no_creation_defaults(self): + """ + Tests an upsert that already exists. Defaults are used but don't matter since the object already existed. + """ + G(TestModel, int_field=1, float_field=None, char_field=None) + model_obj, created = TestModel.objects.upsert(int_field=1, defaults={'float_field': 1.0}) + self.assertFalse(created) + self.assertEquals(model_obj.int_field, 1) + self.assertIsNone(model_obj.float_field) + self.assertIsNone(model_obj.char_field) + + def test_upsert_no_creation_updates(self): + """ + Tests an upsert that already exists. Updates are used. + """ + G(TestModel, int_field=1, float_field=2.0, char_field=None) + model_obj, created = TestModel.objects.upsert(int_field=1, updates={'float_field': 1.0}) + self.assertFalse(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 1.0) + self.assertIsNone(model_obj.char_field) + + def test_upsert_no_creation_defaults_updates(self): + """ + Tests an upsert that already exists. Defaults are used and so are updates. + """ + G(TestModel, int_field=1, float_field=2.0, char_field='Hi') + model_obj, created = TestModel.objects.upsert( + int_field=1, defaults={'float_field': 1.0}, updates={'char_field': 'Hello'}) + self.assertFalse(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 2.0) + self.assertEquals(model_obj.char_field, 'Hello') + + def test_upsert_no_creation_defaults_updates_override(self): + """ + Tests an upsert that already exists. Defaults are used and so are updates. Updates override the defaults. + """ + G(TestModel, int_field=1, float_field=3.0, char_field='Hi') + model_obj, created = TestModel.objects.upsert( + int_field=1, defaults={'float_field': 1.0}, updates={'char_field': 'Hello', 'float_field': 2.0}) + self.assertFalse(created) + self.assertEquals(model_obj.int_field, 1) + self.assertEquals(model_obj.float_field, 2.0) + self.assertEquals(model_obj.char_field, 'Hello')