├── .gitignore ├── .travis.yml ├── AUTHORS.txt ├── CHANGES.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── autofixture ├── __init__.py ├── autofixtures.py ├── base.py ├── compat.py ├── constraints.py ├── generators.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── loadtestdata.py ├── models.py ├── placeholder.py ├── signals.py └── values.py ├── autofixture_tests ├── __init__.py ├── appconfig_test │ ├── __init__.py │ ├── apps.py │ ├── autofixtures.py │ ├── models.py │ └── test_autodiscover.py ├── compat.py ├── media │ ├── emptyfiles │ │ ├── empty.txt │ │ └── nofilextension │ └── textfiles │ │ ├── ispum.txt │ │ └── lorem.txt ├── models.py ├── sample_app │ ├── __init__.py │ ├── admin.py │ └── models.py ├── settings.py ├── tests │ ├── __init__.py │ ├── test_autodiscover.py │ ├── test_base.py │ ├── test_generator.py │ ├── test_geodjango.py │ ├── test_user_fixture.py │ └── test_values.py └── urls.py ├── docs ├── Makefile ├── builtin_autofixtures.rst ├── conf.py ├── contribute.rst ├── index.rst ├── installation.rst ├── loadtestdata.rst ├── make.bat ├── requirements.txt └── usage.rst ├── fabfile.py ├── manage.py ├── requirements └── tests.txt ├── runtests.py ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | dist/ 4 | docs/_build 5 | include/ 6 | lib/ 7 | local/ 8 | .tox/ 9 | db.sqlite 10 | *.egg-info 11 | *.pyc 12 | *.swp 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | cache: 4 | directories: 5 | - $HOME/.cache/pip 6 | 7 | env: 8 | global: 9 | - DJANGO_SETTINGS_MODULE=autofixture_tests.settings 10 | matrix: 11 | - TOX_ENV=docs 12 | - TOX_ENV=py27-14 13 | - TOX_ENV=py27-15 14 | - TOX_ENV=py27-16 15 | - TOX_ENV=py27-17 16 | - TOX_ENV=py27-18 17 | - TOX_ENV=py27-19 18 | - TOX_ENV=py33-16 19 | - TOX_ENV=py33-17 20 | - TOX_ENV=py33-18 21 | - TOX_ENV=py34-16 22 | - TOX_ENV=py34-17 23 | - TOX_ENV=py34-18 24 | - TOX_ENV=py35-18 25 | - TOX_ENV=py35-19 26 | - TOX_ENV=pypy-14 27 | - TOX_ENV=pypy-15 28 | - TOX_ENV=pypy-16 29 | - TOX_ENV=pypy-17 30 | - TOX_ENV=pypy-18 31 | - TOX_ENV=pypy-19 32 | 33 | install: 34 | - pip install pip wheel -U 35 | - pip install tox 36 | script: 37 | - tox -e $TOX_ENV -v 38 | deploy: 39 | provider: pypi 40 | user: gremu 41 | password: 42 | secure: HAgxb7ebUfIQSfcxtjKoXO3FCBiujCo4GU2lYO8IUPeSdurPG6e+uABzwg88d7Zt1Zuay2eleAJzqhvwU2bCOKr68wjkNw3yWRslvLAMK3vj2LPPoWYsgmLJ1YiOvPloCdD2sIVSmpLEiN5HLUteh5j6D2BUbw67a9S2TXx1J8c= 43 | on: 44 | tags: true 45 | repo: gregmuellegger/django-autofixture 46 | condition: "$TOX_ENV = py35-18" 47 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | **django-autofixture** was originally created by *Gregor Müllegger* 5 | ( https://github.com/gregmuellegger). But there are lots 6 | of contributors who also volunteered by fixing bugs and enhancing features. 7 | Thanks to all those developers. 8 | 9 | **Contributors:** 10 | * Mikko Hellsing - https://github.com/sorl 11 | * Scott Woodall - https://github.com/scottwoodall 12 | * aschriner - https://github.com/aschriner 13 | * mvdwaeter - https://github.com/mvdwaeter 14 | * Jonathan Tien - https://github.com/ricefield 15 | * Josh Fyne - https://github.com/jfyne 16 | * StillNewb - https://github.com/StillNewb 17 | * Stella Lie - https://github.com/stellalie 18 | * Visgean Skeloru - https://github.com/Visgean 19 | * Andrew Pashkin - https://github.com/AndrewPashkin 20 | * Isotoma Limited - https://www.isotoma.com/ 21 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.12.1 5 | ------ 6 | 7 | * `#85`_: Better examples in README. Thanks to Dan Hitt for the patch. 8 | * `#86`_: Less deprecation warnings when using django-autofixture on Python 9 | 3.5. Thanks to Nick Timkovich for the patch. 10 | * `#87`_: Closing files properly, so you should see less ``ResourceWarnings`` 11 | while using django-autofixture. Thanks to Nick Timkovich for the patch. 12 | 13 | .. _#85: https://github.com/gregmuellegger/django-autofixture/pull/85 14 | .. _#86: https://github.com/gregmuellegger/django-autofixture/pull/86 15 | .. _#87: https://github.com/gregmuellegger/django-autofixture/pull/87 16 | 17 | 0.12.0 18 | ------ 19 | 20 | * `#81`_: Add support for UUID fields. Thanks to @jungornti for the patch. 21 | * `#77`_: Fixing a very rare crash in cases when a generated email in the 22 | ``UserFixture`` already exists. Thanks to Tien Nguyen for the patch. 23 | 24 | .. _#77: https://github.com/gregmuellegger/django-autofixture/pull/77 25 | .. _#81: https://github.com/gregmuellegger/django-autofixture/pull/81 26 | 27 | 0.11.0 28 | ------ 29 | 30 | * `#75`_: Support for Django 1.9. Thanks to Adam Dobrawy for the patch. 31 | * `#67`_: If many to many relations are created in a autofixture, we now make sure 32 | that a registered autofixture is used for this if there is any. Thanks to 33 | Andrew Lewisohn for the patch. 34 | 35 | .. _#75: https://github.com/gregmuellegger/django-autofixture/pull/75 36 | .. _#67: https://github.com/gregmuellegger/django-autofixture/pull/67 37 | 38 | 0.10.1 39 | ------ 40 | 41 | * Fixing unique constraint checks for multiple ``None`` values. Thanks to 42 | Andrew Lewisohn for the patch. See `#66`_. 43 | 44 | .. _#66: https://github.com/gregmuellegger/django-autofixture/pull/66 45 | 46 | 0.10.0 47 | ------ 48 | 49 | * Supporting Django 1.7 style app configs in ``settings.INSTALLED_APPS`` 50 | when auto-importing autofixture definitions with 51 | ``autofixture.autodiscover``. 52 | * Adding ``autofixture.generators.PositiveDecimalGenerator``. 53 | 54 | 0.9.2 55 | ----- 56 | 57 | * Fixed ``UserFixture`` that generated usernames with more than 30 characters. 58 | 59 | 0.9.1 60 | ----- 61 | 62 | * Fixed unique constraint for models that have multiple unique_togethers set. 63 | 64 | 0.9 65 | --- 66 | * Make ``ImageGenerator`` consider the given file storage. Thanks to Andrew 67 | Pashkin for the patch. 68 | * Fixing check for unique constraint during data generation if the field 69 | allows to be nullable. Thanks for Andrew Pashkin for the report and fix. 70 | 71 | 0.8.0 72 | ----- 73 | 74 | * Adding support for django's ``ImageField``. Thanks to Visgean Skeloru for 75 | the patch. 76 | 77 | 0.7.0 78 | ----- 79 | 80 | * Adding ``AutoFixture.pre_process_instance`` method. 81 | * Allow arbitrary keyword arguments for ``AutoFixture.create`` method. 82 | * Fixing ``autofixture.unregister`` function. 83 | * Fixing ``UserFixture.post_process_instance``. 84 | 85 | 0.6.3 86 | ----- 87 | 88 | * Fixing long stated issue with GenericRelation fields. Thanks to StillNewb 89 | for the patch. 90 | 91 | 0.6.2 92 | ----- 93 | 94 | * Supporting Django 1.6. 95 | 96 | 0.6.1 97 | ----- 98 | 99 | * Fixing issue with models that have a selfreferencing ForeignKey field. 100 | Thanks to Josh Fyne for the patch. 101 | 102 | 0.6.0 103 | ----- 104 | 105 | * Adding ``generators.WeightedGenerator`` for propabilistic selection of 106 | values. Thanks to Jonathan Tien for the idea and patch. 107 | * Supporting model inheritance. Thanks to Josh Fyne for the patch. 108 | 109 | 0.5.0 110 | ----- 111 | 112 | * Adding ``FirstNameGenerator`` and ``LastNameGenerator``. Thanks to Jonathan 113 | Tien for the initial patch. 114 | * Registered Autofixtures are used for models that are created for foreignkeys 115 | and many to many relations. Thanks to Theo Spears for the report. 116 | 117 | 0.4.0 118 | ----- 119 | 120 | * Python 3 support! Though we had to drop Python 2.5 support. If you cannot 121 | upgrade to Python 2.6 by yet, please consider using the 0.3.x versions of 122 | django-autofixture. 123 | By the way: by Python 3 support, I mean, that the test suite is running 124 | without any errors. I have not tested yet the library in production for 125 | Python 3. So please test and submit bug reports if you encounter any. 126 | 127 | 0.3.2 128 | ----- 129 | 130 | * ``DateTimeField`` receive timezone aware datetime objects now. Thanks to 131 | Scott Woodall for the report and patch. 132 | * Adding ``static_domain`` parameter to ``EmailGenerator`` to allow the 133 | production of emails that will always have the same domain. Thanks to 134 | mvdwaeter for the initial patch. 135 | 136 | 0.3.1 137 | ----- 138 | 139 | * ``field_values`` were not picked up if there was a default value assigned to 140 | the field. Thanks to sirex for the report. 141 | 142 | 0.3.0 143 | ----- 144 | 145 | * Adding better support for subclassing ``AutoFixture`` through merging of 146 | nested ``Values`` classes. 147 | * Renamed attribute and argument ``none_chance`` to better matching name ``empty_p`` for generators 148 | and ``none_p`` for ``AutoFixture``. 149 | * Fixed some issues with management command options. Thanks Mikko Hellsing for 150 | his hard work. 151 | * Fixed issues in unregister(). Thanks Mikko Hellsing for the report. 152 | * Adding support for ``FloatField``. Thanks to Jyr Gaxiola for the report. 153 | 154 | 0.2.5 155 | ----- 156 | 157 | * Fixing issue with ``--generate-fk`` option in management command. Thanks 158 | Mikko Hellsing for the `report and fix`_. 159 | 160 | .. _report and fix: http://github.com/gregmuellegger/django-autofixture/issues/issue/1/ 161 | 162 | 0.2.4 163 | ----- 164 | 165 | * Using ``Autofixture.Values`` for defining initial values in ``Autofixture`` 166 | subclasses. 167 | 168 | * Making autodiscover more robust. Don't break if some module can't be 169 | imported or throws any other exception. 170 | 171 | 0.2.3 172 | ----- 173 | 174 | * Fixing bug when a ``CharField`` with ``max_length`` smaller than 15 is used. 175 | 176 | * ``AutoFixture.field_values`` accepts callables as values. 177 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Gregor Müllegger 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the author nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.txt 2 | include CHANGES.rst 3 | include LICENSE.txt 4 | include MANIFEST.in 5 | include README.rst 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | django-autofixture 3 | ================== 4 | 5 | |build| |package| 6 | 7 | This app aims to provide a simple way of loading masses of randomly generated 8 | test data into your development database. You can use a management command to 9 | load test data through command line. 10 | 11 | It is named *autofixture* because it is based on django's fixtures. Without 12 | *autofixture* you add test data through the admin to see how the non-static 13 | pages on your site look. You export data by using ``dumpdata`` to 14 | send it to your colleagues or to preserve it before you make a ``manage.py 15 | reset app`` and so on. As your site grows in complexity the process of adding 16 | and re-adding data becomes more and more annoying. 17 | 18 | This is where autofixtures will help! 19 | 20 | 21 | Requirements 22 | ============ 23 | 24 | * We require and support Django 1.4 to 1.9 25 | 26 | 27 | Installation 28 | ============ 29 | 30 | You must make the ``autofixture`` package available on your python path. 31 | Either drop it into your project directory or install it from the python 32 | package index with ``pip install django-autofixture``. You can also use 33 | ``easy_install django-autofixture`` if you don't have pip available. 34 | 35 | To use the management command you must add ``'autofixture'`` to the 36 | ``INSTALLED_APPS`` setting in your django settings file. You don't need to do 37 | this if you want to use the ``autofixture`` package only as library. 38 | 39 | 40 | Management command 41 | ================== 42 | 43 | The ``loadtestdata`` accepts the following syntax:: 44 | 45 | python manage.py loadtestdata [options] app.Model:# [app.Model:# ...] 46 | 47 | It's nearly self explanatory. Supply names of models, prefixed with its app 48 | name. After that, place a colon and tell the command how many objects you want 49 | to create. Here is an example of how to create three categories and twenty 50 | entries for your blogging app:: 51 | 52 | python manage.py loadtestdata blog.Category:3 blog.Entry:20 53 | 54 | Voila! You have ready-to-use testing data populated to your database. The 55 | model fields are filled with data by producing randomly generated values 56 | depending on the type of the field. E.g. text fields are filled with lorem 57 | ipsum dummies, date fields are populated with random dates from the last 58 | year etc. 59 | 60 | There are a few command line options available. Mainly to control the 61 | behavior of related fields. If foreingkey or many to many fields should be 62 | populated with existing data or if the related models are also generated on 63 | the fly. Please have a look at the help page of the command for more 64 | information:: 65 | 66 | python manage.py help loadtestdata 67 | 68 | 69 | Using autofixtures as a tool for unittests 70 | ========================================== 71 | 72 | Testing the behavior of complex models has always bugged me. Sometimes models 73 | have many restrictions or many related objects which they depend on. One 74 | solution would be to use traditional fixtures dumped from your production 75 | database. But while in development when database schemes are changing 76 | frequently, it can be time consuming and sometimes difficult to deep track of 77 | changes and what each dump contains. 78 | 79 | Autofixtures to the rescue! 80 | 81 | Let's start with the basics. We create an ``AutoFixture`` instance for the 82 | ``Entry`` model and tell it to create ten model instances:: 83 | 84 | >>> from autofixture import AutoFixture 85 | >>> fixture = AutoFixture(Entry) 86 | >>> entries = fixture.create(10) 87 | 88 | Here are further examples for newer developers. 89 | 90 | I have a ``Listing`` model and I want it populated with 10 objects. 91 | 92 | :: 93 | 94 | >>> from autofixture import AutoFixture 95 | >>> fixture = AutoFixture(Listing) 96 | >>> entries = fixture.create(10) 97 | 98 | Here I've added field values which allow you to default a field to a certain 99 | value rather than the random entries supplied by *autofixture*. 100 | 101 | Generic Example including field_values: 102 | 103 | :: 104 | 105 | from .models import 106 | fixture = AutoFixture(, field_values={'':}) 107 | 108 | Specific example:: 109 | 110 | from main.models import Listing 111 | fixture = AutoFixture(Listing, field_values={'needed_players': randint(2,10)}) 112 | entries=fixture.create(30) 113 | 114 | In the above, I wanted the ``'needed_players'`` (in the Session model) to have 115 | only numbers between 2 and 10, but I could have put ``{'needed_players': 5}`` 116 | if I had wanted all ``'needed_players'`` instances to be ``5``. 117 | 118 | ======================================== 119 | 120 | Now you can play around and test your blog entries. By default, dependencies 121 | of foreignkeys and many to many relations are populated by randomly selecting 122 | an already existing object of the related model. But, what if you don't have 123 | one yet? You can provide the ``generate_fk`` attribute which allows the 124 | autofixture instance to follow foreignkeys by generating new related models:: 125 | 126 | fixture = AutoFixture(Entry, generate_fk=True) 127 | 128 | This generates new instances for *all* foreignkey fields of ``Entry``. Unless 129 | the model has a foreign key reference to itself, wherein the field will be set 130 | to None if allowed or raise a ``CreateInstanceError``. This is to prevent max 131 | recursion depth errors. It's possible to limit this behaviour to single 132 | fields:: 133 | 134 | fixture = AutoFixture(Entry, generate_fk=['author']) 135 | 136 | This will only create new authors automatically and doesn't touch other 137 | tables. The same is possible with many to many fields. But you need to 138 | additionally specify how many objects should be created for the m2m relation:: 139 | 140 | fixture = AutoFixture(Entry, generate_m2m={'categories': (1,3)}) 141 | 142 | All created entry models get one to three new categories assigned. 143 | 144 | Setting custom values for fields 145 | -------------------------------- 146 | 147 | As shown the the examples above, it's often necessary to have a specific field 148 | contain a specific value. This is easily achieved with the ``field_values`` 149 | attribute of ``AutoFixture``:: 150 | 151 | fixture = AutoFixture(Entry, 152 | field_values={'pub_date': datetime(2010, 2, 1)}) 153 | 154 | 155 | Limiting the set of models assigned to a ForeignKey field 156 | ---------------------------------------------------------- 157 | 158 | You could, for example, limit the Users assigned to a foreignkey field to only 159 | non-staff Users. Or create Entries for all Blogs not belonging to Yoko Ono. 160 | Use the same construction as ForeignKey.limit_choices_to_ attribute:: 161 | 162 | from autofixture import AutoFixture, generators 163 | fixture = AutoFixture(Entry, field_values={ 164 | 'blog': generators.InstanceSelector( 165 | Blog, 166 | limit_choices_to={'name__ne':"Yoko Ono's blog"}) 167 | }) 168 | 169 | 170 | Custom autofixtures 171 | =================== 172 | 173 | To have custom autofixtures for your model, you can easily subclass 174 | ``AutoFixture`` somewhere (e.g. in myapp/autofixtures.py) :: 175 | 176 | from models import MyModel 177 | from autofixture import generators, register, AutoFixture 178 | 179 | class MyModelAutoFixture(AutoFixture): 180 | field_values = { 181 | 'name': generators.StaticGenerator('this_is_my_static_name'), 182 | } 183 | 184 | register(MyModel, MyModelAutoFixture) 185 | 186 | 187 | Then, ``loadtestdata`` will automatically use your custom fixtures. :: 188 | 189 | python manage.py loadtestdata app.MyModel:10 190 | 191 | You can load all ``autofixtures.py`` files of your installed apps 192 | automatically like you can do with the admin autodiscover. Do so by running 193 | ``autofixture.autodiscover()`` somewhere in the code, preferably in the 194 | ``urls.py``. 195 | 196 | 197 | More 198 | ==== 199 | 200 | There is so much more to explore which might be useful to you and your 201 | projects: 202 | 203 | * There are ways to register custom ``AutoFixture`` subclasses with models 204 | that are automatically used when calling ``loadtestdata`` on the model. 205 | * More control for related models, even with relations of related models... 206 | (e.g. by using ``generate_fk=['author', 'author__user']``) 207 | * Custom constraints that are used to ensure that created models are 208 | valid (e.g. ``unique`` and ``unique_together`` constraints, which are 209 | already handled by default) 210 | 211 | 212 | Contribute 213 | ========== 214 | 215 | You can find the latest development version on github_. Get there and fork it, 216 | file bugs or send me nice wishes. 217 | 218 | To start developing, make sure the test suite passes:: 219 | 220 | virtualenv .env 221 | source .env/bin/activate 222 | pip install -r requirements/tests.txt 223 | python setup.py test 224 | 225 | Now go, do some coding. 226 | 227 | Feel free to drop me a message about critiques or feature requests. You can get 228 | in touch with me by mail_ or twitter_. 229 | 230 | Happy autofixturing! 231 | 232 | .. _github: https://github.com/gregmuellegger/django-autofixture 233 | .. _mail: mailto:gregor@muellegger.de 234 | .. _twitter: http://twitter.com/gregmuellegger 235 | .. _ForeignKey.limit_choices_to: http://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.limit_choices_to 236 | 237 | .. |build| image:: https://travis-ci.org/gregmuellegger/django-autofixture.svg?branch=master 238 | :alt: Build Status 239 | :scale: 100% 240 | :target: https://travis-ci.org/gregmuellegger/django-autofixture 241 | 242 | .. |package| image:: https://badge.fury.io/py/django-autofixture.svg 243 | :alt: Package Version 244 | :scale: 100% 245 | :target: http://badge.fury.io/py/django-autofixture 246 | -------------------------------------------------------------------------------- /autofixture/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import warnings 4 | from autofixture.base import AutoFixture 5 | from autofixture.constraints import InvalidConstraint 6 | from autofixture.compat import getargnames 7 | 8 | 9 | if sys.version_info[0] < 3: 10 | string_types = basestring 11 | else: 12 | string_types = str 13 | 14 | 15 | __version__ = '0.12.2.dev1' 16 | 17 | 18 | REGISTRY = {} 19 | 20 | 21 | def register(model, autofixture, overwrite=False, fail_silently=False): 22 | ''' 23 | Register a model with the registry. 24 | 25 | Arguments: 26 | 27 | *model* can be either a model class or a string that contains the model's 28 | app label and class name seperated by a dot, e.g. ``"app.ModelClass"``. 29 | 30 | *autofixture* is the :mod:`AutoFixture` subclass that shall be used to 31 | generated instances of *model*. 32 | 33 | By default :func:`register` will raise :exc:`ValueError` if the given 34 | *model* is already registered. You can overwrite the registered *model* if 35 | you pass ``True`` to the *overwrite* argument. 36 | 37 | The :exc:`ValueError` that is usually raised if a model is already 38 | registered can be suppressed by passing ``True`` to the *fail_silently* 39 | argument. 40 | ''' 41 | from .compat import get_model 42 | 43 | if isinstance(model, string_types): 44 | model = get_model(*model.split('.', 1)) 45 | if not overwrite and model in REGISTRY: 46 | if fail_silently: 47 | return 48 | raise ValueError( 49 | u'%s.%s is already registered. You can overwrite the registered ' 50 | u'autofixture by providing the `overwrite` argument.' % ( 51 | model._meta.app_label, 52 | model._meta.object_name, 53 | )) 54 | REGISTRY[model] = autofixture 55 | 56 | 57 | def unregister(model_or_iterable, fail_silently=False): 58 | ''' 59 | Remove one or more models from the autofixture registry. 60 | ''' 61 | from django.db import models 62 | from .compat import get_model 63 | 64 | if issubclass(model_or_iterable, models.Model): 65 | model_or_iterable = [model_or_iterable] 66 | for model in model_or_iterable: 67 | if isinstance(model, string_types): 68 | model = get_model(*model.split('.', 1)) 69 | try: 70 | del REGISTRY[model] 71 | except KeyError: 72 | if fail_silently: 73 | continue 74 | raise ValueError( 75 | u'The model %s.%s is not registered.' % ( 76 | model._meta.app_label, 77 | model._meta.object_name, 78 | )) 79 | 80 | 81 | def get(model, *args, **kwargs): 82 | ''' 83 | Get an autofixture instance for the passed in *model* sing the either an 84 | appropiate autofixture that was :ref:`registry ` or fall back 85 | to the default:class:`AutoFixture` class. *model* can be a model class or 86 | its string representation (e.g. ``"app.ModelClass"``). 87 | 88 | All positional and keyword arguments are passed to the autofixture 89 | constructor. 90 | ''' 91 | from .compat import get_model 92 | 93 | if isinstance(model, string_types): 94 | model = get_model(*model.split('.', 1)) 95 | if model in REGISTRY: 96 | return REGISTRY[model](model, *args, **kwargs) 97 | else: 98 | return AutoFixture(model, *args, **kwargs) 99 | 100 | 101 | def create(model, count, *args, **kwargs): 102 | ''' 103 | Create *count* instances of *model* using the either an appropiate 104 | autofixture that was :ref:`registry ` or fall back to the 105 | default:class:`AutoFixture` class. *model* can be a model class or its 106 | string representation (e.g. ``"app.ModelClass"``). 107 | 108 | All positional and keyword arguments are passed to the autofixture 109 | constructor. It is demonstrated in the example below which will create ten 110 | superusers:: 111 | 112 | import autofixture 113 | admins = autofixture.create('auth.User', 10, field_values={'is_superuser': True}) 114 | 115 | .. note:: See :ref:`AutoFixture` for more information. 116 | 117 | :func:`create` will return a list of the created objects. 118 | ''' 119 | from .compat import get_model 120 | 121 | if isinstance(model, string_types): 122 | model = get_model(*model.split('.', 1)) 123 | if model in REGISTRY: 124 | autofixture_class = REGISTRY[model] 125 | else: 126 | autofixture_class = AutoFixture 127 | # Get keyword arguments that the create_one method accepts and pass them 128 | # into create_one instead of AutoFixture.__init__ 129 | argnames = set(getargnames(autofixture_class.create_one)) 130 | argnames -= set(['self']) 131 | create_kwargs = {} 132 | for argname in argnames: 133 | if argname in kwargs: 134 | create_kwargs[argname] = kwargs.pop(argname) 135 | autofixture = autofixture_class(model, *args, **kwargs) 136 | return autofixture.create(count, **create_kwargs) 137 | 138 | 139 | def create_one(model, *args, **kwargs): 140 | ''' 141 | :func:`create_one` is exactly the as the :func:`create` function but a 142 | shortcut if you only want to generate one model instance. 143 | 144 | The function returns the instanciated model. 145 | ''' 146 | return create(model, 1, *args, **kwargs)[0] 147 | 148 | 149 | LOADING = False 150 | 151 | def autodiscover(): 152 | ''' 153 | Auto-discover INSTALLED_APPS autofixtures.py and tests.py modules and fail 154 | silently when not present. This forces an import on them to register any 155 | autofixture bits they may want. 156 | ''' 157 | from .compat import importlib 158 | 159 | # Bail out if autodiscover didn't finish loading from a previous call so 160 | # that we avoid running autodiscover again when the URLconf is loaded by 161 | # the exception handler to resolve the handler500 view. This prevents an 162 | # autofixtures.py module with errors from re-registering models and raising a 163 | # spurious AlreadyRegistered exception (see #8245). 164 | global LOADING 165 | if LOADING: 166 | return 167 | LOADING = True 168 | app_paths = {} 169 | 170 | # For each app, we need to look for an autofixture.py inside that app's 171 | # package. We can't use os.path here -- recall that modules may be 172 | # imported different ways (think zip files) -- so we need to get 173 | # the app's __path__ and look for autofixture.py on that path. 174 | 175 | # Step 1: find out the app's __path__ Import errors here will (and 176 | # should) bubble up, but a missing __path__ (which is legal, but weird) 177 | # fails silently -- apps that do weird things with __path__ might 178 | # need to roll their own autofixture registration. 179 | 180 | import imp 181 | try: 182 | from django.apps import apps 183 | 184 | for app_config in apps.get_app_configs(): 185 | app_paths[app_config.name] = [app_config.path] 186 | 187 | except ImportError: 188 | # Django < 1.7 189 | from django.conf import settings 190 | 191 | for app in settings.INSTALLED_APPS: 192 | mod = importlib.import_module(app) 193 | try: 194 | app_paths[app] = mod.__path__ 195 | except AttributeError: 196 | continue 197 | 198 | for app, app_path in app_paths.items(): 199 | # Step 2: use imp.find_module to find the app's autofixtures.py. For some 200 | # reason imp.find_module raises ImportError if the app can't be found 201 | # but doesn't actually try to import the module. So skip this app if 202 | # its autofixtures.py doesn't exist 203 | try: 204 | file, _, _ = imp.find_module('autofixtures', app_path) 205 | except ImportError: 206 | continue 207 | else: 208 | if file: 209 | file.close() 210 | 211 | # Step 3: import the app's autofixtures file. If this has errors we want them 212 | # to bubble up. 213 | try: 214 | importlib.import_module("%s.autofixtures" % app) 215 | except Exception as e: 216 | warnings.warn(u'Error while importing %s.autofixtures: %r' % 217 | (app, e)) 218 | 219 | for app, app_path in app_paths.items(): 220 | try: 221 | file, _, _ = imp.find_module('tests', app_path) 222 | except ImportError: 223 | continue 224 | else: 225 | if file: 226 | file.close() 227 | 228 | try: 229 | importlib.import_module("%s.tests" % app) 230 | except Exception as e: 231 | warnings.warn(u'Error while importing %s.tests: %r' % 232 | (app, e)) 233 | 234 | # autodiscover was successful, reset loading flag. 235 | LOADING = False 236 | -------------------------------------------------------------------------------- /autofixture/autofixtures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import autofixture 3 | import string 4 | from datetime import datetime 5 | from django.contrib.auth.hashers import make_password 6 | from django.contrib.auth.models import User 7 | from django.utils import timezone 8 | from autofixture import AutoFixture 9 | from autofixture import generators 10 | from .compat import get_field 11 | 12 | 13 | class UserFixture(AutoFixture): 14 | ''' 15 | :class:`UserFixture` is automatically used by default to create new 16 | ``User`` instances. It uses the following values to assure that you can 17 | use the generated instances without any modification: 18 | 19 | * ``username`` only contains chars that are allowed by django's auth forms. 20 | * ``email`` is unique. 21 | * ``first_name`` and ``last_name`` are single, random words of the lorem 22 | ipsum text. 23 | * ``is_staff`` and ``is_superuser`` are always ``False``. 24 | * ``is_active`` is always ``True``. 25 | * ``date_joined`` and ``last_login`` are always in the past and it is 26 | assured that ``date_joined`` will be lower than ``last_login``. 27 | ''' 28 | class Values(object): 29 | username = generators.StringGenerator( 30 | max_length=30, 31 | chars=string.ascii_letters + string.digits + '_') 32 | first_name = generators.LoremWordGenerator(1) 33 | last_name = generators.LoremWordGenerator(1) 34 | password = staticmethod(lambda: make_password(None)) 35 | is_active = True 36 | # don't generate admin users 37 | is_staff = False 38 | is_superuser = False 39 | date_joined = generators.DateTimeGenerator(max_date=timezone.now()) 40 | last_login = generators.DateTimeGenerator(max_date=timezone.now()) 41 | 42 | # don't follow permissions and groups 43 | follow_m2m = False 44 | 45 | def __init__(self, *args, **kwargs): 46 | ''' 47 | By default the password is set to an unusable value, this makes it 48 | impossible to login with the generated users. If you want to use for 49 | example ``autofixture.create_one('auth.User')`` in your unittests to have 50 | a user instance which you can use to login with the testing client you 51 | can provide a ``username`` and a ``password`` argument. Then you can do 52 | something like:: 53 | 54 | autofixture.create_one('auth.User', username='foo', password='bar`) 55 | self.client.login(username='foo', password='bar') 56 | ''' 57 | self.username = kwargs.pop('username', None) 58 | self.password = kwargs.pop('password', None) 59 | super(UserFixture, self).__init__(*args, **kwargs) 60 | if self.username: 61 | self.field_values['username'] = generators.StaticGenerator( 62 | self.username) 63 | 64 | def unique_email(self, model, instance): 65 | if User.objects.filter(email=instance.email).exists(): 66 | raise autofixture.InvalidConstraint((get_field(model,'email'),)) 67 | 68 | def prepare_class(self): 69 | self.add_constraint(self.unique_email) 70 | 71 | def post_process_instance(self, instance, commit): 72 | # make sure user's last login was not before he joined 73 | changed = False 74 | if instance.last_login < instance.date_joined: 75 | instance.last_login = instance.date_joined 76 | changed = True 77 | if self.password: 78 | instance.set_password(self.password) 79 | changed = True 80 | if changed and commit: 81 | instance.save() 82 | return instance 83 | 84 | 85 | autofixture.register(User, UserFixture, fail_silently=True) 86 | -------------------------------------------------------------------------------- /autofixture/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import warnings 3 | from django import VERSION 4 | from django.db.models import fields, ImageField 5 | from django.conf import settings 6 | from django.db.models.fields import related 7 | from django.utils.six import with_metaclass 8 | 9 | import autofixture 10 | from autofixture import constraints, generators, signals 11 | from autofixture.values import Values 12 | from autofixture.compat import ( 13 | OrderedDict, 14 | get_GenericRelation, 15 | get_remote_field, 16 | get_remote_field_to, 17 | getargnames, 18 | ) 19 | 20 | if 'django.contrib.gis' in settings.INSTALLED_APPS: 21 | from django.contrib.gis.db.models import PointField 22 | 23 | 24 | class CreateInstanceError(Exception): 25 | pass 26 | 27 | 28 | class Link(object): 29 | ''' 30 | Handles logic of following or generating foreignkeys and m2m relations. 31 | ''' 32 | def __init__(self, fields=None, default=None): 33 | self.fields = {} 34 | self.subfields = {} 35 | self.default = default 36 | 37 | fields = fields or {} 38 | if fields is True: 39 | fields = {'ALL': None} 40 | if not isinstance(fields, dict): 41 | fields = dict([(v, None) for v in fields]) 42 | for field, value in fields.items(): 43 | try: 44 | fieldname, subfield = field.split('__', 1) 45 | self.subfields.setdefault(fieldname, {})[subfield] = value 46 | except ValueError: 47 | self.fields[field] = value 48 | 49 | def __getitem__(self, key): 50 | return self.fields.get(key, 51 | self.fields.get('ALL', self.default)) 52 | 53 | def __iter__(self): 54 | for field in self.fields: 55 | yield field 56 | for key, value in self.subfields.items(): 57 | yield '%s__%s' % (key, value) 58 | 59 | def __contains__(self, value): 60 | if 'ALL' in self.fields: 61 | return True 62 | if value in self.fields: 63 | return True 64 | return False 65 | 66 | def get_deep_links(self, field): 67 | if 'ALL' in self.fields: 68 | fields = {'ALL': self.fields['ALL']} 69 | else: 70 | fields = self.subfields.get(field, {}) 71 | if 'ALL' in fields: 72 | fields = {'ALL': fields['ALL']} 73 | return Link(fields, default=self.default) 74 | 75 | 76 | class AutoFixtureMetaclass(type): 77 | def __new__(mcs, name, bases, attrs): 78 | values = Values() 79 | for base in bases[::-1]: 80 | values += base.field_values 81 | values += Values(attrs.pop('Values', {})) 82 | values += attrs.get('field_values', Values()) 83 | attrs['field_values'] = values 84 | return super(AutoFixtureMetaclass, mcs).__new__(mcs, name, bases, attrs) 85 | 86 | 87 | class AutoFixtureBase(object): 88 | ''' 89 | .. We don't support the following fields yet: 90 | 91 | * ``XMLField`` 92 | * ``FileField`` 93 | 94 | Patches are welcome. 95 | ''' 96 | class IGNORE_FIELD(object): 97 | pass 98 | 99 | overwrite_defaults = False 100 | follow_fk = True 101 | generate_fk = False 102 | follow_m2m = {'ALL': (1,5)} 103 | generate_m2m = False 104 | 105 | none_p = 0.2 106 | tries = 1000 107 | 108 | field_to_generator = OrderedDict(( 109 | (fields.BooleanField, generators.BooleanGenerator), 110 | (fields.NullBooleanField, generators.NullBooleanGenerator), 111 | (fields.DateTimeField, generators.DateTimeGenerator), 112 | (fields.DateField, generators.DateGenerator), 113 | (fields.PositiveSmallIntegerField, generators.PositiveSmallIntegerGenerator), 114 | (fields.PositiveIntegerField, generators.PositiveIntegerGenerator), 115 | (fields.SmallIntegerField, generators.SmallIntegerGenerator), 116 | (fields.IntegerField, generators.IntegerGenerator), 117 | (fields.FloatField, generators.FloatGenerator), 118 | (fields.IPAddressField, generators.IPAddressGenerator), 119 | (fields.GenericIPAddressField, generators.IPAddressGenerator), 120 | (fields.TextField, generators.LoremGenerator), 121 | (fields.TimeField, generators.TimeGenerator), 122 | (ImageField, generators.ImageGenerator), 123 | )) 124 | 125 | # UUIDField was added in Django 1.8 126 | if hasattr(fields, 'UUIDField'): 127 | field_to_generator[fields.UUIDField] = generators.UUIDGenerator 128 | 129 | if 'django.contrib.gis' in settings.INSTALLED_APPS: 130 | field_to_generator[PointField] = generators.PointFieldGenerator 131 | 132 | field_values = Values() 133 | 134 | default_constraints = [ 135 | constraints.unique_constraint, 136 | constraints.unique_together_constraint] 137 | 138 | def __init__(self, model, 139 | field_values=None, none_p=None, overwrite_defaults=None, 140 | constraints=None, follow_fk=None, generate_fk=None, 141 | follow_m2m=None, generate_m2m=None): 142 | ''' 143 | Parameters: 144 | ``model``: A model class which is used to create the test data. 145 | 146 | ``field_values``: A dictionary with field names of ``model`` as 147 | keys. Values may be static values that are assigned to the field, 148 | a ``Generator`` instance that generates a value on the fly or a 149 | callable which takes no arguments and returns the wanted value. 150 | 151 | ``none_p``: The chance (between 0 and 1, 1 equals 100%) to 152 | assign ``None`` to nullable fields. 153 | 154 | ``overwrite_defaults``: All default values of fields are preserved 155 | by default. If set to ``True``, default values will be treated 156 | like any other field. 157 | 158 | ``constraints``: A list of callables. The constraints are used to 159 | verify if the created model instance may be used. The callable 160 | gets the actual model as first and the instance as second 161 | parameter. The instance is not populated yet at this moment. The 162 | callable may raise an :exc:`InvalidConstraint` exception to 163 | indicate which fields violate the constraint. 164 | 165 | ``follow_fk``: A boolean value indicating if foreign keys should be 166 | set to random, already existing, instances of the related model. 167 | 168 | ``generate_fk``: A boolean which indicates if related models should 169 | also be created with random values. The *follow_fk* parameter will 170 | be ignored if *generate_fk* is set to ``True``. 171 | 172 | ``follow_m2m``: A tuple containing minium and maximum of model 173 | instances that are assigned to ``ManyToManyField``. No new 174 | instances will be created. Default is (1, 5). You can ignore 175 | ``ManyToManyField`` fields by setting this parameter to ``False``. 176 | 177 | ``generate_m2m``: A tuple containing minimum and maximum number of 178 | model instance that are newly created and assigned to the 179 | ``ManyToManyField``. Default is ``False`` which disables the 180 | generation of new related instances. The value of ``follow_m2m`` 181 | will be ignored if this parameter is set. 182 | ''' 183 | self.model = model 184 | self.field_values = Values(self.__class__.field_values) 185 | self.field_values += Values(field_values) 186 | self.constraints = constraints or [] 187 | if none_p is not None: 188 | self.none_p = none_p 189 | if overwrite_defaults is not None: 190 | self.overwrite_defaults = overwrite_defaults 191 | 192 | if follow_fk is not None: 193 | self.follow_fk = follow_fk 194 | if not isinstance(self.follow_fk, Link): 195 | self.follow_fk = Link(self.follow_fk) 196 | 197 | if generate_fk is not None: 198 | self.generate_fk = generate_fk 199 | if not isinstance(self.generate_fk, Link): 200 | self.generate_fk = Link(self.generate_fk) 201 | 202 | if follow_m2m is not None: 203 | if not isinstance(follow_m2m, dict): 204 | if follow_m2m: 205 | follow_m2m = Link({'ALL': follow_m2m}) 206 | else: 207 | follow_m2m = Link(False) 208 | self.follow_m2m = follow_m2m 209 | if not isinstance(self.follow_m2m, Link): 210 | self.follow_m2m = Link(self.follow_m2m) 211 | 212 | if generate_m2m is not None: 213 | if not isinstance(generate_m2m, dict): 214 | if generate_m2m: 215 | generate_m2m = Link({'ALL': generate_m2m}) 216 | else: 217 | generate_m2m = Link(False) 218 | self.generate_m2m = generate_m2m 219 | if not isinstance(self.generate_m2m, Link): 220 | self.generate_m2m = Link(self.generate_m2m) 221 | 222 | for constraint in self.default_constraints: 223 | self.add_constraint(constraint) 224 | 225 | self._field_generators = {} 226 | 227 | self.prepare_class() 228 | 229 | def prepare_class(self): 230 | ''' 231 | This method is called after the :meth:`__init__` method. It has no 232 | semantic by default. 233 | ''' 234 | pass 235 | 236 | def add_field_value(self, name, value): 237 | ''' 238 | Pass a *value* that should be assigned to the field called *name*. 239 | Thats the same as specifying it in the *field_values* argument of the 240 | :meth:`constructor `. 241 | ''' 242 | self.field_values[name] = value 243 | 244 | def add_constraint(self, constraint): 245 | ''' 246 | Add a *constraint* to the autofixture. 247 | ''' 248 | self.constraints.append(constraint) 249 | 250 | def is_inheritance_parent(self, field): 251 | ''' 252 | Checks if the field is the automatically created OneToOneField used by 253 | django mulit-table inheritance 254 | ''' 255 | return ( 256 | isinstance(field, related.OneToOneField) and 257 | field.primary_key and 258 | issubclass(field.model, get_remote_field_to(field)) 259 | ) 260 | 261 | def get_generator(self, field): 262 | ''' 263 | Return a value generator based on the field instance that is passed to 264 | this method. This function may return ``None`` which means that the 265 | specified field will be ignored (e.g. if no matching generator was 266 | found). 267 | ''' 268 | if isinstance(field, fields.AutoField): 269 | return None 270 | if self.is_inheritance_parent(field): 271 | return None 272 | if ( 273 | field.default is not fields.NOT_PROVIDED and 274 | not self.overwrite_defaults and 275 | field.name not in self.field_values): 276 | return None 277 | kwargs = {} 278 | 279 | if field.name in self.field_values: 280 | value = self.field_values[field.name] 281 | if isinstance(value, generators.Generator): 282 | return value 283 | elif isinstance(value, AutoFixture): 284 | return generators.InstanceGenerator(autofixture=value) 285 | elif callable(value): 286 | return generators.CallableGenerator(value=value) 287 | return generators.StaticGenerator(value=value) 288 | 289 | if field.null: 290 | kwargs['empty_p'] = self.none_p 291 | if field.choices: 292 | return generators.ChoicesGenerator(choices=field.choices, **kwargs) 293 | if isinstance(field, related.ForeignKey): 294 | # if generate_fk is set, follow_fk is ignored. 295 | is_self_fk = (get_remote_field_to(field)().__class__ == self.model) 296 | if field.name in self.generate_fk and not is_self_fk: 297 | return generators.InstanceGenerator( 298 | autofixture.get( 299 | get_remote_field_to(field), 300 | follow_fk=self.follow_fk.get_deep_links(field.name), 301 | generate_fk=self.generate_fk.get_deep_links(field.name)), 302 | limit_choices_to=get_remote_field(field).limit_choices_to) 303 | if field.name in self.follow_fk: 304 | selected = generators.InstanceSelector( 305 | get_remote_field_to(field), 306 | limit_choices_to=get_remote_field(field).limit_choices_to) 307 | if selected.get_value() is not None: 308 | return selected 309 | if field.blank or field.null: 310 | return generators.NoneGenerator() 311 | if is_self_fk and not field.null: 312 | raise CreateInstanceError( 313 | u'Cannot resolve self referencing field "%s" to "%s" without null=True' % ( 314 | field.name, 315 | '%s.%s' % ( 316 | get_remote_field_to(field)._meta.app_label, 317 | get_remote_field_to(field)._meta.object_name, 318 | ) 319 | )) 320 | raise CreateInstanceError( 321 | u'Cannot resolve ForeignKey "%s" to "%s". Provide either ' 322 | u'"follow_fk" or "generate_fk" parameters.' % ( 323 | field.name, 324 | '%s.%s' % ( 325 | get_remote_field_to(field)._meta.app_label, 326 | get_remote_field_to(field)._meta.object_name, 327 | ) 328 | )) 329 | if isinstance(field, related.ManyToManyField): 330 | if field.name in self.generate_m2m: 331 | min_count, max_count = self.generate_m2m[field.name] 332 | return generators.MultipleInstanceGenerator( 333 | autofixture.get(get_remote_field_to(field)), 334 | limit_choices_to=get_remote_field(field).limit_choices_to, 335 | min_count=min_count, 336 | max_count=max_count, 337 | **kwargs) 338 | if field.name in self.follow_m2m: 339 | min_count, max_count = self.follow_m2m[field.name] 340 | return generators.InstanceSelector( 341 | get_remote_field_to(field), 342 | limit_choices_to=get_remote_field(field).limit_choices_to, 343 | min_count=min_count, 344 | max_count=max_count, 345 | **kwargs) 346 | if field.blank or field.null: 347 | return generators.StaticGenerator([]) 348 | raise CreateInstanceError( 349 | u'Cannot assign instances of "%s" to ManyToManyField "%s". ' 350 | u'Provide either "follow_m2m" or "generate_m2m" argument.' % ( 351 | '%s.%s' % ( 352 | get_remote_field_to(field)._meta.app_label, 353 | get_remote_field_to(field)._meta.object_name, 354 | ), 355 | field.name, 356 | )) 357 | if isinstance(field, fields.FilePathField): 358 | return generators.FilePathGenerator( 359 | path=field.path, match=field.match, recursive=field.recursive, 360 | max_length=field.max_length, **kwargs) 361 | if isinstance(field, fields.CharField): 362 | if isinstance(field, fields.SlugField): 363 | generator = generators.SlugGenerator 364 | elif isinstance(field, fields.EmailField): 365 | return generators.EmailGenerator( 366 | max_length=min(field.max_length, 30)) 367 | elif isinstance(field, fields.URLField): 368 | return generators.URLGenerator( 369 | max_length=min(field.max_length, 25)) 370 | elif field.max_length > 15: 371 | return generators.LoremSentenceGenerator( 372 | common=False, 373 | max_length=field.max_length) 374 | else: 375 | generator = generators.StringGenerator 376 | return generator(max_length=field.max_length) 377 | if isinstance(field, fields.DecimalField): 378 | return generators.DecimalGenerator( 379 | decimal_places=field.decimal_places, 380 | max_digits=field.max_digits) 381 | if hasattr(fields, 'BigIntegerField'): 382 | if isinstance(field, fields.BigIntegerField): 383 | return generators.IntegerGenerator( 384 | min_value=-field.MAX_BIGINT - 1, 385 | max_value=field.MAX_BIGINT, 386 | **kwargs) 387 | if isinstance(field, ImageField): 388 | return generators.ImageGenerator(storage=field.storage, **kwargs) 389 | for field_class, generator in self.field_to_generator.items(): 390 | if isinstance(field, field_class): 391 | return generator(**kwargs) 392 | return None 393 | 394 | def get_value(self, field): 395 | ''' 396 | Return a random value that can be assigned to the passed *field* 397 | instance. 398 | ''' 399 | if field not in self._field_generators: 400 | self._field_generators[field] = self.get_generator(field) 401 | generator = self._field_generators[field] 402 | if generator is None: 403 | return self.IGNORE_FIELD 404 | value = generator() 405 | return value 406 | 407 | def process_field(self, instance, field, is_m2m=False): 408 | value = self.get_value(field) 409 | if value is self.IGNORE_FIELD: 410 | return 411 | if is_m2m and VERSION[0] >= 2: 412 | getattr(instance, field.name).set(value) 413 | else: 414 | setattr(instance, field.name, value) 415 | 416 | def process_m2m(self, instance, field): 417 | # check django's version number to determine how intermediary models 418 | # are checked if they are auto created or not. 419 | auto_created_through_model = False 420 | through = get_remote_field(field).through 421 | auto_created_through_model = through._meta.auto_created 422 | 423 | if auto_created_through_model: 424 | return self.process_field(instance, field, is_m2m=True) 425 | # if m2m relation has intermediary model: 426 | # * only generate relation if 'generate_m2m' is given 427 | # * first generate intermediary model and assign a newly created 428 | # related model to the foreignkey 429 | kwargs = {} 430 | if field.name in self.generate_m2m: 431 | # get fk to related model on intermediary model 432 | related_fks = [fk 433 | for fk in through._meta.fields 434 | if isinstance(fk, related.ForeignKey) and \ 435 | get_remote_field_to(fk) is get_remote_field_to(field)] 436 | self_fks = [fk 437 | for fk in through._meta.fields 438 | if isinstance(fk, related.ForeignKey) and \ 439 | get_remote_field_to(fk) is self.model] 440 | assert len(related_fks) == 1 441 | assert len(self_fks) == 1 442 | related_fk = related_fks[0] 443 | self_fk = self_fks[0] 444 | min_count, max_count = self.generate_m2m[field.name] 445 | intermediary_model = generators.MultipleInstanceGenerator( 446 | AutoFixture( 447 | through, 448 | field_values={ 449 | self_fk.name: instance, 450 | related_fk.name: generators.InstanceGenerator( 451 | autofixture.get(get_remote_field_to(field))) 452 | }), 453 | min_count=min_count, 454 | max_count=max_count, 455 | **kwargs).generate() 456 | 457 | def check_constrains(self, *args, **kwargs): 458 | raise TypeError( 459 | 'This method was renamed recently, since it contains a typo. ' 460 | 'Please use the check_constraints method from now on.') 461 | 462 | def check_constraints(self, instance): 463 | ''' 464 | Return fieldnames which need recalculation. 465 | ''' 466 | recalc_fields = [] 467 | for constraint in self.constraints: 468 | try: 469 | constraint(self.model, instance) 470 | except constraints.InvalidConstraint as e: 471 | recalc_fields.extend(e.fields) 472 | return recalc_fields 473 | 474 | def post_process_instance(self, instance, commit): 475 | ''' 476 | Overwrite this method to modify the created *instance* before it gets 477 | returned by the :meth:`create` or :meth:`create_one`. 478 | It gets the generated *instance* and must return the modified 479 | instance. The *commit* parameter indicates the *commit* value that the 480 | user passed into the :meth:`create` method. It defaults to ``True`` 481 | and should be respected, which means if it is set to ``False``, the 482 | *instance* should not be saved. 483 | ''' 484 | return instance 485 | 486 | def pre_process_instance(self, instance): 487 | ''' 488 | Same as :meth:`post_process_instance`, but it is being called before 489 | saving an *instance*. 490 | ''' 491 | return instance 492 | 493 | def create_one(self, commit=True): 494 | ''' 495 | Create and return one model instance. If *commit* is ``False`` the 496 | instance will not be saved and many to many relations will not be 497 | processed. 498 | 499 | Subclasses that override ``create_one`` can specify arbitrary keyword 500 | arguments. They will be passed through by the 501 | :meth:`autofixture.base.AutoFixture.create` method and the helper 502 | functions :func:`autofixture.create` and 503 | :func:`autofixture.create_one`. 504 | 505 | May raise :exc:`CreateInstanceError` if constraints are not satisfied. 506 | ''' 507 | tries = self.tries 508 | instance = self.model() 509 | process = instance._meta.fields 510 | while process and tries > 0: 511 | for field in process: 512 | self.process_field(instance, field) 513 | process = self.check_constraints(instance) 514 | tries -= 1 515 | if tries == 0: 516 | raise CreateInstanceError( 517 | u'Cannot solve constraints for "%s", tried %d times. ' 518 | u'Please check value generators or model constraints. ' 519 | u'At least the following fields are involved: %s' % ( 520 | '%s.%s' % ( 521 | self.model._meta.app_label, 522 | self.model._meta.object_name), 523 | self.tries, 524 | ', '.join([field.name for field in process]), 525 | )) 526 | 527 | instance = self.pre_process_instance(instance) 528 | 529 | if commit: 530 | instance.save() 531 | 532 | #to handle particular case of GenericRelation 533 | #in Django pre 1.6 it appears in .many_to_many 534 | many_to_many = [f for f in instance._meta.many_to_many 535 | if not isinstance(f, get_GenericRelation())] 536 | for field in many_to_many: 537 | self.process_m2m(instance, field) 538 | signals.instance_created.send( 539 | sender=self, 540 | model=self.model, 541 | instance=instance, 542 | committed=commit) 543 | 544 | post_process_kwargs = {} 545 | if 'commit' in getargnames(self.post_process_instance): 546 | post_process_kwargs['commit'] = commit 547 | else: 548 | warnings.warn( 549 | "Subclasses of AutoFixture need to provide a `commit` " 550 | "argument for post_process_instance methods", DeprecationWarning) 551 | return self.post_process_instance(instance, **post_process_kwargs) 552 | 553 | def create(self, count=1, commit=True, **kwargs): 554 | ''' 555 | Create and return ``count`` model instances. If *commit* is ``False`` 556 | the instances will not be saved and many to many relations will not be 557 | processed. 558 | 559 | May raise ``CreateInstanceError`` if constraints are not satisfied. 560 | 561 | The method internally calls :meth:`create_one` to generate instances. 562 | ''' 563 | object_list = [] 564 | for i in range(count): 565 | instance = self.create_one(commit=commit, **kwargs) 566 | object_list.append(instance) 567 | return object_list 568 | 569 | def iter(self, count=1, commit=True): 570 | for i in range(count): 571 | yield self.create_one(commit=commit) 572 | 573 | def __iter__(self): 574 | yield self.create_one() 575 | 576 | 577 | class AutoFixture(with_metaclass(AutoFixtureMetaclass, AutoFixtureBase)): 578 | pass 579 | -------------------------------------------------------------------------------- /autofixture/compat.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | 4 | def get_GenericForeignKey(): 5 | try: 6 | from django.contrib.contenttypes.fields import GenericForeignKey 7 | # For Django 1.6 and earlier 8 | except ImportError: 9 | from django.contrib.contenttypes.generic import GenericForeignKey 10 | return GenericForeignKey 11 | 12 | 13 | def get_GenericRelation(): 14 | try: 15 | from django.contrib.contenttypes.fields import GenericRelation 16 | # For Django 1.6 and earlier 17 | except ImportError: 18 | from django.contrib.contenttypes.generic import GenericRelation 19 | return GenericRelation 20 | 21 | try: 22 | from collections import OrderedDict 23 | except ImportError: 24 | from django.utils.datastructures import SortedDict as OrderedDict 25 | 26 | 27 | try: 28 | import importlib 29 | except ImportError: 30 | from django.utils import importlib 31 | 32 | 33 | try: 34 | from django.db.transaction import atomic 35 | # For django 1.5 and earlier 36 | except ImportError: 37 | from django.db.transaction import commit_on_success as atomic 38 | 39 | 40 | try: 41 | from django.apps import apps 42 | get_model = apps.get_model 43 | except ImportError: 44 | from django.db.models import get_model 45 | 46 | 47 | def get_field(model, field_name): 48 | if django.VERSION < (1, 8): 49 | return model._meta.get_field_by_name(field_name)[0] 50 | else: 51 | return model._meta.get_field(field_name) 52 | 53 | 54 | def get_remote_field(field): 55 | if django.VERSION < (1, 9): 56 | return field.rel 57 | else: 58 | return field.remote_field 59 | 60 | 61 | def get_remote_field_to(field): 62 | if django.VERSION < (1, 9): 63 | return field.rel.to 64 | else: 65 | return field.remote_field.model 66 | 67 | 68 | try: 69 | # added in Python 3.0 70 | from inspect import signature 71 | def getargnames(callable): 72 | return list(signature(callable).parameters.keys()) 73 | 74 | except ImportError: 75 | # loud DeprecationWarnings in 3.5 76 | from inspect import getargspec 77 | def getargnames(callable): 78 | return getargspec(callable).args 79 | -------------------------------------------------------------------------------- /autofixture/constraints.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db.models.fields import related 3 | from .compat import get_field, get_remote_field_to 4 | 5 | 6 | class InvalidConstraint(Exception): 7 | def __init__(self, fields, *args, **kwargs): 8 | self.fields = fields 9 | super(InvalidConstraint, self).__init__(*args, **kwargs) 10 | 11 | 12 | def _is_unique_field(field): 13 | if not field.unique: 14 | return False 15 | if field.primary_key: 16 | # Primary key fields should not generally be checked for unique constraints, except when the 17 | # primary key is a OneToOne mapping to an external table not via table inheritance, in which 18 | # case we don't want to create new objects which will overwrite existing objects. 19 | return (isinstance(field, related.OneToOneField) and 20 | not issubclass(field.model, get_remote_field_to(field))) 21 | else: 22 | return True 23 | 24 | 25 | def unique_constraint(model, instance): 26 | error_fields = [] 27 | for field in instance._meta.fields: 28 | if _is_unique_field(field): 29 | value = getattr(instance, field.name) 30 | 31 | # If the value is none and the field allows nulls, skip it 32 | if value is None and field.null: 33 | continue 34 | 35 | check = {field.name: value} 36 | 37 | if model._default_manager.filter(**check).exists(): 38 | error_fields.append(field) 39 | if error_fields: 40 | raise InvalidConstraint(error_fields) 41 | 42 | 43 | def unique_together_constraint(model, instance): 44 | if not instance._meta.unique_together: 45 | return 46 | error_fields = [] 47 | for unique_fields in instance._meta.unique_together: 48 | check = {} 49 | for field_name in unique_fields: 50 | if not get_field(instance, field_name).primary_key: 51 | check[field_name] = getattr(instance, field_name) 52 | if all(e is None for e in check.values()): 53 | continue 54 | 55 | if model._default_manager.filter(**check).exists(): 56 | error_fields.extend([ 57 | get_field(instance, field_name) 58 | for field_name in unique_fields 59 | ]) 60 | if error_fields: 61 | raise InvalidConstraint(error_fields) 62 | -------------------------------------------------------------------------------- /autofixture/generators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import uuid 4 | from django.conf import settings 5 | from django.core.files.base import ContentFile 6 | from django.core.files.storage import default_storage 7 | #from django.contrib.gis.geos import Point 8 | try: 9 | from django.utils import lorem_ipsum 10 | except ImportError: 11 | # Support Django < 1.8 12 | from django.contrib.webdesign import lorem_ipsum 13 | import os 14 | import random 15 | import re 16 | import string 17 | import sys 18 | from decimal import Decimal 19 | 20 | 21 | if sys.version_info[0] < 3: 22 | str_ = unicode 23 | else: 24 | str_ = str 25 | 26 | 27 | # backporting os.path.relpath, only availabe in python >= 2.6 28 | try: 29 | relpath = os.path.relpath 30 | except AttributeError: 31 | def relpath(path, start=os.curdir): 32 | """Return a relative version of a path""" 33 | 34 | if not path: 35 | raise ValueError("no path specified") 36 | 37 | start_list = os.path.abspath(start).split(os.path.sep) 38 | path_list = os.path.abspath(path).split(os.path.sep) 39 | 40 | # Work out how much of the filepath is shared by start and path. 41 | i = len(os.path.commonprefix([start_list, path_list])) 42 | 43 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 44 | if not rel_list: 45 | return os.curdir 46 | return os.path.join(*rel_list) 47 | 48 | 49 | class Generator(object): 50 | coerce_type = staticmethod(lambda x: x) 51 | empty_value = None 52 | empty_p = 0 53 | 54 | def __init__(self, empty_p=None, coerce=None): 55 | if empty_p is not None: 56 | self.empty_p = empty_p 57 | if coerce: 58 | self.coerce_type = coerce 59 | 60 | def coerce(self, value): 61 | return self.coerce_type(value) 62 | 63 | def generate(self): 64 | raise NotImplementedError 65 | 66 | def get_value(self): 67 | if random.random() < self.empty_p: 68 | return self.empty_value 69 | value = self.generate() 70 | return self.coerce(value) 71 | 72 | def __call__(self): 73 | return self.get_value() 74 | 75 | 76 | class StaticGenerator(Generator): 77 | def __init__(self, value, *args, **kwargs): 78 | self.value = value 79 | super(StaticGenerator, self).__init__(*args, **kwargs) 80 | 81 | def generate(self): 82 | return self.value 83 | 84 | 85 | class CallableGenerator(Generator): 86 | def __init__(self, value, args=None, kwargs=None, *xargs, **xkwargs): 87 | self.value = value 88 | self.args = args or () 89 | self.kwargs = kwargs or {} 90 | super(CallableGenerator, self).__init__(*xargs, **xkwargs) 91 | 92 | def generate(self): 93 | return self.value(*self.args, **self.kwargs) 94 | 95 | 96 | class NoneGenerator(Generator): 97 | def generate(self): 98 | return self.empty_value 99 | 100 | 101 | class StringGenerator(Generator): 102 | coerce_type = str_ 103 | singleline_chars = string.ascii_letters + u' ' 104 | multiline_chars = singleline_chars + u'\n' 105 | 106 | def __init__(self, chars=None, multiline=False, min_length=1, max_length=1000, *args, **kwargs): 107 | assert min_length >= 0 108 | assert max_length >= 0 109 | self.min_length = min_length 110 | self.max_length = max_length 111 | if chars is None: 112 | if multiline: 113 | self.chars = self.multiline_chars 114 | else: 115 | self.chars = self.singleline_chars 116 | else: 117 | self.chars = chars 118 | super(StringGenerator, self).__init__(*args, **kwargs) 119 | 120 | def generate(self): 121 | length = random.randint(self.min_length, self.max_length) 122 | value = u'' 123 | for x in range(length): 124 | value += random.choice(self.chars) 125 | return value 126 | 127 | 128 | class SlugGenerator(StringGenerator): 129 | def __init__(self, chars=None, *args, **kwargs): 130 | if chars is None: 131 | chars = string.ascii_lowercase + string.digits + '-' 132 | super(SlugGenerator, self).__init__(chars, multiline=False, *args, **kwargs) 133 | 134 | 135 | class LoremGenerator(Generator): 136 | coerce_type = str_ 137 | common = True 138 | count = 3 139 | method = 'b' 140 | 141 | def __init__(self, count=None, method=None, common=None, max_length=None, *args, **kwargs): 142 | if count is not None: 143 | self.count = count 144 | if method is not None: 145 | self.method = method 146 | if common is not None: 147 | self.common = common 148 | self.max_length = max_length 149 | super(LoremGenerator, self).__init__(*args, **kwargs) 150 | 151 | def generate(self): 152 | if self.method == 'w': 153 | lorem = lorem_ipsum.words(self.count, common=self.common) 154 | elif self.method == 's': 155 | lorem = u' '.join([ 156 | lorem_ipsum.sentence() 157 | for i in range(self.count)]) 158 | else: 159 | paras = lorem_ipsum.paragraphs(self.count, common=self.common) 160 | if self.method == 'p': 161 | paras = ['

%s

' % p for p in paras] 162 | lorem = u'\n\n'.join(paras) 163 | if self.max_length: 164 | length = random.randint(round(int(self.max_length) / 10), 165 | self.max_length) 166 | lorem = lorem[:max(1, length)] 167 | return lorem.strip() 168 | 169 | 170 | class LoremSentenceGenerator(LoremGenerator): 171 | method = 's' 172 | 173 | 174 | class LoremHTMLGenerator(LoremGenerator): 175 | method = 'p' 176 | 177 | 178 | class LoremWordGenerator(LoremGenerator): 179 | count = 7 180 | method = 'w' 181 | 182 | 183 | class IntegerGenerator(Generator): 184 | coerce_type = int 185 | min_value = - 10 ** 5 186 | max_value = 10 ** 5 187 | 188 | def __init__(self, min_value=None, max_value=None, *args, **kwargs): 189 | if min_value is not None: 190 | self.min_value = min_value 191 | if max_value is not None: 192 | self.max_value = max_value 193 | super(IntegerGenerator, self).__init__(*args, **kwargs) 194 | 195 | def generate(self): 196 | value = random.randint(self.min_value, self.max_value) 197 | return value 198 | 199 | 200 | class SmallIntegerGenerator(IntegerGenerator): 201 | min_value = -2 ** 7 202 | max_value = 2 ** 7 - 1 203 | 204 | 205 | class PositiveIntegerGenerator(IntegerGenerator): 206 | min_value = 0 207 | 208 | 209 | class PositiveSmallIntegerGenerator(SmallIntegerGenerator): 210 | min_value = 0 211 | 212 | 213 | class FloatGenerator(IntegerGenerator): 214 | coerce_type = float 215 | decimal_digits = 1 216 | 217 | def __init__(self, decimal_digits=None, *args, **kwargs): 218 | if decimal_digits is not None: 219 | self.decimal_digits = decimal_digits 220 | super(FloatGenerator, self).__init__(*args, **kwargs) 221 | 222 | def generate(self): 223 | value = super(FloatGenerator, self).generate() 224 | value = float(value) 225 | if self.decimal_digits: 226 | digits = random.randint(1, 10 ** self.decimal_digits) - 1 227 | digits = float(digits) 228 | value = value + digits / (10 ** self.decimal_digits) 229 | return value 230 | 231 | 232 | class ChoicesGenerator(Generator): 233 | def __init__(self, choices=(), values=(), *args, **kwargs): 234 | assert len(choices) or len(values) 235 | self.choices = list(choices) 236 | if not values: 237 | self.values = [k for k, v in self.choices] 238 | else: 239 | self.values = list(values) 240 | super(ChoicesGenerator, self).__init__(*args, **kwargs) 241 | 242 | def generate(self): 243 | return random.choice(self.values) 244 | 245 | 246 | class BooleanGenerator(ChoicesGenerator): 247 | def __init__(self, none=False, *args, **kwargs): 248 | values = (True, False) 249 | if none: 250 | values = values + (None,) 251 | super(BooleanGenerator, self).__init__(values=values, *args, **kwargs) 252 | 253 | 254 | class NullBooleanGenerator(BooleanGenerator): 255 | def __init__(self, none=True, *args, **kwargs): 256 | super(NullBooleanGenerator, self).__init__(none=none, *args, **kwargs) 257 | 258 | 259 | class DateTimeGenerator(Generator): 260 | def __init__(self, min_date=None, max_date=None, *args, **kwargs): 261 | from django.utils import timezone 262 | if min_date is not None: 263 | self.min_date = min_date 264 | else: 265 | self.min_date = timezone.now() - datetime.timedelta(365 * 5) 266 | if max_date is not None: 267 | self.max_date = max_date 268 | else: 269 | self.max_date = timezone.now() + datetime.timedelta(365 * 1) 270 | assert self.min_date < self.max_date 271 | super(DateTimeGenerator, self).__init__(*args, **kwargs) 272 | 273 | def generate(self): 274 | diff = self.max_date - self.min_date 275 | seconds = random.randint(0, diff.days * 3600 * 24 + diff.seconds) 276 | return self.min_date + datetime.timedelta(seconds=seconds) 277 | 278 | 279 | class DateGenerator(Generator): 280 | min_date = datetime.date.today() - datetime.timedelta(365 * 5) 281 | max_date = datetime.date.today() + datetime.timedelta(365 * 1) 282 | 283 | def __init__(self, min_date=None, max_date=None, *args, **kwargs): 284 | if min_date is not None: 285 | self.min_date = min_date 286 | if max_date is not None: 287 | self.max_date = max_date 288 | assert self.min_date < self.max_date 289 | super(DateGenerator, self).__init__(*args, **kwargs) 290 | 291 | def generate(self): 292 | diff = self.max_date - self.min_date 293 | days = random.randint(0, diff.days) 294 | date = self.min_date + datetime.timedelta(days=days) 295 | return date 296 | return datetime.date(date.year, date.month, date.day) 297 | 298 | 299 | class DecimalGenerator(Generator): 300 | coerce_type = Decimal 301 | 302 | max_digits = 24 303 | decimal_places = 10 304 | 305 | def __init__(self, max_digits=None, decimal_places=None, *args, **kwargs): 306 | if max_digits is not None: 307 | self.max_digits = max_digits 308 | if decimal_places is not None: 309 | self.decimal_places = decimal_places 310 | super(DecimalGenerator, self).__init__(*args, **kwargs) 311 | 312 | def generate(self): 313 | maxint = 10 ** self.max_digits - 1 314 | value = ( 315 | Decimal(random.randint(-maxint, maxint)) / 316 | 10 ** self.decimal_places) 317 | return value 318 | 319 | 320 | class PositiveDecimalGenerator(DecimalGenerator): 321 | def generate(self): 322 | maxint = 10 ** self.max_digits - 1 323 | value = ( 324 | Decimal(random.randint(0, maxint)) / 325 | 10 ** self.decimal_places) 326 | return value 327 | 328 | 329 | class FirstNameGenerator(Generator): 330 | """ Generates a first name, either male or female """ 331 | 332 | male = [ 333 | 'Abraham', 'Adam', 'Anthony', 'Brian', 'Bill', 'Ben', 'Calvin', 334 | 'David', 'Daniel', 'George', 'Henry', 'Isaac', 'Ian', 'Jonathan', 335 | 'Jeremy', 'Jacob', 'John', 'Jerry', 'Joseph', 'James', 'Larry', 336 | 'Michael', 'Mark', 'Paul', 'Peter', 'Phillip', 'Stephen', 'Tony', 337 | 'Titus', 'Trevor', 'Timothy', 'Victor', 'Vincent', 'Winston', 'Walt'] 338 | female = [ 339 | 'Abbie', 'Anna', 'Alice', 'Beth', 'Carrie', 'Christina', 'Danielle', 340 | 'Emma', 'Emily', 'Esther', 'Felicia', 'Grace', 'Gloria', 'Helen', 341 | 'Irene', 'Joanne', 'Joyce', 'Jessica', 'Kathy', 'Katie', 'Kelly', 342 | 'Linda', 'Lydia', 'Mandy', 'Mary', 'Olivia', 'Priscilla', 343 | 'Rebecca', 'Rachel', 'Susan', 'Sarah', 'Stacey', 'Vivian'] 344 | 345 | def __init__(self, gender=None): 346 | self.gender = gender 347 | self.all = self.male + self.female 348 | 349 | def generate(self): 350 | if self.gender == 'm': 351 | return random.choice(self.male) 352 | elif self.gender == 'f': 353 | return random.choice(self.female) 354 | else: 355 | return random.choice(self.all) 356 | 357 | 358 | class LastNameGenerator(Generator): 359 | """ Generates a last name """ 360 | 361 | surname = [ 362 | 'Smith', 'Walker', 'Conroy', 'Stevens', 'Jones', 'Armstrong', 363 | 'Johnson', 'White', 'Stone', 'Strong', 'Olson', 'Lee', 'Forrest', 364 | 'Baker', 'Portman', 'Davis', 'Clark', 'Brown', 'Roberts', 'Ellis', 365 | 'Jackson', 'Marshall', 'Wang', 'Chen', 'Chou', 'Tang', 'Huang', 'Liu', 366 | 'Shih', 'Su', 'Song', 'Yang', 'Chan', 'Tsai', 'Wong', 'Hsu', 'Cheng', 367 | 'Chang', 'Wu', 'Lin', 'Yu', 'Yao', 'Kang', 'Park', 'Kim', 'Choi', 368 | 'Ahn', 'Mujuni'] 369 | 370 | def generate(self): 371 | return random.choice(self.surname) 372 | 373 | class FullNameGenerator(Generator): 374 | """ Generates a full name joining FirstName and LastName """ 375 | 376 | def generate(self): 377 | return " ".join([FirstNameGenerator().generate(), \ 378 | LastNameGenerator().generate()]) 379 | 380 | 381 | class EmailGenerator(StringGenerator): 382 | chars = string.ascii_lowercase 383 | 384 | def __init__(self, chars=None, max_length=30, tlds=None, static_domain=None, *args, **kwargs): 385 | assert max_length >= 6 386 | if chars is not None: 387 | self.chars = chars 388 | self.tlds = tlds 389 | self.static_domain = static_domain 390 | super(EmailGenerator, self).__init__(self.chars, max_length=max_length, *args, **kwargs) 391 | 392 | def generate(self): 393 | maxl = self.max_length - 2 394 | 395 | if self.static_domain is None: 396 | if self.tlds: 397 | tld = random.choice(self.tlds) 398 | elif maxl > 4: 399 | tld = StringGenerator(self.chars, min_length=3, max_length=3).generate() 400 | maxl -= len(tld) 401 | assert maxl >= 2 402 | else: 403 | maxl -= len(self.static_domain) 404 | 405 | name = StringGenerator(self.chars, min_length=1, max_length=maxl-1).generate() 406 | maxl -= len(name) 407 | 408 | if self.static_domain is None: 409 | domain = StringGenerator(self.chars, min_length=1, max_length=maxl).generate() 410 | return '%s@%s.%s' % (name, domain, tld) 411 | else: 412 | return '%s@%s' % (name, self.static_domain) 413 | 414 | 415 | class URLGenerator(StringGenerator): 416 | chars = string.ascii_lowercase 417 | protocol = 'http' 418 | tlds = () 419 | 420 | def __init__(self, chars=None, max_length=30, protocol=None, tlds=None, 421 | *args, **kwargs): 422 | if chars is not None: 423 | self.chars = chars 424 | if protocol is not None: 425 | self.protocol = protocol 426 | if tlds is not None: 427 | self.tlds = tlds 428 | assert max_length > ( 429 | len(self.protocol) + len('://') + 430 | 1 + len('.') + 431 | max([2] + [len(tld) for tld in self.tlds if tld])) 432 | super(URLGenerator, self).__init__( 433 | chars=self.chars, max_length=max_length, *args, **kwargs) 434 | 435 | def generate(self): 436 | maxl = self.max_length - len(self.protocol) - 4 # len(://) + len(.) 437 | if self.tlds: 438 | tld = random.choice(self.tlds) 439 | maxl -= len(tld) 440 | else: 441 | tld_max_length = 3 if maxl >= 5 else 2 442 | tld = StringGenerator(self.chars, 443 | min_length=2, max_length=tld_max_length).generate() 444 | maxl -= len(tld) 445 | domain = StringGenerator(chars=self.chars, max_length=maxl).generate() 446 | return u'%s://%s.%s' % (self.protocol, domain, tld) 447 | 448 | 449 | class IPAddressGenerator(Generator): 450 | coerce_type = str_ 451 | 452 | def generate(self): 453 | return '.'.join([str_(part) for part in [ 454 | IntegerGenerator(min_value=1, max_value=254).generate(), 455 | IntegerGenerator(min_value=0, max_value=254).generate(), 456 | IntegerGenerator(min_value=0, max_value=254).generate(), 457 | IntegerGenerator(min_value=1, max_value=254).generate(), 458 | ]]) 459 | 460 | 461 | class TimeGenerator(Generator): 462 | coerce_type = str_ 463 | 464 | def generate(self): 465 | return u'%02d:%02d:%02d' % ( 466 | random.randint(0,23), 467 | random.randint(0,59), 468 | random.randint(0,59), 469 | ) 470 | 471 | 472 | class FilePathGenerator(Generator): 473 | coerce_type = str_ 474 | 475 | def __init__(self, path, match=None, recursive=False, max_length=None, *args, **kwargs): 476 | self.path = path 477 | self.match = match 478 | self.recursive = recursive 479 | self.max_length = max_length 480 | super(FilePathGenerator, self).__init__(*args, **kwargs) 481 | 482 | def generate(self): 483 | filenames = [] 484 | if self.match: 485 | match_re = re.compile(self.match) 486 | if self.recursive: 487 | for root, dirs, files in os.walk(self.path): 488 | for f in files: 489 | if self.match is None or self.match_re.search(f): 490 | f = os.path.join(root, f) 491 | filenames.append(f) 492 | else: 493 | try: 494 | for f in os.listdir(self.path): 495 | full_file = os.path.join(self.path, f) 496 | if os.path.isfile(full_file) and \ 497 | (self.match is None or match_re.search(f)): 498 | filenames.append(full_file) 499 | except OSError: 500 | pass 501 | if self.max_length: 502 | filenames = [fn for fn in filenames if len(fn) <= self.max_length] 503 | return random.choice(filenames) 504 | 505 | 506 | class MediaFilePathGenerator(FilePathGenerator): 507 | ''' 508 | Generates a valid filename of an existing file from a subdirectory of 509 | ``settings.MEDIA_ROOT``. The returned filename is relative to 510 | ``MEDIA_ROOT``. 511 | ''' 512 | def __init__(self, path='', *args, **kwargs): 513 | from django.conf import settings 514 | path = os.path.join(settings.MEDIA_ROOT, path) 515 | super(MediaFilePathGenerator, self).__init__(path, *args, **kwargs) 516 | 517 | def generate(self): 518 | from django.conf import settings 519 | filename = super(MediaFilePathGenerator, self).generate() 520 | filename = relpath(filename, settings.MEDIA_ROOT) 521 | return filename 522 | 523 | 524 | class InstanceGenerator(Generator): 525 | ''' 526 | Naive support for ``limit_choices_to``. It assignes specified value to 527 | field for dict items that have one of the following form:: 528 | 529 | fieldname: value 530 | fieldname__exact: value 531 | fieldname__iexact: value 532 | ''' 533 | def __init__(self, autofixture, limit_choices_to=None, *args, **kwargs): 534 | self.autofixture = autofixture 535 | limit_choices_to = limit_choices_to or {} 536 | for lookup, value in limit_choices_to.items(): 537 | bits = lookup.split('__') 538 | if len(bits) == 1 or \ 539 | len(bits) == 2 and bits[1] in ('exact', 'iexact'): 540 | self.autofixture.add_field_value(bits[0], StaticGenerator(value)) 541 | super(InstanceGenerator, self).__init__(*args, **kwargs) 542 | 543 | def generate(self): 544 | return self.autofixture.create()[0] 545 | 546 | 547 | class MultipleInstanceGenerator(InstanceGenerator): 548 | empty_value = [] 549 | 550 | def __init__(self, *args, **kwargs): 551 | self.min_count = kwargs.pop('min_count', 1) 552 | self.max_count = kwargs.pop('max_count', 10) 553 | super(MultipleInstanceGenerator, self).__init__(*args, **kwargs) 554 | 555 | def generate(self): 556 | instances = [] 557 | for i in range(random.randint(self.min_count, self.max_count)): 558 | instances.append( 559 | super(MultipleInstanceGenerator, self).generate()) 560 | return instances 561 | 562 | 563 | class InstanceSelector(Generator): 564 | ''' 565 | Select one or more instances from a queryset. 566 | ''' 567 | empty_value = [] 568 | 569 | def __init__(self, queryset, min_count=None, max_count=None, fallback=None, 570 | limit_choices_to=None, *args, **kwargs): 571 | from django.db.models.query import QuerySet 572 | if not isinstance(queryset, QuerySet): 573 | queryset = queryset._default_manager.all() 574 | limit_choices_to = limit_choices_to or {} 575 | self.queryset = queryset.filter(**limit_choices_to) 576 | self.fallback = fallback 577 | self.min_count = min_count 578 | self.max_count = max_count 579 | super(InstanceSelector, self).__init__(*args, **kwargs) 580 | 581 | def generate(self): 582 | if self.max_count is None: 583 | try: 584 | return self.queryset.order_by('?')[0] 585 | except IndexError: 586 | return self.fallback 587 | else: 588 | min_count = self.min_count or 0 589 | count = random.randint(min_count, self.max_count) 590 | return self.queryset.order_by('?')[:count] 591 | 592 | class WeightedGenerator(Generator): 593 | """ 594 | Takes a list of generator objects and integer weights, of the following form: 595 | [(generator, weight), (generator, weight),...] 596 | and returns a value from a generator chosen randomly by weight. 597 | """ 598 | 599 | def __init__(self, choices): 600 | self.choices = choices 601 | 602 | def weighted_choice(self, choices): 603 | total = sum(w for c, w in choices) 604 | r = random.uniform(0, total) 605 | upto = 0 606 | for c, w in choices: 607 | if upto + w > r: 608 | return c 609 | upto += w 610 | 611 | def generate(self): 612 | return self.weighted_choice(self.choices).generate() 613 | 614 | class ImageGenerator(Generator): 615 | ''' 616 | Generates a valid palceholder image and saves it to the ``settings.MEDIA_ROOT`` 617 | The returned filename is relative to ``MEDIA_ROOT``. 618 | ''' 619 | 620 | default_sizes = ( 621 | (100,100), 622 | (200,300), 623 | (400,600), 624 | ) 625 | 626 | def __init__(self, width=None, height=None, sizes=None, 627 | path='_autofixture', storage=None, *args, **kwargs): 628 | self.width = width 629 | self.height = height 630 | self.sizes = list(sizes or self.default_sizes) 631 | if self.width and self.height: 632 | self.sizes.append((width, height)) 633 | self.path = path 634 | self.storage = storage or default_storage 635 | super(ImageGenerator, self).__init__(*args, **kwargs) 636 | 637 | def generate_file_path(self, width, height, suffix=None): 638 | suffix = suffix if suffix is not None else '' 639 | filename ='{width}x{height}{suffix}.png'.format( 640 | width=width, height=height, suffix=suffix) 641 | return os.path.join(self.path, filename) 642 | 643 | def generate(self): 644 | from .placeholder import get_placeholder_image 645 | 646 | width, height = random.choice(self.sizes) 647 | 648 | # Ensure that _autofixture folder exists. 649 | i = 0 650 | path = self.generate_file_path(width, height) 651 | 652 | while self.storage.exists(path): 653 | i += 1 654 | path = self.generate_file_path(width, height, '_{0}'.format(i)) 655 | 656 | return self.storage.save( 657 | path, 658 | ContentFile(get_placeholder_image(width, height)) 659 | ) 660 | 661 | 662 | class UUIDGenerator(Generator): 663 | ''' 664 | Generates random uuid4. 665 | ''' 666 | 667 | def generate(self): 668 | return uuid.uuid4() 669 | 670 | 671 | # Geo 672 | if 'django.contrib.gis' in settings.INSTALLED_APPS: 673 | class PointFieldGenerator(Generator): 674 | 675 | def generate(self): 676 | latitude = random.uniform(-90, 90) 677 | longitude = random.uniform(-180, 180) 678 | return Point(longitude, latitude) 679 | -------------------------------------------------------------------------------- /autofixture/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture/management/__init__.py -------------------------------------------------------------------------------- /autofixture/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture/management/commands/__init__.py -------------------------------------------------------------------------------- /autofixture/management/commands/loadtestdata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Use the ``loadtestdata`` command like this:: 4 | 5 | django-admin.py loadtestdata [options] app.Model:# [app.Model:# ...] 6 | 7 | Its nearly self explanatory. Supply names of models, prefixed with their app 8 | name. After that, place a colon and tell the command how many objects you want 9 | to create. Here is an example of how to create three categories and twenty 10 | entries for you blogging app:: 11 | 12 | django-admin.py loadtestdata blog.Category:3 blog.Entry:20 13 | 14 | Voila! You have ready to use testing data populated to your database. The 15 | model fields are filled with data by producing randomly generated values 16 | depending on the type of the field. E.g. text fields are filled with lorem 17 | ipsum dummies, date fields are populated with random dates from the last 18 | years etc. 19 | 20 | There are a few command line options available. Mainly to control the 21 | behavior of related fields. If foreingkey or many to many fields should be 22 | populated with existing data or if the related models are also generated on 23 | the fly. Please have a look at the help page of the command for more 24 | information:: 25 | 26 | django-admin.py help loadtestdata 27 | ''' 28 | import django 29 | from django.utils.encoding import smart_text 30 | from django.db import models 31 | from django.core.management.base import BaseCommand, CommandError 32 | import autofixture 33 | from autofixture import signals 34 | from ...compat import atomic 35 | from ...compat import importlib 36 | from ...compat import get_model 37 | 38 | if django.VERSION < (1, 9): 39 | from optparse import make_option 40 | else: 41 | def make_option(*args, **kwargs): 42 | return {'args': args, 'kwargs': kwargs} 43 | 44 | 45 | class Command(BaseCommand): 46 | help = 'Create random model instances for testing purposes.' 47 | args = 'app.Model:# [app.Model:# ...]' 48 | 49 | def __init__(self, *args, **kwargs): 50 | params = ( 51 | make_option('-d', '--overwrite-defaults', action='store_true', 52 | dest='overwrite_defaults', default=None, 53 | help=u'Generate values for fields with default values. Default is to use ' + 54 | 'default values.'), 55 | make_option('--no-follow-fk', action='store_true', dest='no_follow_fk', default=None, 56 | help=u'Ignore foreignkeys while creating model instances.'), 57 | make_option('--generate-fk', action='store', dest='generate_fk', default=None, 58 | help=u'Do not use already existing instances for ForeignKey relations. ' + 59 | 'Create new instances instead. You can specify a comma sperated list of ' + 60 | 'field names or ALL to indicate that all foreignkeys should be generated ' + 61 | 'automatically.'), 62 | make_option('--no-follow-m2m', action='store_true', dest='no_follow_m2m', default=None, 63 | help=u'Ignore many to many fields while creating model instances.'), 64 | make_option('--follow-m2m', action='store', dest='follow_m2m', default=None, 65 | help=u'Specify minimum and maximum number of instances that are assigned ' + 66 | 'to a m2m relation. Use two, colon separated numbers in the form of: ' + 67 | 'min,max. Default is 1,5.\nYou can limit following of many to many ' + 68 | 'relations to specific fields using the following format:\nfield1:min:max' + 69 | ',field2:min:max ...'), 70 | make_option('--generate-m2m', action='store', dest='generate_m2m', default=None, 71 | help=u'Specify minimum and maximum number of instances that are newly ' + 72 | 'created and assigned to a m2m relation. Use two, colon separated ' + 73 | 'numbers in the form of: min:max. Default is to not generate many to ' + 74 | 'many related models automatically. You can select specific of many to ' + 75 | 'many fields which are automatically generated. Use the following ' + 76 | 'format:\nfield1:min:max,field2:min:max ...'), 77 | make_option('-u', '--use', action='store', dest='use', default='', 78 | help=u'Specify a autofixture subclass that is used to create the test ' + 79 | 'data. E.g. myapp.autofixtures.MyAutoFixture') 80 | ) 81 | 82 | if django.VERSION < (1, 9): 83 | self.option_list = BaseCommand.option_list + params 84 | else: 85 | self.argument_params = params 86 | super(Command, self).__init__(*args, **kwargs) 87 | 88 | def add_arguments(self, parser): 89 | parser.add_argument('args', nargs='+') 90 | for option in self.argument_params: 91 | parser.add_argument(*option['args'], **option['kwargs']) 92 | 93 | def format_output(self, obj): 94 | output = smart_text(obj) 95 | if len(output) > 50: 96 | output = u'{0} ...'.format(output[:50]) 97 | return output 98 | 99 | def print_instance(self, sender, model, instance, **kwargs): 100 | if self.verbosity < 1: 101 | return 102 | print(u'{0}(pk={1}): {2}'.format( 103 | '.'.join(( 104 | model._meta.app_label, 105 | model._meta.object_name)), 106 | smart_text(instance.pk), 107 | self.format_output(instance), 108 | )) 109 | if self.verbosity < 2: 110 | return 111 | for field in instance._meta.fields: 112 | if isinstance(field, models.ForeignKey): 113 | obj = getattr(instance, field.name) 114 | if isinstance(obj, models.Model): 115 | print('| {0} (pk={1}): {2}'.format( 116 | field.name, 117 | obj.pk, 118 | self.format_output(obj))) 119 | for field in instance._meta.many_to_many: 120 | qs = getattr(instance, field.name).all() 121 | if qs.exists(): 122 | print('| {0} (count={1}):'.format( 123 | field.name, 124 | qs.count())) 125 | for obj in qs: 126 | print('| | (pk={0}): {1}'.format( 127 | obj.pk, 128 | self.format_output(obj))) 129 | 130 | @atomic 131 | def handle(self, *attrs, **options): 132 | error_option = None 133 | # 134 | # follow options 135 | # 136 | if options['no_follow_fk'] is None: 137 | follow_fk = None 138 | else: 139 | follow_fk = False 140 | if options['no_follow_m2m'] is None: 141 | follow_m2m = None 142 | # this is the only chance for the follow_m2m options to be parsed 143 | if options['follow_m2m']: 144 | try: 145 | value = options['follow_m2m'].split(',') 146 | if len(value) == 1 and value[0].count(':') == 1: 147 | follow_m2m = [int(i) for i in value[0].split(':')] 148 | else: 149 | follow_m2m = {} 150 | for field in value: 151 | key, minval, maxval = field.split(':') 152 | follow_m2m[key] = int(minval), int(maxval) 153 | except ValueError: 154 | error_option = '--follow-m2m={0}'.format(options['follow_m2m']) 155 | else: 156 | follow_m2m = False 157 | # 158 | # generation options 159 | # 160 | if options['generate_fk'] is None: 161 | generate_fk = None 162 | else: 163 | generate_fk = options['generate_fk'].split(',') 164 | generate_m2m = None 165 | if options['generate_m2m']: 166 | try: 167 | value = [v for v in options['generate_m2m'].split(',') if v] 168 | if len(value) == 1 and value[0].count(':') == 1: 169 | generate_m2m = [int(i) for i in value[0].split(':')] 170 | else: 171 | generate_m2m = {} 172 | for field in value: 173 | key, minval, maxval = field.split(':') 174 | generate_m2m[key] = int(minval), int(maxval) 175 | except ValueError: 176 | error_option = '--generate-m2m={0}'.format(options['generate_m2m']) 177 | 178 | if error_option: 179 | raise CommandError( 180 | u'Invalid option {0}\nExpected: {1}=field:min:max,field2:min:max... (min and max must be numbers)'.format( 181 | error_option, 182 | error_option.split('=', 1)[0])) 183 | 184 | use = options['use'] 185 | if use: 186 | use = use.split('.') 187 | use = getattr(importlib.import_module('.'.join(use[:-1])), use[-1]) 188 | 189 | overwrite_defaults = options['overwrite_defaults'] 190 | self.verbosity = int(options['verbosity']) 191 | 192 | models = [] 193 | for attr in attrs: 194 | try: 195 | app_label, model_label = attr.split('.') 196 | model_label, count = model_label.split(':') 197 | count = int(count) 198 | except ValueError: 199 | raise CommandError( 200 | u'Invalid argument: {0}\nExpected: app_label.ModelName:count (count must be a number)'.format(attr)) 201 | model = get_model(app_label, model_label) 202 | if not model: 203 | raise CommandError( 204 | u'Unknown model: {0}.{1}'.format(app_label, model_label)) 205 | models.append((model, count)) 206 | 207 | signals.instance_created.connect( 208 | self.print_instance) 209 | 210 | autofixture.autodiscover() 211 | 212 | kwargs = { 213 | 'overwrite_defaults': overwrite_defaults, 214 | 'follow_fk': follow_fk, 215 | 'generate_fk': generate_fk, 216 | 'follow_m2m': follow_m2m, 217 | 'generate_m2m': generate_m2m, 218 | } 219 | 220 | for model, count in models: 221 | if use: 222 | fixture = use(model, **kwargs) 223 | fixture.create(count) 224 | else: 225 | autofixture.create(model, count, **kwargs) 226 | 227 | -------------------------------------------------------------------------------- /autofixture/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture/models.py -------------------------------------------------------------------------------- /autofixture/placeholder.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import io 4 | from PIL import Image 5 | from PIL import ImageDraw 6 | from PIL import ImageColor 7 | from PIL import ImageFont 8 | from PIL import ImageOps 9 | 10 | get_color = lambda name: ImageColor.getrgb(name) 11 | 12 | 13 | def get_placeholder_image(width, height, name=None, fg_color=get_color('black'), 14 | bg_color=get_color('grey'), text=None, font=u'Verdana.ttf', 15 | fontsize=42, encoding=u'unic', mode='RGBA', fmt=u'PNG'): 16 | """Little spin-off from https://github.com/Visgean/python-placeholder 17 | that not saves an image and instead returns it.""" 18 | size = (width, height) 19 | text = text if text else '{0}x{1}'.format(width, height) 20 | 21 | try: 22 | font = ImageFont.truetype(font, size=fontsize, encoding=encoding) 23 | except IOError: 24 | font = ImageFont.load_default() 25 | 26 | result_img = Image.new(mode, size, bg_color) 27 | 28 | text_size = font.getsize(text) 29 | text_img = Image.new("RGBA", size, bg_color) 30 | 31 | #position for the text: 32 | left = size[0] / 2 - text_size[0] / 2 33 | top = size[1] / 2 - text_size[1] / 2 34 | 35 | drawing = ImageDraw.Draw(text_img) 36 | drawing.text((left, top), 37 | text, 38 | font=font, 39 | fill=fg_color) 40 | 41 | txt_img = ImageOps.fit(text_img, size, method=Image.BICUBIC, centering=(0.5, 0.5)) 42 | 43 | result_img.paste(txt_img) 44 | file_obj = io.BytesIO() 45 | txt_img.save(file_obj, fmt) 46 | 47 | return file_obj.getvalue() -------------------------------------------------------------------------------- /autofixture/signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.dispatch import Signal 3 | 4 | 5 | instance_created = Signal(providing_args=['model', 'instance', 'committed']) 6 | -------------------------------------------------------------------------------- /autofixture/values.py: -------------------------------------------------------------------------------- 1 | from django.utils.six import with_metaclass 2 | 3 | 4 | class ValuesMetaclass(type): 5 | def __new__(mcs, name, bases, attrs): 6 | parent_value_attrs = {} 7 | # left most base class overwrites righter base classes 8 | for base in bases[::-1]: 9 | if hasattr(base, '_value_attrs'): 10 | parent_value_attrs.update(base._value_attrs) 11 | defined_value_attrs = {} 12 | for key in attrs: 13 | if not key.startswith('__'): 14 | defined_value_attrs[key] = attrs[key] 15 | 16 | for key in defined_value_attrs: 17 | del attrs[key] 18 | 19 | attrs['_value_attrs'] = {} 20 | attrs['_value_attrs'].update(parent_value_attrs) 21 | attrs['_value_attrs'].update(defined_value_attrs) 22 | return super(ValuesMetaclass, mcs).__new__(mcs, name, bases, attrs) 23 | 24 | 25 | class ValuesBase(dict): 26 | def __init__(self, *parents, **values): 27 | self.update(self._value_attrs) 28 | for parent in parents: 29 | if parent is None: 30 | continue 31 | if isinstance(parent, dict): 32 | self.update(parent) 33 | else: 34 | for attr in dir(parent): 35 | if not attr.startswith('__'): 36 | self[attr] = getattr(parent, attr) 37 | self.update(**values) 38 | 39 | def __add__(self, other): 40 | return self.__class__(self, other) 41 | 42 | def __radd__(self, other): 43 | return self.__class__(other, self) 44 | 45 | def __iadd__(self, other): 46 | self.update(other) 47 | return self 48 | 49 | 50 | class Values(with_metaclass(ValuesMetaclass, ValuesBase)): 51 | pass 52 | -------------------------------------------------------------------------------- /autofixture_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture_tests/__init__.py -------------------------------------------------------------------------------- /autofixture_tests/appconfig_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture_tests/appconfig_test/__init__.py -------------------------------------------------------------------------------- /autofixture_tests/appconfig_test/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfigTestConfig(AppConfig): 5 | name = 'autofixture_tests.appconfig_test' 6 | verbose_name = "AppConfig Test" 7 | -------------------------------------------------------------------------------- /autofixture_tests/appconfig_test/autofixtures.py: -------------------------------------------------------------------------------- 1 | import autofixture 2 | from .models import TestModel 3 | 4 | 5 | class CustomTestModelFixture(autofixture.AutoFixture): 6 | pass 7 | 8 | 9 | autofixture.register(TestModel, CustomTestModelFixture) 10 | -------------------------------------------------------------------------------- /autofixture_tests/appconfig_test/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TestModel(models.Model): 5 | pass 6 | -------------------------------------------------------------------------------- /autofixture_tests/appconfig_test/test_autodiscover.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.conf import settings 3 | from django.test import TestCase 4 | 5 | import autofixture 6 | from ..compat import skipIf 7 | from .models import TestModel 8 | 9 | 10 | class AutodiscoverTest(TestCase): 11 | @skipIf(django.VERSION < (1, 7), 'AppConfig only applies to Django >= 1.7') 12 | def test_testmodel_fixture_was_loaded(self): 13 | self.assertTrue(any( 14 | app for app in settings.INSTALLED_APPS 15 | if 'AppConfigTestConfig' in app)) 16 | 17 | testmodelfixture = autofixture.REGISTRY[TestModel] 18 | self.assertEqual(testmodelfixture.__name__, 'CustomTestModelFixture') 19 | -------------------------------------------------------------------------------- /autofixture_tests/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest import skipIf 3 | except ImportError: 4 | from django.utils.unittest import skipIf 5 | 6 | try: 7 | from django.conf.urls.defaults import url 8 | except ImportError: 9 | from django.conf.urls import url 10 | 11 | try: 12 | from django.conf.urls.defaults import include 13 | except ImportError: 14 | from django.conf.urls import include 15 | -------------------------------------------------------------------------------- /autofixture_tests/media/emptyfiles/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture_tests/media/emptyfiles/empty.txt -------------------------------------------------------------------------------- /autofixture_tests/media/emptyfiles/nofilextension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture_tests/media/emptyfiles/nofilextension -------------------------------------------------------------------------------- /autofixture_tests/media/textfiles/ispum.txt: -------------------------------------------------------------------------------- 1 | Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 2 | amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy 3 | eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 4 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita 5 | kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 6 | -------------------------------------------------------------------------------- /autofixture_tests/media/textfiles/lorem.txt: -------------------------------------------------------------------------------- 1 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit 2 | lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure 3 | dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore 4 | eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui 5 | blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla 6 | facilisi. 7 | -------------------------------------------------------------------------------- /autofixture_tests/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.files.storage import FileSystemStorage 3 | import os 4 | from datetime import datetime 5 | from django.db import models 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.utils.timezone import utc 8 | 9 | from autofixture.compat import get_GenericForeignKey 10 | from autofixture.compat import get_GenericRelation 11 | 12 | try: 13 | from django.db.models import GenericIPAddressField as IPAddressField 14 | except ImportError: 15 | from django.models import IPAddressField 16 | 17 | filepath = os.path.dirname(os.path.abspath(__file__)) 18 | 19 | 20 | def y2k(): 21 | return datetime(2000, 1, 1).replace(tzinfo=utc) 22 | 23 | 24 | class SimpleModel(models.Model): 25 | name = models.CharField(max_length=50) 26 | 27 | 28 | class OtherSimpleModel(models.Model): 29 | name = models.CharField(max_length=50) 30 | 31 | 32 | class UniqueNullFieldModel(models.Model): 33 | name = models.CharField(max_length=15, null=True, blank=True, unique=True) 34 | 35 | 36 | class UniqueTogetherNullFieldModel(models.Model): 37 | field_one = models.CharField(max_length=15, null=True, blank=True) 38 | field_two = models.CharField(max_length=15, null=True, blank=True) 39 | 40 | class Meta: 41 | unique_together = ['field_one', 'field_two'] 42 | 43 | 44 | class MultipleUniqueTogetherNullFieldModel(models.Model): 45 | field_one = models.CharField(max_length=15, null=True, blank=True) 46 | field_two = models.CharField(max_length=15, null=True, blank=True) 47 | 48 | field_three = models.CharField(max_length=15, null=True, blank=True) 49 | field_four = models.CharField(max_length=15, null=True, blank=True) 50 | field_five = models.CharField(max_length=15, null=True, blank=True) 51 | 52 | class Meta: 53 | verbose_name = 'Multi unique_together null field' 54 | unique_together = ( 55 | ['field_one', 'field_two'], 56 | ['field_three', 'field_four', 'field_five'], 57 | ) 58 | 59 | 60 | class DeepLinkModel1(models.Model): 61 | related = models.ForeignKey('SimpleModel') 62 | related2 = models.ForeignKey('SimpleModel', 63 | related_name='deeplinkmodel1_rel2', 64 | null=True, 65 | blank=True) 66 | 67 | 68 | class DeepLinkModel2(models.Model): 69 | related = models.ForeignKey('DeepLinkModel1') 70 | 71 | 72 | class NullableFKModel(models.Model): 73 | m2m = models.ManyToManyField('SimpleModel', null=True, blank=True) 74 | 75 | 76 | class BasicModel(models.Model): 77 | chars = models.CharField(max_length=50) 78 | shortchars = models.CharField(max_length=2) 79 | blankchars = models.CharField(max_length=100, blank=True) 80 | nullchars = models.CharField(max_length=100, blank=True, null=True) 81 | slugfield = models.SlugField() 82 | textfield = models.TextField() 83 | blankfloatfield = models.FloatField(null=True, blank=True) 84 | floatfield = models.FloatField() 85 | 86 | defaultint = models.IntegerField(default=1) 87 | intfield = models.IntegerField() 88 | pintfield = models.PositiveIntegerField() 89 | sintfield = models.SmallIntegerField() 90 | psintfield = models.PositiveSmallIntegerField() 91 | 92 | STRING_CHOICES = ( 93 | ('a', 'A'), 94 | ('b', 'B'), 95 | ('c', 'C'), 96 | ) 97 | choicefield = models.CharField(choices=STRING_CHOICES, max_length=1) 98 | 99 | datefield = models.DateField() 100 | datetimefield = models.DateTimeField() 101 | defaultdatetime = models.DateTimeField(default=y2k) 102 | timefield = models.TimeField() 103 | 104 | decimalfield = models.DecimalField(max_digits=10, decimal_places=4) 105 | 106 | emailfield = models.EmailField() 107 | ipaddressfield = IPAddressField() 108 | urlfield = models.URLField() 109 | rfilepathfield = models.FilePathField(path=filepath, recursive=True) 110 | filepathfield = models.FilePathField(path=filepath) 111 | mfilepathfield = models.FilePathField(path=filepath, match=r'^.+\.py$') 112 | imgfield = models.ImageField(upload_to='_autofixtures') 113 | 114 | 115 | class UniqueTestModel(models.Model): 116 | CHOICES = [(i, i) for i in range(10)] 117 | 118 | choice1 = models.PositiveIntegerField(choices=CHOICES, unique=True) 119 | 120 | 121 | class UniqueTogetherTestModel(models.Model): 122 | CHOICES = [(i, i) for i in range(10)] 123 | 124 | choice1 = models.PositiveIntegerField(choices=CHOICES) 125 | choice2 = models.PositiveIntegerField(choices=CHOICES) 126 | 127 | class Meta: 128 | unique_together = ('choice1', 'choice2') 129 | 130 | 131 | class RelatedModel(models.Model): 132 | related = models.ForeignKey(BasicModel, related_name='rel1') 133 | limitedfk = models.ForeignKey(SimpleModel, 134 | limit_choices_to={'name__exact': 'foo'}, 135 | related_name='rel2', 136 | null=True, 137 | blank=True) 138 | 139 | 140 | class O2OModel(models.Model): 141 | o2o = models.OneToOneField(SimpleModel) 142 | 143 | 144 | class O2OPrimaryKeyModel(models.Model): 145 | o2o = models.OneToOneField(SimpleModel, primary_key=True) 146 | 147 | 148 | class InheritModel(SimpleModel): 149 | extrafloatfield = models.FloatField() 150 | 151 | 152 | class InheritUniqueTogetherModel(SimpleModel): 153 | extrafloatfield = models.FloatField() 154 | 155 | class Meta: 156 | unique_together = ('extrafloatfield', 'simplemodel_ptr') 157 | 158 | 159 | class SelfReferencingModel(models.Model): 160 | parent_self = models.ForeignKey('self', blank=True, null=True) 161 | 162 | 163 | class SelfReferencingModelNoNull(models.Model): 164 | parent_self = models.ForeignKey('self') 165 | 166 | 167 | class M2MModel(models.Model): 168 | m2m = models.ManyToManyField(SimpleModel, related_name='m2m_rel1') 169 | secondm2m = models.ManyToManyField( 170 | OtherSimpleModel, related_name='m2m_rel2', null=True, blank=True) 171 | 172 | 173 | class ThroughModel(models.Model): 174 | simple = models.ForeignKey('SimpleModel') 175 | other = models.ForeignKey('M2MModelThrough') 176 | 177 | 178 | class M2MModelThrough(models.Model): 179 | m2m = models.ManyToManyField( 180 | SimpleModel, related_name='m2mthrough_rel1', through=ThroughModel) 181 | 182 | 183 | class GFKModel(models.Model): 184 | content_type = models.ForeignKey(ContentType) 185 | object_id = models.PositiveIntegerField() 186 | content_object = get_GenericForeignKey()('content_type', 'object_id') 187 | 188 | 189 | class GRModel(models.Model): 190 | gr = get_GenericRelation()('GFKModel') 191 | 192 | 193 | class DummyStorage(FileSystemStorage): 194 | pass 195 | 196 | 197 | dummy_storage = DummyStorage() 198 | 199 | 200 | class ImageModel(models.Model): 201 | imgfield = models.ImageField(upload_to='_autofixtures', 202 | storage=dummy_storage) 203 | 204 | 205 | class RelationWithCustomAutofixtureModel(models.Model): 206 | user = models.ForeignKey('auth.User', related_name='user1+') 207 | users = models.ManyToManyField('auth.User', related_name='user2+') 208 | -------------------------------------------------------------------------------- /autofixture_tests/sample_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assem-ch/django-autofixture/4994539018f7164b56b3171a5ada06b7731a7bec/autofixture_tests/sample_app/__init__.py -------------------------------------------------------------------------------- /autofixture_tests/sample_app/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from autofixture_tests.sample_app.models import Post 4 | 5 | 6 | admin.site.register(Post) 7 | -------------------------------------------------------------------------------- /autofixture_tests/sample_app/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | 4 | 5 | class Author(models.Model): 6 | name = models.CharField(max_length=50) 7 | 8 | def __unicode__(self): 9 | return self.name 10 | 11 | 12 | class Category(models.Model): 13 | name = models.CharField(max_length=50) 14 | 15 | 16 | class Post(models.Model): 17 | name = models.CharField(max_length=50) 18 | text = models.TextField() 19 | author = models.ForeignKey(Author) 20 | categories = models.ManyToManyField(Category, null=True, blank=True) 21 | 22 | def __unicode__(self): 23 | return '%s: %s' % (self.name, self.text) 24 | -------------------------------------------------------------------------------- /autofixture_tests/settings.py: -------------------------------------------------------------------------------- 1 | import django 2 | import os 3 | import warnings 4 | 5 | 6 | warnings.simplefilter('always') 7 | 8 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'NAME': os.path.join(PROJECT_ROOT, 'db.sqlite'), 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | } 15 | } 16 | 17 | SITE_ID = 1 18 | 19 | # Set in order to catch timezone aware vs unaware comparisons 20 | USE_TZ = True 21 | 22 | # If you set this to False, Django will make some optimizations so as not 23 | # to load the internationalization machinery. 24 | USE_I18N = True 25 | USE_L10N = True 26 | 27 | # Absolute path to the directory that holds media. 28 | # Example: "/home/media/media.lawrence.com/" 29 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media') 30 | 31 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 32 | # trailing slash if there is a path component (optional in other cases). 33 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 34 | MEDIA_URL = '/media/' 35 | 36 | # Make this unique, and don't share it with anybody. 37 | SECRET_KEY = '0' 38 | 39 | ROOT_URLCONF = 'autofixture_tests.urls' 40 | 41 | INSTALLED_APPS = ( 42 | 'django.contrib.auth', 43 | 'django.contrib.contenttypes', 44 | 'django.contrib.sessions', 45 | 'django.contrib.sites', 46 | 'django.contrib.admin', 47 | 48 | 'autofixture', 49 | 'autofixture_tests', 50 | 'autofixture_tests.tests', 51 | 'autofixture_tests.sample_app', 52 | ) 53 | 54 | if django.VERSION >= (1, 7): 55 | INSTALLED_APPS = INSTALLED_APPS + ( 56 | 'autofixture_tests.appconfig_test.apps.AppConfigTestConfig', 57 | ) 58 | 59 | MIDDLEWARE_CLASSES = () 60 | 61 | 62 | if django.VERSION < (1, 6): 63 | TEST_RUNNER = 'discover_runner.DiscoverRunner' 64 | else: 65 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 66 | -------------------------------------------------------------------------------- /autofixture_tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.test import TestCase 6 | 7 | 8 | class FileSystemCleanupTestCase(TestCase): 9 | def setUp(self): 10 | self.cleanup_dirs = ['_autofixture'] 11 | 12 | def tearDown(self): 13 | for path in self.cleanup_dirs: 14 | img_folder = os.path.join(settings.MEDIA_ROOT, path) 15 | if os.path.exists(img_folder): 16 | shutil.rmtree(img_folder) 17 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_autodiscover.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | import autofixture 4 | 5 | 6 | autofixture.autodiscover() 7 | 8 | 9 | class AutodiscoverTestCase(TestCase): 10 | def test_builtin_fixtures(self): 11 | from autofixture.autofixtures import UserFixture 12 | self.assertEqual(autofixture.REGISTRY[User], UserFixture) 13 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import autofixture 4 | from django.core.management import call_command 5 | from decimal import Decimal 6 | from datetime import date, datetime 7 | from autofixture import generators, constraints 8 | from autofixture.base import AutoFixture, CreateInstanceError, Link 9 | from autofixture.compat import get_field 10 | from autofixture.values import Values 11 | from . import FileSystemCleanupTestCase 12 | from ..models import y2k 13 | from ..models import ( 14 | SimpleModel, OtherSimpleModel, DeepLinkModel1, DeepLinkModel2, 15 | NullableFKModel, BasicModel, UniqueTestModel, UniqueTogetherTestModel, 16 | RelatedModel, O2OModel, O2OPrimaryKeyModel, InheritModel, InheritUniqueTogetherModel, 17 | M2MModel, ThroughModel, M2MModelThrough, SelfReferencingModel, 18 | UniqueNullFieldModel, UniqueTogetherNullFieldModel, 19 | MultipleUniqueTogetherNullFieldModel, SelfReferencingModelNoNull, GFKModel, 20 | GRModel, RelationWithCustomAutofixtureModel) 21 | 22 | 23 | autofixture.autodiscover() 24 | 25 | 26 | if sys.version_info[0] < 3: 27 | str_ = unicode 28 | else: 29 | str_ = str 30 | 31 | 32 | class SimpleAutoFixture(AutoFixture): 33 | field_values = { 34 | 'name': generators.StaticGenerator('foo'), 35 | } 36 | 37 | 38 | class BasicValueFixtureBase(AutoFixture): 39 | field_values = Values(blankchars='bar') 40 | 41 | 42 | class BasicValueFixture(BasicValueFixtureBase): 43 | class Values: 44 | chars = 'foo' 45 | shortchars = staticmethod(lambda: 'a') 46 | intfield = generators.IntegerGenerator(min_value=1, max_value=13) 47 | 48 | field_values = { 49 | 'nullchars': 'spam', 50 | } 51 | 52 | 53 | class TestBasicModel(FileSystemCleanupTestCase): 54 | def assertEqualOr(self, first, second, fallback): 55 | if first != second and not fallback: 56 | self.fail() 57 | 58 | def test_create(self): 59 | filler = AutoFixture(BasicModel) 60 | filler.create(10) 61 | self.assertEqual(BasicModel.objects.count(), 10) 62 | 63 | def test_constraints(self): 64 | filler = AutoFixture( 65 | BasicModel, 66 | overwrite_defaults=False) 67 | for obj in filler.create(100): 68 | self.assertTrue(len(obj.chars) > 0) 69 | self.assertEqual(type(obj.chars), str_) 70 | self.assertTrue(len(obj.shortchars) <= 2) 71 | self.assertEqual(type(obj.shortchars), str_) 72 | self.assertTrue(type(obj.blankchars), str_) 73 | self.assertEqualOr(type(obj.nullchars), str_, None) 74 | self.assertEqual(type(obj.slugfield), str_) 75 | self.assertEqual(type(obj.defaultint), int) 76 | self.assertEqual(obj.defaultint, 1) 77 | self.assertEqual(type(obj.intfield), int) 78 | self.assertEqual(type(obj.sintfield), int) 79 | self.assertEqual(type(obj.pintfield), int) 80 | self.assertEqual(type(obj.psintfield), int) 81 | self.assertEqual(type(obj.datefield), date) 82 | self.assertEqual(type(obj.datetimefield), datetime) 83 | self.assertEqual(type(obj.defaultdatetime), datetime) 84 | self.assertEqual(obj.defaultdatetime, y2k()) 85 | self.assertEqual(type(obj.decimalfield), Decimal) 86 | self.assertTrue('@' in obj.emailfield) 87 | self.assertTrue('.' in obj.emailfield) 88 | self.assertTrue(' ' not in obj.emailfield) 89 | self.assertTrue(obj.ipaddressfield.count('.'), 3) 90 | self.assertTrue(len(obj.ipaddressfield) >= 7) 91 | self.assertEqual(BasicModel.objects.count(), 100) 92 | 93 | def test_field_values(self): 94 | int_value = 1 95 | char_values = (u'a', u'b') 96 | filler = AutoFixture( 97 | BasicModel, 98 | field_values={ 99 | 'intfield': 1, 100 | 'chars': generators.ChoicesGenerator(values=char_values), 101 | 'shortchars': lambda: u'ab', 102 | }) 103 | for obj in filler.create(100): 104 | self.assertEqual(obj.intfield, int_value) 105 | self.assertTrue(obj.chars in char_values) 106 | self.assertEqual(obj.shortchars, u'ab') 107 | 108 | def test_field_values_overwrite_defaults(self): 109 | fixture = AutoFixture( 110 | BasicModel, 111 | field_values={ 112 | 'defaultint': 42, 113 | }) 114 | obj = fixture.create(1)[0] 115 | self.assertEqual(obj.defaultint, 42) 116 | 117 | 118 | class TestRelations(FileSystemCleanupTestCase): 119 | def test_generate_foreignkeys(self): 120 | filler = AutoFixture( 121 | RelatedModel, 122 | generate_fk=True) 123 | for obj in filler.create(100): 124 | self.assertEqual(obj.related.__class__, BasicModel) 125 | self.assertEqual(obj.limitedfk.name, 'foo') 126 | 127 | def test_deep_generate_foreignkeys(self): 128 | filler = AutoFixture( 129 | DeepLinkModel2, 130 | generate_fk=True) 131 | for obj in filler.create(10): 132 | self.assertEqual(obj.related.__class__, DeepLinkModel1) 133 | self.assertEqual(obj.related.related.__class__, SimpleModel) 134 | self.assertEqual(obj.related.related2.__class__, SimpleModel) 135 | 136 | def test_deep_generate_foreignkeys2(self): 137 | filler = AutoFixture( 138 | DeepLinkModel2, 139 | follow_fk=False, 140 | generate_fk=('related', 'related__related')) 141 | for obj in filler.create(10): 142 | self.assertEqual(obj.related.__class__, DeepLinkModel1) 143 | self.assertEqual(obj.related.related.__class__, SimpleModel) 144 | self.assertEqual(obj.related.related2, None) 145 | 146 | def test_generate_only_some_foreignkeys(self): 147 | filler = AutoFixture( 148 | RelatedModel, 149 | generate_fk=('related',)) 150 | for obj in filler.create(100): 151 | self.assertEqual(obj.related.__class__, BasicModel) 152 | self.assertEqual(obj.limitedfk, None) 153 | 154 | def test_follow_foreignkeys(self): 155 | related = AutoFixture(BasicModel).create()[0] 156 | self.assertEqual(BasicModel.objects.count(), 1) 157 | 158 | simple = SimpleModel.objects.create(name='foo') 159 | simple2 = SimpleModel.objects.create(name='bar') 160 | 161 | filler = AutoFixture( 162 | RelatedModel, 163 | follow_fk=True) 164 | for obj in filler.create(100): 165 | self.assertEqual(obj.related, related) 166 | self.assertEqual(obj.limitedfk, simple) 167 | 168 | def test_follow_only_some_foreignkeys(self): 169 | related = AutoFixture(BasicModel).create()[0] 170 | self.assertEqual(BasicModel.objects.count(), 1) 171 | 172 | simple = SimpleModel.objects.create(name='foo') 173 | simple2 = SimpleModel.objects.create(name='bar') 174 | 175 | filler = AutoFixture( 176 | RelatedModel, 177 | follow_fk=('related',)) 178 | for obj in filler.create(100): 179 | self.assertEqual(obj.related, related) 180 | self.assertEqual(obj.limitedfk, None) 181 | 182 | def test_follow_fk_for_o2o(self): 183 | # OneToOneField is the same as a ForeignKey with unique=True 184 | filler = AutoFixture(O2OModel, follow_fk=True) 185 | 186 | simple = SimpleModel.objects.create() 187 | obj = filler.create()[0] 188 | self.assertEqual(obj.o2o, simple) 189 | 190 | self.assertRaises(CreateInstanceError, filler.create) 191 | 192 | def test_generate_fk_for_o2o(self): 193 | # OneToOneField is the same as a ForeignKey with unique=True 194 | filler = AutoFixture(O2OModel, generate_fk=True) 195 | 196 | all_o2o = set() 197 | for obj in filler.create(10): 198 | all_o2o.add(obj.o2o) 199 | 200 | self.assertEqual(set(SimpleModel.objects.all()), all_o2o) 201 | 202 | def test_follow_fk_for_o2o_primary_key(self): 203 | # OneToOneField on primary key should follow if it is not table inheritance 204 | filler = AutoFixture(O2OPrimaryKeyModel, follow_fk=True) 205 | 206 | simple = SimpleModel.objects.create() 207 | obj = filler.create()[0] 208 | self.assertEqual(obj.o2o, simple) 209 | 210 | self.assertRaises(CreateInstanceError, filler.create) 211 | 212 | def test_generate_fk_for_o2o_primary_key(self): 213 | # OneToOneField on primary key should follow if it is not table inheritance 214 | filler = AutoFixture(O2OPrimaryKeyModel, generate_fk=True) 215 | 216 | all_o2o = set() 217 | for obj in filler.create(10): 218 | all_o2o.add(obj.o2o) 219 | 220 | self.assertEqual(set(SimpleModel.objects.all()), all_o2o) 221 | 222 | def test_follow_m2m(self): 223 | related = AutoFixture(SimpleModel).create()[0] 224 | self.assertEqual(SimpleModel.objects.count(), 1) 225 | 226 | filler = AutoFixture( 227 | M2MModel, 228 | follow_m2m=(2, 10)) 229 | for obj in filler.create(10): 230 | self.assertEqual(list(obj.m2m.all()), [related]) 231 | 232 | def test_follow_only_some_m2m(self): 233 | related = AutoFixture(SimpleModel).create()[0] 234 | self.assertEqual(SimpleModel.objects.count(), 1) 235 | other_related = AutoFixture(OtherSimpleModel).create()[0] 236 | self.assertEqual(OtherSimpleModel.objects.count(), 1) 237 | 238 | filler = AutoFixture( 239 | M2MModel, 240 | none_p=0, 241 | follow_m2m={ 242 | 'm2m': (2, 10), 243 | }) 244 | for obj in filler.create(10): 245 | self.assertEqual(list(obj.m2m.all()), [related]) 246 | self.assertEqual(list(obj.secondm2m.all()), []) 247 | 248 | def test_generate_m2m(self): 249 | filler = AutoFixture( 250 | M2MModel, 251 | none_p=0, 252 | generate_m2m=(1, 5)) 253 | all_m2m = set() 254 | all_secondm2m = set() 255 | for obj in filler.create(10): 256 | self.assertTrue(1 <= obj.m2m.count() <= 5) 257 | self.assertTrue(1 <= obj.secondm2m.count() <= 5) 258 | all_m2m.update(obj.m2m.all()) 259 | all_secondm2m.update(obj.secondm2m.all()) 260 | self.assertEqual(SimpleModel.objects.count(), len(all_m2m)) 261 | self.assertEqual(OtherSimpleModel.objects.count(), len(all_secondm2m)) 262 | 263 | def test_generate_m2m_with_custom_autofixture(self): 264 | filler = AutoFixture(RelationWithCustomAutofixtureModel, 265 | generate_fk=True, 266 | generate_m2m=(1, 1)) 267 | instance = filler.create_one() 268 | self.assertEqual(instance.users.count(), 1) 269 | user = instance.users.get() 270 | 271 | # Detect that the UserFixture was used. 272 | self.assertTrue(' ' not in user.username) 273 | self.assertTrue(' ' not in user.first_name) 274 | self.assertTrue(' ' not in user.last_name) 275 | 276 | def test_generate_only_some_m2m(self): 277 | filler = AutoFixture( 278 | M2MModel, 279 | none_p=0, 280 | generate_m2m={ 281 | 'm2m': (1, 5), 282 | }) 283 | all_m2m = set() 284 | all_secondm2m = set() 285 | for obj in filler.create(10): 286 | self.assertTrue(1 <= obj.m2m.count() <= 5) 287 | self.assertEqual(0, obj.secondm2m.count()) 288 | all_m2m.update(obj.m2m.all()) 289 | all_secondm2m.update(obj.secondm2m.all()) 290 | self.assertEqual(SimpleModel.objects.count(), len(all_m2m)) 291 | self.assertEqual(OtherSimpleModel.objects.count(), len(all_secondm2m)) 292 | 293 | def test_generate_m2m_with_intermediary_model(self): 294 | filler = AutoFixture( 295 | M2MModelThrough, 296 | generate_m2m=(1, 5)) 297 | all_m2m = set() 298 | for obj in filler.create(10): 299 | self.assertTrue(1 <= obj.m2m.count() <= 5) 300 | all_m2m.update(obj.m2m.all()) 301 | self.assertEqual(SimpleModel.objects.count(), len(all_m2m)) 302 | 303 | def test_generate_fk_to_self(self): 304 | ''' When a model with a reference to itself is encountered, If NULL is allowed 305 | don't generate a new instance of itself as a foreign key, so as not to reach 306 | pythons recursion limit 307 | ''' 308 | filler = AutoFixture(SelfReferencingModel, generate_fk=True) 309 | model = filler.create_one() 310 | self.assertEqual(model.parent_self, None) 311 | self.assertEqual(SelfReferencingModel.objects.count(), 1) 312 | 313 | def test_generate_fk_to_self_no_null(self): 314 | ''' Throw an exception when a model is encountered which references itself but 315 | does not allow NULL values to be set. 316 | ''' 317 | filler = AutoFixture(SelfReferencingModelNoNull, generate_fk=True) 318 | self.assertRaises(CreateInstanceError, filler.create_one) 319 | 320 | def test_generate_fk_to_self_follow(self): 321 | filler = AutoFixture(SelfReferencingModel, follow_fk=True) 322 | first = filler.create_one() 323 | self.assertEqual(SelfReferencingModel.objects.count(), 1) 324 | 325 | filler = AutoFixture(SelfReferencingModel, follow_fk=True) 326 | second = filler.create_one() 327 | self.assertEqual(SelfReferencingModel.objects.count(), 2) 328 | self.assertEqual(second.parent_self, first) 329 | 330 | 331 | class TestInheritModel(FileSystemCleanupTestCase): 332 | def test_inheritence_model(self): 333 | filler = AutoFixture(InheritModel) 334 | filler.create(10) 335 | self.assertEqual(InheritModel.objects.count(), 10) 336 | 337 | def test_inheritence_unique_together_model(self): 338 | filler = AutoFixture(InheritUniqueTogetherModel) 339 | filler.create(10) 340 | self.assertEqual(InheritUniqueTogetherModel.objects.count(), 10) 341 | 342 | 343 | class TestUniqueConstraints(FileSystemCleanupTestCase): 344 | def test_unique_field(self): 345 | filler = AutoFixture(UniqueTestModel) 346 | count = len(get_field(filler.model, 'choice1').choices) 347 | for obj in filler.create(count): 348 | pass 349 | 350 | def test_unique_together(self): 351 | filler = AutoFixture(UniqueTogetherTestModel) 352 | count1 = len(get_field(filler.model, 'choice1').choices) 353 | count2 = len(get_field(filler.model, 'choice2').choices) 354 | for obj in filler.create(count1 * count2): 355 | pass 356 | 357 | def test_unique_constraint_null(self): 358 | fixture = AutoFixture( 359 | UniqueNullFieldModel, 360 | field_values={ 361 | 'name': generators.NoneGenerator() 362 | } 363 | ) 364 | self.assertIn(constraints.unique_constraint, fixture.constraints) 365 | fixture.create_one() 366 | # Creating another entry with a null value should not raise an 367 | # exception as a unique column can contain multiple null values 368 | fixture.create_one() 369 | 370 | def test_unique_together_constraint_nulls(self): 371 | fixture = AutoFixture( 372 | UniqueTogetherNullFieldModel, 373 | field_values={ 374 | 'field_one': generators.NoneGenerator(), 375 | 'field_two': generators.NoneGenerator() 376 | } 377 | ) 378 | self.assertIn(constraints.unique_together_constraint, 379 | fixture.constraints) 380 | fixture.create_one() 381 | fixture.create_one() 382 | 383 | def test_unique_together_constraint_one_field_null(self): 384 | fixture = AutoFixture( 385 | UniqueTogetherNullFieldModel, 386 | field_values={ 387 | 'field_one': generators.NoneGenerator(), 388 | 'field_two': generators.StaticGenerator('test_string') 389 | } 390 | ) 391 | self.assertIn(constraints.unique_together_constraint, 392 | fixture.constraints) 393 | with self.assertRaises(CreateInstanceError): 394 | fixture.create_one() 395 | fixture.create_one() 396 | 397 | def test_multiple_unique_together_constraint_nulls(self): 398 | fixture = AutoFixture( 399 | MultipleUniqueTogetherNullFieldModel, 400 | field_values={ 401 | 'field_one': generators.NoneGenerator(), 402 | 'field_two': generators.NoneGenerator(), 403 | 'field_three': generators.NoneGenerator(), 404 | 'field_four': generators.NoneGenerator(), 405 | 'field_five': generators.NoneGenerator(), 406 | } 407 | ) 408 | self.assertIn(constraints.unique_together_constraint, 409 | fixture.constraints) 410 | fixture.create_one() 411 | fixture.create_one() 412 | 413 | def test_multiple_unique_together_constraint_one_field_null(self): 414 | fixture = AutoFixture( 415 | MultipleUniqueTogetherNullFieldModel, 416 | field_values={ 417 | 'field_one': generators.NoneGenerator(), 418 | 'field_two': generators.NoneGenerator(), 419 | 'field_three': generators.NoneGenerator(), 420 | 'field_four': generators.NoneGenerator(), 421 | 'field_five': generators.StaticGenerator('test_string'), 422 | } 423 | ) 424 | self.assertIn(constraints.unique_together_constraint, 425 | fixture.constraints) 426 | with self.assertRaises(CreateInstanceError): 427 | fixture.create_one() 428 | fixture.create_one() 429 | 430 | def test_multiple_unique_together_constraint_one_field_null_first_unique_together_tuple(self): 431 | fixture = AutoFixture( 432 | MultipleUniqueTogetherNullFieldModel, 433 | field_values={ 434 | 'field_one': generators.NoneGenerator(), 435 | 'field_two': generators.StaticGenerator('test_string'), 436 | 'field_three': generators.NoneGenerator(), 437 | 'field_four': generators.NoneGenerator(), 438 | 'field_five': generators.NoneGenerator(), 439 | } 440 | ) 441 | self.assertIn(constraints.unique_together_constraint, 442 | fixture.constraints) 443 | with self.assertRaises(CreateInstanceError): 444 | fixture.create_one() 445 | fixture.create_one() 446 | 447 | 448 | class TestGenerators(FileSystemCleanupTestCase): 449 | def test_instance_selector(self): 450 | AutoFixture(SimpleModel).create(10) 451 | 452 | result = generators.InstanceSelector(SimpleModel).generate() 453 | self.assertEqual(result.__class__, SimpleModel) 454 | 455 | for i in range(10): 456 | result = generators.InstanceSelector( 457 | SimpleModel, max_count=10).generate() 458 | self.assertTrue(0 <= len(result) <= 10) 459 | for obj in result: 460 | self.assertEqual(obj.__class__, SimpleModel) 461 | for i in range(10): 462 | result = generators.InstanceSelector( 463 | SimpleModel, min_count=5, max_count=10).generate() 464 | self.assertTrue(5 <= len(result) <= 10) 465 | for obj in result: 466 | self.assertEqual(obj.__class__, SimpleModel) 467 | for i in range(10): 468 | result = generators.InstanceSelector( 469 | SimpleModel, min_count=20, max_count=100).generate() 470 | # cannot return more instances than available 471 | self.assertEqual(len(result), 10) 472 | for obj in result: 473 | self.assertEqual(obj.__class__, SimpleModel) 474 | 475 | # works also with queryset as argument 476 | result = generators.InstanceSelector(SimpleModel.objects.all()).generate() 477 | self.assertEqual(result.__class__, SimpleModel) 478 | 479 | 480 | class TestLinkClass(FileSystemCleanupTestCase): 481 | def test_flat_link(self): 482 | link = Link(('foo', 'bar')) 483 | self.assertTrue('foo' in link) 484 | self.assertTrue('bar' in link) 485 | self.assertFalse('spam' in link) 486 | 487 | self.assertEqual(link['foo'], None) 488 | self.assertEqual(link['spam'], None) 489 | 490 | def test_nested_links(self): 491 | link = Link(('foo', 'foo__bar', 'spam__ALL')) 492 | self.assertTrue('foo' in link) 493 | self.assertFalse('spam' in link) 494 | self.assertFalse('egg' in link) 495 | 496 | foolink = link.get_deep_links('foo') 497 | self.assertTrue('bar' in foolink) 498 | self.assertFalse('egg' in foolink) 499 | 500 | spamlink = link.get_deep_links('spam') 501 | self.assertTrue('bar' in spamlink) 502 | self.assertTrue('egg' in spamlink) 503 | 504 | def test_links_with_value(self): 505 | link = Link({'foo': 1, 'spam__egg': 2}, default=0) 506 | self.assertTrue('foo' in link) 507 | self.assertEqual(link['foo'], 1) 508 | self.assertFalse('spam' in link) 509 | self.assertEqual(link['spam'], 0) 510 | 511 | spamlink = link.get_deep_links('spam') 512 | self.assertTrue('egg' in spamlink) 513 | self.assertEqual(spamlink['bar'], 0) 514 | self.assertEqual(spamlink['egg'], 2) 515 | 516 | def test_always_true_link(self): 517 | link = Link(True) 518 | self.assertTrue('field' in link) 519 | self.assertTrue('any' in link) 520 | 521 | link = link.get_deep_links('field') 522 | self.assertTrue('field' in link) 523 | self.assertTrue('any' in link) 524 | 525 | link = Link(('ALL',)) 526 | self.assertTrue('field' in link) 527 | self.assertTrue('any' in link) 528 | 529 | link = link.get_deep_links('field') 530 | self.assertTrue('field' in link) 531 | self.assertTrue('any' in link) 532 | 533 | def test_inherit_always_true_value(self): 534 | link = Link({'ALL': 1}) 535 | self.assertEqual(link['foo'], 1) 536 | 537 | sublink = link.get_deep_links('foo') 538 | self.assertEqual(sublink['bar'], 1) 539 | 540 | 541 | class TestRegistry(FileSystemCleanupTestCase): 542 | def setUp(self): 543 | self.original_registry = autofixture.REGISTRY 544 | autofixture.REGISTRY = {} 545 | 546 | def tearDown(self): 547 | autofixture.REGISTRY = self.original_registry 548 | 549 | def test_registration(self): 550 | autofixture.register(SimpleModel, SimpleAutoFixture) 551 | self.assertTrue(SimpleModel in autofixture.REGISTRY) 552 | self.assertEqual(autofixture.REGISTRY[SimpleModel], SimpleAutoFixture) 553 | 554 | def test_unregister(self): 555 | autofixture.register(SimpleModel, SimpleAutoFixture) 556 | self.assertTrue(SimpleModel in autofixture.REGISTRY) 557 | self.assertEqual(autofixture.REGISTRY[SimpleModel], SimpleAutoFixture) 558 | 559 | autofixture.unregister(SimpleModel) 560 | self.assertFalse(SimpleModel in autofixture.REGISTRY) 561 | 562 | def test_create(self): 563 | autofixture.register(SimpleModel, SimpleAutoFixture) 564 | for obj in autofixture.create(SimpleModel, 10): 565 | self.assertEqual(obj.name, 'foo') 566 | obj = autofixture.create_one(SimpleModel) 567 | self.assertEqual(obj.name, 'foo') 568 | 569 | def test_overwrite_attributes(self): 570 | autofixture.register(SimpleModel, SimpleAutoFixture) 571 | for obj in autofixture.create( 572 | SimpleModel, 10, field_values={'name': 'bar'}): 573 | self.assertEqual(obj.name, 'bar') 574 | obj = autofixture.create_one( 575 | SimpleModel, field_values={'name': 'bar'}) 576 | self.assertEqual(obj.name, 'bar') 577 | 578 | def test_registered_fixture_is_used_for_fk(self): 579 | class BasicModelFixture(AutoFixture): 580 | field_values={'chars': 'Hello World!'} 581 | 582 | autofixture.register(BasicModel, BasicModelFixture) 583 | 584 | fixture = AutoFixture(RelatedModel, generate_fk=['related']) 585 | obj = fixture.create_one() 586 | self.assertTrue(obj) 587 | self.assertEqual(obj.related.chars, 'Hello World!') 588 | 589 | def test_registered_fixture_is_used_for_m2m(self): 590 | class SimpleModelFixture(AutoFixture): 591 | field_values={'name': 'Jon Doe'} 592 | 593 | autofixture.register(SimpleModel, SimpleModelFixture) 594 | 595 | fixture = AutoFixture(M2MModel, generate_m2m={'m2m': (5,5)}) 596 | obj = fixture.create_one() 597 | self.assertTrue(obj) 598 | 599 | self.assertEqual(obj.m2m.count(), 5) 600 | self.assertEqual( 601 | list(obj.m2m.values_list('name', flat=True)), 602 | ['Jon Doe'] * 5) 603 | 604 | 605 | class TestAutofixtureAPI(FileSystemCleanupTestCase): 606 | def setUp(self): 607 | self.original_registry = autofixture.REGISTRY 608 | autofixture.REGISTRY = {} 609 | 610 | def tearDown(self): 611 | autofixture.REGISTRY = self.original_registry 612 | 613 | def test_values_class(self): 614 | autofixture.register(BasicModel, BasicValueFixture) 615 | for obj in autofixture.create(BasicModel, 10): 616 | self.assertEqual(obj.chars, 'foo') 617 | self.assertEqual(obj.shortchars, 'a') 618 | self.assertEqual(obj.blankchars, 'bar') 619 | self.assertEqual(obj.nullchars, 'spam') 620 | self.assertTrue(1 <= obj.intfield <= 13) 621 | 622 | 623 | class TestManagementCommand(FileSystemCleanupTestCase): 624 | def setUp(self): 625 | self.original_registry = autofixture.REGISTRY 626 | autofixture.REGISTRY = {} 627 | 628 | def call(self, *args, **kwargs): 629 | return call_command('loadtestdata', *args, verbosity=0, **kwargs) 630 | 631 | def tearDown(self): 632 | autofixture.REGISTRY = self.original_registry 633 | 634 | def test_basic(self): 635 | self.call('autofixture_tests.SimpleModel:1') 636 | self.assertEqual(SimpleModel.objects.count(), 1) 637 | 638 | self.call('autofixture_tests.SimpleModel:5') 639 | self.assertEqual(SimpleModel.objects.count(), 6) 640 | 641 | def test_generate_fk(self): 642 | self.call('autofixture_tests.DeepLinkModel2:1', 643 | generate_fk='related,related__related') 644 | obj = DeepLinkModel2.objects.get() 645 | self.assertTrue(obj.related) 646 | self.assertTrue(obj.related.related) 647 | self.assertEqual(obj.related.related2, obj.related.related) 648 | 649 | def test_generate_fk_with_no_follow(self): 650 | self.call('autofixture_tests.DeepLinkModel2:1', 651 | generate_fk='related,related__related', 652 | no_follow_fk=True) 653 | obj = DeepLinkModel2.objects.get() 654 | self.assertTrue(obj.related) 655 | self.assertTrue(obj.related.related) 656 | self.assertEqual(obj.related.related2, None) 657 | 658 | def test_generate_fk_with_ALL(self): 659 | self.call('autofixture_tests.DeepLinkModel2:1', 660 | generate_fk='ALL') 661 | obj = DeepLinkModel2.objects.get() 662 | self.assertTrue(obj.related) 663 | self.assertTrue(obj.related.related) 664 | self.assertTrue(obj.related.related2) 665 | self.assertTrue(obj.related.related != obj.related.related2) 666 | 667 | def test_no_follow_m2m(self): 668 | AutoFixture(SimpleModel).create(1) 669 | 670 | self.call('autofixture_tests.NullableFKModel:1', 671 | no_follow_m2m=True) 672 | obj = NullableFKModel.objects.get() 673 | self.assertEqual(obj.m2m.count(), 0) 674 | 675 | def test_follow_m2m(self): 676 | AutoFixture(SimpleModel).create(10) 677 | AutoFixture(OtherSimpleModel).create(10) 678 | 679 | self.call('autofixture_tests.M2MModel:25', 680 | follow_m2m='m2m:3:3,secondm2m:0:10') 681 | 682 | for obj in M2MModel.objects.all(): 683 | self.assertEqual(obj.m2m.count(), 3) 684 | self.assertTrue(0 <= obj.secondm2m.count() <= 10) 685 | 686 | def test_generate_m2m(self): 687 | self.call('autofixture_tests.M2MModel:10', 688 | generate_m2m='m2m:1:1,secondm2m:2:5') 689 | 690 | all_m2m, all_secondm2m = set(), set() 691 | for obj in M2MModel.objects.all(): 692 | self.assertEqual(obj.m2m.count(), 1) 693 | self.assertTrue( 694 | 2 <= obj.secondm2m.count() <= 5 or 695 | obj.secondm2m.count() == 0) 696 | all_m2m.update(obj.m2m.all()) 697 | all_secondm2m.update(obj.secondm2m.all()) 698 | self.assertEqual(all_m2m, set(SimpleModel.objects.all())) 699 | self.assertEqual(all_secondm2m, set(OtherSimpleModel.objects.all())) 700 | 701 | def test_using_registry(self): 702 | autofixture.register(SimpleModel, SimpleAutoFixture) 703 | self.call('autofixture_tests.SimpleModel:10') 704 | for obj in SimpleModel.objects.all(): 705 | self.assertEqual(obj.name, 'foo') 706 | 707 | def test_use_option(self): 708 | self.call('autofixture_tests.SimpleModel:10', 709 | use='autofixture_tests.tests.test_base.SimpleAutoFixture') 710 | for obj in SimpleModel.objects.all(): 711 | self.assertEqual(obj.name, 'foo') 712 | 713 | 714 | class TestGenericRelations(FileSystemCleanupTestCase): 715 | def assertNotRaises(self, exc_type, func, msg=None, 716 | args=None, kwargs=None): 717 | args = args or [] 718 | kwargs = kwargs or {} 719 | try: 720 | func(*args, **kwargs) 721 | except exc_type as exc: 722 | if msg is not None and exc.message != msg: 723 | return 724 | self.fail('{} failed with {}'.format(func, exc)) 725 | 726 | def test_process_gr(self): 727 | """Tests the bug when GenericRelation field being processed 728 | by autofixture.base.AutoFixtureBase#process_m2m 729 | and through table appears as None. 730 | """ 731 | count = 10 732 | fixture = AutoFixture(GRModel) 733 | self.assertNotRaises(AttributeError, fixture.create, 734 | msg="'NoneType' object has no attribute '_meta'", args=[count]) 735 | self.assertEqual(GRModel.objects.count(), count) 736 | 737 | 738 | class TestShortcuts(FileSystemCleanupTestCase): 739 | def test_commit_kwarg(self): 740 | instances = autofixture.create(BasicModel, 3, commit=False) 741 | self.assertEqual([i.pk for i in instances], [None] * 3) 742 | 743 | instance = autofixture.create_one(BasicModel, commit=False) 744 | self.assertEqual(instance.pk, None) 745 | 746 | 747 | class TestPreProcess(FileSystemCleanupTestCase): 748 | def test_pre_process_instance_not_yet_saved(self): 749 | self_ = self 750 | class TestAutoFixture(AutoFixture): 751 | def pre_process_instance(self, instance): 752 | self_.assertIsNone(instance.pk) 753 | return instance 754 | 755 | TestAutoFixture(BasicModel).create_one() 756 | 757 | self.assertEqual(BasicModel.objects.count(), 1) 758 | 759 | def test_pre_process_has_effect(self): 760 | expected_string = generators.LoremGenerator(max_length=50)() 761 | 762 | class TestAutoFixture(AutoFixture): 763 | def pre_process_instance(self, instance): 764 | instance.name = expected_string 765 | return instance 766 | 767 | instance = TestAutoFixture(SimpleModel).create_one() 768 | self.assertEqual(instance.name, expected_string) 769 | 770 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_generator.py: -------------------------------------------------------------------------------- 1 | from autofixture_tests.models import ImageModel, dummy_storage 2 | import os 3 | from operator import truediv 4 | 5 | from django import forms 6 | from django.conf import settings 7 | from django.utils import timezone 8 | from django.test.utils import override_settings 9 | from PIL import Image 10 | 11 | from autofixture import generators, AutoFixture 12 | from . import FileSystemCleanupTestCase 13 | 14 | 15 | class FilePathTests(FileSystemCleanupTestCase): 16 | def test_media_path_generator(self): 17 | generate = generators.MediaFilePathGenerator(recursive=True) 18 | for i in range(10): 19 | path = generate() 20 | self.assertTrue(len(path) > 0) 21 | self.assertFalse(path.startswith('/')) 22 | media_path = os.path.join(settings.MEDIA_ROOT, path) 23 | self.assertTrue(os.path.exists(media_path)) 24 | self.assertTrue(os.path.isfile(media_path)) 25 | 26 | def test_media_path_generator_in_subdirectory(self): 27 | generate = generators.MediaFilePathGenerator(path='textfiles') 28 | for i in range(10): 29 | path = generate() 30 | self.assertTrue(path.startswith('textfiles/')) 31 | self.assertTrue(path.endswith('.txt')) 32 | 33 | 34 | class DateTimeTests(FileSystemCleanupTestCase): 35 | @override_settings(USE_TZ=True) 36 | def test_is_datetime_timezone_aware(self): 37 | generate = generators.DateTimeGenerator() 38 | date_time = generate() 39 | self.assertTrue(timezone.is_aware(date_time)) 40 | 41 | @override_settings(USE_TZ=False) 42 | def test_is_datetime_timezone_not_aware(self): 43 | generate = generators.DateTimeGenerator() 44 | date_time = generate() 45 | self.assertFalse(timezone.is_aware(date_time)) 46 | 47 | 48 | class EmailForm(forms.Form): 49 | email = forms.EmailField() 50 | 51 | 52 | class EmailGeneratorTests(FileSystemCleanupTestCase): 53 | def test_email(self): 54 | generate = generators.EmailGenerator() 55 | form = EmailForm({'email': generate()}) 56 | self.assertTrue(form.is_valid()) 57 | 58 | def test_email_with_static_domain(self): 59 | generate = generators.EmailGenerator(static_domain='djangoproject.com') 60 | email = generate() 61 | self.assertTrue(email.endswith('djangoproject.com')) 62 | email = generate() 63 | self.assertTrue(email.endswith('djangoproject.com')) 64 | 65 | 66 | class WeightedGeneratorTests(FileSystemCleanupTestCase): 67 | def test_simple_weights(self): 68 | results = {"Red": 0, "Blue": 0} 69 | choices = [(generators.StaticGenerator("Red"), 50), 70 | (generators.StaticGenerator("Blue"), 50)] 71 | generate = generators.WeightedGenerator(choices) 72 | 73 | runs = 10000 74 | 75 | for i in range(runs): 76 | results[generate()] += 1 77 | 78 | MARGIN = 0.025 79 | 80 | self.assertTrue(0.5 - MARGIN < truediv(results["Red"], runs) < 0.5 + MARGIN) 81 | self.assertTrue(0.5 - MARGIN < truediv(results["Blue"], runs) < 0.5 + MARGIN) 82 | 83 | def test_complex_weights(self): 84 | results = {"frosh": 0, "soph": 0, "jr": 0, "sr": 0} 85 | choices = [(generators.StaticGenerator("frosh"), 35), 86 | (generators.StaticGenerator("soph"), 20), 87 | (generators.StaticGenerator("jr"), 30), 88 | (generators.StaticGenerator("sr"), 15)] 89 | generate = generators.WeightedGenerator(choices) 90 | 91 | runs = 10000 92 | 93 | for i in range(runs): 94 | results[generate()] += 1 95 | 96 | MARGIN = 0.025 97 | 98 | self.assertTrue(0.35 - MARGIN < truediv(results["frosh"], runs) < 0.35 + MARGIN, results["frosh"] / 1.0 * runs) 99 | self.assertTrue(0.20 - MARGIN < truediv(results["soph"], runs) < 0.20 + MARGIN) 100 | self.assertTrue(0.30 - MARGIN < truediv(results["jr"], runs) < 0.30 + MARGIN) 101 | self.assertTrue(0.15 - MARGIN < truediv(results["sr"], runs) < 0.15 + MARGIN) 102 | 103 | 104 | class ImageGeneratorTests(FileSystemCleanupTestCase): 105 | def test_image_generator(self): 106 | generate = generators.ImageGenerator() 107 | media_file = generate() 108 | 109 | file_path = os.path.join(settings.MEDIA_ROOT, media_file) 110 | self.assertTrue(os.path.exists(file_path)) 111 | 112 | with open(file_path, 'rb') as f: 113 | image = Image.open(f) 114 | self.assertTrue(image.size in generators.ImageGenerator.default_sizes) 115 | 116 | def test_width_height(self): 117 | media_file = generators.ImageGenerator(125, 225).generate() 118 | file_path = os.path.join(settings.MEDIA_ROOT, media_file) 119 | self.assertTrue(os.path.exists(file_path)) 120 | 121 | with open(file_path, 'rb') as f: 122 | image = Image.open(f) 123 | self.assertTrue(image.size, (125, 225)) 124 | 125 | def test_filenames_dont_clash(self): 126 | media_file = generators.ImageGenerator(100, 100).generate() 127 | file_path1 = os.path.join(settings.MEDIA_ROOT, media_file) 128 | self.assertTrue(os.path.exists(file_path1)) 129 | 130 | media_file = generators.ImageGenerator(100, 100).generate() 131 | file_path2 = os.path.join(settings.MEDIA_ROOT, media_file) 132 | self.assertTrue(os.path.exists(file_path2)) 133 | 134 | self.assertNotEqual(file_path1, file_path2) 135 | 136 | def test_path(self): 137 | self.cleanup_dirs.append('mycustompath/withdirs') 138 | 139 | media_file = generators.ImageGenerator(path='mycustompath/withdirs').generate() 140 | file_path = os.path.join(settings.MEDIA_ROOT, media_file) 141 | self.assertTrue(os.path.exists(file_path)) 142 | 143 | self.assertTrue(media_file.startswith('mycustompath/withdirs/')) 144 | self.assertTrue('_autofixture' not in media_file) 145 | 146 | def test_storage(self): 147 | """Storage is handled properly if defined on a field""" 148 | o = AutoFixture(ImageModel).create_one() 149 | 150 | self.assertTrue(dummy_storage.exists(o.imgfield.name)) 151 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_geodjango.py: -------------------------------------------------------------------------------- 1 | from django.contrib.gis.geos import Point 2 | from django.test.utils import override_settings 3 | 4 | from autofixture import generators 5 | from . import FileSystemCleanupTestCase 6 | 7 | 8 | class GeoDjangoPointTests(FileSystemCleanupTestCase): 9 | 10 | @override_settings(INSTALLED_APPS=( 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.sites', 15 | 'django.contrib.admin', 16 | 'django.contrib.gis', 17 | 18 | 'autofixture', 19 | 'autofixture_tests', 20 | 'autofixture_tests.tests', 21 | 'autofixture_tests.sample_app', 22 | )) 23 | def test_point(self): 24 | 25 | point = generators.PointFieldGenerator().generate() 26 | self.assertIsInstance(point, Point) 27 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_user_fixture.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.hashers import is_password_usable 2 | from django.contrib.auth.models import User 3 | from django.test import TestCase 4 | import autofixture 5 | 6 | 7 | autofixture.autodiscover() 8 | 9 | 10 | class UserFixtureTest(TestCase): 11 | def test_discover(self): 12 | self.assertTrue(User in autofixture.REGISTRY) 13 | 14 | def test_basic(self): 15 | user = autofixture.create_one(User) 16 | self.assertTrue(user.username) 17 | self.assertTrue(len(user.username) <= 30) 18 | self.assertFalse(is_password_usable(user.password)) 19 | 20 | def test_password_setting(self): 21 | user = autofixture.create_one(User, password='known') 22 | self.assertTrue(user.username) 23 | self.assertTrue(is_password_usable(user.password)) 24 | self.assertTrue(user.check_password('known')) 25 | 26 | loaded_user = User.objects.get() 27 | self.assertEqual(loaded_user.password, user.password) 28 | self.assertTrue(loaded_user.check_password('known')) 29 | 30 | # def test_commit(self): 31 | # user = autofixture.create_one(User, commit=False) 32 | # self.assertEqual(user.pk, None) 33 | -------------------------------------------------------------------------------- /autofixture_tests/tests/test_values.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from autofixture.values import Values 3 | 4 | 5 | class ValuesTests(TestCase): 6 | def test_init(self): 7 | values = Values({'a': 1, 'c': 4}, a=2, b=3) 8 | self.assertEqual(values['a'], 2) 9 | self.assertEqual(values['b'], 3) 10 | self.assertEqual(values['c'], 4) 11 | 12 | values = Values(values) 13 | self.assertEqual(values['a'], 2) 14 | self.assertEqual(values['b'], 3) 15 | self.assertEqual(values['c'], 4) 16 | 17 | class Data: 18 | a = 1 19 | b = 3 20 | _c = 4 21 | values = Values(Data, a=2) 22 | self.assertEqual(values['a'], 2) 23 | self.assertEqual(values['b'], 3) 24 | self.assertEqual(values['_c'], 4) 25 | 26 | values = Values(Data(), a=2) 27 | self.assertEqual(values['a'], 2) 28 | self.assertEqual(values['b'], 3) 29 | self.assertEqual(values['_c'], 4) 30 | 31 | values = Values(Values(a=1, c=4), a=2, b=3) 32 | self.assertEqual(values['a'], 2) 33 | self.assertEqual(values['b'], 3) 34 | self.assertEqual(values['c'], 4) 35 | 36 | def test_add_operation(self): 37 | values = Values(a=1, b=3) 38 | values = values + {'a': 2, 'c': 4} 39 | self.assertEqual(values['a'], 2) 40 | self.assertEqual(values['b'], 3) 41 | self.assertEqual(values['c'], 4) 42 | 43 | def test_radd_operation(self): 44 | values = Values(a=1, b=3) 45 | values = {'a': 2, 'c': 4} + values 46 | self.assertEqual(values['a'], 1) 47 | self.assertEqual(values['b'], 3) 48 | self.assertEqual(values['c'], 4) 49 | 50 | def test_iadd_operation(self): 51 | values = Values(a=1, b=3) 52 | values += {'a': 2, 'c': 4} 53 | self.assertEqual(values['a'], 2) 54 | self.assertEqual(values['b'], 3) 55 | self.assertEqual(values['c'], 4) 56 | 57 | def test_subclassing(self): 58 | class AB(Values): 59 | a = 1 60 | b = 2 61 | 62 | values = AB() 63 | self.assertEqual(values['a'], 1) 64 | self.assertEqual(values['b'], 2) 65 | self.assertRaises(KeyError, values.__getitem__, 'c') 66 | self.assertRaises(AttributeError, getattr, values, 'a') 67 | self.assertRaises(AttributeError, getattr, values, 'b') 68 | self.assertRaises(AttributeError, getattr, values, 'c') 69 | 70 | values = AB(b=3, c=4) 71 | self.assertEqual(values['a'], 1) 72 | self.assertEqual(values['b'], 3) 73 | self.assertEqual(values['c'], 4) 74 | 75 | values += {'a': 2} 76 | self.assertEqual(values['a'], 2) 77 | self.assertEqual(values['b'], 3) 78 | self.assertEqual(values['c'], 4) 79 | 80 | def test_sub_subclassing(self): 81 | class AB(Values): 82 | a = 1 83 | b = 2 84 | 85 | class ABCD(AB): 86 | c = 3 87 | d = 4 88 | 89 | values = ABCD(a=2, c=4) 90 | self.assertEqual(values['a'], 2) 91 | self.assertEqual(values['b'], 2) 92 | self.assertEqual(values['c'], 4) 93 | self.assertEqual(values['d'], 4) 94 | 95 | def test_multiple_inheritance(self): 96 | class A(Values): 97 | a = 1 98 | 99 | class AB(Values): 100 | a = 2 101 | b = 3 102 | 103 | class ABC(A, AB): 104 | c = 4 105 | 106 | values = ABC() 107 | self.assertEqual(values['a'], 1) 108 | self.assertEqual(values['b'], 3) 109 | self.assertEqual(values['c'], 4) 110 | -------------------------------------------------------------------------------- /autofixture_tests/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.http import HttpResponse 3 | 4 | 5 | def handle404(request): 6 | return HttpResponse('404') 7 | 8 | 9 | def handle500(request): 10 | return HttpResponse('500') 11 | 12 | handler404 = 'autofixture_tests.urls.handle404' 13 | handler500 = 'autofixture_tests.urls.handle500' 14 | 15 | 16 | urlpatterns = [ 17 | ] 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-autofixture.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-autofixture.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /docs/builtin_autofixtures.rst: -------------------------------------------------------------------------------- 1 | Builtin :class:`AutoFixture` subclasses 2 | ======================================= 3 | 4 | There are some :class:`AutoFixture` subclasses that are shipped by default 5 | with ``django-autofixture``. They are listed below. 6 | 7 | .. _UserFixture: 8 | .. autoclass:: autofixture.autofixtures.UserFixture 9 | :members: __init__ 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-autofixture documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Mar 8 22:17:43 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import re 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.append(os.path.abspath('.')) 21 | 22 | PROJECT_PATH = os.path.dirname( 23 | os.path.dirname(os.path.abspath(__file__))) 24 | 25 | sys.path.insert(0, PROJECT_PATH) 26 | 27 | from django.conf import settings 28 | settings.configure(DATABASES={ 29 | 'default': { 30 | 'ENGINE': 'django.db.backends.sqlite3', 31 | 'NAME': ':memory:', 32 | }, 33 | }) 34 | 35 | # -- General configuration ----------------------------------------------------- 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be extensions 38 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The suffix of source filenames. 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = u'django-autofixture' 57 | copyright = u'2013, Gregor Müllegger' 58 | 59 | def get_release(package): 60 | """ 61 | Return package version as listed in `__version__` in `init.py`. 62 | """ 63 | init_path = os.path.join(PROJECT_PATH, package, '__init__.py') 64 | init_py = open(init_path).read() 65 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 66 | 67 | # The full version, including alpha/beta/rc tags. 68 | release = get_release('autofixture') 69 | 70 | # The version info for the project you're documenting, acts as replacement for 71 | # |version| and |release|, also used in various other places throughout the 72 | # built documents. 73 | # 74 | # The short X.Y version. 75 | version = '.'.join(release.split('.')[:2]) 76 | 77 | # The language for content autogenerated by Sphinx. Refer to documentation 78 | # for a list of supported languages. 79 | #language = None 80 | 81 | # There are two options for replacing |today|: either, you set today to some 82 | # non-false value, then it is used: 83 | #today = '' 84 | # Else, today_fmt is used as the format for a strftime call. 85 | #today_fmt = '%B %d, %Y' 86 | 87 | # List of documents that shouldn't be included in the build. 88 | #unused_docs = [] 89 | 90 | # List of directories, relative to source directory, that shouldn't be searched 91 | # for source files. 92 | exclude_trees = ['_build'] 93 | 94 | # The reST default role (used for this markup: `text`) to use for all documents. 95 | #default_role = None 96 | 97 | # If true, '()' will be appended to :func: etc. cross-reference text. 98 | #add_function_parentheses = True 99 | 100 | # If true, the current module name will be prepended to all description 101 | # unit titles (such as .. function::). 102 | #add_module_names = True 103 | 104 | # If true, sectionauthor and moduleauthor directives will be shown in the 105 | # output. They are ignored by default. 106 | #show_authors = False 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # A list of ignored prefixes for module index sorting. 112 | #modindex_common_prefix = [] 113 | 114 | 115 | # -- Options for HTML output --------------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. Major themes that come with 118 | # Sphinx are currently 'default' and 'sphinxdoc'. 119 | #html_theme = 'alabaster' 120 | 121 | # Theme options are theme-specific and customize the look and feel of a theme 122 | # further. For a list of options available for each theme, see the 123 | # documentation. 124 | #html_theme_options = {} 125 | 126 | # Add any paths that contain custom themes here, relative to this directory. 127 | #html_theme_path = [] 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | #html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as html_title. 134 | #html_short_title = None 135 | 136 | # The name of an image file (relative to this directory) to place at the top 137 | # of the sidebar. 138 | #html_logo = None 139 | 140 | # The name of an image file (within the static path) to use as favicon of the 141 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 142 | # pixels large. 143 | #html_favicon = None 144 | 145 | # Add any paths that contain custom static files (such as style sheets) here, 146 | # relative to this directory. They are copied after the builtin static files, 147 | # so a file named "default.css" will overwrite the builtin "default.css". 148 | #html_static_path = ['_static'] 149 | 150 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 151 | # using the given strftime format. 152 | #html_last_updated_fmt = '%b %d, %Y' 153 | 154 | # If true, SmartyPants will be used to convert quotes and dashes to 155 | # typographically correct entities. 156 | #html_use_smartypants = True 157 | 158 | # Custom sidebar templates, maps document names to template names. 159 | #html_sidebars = {} 160 | 161 | # Additional templates that should be rendered to pages, maps page names to 162 | # template names. 163 | #html_additional_pages = {} 164 | 165 | # If false, no module index is generated. 166 | #html_use_modindex = True 167 | 168 | # If false, no index is generated. 169 | #html_use_index = True 170 | 171 | # If true, the index is split into individual pages for each letter. 172 | #html_split_index = False 173 | 174 | # If true, links to the reST sources are added to the pages. 175 | #html_show_sourcelink = True 176 | 177 | # If true, an OpenSearch description file will be output, and all pages will 178 | # contain a tag referring to it. The value of this option must be the 179 | # base URL from which the finished HTML is served. 180 | #html_use_opensearch = '' 181 | 182 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 183 | #html_file_suffix = '' 184 | 185 | # Output file base name for HTML help builder. 186 | htmlhelp_basename = 'django-autofixturedoc' 187 | 188 | 189 | # -- Options for LaTeX output -------------------------------------------------- 190 | 191 | # The paper size ('letter' or 'a4'). 192 | #latex_paper_size = 'letter' 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #latex_font_size = '10pt' 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, author, documentclass [howto/manual]). 199 | latex_documents = [ 200 | ('index', 'django-autofixture.tex', u'django-autofixture Documentation', 201 | u'Gregor Müllegger', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # Additional stuff for the LaTeX preamble. 213 | #latex_preamble = '' 214 | 215 | # Documents to append as an appendix to all manuals. 216 | #latex_appendices = [] 217 | 218 | # If false, no module index is generated. 219 | #latex_use_modindex = True 220 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | If you want to use an isolated environment while hacking on 5 | ``django-autofixture`` you can run the following commands from the project's 6 | root directory:: 7 | 8 | virtualenv . --no-site-packages 9 | source bin/activate 10 | pip install -r requirements/tests.txt 11 | 12 | Please run now the tests that are shipped with ``autofixture`` to see if 13 | everything is working:: 14 | 15 | python runtests.py 16 | 17 | Happy hacking! 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to django-autofixture's documentation! 2 | ============================================== 3 | 4 | This app aims to provide a simple way of loading masses of randomly generated 5 | test data into your development database. You can use a :ref:`management 6 | command ` to load test data through command line. 7 | 8 | It is named *autofixture* because of the similarity of how I mainly used 9 | django's fixtures. Usually you add test data through the admin to see how your 10 | site looks with non static pages. You export data by using ``dumpdata`` to 11 | send it to your colleagues or to preserve it before you make a ``manage.py 12 | reset app`` and so on. Your site gets more and more complex and adding test 13 | data gets more and more annoying. 14 | 15 | This is the usecase where autofixtures should help you to save time that can 16 | actually be spent on hacking. 17 | 18 | .. _contents: 19 | 20 | Contents 21 | ======== 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | installation 27 | loadtestdata 28 | usage 29 | builtin_autofixtures 30 | contribute 31 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Download and install with ``pip`` or ``easy_install`` 5 | ----------------------------------------------------- 6 | 7 | You can install the ``django-autofixture`` like any other python package. The 8 | prefered way is to use `pip `_. Please run the 9 | following command in your terminal:: 10 | 11 | pip install django-autofixture 12 | 13 | This will install the package in your system wide python installation. 14 | 15 | You can fall back to the :command:`easy_install` command if :command:`pip` is 16 | not available on your system:: 17 | 18 | easy_install django-autofixture 19 | 20 | .. note:: In most cases you need admin previlegies to install a package into 21 | your system. You can get these previlegies by prefixing the commands above 22 | with ``sudo``. 23 | 24 | .. _INSTALLED_APPS: 25 | 26 | Add ``autofixture`` to your django project 27 | ------------------------------------------ 28 | 29 | Usually you want to add ``autofixture`` to your ``INSTALLED_APPS`` in the 30 | settings file of your django project. This will make the :ref:`loadtestdata 31 | ` management command available to your use. 32 | 33 | Using the development version 34 | ----------------------------- 35 | 36 | You can ofcourse also install and use the current development version. All you 37 | need is to have the `git `_ and `setuptools 38 | `_ installed. 39 | 40 | Now get the repository from `github 41 | `_ and run:: 42 | 43 | git clone git://github.com/gregmuellegger/django-autofixture.git 44 | 45 | This will download the project into your local directory. :command:`cd` to the 46 | ``django-autofixture`` directory and run:: 47 | 48 | python setup.py install 49 | 50 | Now follow the instructions under :ref:`INSTALLED_APPS` and everything will be 51 | in place to use ``django-autofixture``. 52 | -------------------------------------------------------------------------------- /docs/loadtestdata.rst: -------------------------------------------------------------------------------- 1 | .. _loadtestdata: 2 | 3 | The ``loadtestdata`` management command 4 | ======================================= 5 | 6 | .. automodule:: autofixture.management.commands.loadtestdata 7 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=_build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 24 | echo. changes to make an overview over all changed/added/deprecated items 25 | echo. linkcheck to check all external links for integrity 26 | echo. doctest to run all doctests embedded in the documentation if enabled 27 | goto end 28 | ) 29 | 30 | if "%1" == "clean" ( 31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 32 | del /q /s %BUILDDIR%\* 33 | goto end 34 | ) 35 | 36 | if "%1" == "html" ( 37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 38 | echo. 39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 40 | goto end 41 | ) 42 | 43 | if "%1" == "dirhtml" ( 44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 47 | goto end 48 | ) 49 | 50 | if "%1" == "pickle" ( 51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 52 | echo. 53 | echo.Build finished; now you can process the pickle files. 54 | goto end 55 | ) 56 | 57 | if "%1" == "json" ( 58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 59 | echo. 60 | echo.Build finished; now you can process the JSON files. 61 | goto end 62 | ) 63 | 64 | if "%1" == "htmlhelp" ( 65 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 66 | echo. 67 | echo.Build finished; now you can run HTML Help Workshop with the ^ 68 | .hhp project file in %BUILDDIR%/htmlhelp. 69 | goto end 70 | ) 71 | 72 | if "%1" == "qthelp" ( 73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 74 | echo. 75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 76 | .qhcp project file in %BUILDDIR%/qthelp, like this: 77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-autofixture.qhcp 78 | echo.To view the help file: 79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-autofixture.ghc 80 | goto end 81 | ) 82 | 83 | if "%1" == "latex" ( 84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 85 | echo. 86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 87 | goto end 88 | ) 89 | 90 | if "%1" == "changes" ( 91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 92 | echo. 93 | echo.The overview file is in %BUILDDIR%/changes. 94 | goto end 95 | ) 96 | 97 | if "%1" == "linkcheck" ( 98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 99 | echo. 100 | echo.Link check complete; look for any errors in the above output ^ 101 | or in %BUILDDIR%/linkcheck/output.txt. 102 | goto end 103 | ) 104 | 105 | if "%1" == "doctest" ( 106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 107 | echo. 108 | echo.Testing of doctests in the sources finished, look at the ^ 109 | results in %BUILDDIR%/doctest/output.txt. 110 | goto end 111 | ) 112 | 113 | :end 114 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | -r tests.txt 3 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Howto use the library 4 | ===================== 5 | 6 | Its easy to get started with the :doc:`loadtestdata management command 7 | ` but its quite limited if you want to have more control of how 8 | your test data should be created. This chapter describes how you use the 9 | library in your python environment like the shell, a custom script or in 10 | unittests. 11 | 12 | Creating model instances 13 | ------------------------ 14 | 15 | The :mod:`autofixture` module contains a few shortcuts to make the creation of 16 | test data as fast as possible. 17 | 18 | .. _shortcuts: 19 | 20 | .. autofunction:: autofixture.create 21 | 22 | .. autofunction:: autofixture.create_one 23 | 24 | .. _AutoFixture: 25 | 26 | Using the :class:`~autofixture.base.AutoFixture` class 27 | ------------------------------------------------------ 28 | 29 | .. autoclass:: autofixture.base.AutoFixture 30 | :members: __init__, add_field_value, add_constraint, 31 | create, create_one 32 | 33 | The :class:`~autofixture.base.AutoFixture` registry 34 | --------------------------------------------------- 35 | 36 | .. _registry: 37 | 38 | Since :class:`~autofixture.base.AutoFixture` is designed to fit for almost all 39 | models, its very generic and doesn't know anything about the actual logic and 40 | meanings of relations or the purpose of your model fields. This makes it 41 | sometimes a bit difficult to provide the correct ``field_values`` in all 42 | places where you want ``autofixture`` to instanciate your models. 43 | 44 | So there is a registry to register custom 45 | :class:`~autofixture.base.AutoFixture` subclasses with specific models. These 46 | subclasses are then used by default if you generate test data either with the 47 | :ref:`loadtestdata ` management command or with one of the 48 | :ref:`shortcuts ` in :mod:`autofixture`. 49 | 50 | .. autofunction:: autofixture.register 51 | 52 | .. autofunction:: autofixture.unregister 53 | 54 | .. autofunction:: autofixture.get 55 | 56 | Subclassing :class:`AutoFixture` 57 | -------------------------------- 58 | 59 | .. _subclassing: 60 | .. _values: 61 | 62 | In most cases it will by sufficient to provide a different logic to generate 63 | the values for your model fields in :class:`~autofixture.base.AutoFixture` 64 | subclasses. This can be simply done by a nested ``Values`` class:: 65 | 66 | class EntryFixture(AutoFixture): 67 | class Values: 68 | title = 'My static title' 69 | status = staticmethod(lambda: random.choice((1,2))) 70 | pub_date = generators.DateTimeGenerator( 71 | min_date=datetime(2009,1,1), 72 | max_date=datetime(2009,12,31)) 73 | 74 | This will make sure that ``title`` is always ``'My static title'``, status is 75 | either ``1`` or ``2`` and that ``pub_date`` is in the somewhere in 2009. 76 | 77 | Like you can see in the example you can apply static values, simple callables 78 | or specific generators to specific fields. However remember to use the 79 | ``staticmethod`` decorator when using a ``method`` as callable - like the 80 | ``lambda`` statement in the example. It's in fact also just a shorter 81 | definition of a method. 82 | 83 | *A note on subclassing subclasses and turtles all the way down:* Sometimes 84 | it's usefull for a project to have a common base class for all the registered 85 | *AutoFixtures*. This is possible and easy since you don't need 86 | to re-define all the field definitions in the nested ``Values`` class. The 87 | :class:`~autofixture.base.AutoFixture` class cares about this and will 88 | collect all ``Values`` of base classes and merge them together. For 89 | clarification here a short example:: 90 | 91 | class CommonFixture(AutoFixture): 92 | class Values: 93 | tags = generators.ChoicesGenerator( 94 | values=('apple', 'banana', 'orange')) 95 | 96 | class EntryFixture(AutoFixture): 97 | class Values: 98 | title = 'My static title' 99 | 100 | # all created entries will have the same title 'My static title' and one 101 | # tag out of apple, banana and orange. 102 | EntryFixture(Entry).create(5) 103 | 104 | If you want to digg deeper and need to customize more logic of model creation, 105 | you can override some handy methods of the 106 | :class:`~autofixture.base.AutoFixture` class: 107 | 108 | .. automethod:: autofixture.base.AutoFixture.prepare_class 109 | 110 | .. automethod:: autofixture.base.AutoFixture.post_process_instance 111 | 112 | .. automethod:: autofixture.base.AutoFixture.get_generator 113 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from fabric.api import abort, cd, local, env, run, settings, sudo 4 | 5 | 6 | ################# 7 | # Documentation # 8 | ################# 9 | 10 | def packagedocs(): 11 | builddocs('html') 12 | try: 13 | os.mkdir('dist') 14 | except OSError: 15 | pass 16 | with cd('docs/_build/html'): 17 | local('find -print | zip docs.zip -@') 18 | local('mv docs/_build/html/docs.zip dist') 19 | 20 | def builddocs(output='html'): 21 | with cd('docs'): 22 | local('make %s' % output, capture=False) 23 | 24 | def opendocs(where='index', how='default'): 25 | ''' 26 | Rebuild documentation and opens it in your browser. 27 | 28 | Use the first argument to specify how it should be opened: 29 | 30 | `d` or `default`: Open in new tab or new window, using the default 31 | method of your browser. 32 | 33 | `t` or `tab`: Open documentation in new tab. 34 | 35 | `n`, `w` or `window`: Open documentation in new window. 36 | ''' 37 | import webbrowser 38 | docs_dir = os.path.join( 39 | os.path.dirname(os.path.abspath(__file__)), 40 | 'docs') 41 | index = os.path.join(docs_dir, '_build/html/%s.html' % where) 42 | builddocs('html') 43 | url = 'file://%s' % os.path.abspath(index) 44 | if how in ('d', 'default'): 45 | webbrowser.open(url) 46 | elif how in ('t', 'tab'): 47 | webbrowser.open_new_tab(url) 48 | elif how in ('n', 'w', 'window'): 49 | webbrowser.open_new(url) 50 | 51 | docs = opendocs 52 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "autofixture_tests.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | coverage 3 | django-discover-runner 4 | Pillow 5 | tox 6 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import os, sys 4 | 5 | 6 | os.environ['DJANGO_SETTINGS_MODULE'] = 'autofixture_tests.settings' 7 | 8 | 9 | # Adding current directory to ``sys.path``. 10 | parent = os.path.dirname(os.path.abspath(__file__)) 11 | sys.path.insert(0, parent) 12 | 13 | 14 | def runtests(*argv): 15 | argv = list(argv) or [ 16 | 'autofixture', 17 | 'autofixture_tests', 18 | ] 19 | opts = argparser.parse_args(argv) 20 | 21 | if opts.coverage: 22 | from coverage import coverage 23 | test_coverage = coverage( 24 | branch=True, 25 | source=['autofixture']) 26 | test_coverage.start() 27 | 28 | # Run tests. 29 | from django.core.management import execute_from_command_line 30 | execute_from_command_line([sys.argv[0], 'test'] + opts.appname) 31 | 32 | if opts.coverage: 33 | test_coverage.stop() 34 | 35 | # Report coverage to commandline. 36 | test_coverage.report(file=sys.stdout) 37 | 38 | 39 | argparser = argparse.ArgumentParser(description='Process some integers.') 40 | argparser.add_argument('appname', nargs='*') 41 | argparser.add_argument('--no-coverage', dest='coverage', action='store_const', 42 | const=False, default=True, help='Do not collect coverage data.') 43 | 44 | 45 | if __name__ == '__main__': 46 | runtests(*sys.argv[1:]) 47 | 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import os 5 | from setuptools import setup 6 | 7 | 8 | def get_version(package): 9 | """ 10 | Return package version as listed in `__version__` in `init.py`. 11 | """ 12 | init_py = open(os.path.join(package, '__init__.py')).read() 13 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 14 | 15 | 16 | class UltraMagicString(object): 17 | ''' 18 | Taken from 19 | http://stackoverflow.com/questions/1162338/whats-the-right-way-to-use-unicode-metadata-in-setup-py 20 | ''' 21 | def __init__(self, value): 22 | self.value = value 23 | 24 | def __str__(self): 25 | return self.value 26 | 27 | def __unicode__(self): 28 | return self.value.decode('UTF-8') 29 | 30 | def __add__(self, other): 31 | return UltraMagicString(self.value + str(other)) 32 | 33 | def split(self, *args, **kw): 34 | return self.value.split(*args, **kw) 35 | 36 | 37 | long_description = UltraMagicString(u'\n\n'.join(( 38 | open('README.rst').read(), 39 | open('CHANGES.rst').read(), 40 | ))) 41 | 42 | 43 | setup( 44 | name = 'django-autofixture', 45 | version = get_version('autofixture'), 46 | url = 'https://github.com/gregmuellegger/django-autofixture', 47 | license = 'BSD', 48 | description = 'Provides tools to auto generate test data.', 49 | long_description = long_description, 50 | author = UltraMagicString('Gregor Müllegger'), 51 | author_email = 'gregor@muellegger.de', 52 | classifiers = [ 53 | 'Development Status :: 4 - Beta', 54 | 'Environment :: Web Environment', 55 | 'Framework :: Django', 56 | 'Framework :: Django :: 1.4', 57 | 'Framework :: Django :: 1.5', 58 | 'Framework :: Django :: 1.6', 59 | 'Framework :: Django :: 1.7', 60 | 'Framework :: Django :: 1.8', 61 | 'Framework :: Django :: 1.9', 62 | 'Intended Audience :: Developers', 63 | 'License :: OSI Approved :: BSD License', 64 | 'Natural Language :: English', 65 | 'Operating System :: OS Independent', 66 | 'Programming Language :: Python', 67 | 'Programming Language :: Python :: 2.6', 68 | 'Programming Language :: Python :: 2.7', 69 | 'Programming Language :: Python :: 3.3', 70 | 'Programming Language :: Python :: 3.4', 71 | 'Programming Language :: Python :: 3.5', 72 | ], 73 | packages = [ 74 | 'autofixture', 75 | 'autofixture.management', 76 | 'autofixture.management.commands'], 77 | install_requires = ['setuptools'], 78 | test_suite = 'runtests.runtests', 79 | ) 80 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.8 3 | envlist = 4 | docs, 5 | py27-{14,15,16,17,18,19,master}, 6 | py33-{15,16,17,18}, 7 | py34-{15,16,17,18,master}, 8 | py35-{18,19,master}, 9 | pypy-{14,15,16,17,18,19,master} 10 | 11 | [testenv] 12 | commands = python runtests.py 13 | deps = 14 | 14: Django >= 1.4, < 1.5 15 | 15: Django >= 1.5, < 1.6 16 | 16: Django >= 1.6, < 1.7 17 | 17: Django >= 1.7, < 1.8 18 | 18: Django >= 1.8, < 1.9 19 | 19: Django >= 1.9, < 1.10 20 | master: https://github.com/django/django/archive/master.tar.gz 21 | -r{toxinidir}/requirements/tests.txt 22 | 23 | [testenv:docs] 24 | changedir = docs 25 | deps = 26 | Sphinx 27 | Django >= 1.8, < 1.9 28 | commands = 29 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 30 | --------------------------------------------------------------------------------