├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── runtests.py ├── setup.cfg ├── setup.py ├── src └── django_dumpdata_one │ ├── __init__.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── dumpdata_one.py │ └── models.py ├── tests ├── __init__.py ├── factories.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── test_child_model.py ├── test_dumpdata_one_command.py ├── test_file_fields.py └── test_models.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | dist/ 4 | build/ 5 | .tox 6 | .coverage 7 | *.egg-info -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: python 4 | 5 | sudo: false 6 | 7 | cache: pip 8 | 9 | python: 10 | - "3.8" 11 | - "3.7" 12 | - "3.6" 13 | 14 | install: 15 | - pip install tox-travis codecov 16 | 17 | script: 18 | - tox 19 | 20 | after_success: 21 | - codecov 22 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The primary author of django-dumpdata-one is Stjepan Zlodi (Ascalia.io). 2 | 3 | Ylodi - https://github.com/Ylodi 4 | aadomenech - https://github.com/aadomenech 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## [0.8.6] - 2022-08-30 4 | - check and test for Django 4.0, 4.1 and Python 3.10 5 | 6 | ## [0.8.5] - 2021-08-09 7 | - add --database argument (aadomenech) 8 | 9 | ## [0.8.4] - 2021-04-07 10 | - check and test for Django 3.2 11 | 12 | ## [0.8.3] - 2020-07-17 13 | ### Fixed 14 | - roll back to .values method to retrieve field values 15 | - a simple hack to get file url 16 | 17 | ## [0.8.2] - 2020-07-01 18 | ### Added 19 | - dump full file/image url `--full_url={field_name},{other_file_name}` 20 | - limit number of records `--limit={number}` 21 | 22 | ### Changed 23 | - rewrite way to retrieve a field value 24 | 25 | ## [0.8.1] - 2020-06-02 26 | ### Added 27 | - dump all model fields with argument `--fields=*` 28 | 29 | ### Fixed 30 | - multiple fields in order argument 31 | 32 | ## [0.8] - 2020-05-22 33 | 34 | Initial public release -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Django Dumpdata One 2 | =================================== 3 | 4 | As an open source project, *Django Dumpdata One* welcomes contributions of any kind. 5 | 6 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 7 | 8 | Code contribution 9 | ----------------- 10 | 11 | #. Create a fork 12 | #. Clone your new fork locally 13 | #. Track the original repository as a remote of the fork 14 | #. Create a new branch for your changes 15 | #. Do your work 16 | #. Test your work 17 | #. Add, commit and push the working changes 18 | #. Submit your pull request 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Ascalia.io and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Ascalia nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include AUTHORS 4 | include CONTRIBUTING.rst 5 | include tox.ini 6 | include runtests.py 7 | graft src -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/ascaliaio/django-dumpdata-one.svg 2 | :target: https://travis-ci.org/github/ascaliaio/django-dumpdata-one/ 3 | 4 | .. image:: https://codecov.io/gh/ascaliaio/django-dumpdata-one/branch/master/graph/badge.svg 5 | :target: https://codecov.io/gh/ascaliaio/django-dumpdata-one 6 | 7 | Django Dumpdata One 8 | =================== 9 | 10 | Custom ``dumpdata`` command which allows to exporting from given fields of a model 11 | and filter that data using standard Django lookups for filtering. 12 | 13 | The exported data structure is compatible with Django ``dumpdata`` structure which 14 | allows you to use standard ``loaddata`` command for import. 15 | 16 | Installation 17 | ------------ 18 | 19 | To get the latest stable release from PyPi 20 | 21 | .. code-block:: bash 22 | 23 | pip install django-dumpdata-one 24 | 25 | 26 | Add ``dumpdata_one`` to your ``INSTALLED_APPS`` 27 | 28 | .. code-block:: python 29 | 30 | INSTALLED_APPS = ( 31 | ..., 32 | "django_dumpdata_one", 33 | ) 34 | 35 | Usage 36 | ----- 37 | 38 | Export data: 39 | 40 | .. code-block:: bash 41 | 42 | ./manage.py dumpdata_one app_name.model_name --fields=field1,field2 > dump_file.json 43 | 44 | 45 | Import data: 46 | 47 | .. code-block:: bash 48 | 49 | ./manage.py loaddata dump_file.json 50 | 51 | 52 | How to use filters? If you not familiar take a look at Django Field 53 | lookups - https://docs.djangoproject.com/en/3.0/topics/db/queries/#field-lookups 54 | 55 | .. code-block:: bash 56 | 57 | ./manage.py dumpdata_one app_name.model_name --fields=field1 --filter=name__icontains=django 58 | 59 | ./manage.py dumpdata_one app_name.model_name --fields=field1 --filter=name__icontains=django,pk__gt=300 60 | 61 | Set order by: 62 | 63 | .. code-block:: bash 64 | 65 | ./manage.py dumpdata_one app_name.model_name --fields=field1,field2 --order=field2,field2 66 | 67 | Export all fields: 68 | 69 | .. code-block:: bash 70 | 71 | ./manage.py dumpdate_one app_name.model_name --fields=* 72 | 73 | 74 | Limit number of exported records: 75 | 76 | .. code-block:: bash 77 | 78 | ./manage.py dumpdata_one app_name.model_name --fields=* --limit=10 79 | 80 | 81 | Export full file URL: 82 | 83 | .. code-block:: bash 84 | 85 | ./manage.py dumpdata_one app_name.model_name --fields=image --full_url=image 86 | 87 | 88 | Export from another database than 'default': 89 | 90 | .. code-block:: bash 91 | 92 | ./manage.py dumpdata_one app_name.model_name --database=other_database -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A standalone test runner script, configuring the minimum settings 4 | required for tests to execute. 5 | 6 | Re-use at your own risk: many Django applications will require 7 | different settings and/or templates to run their tests. 8 | 9 | Inspired by James Bennett 10 | https://www.b-list.org/weblog/2020/feb/03/how-im-testing-2020/ 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | 17 | APP_DIR = os.path.abspath(os.path.dirname(__file__)) 18 | 19 | 20 | # Minimum settings required for the app's tests. 21 | SETTINGS_DICT = { 22 | "BASE_DIR": APP_DIR, 23 | "INSTALLED_APPS": ( 24 | "django.contrib.auth", 25 | "django.contrib.contenttypes", 26 | "django.contrib.sessions", 27 | "django.contrib.sites", 28 | 29 | "django_dumpdata_one", 30 | "tests", 31 | ), 32 | # Test cases will override this liberally. 33 | # "ROOT_URLCONF": "django_registration.backends.activation.urls", 34 | "DATABASES": { 35 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} 36 | }, 37 | "MIDDLEWARE": ( 38 | "django.middleware.common.CommonMiddleware", 39 | "django.middleware.csrf.CsrfViewMiddleware", 40 | "django.contrib.sessions.middleware.SessionMiddleware", 41 | "django.contrib.auth.middleware.AuthenticationMiddleware", 42 | ), 43 | "SITE_ID": 1, 44 | "TEMPLATES": [ 45 | { 46 | "BACKEND": "django.template.backends.django.DjangoTemplates", 47 | "DIRS": [os.path.join(APP_DIR, "tests/templates")], 48 | "OPTIONS": { 49 | "context_processors": [ 50 | "django.contrib.auth.context_processors.auth", 51 | "django.template.context_processors.debug", 52 | "django.template.context_processors.i18n", 53 | "django.template.context_processors.media", 54 | "django.template.context_processors.static", 55 | "django.template.context_processors.tz", 56 | "django.contrib.messages.context_processors.messages", 57 | ] 58 | }, 59 | } 60 | ], 61 | "MEDIA_URL": "https://media.example.com/", 62 | "DEFAULT_AUTO_FIELD": "django.db.models.AutoField" 63 | } 64 | 65 | 66 | def run_tests(): 67 | # Making Django run this way is a two-step process. First, call 68 | # settings.configure() to give Django settings to work with: 69 | from django.conf import settings 70 | 71 | settings.configure(**SETTINGS_DICT) 72 | 73 | # Then, call django.setup() to initialize the application cache 74 | # and other bits: 75 | import django 76 | 77 | django.setup() 78 | 79 | # Now we instantiate a test runner... 80 | from django.test.utils import get_runner 81 | 82 | TestRunner = get_runner(settings) 83 | 84 | # And then we run tests and return the results. 85 | test_runner = TestRunner(verbosity=2, interactive=True) 86 | failures = test_runner.run_tests(["tests"]) 87 | sys.exit(bool(failures)) 88 | 89 | 90 | if __name__ == "__main__": 91 | run_tests() 92 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | 4 | [coverage:report] 5 | fail_under = 100 6 | 7 | [flake8] 8 | exclude = __pycache__,.pyc 9 | 10 | [isort] 11 | include_trailing_comma = True 12 | known_first_party = django_dumpdata_one 13 | known_thirs_party = django 14 | lines_after_imports = 2 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | 6 | setup( 7 | name="django-dumpdata-one", 8 | zip_safe=False, 9 | version="0.8.6", 10 | description="Django management command to export choosen data from one table", 11 | long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(), 12 | long_description_content_type="text/x-rst", 13 | author="Stjepan Zlodi", 14 | author_email="stjepan@gmail.com", 15 | url="https://github.com/ascaliaio/django-dumpdata-one", 16 | package_dir={"": "src"}, 17 | packages=find_packages("src"), 18 | classifiers=[ 19 | "Development Status :: 4 - Beta", 20 | "Environment :: Web Environment", 21 | "Framework :: Django", 22 | "Framework :: Django :: 2.2", 23 | "Framework :: Django :: 3.0", 24 | "Framework :: Django :: 3.1", 25 | "Framework :: Django :: 3.2", 26 | "Framework :: Django :: 4.0", 27 | "Framework :: Django :: 4.1", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: BSD License", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3.10", 40 | "Topic :: Utilities", 41 | ], 42 | python_requires=">=3.6", 43 | install_requires=["Django>=2.2"] 44 | ) 45 | -------------------------------------------------------------------------------- /src/django_dumpdata_one/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/src/django_dumpdata_one/__init__.py -------------------------------------------------------------------------------- /src/django_dumpdata_one/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/src/django_dumpdata_one/management/__init__.py -------------------------------------------------------------------------------- /src/django_dumpdata_one/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/src/django_dumpdata_one/management/commands/__init__.py -------------------------------------------------------------------------------- /src/django_dumpdata_one/management/commands/dumpdata_one.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.apps import apps 3 | from django.core.management.base import BaseCommand 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def add_arguments(self, parser): 10 | 11 | parser.add_argument('app_model', type=str) 12 | parser.add_argument('--order', type=str, default='pk') 13 | parser.add_argument('--fields', type=str) 14 | parser.add_argument('--filter', type=str) 15 | parser.add_argument('--full_url', type=str) 16 | parser.add_argument('--limit', type=int) 17 | parser.add_argument('--database', type=str) 18 | 19 | def handle(self, *args, **options): 20 | 21 | app_model = options['app_model'] 22 | fields = options['fields'] 23 | order = options['order'] 24 | filters = self.get_filter_dict(options['filter']) 25 | full_urls = options['full_url'] 26 | limit = options['limit'] 27 | database = options['database'] or 'default' 28 | 29 | Model = apps.get_model(app_model) 30 | 31 | if fields == '*': 32 | fields = self.get_all_fields(Model) 33 | else: 34 | fields = fields.split(',') if fields is not None else [] 35 | 36 | if 'pk' not in fields: 37 | fields.append('pk') 38 | 39 | full_urls = full_urls.split(',') if full_urls is not None else [] 40 | 41 | records = Model.objects.using(database).all() 42 | if filters: 43 | records = records.filter(**filters) 44 | 45 | order_by = self.get_order_list(order) 46 | 47 | records = records.order_by(*order_by) 48 | records = records.values(*fields) 49 | if limit: 50 | records = records[:limit] 51 | 52 | dump_structure = self.get_dump_structure( 53 | app_model, records, fields, full_urls, Model 54 | ) 55 | result = json.dumps(dump_structure, cls=DjangoJSONEncoder) 56 | return result 57 | 58 | def get_dump_structure(self, app_model, records, fields, full_urls, Model): 59 | results = [] 60 | for item in records: 61 | values = {} 62 | for key, value in item.items(): 63 | if key != "pk": 64 | values[key] = value 65 | 66 | if key in full_urls: 67 | # it's a hack, if you know how to do it smarter just say ;-) 68 | # problem is more complicated with custom storage providers 69 | hack = Model() 70 | setattr(hack, key, value) 71 | values[f"{key}__full_url"] = getattr(hack, key).url 72 | 73 | item_structure = { 74 | "model": app_model, 75 | "pk": item.get("pk"), 76 | "fields": values 77 | } 78 | results.append(item_structure) 79 | 80 | return results 81 | 82 | def get_filter_dict(self, filter_string): 83 | filters = {} 84 | if filter_string: 85 | pairs = filter_string.split(',') 86 | for pair in pairs: 87 | key, value = pair.split('=') 88 | filters[key] = value 89 | 90 | return filters 91 | 92 | def get_order_list(self, order): 93 | return order.split(",") 94 | 95 | def get_all_fields(self, Model): 96 | return [field.name for field in Model._meta.fields] 97 | -------------------------------------------------------------------------------- /src/django_dumpdata_one/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/src/django_dumpdata_one/models.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/tests/__init__.py -------------------------------------------------------------------------------- /tests/factories.py: -------------------------------------------------------------------------------- 1 | from factory import SubFactory 2 | from factory.django import DjangoModelFactory 3 | from faker import Factory 4 | 5 | from .models import OneModel, ChildModel 6 | 7 | 8 | faker = Factory.create() 9 | 10 | 11 | class OneModelFactory(DjangoModelFactory): 12 | name = faker.name() 13 | decimal_value = faker.random_digit() 14 | 15 | class Meta: 16 | model = OneModel 17 | 18 | 19 | class ModelWithFileFactory(OneModelFactory): 20 | document = faker.file_path() 21 | 22 | 23 | class ChildModelFactory(DjangoModelFactory): 24 | one = SubFactory(OneModelFactory) 25 | name = faker.name() 26 | 27 | class Meta: 28 | model = ChildModel 29 | -------------------------------------------------------------------------------- /tests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | import django.db.models.deletion 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | initial = True 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='OneModel', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=100)), 18 | ('text', models.TextField(default='')), 19 | ('is_active', models.BooleanField(default=True)), 20 | ('time_is', models.DateTimeField(auto_now_add=True)), 21 | ('decimal_value', models.DecimalField(decimal_places=2, max_digits=5)), 22 | ('integer_value', models.IntegerField(default=1)), 23 | ('document', models.FileField(blank=True, null=True, upload_to='')), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='ChildModel', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('name', models.CharField(max_length=100)), 31 | ('one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.OneModel')), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascaliaio/django-dumpdata-one/991eee8f10a4d8fffbd3498ce9617a2d64f2029b/tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class OneModel(models.Model): 5 | name = models.CharField(max_length=100) 6 | text = models.TextField(default='') 7 | is_active = models.BooleanField(default=True) 8 | time_is = models.DateTimeField(auto_now_add=True) 9 | decimal_value = models.DecimalField(max_digits=5, decimal_places=2) 10 | integer_value = models.IntegerField(default=1) 11 | document = models.FileField(blank=True, null=True) 12 | 13 | 14 | class ChildModel(models.Model): 15 | one = models.ForeignKey(OneModel, on_delete=models.CASCADE) 16 | name = models.CharField(max_length=100) 17 | -------------------------------------------------------------------------------- /tests/test_child_model.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import json 3 | 4 | from django.core.management import call_command 5 | from django.test import TestCase 6 | 7 | from .factories import ChildModelFactory 8 | 9 | 10 | class DumpChildModelTests(TestCase): 11 | def setUp(self): 12 | self.APP_MODEL = 'tests.childmodel' 13 | self.COMMAND = 'dumpdata_one' 14 | 15 | def test_with_100_records(self): 16 | BATCH_SIZE = 100 17 | ChildModelFactory.create_batch(size=BATCH_SIZE) 18 | 19 | FIELDS_STRING = "one,name" 20 | 21 | out = StringIO() 22 | call_command( 23 | self.COMMAND, 24 | self.APP_MODEL, 25 | fields=FIELDS_STRING, 26 | stdout=out 27 | ) 28 | 29 | results = json.loads(out.getvalue()) 30 | 31 | self.assertEqual(len(results), BATCH_SIZE) 32 | -------------------------------------------------------------------------------- /tests/test_dumpdata_one_command.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import json 3 | 4 | from django.core.management import call_command 5 | from django.core.management.base import CommandError 6 | from django.test import TestCase 7 | 8 | from .factories import OneModelFactory 9 | 10 | 11 | class DumpDataOneCallTests(TestCase): 12 | def setUp(self): 13 | self.APP_MODEL = 'tests.onemodel' 14 | self.COMMAND = 'dumpdata_one' 15 | self.NON_EXISTING_MODEL = 'zero.model' 16 | 17 | def test_without_parameter(self): 18 | 19 | out = StringIO() 20 | 21 | with self.assertRaises(CommandError): 22 | call_command(self.COMMAND, stdout=out) 23 | 24 | def test_with_invalid_app_model(self): 25 | 26 | out = StringIO() 27 | 28 | with self.assertRaises(LookupError): 29 | call_command(self.COMMAND, self.NON_EXISTING_MODEL, stdout=out) 30 | 31 | def test_with_no_records(self): 32 | 33 | out = StringIO() 34 | call_command(self.COMMAND, self.APP_MODEL, stdout=out) 35 | 36 | results = json.loads(out.getvalue()) 37 | 38 | self.assertEqual(results, []) 39 | 40 | def test_with_one_records(self): 41 | OneModelFactory() 42 | 43 | out = StringIO() 44 | call_command(self.COMMAND, self.APP_MODEL, stdout=out) 45 | 46 | results = json.loads(out.getvalue()) 47 | 48 | self.assertEqual(len(results), 1) 49 | 50 | one = results[0] 51 | 52 | self.assertEqual(one.get('model'), self.APP_MODEL) 53 | 54 | def test_with_100_records(self): 55 | BATCH_SIZE = 100 56 | OneModelFactory.create_batch(size=BATCH_SIZE) 57 | 58 | out = StringIO() 59 | call_command(self.COMMAND, self.APP_MODEL, stdout=out) 60 | 61 | results = json.loads(out.getvalue()) 62 | 63 | self.assertEqual(len(results), BATCH_SIZE) 64 | 65 | def test_100_records_with_filter(self): 66 | BATCH_SIZE = 100 67 | OneModelFactory.create_batch(size=BATCH_SIZE) 68 | 69 | FILTER_STRING = 'pk__gt=0,integer_value=1' 70 | FIELDS_STRING = 'pk,name,decimal_value' 71 | 72 | out = StringIO() 73 | call_command( 74 | self.COMMAND, 75 | self.APP_MODEL, 76 | fields=FIELDS_STRING, 77 | filter=FILTER_STRING, 78 | stdout=out 79 | ) 80 | 81 | results = json.loads(out.getvalue()) 82 | 83 | self.assertEqual(len(results), BATCH_SIZE) 84 | 85 | def test_get_all_fields_method(self): 86 | OneModelFactory() 87 | 88 | out = StringIO() 89 | call_command( 90 | self.COMMAND, 91 | self.APP_MODEL, 92 | fields='*', 93 | stdout=out 94 | ) 95 | 96 | results = json.loads(out.getvalue()) 97 | 98 | self.assertEqual(len(results), 1) 99 | 100 | def test_limit_argument(self): 101 | BATCH_SIZE = 100 102 | LIMIT = 50 103 | OneModelFactory.create_batch(size=BATCH_SIZE) 104 | 105 | out = StringIO() 106 | call_command( 107 | self.COMMAND, 108 | self.APP_MODEL, 109 | fields='*', 110 | limit=LIMIT, 111 | stdout=out 112 | ) 113 | 114 | results = json.loads(out.getvalue()) 115 | 116 | self.assertEqual(len(results), LIMIT) 117 | -------------------------------------------------------------------------------- /tests/test_file_fields.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import json 3 | 4 | from django.core.management import call_command 5 | from django.test import TestCase 6 | 7 | from .factories import ModelWithFileFactory 8 | 9 | 10 | class DumpDataWithFilesTests(TestCase): 11 | def setUp(self): 12 | self.APP_MODEL = 'tests.onemodel' 13 | self.COMMAND = 'dumpdata_one' 14 | 15 | def test_one_file(self): 16 | record = ModelWithFileFactory() 17 | 18 | out = StringIO() 19 | call_command( 20 | self.COMMAND, 21 | self.APP_MODEL, 22 | fields="document", 23 | stdout=out 24 | ) 25 | 26 | results = json.loads(out.getvalue()) 27 | 28 | self.assertEqual(len(results), 1) 29 | 30 | result_record = results[0] 31 | 32 | self.assertEqual( 33 | record.document.name, 34 | result_record.get("fields").get("document") 35 | ) 36 | 37 | def test_one_file_with_full_url(self): 38 | record = ModelWithFileFactory() 39 | 40 | out = StringIO() 41 | call_command( 42 | self.COMMAND, 43 | self.APP_MODEL, 44 | fields="document", 45 | full_url="document", 46 | stdout=out 47 | ) 48 | 49 | results = json.loads(out.getvalue()) 50 | 51 | self.assertEqual(len(results), 1) 52 | 53 | result_record = results[0] 54 | 55 | self.assertEqual( 56 | record.document.name, 57 | result_record.get("fields").get("document") 58 | ) 59 | 60 | self.assertEqual( 61 | result_record.get("fields").get("document__full_url"), 62 | record.document.url 63 | ) 64 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from .factories import OneModelFactory 4 | from .models import OneModel 5 | 6 | 7 | class OneModelTests(TestCase): 8 | 9 | def test_model(self): 10 | NUM_RECORDS = 100 11 | OneModelFactory.create_batch(size=NUM_RECORDS) 12 | 13 | self.assertEqual(OneModel.objects.all().count(), NUM_RECORDS) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py36,py37,py38,py39,py310}-django{22,30,31,32,40,41} 4 | 5 | [cleanup] 6 | commands = 7 | find {toxinidir}/tests -type f -name "*.pyc" -delete 8 | find {toxinidir}/tests -type d -name "__pycache__" -delete 9 | find {toxinidir}/src -type f -name "*.pyc" -delete 10 | find {toxinidir}/src -type d -name "__pycache__" -delete 11 | find {toxinidir}/src -type f -path "*.egg-info*" -delete 12 | find {toxinidir}/src -type d -path "*.egg-info" -delete 13 | 14 | [testenv] 15 | whitelist_externals = 16 | find 17 | rm 18 | setenv = 19 | PYTHONWARNINGS=once::DeprecationWarning 20 | commands = 21 | coverage run --source django_dumpdata_one runtests.py 22 | coverage report -m 23 | {[cleanup]commands} 24 | deps = 25 | coverage 26 | factory-boy 27 | django22: Django>=2.2,<3.0 28 | django30: Django>=3.0,<3.1 29 | django31: Django>=3.1,<3.2 30 | django32: Django>=3.2,<3.3 31 | django40: Django>=4.0,<4.1 32 | django41: Django>=4.1,<4.2 33 | 34 | [travis] 35 | python = 36 | 3.6: py36 37 | 3.7: py37 38 | 3.8: py38 39 | 3.9: py39 40 | 3.10: py310 41 | --------------------------------------------------------------------------------