pax_global_header00006660000000000000000000000064141610756700014521gustar00rootroot0000000000000052 comment=3bedef5950f36294b7fc2ba33e1f4d6e781960a8 django-import-export-2.7.1/000077500000000000000000000000001416107567000156215ustar00rootroot00000000000000django-import-export-2.7.1/.github/000077500000000000000000000000001416107567000171615ustar00rootroot00000000000000django-import-export-2.7.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001416107567000213445ustar00rootroot00000000000000django-import-export-2.7.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012601416107567000240350ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Versions (please complete the following information):** - Django Import Export: [e.g. 1.2, 2.0] - Python [e.g. 3.6, 3.7] - Django [e.g. 1.2, 2.0] **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. django-import-export-2.7.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000007501416107567000250730ustar00rootroot00000000000000**Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. django-import-export-2.7.1/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000011471416107567000235400ustar00rootroot00000000000000--- name: Question about: Do you have a general question? title: '' labels: question assignees: '' --- django-import-export-2.7.1/.github/pull_request_template.md000066400000000000000000000003471416107567000241260ustar00rootroot00000000000000**Problem** What problem have you solved? **Solution** How did you solve the problem? **Acceptance Criteria** Have you written tests? Have you included screenshots of your changes if applicable? Did you document your changes? django-import-export-2.7.1/.github/stale.yml000066400000000000000000000026311416107567000210160ustar00rootroot00000000000000# Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 180 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. daysUntilClose: 14 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - pinned - security # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. # closeComment: > # Your comment here. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': pulls: markComment: > This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. django-import-export-2.7.1/.github/workflows/000077500000000000000000000000001416107567000212165ustar00rootroot00000000000000django-import-export-2.7.1/.github/workflows/django-import-export-ci.yml000066400000000000000000000104371416107567000264300ustar00rootroot00000000000000name: django-import-export CI on: push: branches: - main pull_request: branches: - main jobs: test: runs-on: ubuntu-latest env: USERNAME: testuser PASSWD: ${{ secrets.TEST_PASSWD }} strategy: max-parallel: 4 matrix: db: [ sqlite, postgres, mysql ] python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10" ] django-version: [ 2.2, 3.0, 3.1, 3.2, 4.0, main ] include: - db: postgres db_port: 5432 - db: mysql db_port: 3306 exclude: - django-version: main python-version: 3.6 - django-version: main python-version: 3.7 - django-version: 4.0 python-version: 3.6 - django-version: 4.0 python-version: 3.7 - django-version: 2.2 python-version: "3.10" - django-version: 3.0 python-version: "3.10" - django-version: 3.1 python-version: "3.10" - django-version: 3.2 python-version: "3.10" services: mysql: image: mysql:8.0 env: IMPORT_EXPORT_TEST_TYPE: mysql-innodb IMPORT_EXPORT_MYSQL_USER: ${{ env.TESTUSER }} IMPORT_EXPORT_MYSQL_PASSWORD: ${{ env.PASSWD }} MYSQL_USER: ${{ env.TESTUSER }} MYSQL_PASSWORD: ${{ env.IMPORT_EXPORT_MYSQL_PASSWORD }} MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: import_export ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 postgres: image: postgres env: IMPORT_EXPORT_TEST_TYPE: postgres IMPORT_EXPORT_POSTGRESQL_USER: postgres IMPORT_EXPORT_POSTGRESQL_PASSWORD: ${{ env.PASSWD }} POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: import_export ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out repository code uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Run isort checks uses: jamescurtin/isort-action@master with: sortPaths: "import_export tests" configuration: "--check-only" - name: Install Dependencies run: | python -m pip install --upgrade pip pip install -r requirements/base.txt pip install -r requirements/test.txt - if: matrix.django-version != 'main' name: Upgrade Django version (release) run: | python -m pip install "Django~=${{ matrix.django-version }}.0" - if: matrix.django-version == 'main' name: Upgrade Django version (main) run: | python -m pip install "https://github.com/django/django/archive/main.tar.gz" - name: List versions run: | echo "Python ${{ matrix.python-version }} -> Django ${{ matrix.django-version }}" python --version echo "Django `django-admin --version`" - name: Run Tests env: DB: ${{ matrix.db }} DB_HOST: 127.0.0.1 DB_PORT: ${{ matrix.db_port }} DB_PASSWORD: ${{ env.PASSWD }} run: >- PYTHONPATH=".:tests:$PYTHONPATH" python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m coverage run --omit='setup.py,./tests/*,./import_export/locale/*' --source=. tests/manage.py test core --settings= - name: Upload coverage data to coveralls.io run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.db }}-${{ matrix.django-version }}-${{ matrix.python-version }} COVERALLS_PARALLEL: true coveralls: name: Indicate completion to coveralls.io needs: test runs-on: ubuntu-latest container: python:3-slim steps: - name: Finished run: | pip3 install --upgrade coveralls coveralls --service=github --finish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} django-import-export-2.7.1/.gitignore000066400000000000000000000002441416107567000176110ustar00rootroot00000000000000*.log *.pot *.pyc local_settings.py docs/_build build/ dist/ /django-import-export/ *.egg-info/ .tox/ .idea/ *.python-version .coverage *.sw[po] tests/database.db django-import-export-2.7.1/AUTHORS000066400000000000000000000056401416107567000166760ustar00rootroot00000000000000The django-import-export was original created by Bojan Mihelac (bmihelac). The following is a list of much appreciated contributors: * Titusz * jmaupetit (Julien Maupetit) * geeknam (Nam Ngo) * tepez * rubinsztajn (Aaron Rubinstein) * pombredanne * djm (Darian Moody) * pjdelport (Pi Delport) * rhunwicks (Roger Hunwicks) * trbs (IdoTech) * mulder999 * inodb * paulshannon (Paul Shannon) * tomd-aptivate (Tom Daley) * mauler * brad (Brad Pitcher) * xevinbox (Степан) * peterisb * qris (Chris Wilson) * andreacimino (Andrea Cimino) * frank-u (Oleksandr Poliatykin) * Kobold (Andy Kish) * cherepski * ludwiktrammer (Ludwik Trammer) * cuchac * aidanlister (Aidan Lister) * tabac * GroSte * w0rp * nastyako (Nastya Konak) * ianwalter (Ian Walter) * amarandon (Alex Marandon) * caljess599 * mightygraf (Denis K) * spookylukey (Luke Plant) * kane-c * ddiazpinto (David Díaz) * jeberger (Jérôme M. Berger) * schemacs (Lele Long) * jbub (Juraj Bubniak) * manelclos (Manel Clos) * pkozlov (Pavel Kozlov) * vparitskiy * snj (KINOSHITA Shinji) * AyumuKasuga (Andrey Kostakov) * luto * gallochri (Christian Galeffi) * ericdwang (Eric Wang) * gonzalobustos (Gonzalo Bustos) * domenkozar (Domen Kožar) * staranjeet (Taranjeet Singh) * mikhailmain * retrry (Tadas Barzdžius) * ericbuckley (Eric Buckley) * PetrDlouhy * shalakhin (Olexandr Shalakhin) * fabian (Fabian Vogler) * citmusa (Citlalli Murillo) * jarekwg (Jarek Glowacki) * fossilet (Yongzhi Pan) * jnns (Jannis) * sgarcialaguna * carlosp420 (Carlos Peña) * freelancersunion (Freelancers Union) * jbradberry (Jeff Bradberry) * antoniablair * kellerkind * yueyoum (Johnnie Wang) * shaggyfrog (Thomas Hauk) * suriya (Suriya Subramanian) * arj03 (Anders Rune Jensen) * arseniy-panfilov (Arseniy Panfilov) * jheld (Jason Held) * pajod (Paul J. Dorn) * illagrenan (Vašek Dohnal) * browniebroke (Bruno Alla) * ahmed-cheikh (Ahmed Cheikh) * jschneier (Josh Schneier) * brianmay (Brian May) * williamwang61 (Will Wang) * adamcharnock (Adam Charnock) * paveltyavin (Pavel) * jameshiew (James Hiew) * mgrdcm (Dan Moore) * korkmaz (Ahmet Korkmaz) * petrmifek * ryan-copperleaf (Ryan O’Hara) * gatsinski (Hristo Gatsinski) * raghavsethi (Raghav Sethi) * jdufresne (Jon Dufresne) * trik (Marco Marche) * krishraghuram (Raghuram Krishnaswami) * int_ua (Serhiy Zahoriya) * Marpop (Marcin Popławski) * frnhr (Fran Hrženjak) * gelbander (Gustaf Elbander) * dfirst * lorenzomorandini * piotr-szpetkowski (Piotr Szpetkowski) * pacotole (Pacotole) * KenyaDonDraper * andrew-bro (Andrei Loskutov) * toivomattila (Toivo Mattila) * ZuluPro (Anthony Monthe) * kunal15595 (Kunal Khandelwal) * fatanugraha (Fata Nugraha) * aniav (Ania Warzecha) * ababic (Andy Babic) * BramManuel (Bram Janssen) * mofe (Moritz Federspiel) * kjpc-tech (Kyle) * Matthew Hegarty * jinmay (jinmyeong Cho) * DonQueso89 (Kees van Ekeren) * yazdanRa (Yazdan Ranjbar) * Gabriel Warkentin * striveforbest (Alex Zagoro) * josx (José Luis Di Biase) * Jan Rydzewski django-import-export-2.7.1/CODE_OF_CONDUCT.md000066400000000000000000000126531416107567000204270ustar00rootroot00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [django-import-export team](https://github.com/orgs/django-import-export/people). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations django-import-export-2.7.1/CONTRIBUTING.md000066400000000000000000000064561416107567000200650ustar00rootroot00000000000000Contributing ============ django-import-export is open-source and, as such, grows (or shrinks) & improves in part due to the community. Below are some guidelines on how to help with the project. By contributing you agree to abide by the [Contributor Code of Conduct][coc] Philosophy ---------- * django-import-export is BSD-licensed. All contributed code must be either * the original work of the author, contributed under the BSD, or... * work taken from another project released under a BSD-compatible license. * GPL'd (or similar) works are not eligible for inclusion. * django-import-export's git master branch should always be stable, production-ready & passing all tests. Guidelines For Reporting An Issue/Feature ----------------------------------------- So you've found a bug or have a great idea for a feature. Here's the steps you should take to help get it added/fixed in django-import-export: * First, check to see if there's an existing issue/pull request for the bug/feature. All issues are at https://github.com/django-import-export/django-import-export/issues and pull reqs are at https://github.com/django-import-export/django-import-export/pulls. * If there isn't one there, please file an issue. The ideal report includes: * A description of the problem/suggestion. * How to recreate the bug. * If relevant, including the versions of your: * Python interpreter * Django * tablib version * django-import-export * Optionally of the other dependencies involved * Ideally, creating a pull request with a (failing) test case demonstrating what's wrong. This makes it easy for us to reproduce & fix the problem. Guidelines For Contributing Code -------------------------------- If you're ready to take the plunge & contribute back some code/docs, the process should look like: * Fork the project on GitHub into your own account. * Clone your copy of django-import-export. * Make a new branch in git & commit your changes there. * Push your new branch up to GitHub. * Again, ensure there isn't already an issue or pull request out there on it. If there is & you feel you have a better fix, please take note of the issue number & mention it in your pull request. * Create a new pull request (based on your branch), including what the problem/feature is, versions of your software & referencing any related issues/pull requests. In order to be merged into django-import-export, contributions must have the following: * A solid patch that: * is clear. * works across all supported versions of Python/Django. * follows the existing style of the code base (mostly PEP-8). * comments included as needed to explain why the code functions as it does * A test case that demonstrates the previous flaw that now passes with the included patch. * If it adds/changes a public API, it must also include documentation for those changes. * Must be appropriately licensed (see [Philosophy](#philosophy)). * Adds yourself to the AUTHORS file. If your contribution lacks any of these things, they will have to be added by a core contributor before being merged into django-import-export proper, which may take substantial time for the all-volunteer team to get to. [coc]: https://github.com/django-import-export/django-import-export/blob/master/CODE_OF_CONDUCT.md django-import-export-2.7.1/LICENSE000066400000000000000000000024771416107567000166400ustar00rootroot00000000000000Copyright (c) Bojan Mihelac and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-import-export-2.7.1/MANIFEST.in000066400000000000000000000002611416107567000173560ustar00rootroot00000000000000include LICENSE include AUTHORS include README.rst recursive-include import_export/templates * recursive-include import_export/locale * recursive-include import_export/static * django-import-export-2.7.1/Makefile000066400000000000000000000036501416107567000172650ustar00rootroot00000000000000.PHONY: clean-pyc clean-build release docs help .PHONY: lint test coverage test-codecov .DEFAULT_GOAL := help RUN_TEST_COMMAND=PYTHONPATH=".:tests:${PYTHONPATH}" django-admin test core --settings=settings help: @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' clean: clean-build clean-pyc clean-tests clean-build: ## remove build artifacts rm -fr build/ rm -fr dist/ rm -fr *.egg-info clean-pyc: ## remove Python file artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + clean-tests: ## remove pytest artifacts rm -fr .pytest_cache/ rm -fr htmlcov/ rm -fr django-import-export/ lint: ## check style with isort isort --check-only . test: ## run tests quickly with the default Python $(RUN_TEST_COMMAND) messages: ## generate locale file translations cd import_export && django-admin makemessages -a && django-admin compilemessages && cd .. coverage: ## generates codecov report coverage run --omit='setup.py,tests/*' --source=. tests/manage.py test core --settings= coverage report sdist: clean ## package python setup.py sdist ls -l dist release: clean install-deploy-requirements sdist ## package and upload a release fullrelease install-base-requirements: ## install package requirements pip install -r requirements/base.txt install-test-requirements: ## install requirements for testing pip install -r requirements/test.txt install-deploy-requirements: ## install requirements for deployment pip install -r requirements/deploy.txt install-docs-requirements: ## install requirements for docs pip install -r requirements/docs.txt install-requirements: install-base-requirements install-test-requirements install-deploy-requirements install-docs-requirements build-html-doc: ## builds the project documentation in HTML format DJANGO_SETTINGS_MODULE=tests.settings make html -C docs django-import-export-2.7.1/README.rst000066400000000000000000000051071416107567000173130ustar00rootroot00000000000000==================== django-import-export ==================== .. image:: https://github.com/django-import-export/django-import-export/actions/workflows/django-import-export-ci.yml/badge.svg :target: https://github.com/django-import-export/django-import-export/actions/workflows/django-import-export-ci.yml :alt: Build status on Github .. image:: https://coveralls.io/repos/github/django-import-export/django-import-export/badge.svg?branch=main :target: https://coveralls.io/github/django-import-export/django-import-export?branch=main .. image:: https://img.shields.io/pypi/v/django-import-export.svg :target: https://pypi.org/project/django-import-export/ :alt: Current version on PyPi .. image:: http://readthedocs.org/projects/django-import-export/badge/?version=stable :target: https://django-import-export.readthedocs.io/en/stable/ :alt: Documentation .. image:: https://img.shields.io/pypi/pyversions/django-import-export :alt: PyPI - Python Version .. image:: https://img.shields.io/pypi/djversions/django-import-export :alt: PyPI - Django Version django-import-export is a Django application and library for importing and exporting data with included admin integration. Features: * support multiple formats (Excel, CSV, JSON, ... and everything else that `tablib`_ supports) * admin integration for importing * preview import changes * admin integration for exporting * export data respecting admin filters .. image:: docs/_static/images/django-import-export-change.png * Documentation: https://django-import-export.readthedocs.io/en/stable/ * GitHub: https://github.com/django-import-export/django-import-export/ * Free software: BSD license * PyPI: https://pypi.org/project/django-import-export/ Example app ----------- To run the demo app:: cd tests ./manage.py makemigrations ./manage.py migrate ./manage.py createsuperuser ./manage.py loaddata category book ./manage.py runserver Contribute ---------- If you'd like to contribute, simply fork `the repository`_, commit your changes to the **develop** branch (or branch off of it), and send a pull request. Make sure you add yourself to AUTHORS_. As most projects, we try to follow PEP8_ as closely as possible. Please bear in mind that most pull requests will be rejected without proper unit testing. .. _`PEP8`: https://www.python.org/dev/peps/pep-0008/ .. _`tablib`: https://github.com/jazzband/tablib .. _`the repository`: https://github.com/django-import-export/django-import-export/ .. _AUTHORS: https://github.com/django-import-export/django-import-export/blob/master/AUTHORS django-import-export-2.7.1/docs/000077500000000000000000000000001416107567000165515ustar00rootroot00000000000000django-import-export-2.7.1/docs/Makefile000066400000000000000000000127651416107567000202240ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-shop-discounts.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-shop-discounts.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-shop-discounts" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-shop-discounts" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-import-export-2.7.1/docs/_static/000077500000000000000000000000001416107567000201775ustar00rootroot00000000000000django-import-export-2.7.1/docs/_static/images/000077500000000000000000000000001416107567000214445ustar00rootroot00000000000000django-import-export-2.7.1/docs/_static/images/django-import-export-action.png000066400000000000000000001123221416107567000275170ustar00rootroot00000000000000PNG  IHDRbbKGD pHYs  tIME U IDATx{|W2d&3${ 0T-#xax*Iw-QGwiw([-Њ r& fJHB$sI~$B[࡙~> Y =fffffffffffffQ`Q`Q`+R_1Ss>*f&?߼;O*taB#,Qĝ&;yʼn:"||A _q +p a6^+t-ܜSGZk1 ȗ00W3,t?C) ܶȉgsn {o_~5Ğ]s%1 ,jaȞb?A51*xcX`u$"""""^`) @qlioe=~x#m,""""""r9M<{2[xd+x-ek^=2;bi77MTzgUdsz;{ηAFvkRGu]Oi!uܘa0PW7C) ;޺,OۿapKH>2SذfM7w?NJ)$@_p|^O/{wfKрͩcA|6#0SV:8)C5ZY)mDIl;IUP_""0?GNdTV:SR2ouĎ6RW ow>sX]/5sߪdfs!ev볌`@2^,-KD vd #pfF tC9<ڼ2^t^`ضJ-5ZxplDU*'ҙN~z#MIl)$k/Ŗ(}|&jY2صց־J !̺w>[t|r[F)dx?ȎPڿyffݻEGjixpp_^<Ǯ׬$uϯ,5&wH`}#s!̉`.:̖cYw{x(W0ؓȱ4_@#'9z[ogP;̋Nψ9 + (Rf-fb{Fw?0~ω󯷲;{-tݢ|~s 3^Ŧϧ<,O>0ťaE:>s{th.&-kϥ HԅqnͯƋ5w1avbKnߺ\>S6N,z6Va=݅6ݐMs Cˈ=pϠ#ob!yqOs)9lX|'r\/Fx}вStNO6V: `@N*㣋SϻjK))~qdC'kU̽Qf>,Lfsu8c[-,ϧt9"fG3+Na1NdVsےb Y|ԅas4~{$Z ,,9dK^#Sȶ꺺IHhariq',_ζY )PV2g'yJ夰gI~;))Ė&acbYCF\X¿%MESGadOǬֱ>L.' ۡ.7P %"<dﻞ/!r:mt'cZ=w~4>[>6̷FJ23Bi0JA'Fs+/ E+Sq+ɾ`uOƦ^HI~zYp܆N;+B8X))%n*i9v 4ZXe :+鹮7p'v6fWS9h6Y$cMㅲq}-_eYU툇'Q3⎹[ˎ݃Cc/w L cG“XdiǜHD^{.hɬ vv_Yfz}o_ع'N#=ygF@# o0o=-MI_eìZr:z}E{99Ma gqgM!X # ıyX֮Jo// jx2w| lq`jPe I7Ye(@Dj:-DϊTs>l[i * ugUd[{ )yzd_?q%+<Kxb$5'ټozr[}{..G:޻W d:h..+xe|%dbϸhŷc~> zݕHZ.g[㧝a}J7y9hgy'YG¼g"V+q3x?ߧ_M+Y\|]HEŞ֩l9(E;?9[*~\GkHI]8ZࣥE<>,Og ҍ#0ZG*Z/1&{aX|c>UEYK⹚PHU :_q}iDğ㩅t}@=y(;KK)evY:2qH9P_كN6q##Vcb)puʧt-Fu)a[X<s$~֚IP2/He^ \8 |;3j9C5wGyҀ940I64c#yyk2v]ћaaOa;G먏+HF3+8

A0_3Oqfgغ'02|YÉNCtY Nj:I d2&ļۧw".[`1cz̳W'8mib_ %:4]U✴r=21dbVp]D%!)GuS|ډl>a1+Nq:i3+E-#v3[{2ԞlaĀuC N;(i8Ymh{X" -?98 dxB9q&.%Nw,q{ʘN_$3t6RPϥԽ YSecnbvy::DMp"S ̎?Wxf6 s>c[e]^C|tFu98—myLpYEC"xri Gxb68Zhut&]-,Nq~KV;5AqD 7Fh7yv'qq)\z3La])l ᶔzXl^X8~K_-F;ި ,MtNOi"`4BUyipX?&LȘ.fDS9IGC[#It2;?zU7G=<1;_ 131'!=Ys㝒m Vh>6:a}VoΏ$Gbs[bm )wZv};LAY:JPyk5”l \@Áyb;𷐘Gt$|Ղ"+Yy$9K;*C<7֔lcK8pٞ}?(;:c KN6I~8p7Ϯbsh!woL) [1;k(dcM;x0ξ׏QGV:TT@D"zCk yb)XgeΓt&$6OjĞwUv-iKg #1)_rZ*98F__ܛ9oLkgj/K\^{qG(!KHRDT'f)l #mQ8am58 +~_Ȩ8,]@7fЁ),f9眾+MV2;пHBf&;_뼔Fw#eI [O)}`_sԞV)~a1+f'-<Fb܄;REKN~y;OߚȦ $.ccϮKTݽ|v+!ps~`sG>qV|-#b[$Kv;^E0$VlޑKj7٦Ž`Jv7u}?M;pWNL3_9_]nQSXt踔 \|!`s%o!HD_0Wik8ܻ?ϻX;3N.~Y'I_e  !?#Ѓ̀|^3 mTDǓjt74udN('*>bgqLqOdI\|7or re|A(. tA'Z0dDa,ʴ LJ$ M;h&y9eLbɢt66 ٧-Fû![ۨ J\0IW\:8’GA6YodCqYf6ct7ঌ[<<[lYQFβۓ'GZs[)i9N~iQvli!d  PvɃ/Nr>P 3n]PE˥oi":2\|gk! -acbŲrn4D!WH`{.)tut#ta@25ػL_&F :{ʊj\ KX'K+,mQ `e4@G&,88#""I D;߭M̻=J8q}.0Mb}1nSփ]Ay>B1Zضf*+L㑿=[Ӽ0uEup$~䣶sxڠJŲ ;s~~:܂HcV$xu8Y-9æ:Dnt e_J?(gw+t@ EaܝufK"Zβͧ4j8Uq>(."zGu l-XĆ'Rv?ʽӐ'Hma^,19b6<0<&8zS)ns]N~9xyNW X?:x]*D+xw4[1龴AB$C γDߖLDqsX,;[)YӉ=GI]uN.yÁ|fikhn ڸR aogKa^teJJesluVuQq-  $O'0^#Y:-ꎥph/EW<8 y5㣝nxGu5ȶ.G.c[]I B`eob(ʿ,`Ma6?WHh hБq:PQG}FMWx"W`9I~y~=ڝwF|#8;]1ڙ=Td(_N+O>?php z:)VuCC]xv y<^|gNr~@n'Z<+.btuB?{Twl'k]SY^=hCmkCj)ŚEe!ڧ-~c {{V8qeQ yF/vah/ߚ#|u/ιm r*?Y3xߥ^eC:|os}a9߶o6Ƭ)W Gs/gyqQq^y?6Ms GDz:*yѯa}6w:tu}_dkf{)bG8gŒ IXEDDDDpuO6w?hMK&NN}8z~7<^b ,h|f [=u`{Y DP ǯcKT1VO$4 Vi$-&}e4^R̽≼SN~SX1OI/@FoX*e0sz-ڐءhaíuD8Dp~쏐)70S~bȞJ3>SX:Ci3n5d|Ti 6̓h{5xH'?YX¿EFq4ew8&7_'""_^_:/6s_z2Զr*0*xj 5MЁT^sZ/L~9|: z?JÈW S/v;k|ga +p'DW2o`͚᣿lϱ&z<JJ'X,%q[l͢j^dTH&ȝ!s7w}!h 3ͩ㞛魣;SVGU*}dCvL0緙,W#lc:{`E&.,c񲽌5_-'؆R^xڷm3elD~9Q9Ͻz#dɣ/|Onɒ_j+9I+/? Xtp5uk uFe?!sdx6TEDDDDDR_)? +mz6k䧷y.jnnQύPLWPDDDDDDfޥd뿙?_q.{*,Ne]f߽{F*cS""""""W/EDDDDDDD>_FDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDEDDDDDD.¸`Ղ4,"""""","""""","""""","""""","""""","""""","""""","""""","""""","""""","""""","""""",""""""%e"e^W?}"2Zʏ4k6✩2тvr~ss699))1G0NjO>""""""a|jʲ,"~~ɾF$ӟ|!fMK$YVz>:%Y66$3/$ʳ%OfaT=͡*[gqGz$󎐇M:KtOeY c\DDDDDD;g$2FvQVZ“2OɜMK]%u9)̊2hf߱ Y6{ 0Yo(HJa#WϠǻp:kȳ!)u*wg11.>i~sG 4e↖jxf7}g4]2!* PWߎߎX- wQq |oTÁs]t3mr,ٍ:HN'3ʄ;@} b">[E0GfLPvPRHUWڸif ]n ;xw4wܐ@,@h8 \-679\Ķ,O vT!IFy n \CY:㧲nv ?`1o?/e)dƪjj0E0-J؈ h.' ^˼$3:wQadjj,I5{KOW^uݘHa]pSL_6!D$3+*zE!LJùZ?@UYią;a]thDoC$Ch609޲q;l9"""""2nW}r&OM&'nvG5-*.9=34嘳.gVmWI'+(AV["DwSpA8:[iCLz<5Şч,Gl"==mE#me$g1/}OFY]%oN2ct$2Z4]kG :Z<|.Hg/0փ]Kw%V+vZ4Y%Sy.>i͓ch(i+74Pn"[hop`T$O>8%.15$j+X2q>Ɛn&Sh o41AWhCHڇN!oh"8FMk𠗳nR't; .nθEDDDD .*[Cn+9O"|9qnW$YBin]`< !vdMs.ڻ#`$3x,fr@0/%DLJ$;:@y9>u!,[2lce T Zx se[NSw±Dh[EW[>i'!:&0'ASu }m5zJgF?DH I a2h ;^*[jV:JwS#VYWoF2#CrW7H-F-HVPtq݅.B;I3Bk(AW+Nwߨ!t`G',i0i`fLuF^*:zI9HmstnB˜2))f?u|\V͇=ٙ`;0CcUΠ L[FOSsCzxIH\_6ɫx#0nY8I}˺a:]|in8MܓTƢ.>*m30;3')>J]-{L[R'q%@Uݸ=# 31Y6‡n`a1NcI]frR<_uGy[fF[<etR <&LG8d8 vG&wFKoc{z0DJ&9" $[Cۣϵ{[f3{߾R{/*uБO̞ʲEYWk3'Z\lYn_7g*,b9ur䨓?g\@OMcȇ1.|tEIi-Ӓ^);߹Ɉ 뤼 wJ$a}WGxjZgK!sZ롴\=E8O';@}^[ɐ82'%29 O7N7aMK A|7bIu0s =x.NV/2BQɟ\5%-41ܒnYEq\9FUii&<?]AxCeϑ%"6UXf6s(H7O.,m쩱;)*oC5,"""""Wd TU """""","""""","""""","""""","""""","""""","""""","""""","""""",""""""rM Q5 ffffffffffffffQ`Q`Q`Q`Q`1 DDDDD䋤3gP]]vR6 ĉ2e 111WuyCzzzzl"""""r&//U5"))ٳg3qDfq:kTzz:zȕͥIqKJJbٲe櫦L藈\8jjjطoUU/W`n{I63ʕ/{Kʮ<'ϳ^=`oCgsS%}?wWs[oF2s)Ӧb}f3/JD"""""_.MMMu5b͚5l6\.;vjjjhjjߧ* ~jObJ_Euiܱ,#h+XA/3g\ 17)ٵ}&q}+0wy?/"eC<~c1&vBC?k4%-OSI|_s-?pLz;bbI|y?%A{?%OH8V+ygBGD-/.e'dc ?}~7gop&dcjÝY~ k佟>NBm|D+"?onb 'yv1y*߽מ9?>4{b0q2U~Z """"Wuy98}4Ӊ9^OH#*xiKg.U1 ^ ;My{Fx}iR1̈oy$)̌wsy|fIɩ 3M0ƊK}qWRgc>&.V\6hrRRҐ6t|9)LV"rX+ݼ疌X&3YFq'.^Uy9/rHbƒpgP{wEs)pX+&ęKy mc_&/~MeD&4nY0+- NbFZ>H+rzme クʬش2bL$\w7=HZ٫lRMEjzϝIZZ"&ZF}NB{xO,.}Om׾7´vKaÏ:/E"GKrf&،[X5$dЫhߕCJkd"3?ȃ =+Kc37%S_zh=YmGxdhue$wM#j%26[=bk{m^}Ü/6q&K?rj^N_ F3HS4cAW>sVå>ob z*>|x(r fkײvZ[H6l`'|X;:mF28\xaD _Ĵ uծP=S>̛ͼI =@0 V\ogddW-î0H\?S=xu37Ǔ?}.L_Kç^Gfqc"xl>;{IӦ.rhn,Py$K EDh/l6%hl}OF$ϋ!++PtM ,]rAL$ZLm(`y$h#sX8|ڄˇn`y ^qĔòײ6ccY*N| xdLgb%mn"-xk9p,)H@,x=Ml 7XXc0 0J oO@ sߚM[8DDDDFedk0,;&1vK͵m$Ii~ycw|_` . Cewvt_8~>Ku/osn!8. dhWg'6q?ډl;'O6cƌ IDATUp&nP`a6Rlwxg_Msqyqa0o塱uonfommi˘) ߛ94Ljp-éB!8OW@\2oS(hM`f-;ۇy\x0i:MH\u8:kƀkWR.0GPPVOm=eQȼo r@B@0xpvfs 0pz<Ԗ䑷f-'=+6@geXޖxxz'U!cC[0]S]ji 8uqx\<DNCM ?6|> }^ ?>|:M:DDDDFqƝ}>ڍ٩:y, ;,9Qg 3`iyl,]t::?T9gy6__6o?NF6jbMRo#9K\u(qKK M/K@'ٴi/IG['!҄gժ|jUD~i;8܀1c4\b&`9} aXL]O[j)ò[JN-,w?Hכ| SasRc71Oi;שд-X= #S=wlDk3{[` o)y 7FҘv-s-%L5/DŽ1-=AN5;XH=]b$6Ok q]R{Vl]sADDDd$Zg[M8xR0+{~+y{zgOY8 ?5Ik{ձkZv@`+CeSj)dOwq\uQ3Imd My&چ¦0/d=gKA$ d '?m/ֽ˧gp=yP|O9NFR=eV6\"##OE\ׯ^;UyhCy=)\^+#WGGuuug}Or||>oD{zJͧM=ML2oL=cO=X-909i/"wr~RfOI:voWXWC1\kKc}L8!uy-yo?!v3kn`&)z s{lQm{(yJvG^ˬE,= OQir#mm>uAޞ6jײm)z s.Y귭pf>mvuz)y9sOjCOkC:eE,^8zkZnOx!z={i7ka{6ԲmZo}|{ۢũ$ gm(}e-%O9["Ks9PS6,{L=ԬzUcә=}7b_)IRYh§x%Om =ÁyCwjq4bc5kv{L,Sse?~%{{.LeeeݻW 1L4f տS?͂!s/ůWx&{%r?Q^ѣjQ"&& \C"gqI] L ٽӵSTc(#~rayFTuXdedKS&7&5Acc#͸n5JFp ayuL;:_ކ@M|)ӮKDDD|ƌCxx8&I"墹cǎ1a„QjVW|t#L8QSfϡCh7SBCC.n/;Z[[G%"""d03qƩ1dT3L}_566s_e̙n̜9???SJLLL_P|-4~l20W%"""&aGUpWssѣG8q⨮ =͘{yzl痈(0_1nad2kn",<<|vzz^~.D8>ݶFs!""rQY?? AZDDD\YDD.z`>B}sYX'kFὠ{n5+{xgQwzV/j:EDDyҸy5+Tz9m'ꇟ?*F$]j9)N/^0"j;X=ןqZz VXH9l!2˖r[IsW̫k†F_Ə<.\$`Hå/DYzQìQ>;wZ`Ro rz8zFr1=bl5^XK-w/J8"[k]eOdYw#a\'=nKSTVG7iƸ 8<{r"w'G@b|~ A累s qw-nk M lXy8xqAȤqO5[\/_ڑy:첛N DDD2^jCʰ>U{qHf$t:˫F:B\RRR(//?dܱw-#:iGk•8U`v/{&b:wcpOYsn =}k`>~h?æ1?=y61;6qk)_KqN4[0sjUV:Vivs)eu:4v2ކ5gهÅs|=fAqppa $v6:k4MG|bn8vN[ d|\;`֛\.!v 1LQpZVrE5d?)9WsȂ9{(Ye`?a=v6S+Vp oV^𛅬,~Wimd͊?QVbE!#{z^A+ yl^6nf[i8֮+kp:]]]tuu0w7/'eo݈%Ї/dShoyαJ^nOJ")k,&>uN߻,Mz22tj<%+)D֖~ܶ lRsXo{ljc{Ҳ}Xw}}ެ:jlnaӖ#} {-TI#wσoe\ K GU3QI]{=6Rq= =ȲYRX.c~b`<|+v:g)2\Aƍ|,V&tA);Co^¡F X jpiCP̢w2qlWBf?Qbj$QX "u{9ye6rȿkj~=H]s i'wy; H_ZYY4HϠ'䧑$h'[ HyA0#Ilfgqt2 7@FNx")]MʢBO,l*@}iƖƝeS_Rv߀,*mz )~EYm\Bfjr1YOp䧳E'ix%JrXs?1?T,&XҔz8XX;lfe. {B<EIIրlRi SRRHNN=99f0ȸTAOùe/N:D:/Dw y6wk9Ckl;ԥ<el9ک-Dz^ rmGޥ #鋗iW u DL'|u!SX跸9‡H/4}߼w^>?Wd'9??T2`3 C̞=*.DW tz8AQL[ũVp@FI!9|Cq!f38f$c1 mivO Isp4',.!٭YCr*ډu3`]0 JJgL^s {xbbv&/Spno3ٌxzf(*Eزw(VKѵ=e ?wnSEnI%ݫ瓟{-wYA,-(>3sM!GIϲaGM A Ғ1Cab癇Zrw6bEo&&-Xʭv1Ύ#j6h^5rǙ_=3Ls!ĄlBIY`L Mxワ{qorB追T]|/;֭o>/-;㇁ z=lzNbh,s?IV6X˛: ͷ&ێ:o |W&_{_klj}88B&]tsj.:`N; NJf頩KEthpmm<}}nu¥lZo` !Ķ{m'* s njKwhLLtIM3v u[_1*jI4D|i;R38qg t`F'rq8gؽEs1Su#>;e~ Ae';Ť F VϩhawJY픙ݡs4XxaG>yw}a1lҌUVEzaI+9bH{2 cJn5lي]Jnҍ[i_GTyʿ :ܒj碥#sZ?k)]1Qޠ W}5ǻzoW]фp.Lix.^|~N$dk%;IT:bH{һpScߧ|KG؈puGse(/;BINO%!@P_p7c>;gM& mG6ʼn不]ό0wT&6cF?:KdP ab]Wrxm jna""|x+{<әm~Sh%8Ƽ[i?jJ7lŐz?B  ؄=$rDBwu_8c,c0uČC:̣s@٩/#n#'' sH'rr)KK$1 `=]V TPēt'NERS#,$Zhq81'&m@z7g Aiap;C~91t=$FCIwzowP[t;%%)=| IDATSvxHSN3cxҋz뮡0wVY9efeeN^̼P}:uWXV>Z91а͇ Ώ'n@X3AxiuMtt5_׵5qy`ƚxg">ObL< Gt#W>q//~L f0ŦW}Bb1?u[J㥥lXgdΉHMlYr 0lYYsS0|I Y$No1C4ۉ2k(z24 Β"jOFY "'D(NAIN瑬,R sH2;PGAzvn"ϖOl ,䜦i9DߑCft+8((tU-=&;?l\rˁ?ԾXaۋ(s$X_BnV"3KHIO! ϋeOIIN.1鼘ݞV'%9|;if8]`HRV:;Vqpdugs䔁 v ˎhY8Jyi)/ a3 λ+/ʜlzeVN#. z{Z1ۛ^eíKI>T9.h`b f8'w;uuK|D?^ɾ |t\,[ݾ |曾N׀Wm%~_sw˜C݉Ej0|D/|J};_XS}Peo| a}{ynoG_/ e?᧾u&h%/4UFW%tOlVun)stþgN%5zP|A wYkm/A_WjzM-yb/0߲۞A=ea߫ծ}0_ز-ڿDm=1/SyO6]2ʖ.cx?{ys)Ƕ>s_cccOSSӐ?G=LvF%"""~hǎ̘1Je`:;hxc;kȷGoMɬ!'qt ۷oWߙ5tM7mll='2_/i>tШ=DDD D.7 k87љd$kXv㰗QϞHT˅5ؐ O܇d(0XF˅dRci򭓚p"K|}#**w`0 zꢣ8DDD>y蠱fnDF%Hxx8QQQҰꐏW+|$X|w/ޟI~L;K6/Q`r?`bL0Q[Num0i{.\F4$[D"殮.&XLq{iNꪫ[+,*5ȅ ò=N`"""","rޟ+ay_"""" ""rp+=0lf9J WrEDDDYDD2<"""" ""Wt8,8^=C/""",""""""2z577먈ei؝}?WӉ`ꪫO'p恌F 5߼ᮻw}_c&lf/a,"""@Ddbx] zT c|毁/mXQ`P#cuqMDDDbѤ_""""""" """"""" """"""" """"""" """""""e;Kvgg'UUU=zA1 3m4 \YYUW]ERRҐ}'TVVbtEDDDDDd榦&̙rxl٢#-"GGRYDDDGE{{"v`x</{UK9oY1gBG[k\y _#׼ [Ox8"I~0-V.e]Z\7]1kJml:~yr5u?0:DDDDDDYѱ]3{M Wrx(4V,{v=U `MPAڽDoĴ{?)⥼7l8x s]4QO>I]~/=լ_]ʟ_ 4ofIb5HSXQ`>\S>BE̶@]g^ h%4@PZ~ֆ,&0#J5Dx+1sy9̽b \ ąVBĂX<;5}[)oHev6'4;M> qQ2%,g#Aqf9'#;G6\շh40 ͟E0tgQ6gz~xGqcc):>z=3/Dפ(V_JO\e拭g8vD*L5#\Йt,40ko˓S!PfΞ2!qQ2ϔvE0pe E))8rgR1g/ +ˏR~OYpt'=d\cbZ^A|ṽi/J$&O+W*hZkqdr+ wKGٹ}ώy?惚Gѷ<[+h+}cgsK_WvO<;Egu#-,]$+I6fc)~ XIϚNKI!=m/JYEDDDe;$;!!mFWWנ ˜7 r^GG_|^w !!!hE|tuuIgg'^7Cӆ|$ |'6Wa>uy}I)_yݤAػ$+IANb<eU|w`0`0ꪫO'5βt $<SrQ鿨'h';בI!KQ3\PjBfa9d(0L=:4FyEXKF,"2RX(6D!YdĖ㋤$GQ̬Aµ˶;wDgg """馛 t4 )؋)*."gΓ,YGEQ:5G}IIIC'Gq=KdfɯQXyYvl'Gf'[–ENF:祓eU;\(F|CԤ#-"#\ 1/<Ԝ2_deWlB;a${YDDDDr>+萶^?~:E.,rv% yLߕKVl+ (ȸɓ &_YDDDD.vH㡽}^Pu[`7GN~<*?W5!Z&ftȹf@b6v_vR 922 n"""" #1%k`HQTرd(0`@mN?y%?=r^SUL$xm^Nby(3o |QsnqSx$Ͻu#^3L'E'%|D:H@tbOc2_8G?¿ooN|ɡW>v=M O^;,e/dʗ緯UC.7qCqCnAj&f&HE (V~l8C q7TTMÑF&^O:K[r|O$"tvy}χVr $7@.TYʪ0O_OE Oo ʿ7ww%] 寯F3-<~g WXhwmn? 4}5KoP'mSXHmM}m?GoU8yVq/ xbEDDDDPh_BX 2 fF~^rWM@Qf"pr58wC\V& LtULOImN/w{uM<4+N6WP[?۫DU-u7m,M Ĵ`]tXNWnA[u|-י -g,DH-l{]ۛo= ίtvtUlp[xfDB< b[x&o#LO[C>Pjf :/EDDDD/`80'i=Chy<:2ⷑ̉,6qwJvQVfwgf:t<& mu7 g֏V/Y@tTU!3ᗻq8.K~n gc3£LG煛C0118AwD _7j/֩ ՋZDDDDDξ}r}qrd}}^}=Yd EﲮD7_YwxKu&Njs A;-xAsqQWLLHY;0xzACwt0@?u] _pϼo䈽Xn!h %h [Y>s*)!k} DuRh[X߯: L.'s|71z3+1QI 1W(0GDD0sL^ BB.Ry`f:~{&_Ǒ˲<6Ů+rQ$\ob ocܬΨ˘nDwvK%(Fð+0᪭p%N8R*Loպ]mj"]DP(&*uZL^cN*8).xjp:?|$3BBK8y$K<b>o 8|uƫ/2,/M_7s,|?WsOeCux/6T凵S#.X`SWT^7?4q_7×~Ϲe_J1dL nItk$I't:]~}R8[ok}=֗^|*g L=V:;;Kaff)lϦWU먍F-]3]{룷^  "Ig(^)z~},O??,I$`Vך_|d™n;?ρScK$I:zA#)gg?-/oGnG__Ggvf?p#?ĽZ\HV9M8hWD0rnRN A@$hv$I fI[cdlDݠy(s!PteY|:5J]nG$I%ْ(D ņ!FaO۞צYkptiL*N,P*@]mG$I{%iˇ׷Hכ -8XL@'Kj;$I2%[ǏEnh63f* _wLݎ$I fIX}mAکREu4pM;Bn#Ic%iQ3}0OR.;J:[WT5<4R#%kQ-8:  9eIw=̒49_Yhּ҅2B8! d`,s+WDлَ$I~.yV$uNCavv'شs-%/IDAT.흿Ǯ-In]|ſׯd̒΅`^pbzK,̒$`>THXcy.?oF{K0;h$Ic%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%Isԇ<Ӑ$I2%I$I2%I֢\ʇsՒ0jh(HF M1\imjF7)&B -*Id0Kҹ!Lr_X˿΃M!htr?=;)SԞ;^ dؘͧ]0ly3.v#?ȗ^TgK ]Gm)?yon}_N/n {l|ϮgO(;%KYٵgxS?%om䮇?O|7W$IyqүCۿXc<ϳ[1$>yÏ>ƣ`gj/M_YR./&#Oj$W{JMcͥG}#Q{"ɽ<ȷymh#_{1v*[]sk>dhZ><{|jN_5Ʋ$I$gM%׭H?+dRVdS,g`j_:ox}x8CSKXw2a-n ֯[`?+>5S*G/[u+" *^{wc)|%n/K$I_=5Is,w ]ڡc<O?I7a4|ci^ɷIhKOe\4X? 00> ֮M )>~i\$I$ 26dAC%`7rĦ ?ljAk~KOq`L Sn0oL\OoçZW|Ul;905U$I$΃%7,bs}|o#lqx>.0j/ $S@Gc`ɮw1A?kVq/܋M&&طfx|{bbJ?nӞ|{`kW^brh-$I$΋dGR[x)p!~ÝŋLv61 y>jYZ澕fffu?6^y'wq^zzzoAA$I]-S4k/!&&Ծ_!3}lZUkYa,K:X a,$I }yg4]r7u3y-Xzw_AÕ$I${ allƑJ$IK%ihU A!-R[ܺQ&Xj-/o׋d1҅*-*Id0KRwkR$b-=y~helx6ͬfo=2\c8rtB})Y$L$[z¶wVTHk{a~0J6⌍\07Hd,1$I:SaEg/1R}Xum* b/ -@8IkmOeN,|$I fI:QBC{BDJ2é0 +# 59%Id0KR EA~"p[ b$!XNj4hΏR 'G)ת|5BY$`2|cr9:"waQŲdVg,?FզU/1RxeAq}LxN\܀h"JJ$O%IBlyV.GL0Ț63^L)f&UŅo&s/P4ܴr.\΍g(墎V$ N$w3tvvv6334ٴs~?{o}}}3$I*.ɖ$I$`$I$`$I$-~9rĿI]t_OZ󷞞 eItF.by1 D4I:|,OOOIz{{ ~IlI$I fI$I fI$I fIjh@Pw$ID#E %3D,,$I fI a !Ih$[:X@4_ @ph5#F[UF3qA@DHdnI"dFA@8Xo;fI$Y[!PteY|:5JxL3IX3s{!Z@2[5\l-7xNCϲ![h%I fI^mG&Ȥi 2qB"m!#K+BƉ$FFHEÄ" kVƝ̒$I$u\B% qQg/1R+±17N]ġp%I fIn A ]F,SvI$I,I<å6tBqnIVР`}u^Ad4$I/+%INPC#5¡Qұb b e6r$$UFr߅[#g$I,IX0 i\}Gapm!I_j;%I fIn'R8uBD}-Y$`$I$`FH$̒S&s4 Tk4]b-Id0K ܹ}KS,^$I﹠t$n澕taffi [i r! 9 HfI$I fI$I fI$I fIZ N>N7}GO%IU# ϡm/;?ܽԯ_$V̿oY$u9)wrɒ$`$-(;[brc===v](."t{fId0K N=>Oޓ~^fI0Kٳp YcS4υΒ$[Yރ[=7>yc$I]o0KurS(wSɑl4K$YNa|.3%I,I,I,Is<0K$̒$#S$<$I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I2%I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I,I$I fI$I fI$I fI$I fI$I fI$I_ IDATx{\΅ی0"7E4A*MԤj65m_Mnf=g&9'kc[Y=[cSm6Dkh0+p 6 `R~>}4|/W YW#7f@DDDDDDd$(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" X\>x]QdJi~{{O^`.{̏ϲ0Dn#F"""""ҋqDfhe#o HMܢޱG4&<<[,wːyGrc>73%) x܍,/dא;MdDHDDDDDlR3Ndq3|#;{ݿ89R(L5}#)OY#v+u,4@`k<l#\'*K@|l͘zy  """""""w4ݓSKt3Ey^xOwVpt4ov=3TdhYenv3B)>)G"޸3djpg 0N&{Sd%S mX+܂l '[Ʀ>SOIOȺKdu ,߸~廪aֵc/f.Ԉ+3R^{( )onჷ3ZwV3/Qn3s + 3}7OOOykG>ƷIzV.q Bc8=įjf~}bOjȘK،jjr8/~_䟾s-ϥZi18y$ɦVSƊ)n,Xot)w^dEJ Wc賂<^K=?{]?O+:g?KQ:]ü6,FgN (SLP#;4a~ ;pr'(*nĴl?rwfγΟ–26D `JLbb'hfʊ+غNϟOc> ^.hJLͿN!Vy7Ff0U[/AF&f[M4̇dV$=2,K`(鮞b_ӪXTu3Xݕ̹Tǥ%=ٵufcuu<dz?wex}Cqeˉz@y}<[8Wg3X#z9I+z:({KdT6x9; $\ȆNjE0xٔ;Nv-'/O8zmU̿QYLS|^ww_ bظ"q^s4d,L骧*-{˥n s#q"Ooz׫z}c 8Ö=6gy1BPǮqifcwD֮tS[ǿU]qY7fq~^2b6pta]ߠ-ewsiyvgl6.ISl9t{Q_W)q6w<Ky~%2/bdOxh̡쪺y'7%+ Kf 5[E<m擂$6WB&;+-d ~/S3-Jf VeTr" |LT͏Hk,9gLfڄ)_/)~JN`[n$5m7b9^IibT90TW/+1J'|xIJo) Vf?IJV+rʄ7,\XD1j7h)I`[A-6ҫB3ٔ6&=,]ZV><5\`:wv Ova{vl|BAd\gu y㉔: #ܑQ : x76H'xjHLg1?P눠+ZCDT>71HJs8j/,D]9jx3%Ͽ 5yP21DF0i"zvډOM4g[Q-xIZ_9NFwODDtH"sh@F.Ycfl"&Dtywv^/5lXjEm3o< ɓx*ߺD#1ƪ3hRU!Ӕc;e{>uN qpdF0.3i{C-g6ogTR޳eaKiG043!֒9*|0h* 9,e'w5;vbX5)u.KdAe̙ D҂i鐿Jap7xcogXYf^v_5@d}<{$H|T:KAK&΄)lY͖P`?[mxppgf ' Ӈ46κ'dz~u*gˁF Vqf/vSH=EDA473SUW8)߷W=ά|w.`Nx3F&/)zg,Y5#{y8-Uy~wGsVbu t^S/[O?Kow,vIΰq) 6 +l|ko6L_2RkH}# ,c~cܴ'R̋k#U#:7{l AUVrZ6dǓielfx^~/Ϻ{hJUwVeOᳮMVZzTx^E`񙘎kg0%xj |a/w{@r|5*xt^VO̬r=yU/r6 [Q||9w:7Q<{jHvLd/(nK Kl|^`trF!),hgTИ{;wDd{!"oxb4/WT4)m>KȸheZ|loJίz](ݓ88+_-h5 ""_b޺fʬdlc-l,H !!C/2YYfckiO8m, i); ?&uU.e 3c`03wq`òRwo+E&JZzVVӀdKfβ>檉zq{˽XL'2u: Su ɥ*̋c̽gB* eyJFX)'} ā' f8wѾSLs_*l L9#{;-\dp69(R27^w X^=}FMiL+Oy)NVk#=0wɘ5N`[4cuE<2)=.0F<gZOa%%vԻ4w&#"'EY-, qC71=AF='/rR 3G1:xzr w-|Nl4ٱ IxB Sý M?'SwhxΥ+apG Ǔ umc˘'s{7UM&aߑeYl7jhx^ F > ??As\#AaKiFrs'` 2thE)C[[1 #Zjc(hp~-6x^=f>ɺٮ~);AJpG^L$"2(2l9!q|Zʤ0LfZu0M̽oO?|O?| ͘z_laO/`@Mk!l66pb;Xb^0;\OO /!3Iվ2b eH';KaդLq=CޠeSt 83,%`eg88 #왡- 2xp}<}<֦v[w2d& LJGYy>`&[":s ecL]Pfb$a2ӫ?S3lTĀ 5tL+s3},n(2s2mw_`4iFBcA{O̱Y묵VS&Dvl^\l侏U~Β8g֦18_PB0NjnhhGp/eU󍩗b~xao9ډ+zX剌nLMW y~3Mpot+6padub#5k 4+_8<9)9zgc9\2*#CMV̮}LzهUTT!E{5$Lv&"Et܇ÂVV]ܝ֎^w}xXgp̈́r[q:!>,{oAB*TUsrIٖjfNuf&[ &ư,^K)f{qtO)yX9)˿IA{>V_Km_Œ:\jQW} il"' &1rQ=?N[ gd0qTcgLuIΘΤqm+3'AQr;;*>^\3HHYZMcEt~:Oȣ|vF`&j$ 8c{p6|la=Yb#4 (ձm'nF8ic_Hh|`?V?Sۉ4m{YLKݺ<3gD^ݓ_b/v;8B@v UC'Nev] ~";H99g&?Km%^zOiSK??fd(C ?efO7 ̘n,^kkl\َzdN}3fcYscKX+s2n[Gp>d|m)4$&q*c( 2LL`>ggvp0fLS6͒i6<&(;~N=7NgίšNъdG)l*w)_=a ~|ٳߌ96g֦(`k+l&>- /0$"JysO,Xɲy/mw+zN;Τ_Gww}u]ܓU+Qks~u>l}yr(s3=yc({غu+YzMqv=j}c.Wl\%8[̅tnҴZR AYx aD6>{0<\&+}F5  Uk-'~Puoچh̉p2dxjpd|F# x^jHT1k|$%{Eu8Qc5JW8;'_lV;v tgsKx!7-bN/sh!/,5A|mx;z}«/l>g-O<=[S|&}HzE,&p^ܳOhohԜɺ}>U7:C9kًygw.o>g}?AF ~|?Ϫcͻ}שw}9YO"vf ZC͉xuy<ՙUVA:/݋=5~w^}Os{(} q CVKy/13G}]Cv}chnJ"2Y.qOyj3tLmq_޸(JE1:w'FZb(#AtLf^D_- uZˀ7[oC0St?~, Psb(Cdk IDATW?ϴc@'?ȸú$}Qܟxga_0/5oy/ Fy\p/pMїxz96^ak˱f뗝'4ݖ|,_Kzc2fD ˔l0 ofF,xg3wW.xRy515l̸ ̈́i|3g; }R%1S-.w>y<(jhc^{{#ỳvF=`A&1 rˊ%edaS2ln%=.`NdNk8Ϳm»y,|ŻV&S*xOSxoyAsGfY5A$>Oy]SZJ"J"icozRQlMOƺ+Hݔ"oM0Z^IrpGW|PMrN^ӓPz֊'kx!*+x:}f 4IcCM 6dĽ5@c9q&7 )k̦ݑ<1 "-NX*&r~EEEeUaYtX+l:1AyE=T-20PS{{r\i#ߛ\A:^_kxz,F> k6܃)ñDDDDDD䆺7egc:7uWeab5gt;g6<}U,"""""r+2%""""""rmn{pEDDDDDD """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*͛w """"""iWDDDDDDF\pEDDDDDdTPQAWDDDDDDF\pEDDDDDdTPQAWDDDDDDF\pEDDDDDdTPQAWDDDDDDFmieVŚ _Y4?S?9b;NB,̞¢ g9~}5N71j&>BkK  T]==1c]|yGb""""""7`Lg(rDd5w:aO*.!l-u P&Maf*8?ITDDDDDpX&3?psGZ,otc7iQVB[vCQE(I U WDDDDDnJ %ЬdXoQ،KLL|zKƔhlvk3Nffo%8˱Je2?>.\ོKԷT-Z۰۫ȵvކ8i f2! >ϳߘ0;;+[4@ 1wSiaf 6*.qo28e"wDYq U>#ca %l x-Տh/?d*t22SZ+C:㷅H2Luہ?\&,~2ߛGÁ}+g<0ϻE'>ZȶF'ƟL C-QGcGy2vu)<>+_PTsnl!kXl+.'+bBږc$G21K:ګɿ<) d !e T |\,c ԕWz*C"9ؾ#>k&'%fvgUqel}Z% %ۀJd4_@U+r✛6bM4YX4ٟ˷'Rڵ[ڮ[IkӁ&e"--m#KIdnXrN)j0R\>|^Pͱ:Kh)?S[y@L|$aMjh50>ãJR(*N;@FRqWQp+ܙ,"""""2]Y>q8)2Xx4Q՝TմAR&:n+8ֳVo; cDV\0>S醱39:ȸ6ʘPXwZM,c:Գanq{@!%BX1!J> !Ն{^Wj&4)&+wMh5mA 뛩;n7QhAYw ."""""rcxy(otzMu Ɖgg-HFk\ck \w[;X"` a|dZuᵎU|sIX#c\FqrqVe2vwQPILDn˜EH&DGpԱ\ϱ Ξ,"""""r{p4{!BbXgCE5G˅Vܔ7:Wjlfʄ)jW8FkMM'#*8h'<ʂƄj Z1D'ڈik5MQ MqG ОxC m>k@#Ŏgm )P/b"ޤpEDDDD߲np[dl-U0\Ѧ1dd2?B-ٷ$seCmH2:+9ei,LXfmϛqnoaMqLY+,l㳒˴pGco&!4DdP31fyOCM a|5!0'gGY]+ }SU@mH3'Fbvs8'0ƆEmSEDDDD j||fMۋ;_t ߵc>k dbM8Z?t5Vxm99q),rhq;9t[kϱ0 hu9r0G[3G4Ra&lOK/h?Z8^_MeAm…>wuwhaQr Yc[Ce 69)zjtBq \@WDDDDDDF\pEDDDDDdTPQAWDDDDDDF\pEDDDDDdTPQAWDDDDDDF\pEDDDDDdTPQ!ʕ+Wnv!DDDDDDD(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" """"""2*(Ȩ+"""""" ƛ]hhhTVVp8p:7HÒHhh(111aZG|mdXZL0bbbF|s3܈>)!W\r !""""2JrssEQiii1a„/F̚5Ks >i """"r;tvfJKKc׼ho#ϭ+"""",Þ={hhhE!Ylfy|Hs빖>)e}قIUU:_6RzO?{^ 쯿z[ 7{65K /"""7¡CC i/c}n=:ɍ0#j~k62Ϯ)g݋Vs~.V./k%'77f m m>9o;;3DE0ED lbʵ,[SCcC795-rN7t|nL^M{I~™hXٷ/zX,VOnap5<A'wxBj@x iw0C^fwN!}`˔yt)@ZGV/睝)qwlwjt~{9NtÔ9KY ׺lͧ?#x9_O]#4}<;o\~'tGNLVYGw ޽(?{OvC59ܹ4|5^{v'>KZN{s}#}M/y}й<:?N3?by՘{7+)Y(k%3!DLAܧQՙ1̉Y^;R̉ U713Zrvm&nYbQ( S{*#;v _7bGUCmΟ?OVVVϯ|Mki>=Ñ4?]z9#dY3_Lv.>m'mraכ%8"m*V,ks?.vgs,垔r~><tgv䌳ool8#F3!wWNJ9n&+Vbn_gŊxv^u%`w9>p/fZ=5k5P(#pM$/&bNvs/ E3';go59} yKfky$=+)?|׼?dӢpk.VoZM؞n6=ٮv6㹻R6`?>$I~\(OIr^=ϳK{--"""p|m}*=J4Su#%gij0JZJj&wfDa?=3o;F W?*C<.]*r~JXMLLoÁ@}r=5lZƹv,*7r6=QLD'%x'{^yzKwv^ kfa202䵿{†'aNx"HH_ʆYCBNvwKq|(S;ː'cMI^yװ U {\`oN^?ld'y>yd.snf{)g9۰QDDD{| ͟؏.| KI0aO <, za2|o"l[wNa&|QDO&sinM'R"0H~'I)"""#bqqA\V2fD7QT>@rO"m*~C8?W@ hh3a5_EU 4R;g62FY~=fI]גq IjKn#s\)n*" qV%:e%1~j*#nad,$O^Dљ&bw;Aq48 ‚9&A/Xv=#i݃wC:. XV*J'3"==UVu/Pnc݃ `"a!s)'~=9,]E#Ω&yy2+mrJ]lrFP+;{ӯ)K9vk 1oX>MO&f^,\͞`w 4e}q=>cR?WwXدb&!-g;`CQ\0;쥙5%1]qȘIIO1S?\Mcae>ٳ݆cjgҾ50s!;sz>j.dXOW${wW%}HSEDD.?CC R,穜:\\wZ+h pJ`> XVi` vkG,S; A``JtY?x8@}qWYw`גP|5u#Yr'N8N'b:55>4 z7R]&fQB#M$,}M#S$dF>VM3wSz9Ӕ@]n8z<6)C.Wο9>‰ȈX,#3.r.s3RN Pm4zoۻC rEHPJc1\նjդ$mirrYi_~m3=M[mƤ<cB*  |t`Zfϳf2k><7EuI{ Expv2ۗk"Iq#IiMIj|wNRʴ˄>ZLq J^5H6S0vkF3t KJUt@g~C%f&&DH&zU!VR.-J Ewށ/%%-Wr8WZZڹn"^}Ns:׮ᩍZ%J\3YCG}&e #y *9=6jP#Inܲ) l܃,)ֳGj]ZmGenfu$y=}<]9;;*rL IDATjH晗T2^}^[F-ReaWjS^{%IUF-N{T**uRL]cIeyrzwȤi7)[ 87iby_rYs&UսK7/znnv0K`HlSHǛRp޿bqvi5C7e~C6:[=:Gz:Ucѥ˗KG݆˫ʮHTg緯{/y VIE99 YpNI^5Uk׮އA++kTGb|uLIl+aIE*R쮳*ۗ=] ]ﯷR9}oVIrUPN~6A6)gO234zp[T]~'w]6mVԣ]}k m-QN*K SlM Ȉ`l}aT,_떽A%jBhK ?RU*W2,eeϱ(4*Z= O_oQck4BXRz;\ya3vTݢ>vЎX}P_FD%*qBUn]tX%*EQzʭiYb\̞a!mvm=UVjtrk:']:Y]#d6w˄za^9Ž=c׶nI;6\msuV\n}2'Ir׶?\x|uusBSm.xpH/JkWWﻋ=y|6_-njjp~'w([5wCwxJ>,gT%O9FY*eY`wvMȘ%וAjcr,eE{~8_U= nM;=Ybu0رST\wޡA@yDbt,IHAnw3m0wng0CnKB{YX+?.;WS-e%ԭѳ1ޡ VTTT-ITqqq9g8ҫ:N u(|~M/W-ۡ|Pq_ƹac3Ҩ|=&SZ/-GMN?szeR5J[m;5 p2MNU^_vР|m/PM~mYAcs[^a{[ֽڰsSz>Kk9V 4>84L;שO׶Չ߶^^nwi{~%qRtH/nyG595*/Ֆu딣X弴\-k^6U-G} s6h4LM(ݬ6hCn[hۺ @O]].v{z.Kv$Qmmmm>],|N;ưۢ2+Cw,YzΙuWiwXNI)*:ݑTQ>W?^ha m@;h{!U4H2jʼ+CEw>AQ^{+'cZܡ-.]+][*kq{[;ڰ43C/v;M:VQM|mxb>_v6]mZI*2ܫ_>z]ǼJsjud=k_~ }WZ31׿o/c_;905EAۮCYzqk}ݷWPi{yFʰjz;.Ԓi ӧO>#wHIIQJJOW1=SZZgT*++ӧ_'ժh($$4ߣH(>>^IIIÇpN| 0`\!p@`\!p@`\!p@`\!p@`\!p`w~]7%vmzp@`\!p@`\!FhSxS3Z7u+%sp-A6Uz; ͷ.ԢoX=|ՊeG>K++Z~ݗt"iaGOKQEyґђ4ZeON{%UM6)O t֭sc&Ǵ!F+$r0S_}$p}rFuuujnc /00PjyG/.M\hhdsJ^566|/Ԟj}{=5M'Zd֎Rzh4@˥Rj„ ln@NSuuu*--Մ hHHƏ/ͦQoby$LOp}FwSplRSSXSSŝ mmmCrε+00vp~u+O9p}N&Lw3IRDDJKKɺ:%%%=A7<<\eee\f.Ξ?CrJe#8t>^ОJm(`-K\innfX2F vs]{n;ç$v@qP|~p+I?m;w͞o(Gh8}ejoy"W9jԨn?|EuMT9 ӿE)F3n2͡߀قՋu:Q㧿{t:g޵Fhw[zTVm++=t9DʾQcS,K2'(-#CbWMKW˴<5\=><5Gvd)n~}y!X#$Mpf):^MM>غ]LCp%$N缠'jʛ=tgVZ"f7РXMO=AodUtOI|=`怲*W+$;#;G+?A5kS|\=md۵K 2S֎펿[w$(:Rp7 l&|fvEorS{p)tiXznPMA-XRczr)RS?mzU{[`jpףFd$I&ζxˡ:ūОR־piV(qJ9\$(Q2_q;51jv)Ϟūjv-Wk5d+P՚rF{ b[-;+[4-^=Mg #[91wKVknVv}cI'GET-{ǵ;B ĭ"SV7Ki3[VʖݕB-^ ty S&%-YE &)n_0.OYo)dJ}㛫xK:YIgi+*lvWHCoJb3F (==t-X`Put jmm=sFlRdEt]*xVt}djo_Gtz>\܁v<:UXaR4eIҜ9c<^3Y٩*iZ.zU{v%O+$%DY/˚4WbdӴ`=nSQJLSLY1Ӵ }zP꧞*:#%KS|Y5=[%2*nqB[UAeڿVKbiAI/<=WST$w g6W))Q%ɦcBGGRRCеw҅=]jjkfi[H/}7ת?|ESƴJc[Oї*~`v6 O=Rz8"BFY/= d7iXԱ^U!I~ɛ2US].l]2)*ζ=Iۺ9_];\5ZCIjw9>J7l$v9pg徬5g%օ!ً{)*HcԖCߟj\rRMs5(&VMv4[Y!&㔫׫I dw(5DdrϕE1MƳOݣodSHYv쩖p<'S7[u- _9kRS.^>*rgS"CY9R/y?v9^rG&w1DQ]AqH5*F[%YFƶEJz7&_E_b/06V//Óڿ0oE /aK CRzn;uvwu>4Fj*9);wbG/lkZbZqOUj UU]QE&*8@&*=*{Q*ΣG Ux\e;pFsճ<~'K9zޝKv(כŋf)jÕ4%J $)]ᒣLMJZG5-nfQB#Ob)$u%Na{_X$jFԞGCds@MVM\e vrZ_UaY&O̗/%*u($QV-ڧbXcOiU6(.>doJ“5&^е?ú•tmtua]Jm΀+kֵ_R{V?^'9GՑlcQkLE+Z*%7b>mM06Ӵ$ݩ]_ҟ;]AAJX؜ױrmqSX/ޢ_N qI 3u{5g-R#GW2)u^|_ Mvy0%-RFNj;-UcmY-^@}iwn'Ę`b=)rxTV=K+fu gU~IUj_Ÿ\ +:ݤY ~W;)EWe=ڵXvgi nﲊr~UTUK*H\y:{(8p@iiiQewQڰ\nۿ?tQoWm`$vaEH[{ ܮz{b= G}+"^ #bj*?J_g >3p0>()Ct*00۱@\.Y,I vBp1")""۱9L#  GUZZ*=HtNSuuujhhЄ =ǏKf+޸Ե>ǣV\.%&&:@'VQ!˥թ(00PjyG/.zjiiw\]  |z3ە!.C  0.C  0*UUbnqWǟ.lc5ֻumI [ڹmֲ>Q~n `0> l%d8U]kczal[}\O=B a4pb2 lTdTb4e e^Qɞ290Cg=e"G}1mKj_z4uul]3N3hP]=޿Nkw6bg$ӏ<:j.xYSL]5oSRiwmْyR&)HChIf)' Fzp`x TvMLR|$n@:X+%/HU̎#[ܶ2%,y@#Z\܍t]Az)dx 5s,mkGV(sQ6eW-I٭* ]c="IY=.uԛ%1ZrZH&!5}Zr`\LIҕj jyBE֜eŠI+' m.۫3nE^lfIf Ej2oӌ(IIKŽ{䔜J` Z}M BkٻUqrt(Jc*. 6{X*:[_4єIZ0æg~Eu32Oz u_PwE6Yjp-Kj[z3Jyeu#E C`Hf[bb%$,8Ae9$Y`lꃲ"=e֤^rڒOhZ%X- `6wzwZ-3ڛNS[meN]EB{ݵÌ%mIr8ݽ{),LjTy̭3B6C@}dUaajkkz{-c2ӧd2 s `gj/ZeoIӲ8;D{skeIOilY*'h\ڴWq=Nڶ7O7*eqE(X*dp"V=^Y ]*˩7)ϩq("*JF4@G 7ocǎ@fOIDATH7?Mխߥf( =I & MɼKۢ?˓92U ,ѸYjhpKk2Ua6#kтd}kEٽQo99y\.Z@lҳOȏJ^RŌjkkkEENs@rrr4wݷo222.Yv$ a}SYVxb&1]{{pN?AqUq@YY2.%nCvpa.~u~qUZuNՋ/9D;&7hzeoj=mt-\JI6yPj~$Ofy*9o>g']svl%^EwuelVn3}絺o ;3[~?Rd{tho~=*o8z_tj-HbI[O_z]<pͲMVrjwga>4tT;uV%Ky9*qR;5K%:FyO"uR'\r>5ZJS\}чxd~>閳))DׄvT1+rlTRF:%[d:3%tB$9pnvh9K5//0\)yw~WZ|.~5gԘ9t0$=46LCC*&fRoL8I>l9&Q':c==ӾTHH"mV;NQRs~FKWSNi"+))QDD#yMеfE{ tzģٷ*9hDJ'**ثV(}vdIKjM:][}3 ۰C߿_1L Sjjꐶ?7J̉,a^|q^S]3Z4YI>oyӾMҍmkSK\>|ޣ,6CfT[[[/*r:/$rO>鷜dRppV/.l7% ۵jZ>wE 0.}2p1 Bꇖ4`\!p@`\\a]zxYUo?Pp`P^}[#>WU`l67a5fGVɺg-0|p#""|Up'U`мz^"ew )(ZT^,Wחn>d!`x\Oi]4kכ*C é9rR-HZߪ߫ 0|6>|uWgW8췆b\֛5/z}%cbsaq̩?: Qx{Uj/M0[X-bV3w Qmmmmn|P?`>DrߗndRddfϞ-:-| {?O5J&Mܱc֦naZ\˟=>Fjkk_rrvp7)5CNZZZT`2 {un۷ܧX]N7IA`X]&h8lvW_E>tEao \7|zS5U76 뵷E"T~H!SdӵA?^8Qb=0|nN3'%i٭Ӕ(쌪yR^ӗ|n{pO߿_1L ׌3M'8%IRܒ,fR{߫m/M;04\׹:{N)Z{5KUkdVcFag?+o9ɤ!h:<٦kֿ-SM~CK>_R՜Uh4|̫#T4N[>_\)F@W---qWU/Z[?\`@vm.FK^zey00Iw|%]ޏUG*"&Qdi:ܼb4Kktqi4]emdr^ϬtM*׉Ky6\USJ_PWr) ʟ dҸV莤RrEbMNݩ|Znݽ|"Oμ=o9uZ8٥#oޯ\oՔ/7k\}W%$[{O)lr}r_Uo嫶s9*9ǥkWn!C csґ&]=gd Ziij>qD{=zBDR`dUS(ZS')uuUJKQ}6E_\Wd ָHnUSgӔl#Ce^NYubS`v7LM.ɦHQK^ߥFaꑚt*?kjU̐,"جaɥZWnjjR%XVd &9%KשZd ^t ˩VLR`J3[^|5/@W QiJW.(p]e2)rxׁMʥڣy*qEkWJn8R\6RjMU&M.`X%Uj: zp`p6O_\|`*7jᜫ{h#Ӵf^x[=&FSnEɽ[SM:?/͒%:Eoߊ(rMڣ eɱ  sy' 2p9uuu C,""MY3f` 0.C  0.C>xV^2&I1cao"ShT[[[/*r\*w=Z&Mܱcڪ4_4]XV7_---nKkeݮd/99Yv}Nss:CQ {un۷گS<uݬk ZWf W(q`b;qeƺ$9/|Pw~uD[%͞sKl{9O~[AJ Yrn6 7ozuw3M_J( Μm,I&Ym n:P|]TZOs '+azzfWcsfSnY߭Wfu+xJJs`{:wB3"Qyf]V#rʥG2p%k6& K÷kLU#}r%iFi:}lUWRp"̒d(ꚼCF {PII-WRRiD`n|u9aVI)M jW[И`EuHfo{9VD0sp`$#GhjmmdRXXN:lJӉ_7JB9h՝-:xLϼbaMzU Hҝ #p͜9Sr&I=9BS6G}79m=#zURxL,0#Ĩ6_Tr|Q :G .Q@@߮=oWC  0.C  0.C0O>WU`YV7|gWU`EDD s7% ۵ 0.C  0.C  0Qmmmmn\!p@`\!d>ّQ+ujӦz)ZkMV?0uju"q~g紩T\Ct-&зt^/) &w)sLeOI=zm1>ݗ($9Js&eG6%}wSj䏵՝ CRܴQY}6S<H2=aӴ⾻4+j7DiW)j3)3X/>MnIڲEpoکںN :eg~boֺOVUsZUk3kq|s~b|2ux6]3pȔT۞ˑ{.ąZ89LAAJϼYQCow]St,M^̙n~XbhVf&YdKǫaJ$\wy~ծkUzg"!!}?K2`D_uW)WLЀFNPUU/9D)܉ESoiWv֠(ٜv9;琞}jnVuaH\u?wGZX OO^if=`zo3o{稗CA BdvХG9,JA(ʡO:rڢd zLѦnW5(*>W>D9m90!Um6.ӄةGv(Uۦ곱Y&ܡYbm*TۭYzȢw((,ESߞR'z]FYb3vTUm]mnIo]'{[d;#ڨab;6\e/\P);)hڵZ8 IDATx{|TաOm2IATFA6/Es,>KhP/EZBAM &% L`I?r.2ٗ{fZ{>s| """"""r1(H@R @,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,""""""IXDDDDDD%,];E *ZӅ^έML<~x>6f6^ u!m!n 7 yE l*iE}A}x`0/;LMaiMAõ6ų阱_6n7wMtp(KSaaS|wF;޼ia1mEsN*bȶ"3M!`+3k"-yl#eLb;j_ -|PK<㬽[R"""""""74dźRDDDDDDΏ1C<Os NܼJn;۹6o O;R?Sk駹1 XPE8/n25W˜/&af<_ֻSdT#\[‰.̀I^$gƭgu|sWDH;F2:vwoB iu9,DjfwokWyRǽj(̾MfndeVafߒp8Oyy߹7ͨu;|ȨdyF#[Ak'$>g5:B+??W[GF7rϼ؊Bm8v'ܳ~<my4oBKH3~YSkY:3fv;:-NtJ g}Ids=q=WwJ Q:Vjl&*Dv4eWjhbeV% ;p'G,|ϛtJ(gM$ẒOrlR1͞5-o'/6?pmIċ&͹5n"yN*+gp{x,i3|[kop_=c%gL93QM^/ c0mU<8}ηaWJ48~htYo11u 陃`g\"r64B,c}y ;A̾cmmY>A #w'ŽvW;rXM~Q˿ܝΗr+whv$,!eۛl&U9;2nwgmnSx(͇~= r-<6:4ee2X #HMl&o/^Wi6tA'<{!+gv/j_9`e67m]G}_~ݟuЫ|ogi7u>4&fyxcl1m4`a[ ^ ,LOugsin{0|Ɨ7BunY۹nC 9j13ul^4Xp(ywؘituyŽ/ZW;v9ʺ)Mlx#m#hهX eœp,;Ա *3XFq$4 u0 gv~:ZJx03eaBg&?lD2rilq[-ĺr^j~Cd\%y[-1l`eL\^مdMax|aY2b9 _6M3EsAaW6ۏRހ;KRr=dĩ hj&dWqr*?hilgjF)?: 1lٕL3ȨfTכ~9뻜v+Ygk~$hÌi'#e)|""2SED !tC^0q<ڎ\ }~c[z rR/h}.:w[!9Fp=f!'5%=4&&Nߚjx!ų"0+鹎ޡQ=K=**z5s̬/)HjͳsOj t~ ml~QyHzowk !'%uNKdAiL+g@6W&;mc!/qs;l:.i7hAӓ#RspO p32w6y4&~ճkXaj 1 XP.{M>0sYSH_=Ic<<=rwn!yM]cyr;ziI?e;)BdJ v&̎X#fL*3uuϪ'puٟy3F&/'K'SqxG8FX~fM[̣9xP\}6u֯c_XRVŜ> i|yg}KrƂOdV.w:ibHEmCw>=74~l ĸ#(s8l)ǹ#[[y?4U[q233)O-I|ܽ5-+BWMF> iOWCGw^+4IS3SetCdz8_ӭ B5n1V&S3Jl%V@\rsC [. OcQiPk&Ow˟vN .IR niʚx>8fG_?-<֒Ě7(nK3J||!{Gfh/|T6 .;}{Ȫ$c[&Cqe;o.gEbOW* pL5 < ȸ84-Ei>(rWq:{]Xp^ 9>a*%:FP;E%C Nʂ *`zZ0y/=4R꣢ ;'vQ:0(h6>g{vǺ4醚4&]eQV/p]i8P܌bX^z9pWp''a9oRLVψ@# \3/2%CaZE%GoK5v{{SӐ>Pb >k?x^Aܡlk{'mȎ&Sw+>: _CkvZiV"y1 ZW doJz Q{}{}aoC ;'_} Ƶ['f6~0Oe*B0I f!R#<0`yyk!2tdE)Ca`NMa{{cFߕaaIkQMSYns[Wmd6 &P':9f& PfCm h`L7N;.;._D[n5rZf3Զ>'r;jYxoņZv6[jw̉!y0+#1H]PS $Xz. 46PMNJ8&{)YKV@<98t١94@/&` O`m77+c-[&b!%m凎QU &`&8UPV޽Y4s^ $خ,Ӎ<3s3-J`@m>ECGY^v);ճU`i}4Y"'5`gIOLMAܐ|fvyB);CWrv\'` EZ>jϧտu[x$Ԑ *L[wQc<x;?GəhHJGhkyxLwy??dj'GD-$Ҽ^S,6r͜hGwv^Hq!8S4LPcA ,.&Oc/s^;HAVoaNKldQjYnm+ 1 4zF:T 2ma`imϳe :4QWfj辍gډr3jT!jDMCB&! +Cb`Tu5ZD :x܃ p9fW0Ўib 97偻 Yz$ݥ*_~""C},ocO:5iK޸V/pfY!~^,a`v\|,N"\ir fF0 N4c;u )&KzعKF;Gv/{r_Y#-L&4(KϰssKNh-6ԕ5f8,fQP礴(g s++*y\NBfL#j^Cu]NJy!ak%9}&)\<ߛf󃝱hoee3uCr_]½P)Y=c4&ği`-O)'đGvۂ9P~gmKNry5m"cp$X{n2d~0{x]|x@΍I!~'f% kHs3MHOb4^UUCSSTp'8yI6wkhRX2k,ék,?ɖ#ձu ')]t\ ^{CȜmbO`> HW/͟4+̰ⱗ᱗bkLόg˶dߘD=u%X؞!bއe>־Ng},N5,a>s:!N{h|ge_8\4%Ϛ"&4 'hT$3Ԩ;WZKhݷ뛘yM:6{nsXl&tzMNR#~2t.f!Ytvl'w3 U-`)D΀7x,Xc’ K:a*nYksPx\_6rc?+s~ PL^89'^'ZM$4wSj024_lw)vk{LvrRnM0&踇r֗?nz^Ch;\qLsMtX'RS|Uu@nta{L釥=o67w%.C$rw{^>J@kC,u CdiSxwݶj؜OG'xiGO~sY,/ʑu0ؽ7z8RKD7^c%) T |y {?u;|`=CE^) זb^N[OM j{x]l^!{}3O|3<ԇ0Hi>ɽk{4Crq/ugC,YN<ճNh/l|/vxlu5 ~^ `|TARVp`}^Cy;_v>::z/<5ζ+yO]|>K˃T?5L%~P-a]>umByp6`'zk۽8/l>>:}ʜ׳G0\P5pR/ !Q_Cw^.K"͍ۊ"mut 3nq OsC(U0eG#q0ؓC|zZ)*AsX K‡2v6um5S-d,q#>HE7>v}޳ӎK"w IDAT֣fdG-ϨzOsK+b0 )4r=0s6RnY'/xUΜ}l, < Kbb;fD6 .gSW_ aqV$];a4<.#ƘSܒLOJn$e ΧX1Z\Ȩz`\̖q#NY\tq9tЬ/NA!Ӧ6frJƔނSC-,wu_qz;>㚸~FcS,xwSϥgf2zu˜U8OA,I*")գ]"NId[i$.&nɬ'7 LgYף>;zWF X:CqQ8.cEkYU̯,1UE#BdL;-[Φ^å);b=EDdOC4[X=S~jȶ n˓~ݩg+`UW#s'ChO{K$OUT [&PHK 3{ۧw(""qIgnjgΜGOjc;nKy;~}@l/ǷBm8vLaSIĨzt4:?U2$P af4U0u4\SdּɽY,]h$x bdǟ2L;Vfw u;).̚1 ~s/rgQ9g@:@UlɝnS9W!_|G g:Vjz v wdVKe OJ #i0uӺ@E^2m 4L^Lh)cA]J:<9ZxYOuk"Ȏ^%"". Xv g'h\7smXw^b?ZQBj,=HDDDDDD.K  DcT I=iPc.@ljaa47DǶ112]DDDDDD\IUSB<(g!#.Z1gopRYEamhӂjIh &ova`""""""qx w\_JySxi4O:<+*<,LaEzk*NF&ZåEDDDDDƢKZ"""""""gҾXDDDDDD,)H@R @,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,""""""IXDDDDDD$b H_2|C,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,""""""IXDDDDDD$b H """""" meV[؁!\y .ɝ4_PSnb8@ki~(8WÜrωGc;ѩ3a AL*v~a2tY^rUDDDDD>.^ < ;+yw_Gی$NMֹ3 V{.v V`#̽r*fA{e Żr>A|\`ϒ90'M&+ŞǵފalnJBw;͎6]@|3׎JsG q""""".@fM9H碬(s@X ~0GG\;5WDkHAZ܁!>y識iiIrİ`q_B 'ƒJD0P\^OLvP !-|#Ϝ̔jvpqFy7:ʚԶ*~{3ִYqY_$Xos;MXdGۧxr|>' 8s?9yv'_?M wu <M4R0`p蕂P_^֮7sVwVysɶuaIb<TQˇ1i-\53KK5v7w"ftnq;!*e:4DI~Dsd5h0Q1VRF;enHsq$8  &99a>Qj LLCoSwQbAOPWZȑ R0,""""r=Ėxh$L|\a'`?ʛKQ@+K;WQ2+S'^Fm?ʂYVb54nB;{IʻI|3eҶr!:-OEy2Q:zx*#1nsƱ OmoN`Nqz0Xֹ dyWڲOԵ|tR  KT(AAvگM̝Mkq]gنwduv8(uFa o6\gމsB$AԶtLs4qK;^+H9i!Ԟ61-H[C5E`{<ؙfiTv ~s3sKEĭU'21uN:Kc&ۺI ^W<Bx0""``gk #<Ø' vr4uCA18`ZivX"0:ԳQ$MB{+SF xn2 yp3)g g@[-**DFs?T6=A⨣f]-J9϶|D%Zvo 1 W>(o9+C\6+[R TW"4a2bj0DMy ~ 11^v>{(#6{o1~|}e%%> _LdeSrk#'>=u=Mgg&:SiCiZ\Y^G(eMgab粮Nǟ%% 8,I>kS2.>.=MMp8Eǚ8==-aĘÈԔ|5Dk7=(e!%sfxC3Tb!j}.\} B(OSpDž2J0ƅE0-ي7GOyL ^¢cH1."VZc!q3YDDDDRsчL|pL⛋:_tGÍF]v'~Jd/p45tkajYl̩\7KVG{#H<ƿr: /SPf(S|x0gɵ;|n1!^ UOgcy S5-,͐T34}e׳\Z\qmx0?vrOG=Op0(I LOk G|W{)'#1/TAH{EDDDDd,9\xBi AXDDDDDDؿXy(-9F.)!@,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,"""""")̙3g.v!DDDDDDD>k!@,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R @,""""""IXDDDDDD$b H """"""EDDDDD$ )H@R tqg?\bHQydu#e""""""c$b H]~s'5^y $l%,c+{V % @8iWsӝwl kzh Sj=$6=ɞ_q,|I? c/p~;I0RWxaq?;^,N3pO o'ji+q d͝Ʉ{j( vU'ů<Db NKЎOXߴC/i,$&99'5_fYArȘ56L{OSqդƌ'b|W,.ߞm_r _X G0>&V|=Xk1 9^5|W55^m5dGl/{*z=$4ŧr_#&Ǡ+"""""riq\0^E D 1̴pkϭFC2 g^ D0cWC<s֝,7H;I;W핈 ʾs\Dc4a.Wc8 q11.4,ۘ0dC% Vl[,@DhGO^߀ȥb 5.yT5qFNc "bGSѬ@DDDDDDƸK+7;zF^N :q:0~>LDDDDDDJ`SԴFmIK2А_1pkg% F}M\>ׁPEߞ_;vPm#s<_ ofi="K؈0X @lCgy@+XF4J>n~}:ϫs;dcsQns-[^{PUzlC.~f]=gy+nȎ2c1qѽDD> XI8P`?-?~ E K93.v)Ϋ"PB,[)0cp;7 T7DVy9Cֲ.J{''`΅,ڈY 'n0g|PO|9?|s:)S乪Bn%>Iƹ&csGs݊e,:煍ĥOHr8.zqo.듎B6YF͖͆ͪu:uyZk16Xxא:Vٰ 9d.WYu6:n#M3߲5l*GxkuodƲ{yC'.bŽYX' WE/MɹrtqY;jp]~VƎߺ6=j}+L,;oUMT}Uٝ]g[ػ8;8vh2 M(: wiGlѾ##^F{4+|m!%6c!{%\k^A%-CUcE 㖒i&6L*51KԈZr^UBϏss<4U۩ѮWNnWS?iϋĜB6L/* p' JQq$ζi.v%y35OrFWu)[drrr{hc7~:> p؏ꎌPz`n=ӽGm}||ٝl9aAH8ٳǩHPWXOYI"9ܸ/}sOʒ%;iKB/1`N'/-]c7 xB4=u#l=ñ0' pđL᳜,޴v-isj M-f6?{ ÌO$B45nӼ(}9;8|85Vܬ~ IDATPuwF;;N9 tv?3}?Ls^(ނCqD$ D< =Ŋ^եG:)kut(i(<:N1r!;3؜.0q/F219o3f;N~w9xWO7!JFn /-82d'sPh[̡ٜV݁SQlec̤07rh'©h9ÎB=Gab -}N%HWKhkǒgF?0~䀇#GzH.A*֌\J^ ~I4y~M*U("qb#ijKqnV Euzf F`$n|vWaf`/(7rfChff#}_z9;Jrɰf(MYr;fqͬMÆ-ʊ GhtrK(Xq @2eNf3VG 8nwX \gfbllN\A`J,I=3FfcLfEJXm(/dOH Ca|h6U4ą=[ IrQFܪ6W*׆`=em|_+ SZF l ю3'k{(bF3fXoz3tL]392C%Ȑǃon#G.H'vs:M(i'##-$pMj=&s[Va˰(*'?nc2)s ÙKՆ/Wx0=?#ҷ8 ;ϑsjs9I_AZɡ%_l->_+-7dNxو9# Ոٜ1ęiWLjCn-Mr%)b7H1kΰkD:%%f3)(yGx4y!""q\^j6[C78~/-Օl]cnl}3D4d-K 70buXNlJZ{XOȬXIeCmteuBޖjmY#'| _<=쩪+."=Hfx''oNǖ-8BnvB"qipր71t#5.>5)%}>cMf@ de4oW@J[?qv֛5dcG1m2wPs8A|vߍ2Hf>]> 9a{Hf30>M]x0bNwL<7C4qj0aHJ2P$Α̱M t AZÿ$fttv˘QHAr ^~=Ybxh#f=ә6m) BNVg ۊ)A\F26׼w̧ps-,dzWHkfܨN\}+]Z|X F{öywD Σ/oVX l/a_6 s1><^HIqZU3|+Z)>Pzx,&vo{7`[}P}Oc]o^evpڼD<2F0Ϋ<'ؘ:8Y)y;9`†*wDpk!3ɷqEcr;vv{[=ԟ-@sA=^7D PZ7'kX\[eO'3>lH/đXJۋ s^ ػdpqC\2FX7n3Mt;0$W n18ˑ-8}G%~a-]lӆ0g 30<~|#m_xg06SYB3 Wt+2+S9iTɋ #8 9ZHGn`&9Wha|!gmu9I:[Zinnzc5/s|=JW輝o1QIo;f66ᦟ4̝7^s{7>5s9F+5Jw jl'R?m=ڈQ2kJ`NY C} UT(,/\;&_aDDd">dz (sJ&'4OK:.ٞm$cO-Z|&o^Qa̷)VqHϏ5#q' gb/k6"rm0oҜ~/oNĖA_MexJuuُv7Ud8 ٲ{s7u'D Js&Auux I1bu`Q1;4f=约C=7VJ&fuG`[EqnAz+1'fwc͑Sl]6Z>=L:x릝7.IMT}n/YI4Dqyܦ~~>:i3!+9}`;jر<=^:z Wͣh cOy=^<> )S~ӘN2u20ut̾*[:vݳ7vrf賙l;0w4aΧp)yV3#ۙ{ÜNuq(9߁Ǎ/y^u GwR}o#;H,`|%;:Xi@'nmd;0@*WLڄj6w'}#sǀMmNZf:M?f HbNZTrBMǾiq7!K!N+T u`;=o3_$t6ZIi^o'*W{QwZp%>ږۛyz:)}t?aHv؉P ]WMg#SgG]KH?m-n:T)6v@Mss8ngk/Bs {{fZ!DG.)1If g-{yvqM1Z =46{x[R=P]4Wogg܌D,#4LDX~ktz4Nus)yȡ= xjwRy _~n,tNwK)(έnk_+)?Ol]|8XU3u>Cg^yy!"׸??kK9ЙIY^BR(\AnAJZڍ4[-;9q:5g^q2=\(+߆Z}|ح1;_`l} *o,%o /[q+J*ԕ>*:Q ~AffeEbۖ*<~09r<)7)[Ögg9;R6\ƆDB"$`TqSl=^VCen@rl(hɖ=- `$cڵO`qūvwC krPh񾦿~vS`冝@sF[9URhݺn@_>3 u^Vkyj{-i&񙽝.3(3Ӷޛm$;4n6@97gk/؁Vҷ0兽S9U=DcLn|̧5߭z~ϹƼtf;縦ȣ+eKnT;:Oy4TWS5 jv8,婵T{8c:xk#3l;Ĺflؼ.U0̈05eɸ/(%xO2*7e W!U~^|Y|#飶qfn="qj)dÖ;~]O˖Ulg787aH髥pm#'kRǍ_Bw9STK~IDsZ""_/i3਱+l7uEDDb6SZO{4" HBDD$4dZDDDDDDR'""""""HTR@,""""""QID%""""""HTR@,""""""QID%""""""HTί]F   rڵH&EDDDDDDh"Oׯ_Ď]#&&& """"""wεkڵkŃBS""""""c<Wʧ~tDj`WHڵkbw_DRD*""""""QID%""""""HTR@,""""""QID%""""""HTtDDDDDv)׿G>i ɑ͚_?w}Jr[r+Wx"kmmt2.vP'hѢ/8E!"""""2S~$]?G: E$]KNmQ@,""""""˽^bJ EDDDDD$*) XDDDDDDbJ EDDDDD$*) XDDDDDDRL """""_~ųkx$N~qל3b}Y‡^nG?AploďOG+ 7;~p O _wsK?sRc?Zyg4K^7[~ǥd>/qʲ+~]U/FVIJ2|ś7j/[[ϫY_ ^GU◐bYPɹt9lxL_g:'/ V1x\g~Z=4>ɦm)}UA~PM,_={xq|evQg}NFn,~ι?YopO@T6/{+#??_gy_Oy{~p,[׿;6w( iȴ, v/?>+<.ݦ*?jspGmhڙM|LO~ubT\=!|}0@? |M ÷A#YgK]˟θ\7\dsT]+PWK&?zd2}Ȃ2ЃF?磰rw\?~xOPz RoqsT lHD:?.ߺEߤ~ۏg do6rYG+.:+=|x(9?Χs( cw4dZDDDDDRLL(HlAB¢_jz~rξi-?9;] hƆ*}twͣN2g]Үxxf͂ ?u~qm^x*EߟEz."S3)fɛܠXDDDDDh(ЍGwһi N0<oU?ME<x̒}~~9 |4CW3C_E!`C G j)(ACEDDDDd %Oz5,?_" iq=Wz' `WgI>~_%.CxӞ#KåCFӇhw}[ag{rK~|o}A1|FQ'}O߄߿KS~s]نO'H?^Ô&o.2A E?du73L|=KޢȂw!3~f' gfj>.]~ÿ$~h\<{(ȳQ>z_ɥiG\~z$v|xHZDDDDDn_z#I{/w=}[}',Z NhȴD%""""""HTR@,""""""QID%""""""HTR@,""""""QI|._$"""""2+%9I7$F: Ej͊?t.?^p[r+Wx"kEm:{>O#K,w}moO>aѢE_`ODL$b XDDDDDDbJ EDDDDD$*) XDDDDDDbJ EDDDDD$*E, vZv/""""""v5/rsll,ڽDg}FlllڵkXDDDDD$ǂ}ׯ_ί]Ƨ~XDDDDD$w}rGttDbH,""""""HTR@,""""""QID%"""" IDAT""HTR@,""""""QID%""""""HTR@,""""""QID%""""""HTR@,""""""QID%""""""HTR@,""""""QID%""""""HTR@,""""""Q)zb YYdeRD:5""""""rDln5xq%ӧ9 Xze I/" gQ@Uw}ײݙEVq=}."++ Gvj=~`g[\Ƕw,xe{rY 5"7tGɺ3CN93"5?b31J>ÿ%tOǵ:!黇iv&2]TWz0}bK"=nD\{hvBN-""""""]O2tb6Y< ~/u1@HQ`a߾;CN!63LXMֱM1Zs4n?QF:[p 퉷U """"""r}IjҧjڏD :p&CmfV,^=uu{}[::c-k(XɱZK;Hxi=CJ~9O$tQ Vz[y8æ{b1X5S94WX knNC`vHv8HH33m""""""rgbv,/D`c(΂96I|CbNve(p2".9-8^ 6Zd8l &zEDDDDDׯ_t"DDDDDDD{XDDDDDDsP@,""""""QID%""""""HTR@,""""""QID%""""""HTR@,""""""QID%""""""b"+WDj"""""wEE: "Q+b]hȴD'""""""HTR@,""""""QID%""""""HTR@,""""""Q)& Sz]:=5xSH-fWY 5L3/x|ݻL/m(++^bTtV-l-ϓ97l!{=@,# ?S.z9lze/!vL&l)^ƅW^S0|C{9w!؞*j‹i걏I m:֯YF,ul{CB ֬_-~K^s0Zj<\&Tbe';NN_WSݱ<]'|16.Kkrm' m_yfԦdUɹn?HZ'7HZtPϊ[֔2vkZB<}X7sWt.ĉVXʞMKԜ|+m:Jc!,ϓ6 "Wv 66w'o+-n9K+xl~- kZ,Q]z.3|3a \ ~?Đf,-e7a0HBX[e'# RZ޼l4mЂ3;-VVsKغy ~|oݽQv;itlm3Ȱa ix l"mӡ_зIk^b{?Fa0`bIXcSylt]TN=0OVob0+iI12xVGG`dO($Ntua r[qbO撷";}#"ELI]M T8EҰ.ޫݸ0Xs=Y%-o)bݐ2Fk/Lށ9rIbMKx0<1,+a%R <\9)&{pChVHZYLBoYSK ;2WL 4z/00 .8c=;f|Rs7g ,<0X_0aSXɲK1?\JKH̡Z8Ųnc8)ٞvZϝ5δ3c QUvLܛ Z037&zgbie1)aGc `Nl<>_͋Ǻb,TOZU3eMu eOngϊkujtyL H[whbn9zG,7LfzMifܼ$V낽#7aYpW*Swy,64IscM8IbIZ&u{c&݃'ӱ^9)~" ]XG9sSgє]s!._`J` o`<1 ,:@#$fqzvwMz2G>BncOH:Ltn&GI7zǃv<梶òm?rN LzT S|Kϫ&dH @yVW{&V-uº8ݽSv:itmdw]&3&*q<ęl2mNZ!Ӧ%&*@B,pcץYug~z`g=4T(Ěn* 4(!c*8AS*0MNJխMgz-^O]lta=MꅤysN?̓3:5y844&u H53q0qL*ʹ.c=Iidj@rר''ag1S:YCFyS*mԁ&K (dʹLhM$ xxfIQŠY$,`áoR`i+<7`-oXZ$OZc4wTvVܛ="L& b ?vs5C.^)4_ شlͦ})TbHQ<~]̡&'™ͦajk8q cSr%2FjL!\cߕw{cȐXI&`Mu M=ʱ3LKwXׯ_Ď#=ZqI@i] 9úFRQ}aæJ2$rO0ԛ,) E.3`c$-sCE32H+LIrD>"XDDDD$D}NH$( XDDDDDDbJ EDDDDD$*Djǟ~iv-""""r,"]$ XDDDDDDbJ EDDDDD$*) XDDDDDDR~X"n2 ]/.5ZDd2HTR@,""""""QID( x*dHD}x;=xNހxUTuN^KrTYИfW:L6PY ++\'[LwNY7[:?GU '+\[h~k/bţlX"-YQK+#T ]p Kqgluz\: vDDGRGj@XPUwn>w-gg݆Nϻt*wݦl@db kwߦ"=" ,jk9U4{*;;KZ'}"rx6#^\{Ki~7u!kT#?psWJk pkvt@o)!p7UŬ TkZ]-N7-U=Jp 7|еs5!֓zqWr~?F{sI1YOn^ovwW4WaϞqqt/Lb'~?YޱB׏:ZEqnJY8x8"w^V_:K,bJ}X$ 0|R8c(H}sn]"" %Dz[َ1@ 5vD_Nę{\"#g.dp&9Z)@圸icEbI(hO)^t'hޘ,drƽx9߶mO_ 8O~=g>)??ĵJ78W?\g>Zb$mY>iFדP&˓c}#s}فaGfif`tLhc5ϡϡ,?xߍ -e,2ENU~ݧ5w-OTv=;ZfKK|4Pm빐S79ڃXD9&nHP.m$ǿ %R<0 *܋}~$)^@LUmGC ǓwPq--gzI mt7U=ڭ&3Q:SAQ7'v\ΰP̙ uAHJ\)ry`ĩQSϷι' [PY),T 'fK__] UWyauX36G0Wj?lit[P]T)*Pkg(SXDF9FV.mJPD^$IU8]5o*Cnrf;8 )S(UxX(A޹rȗ Q$B*3o+2w,Sw2CC6qs;%L&I$Z5A$"0QʕNvP0_,'rP^~/ y ~h 2.sk\^,,gGn4!(/:'DN[D ᑪݫfUoU-I]^ ؏$2nIp{2QXfxj#][hz IDAT9+"o\&[%\5iח}ue;,ϫP.CX@]Q~م1/224@Tm"njRmy8T>-R)aϏL" _ {!ů=~$IO ^^Z2?#sZjw_I^fu]~hI\!nl|}ٟ<~%IzuO|Hҋ@cs!HlX>녞2)Ӓ$Ng $I$%I$IvX$IKbI$IR,%I$Id $I$ŒX$IKׁ777В$IҁQ]]Cb!$I$ŒS%I$Id $I$ŒX$IKbI$IR,%I$Id $I$ŒX$IKbI$IR,%I$Id $I$Rqpy=DUV ~E$Ib=L}6 {i 4sk{N d{y'~<{C͍46O.?,C=LCs{/I$^HQū!5(9.m'[Lf}8S*g'X;~3G!w>&R:ۯ9$IzV^F衽y2<_xܠhvk|剽,N*7r鉷VXG9Vs;(?D"˗ln 8XqT45ԤO[_UbI=Bb9d~yI-??ĵJ7@\?\g>Zb$mY>iFדP&˓c}#s}فa_Lң\ZD>Ft!{sYr,pjsg逸CD9&nHP.m$IbT5v4pՈh}r jҽ TXLř;$j!H9p#Y}6$IyI^"J()kc˫L6WD?ii t42hkB\JXO VH]*IeD9*߂d}a5(Er $I $LTF 4N+*/A6$HT(DgVdYpe0bmovJLH0~kd0$Q`(K$$a$Ut@ɘ IDATx{|TOmd#DEhvKOSl=ZrzŗM b"BAM &% $!dr$s ?I2$%z̬ff}g-W"""""""㙂 """"""">(8,""""""⃂ """"""">(8,""""""⃂ """"""">(8,""""""⃂ """"""">(8pgWFuQ. >5\e:krr޽?S3\oҺ0gpȨuN dws{.5 r bRB0pp/k_130򋙖\@3[wgjHja1mrF }jy@0^8Iԩu\L><]o]Ʈja1c\vE9¸?瞁u3oZ&L# ⊴.+gې|ڸ GUf TY("Y%y """""""gyկ!ߜ&,}""""""r8"ٔSs+! 'XHjzMR>Xמy,[ʙM$틥xS;o:_͂GO[6>ΨdAh5o l:2h9~߹=Vx= PfV2-'?l5y}_<:MG ?p w֡1+:܄{o~pg[&?[hho;YyU˧u`ɮl n'X>@1Yd~yߴ|8 }(=9Gam/nmY2v|V;қ=ǝyT{7XP/&c;{wagϠ5wxȺ<ϩX$svpb'Ns 棾Ƹt^XvBPv#)ˮ& %BAq 8W.8FS28 4¸qck~ItdפgCB;o2ɴpoJ79 l<`ɂJYR@|l?28U˛Od-E)<;)=^ d7 gx>'nbbl,[ʆMlz3m0UW'36J 'X6OcՍEWz+3ؐIad62 (m56fű<D{@3ag.7hT],]Z-?4mv0ZZ&;ocv1/5y"4{S 1lx2%B:,[2IݗA8A p1BDԟbv/&֦1ΞEPB~_h6Iu$>7pP 6nwy"mẄ́f)@{Hnˌdۇ ogQj%|ԝ̱&& M؎|A89.=L?ODD|j,kƴ8М& e?fsIa8jz~C\N>K/ n,Jz{uE#dti!+ɏ#E4ŲMǚc̒!L͌ցἵo˧Jsl, ܉sbjͳgpGRxMg;m_#3._őizf뛞G[!d%Aq, n4;М11c&8SV_ [_|wXȼ\_.=@h<u_ E<% ed}ikT#"_=v;TpSI8?]8HfLݳ+x8N\^ á xl ^?Qdɻ<{YPdz'8Dx{[ {* Zxde1btP34rz OoS{pJnUM峾ﻀNn^z${$N-||$'[mP }?= cGa8?߬pt=ߝE?6p}-YTIsicax[`gֳ笟[΢Nyn{G cQn ak'K!u2ފԶ4gK,::]DX~)Is1?܂MjFH6*u8y}y0Pd퀣DI^ Fь Y;TY<̝K)yu& bv^Npao _+熈poE!DDu͔Y"I44q`aeVW3{ }A2-ul*Õ61#O8\dW6bkZ `ނ r :zz&5+ҁyRq% 줠f-6]9J~qmKʛLa Ytb;i䶴(=A*4iE(iƲ*L9cRe쎚NJP ]Q\NxV>qgEjeag=Lw{B3 q,)o/ۓ1/r չ88s!Jv=̌|^:u);IcPuu, 6L̝`] C9&8\}erSVpx\i/?@aY\zJr9xe:)A'88vja97؊x)okgp0KC0WNog ML&! 'OhA;RΫ{S!;x[d & 7 |oҀȞ\ Vzdm&mȮ?{Us;pNeSGΦVڬJ7ֵEs[0&o3$pwoli`n6C+G.3?aL6FXFh1{~l 'g9 HeeӐ OmT6?T Lp|gM;YQ3,C/y|A+CUS0-^XJppY%"2*.;;ͤX!tRKl|zA !̱w"j02{E{Exa߂`}Ӂ0s9Q[,1԰qlĦ'*׽Y$(cgGwvqW ST9m TG1*+':n vP=)s^YvqH1H^ZESp,+n{o{oYmI=kq2DC);|O+CX8)5=ASgdIL7;&@lսe{f X>$j4)B?*vFAN ϶FQ d ۠vE2ErO_o`Г!Q3Ybh'OO]i%3$;Ju <,LYO^]r-ޒAf#v!Z[Z]TUP, '9+v[5ͬ⺡ǜ/!6pi3q4sYcylq0v &/@rL;ww!~j+VB\>&uaK ؟%jeEls]M,B́{4\" KڛB Gk7_gjbnZ-z 3:nM~\P˂`3loEy[aTT ]{ Gy`8v@bbwL`[&4GE{wyG5]pq#xrsIF&q4kgFNn]N>ѡ]Ϫnryhn(NlJmh0UmWmC W lVώfFv)GF[:>aEXKFQVƦ<5vvձl!+MN%3MGG5۽tUX&CeNiQL˹w8d}:bm<]pdS鑺jBY2chdrۭ_nE. -|.Ntx/1 ВnΪ|~kt-YyCul6Fd>?pplr~ےqHۇohGZz?:ki >8=fV-L wb6 "۷=7zĄ;>B;1D~6ׄwbwpTW +~T O[WOch~ҡ4R3ʂK|bO> b, 66.` vξ01WKRDdzs`ap'{N\܍p6aeɜt1A~@o6WEZix̙J7qڌ2&ɽFw#U#6}.'rTV_K1bkw7uɬ=);+H(J+.BG1tƨ$4beNpyuv ry YY7:Iwh+it 1bRRH,`'8Z$"qbܞ`w֖S:Zp[@TO.pB77XљFM&0' JZwRűؤ(JNɛR\G;8,!`e.>=_O,Ӯ݄kF~sm|2dp׫W/ᇯc50OBn6C]z\7Lpn;gD9ܿ;ڐFnYz~??(Ꭹ]_PSۋ365lN1 pcm1vpDDt9)MZk=G]nrw$ݟe_LQd%X'$ $GsۘqV,C0dOȺ6º->Y€BllrϘoκbZҸm.. [C5',tֱc*K&(l ݖz.=1`VH7eyu$(L]w+goo fE =rճmG 7%=wD. S)5]6{Y}HR;p}_23oGR]pp +fbm 嶛Bg5[s)v$]Lfz-o[ۧ>)*d?=ݓ꼽PF :ƢE}JOyP{1fqykx@gUgb>ժ깮˦P_Hsճ"Vg-Qgh!SSToK QI$'C_<¤L&Ђ^ʎb#qTIyN1xN-7_".{=[U4t~cBmy5C\]l [33y^ĴrP7}B?S4q߃Wpe=3Oí]ް0n=O6%[vV 7=<O[^VQ,<_~kh0_ź,GsKKFs#ku(gHM=ݛ[>4k):=U!|R[ >q,#(өk|;$hv&?s^/Wa/v IDAT*ZZA9,.\х?b \6ceW ̬Z[!$mV|q[DDvq58w\;{wUp67rMlu:(c;2j _r{U,{.e2sHq al<k{9YĆ6-ÍtbG~0L:Ϊ}B#jXr~ve imzy?>L{ a/Ns5 `1UFRˏ3ڼ\q|IrȬ/X0j:jMhHSX:-4;|-빡,_ʆoyЂ 71M,لT6˸'3*isټ;o` ;oOV?LpCBm1{9̳SQJMkL7ՓcӟbG 4ZS [)A$>ӝ%xΦ}f6d[K$oSRrÌvlGge@7mJZp̴vq EAC_;J_Z&܄pOsM ܒRyjqWM}2߆G`"%v(+F9wH ǥ&wfƶ/N8EŹ.`<_IDDDDDDƛ  pmpcx;5(4G.ljgQ85AgkWq;^DDDDDD|8S˦18YZ\C0熜]ct9^c<ё۸lV-WX==LDDDDDDƍ;8sbR f ?N奱v9[(w(U)u(OfGio.DDDDDDDμ g@YDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDg ,8e,""""""⃂ """"""">(8,""""""⃂ """"""">(8,""""""⃂ """"""">(8,""""""\ +2SH)|֊\zu:_i;E@ Ҧq`~?ӹaNzMkcrKX.Kcu+3;'_(-7n@ OxĠ~)>ZGmB3#X{=??f"DˉJvTibƴHb =M!=ʟ||$EQl|ZLC]GRYDDDDB2Z-SRxt7zOmUqJy+*ψ%utp;rY%EIi>ΪN:@i g[2Vd-Z7{Q|/qξuh؇EVe$99Kki1.m.!.z6'`ΚrN`:rbr9s B٣%ۨEGxR ޥUNKx 8Z{U+Vwc!eab޴: z6&lNzp_wu'GM{&J]t㢬Y@MF:(fMdmOz\PUp,y@Ǵ), kڛ{MUMće78{:I[7솀s3s 39[aWMGv2OG}m}vwPBKf*/xL;Dӣ}pG{K(pau#S>]>{V_tm쪓T(n ߿& *jw4R5M1Qi]ma8]'|E7~ nr䄘v#}WOUYDDDD+b\gr[ۀQY]:_q9㓴vD\?ut P,?"Na?>aG[>pui%jVn(K6;a|?1, FŸK#Xw'zjfFGpi XZSo35unfGFYOMkJ;qvcwB N;uK]v;AUmN]MBwC=탓6\j _$ψ!%(m&8̌odjۧ[>Dt5j֍ F*kvRUV5$M@#NDeU}>m!(fx 3~T՟rrPa0n6ޡp+"""""#:j)8p~,9).:F.7,""""""⃂ """"""">(8,""""""⃂ """"""">(8,""""""⃂~'Osy\$+'U|B%zUǧ,M9gf~4~.W^ Z\3e.f ^1%ƛ;s)jHncdy1uL; qNz'{ L`уO]EDDDDDd^ "k䯯–'r7I^$V5`g5o'_`wsGv6-ʄEzͿz𝤾@k2WɱyL 8;8>HfABB$O?O_`YE򲈈yiwvPr?$E%~{7L7V/ذ"zՏ ;LqvTcȺUW<5YǏ|>v { ܴn@Xit oP>` [q&㒙$%D,"""""r \(D؁AKZr0 x[!E.B &Tp3rϾECTIXv+ˉrq<`҄k @!Y#!Bp[b$(jp TTtē᤼yN,"""""""8{?lN:I@Hp !$\ͮ@DDDDDDcop>c1j]ac!<SMDDDDDD ;BH0$h-^A^i_%""""""Lpv9\>%I]D`d!MT{78r9KG,f#Fp""""""2}e'yՃ4;ipoˈƙ+:xgy#grrx ;an38΃4 """"""硯ͺnL_0D$G%pɪYkclp"/ cy,ro:̳/=ν/EG([EDDDDDd|;ys]Y64p3DUDDDDDD+U[DDDDDDt(8,""""""WgӥgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgEDDDDDD|PpAYDDDDDDgApdtӇoUeE:ȧ2Zٷ&U;i=GVz& /~J3֑ttVlW*=5(r'Xt6sϘ>lY餧g9F\]9<Ίg""""">u`=W8<;j0a 2,L'k&>b1wd"OgCWd)wr?wݖK',L p#mdzطޭw'sLe\l8'\q,WYϲ8Aeq9-#'1i$&M#b,DhN'-^'TV`n}! us|V0s͝#Xg#8wQYTYXLVyL4ckz8 GFh޷'?jzq2q6?q?Tg +g5kX1/I?fXx g+#n8G_6mW=˰{y4sF_ߋ ϸΣhgdx%mspW.*w>̞YkذoO=]32{g}ot3Y\Ωnbl+W m;,ϝ*K~'V-&=9>پkL]|[ҷLlSe$}b+؛Yn|&U|®r=`Y6o஬ YfUz:wu1M'c>Zt+c5d>[8VdΖ5^㹬WGXɆz5 mbVr';i׳5/]"l /椡> {5-|-I.$8U~MDf$uܷ2 e_9xfӬ>1?ZϪ!܂Fvx^_A</#sŅgt=GλTug9_=Kg?c/fC;<HWw?ܽmas39$#$ h BSUҚ23uU (1UbB~ U _evzK"enaZ(v85=6|8`x=:K9?{>sB;vM[ET*OFV4-OgG6A]] yϽ*_?~W:o0D r?T=?NNmM'gVK=7y'%S;slb;} /lɚ(BM9rϻp],5r`k91LavPldGD7_;s 1D]n6BѩF:΂RdB^OEj4:cT/Iӹ~S`,uqyas G3 eՏ7= qwQ=+P=[qi7έ &t;VGLS̲iW8׆q8P\L'}ě9MJ/uT* l֚bvU4DGET̖B6jivEZt ~eg oYZWCYWH 3E5ab',q9+G07 IDAT ޘ`eqI{8/I#$jS uNX^XI\IKE1Gk<,Z9!m[8Ax~J4Fsr69=ֶK%#- <->[vv»vm[ aǚ|| BlfJVQĶ.d.lWGApr2"UC^q3_Yg7>kό⮥GkhvfQF* Abv"erNgyUm)ٜJ8_ #ݬcߖap$29J9+bl%6Vm3I\MeWJ}`8^Aj\&Gm t0E9`&\&'re\f &|v&6\Tҙ_FGB23lxZx_Y,OƖ3؞坾7)j"g:R4o!Iw7\.LHʛL zI>|}: s n).UEg@CKmdI';b*kiv%g`kYKj )4e,=ࢦMW5$siiL|jFZزjj|pX3:X:RnOM\ /*މ4K%#5!QZz{;3eg8ذ ^`͍fGp咿UϼDS`N`:kh\WrYbH%-1ݹ,Ǜ<<9%Yi!>nj+11+q|sS[!x,DVgl'f:J)MYBs\&]٢T2 "g |ffj_fyδ:X:*>mM\Rf,fsnvo Bo>}Ĭi}|^ؿSs K+;6e<΁2?aϞ= xh3y3۷y154e-7g^:]~U1,K&=;:IIs6O[/6!ϻ1+ܗ덍㤺 GT8|읣zHu=wH?WmColmvK+Z8WZOJwp1: o@939뾄xvȦ:7WNSrSNBSRXu j֝#_|miox<GVΝ\9w*|;bSrΛn8Y1 mts8sϤ[D3)s}s출4!${NAIQ/H|!3 heAI IHXx .g#`{‚80[]`6QYo{w:0XQ̇g4|Bf l[p'bAqIP]a7Kbɜ1 g= }&t :) hf<f3 "=m04T$1#6[FZ_As|D;nAٞ0BfL+ws /h{I[o>a$_aqhhYE+qAxzPq4Sy{1cKz[ä {|2сDp\8֦ZZ$21: ޖ5}HE'ZOhB9Uu͸ V2ҠUcόD61j#<ѳ!$8\e~ZOyU-AHJR~aON`G拳0=B{|l2ǵ zuaLe=D#H)j2G~|ܿZ/ 糾:29*f.ZEPRccTsG\ =tyޒp,䝝i7$⾓awdNSw4'O NqFft_A>Vp~lN\N̰c` .\mm荿#rtC3 ׍/gݴtx/  ugQ4{d\nd~4.\3>3 l |ǏphANga4hWCE%\jL# ,'׿~t\mFN\FL8ko#0 >m&ctspr4 (AGf IFG l%-{qݢΎ1>l1̎ad+'\%FH2/B^>Ȓҗ;}tavԳo:o21; ͅ8eg2xuT 8g[#k̑-/Z6֑DžQ `ĹJϲYY;Ը-~u4{vM\&W`M_0o-e7f@$j-5GQUxAXYr &mV c}APb^ f(:Ig`"Y=8e7`{7lmƉ@},iorj Ǽ_G6L7wӛe;-4ֺR Qg=U{xlzNds:*NN_y+qȻ]\e'fByy>?k+{SNS_izmOrtA@5N$jFPSVK+BU{&-nz:ywPuܤw{(q?;ቄ4j8ξ[Gu!883HVU9MݸpFMF<Z[300;#Ll(t[35X}6I :oi>}eC lQӑO7Κgj]D{_l8̙ܰ);(>ZSNsoI]q<5IxfhYg<nu_*\U6hw6jpfY*D5+>cmw0 D8Q᭓{ŜoeX/K9*VOG&s[(/G뽣tۈX-[)XɏaͮMOQTp٭yl)n8jJ} "2rɌ`IZ4y^d ,> @ML#~m]Dty1[ND+l#i-B$ϖuE#r!+<zѻGOfRI #Q:E鸀m6?I$ge`]AId6SsM0,y>ix]f^ 乨@%p|kdW8p%0i%;aF fQB9_˦v1*8x9`ʨ!NvR3$f1CJ`EꅾBK&9(ܙMAHBdgSh&ݞښ |BXy#g}Ք؉ee1mb6P%VŚ>'1+z ]L qEw!,Hf߾p0K`F޹Zn?8ĝ!D'Ehآ9: #6ta\Bw߶/")7kp9np}Lb|̦bYn<VH+sjyRMg"Dia%R5{q(X:11xe>3fr66491I͊l!wA_yinL(&%獋 O"|?<=8cej}oz3-H#9`dc˚@df2-ϙG+|:nq1} IXI@aR 1N~nL9(kkQj$;m`AX|}ers, !11¼ @XRwqk3nOݿyg;fN19D">=4'ƽ?&:_ڴ&:ar۽s_S+ljD|ۤ7{atGX>tϻ_0º?{s+U0ݸ7;ι˄v_8XiI#utR8ey¨Ư7ϊ}G8g#V)cHX2V{|jɤ$zQ`9.LQd15&#ww%"b gI vt_ֆNt\ (Y0!DG17Q#xiϙ0MFK3r*$jJ&:KSKQϿrm,\@B66BߧvOt"""""""wrTmYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,Lt"&:{ޤILXXD"""""%"裏>DqYDDDD>wJEQww7n{øotwwOt""""R,2n7===}C7!DDDDd"(qGn[RYDШ"7ykK{uk%a~:T@Vu,,;26qq%έޖΆ&LLd2>Qaߤ9-P ===x?oaS#,.%Q>}ww?E7^!G8˺r=nYMvB, Y'n;;=tֱ+\~t'ܙEthfٛ TSlZ9_V3<9!w6#8.#^:n)g,,c*W9?bO?Gpޖ+ar:g<#xw~7NŐ77h3E/1 h(""""{yfƻٳ=0 0,Q$fgi`"hAܐݏ?o㩂͍Q()ct&˭xoi_g>̣_o%O<O-Oz?{d6蝮;](O<-'ufz{[9yuZ׎%;IV?YgO<1scݵ;ܺA~Y)$6IDAT;'huK)iSM'ӟbnv5-Y$Ħ~癐L:nj&}o^wOr9JBŮ$'S͏t)O^KQum?;N2w.֦0?!DDDDvվIKM1onH'==uۊYǾ(oK^du^.W.IO_Ƕ o]][nz6cۆ՞kh*bۆu}zc{o+mX8ԻWgH7)6(ސwys]:ɞ[\d=G8˟~NxgƷS>,PVdůW.յVsd]!u==\_Zjkk8sj{pU?aou-ms/l|V( 0zzZoKVR.~Zj,β)d"獊$-N̞ßl"e9(dS1NmMgGc̣qG&ݒs"{-{ۗ?/P[dzRv;~.^Ð9AL.e}f-'>mOw7κ]]5?&I_.vez-9QsޟȁymG̦#(¾0IuP›zLbX6J9P^DICrvEBvʙ,w7~l?]6k\mc>jk'1<>`LNjs=75`|_"?=g'Y+_{,G"Ì?<-˲w1/wׯЎPG ON]H~']_4v9pם8g;6B/#ɪ/EWw? ڹй便],+-|K^ m:88{`|i-]]0ryܓ̌whlYsb3p l ;сI n5𡿴u7rL4'Au Jj{o=Ǯ̂1ϿWF0w0ljSo-""""Czg8r| &`QU|[?.8i5p"MjG/DXgT5lqEl'$1ǥcT`⢩(pɝֆnD3wC CwNOA_Mgzؓk'? =|qe,z6g'ޟJXOsrO0{l{ٽgzo}{GN|c _7S]HZ\lόj:ƏKXͿze6c[NRH\F 1Py&M_D!"""" &""""""bAEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(q3\vmC)S&:[|v':jXP,""""""bA%"""""""8XP,""""""bA%"""""""8XDG]]]\zk׮aD3 `ʔ)L:__߉GDDDDdL}v':IWWox衇塇g}FWW===DDD(yjWC@$@N4WNt8"""""cJv˵k&: 1Yd4졇z g JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,LtwĬu;8}񼔛F1!Q/  """"""Jff(CuSv%rYl} =|'|'||+lqP \R]'2Y(@Hfr O㊊|5$`.3Ã1DB8G8"tPs‚HabIz̸@nyLDԸf`ZEDDDAsO&η{ƹn#, a։9܆%Ol:C2j]OnVg6ȃmtft`=]ٿg{fOLEM擏c6_BL6lq#l>gh(A't!(,;&-5'8}0YᇁV@ W3u .*-l_aGc߽x{sfyP7-#HZFH~^\w$1͒Ea4V9 h9["y[0K)UB n=AO2eD1a]FLLD!""""2fgqYDDDDDDĂg JEDDDDDD,(qYdg}6aL> 0&: 1YdM2cBtuu=?%""""'%"clԩ|gttt<0-ϽԩS':1 D7]]]\zk׮aD3 `ʔ)L:__߉GDDDDdL)q"""""""8XP,""""""bA%"""""""8XP,""""""bA%"""""""8XP,""""""bgvD """"""w)SLtvOt"""""""w+uYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(q3顳.zzz&:L4 ___l6&ݝm_pv&M][X""""""2zzz2+"p7\O?pnRkz졇4͉ĹG-""""""I&ݵ])[YDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,Lt""""""w4r_ihvO;':֬ӱ_3Ot(c n=A\v >aܠ7zDqOygF<\.L2j g':{?}P5!9%""""""8wDpia)q1s?>YDDDDDDĂg JEDDDDDD,(qYDDDDDDĂg JEDDDDDD,(q3̞ܿ531hW:~k>+O]GwKwSfQ)~_P:;wca_XWǥP9JEDDDDDɔ_opYYGÿK֧YϧǤ|"ėb[K>_|5sO_|o;8?{WcwX ˿w&%˷PWmqaQZ wz?L;yvUǧ h-6@D6`>pc!JEDDDDDgL=EW̞<}yl,'i3kWC}OÿW}ǝ^jXXm.qgWrGo7)}뿫πt_HS)W~gdÕz)qGl:GsOE֟; 8ßNs7y+V~#z8|勤<PRQZ\c,-"""""2.V'0tsEO8pC~_fZ;|󭸰EXSqq:^}}ߣjE^?UC805ppppppp@sT^W TDa VU*t:E n7r3v5<<<^/677nTբjam pJ(QV3 ya0DfIENDB`django-import-export-2.7.1/docs/api_admin.rst000066400000000000000000000002641416107567000212260ustar00rootroot00000000000000===== Admin ===== For instructions on how to use the models and mixins in this module, please refer to :ref:`admin-integration`. .. automodule:: import_export.admin :members: django-import-export-2.7.1/docs/api_fields.rst000066400000000000000000000001151416107567000213770ustar00rootroot00000000000000====== Fields ====== .. autoclass:: import_export.fields.Field :members: django-import-export-2.7.1/docs/api_forms.rst000066400000000000000000000001601416107567000212570ustar00rootroot00000000000000===== Forms ===== .. module:: import_export.forms .. autoclass:: ImportForm .. autoclass:: ConfirmImportForm django-import-export-2.7.1/docs/api_instance_loaders.rst000066400000000000000000000003131416107567000234460ustar00rootroot00000000000000================ Instance loaders ================ .. module:: import_export.instance_loaders .. autoclass:: BaseInstanceLoader .. autoclass:: ModelInstanceLoader .. autoclass:: CachedInstanceLoader django-import-export-2.7.1/docs/api_resources.rst000066400000000000000000000006601416107567000221500ustar00rootroot00000000000000========= Resources ========= Resource -------- .. autoclass:: import_export.resources.Resource :members: ModelResource ------------- .. autoclass:: import_export.resources.ModelResource :members: ResourceOptions (Meta) ---------------------- .. autoclass:: import_export.resources.ResourceOptions :members: modelresource_factory --------------------- .. automethod:: import_export.resources.modelresource_factory django-import-export-2.7.1/docs/api_results.rst000066400000000000000000000002131416107567000216310ustar00rootroot00000000000000======= Results ======= .. currentmodule:: import_export.results Result ------ .. autoclass:: import_export.results.Result :members: django-import-export-2.7.1/docs/api_tmp_storages.rst000066400000000000000000000006311416107567000226430ustar00rootroot00000000000000================== Temporary storages ================== .. currentmodule:: import_export.tmp_storages TempFolderStorage ----------------- .. autoclass:: import_export.tmp_storages.TempFolderStorage :members: CacheStorage ------------ .. autoclass:: import_export.tmp_storages.CacheStorage :members: MediaStorage ------------ .. autoclass:: import_export.tmp_storages.MediaStorage :members: django-import-export-2.7.1/docs/api_widgets.rst000066400000000000000000000014301416107567000216000ustar00rootroot00000000000000======= Widgets ======= .. autoclass:: import_export.widgets.Widget :members: .. autoclass:: import_export.widgets.IntegerWidget :members: .. autoclass:: import_export.widgets.DecimalWidget :members: .. autoclass:: import_export.widgets.CharWidget :members: .. autoclass:: import_export.widgets.BooleanWidget :members: .. autoclass:: import_export.widgets.DateWidget :members: .. autoclass:: import_export.widgets.TimeWidget :members: .. autoclass:: import_export.widgets.DateTimeWidget :members: .. autoclass:: import_export.widgets.DurationWidget :members: .. autoclass:: import_export.widgets.JSONWidget :members: .. autoclass:: import_export.widgets.ForeignKeyWidget :members: .. autoclass:: import_export.widgets.ManyToManyWidget :members: django-import-export-2.7.1/docs/bulk_import.rst000066400000000000000000000057641416107567000216460ustar00rootroot00000000000000============= Bulk imports ============= django-import-export provides a 'bulk mode' to improve the performance of importing large datasets. In normal operation, django-import-export will call ``instance.save()`` as each row in a dataset is processed. Bulk mode means that ``instance.save()`` is not called, and instances are instead added to temporary lists. Once the number of rows processed matches the ``batch_size`` value, then either ``bulk_create()`` or ``bulk_update()`` is called. If ``batch_size`` is set to ``None``, then ``bulk_create()`` / ``bulk_update()`` is only called once all rows have been processed. Bulk deletes are also supported, by applying a ``filter()`` to the temporary object list, and calling ``delete()`` on the resulting query set. Caveats ======= * The model's ``save()`` method will not be called, and ``pre_save`` and ``post_save`` signals will not be sent. * ``bulk_update()`` is only supported in Django 2.2 upwards. * Bulk operations do not work with many-to-many relationships. * Take care to ensure that instances are validated before bulk operations are called. This means ensuring that resource fields are declared appropriately with the correct widgets. If an exception is raised by a bulk operation, then that batch will fail. It's also possible that transactions can be left in a corrupted state. Other batches may be successfully persisted, meaning that you may have a partially successful import. * In bulk mode, exceptions are not linked to a row. Any exceptions raised by bulk operations are logged (and re-raised if ``raise_errors`` is true). * If you use :class:`~import_export.widgets.ForeignKeyWidget` then this can affect performance, because it reads from the database for each row. If this is an issue then create a subclass which caches ``get_queryset()`` results rather than reading for each invocation. For more information, please read the Django documentation on `bulk_create() `_ and `bulk_update() `_. .. _performance_tuning Performance tuning ================== Consider the following if you need to improve the performance of imports. * Enable ``use_bulk`` for bulk create, update and delete operations (read `Caveats`_ first). * If your import is creating instances only (i.e. you are sure there are no updates), then set ``force_init_instance = True``. * If your import is updating or creating instances, and you have a set of existing instances which can be stored in memory, use :class:`~import_export.instance_loaders.CachedInstanceLoader` * By default, import rows are compared with the persisted representation, and the difference is stored against each row result. If you don't need this diff, then disable it with ``skip_diff = True``. * Setting ``batch_size`` to a different value is possible, but tests showed that setting this to ``None`` always resulted in worse performance in both duration and peak memory.django-import-export-2.7.1/docs/celery.rst000066400000000000000000000003331416107567000205650ustar00rootroot00000000000000=========== Using celery to perform imports =========== You can use the 3rd party `django-import-export-celery `_ application to process long imports in celery. django-import-export-2.7.1/docs/changelog.rst000066400000000000000000000336321416107567000212410ustar00rootroot00000000000000Changelog ========= 2.7.1 (2021-12-23) ------------------ - Removed `django_extensions` from example app settings (#1356) - Added support for Django 4.0 (#1357) 2.7.0 (2021-12-07) ------------------ - Big integer support for Integer widget (#788) - Run compilemessages command to keep .mo files in sync (#1299) - Added `skip_html_diff` meta attribute (#1329) - Added python3.10 to tox and CI environment list (#1336) - Add ability to rollback the import on validation error (#1339) - Fix missing migration on example app (#1346) - Fix crash when deleting via admin site (#1347) - Use Github secret in CI script instead of hard-coded password (#1348) - Documentation: correct error in example application which leads to crash (#1353) 2.6.1 (2021-09-30) ------------------ - Revert 'dark mode' css: causes issues in django2.2 (#1330) 2.6.0 (2021-09-15) ------------------ - Added guard for null 'options' to fix crash (#1325) - Updated import.css to support dark mode (#1323) - Fixed regression where overridden mixin methods are not called (#1315) - Fix xls/xlsx import of Time fields (#1314) - Added support for 'to_encoding' attribute (#1311) - Removed travis and replaced with github actions for CI (#1307) - Increased test coverage (#1286) - Fix minor date formatting issue for date with years < 1000 (#1285) - Translate the zh_Hans missing part (#1279) - Remove code duplication from mixins.py and admin.py (#1277) - Fix example in BooleanWidget docs (#1276) - Better support for Django main (#1272) - don't test Django main branch with python36,37 (#1269) - Support Django 3.2 (#1265) - Correct typo in Readme (#1258) - Rephrase logical clauses in docstrings (#1255) - Support multiple databases (#1254) - Update django master to django main (#1251) - Add Farsi translated messages in the locale (#1249) - Update Russian translations (#1244) - Append export admin action using ModelAdmin.get_actions (#1241) - Fix minor mistake in makemigrations command (#1233) - Remove EOL Python 3.5 from CI (#1228) - CachedInstanceLoader defaults to empty when import_id is missing (#1225) - Add kwargs to import_row, import_object and import_field (#1190) - Call load_workbook() with data_only flag (#1095) 2.5.0 (2020-12-30) ------------------ - Changed the default value for ``IMPORT_EXPORT_CHUNK_SIZE`` to 100. (#1196) - Add translation for Korean (#1218) - Update linting, CI, and docs. 2.4.0 (2020-10-05) ------------------ - Fix deprecated Django 3.1 ``Signal(providing_args=...)`` usage. - Fix deprecated Django 3.1 ``django.conf.urls.url()`` usage. 2.3.0 (2020-07-12) ------------------ - Add missing translation keys for all languages (#1144) - Added missing Portuguese translations (#1145) - Add kazakh translations (#1161) - Add bulk operations (#1149) 2.2.0 (2020-06-01) ------------------ - Deal with importing a BooleanField that actually has `True`, `False`, and `None` values. (#1071) - Add row_number parameter to before_import_row, after_import_row and after_import_instance (#1040) - Paginate queryset if Queryset.prefetch_related is used (#1050) 2.1.0 (2020-05-02) ------------------ - Fix DurationWidget handling of zero value (#1117) - Make import diff view only show headers for user visible fields (#1109) - Make confirm_form accessible in get_import_resource_kwargs and get_import_data_kwargs (#994, #1108) - Initialize Decimal with text value, fix #1035 (#1039) - Adds meta flag 'skip_diff' to enable skipping of diff operations (#1045) - Update docs (#1097, #1114, #1122, #969, #1083, #1093) 2.0.2 (2020-02-16) ------------------ - Add support for tablib >= 1.0 (#1061) - Add ability to install a subset of tablib supported formats and save some automatic dependency installations (needs tablib >= 1.0) - Use column_name when checking row for fields (#1056) 2.0.1 (2020-01-15) ------------------ - Fix deprecated Django 3.0 function usage (#1054) - Pin tablib version to not use new major version (#1063) - Format field is always shown on Django 2.2 (#1007) 2.0 (2019-12-03) ---------------- - Removed support for Django < 2.0 - Removed support for Python < 3.5 - feat: Support for Postgres JSONb Field (#904) 1.2.0 (2019-01-10) ------------------ - feat: Better surfacing of validation errors in UI / optional model instance validation (#852) - chore: Use modern setuptools in setup.py (#862) - chore: Update URLs to use https:// (#863) - chore: remove outdated workarounds - chore: Run SQLite tests with in-memory database - fix: Change logging level (#832) - fix: Changed `get_instance()` return val (#842) 1.1.0 (2018-10-02) ------------------ - fix: Django2.1 ImportExportModelAdmin export (#797) (#819) - setup: add django2.1 to test matrix - JSONWidget for jsonb fields (#803) - Add ExportActionMixin (#809) - Add Import Export Permissioning #608 (#804) - write_to_tmp_storage() for import_action() (#781) - follow relationships on ForeignKeyWidget #798 - Update all pypi.python.org URLs to pypi.org - added test for tsv import - added unicode support for TSV for python 2 - Added ExportViewMixin (#692) 1.0.1 (2018-05-17) ------------------ - Make deep copy of fileds from class attr to instance attr (#550) - Fix #612: NumberWidget.is_empty() should strip the value if string type (#613) - Fix #713: last day isn't included in results qs (#779) - use Python3 compatible MySql driver in development (#706) - fix: warning U mode is deprecated in python 3 (#776) - refactor: easier overridding widgets and default field (#769) - Updated documentation regardign declaring fields (#735) - custom js for action form also handles grappelli (#719) - Use 'verbose_name' in breadcrumbs to match Django default (#732) - Add Resource.get_diff_class() (#745) - Fix and add polish translation (#747) - Restore raise_errors to before_import (#749) 1.0.0 (2018-02-13) ------------------ - Switch to semver versioning (#687) - Require Django>=1.8 (#685) - upgrade tox configuration (#737) 0.7.0 (2018-01-17) ------------------ - skip_row override example (#702) - Testing against Django 2.0 should not fail (#709) - Refactor transaction handling (#690) - Resolves #703 fields shadowed (#703) - discourage installation as a zipped egg (#548) - Fixed middleware settings in test app for Django 2.x (#696) 0.6.1 (2017-12-04) ------------------ - Refactors and optimizations (#686, #632, #684, #636, #631, #629, #635, #683) - Travis tests for Django 2.0.x (#691) 0.6.0 (2017-11-23) ------------------ - Refactor import_row call by using keyword arguments (#585) - Added {{ block.super }} call in block bodyclass in admin/base_site.html (#582) - Add support for the Django DurationField with DurationWidget (#575) - GitHub bmihelac -> django-import-export Account Update (#574) - Add intersphinx links to documentation (#572) - Add Resource.get_import_fields() (#569) - Fixed readme mistake (#568) - Bugfix/fix m2m widget clean (#515) - Allow injection of context data for template rendered by import_action() and export_action() (#544) - Bugfix/fix exception in generate_log_entries() (#543) - Process import dataset and result in separate methods (#542) - Bugfix/fix error in converting exceptions to strings (#526) - Fix admin integration tests for the new "Import finished..." message, update Czech translations to 100% coverage. (#596) - Make import form type easier to override (#604) - Add saves_null_values attribute to Field to control whether null values are saved on the object (#611) - Add Bulgarian translations (#656) - Add django 1.11 to TravisCI (#621) - Make Signals code example format correctly in documentation (#553) - Add Django as requirement to setup.py (#634) - Update import of reverse for django 2.x (#620) - Add Django-version classifiers to setup.py’s CLASSIFIERS (#616) - Some fixes for Django 2.0 (#672) - Strip whitespace when looking up ManyToMany fields (#668) - Fix all ResourceWarnings during tests in Python 3.x (#637) - Remove downloads count badge from README since shields.io no longer supports it for PyPi (#677) - Add coveralls support and README badge (#678) 0.5.1 (2016-09-29) ------------------ - French locale not in pypi (#524) - Bugfix/fix undefined template variables (#519) 0.5.0 (2016-09-01) ------------------ - Hide default value in diff when importing a new instance (#458) - Append rows to Result object via function call to allow overriding (#462) - Add get_resource_kwargs to allow passing request to resource (#457) - Expose Django user to get_export_data() and export() (#447) - Add before_export and after_export hooks (#449) - fire events post_import, post_export events (#440) - add **kwargs to export_data / create_dataset - Add before_import_row() and after_import_row() (#452) - Add get_export_fields() to Resource to control what fields are exported (#461) - Control user-visible fields (#466) - Fix diff for models using ManyRelatedManager - Handle already cleaned objects (#484) - Add after_import_instance hook (#489) - Use optimized xlsx reader (#482) - Adds resource_class of BookResource (re-adds) in admin docs (#481) - Require POST method for process_import() (#478) - Add SimpleArrayWidget to support use of django.contrib.postgres.fields.ArrayField (#472) - Add new Diff class (#477) - Fix #375: add row to widget.clean(), obj to widget.render() (#479) - Restore transactions for data import (#480) - Refactor the import-export templates (#496) - Update doc links to the stable version, update rtfd to .io (#507) - Fixed typo in the Czech translation (#495) 0.4.5 (2016-04-06) ------------------ - Add FloatWidget, use with model fields models.FloatField (#433) - Fix default values in fields (#431, #364) Field constructor `default` argument is NOT_PROVIDED instead of None Field clean method checks value against `Field.empty_values` [None, ''] 0.4.4 (2016-03-22) ------------------ - FIX: No static/ when installed via pip #427 - Add total # of imports and total # of updates to import success msg 0.4.3 (2016-03-08) ------------------ - fix MediaStorage does not respect the read_mode parameter (#416) - Reset SQL sequences when new objects are imported (#59) - Let Resource rollback if import throws exception (#377) - Fixes error when a single value is stored in m2m relation field (#177) - Add support for django.db.models.TimeField (#381) 0.4.2 (2015-12-18) ------------------ - add xlsx import support 0.4.1 (2015-12-11) ------------------ - fix for fields with a dyanmic default callable (#360) 0.4.0 (2015-12-02) ------------------ - Add Django 1.9 support - Django 1.4 is not supported (#348) 0.3.1 (2015-11-20) ------------------ - FIX: importing csv in python 3 0.3 (2015-11-20) ---------------- - FIX: importing csv UnicodeEncodeError introduced in 0.2.9 (#347) 0.2.9 (2015-11-12) ------------------ - Allow Field.save() relation following (#344) - Support default values on fields (and models) (#345) - m2m widget: allow trailing comma (#343) - Open csv files as text and not binary (#127) 0.2.8 (2015-07-29) ------------------ - use the IntegerWidget for database-fields of type BigIntegerField (#302) - make datetime timezone aware if USE_TZ is True (#283). - Fix 0 is interpreted as None in number widgets (#274) - add possibility to override tmp storage class (#133, #251) - better error reporting (#259) 0.2.7 (2015-05-04) ------------------ - Django 1.8 compatibility - add attribute inheritance to Resource (#140) - make the filename and user available to import_data (#237) - Add to_encoding functionality (#244) - Call before_import before creating the instance_loader - fixes #193 0.2.6 (2014-10-09) ------------------ - added use of get_diff_headers method into import.html template (#158) - Try to use OrderedDict instead of SortedDict, which is deprecated in Django 1.7 (#157) - fixed #105 unicode import - remove invalid form action "form_url" #154 0.2.5 (2014-10-04) ------------------ - Do not convert numeric types to string (#149) - implement export as an admin action (#124) 0.2.4 (2014-09-18) ------------------ - fix: get_value raised attribute error on model method call - Fixed XLS import on python 3. Optimized loop - Fixed properly skipping row marked as skipped when importing data from the admin interface. - Allow Resource.export to accept iterables as well as querysets - Improve error messages - FIX: Properly handle NullBoleanField (#115) - Backward Incompatible Change previously None values were handled as false 0.2.3 (2014-07-01) ------------------ - Add separator and field keyword arguments to ManyToManyWidget - FIX: No support for dates before 1900 (#93) 0.2.2 (2014-04-18) ------------------ - RowResult now stores exception object rather than it's repr - Admin integration - add EntryLog object for each added/updated/deleted instance 0.2.1 (2014-02-20) ------------------ - FIX import_file_name form field can be use to access the filesystem (#65) 0.2.0 (2014-01-30) ------------------ - Python 3 support 0.1.6 (2014-01-21) ------------------ * Additional hooks for customizing the workflow (#61) 0.1.5 (2013-11-29) ------------------ * Prevent queryset caching when exporting (#44) * Allow unchanged rows to be skipped when importing (#30) * Update tests for Django 1.6 (#57) * Allow different ``ResourceClass`` to be used in ``ImportExportModelAdmin`` (#49) 0.1.4 ----- * Use `field_name` instead of `column_name` for field dehydration, FIX #36 * Handle OneToOneField, FIX #17 - Exception when attempting access something on the related_name. * FIX #23 - export filter not working 0.1.3 ----- * Fix packaging * DB transactions support for importing data 0.1.2 ----- * support for deleting objects during import * bug fixes * Allowing a field to be 'dehydrated' with a custom method * added documentation 0.1.1 ----- * added ExportForm to admin integration for choosing export file format * refactor admin integration to allow better handling of specific formats supported features and better handling of reading text files * include all available formats in Admin integration * bugfixes 0.1.0 ----- * Refactor api django-import-export-2.7.1/docs/conf.py000066400000000000000000000170411416107567000200530ustar00rootroot00000000000000import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.abspath('.')) sys.path.append(os.path.abspath('..')) sys.path.append(os.path.abspath('../tests')) os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' import django django.setup() # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'django-import-export' copyright = '2012–2020, Bojan Mihelac' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # try: from import_export import __version__ # The short X.Y version. version = '.'.join(__version__.split('.')[:2]) # The full version, including alpha/beta/rc tags. release = __version__ except ImportError: version = release = 'dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'django-import-export' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-import-export.tex', 'django-import-export Documentation', 'Bojan Mihelac', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'django-import-export', 'django-import-export Documentation', ['Bojan Mihelac'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'django-import-export', 'django-import-export Documentation', 'Bojan Mihelac', 'django-import-export', 'Import/export data for Django', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. texinfo_appendices = [] # intersphinx documentation intersphinx_mapping = { 'tablib': ('https://tablib.readthedocs.io/en/stable/', None) } django-import-export-2.7.1/docs/getting_started.rst000066400000000000000000000332411416107567000224750ustar00rootroot00000000000000=============== Getting started =============== Test data ========= There are test data files which can be used for importing in the `test/core/exports` directory. The test models =============== For example purposes, we'll use a simplified book app. Here is our ``models.py``:: # app/models.py class Author(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Category(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Book(models.Model): name = models.CharField('Book name', max_length=100) author = models.ForeignKey(Author, blank=True, null=True) author_email = models.EmailField('Author email', max_length=75, blank=True) imported = models.BooleanField(default=False) published = models.DateField('Published', blank=True, null=True) price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) categories = models.ManyToManyField(Category, blank=True) def __str__(self): return self.name .. _base-modelresource: Creating import-export resource =============================== To integrate `django-import-export` with our ``Book`` model, we will create a :class:`~import_export.resources.ModelResource` class in ``admin.py`` that will describe how this resource can be imported or exported:: # app/admin.py from import_export import resources from core.models import Book class BookResource(resources.ModelResource): class Meta: model = Book Exporting data ============== Now that we have defined a :class:`~import_export.resources.ModelResource` class, we can export books:: >>> from app.admin import BookResource >>> dataset = BookResource().export() >>> print(dataset.csv) id,name,author,author_email,imported,published,price,categories 2,Some book,1,,0,2012-12-05,8.85,1 Customize resource options ========================== By default :class:`~import_export.resources.ModelResource` introspects model fields and creates :class:`~import_export.fields.Field`-attributes with an appropriate :class:`~import_export.widgets.Widget` for each field. To affect which model fields will be included in an import-export resource, use the ``fields`` option to whitelist fields:: class BookResource(resources.ModelResource): class Meta: model = Book fields = ('id', 'name', 'price',) Or the ``exclude`` option to blacklist fields:: class BookResource(resources.ModelResource): class Meta: model = Book exclude = ('imported', ) An explicit order for exporting fields can be set using the ``export_order`` option:: class BookResource(resources.ModelResource): class Meta: model = Book fields = ('id', 'name', 'author', 'price',) export_order = ('id', 'price', 'author', 'name') The default field for object identification is ``id``, you can optionally set which fields are used as the ``id`` when importing:: class BookResource(resources.ModelResource): class Meta: model = Book import_id_fields = ('isbn',) fields = ('isbn', 'name', 'author', 'price',) When defining :class:`~import_export.resources.ModelResource` fields it is possible to follow model relationships:: class BookResource(resources.ModelResource): class Meta: model = Book fields = ('author__name',) .. note:: Following relationship fields sets ``field`` as readonly, meaning this field will be skipped when importing data. By default all records will be imported, even if no changes are detected. This can be changed setting the ``skip_unchanged`` option. Also, the ``report_skipped`` option controls whether skipped records appear in the import ``Result`` object, and if using the admin whether skipped records will show in the import preview page:: class BookResource(resources.ModelResource): class Meta: model = Book skip_unchanged = True report_skipped = False fields = ('id', 'name', 'price',) .. seealso:: :doc:`/api_resources` Declaring fields ================ It is possible to override a resource field to change some of its options:: from import_export.fields import Field class BookResource(resources.ModelResource): published = Field(attribute='published', column_name='published_date') class Meta: model = Book Other fields that don't exist in the target model may be added:: from import_export.fields import Field class BookResource(resources.ModelResource): myfield = Field(column_name='myfield') class Meta: model = Book .. seealso:: :doc:`/api_fields` Available field types and options. Advanced data manipulation on export ==================================== Not all data can be easily extracted from an object/model attribute. In order to turn complicated data model into a (generally simpler) processed data structure on export, ``dehydrate_`` method should be defined:: from import_export.fields import Field class BookResource(resources.ModelResource): full_title = Field() class Meta: model = Book def dehydrate_full_title(self, book): book_name = getattr(book, "name", "unknown") author_name = getattr(book.author, "name", "unknown") return '%s by %s' % (book_name, author_name) In this case, the export looks like this: >>> from app.admin import BookResource >>> dataset = BookResource().export() >>> print(dataset.csv) full_title,id,name,author,author_email,imported,published,price,categories Some book by 1,2,Some book,1,,0,2012-12-05,8.85,1 Customize widgets ================= A :class:`~import_export.resources.ModelResource` creates a field with a default widget for a given field type. If the widget should be initialized with different arguments, set the ``widgets`` dict. In this example widget, the ``published`` field is overridden to use a different date format. This format will be used both for importing and exporting resource. :: class BookResource(resources.ModelResource): class Meta: model = Book widgets = { 'published': {'format': '%d.%m.%Y'}, } .. seealso:: :doc:`/api_widgets` available widget types and options. Importing data ============== Let's import some data! .. code-block:: python :linenos: :emphasize-lines: 4,5 >>> import tablib >>> from import_export import resources >>> from core.models import Book >>> book_resource = resources.modelresource_factory(model=Book)() >>> dataset = tablib.Dataset(['', 'New book'], headers=['id', 'name']) >>> result = book_resource.import_data(dataset, dry_run=True) >>> print(result.has_errors()) False >>> result = book_resource.import_data(dataset, dry_run=False) In the fourth line we use :func:`~import_export.resources.modelresource_factory` to create a default :class:`~import_export.resources.ModelResource`. The ModelResource class created this way is equal to the one shown in the example in section :ref:`base-modelresource`. In fifth line a :class:`~tablib.Dataset` with columns ``id`` and ``name``, and one book entry, are created. A field for a primary key field (in this case, ``id``) always needs to be present. In the rest of the code we first pretend to import data using :meth:`~import_export.resources.Resource.import_data` and ``dry_run`` set, then check for any errors and actually import data this time. .. seealso:: :doc:`/import_workflow` for a detailed description of the import workflow and its customization options. Deleting data ------------- To delete objects during import, implement the :meth:`~import_export.resources.Resource.for_delete` method on your :class:`~import_export.resources.Resource` class. The following is an example resource which expects a ``delete`` field in the dataset. An import using this resource will delete model instances for rows that have their column ``delete`` set to ``1``:: class BookResource(resources.ModelResource): delete = fields.Field(widget=widgets.BooleanWidget()) def for_delete(self, row, instance): return self.fields['delete'].clean(row) class Meta: model = Book Signals ======= To hook in the import export workflow, you can connect to ``post_import``, ``post_export`` signals:: from django.dispatch import receiver from import_export.signals import post_import, post_export @receiver(post_import, dispatch_uid='balabala...') def _post_import(model, **kwargs): # model is the actual model instance which after import pass @receiver(post_export, dispatch_uid='balabala...') def _post_export(model, **kwargs): # model is the actual model instance which after export pass .. _admin-integration: Admin integration ================= Exporting --------- Exporting via list filters ~~~~~~~~~~~~~~~~~~~~~~~~~~ Admin integration is achieved by subclassing :class:`~import_export.admin.ImportExportModelAdmin` or one of the available mixins (:class:`~import_export.admin.ImportMixin`, :class:`~import_export.admin.ExportMixin`, :class:`~import_export.admin.ImportExportMixin`):: # app/admin.py from .models import Book from import_export.admin import ImportExportModelAdmin class BookAdmin(ImportExportModelAdmin): resource_class = BookResource admin.site.register(Book, BookAdmin) .. figure:: _static/images/django-import-export-change.png A screenshot of the change view with Import and Export buttons. .. figure:: _static/images/django-import-export-import.png A screenshot of the import view. .. figure:: _static/images/django-import-export-import-confirm.png A screenshot of the confirm import view. Exporting via admin action ~~~~~~~~~~~~~~~~~~~~~~~~~~ Another approach to exporting data is by subclassing :class:`~import_export.admin.ImportExportActionModelAdmin` which implements export as an admin action. As a result it's possible to export a list of objects selected on the change list page:: # app/admin.py from import_export.admin import ImportExportActionModelAdmin class BookAdmin(ImportExportActionModelAdmin): pass .. figure:: _static/images/django-import-export-action.png A screenshot of the change view with Import and Export as an admin action. Note that to use the :class:`~import_export.admin.ExportMixin` or :class:`~import_export.admin.ExportActionMixin`, you must declare this mixin **before** ``admin.ModelAdmin``:: # app/admin.py from django.contrib import admin from import_export.admin import ExportActionMixin class BookAdmin(ExportActionMixin, admin.ModelAdmin): pass Note that :class:`~import_export.admin.ExportActionMixin` is declared first in the example above! Importing --------- It is also possible to enable data import via standard Django admin interface. To do this subclass :class:`~import_export.admin.ImportExportModelAdmin` or use one of the available mixins, i.e. :class:`~import_export.admin.ImportMixin`, or :class:`~import_export.admin.ImportExportMixin`. Customizations are, of course, possible. Customize admin import forms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It is possible to modify default import forms used in the model admin. For example, to add an additional field in the import form, subclass and extend the :class:`~import_export.forms.ImportForm` (note that you may want to also consider :class:`~import_export.forms.ConfirmImportForm` as importing is a two-step process). To use the customized form(s), overload :class:`~import_export.admin.ImportMixin` respective methods, i.e. :meth:`~import_export.admin.ImportMixin.get_import_form`, and also :meth:`~import_export.admin.ImportMixin.get_confirm_import_form` if need be. For example, imagine you want to import books for a specific author. You can extend the import forms to include ``author`` field to select the author from. Customize forms:: from django import forms class CustomImportForm(ImportForm): author = forms.ModelChoiceField( queryset=Author.objects.all(), required=True) class CustomConfirmImportForm(ConfirmImportForm): author = forms.ModelChoiceField( queryset=Author.objects.all(), required=True) Customize ``ModelAdmin``:: class CustomBookAdmin(ImportMixin, admin.ModelAdmin): resource_class = BookResource def get_import_form(self): return CustomImportForm def get_confirm_import_form(self): return CustomConfirmImportForm def get_form_kwargs(self, form, *args, **kwargs): # pass on `author` to the kwargs for the custom confirm form if isinstance(form, CustomImportForm): if form.is_valid(): author = form.cleaned_data['author'] kwargs.update({'author': author.id}) return kwargs admin.site.register(Book, CustomBookAdmin) To further customize admin imports, consider modifying the following :class:`~import_export.admin.ImportMixin` methods: :meth:`~import_export.admin.ImportMixin.get_form_kwargs`, :meth:`~import_export.admin.ImportMixin.get_import_resource_kwargs`, :meth:`~import_export.admin.ImportMixin.get_import_data_kwargs`. Using the above methods it is possible to customize import form initialization as well as importing customizations. .. seealso:: :doc:`/api_admin` available mixins and options. django-import-export-2.7.1/docs/import_workflow.rst000066400000000000000000000142161416107567000225530ustar00rootroot00000000000000==================== Import data workflow ==================== This document describes the import data workflow in detail, with hooks that enable customization of the import process. The central aspect of the import process is a resource's :meth:`~import_export.resources.Resource.import_data` method which is explained below. .. function:: import_data(dataset, dry_run=False, raise_errors=False) The :meth:`~import_export.resources.Resource.import_data` method of :class:`~import_export.resources.Resource` is responsible for importing data from a given dataset. ``dataset`` is required and expected to be a :class:`tablib.Dataset` with a header row. ``dry_run`` is a Boolean which determines if changes to the database are made or if the import is only simulated. It defaults to ``False``. ``raise_errors`` is a Boolean. If ``True``, import should raise errors. The default is ``False``, which means that eventual errors and traceback will be saved in ``Result`` instance. This is what happens when the method is invoked: #. First, a new :class:`~import_export.results.Result` instance, which holds errors and other information gathered during the import, is initialized. Then, an :class:`~import_export.instance_loaders.InstanceLoader` responsible for loading existing instances is initialized. A different :class:`~import_export.instance_loaders.BaseInstanceLoader` can be specified via :class:`~import_export.resources.ResourceOptions`'s ``instance_loader_class`` attribute. A :class:`~import_export.instance_loaders.CachedInstanceLoader` can be used to reduce number of database queries. See the `source `_ for available implementations. #. The :meth:`~import_export.resources.Resource.before_import` hook is called. By implementing this method in your resource, you can customize the import process. #. Each row of the to-be-imported dataset is processed according to the following steps: #. The :meth:`~import_export.resources.Resource.before_import_row` hook is called to allow for row data to be modified before it is imported #. :meth:`~import_export.resources.Resource.get_or_init_instance` is called with current :class:`~import_export.instance_loaders.BaseInstanceLoader` and current row of the dataset, returning an object and a Boolean declaring if the object is newly created or not. If no object can be found for the current row, :meth:`~import_export.resources.Resource.init_instance` is invoked to initialize an object. As always, you can override the implementation of :meth:`~import_export.resources.Resource.init_instance` to customize how the new object is created (i.e. set default values). #. :meth:`~import_export.resources.Resource.for_delete` is called to determine if the passed ``instance`` should be deleted. In this case, the import process for the current row is stopped at this point. #. If the instance was not deleted in the previous step, :meth:`~import_export.resources.Resource.import_obj` is called with the ``instance`` as current object, ``row`` as current row and ``dry run``. :meth:`~import_export.resources.Resource.import_field` is called for each field in :class:`~import_export.resources.Resource` skipping many- to-many fields. Many-to-many fields are skipped because they require instances to have a primary key and therefore assignment is postponed to when the object has already been saved. :meth:`~import_export.resources.Resource.import_field` in turn calls :meth:`~import_export.fields.Field.save`, if ``Field.attribute`` is set and ``Field.column_name`` exists in the given row. #. It then is determined whether the newly imported object is different from the already present object and if therefore the given row should be skipped or not. This is handled by calling :meth:`~import_export.resources.Resource.skip_row` with ``original`` as the original object and ``instance`` as the current object from the dataset. If the current row is to be skipped, ``row_result.import_type`` is set to ``IMPORT_TYPE_SKIP``. #. If the current row is not to be skipped, :meth:`~import_export.resources.Resource.save_instance` is called and actually saves the instance when ``dry_run`` is not set. There are two hook methods (that by default do nothing) giving you the option to customize the import process: * :meth:`~import_export.resources.Resource.before_save_instance` * :meth:`~import_export.resources.Resource.after_save_instance` Both methods receive ``instance`` and ``dry_run`` arguments. #. :meth:`~import_export.resources.Resource.save_m2m` is called to save many to many fields. #. :class:`~import_export.results.RowResult` is assigned with a diff between the original and the imported object fields, as well as and ``import_type`` attribute which states whether the row is new, updated, skipped or deleted. If an exception is raised during row processing and :meth:`~import_export.resources.Resource.import_data` was invoked with ``raise_errors=False`` (which is the default) the particular traceback is appended to :class:`~import_export.results.RowResult` as well. If either the row was not skipped or the :class:`~import_export.resources.Resource` is configured to report skipped rows, the :class:`~import_export.results.RowResult` is appended to the :class:`~import_export.results.Result` #. The :meth:`~import_export.resources.Resource.after_import_row` hook is called #. The :class:`~import_export.results.Result` is returned. Transaction support ------------------- If transaction support is enabled, whole import process is wrapped inside transaction and rollbacked or committed respectively. All methods called from inside of ``import_data`` (create / delete / update) receive ``False`` for ``dry_run`` argument. .. _Dataset: https://tablib.readthedocs.io/en/stable/api/#dataset-object django-import-export-2.7.1/docs/index.rst000066400000000000000000000017321416107567000204150ustar00rootroot00000000000000====================== Django import / export ====================== django-import-export is a Django application and library for importing and exporting data with included admin integration. **Features:** * support multiple formats (Excel, CSV, JSON, ... and everything else that `tablib`_ supports) * admin integration for importing * preview import changes * admin integration for exporting * export data respecting admin filters .. figure:: _static/images/django-import-export-change.png A screenshot of the change view with Import and Export buttons. .. toctree:: :maxdepth: 2 :caption: User Guide installation getting_started import_workflow bulk_import celery changelog .. toctree:: :maxdepth: 2 :caption: API documentation api_admin api_resources api_widgets api_fields api_instance_loaders api_tmp_storages api_results api_forms .. _`tablib`: https://github.com/jazzband/tablib django-import-export-2.7.1/docs/installation.rst000066400000000000000000000102101416107567000217760ustar00rootroot00000000000000============================== Installation and configuration ============================== django-import-export is available on the Python Package Index (PyPI), so it can be installed with standard Python tools like ``pip`` or ``easy_install``:: $ pip install django-import-export This will automatically install many formats supported by tablib. If you need additional formats like ``cli`` or ``Pandas DataFrame``, you should install the appropriate tablib dependencies (e.g. ``pip install tablib[pandas]``). Read more on the `tablib format documentation page`_. .. _tablib format documentation page: https://tablib.readthedocs.io/en/stable/formats/ Alternatively, you can install the git repository directly to obtain the development version:: $ pip install -e git+https://github.com/django-import-export/django-import-export.git#egg=django-import-export Now, you're good to go, unless you want to use django-import-export from the admin as well. In this case, you need to add it to your ``INSTALLED_APPS`` and let Django collect its static files. .. code-block:: python # settings.py INSTALLED_APPS = ( ... 'import_export', ) .. code-block:: shell $ python manage.py collectstatic All prerequisites are set up! See :doc:`getting_started` to learn how to use django-import-export in your project. Settings ======== You can configure the following in your settings file: ``IMPORT_EXPORT_USE_TRANSACTIONS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Controls if resource importing should use database transactions. Defaults to ``False``. Using transactions makes imports safer as a failure during import won’t import only part of the data set. Can be overridden on a ``Resource`` class by setting the ``use_transactions`` class attribute. ``IMPORT_EXPORT_SKIP_ADMIN_LOG`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If set to ``True``, skips the creation of admin log entries when importing. Defaults to ``False``. This can speed up importing large data sets, at the cost of losing an audit trail. Can be overridden on a ``ModelAdmin`` class inheriting from ``ImportMixin`` by setting the ``skip_admin_log`` class attribute. ``IMPORT_EXPORT_TMP_STORAGE_CLASS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Controls which storage class to use for storing the temporary uploaded file during imports. Defaults to ``import_export.tmp_storages.TempFolderStorage``. Can be overridden on a ``ModelAdmin`` class inheriting from ``ImportMixin`` by setting the ``tmp_storage_class`` class attribute. ``IMPORT_EXPORT_IMPORT_PERMISSION_CODE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If set, lists the permission code that is required for users to perform the “import” action. Defaults to ``None``, which means everybody can perform imports. Django’s built-in permissions have the codes ``add``, ``change``, ``delete``, and ``view``. You can also add your own permissions. ``IMPORT_EXPORT_EXPORT_PERMISSION_CODE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If set, lists the permission code that is required for users to perform the “export” action. Defaults to ``None``, which means everybody can perform exports. Django’s built-in permissions have the codes ``add``, ``change``, ``delete``, and ``view``. You can also add your own permissions. ``IMPORT_EXPORT_CHUNK_SIZE`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An integer that defines the size of chunks when iterating a QuerySet for data exports. Defaults to ``100``. You may be able to save memory usage by decreasing it, or speed up exports by increasing it. Can be overridden on a ``Resource`` class by setting the ``chunk_size`` class attribute. Example app =========== There's an example application that showcases what django-import-export can do. It's assumed that you have set up a Python ``venv`` with all required dependencies (from ``test.txt`` requirements file) and are able to run Django locally. You can run the example application as follows:: cd tests ./manage.py makemigrations ./manage.py migrate ./manage.py createsuperuser ./manage.py loaddata category.json book.json ./manage.py runserver Go to http://127.0.0.1:8000 ``books-sample.csv`` contains sample book data which can be imported. django-import-export-2.7.1/docs/make.bat000066400000000000000000000120061416107567000201550ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-shop-discounts.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-shop-discounts.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end django-import-export-2.7.1/import_export/000077500000000000000000000000001416107567000205345ustar00rootroot00000000000000django-import-export-2.7.1/import_export/__init__.py000066400000000000000000000000261416107567000226430ustar00rootroot00000000000000__version__ = '2.7.1' django-import-export-2.7.1/import_export/admin.py000066400000000000000000000504741416107567000222100ustar00rootroot00000000000000import django from django import forms from django.conf import settings from django.contrib import admin, messages from django.contrib.admin.models import ADDITION, CHANGE, DELETION, LogEntry from django.contrib.auth import get_permission_codename from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.http import HttpResponse, HttpResponseRedirect from django.template.response import TemplateResponse from django.urls import path, reverse from django.utils.decorators import method_decorator from django.utils.encoding import force_str from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django.views.decorators.http import require_POST from .forms import ConfirmImportForm, ExportForm, ImportForm, export_action_form_factory from .mixins import BaseExportMixin, BaseImportMixin from .results import RowResult from .signals import post_export, post_import from .tmp_storages import TempFolderStorage class ImportExportMixinBase: def get_model_info(self): app_label = self.model._meta.app_label return (app_label, self.model._meta.model_name) class ImportMixin(BaseImportMixin, ImportExportMixinBase): """ Import mixin. This is intended to be mixed with django.contrib.admin.ModelAdmin https://docs.djangoproject.com/en/dev/ref/contrib/admin/ """ #: template for change_list view change_list_template = 'admin/import_export/change_list_import.html' #: template for import view import_template_name = 'admin/import_export/import.html' #: import data encoding from_encoding = "utf-8" skip_admin_log = None # storage class for saving temporary files tmp_storage_class = None def get_skip_admin_log(self): if self.skip_admin_log is None: return getattr(settings, 'IMPORT_EXPORT_SKIP_ADMIN_LOG', False) else: return self.skip_admin_log def get_tmp_storage_class(self): if self.tmp_storage_class is None: tmp_storage_class = getattr( settings, 'IMPORT_EXPORT_TMP_STORAGE_CLASS', TempFolderStorage, ) else: tmp_storage_class = self.tmp_storage_class if isinstance(tmp_storage_class, str): tmp_storage_class = import_string(tmp_storage_class) return tmp_storage_class def has_import_permission(self, request): """ Returns whether a request has import permission. """ IMPORT_PERMISSION_CODE = getattr(settings, 'IMPORT_EXPORT_IMPORT_PERMISSION_CODE', None) if IMPORT_PERMISSION_CODE is None: return True opts = self.opts codename = get_permission_codename(IMPORT_PERMISSION_CODE, opts) return request.user.has_perm("%s.%s" % (opts.app_label, codename)) def get_urls(self): urls = super().get_urls() info = self.get_model_info() my_urls = [ path('process_import/', self.admin_site.admin_view(self.process_import), name='%s_%s_process_import' % info), path('import/', self.admin_site.admin_view(self.import_action), name='%s_%s_import' % info), ] return my_urls + urls @method_decorator(require_POST) def process_import(self, request, *args, **kwargs): """ Perform the actual import action (after the user has confirmed the import) """ if not self.has_import_permission(request): raise PermissionDenied form_type = self.get_confirm_import_form() confirm_form = form_type(request.POST) if confirm_form.is_valid(): import_formats = self.get_import_formats() input_format = import_formats[ int(confirm_form.cleaned_data['input_format']) ]() tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name']) data = tmp_storage.read(input_format.get_read_mode()) if not input_format.is_binary() and self.from_encoding: data = force_str(data, self.from_encoding) dataset = input_format.create_dataset(data) result = self.process_dataset(dataset, confirm_form, request, *args, **kwargs) tmp_storage.remove() return self.process_result(result, request) def process_dataset(self, dataset, confirm_form, request, *args, **kwargs): res_kwargs = self.get_import_resource_kwargs(request, form=confirm_form, *args, **kwargs) resource = self.get_import_resource_class()(**res_kwargs) imp_kwargs = self.get_import_data_kwargs(request, form=confirm_form, *args, **kwargs) return resource.import_data(dataset, dry_run=False, raise_errors=True, file_name=confirm_form.cleaned_data['original_file_name'], user=request.user, **imp_kwargs) def process_result(self, result, request): self.generate_log_entries(result, request) self.add_success_message(result, request) post_import.send(sender=None, model=self.model) url = reverse('admin:%s_%s_changelist' % self.get_model_info(), current_app=self.admin_site.name) return HttpResponseRedirect(url) def generate_log_entries(self, result, request): if not self.get_skip_admin_log(): # Add imported objects to LogEntry logentry_map = { RowResult.IMPORT_TYPE_NEW: ADDITION, RowResult.IMPORT_TYPE_UPDATE: CHANGE, RowResult.IMPORT_TYPE_DELETE: DELETION, } content_type_id = ContentType.objects.get_for_model(self.model).pk for row in result: if row.import_type != row.IMPORT_TYPE_ERROR and row.import_type != row.IMPORT_TYPE_SKIP: LogEntry.objects.log_action( user_id=request.user.pk, content_type_id=content_type_id, object_id=row.object_id, object_repr=row.object_repr, action_flag=logentry_map[row.import_type], change_message=_("%s through import_export" % row.import_type), ) def add_success_message(self, result, request): opts = self.model._meta success_message = _('Import finished, with {} new and ' \ '{} updated {}.').format(result.totals[RowResult.IMPORT_TYPE_NEW], result.totals[RowResult.IMPORT_TYPE_UPDATE], opts.verbose_name_plural) messages.success(request, success_message) def get_import_context_data(self, **kwargs): return self.get_context_data(**kwargs) def get_context_data(self, **kwargs): return {} def get_import_form(self): """ Get the form type used to read the import format and file. """ return ImportForm def get_confirm_import_form(self): """ Get the form type (class) used to confirm the import. """ return ConfirmImportForm def get_form_kwargs(self, form, *args, **kwargs): """ Prepare/returns kwargs for the import form. To distinguish between import and confirm import forms, the following approach may be used: if isinstance(form, ImportForm): # your code here for the import form kwargs # e.g. update.kwargs({...}) elif isinstance(form, ConfirmImportForm): # your code here for the confirm import form kwargs # e.g. update.kwargs({...}) ... """ return kwargs def get_import_data_kwargs(self, request, *args, **kwargs): """ Prepare kwargs for import_data. """ form = kwargs.get('form') if form: kwargs.pop('form') return kwargs return {} def write_to_tmp_storage(self, import_file, input_format): tmp_storage = self.get_tmp_storage_class()() data = bytes() for chunk in import_file.chunks(): data += chunk tmp_storage.save(data, input_format.get_read_mode()) return tmp_storage def import_action(self, request, *args, **kwargs): """ Perform a dry_run of the import to make sure the import will not result in errors. If there where no error, save the user uploaded file to a local temp file that will be used by 'process_import' for the actual import. """ if not self.has_import_permission(request): raise PermissionDenied context = self.get_import_context_data() import_formats = self.get_import_formats() form_type = self.get_import_form() form_kwargs = self.get_form_kwargs(form_type, *args, **kwargs) form = form_type(import_formats, request.POST or None, request.FILES or None, **form_kwargs) if request.POST and form.is_valid(): input_format = import_formats[ int(form.cleaned_data['input_format']) ]() import_file = form.cleaned_data['import_file'] # first always write the uploaded file to disk as it may be a # memory file or else based on settings upload handlers tmp_storage = self.write_to_tmp_storage(import_file, input_format) # then read the file, using the proper format-specific mode # warning, big files may exceed memory try: data = tmp_storage.read(input_format.get_read_mode()) if not input_format.is_binary() and self.from_encoding: data = force_str(data, self.from_encoding) dataset = input_format.create_dataset(data) except UnicodeDecodeError as e: return HttpResponse(_(u"

Imported file has a wrong encoding: %s

" % e)) except Exception as e: return HttpResponse(_(u"

%s encountered while trying to read file: %s

" % (type(e).__name__, import_file.name))) # prepare kwargs for import data, if needed res_kwargs = self.get_import_resource_kwargs(request, form=form, *args, **kwargs) resource = self.get_import_resource_class()(**res_kwargs) # prepare additional kwargs for import_data, if needed imp_kwargs = self.get_import_data_kwargs(request, form=form, *args, **kwargs) result = resource.import_data(dataset, dry_run=True, raise_errors=False, file_name=import_file.name, user=request.user, **imp_kwargs) context['result'] = result if not result.has_errors() and not result.has_validation_errors(): initial = { 'import_file_name': tmp_storage.name, 'original_file_name': import_file.name, 'input_format': form.cleaned_data['input_format'], } confirm_form = self.get_confirm_import_form() initial = self.get_form_kwargs(form=form, **initial) context['confirm_form'] = confirm_form(initial=initial) else: res_kwargs = self.get_import_resource_kwargs(request, form=form, *args, **kwargs) resource = self.get_import_resource_class()(**res_kwargs) context.update(self.admin_site.each_context(request)) context['title'] = _("Import") context['form'] = form context['opts'] = self.model._meta context['fields'] = [f.column_name for f in resource.get_user_visible_fields()] request.current_app = self.admin_site.name return TemplateResponse(request, [self.import_template_name], context) def changelist_view(self, request, extra_context=None): if extra_context is None: extra_context = {} extra_context['has_import_permission'] = self.has_import_permission(request) return super().changelist_view(request, extra_context) class ExportMixin(BaseExportMixin, ImportExportMixinBase): """ Export mixin. This is intended to be mixed with django.contrib.admin.ModelAdmin https://docs.djangoproject.com/en/dev/ref/contrib/admin/ """ #: template for change_list view change_list_template = 'admin/import_export/change_list_export.html' #: template for export view export_template_name = 'admin/import_export/export.html' #: export data encoding to_encoding = None def get_urls(self): urls = super().get_urls() my_urls = [ path('export/', self.admin_site.admin_view(self.export_action), name='%s_%s_export' % self.get_model_info()), ] return my_urls + urls def has_export_permission(self, request): """ Returns whether a request has export permission. """ EXPORT_PERMISSION_CODE = getattr(settings, 'IMPORT_EXPORT_EXPORT_PERMISSION_CODE', None) if EXPORT_PERMISSION_CODE is None: return True opts = self.opts codename = get_permission_codename(EXPORT_PERMISSION_CODE, opts) return request.user.has_perm("%s.%s" % (opts.app_label, codename)) def get_export_queryset(self, request): """ Returns export queryset. Default implementation respects applied search and filters. """ list_display = self.get_list_display(request) list_display_links = self.get_list_display_links(request, list_display) list_filter = self.get_list_filter(request) search_fields = self.get_search_fields(request) if self.get_actions(request): list_display = ['action_checkbox'] + list(list_display) ChangeList = self.get_changelist(request) changelist_kwargs = { 'request': request, 'model': self.model, 'list_display': list_display, 'list_display_links': list_display_links, 'list_filter': list_filter, 'date_hierarchy': self.date_hierarchy, 'search_fields': search_fields, 'list_select_related': self.list_select_related, 'list_per_page': self.list_per_page, 'list_max_show_all': self.list_max_show_all, 'list_editable': self.list_editable, 'model_admin': self, } if django.VERSION >= (2, 1): changelist_kwargs['sortable_by'] = self.sortable_by if django.VERSION >= (4, 0): changelist_kwargs['search_help_text'] = self.search_help_text cl = ChangeList(**changelist_kwargs) return cl.get_queryset(request) def get_export_data(self, file_format, queryset, *args, **kwargs): """ Returns file_format representation for given queryset. """ request = kwargs.pop("request") if not self.has_export_permission(request): raise PermissionDenied data = self.get_data_for_export(request, queryset, *args, **kwargs) export_data = file_format.export_data(data) encoding = kwargs.get("encoding") if not file_format.is_binary() and encoding: export_data = export_data.encode(encoding) return export_data def get_export_context_data(self, **kwargs): return self.get_context_data(**kwargs) def get_context_data(self, **kwargs): return {} def export_action(self, request, *args, **kwargs): if not self.has_export_permission(request): raise PermissionDenied formats = self.get_export_formats() form = ExportForm(formats, request.POST or None) if form.is_valid(): file_format = formats[ int(form.cleaned_data['file_format']) ]() queryset = self.get_export_queryset(request) export_data = self.get_export_data(file_format, queryset, request=request, encoding=self.to_encoding) content_type = file_format.get_content_type() response = HttpResponse(export_data, content_type=content_type) response['Content-Disposition'] = 'attachment; filename="%s"' % ( self.get_export_filename(request, queryset, file_format), ) post_export.send(sender=None, model=self.model) return response context = self.get_export_context_data() context.update(self.admin_site.each_context(request)) context['title'] = _("Export") context['form'] = form context['opts'] = self.model._meta request.current_app = self.admin_site.name return TemplateResponse(request, [self.export_template_name], context) def changelist_view(self, request, extra_context=None): if extra_context is None: extra_context = {} extra_context['has_export_permission'] = self.has_export_permission(request) return super().changelist_view(request, extra_context) def get_export_filename(self, request, queryset, file_format): return super().get_export_filename(file_format) class ImportExportMixin(ImportMixin, ExportMixin): """ Import and export mixin. """ #: template for change_list view change_list_template = 'admin/import_export/change_list_import_export.html' class ImportExportModelAdmin(ImportExportMixin, admin.ModelAdmin): """ Subclass of ModelAdmin with import/export functionality. """ class ExportActionMixin(ExportMixin): """ Mixin with export functionality implemented as an admin action. """ # Don't use custom change list template. change_list_template = None def __init__(self, *args, **kwargs): """ Adds a custom action form initialized with the available export formats. """ choices = [] formats = self.get_export_formats() if formats: choices.append(('', '---')) for i, f in enumerate(formats): choices.append((str(i), f().get_title())) self.action_form = export_action_form_factory(choices) super().__init__(*args, **kwargs) def export_admin_action(self, request, queryset): """ Exports the selected rows using file_format. """ export_format = request.POST.get('file_format') if not export_format: messages.warning(request, _('You must select an export format.')) else: formats = self.get_export_formats() file_format = formats[int(export_format)]() export_data = self.get_export_data(file_format, queryset, request=request, encoding=self.to_encoding) content_type = file_format.get_content_type() response = HttpResponse(export_data, content_type=content_type) response['Content-Disposition'] = 'attachment; filename="%s"' % ( self.get_export_filename(request, queryset, file_format), ) return response def get_actions(self, request): """ Adds the export action to the list of available actions. """ actions = super().get_actions(request) actions.update( export_admin_action=( ExportActionMixin.export_admin_action, "export_admin_action", _("Export selected %(verbose_name_plural)s"), ) ) return actions @property def media(self): super_media = super().media return forms.Media(js=super_media._js + ['import_export/action_formats.js'], css=super_media._css) class ExportActionModelAdmin(ExportActionMixin, admin.ModelAdmin): """ Subclass of ModelAdmin with export functionality implemented as an admin action. """ class ImportExportActionModelAdmin(ImportMixin, ExportActionModelAdmin): """ Subclass of ExportActionModelAdmin with import/export functionality. Export functionality is implemented as an admin action. """ django-import-export-2.7.1/import_export/exceptions.py000066400000000000000000000003101416107567000232610ustar00rootroot00000000000000class ImportExportError(Exception): """A generic exception for all others to extend.""" pass class FieldError(ImportExportError): """Raised when a field encounters an error.""" pass django-import-export-2.7.1/import_export/fields.py000066400000000000000000000103161416107567000223550ustar00rootroot00000000000000from django.core.exceptions import ObjectDoesNotExist from django.db.models.fields import NOT_PROVIDED from django.db.models.manager import Manager from . import widgets class Field: """ Field represent mapping between `object` field and representation of this field. :param attribute: A string of either an instance attribute or callable off the object. :param column_name: Lets you provide a name for the column that represents this field in the export. :param widget: Defines a widget that will be used to represent this field's data in the export. :param readonly: A Boolean which defines if this field will be ignored during import. :param default: This value will be returned by :meth:`~import_export.fields.Field.clean` if this field's widget did not return an adequate value. :param saves_null_values: Controls whether null values are saved on the object """ empty_values = [None, ''] def __init__(self, attribute=None, column_name=None, widget=None, default=NOT_PROVIDED, readonly=False, saves_null_values=True): self.attribute = attribute self.default = default self.column_name = column_name if not widget: widget = widgets.Widget() self.widget = widget self.readonly = readonly self.saves_null_values = saves_null_values def __repr__(self): """ Displays the module, class and name of the field. """ path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) column_name = getattr(self, 'column_name', None) if column_name is not None: return '<%s: %s>' % (path, column_name) return '<%s>' % path def clean(self, data, **kwargs): """ Translates the value stored in the imported datasource to an appropriate Python object and returns it. """ try: value = data[self.column_name] except KeyError: raise KeyError("Column '%s' not found in dataset. Available " "columns are: %s" % (self.column_name, list(data))) # If ValueError is raised here, import_obj() will handle it value = self.widget.clean(value, row=data, **kwargs) if value in self.empty_values and self.default != NOT_PROVIDED: if callable(self.default): return self.default() return self.default return value def get_value(self, obj): """ Returns the value of the object's attribute. """ if self.attribute is None: return None attrs = self.attribute.split('__') value = obj for attr in attrs: try: value = getattr(value, attr, None) except (ValueError, ObjectDoesNotExist): # needs to have a primary key value before a many-to-many # relationship can be used. return None if value is None: return None # RelatedManager and ManyRelatedManager classes are callable in # Django >= 1.7 but we don't want to call them if callable(value) and not isinstance(value, Manager): value = value() return value def save(self, obj, data, is_m2m=False, **kwargs): """ If this field is not declared readonly, the object's attribute will be set to the value returned by :meth:`~import_export.fields.Field.clean`. """ if not self.readonly: attrs = self.attribute.split('__') for attr in attrs[:-1]: obj = getattr(obj, attr, None) cleaned = self.clean(data, **kwargs) if cleaned is not None or self.saves_null_values: if not is_m2m: setattr(obj, attrs[-1], cleaned) else: getattr(obj, attrs[-1]).set(cleaned) def export(self, obj): """ Returns value from the provided object converted to export representation. """ value = self.get_value(obj) if value is None: return "" return self.widget.render(value, obj) django-import-export-2.7.1/import_export/formats/000077500000000000000000000000001416107567000222075ustar00rootroot00000000000000django-import-export-2.7.1/import_export/formats/__init__.py000066400000000000000000000000001416107567000243060ustar00rootroot00000000000000django-import-export-2.7.1/import_export/formats/base_formats.py000066400000000000000000000120251416107567000252260ustar00rootroot00000000000000from importlib import import_module import tablib class Format: def get_title(self): return type(self) def create_dataset(self, in_stream): """ Create dataset from given string. """ raise NotImplementedError() def export_data(self, dataset, **kwargs): """ Returns format representation for given dataset. """ raise NotImplementedError() def is_binary(self): """ Returns if this format is binary. """ return True def get_read_mode(self): """ Returns mode for opening files. """ return 'rb' def get_extension(self): """ Returns extension for this format files. """ return "" def get_content_type(self): # For content types see # https://www.iana.org/assignments/media-types/media-types.xhtml return 'application/octet-stream' @classmethod def is_available(cls): return True def can_import(self): return False def can_export(self): return False class TablibFormat(Format): TABLIB_MODULE = None CONTENT_TYPE = 'application/octet-stream' def get_format(self): """ Import and returns tablib module. """ try: # Available since tablib 1.0 from tablib.formats import registry except ImportError: return import_module(self.TABLIB_MODULE) else: key = self.TABLIB_MODULE.split('.')[-1].replace('_', '') return registry.get_format(key) @classmethod def is_available(cls): try: cls().get_format() except (tablib.core.UnsupportedFormat, ImportError): return False return True def get_title(self): return self.get_format().title def create_dataset(self, in_stream, **kwargs): return tablib.import_set(in_stream, format=self.get_title()) def export_data(self, dataset, **kwargs): return dataset.export(self.get_title(), **kwargs) def get_extension(self): return self.get_format().extensions[0] def get_content_type(self): return self.CONTENT_TYPE def can_import(self): return hasattr(self.get_format(), 'import_set') def can_export(self): return hasattr(self.get_format(), 'export_set') class TextFormat(TablibFormat): def get_read_mode(self): return 'r' def is_binary(self): return False class CSV(TextFormat): TABLIB_MODULE = 'tablib.formats._csv' CONTENT_TYPE = 'text/csv' def create_dataset(self, in_stream, **kwargs): return super().create_dataset(in_stream, **kwargs) class JSON(TextFormat): TABLIB_MODULE = 'tablib.formats._json' CONTENT_TYPE = 'application/json' class YAML(TextFormat): TABLIB_MODULE = 'tablib.formats._yaml' # See https://stackoverflow.com/questions/332129/yaml-mime-type CONTENT_TYPE = 'text/yaml' class TSV(TextFormat): TABLIB_MODULE = 'tablib.formats._tsv' CONTENT_TYPE = 'text/tab-separated-values' def create_dataset(self, in_stream, **kwargs): return super().create_dataset(in_stream, **kwargs) class ODS(TextFormat): TABLIB_MODULE = 'tablib.formats._ods' CONTENT_TYPE = 'application/vnd.oasis.opendocument.spreadsheet' class HTML(TextFormat): TABLIB_MODULE = 'tablib.formats._html' CONTENT_TYPE = 'text/html' class XLS(TablibFormat): TABLIB_MODULE = 'tablib.formats._xls' CONTENT_TYPE = 'application/vnd.ms-excel' def create_dataset(self, in_stream): """ Create dataset from first sheet. """ import xlrd xls_book = xlrd.open_workbook(file_contents=in_stream) dataset = tablib.Dataset() sheet = xls_book.sheets()[0] dataset.headers = sheet.row_values(0) for i in range(1, sheet.nrows): dataset.append(sheet.row_values(i)) return dataset class XLSX(TablibFormat): TABLIB_MODULE = 'tablib.formats._xlsx' CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' def create_dataset(self, in_stream): """ Create dataset from first sheet. """ from io import BytesIO import openpyxl # 'data_only' means values are read from formula cells, not the formula itself xlsx_book = openpyxl.load_workbook(BytesIO(in_stream), read_only=True, data_only=True) dataset = tablib.Dataset() sheet = xlsx_book.active # obtain generator rows = sheet.rows dataset.headers = [cell.value for cell in next(rows)] for row in rows: row_values = [cell.value for cell in row] dataset.append(row_values) return dataset #: These are the default formats for import and export. Whether they can be #: used or not is depending on their implementation in the tablib library. DEFAULT_FORMATS = [fmt for fmt in ( CSV, XLS, XLSX, TSV, ODS, JSON, YAML, HTML, ) if fmt.is_available()] django-import-export-2.7.1/import_export/forms.py000066400000000000000000000037521416107567000222430ustar00rootroot00000000000000import os.path from django import forms from django.contrib.admin.helpers import ActionForm from django.utils.translation import gettext_lazy as _ class ImportForm(forms.Form): import_file = forms.FileField( label=_('File to import') ) input_format = forms.ChoiceField( label=_('Format'), choices=(), ) def __init__(self, import_formats, *args, **kwargs): super().__init__(*args, **kwargs) choices = [] for i, f in enumerate(import_formats): choices.append((str(i), f().get_title(),)) if len(import_formats) > 1: choices.insert(0, ('', '---')) self.fields['input_format'].choices = choices class ConfirmImportForm(forms.Form): import_file_name = forms.CharField(widget=forms.HiddenInput()) original_file_name = forms.CharField(widget=forms.HiddenInput()) input_format = forms.CharField(widget=forms.HiddenInput()) def clean_import_file_name(self): data = self.cleaned_data['import_file_name'] data = os.path.basename(data) return data class ExportForm(forms.Form): file_format = forms.ChoiceField( label=_('Format'), choices=(), ) def __init__(self, formats, *args, **kwargs): super().__init__(*args, **kwargs) choices = [] for i, f in enumerate(formats): choices.append((str(i), f().get_title(),)) if len(formats) > 1: choices.insert(0, ('', '---')) self.fields['file_format'].choices = choices def export_action_form_factory(formats): """ Returns an ActionForm subclass containing a ChoiceField populated with the given formats. """ class _ExportActionForm(ActionForm): """ Action form with export format ChoiceField. """ file_format = forms.ChoiceField( label=_('Format'), choices=formats, required=False) _ExportActionForm.__name__ = str('ExportActionForm') return _ExportActionForm django-import-export-2.7.1/import_export/instance_loaders.py000066400000000000000000000041331416107567000244240ustar00rootroot00000000000000class BaseInstanceLoader: """ Base abstract implementation of instance loader. """ def __init__(self, resource, dataset=None): self.resource = resource self.dataset = dataset def get_instance(self, row): raise NotImplementedError class ModelInstanceLoader(BaseInstanceLoader): """ Instance loader for Django model. Lookup for model instance by ``import_id_fields``. """ def get_queryset(self): return self.resource.get_queryset() def get_instance(self, row): try: params = {} for key in self.resource.get_import_id_fields(): field = self.resource.fields[key] params[field.attribute] = field.clean(row) if params: return self.get_queryset().get(**params) else: return None except self.resource._meta.model.DoesNotExist: return None class CachedInstanceLoader(ModelInstanceLoader): """ Loads all possible model instances in dataset avoid hitting database for every ``get_instance`` call. This instance loader work only when there is one ``import_id_fields`` field. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) pk_field_name = self.resource.get_import_id_fields()[0] self.pk_field = self.resource.fields[pk_field_name] # If the pk field is missing, all instances in dataset are new # and cache is empty. self.all_instances = {} if self.dataset.dict and self.pk_field.column_name in self.dataset.dict[0]: ids = [self.pk_field.clean(row) for row in self.dataset.dict] qs = self.get_queryset().filter(**{ "%s__in" % self.pk_field.attribute: ids }) self.all_instances = { self.pk_field.get_value(instance): instance for instance in qs } def get_instance(self, row): if self.all_instances: return self.all_instances.get(self.pk_field.clean(row)) return None django-import-export-2.7.1/import_export/locale/000077500000000000000000000000001416107567000217735ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ar/000077500000000000000000000000001416107567000223755ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ar/LC_MESSAGES/000077500000000000000000000000001416107567000241625ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ar/LC_MESSAGES/django.mo000066400000000000000000000037471416107567000257740ustar00rootroot00000000000000l5/hG'  *.6>0Ev!}D5Ezt  1 &8 A N YJd ,    

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;

%s ووجهت أثناء محاولة قراءة ملف: %s

الملف المستورد لديه ترميز خاطئ: %s

فيما يلي إستعراض للبيانات التي سيتم إستيرادها. إذا كنت راضيا عن النتائج, انقر على 'تأكيد الإستيراد'تأكيد الإستيرادحذفأخطاءتصديرتصدير %(verbose_name_plural)s المحددةملف للإستيرادتنسيقالرئيسيةإستبرادرقم الصطرجديدمعاينةتجاهلإرسالهذا المستورد سوف يستورد الحقول التالية : تحديثيجب تحديد تنسيق التصدير.django-import-export-2.7.1/import_export/locale/ar/LC_MESSAGES/django.po000066400000000000000000000074711416107567000257750ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

الملف المستورد لديه ترميز خاطئ: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s ووجهت أثناء محاولة قراءة ملف: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "إستبراد" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "تصدير" #: admin.py:490 msgid "You must select an export format." msgstr "يجب تحديد تنسيق التصدير." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "تصدير %(verbose_name_plural)s المحددة" #: forms.py:10 msgid "File to import" msgstr "ملف للإستيراد" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "تنسيق" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "الرئيسية" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "إرسال" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "فيما يلي إستعراض للبيانات التي سيتم إستيرادها. إذا كنت راضيا عن النتائج, " "انقر على 'تأكيد الإستيراد'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "تأكيد الإستيراد" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "هذا المستورد سوف يستورد الحقول التالية : " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "أخطاء" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "رقم الصطر" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "معاينة" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "جديد" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "تجاهل" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "حذف" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "تحديث" django-import-export-2.7.1/import_export/locale/bg/000077500000000000000000000000001416107567000223635ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/bg/LC_MESSAGES/000077500000000000000000000000001416107567000241505ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/bg/LC_MESSAGES/django.mo000066400000000000000000000044721416107567000257560ustar00rootroot00000000000000 5"/Xh'=LSX/_ 0!Xi?U+  #0HI$  [D]dsFN   %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Hristo Gatsinski Language-Team: LANGUAGE Language: Bulgarian MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %s чрез import_export

%s при опит за четене на файл: %s

Импортирания файл има грешна кодировка: %s

Отдолу виждате преглед на данните за импортиране. Ако сте доволни от резултата, изберете 'Потвърди импортирането'Потвърди импортиранетоИзтритГрешкиЕкспортиранеЕкспортиране на избраните %(verbose_name_plural)sФайл за импортиранеФорматНачалоИмпортиранеИмпортирането е завършено, с {} нови и {} обновени {}.Номер на редаНовПрегледПропуснатИзпълниЩе бъдат импортирани следните полета: ОбновенТрябва да изберете формат за експортиране.django-import-export-2.7.1/import_export/locale/bg/LC_MESSAGES/django.po000066400000000000000000000100401416107567000257450ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Hristo Gatsinski , 2017. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Hristo Gatsinski \n" "Language-Team: LANGUAGE \n" "Language: Bulgarian\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s чрез import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Импортирането е завършено, с {} нови и {} обновени {}." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Импортирания файл има грешна кодировка: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s при опит за четене на файл: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Импортиране" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Експортиране" #: admin.py:490 msgid "You must select an export format." msgstr "Трябва да изберете формат за експортиране." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Експортиране на избраните %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Файл за импортиране" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Начало" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Изпълни" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Отдолу виждате преглед на данните за импортиране. Ако сте доволни от " "резултата, изберете 'Потвърди импортирането'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Потвърди импортирането" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Ще бъдат импортирани следните полета: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Грешки" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Номер на реда" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Преглед" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Нов" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Пропуснат" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Изтрит" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Обновен" django-import-export-2.7.1/import_export/locale/ca/000077500000000000000000000000001416107567000223565ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ca/LC_MESSAGES/000077500000000000000000000000001416107567000241435ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ca/LC_MESSAGES/django.mo000066400000000000000000000034061416107567000257450ustar00rootroot00000000000000l5/hG'  *.6>0Ev!}B5=V -DU\bk} 2 *    

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1);

S'ha trobat: %s mentre es llegia l'arxiu: %s

L'arxiu importat té una codificació incorrecta: %s

A continuació podeu veure una vista prèvia de les dades que s'importaran. Si esteu satisfets amb els resultats, premeu 'Confirmar importació'Confirmar importacióEsborrarErrorsExportarExportar %(verbose_name_plural)s seleccionatsArxiu a importarFormatIniciImportarNúmero de líniaNouVista prèviaOmèsEnviarAquest importador importarà els següents camps: ActualitzarHeu de seleccionar un format d'exportaciódjango-import-export-2.7.1/import_export/locale/ca/LC_MESSAGES/django.po000066400000000000000000000072311416107567000257500ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Manel Clos , 2016. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

L'arxiu importat té una codificació incorrecta: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

S'ha trobat: %s mentre es llegia l'arxiu: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importar" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportar" #: admin.py:490 msgid "You must select an export format." msgstr "Heu de seleccionar un format d'exportació" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionats" #: forms.py:10 msgid "File to import" msgstr "Arxiu a importar" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inici" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "A continuació podeu veure una vista prèvia de les dades que s'importaran. Si " "esteu satisfets amb els resultats, premeu 'Confirmar importació'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Confirmar importació" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Aquest importador importarà els següents camps: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Errors" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Número de línia" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Vista prèvia" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nou" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Omès" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Esborrar" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Actualitzar" #~ msgid "Import finished" #~ msgstr "Importació finalitzada" django-import-export-2.7.1/import_export/locale/cs/000077500000000000000000000000001416107567000224005ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/cs/LC_MESSAGES/000077500000000000000000000000001416107567000241655ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/cs/LC_MESSAGES/django.mo000066400000000000000000000036121416107567000257660ustar00rootroot00000000000000 5"/Xh'=LSX/_ 0!=N?d<uW gqw&~2 *(2 ["g   %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2017-05-02 19:17+0200 Last-Translator: Language-Team: Language: cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; X-Generator: Poedit 2.0.1 %s skrz import_export

Při zpracování souboru nastala chyba %s (soubor %s)

Importovaný soubor má nesprávné kódování: %s

Níže je zobrazen náhled importovaných dat. Pokud je vše v pořádku, stiskněte tlačítko „Provést import”Provést importSmazáníChybyExportVybrán export %(verbose_name_plural)sSoubor k importuFormátDomůImportImport dokončen, {} nové a {} aktualizované {}.Číslo řádkuNovéNáhledPřeskočenéOdeslatBudou importována následující pole: AktualizaceMusíte vybrat formát pro export.django-import-export-2.7.1/import_export/locale/cs/LC_MESSAGES/django.po000066400000000000000000000071411416107567000257720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2017-05-02 19:17+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Poedit 2.0.1\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s skrz import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Import dokončen, {} nové a {} aktualizované {}." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Importovaný soubor má nesprávné kódování: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

Při zpracování souboru nastala chyba %s (soubor %s)

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Import" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Export" #: admin.py:490 msgid "You must select an export format." msgstr "Musíte vybrat formát pro export." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Vybrán export %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Soubor k importu" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formát" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Domů" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Odeslat" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Níže je zobrazen náhled importovaných dat. Pokud je vše v pořádku, stiskněte " "tlačítko „Provést import”" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Provést import" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Budou importována následující pole: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Chyby" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Číslo řádku" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Náhled" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nové" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Přeskočené" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Smazání" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Aktualizace" django-import-export-2.7.1/import_export/locale/de/000077500000000000000000000000001416107567000223635ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/de/LC_MESSAGES/000077500000000000000000000000001416107567000241505ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/de/LC_MESSAGES/django.mo000066400000000000000000000030161416107567000257470ustar00rootroot00000000000000Lh*9@G'Nv 0!;U 0A Xd j v *,    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2016-02-01 17:33+0100 Last-Translator: Jannis Language-Team: Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.8.4 Unten befindet sich eine Vorschau der zu importierenden Daten. Wenn die Ergebnisse zufriedenstellend sind, klicke auf "Import bestätigen".Import bestätigenLöschenFehlerExportierenAusgewählte %(verbose_name_plural)s exportierenZu importierende DateiDateiformatStartImportierenZeilennummerNeuVorschauÜbersprungenAbsendenEs werden die folgenden Felder importiert:ÜberschreibenEs muss ein Exportformat ausgewählt werden.django-import-export-2.7.1/import_export/locale/de/LC_MESSAGES/django.po000066400000000000000000000074361416107567000257640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2016-02-01 17:33+0100\n" "Last-Translator: Jannis \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.8.4\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, fuzzy, python-format #| msgid "

Imported file is not in unicode: %s

" msgid "

Imported file has a wrong encoding: %s

" msgstr "

Importierte Datei hat die falsche Zeichenkodierung: %s

" #: admin.py:264 #, fuzzy, python-format #| msgid "

%s encountred while trying to read file: %s

" msgid "

%s encountered while trying to read file: %s

" msgstr "

%s trat auf beim Versuch die Datei zu lesen: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importieren" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportieren" #: admin.py:490 msgid "You must select an export format." msgstr "Es muss ein Exportformat ausgewählt werden." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Ausgewählte %(verbose_name_plural)s exportieren" #: forms.py:10 msgid "File to import" msgstr "Zu importierende Datei" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Dateiformat" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Start" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Absenden" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Unten befindet sich eine Vorschau der zu importierenden Daten. Wenn die " "Ergebnisse zufriedenstellend sind, klicke auf \"Import bestätigen\"." #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Import bestätigen" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Es werden die folgenden Felder importiert:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Fehler" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Zeilennummer" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Vorschau" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Neu" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Übersprungen" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Löschen" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Überschreiben" #~ msgid "Import finished" #~ msgstr "Import fertiggestellt." django-import-export-2.7.1/import_export/locale/es/000077500000000000000000000000001416107567000224025ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/es/LC_MESSAGES/000077500000000000000000000000001416107567000241675ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/es/LC_MESSAGES/django.mo000066400000000000000000000030271416107567000257700ustar00rootroot00000000000000Lh*9@G'Nv 0!B\ .M`hox 1 -    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); A continuación se muestra una vista previa de los datos a importar. Si estás satisfecho con los resultados, haz clic en 'Confirmar importación'Confirmar importaciónBorrarErroresExportarExportar %(verbose_name_plural)s seleccionadosFichero a importarFormatoInicioImportarNúmero de líneaNuevoVista previaOmitidoEnviarEste importador importará los siguientes campos:ActualizarDebes seleccionar un formato de exportación.django-import-export-2.7.1/import_export/locale/es/LC_MESSAGES/django.po000066400000000000000000000070611416107567000257750ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # David Díaz , 2015. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importar" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportar" #: admin.py:490 msgid "You must select an export format." msgstr "Debes seleccionar un formato de exportación." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionados" #: forms.py:10 msgid "File to import" msgstr "Fichero a importar" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inicio" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "A continuación se muestra una vista previa de los datos a importar. Si estás " "satisfecho con los resultados, haz clic en 'Confirmar importación'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Confirmar importación" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Este importador importará los siguientes campos:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Errores" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Número de línea" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Vista previa" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nuevo" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Omitido" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Borrar" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Actualizar" #~ msgid "Import finished" #~ msgstr "Importación finalizada" django-import-export-2.7.1/import_export/locale/es_AR/000077500000000000000000000000001416107567000227645ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/es_AR/LC_MESSAGES/000077500000000000000000000000001416107567000245515ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/es_AR/LC_MESSAGES/django.mo000066400000000000000000000030331416107567000263470ustar00rootroot00000000000000Lh*9@G'Nv 0!Ga .#Remt} 1 ,    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2015-10-11 18:49-0300 Last-Translator: Gonzalo Bustos Language-Team: Spanish (Argentina) Language: es_AR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.6.10 A continuación se muestra una vista previa de los datos a importar. Si está satisfecho con los resultados, haga clic en 'Confirmar importación'Confirmar importaciónBorrarErroresExportarExportar %(verbose_name_plural)s seleccionadosArchivo a importarFormatoInicioImportarNúmero de líneaNuevoVista previaOmitidoEnviarEste importador importará los siguientes campos:ActualizarDebe seleccionar un formato de exportación.django-import-export-2.7.1/import_export/locale/es_AR/LC_MESSAGES/django.po000066400000000000000000000070371416107567000263620ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Gonzalo Bustos, 2015. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2015-10-11 18:49-0300\n" "Last-Translator: Gonzalo Bustos\n" "Language-Team: Spanish (Argentina)\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importar" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportar" #: admin.py:490 msgid "You must select an export format." msgstr "Debe seleccionar un formato de exportación." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionados" #: forms.py:10 msgid "File to import" msgstr "Archivo a importar" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inicio" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "A continuación se muestra una vista previa de los datos a importar. Si está " "satisfecho con los resultados, haga clic en 'Confirmar importación'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Confirmar importación" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Este importador importará los siguientes campos:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Errores" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Número de línea" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Vista previa" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nuevo" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Omitido" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Borrar" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Actualizar" #~ msgid "Import finished" #~ msgstr "Importación finalizada" django-import-export-2.7.1/import_export/locale/fa/000077500000000000000000000000001416107567000223615ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/fa/LC_MESSAGES/000077500000000000000000000000001416107567000241465ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/fa/LC_MESSAGES/django.mo000066400000000000000000000052571416107567000257560ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!z(wIL7 % 06;"r] & '/ jW  3  ? ] >p      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: 0.0.1 Report-Msgid-Bugs-To: PO-Revision-Date: 2021-03-09 00:29+0030 Last-Translator: Yazdan Ranjbar Language-Team: Persain/Farsi Language: Farsi/Persian MIME-Version: 0.1 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.5.4 %s یه وسیله ورودی-خروجی

در هنگام خواندن فایل %s با %s مواجه شد

فایل بارگذاری شده encode اشتباهی دارد: %s

پایین یک پیش‌نمایش از دیتا‌هایی است که بارگذاری خواهند شد اگر این موارد درست هستروی 'تایید بارگذاری' گلیگ گنیدتایید بارگذاریحذفخطاهاخروجیخروجی %(verbose_name_plural)s انتخاب شدهقایل برای بارگذاریفرمتخانهبارگذاریبارگذاری تمام شد، با {} مورد جدید و {} مورد به روز شده.شماره خطجدیدفیلد‌های غیر اختصاصیلطفا این خطا را تصحیح کنید و سپس مجدد فایل را بارگذاری کنیدنمایشسظردر شدبرخی از سطر‌ها معتبر نبودندارسالاین بارگذاری شامل این فیلد‌ها هست:بروزرسانیشما باید یک فرمت خروجی انتخاب کنیدdjango-import-export-2.7.1/import_export/locale/fa/LC_MESSAGES/django.po000066400000000000000000000103361416107567000257530ustar00rootroot00000000000000# Copyright (C) 2021 THE django-import-export # This file is distributed under the same license as the django-import-export package. # # Yazdan Ranjbar , 2021. msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2021-03-09 00:29+0030\n" "Last-Translator: Yazdan Ranjbar \n" "Language-Team: Persain/Farsi \n" "Language: Farsi/Persian\n" "MIME-Version: 0.1\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.5.4\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s یه وسیله ورودی-خروجی" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "بارگذاری تمام شد، با {} مورد جدید و {} مورد به روز شده." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

فایل بارگذاری شده encode اشتباهی دارد: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

در هنگام خواندن فایل %s با %s مواجه شد

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "بارگذاری" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "خروجی" #: admin.py:490 msgid "You must select an export format." msgstr "شما باید یک فرمت خروجی انتخاب کنید" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "خروجی %(verbose_name_plural)s انتخاب شده" #: forms.py:10 msgid "File to import" msgstr "قایل برای بارگذاری" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "فرمت" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "خانه" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "ارسال" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "پایین یک پیش‌نمایش از دیتا‌هایی است که بارگذاری خواهند شد اگر این موارد درست " "هستروی 'تایید بارگذاری' گلیگ گنید" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "تایید بارگذاری" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "این بارگذاری شامل این فیلد‌ها هست:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "خطاها" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "شماره خط" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "برخی از سطر‌ها معتبر نبودند" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "لطفا این خطا را تصحیح کنید و سپس مجدد فایل را بارگذاری کنید" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "سظر" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "فیلد‌های غیر اختصاصی" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "نمایش" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "جدید" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "در شد" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "حذف" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "بروزرسانی" #~ msgid "Import finished" #~ msgstr "بارگذاری به اتمام رسید" django-import-export-2.7.1/import_export/locale/fr/000077500000000000000000000000001416107567000224025ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/fr/LC_MESSAGES/000077500000000000000000000000001416107567000241675ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000033771416107567000260000ustar00rootroot00000000000000l5/hG'  *.6>0Ev!}A96xR .-AHPYjrz 11    

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1);

%s rencontré en essayant de lire le fichier: %s

Le fichier importé a un encodage erroné: %s

Voici un aperçu des données à importer. Si vous êtes satisfait des résultats, cliquez sur 'Confirmer l'importation'Confirmer l'importationSupprimerErreursExporterExporter %(verbose_name_plural)s selectionnésFichier à importerFormatAccueilImporterNuméro de ligneNouveauAperçuIgnoréSoumettreCet importateur va importer les champs suivants: Mettre à jourVous devez sélectionner un format d'exportation.django-import-export-2.7.1/import_export/locale/fr/LC_MESSAGES/django.po000066400000000000000000000071161416107567000257760ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Le fichier importé a un encodage erroné: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s rencontré en essayant de lire le fichier: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importer" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exporter" #: admin.py:490 msgid "You must select an export format." msgstr "Vous devez sélectionner un format d'exportation." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exporter %(verbose_name_plural)s selectionnés" #: forms.py:10 msgid "File to import" msgstr "Fichier à importer" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Accueil" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Soumettre" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Voici un aperçu des données à importer. Si vous êtes satisfait des " "résultats, cliquez sur 'Confirmer l'importation'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Confirmer l'importation" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Cet importateur va importer les champs suivants: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Erreurs" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Numéro de ligne" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Aperçu" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nouveau" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Ignoré" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Supprimer" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Mettre à jour" django-import-export-2.7.1/import_export/locale/it/000077500000000000000000000000001416107567000224075ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/it/LC_MESSAGES/000077500000000000000000000000001416107567000241745ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/it/LC_MESSAGES/django.mo000066400000000000000000000030011416107567000257650ustar00rootroot00000000000000Lh*9@G'Nv 0!ev  +&Rdl q { $,    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2015-08-30 20:32+0100 Last-Translator: Christian Galeffi Language-Team: Italian Language: it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.5.4 Questa è un'anteprima dei dati che saranno importati. Se il risultato è soddisfacente, premi 'Conferma importazione'Conferma importazioneCancellaErroriEsportareEsporta selezionati %(verbose_name_plural)sFile da importareFormatoHomeImportareNumero lineaNuovoAnteprimaSaltaInviareVerranno importati i seguenti campi:AggiornaDevi selezionare un formato di esportazione.django-import-export-2.7.1/import_export/locale/it/LC_MESSAGES/django.po000066400000000000000000000070021416107567000257750ustar00rootroot00000000000000# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Christian Galeffi , 2015. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2015-08-30 20:32+0100\n" "Last-Translator: Christian Galeffi \n" "Language-Team: Italian \n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.5.4\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importare" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Esportare" #: admin.py:490 msgid "You must select an export format." msgstr "Devi selezionare un formato di esportazione." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Esporta selezionati %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "File da importare" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Home" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Inviare" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Questa è un'anteprima dei dati che saranno importati. Se il risultato è " "soddisfacente, premi 'Conferma importazione'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Conferma importazione" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Verranno importati i seguenti campi:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Errori" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Numero linea" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Anteprima" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nuovo" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Salta" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Cancella" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Aggiorna" #~ msgid "Import finished" #~ msgstr "Importazione terminata" django-import-export-2.7.1/import_export/locale/ja/000077500000000000000000000000001416107567000223655ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ja/LC_MESSAGES/000077500000000000000000000000001416107567000241525ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ja/LC_MESSAGES/django.mo000066400000000000000000000031621416107567000257530ustar00rootroot00000000000000Lh*9@G'Nv 0!;U !+:>!y   *(B/    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; インポートされるデータのプレビューを表示しています。この内容で問題なければ「インポート実行」をクリックしてください。インポート実行削除エラーエクスポート選択した %(verbose_name_plural)s をエクスポートインポートするファイルフォーマットホームインポート行番号新規プレビュースキップ確定以下の列をインポートします。更新エクスポートフォーマットを選択してください。django-import-export-2.7.1/import_export/locale/ja/LC_MESSAGES/django.po000066400000000000000000000072311416107567000257570ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "インポート" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "エクスポート" #: admin.py:490 msgid "You must select an export format." msgstr "エクスポートフォーマットを選択してください。" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "選択した %(verbose_name_plural)s をエクスポート" #: forms.py:10 msgid "File to import" msgstr "インポートするファイル" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "フォーマット" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "ホーム" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "確定" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "インポートされるデータのプレビューを表示しています。この内容で問題なければ" "「インポート実行」をクリックしてください。" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "インポート実行" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "以下の列をインポートします。" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "エラー" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "行番号" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "プレビュー" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "新規" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "スキップ" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "削除" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "更新" #~ msgid "Import finished" #~ msgstr "インポートが完了しました。" django-import-export-2.7.1/import_export/locale/ko/000077500000000000000000000000001416107567000224045ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ko/LC_MESSAGES/000077500000000000000000000000001416107567000241715ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ko/LC_MESSAGES/django.mo000066400000000000000000000031571416107567000257760ustar00rootroot00000000000000Lh*9@GN]d kw_{0@!GDi9@G N[b i v@  2%@'G     Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportFile to importFormatImportLine numberNewPlease correct these errors in your data where possible, then reupload it using the form above.PreviewSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Jinmyeong Cho Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; 다음은 불러올 데이터의 미리보기 입니다.데이터에 문제가 없다면 확인을 눌러 가져오기를 진행하세요.확인삭제에러내보내기파일형식가져오기행 번호생성에러를 수정한 후 파일을 다시 업로드 해주세요.미리보기넘어감유효성 검증에 실패한 행이 있습니다.제출다음의 필드를 가져옵니다: 갱신내보낼 형식을 선택해주세요.django-import-export-2.7.1/import_export/locale/ko/LC_MESSAGES/django.po000066400000000000000000000067741416107567000260110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Jinmyeong Cho , 2020. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Jinmyeong Cho \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "가져오기" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "내보내기" #: admin.py:490 msgid "You must select an export format." msgstr "내보낼 형식을 선택해주세요." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "" #: forms.py:10 msgid "File to import" msgstr "파일" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "형식" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "제출" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "다음은 불러올 데이터의 미리보기 입니다.데이터에 문제가 없다면 확인을 눌러 가" "져오기를 진행하세요." #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "확인" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "다음의 필드를 가져옵니다: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "에러" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "행 번호" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "유효성 검증에 실패한 행이 있습니다." #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "에러를 수정한 후 파일을 다시 업로드 해주세요." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "미리보기" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "생성" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "넘어감" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "삭제" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "갱신" django-import-export-2.7.1/import_export/locale/kz/000077500000000000000000000000001416107567000224175ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/kz/LC_MESSAGES/000077500000000000000000000000001416107567000242045ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/kz/LC_MESSAGES/django.mo000066400000000000000000000055041416107567000260070ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!0-HKOC0a  O " $+ P  8 C 2V V  G      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Muslim Beibytuly Language-Team: LANGUAGE Language: Kazakh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %s арқылы import_export

%s файлды оқып жатқанда кездесті: %s

Импортталған файлда қате кодтау бар: %s

Төменде импортталатын деректерді алдын ала қарау берілген. Егер сіз нәтижелерге қанағаттансаңыз, 'Импортты растау' түймесін басыңыз.Импортты растауЖоюҚателерЭкспортТаңдалған %(verbose_name_plural)s экспорттаңызИмпорттауға арналған файлФорматБасты бетИмпортИмпорт аяқталды, {} жаңа және {} жаңартылды {}.Жол нөміріЖаңаӨріске қатысты емесМүмкіндігінше деректеріңіздегі қателерді түзетіңіз, содан кейін жоғарыдағы пішінді қолданып қайта жүктеңіз.Алдын-ала қарауҚатарӨткізілдіКейбір жолдар тексерілмедіЖіберуБұл импорттаушы келесі өрістерді импорттайды: ЖаңартуСіз экспорт форматын таңдауыңыз керек.django-import-export-2.7.1/import_export/locale/kz/LC_MESSAGES/django.po000066400000000000000000000104621416107567000260110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Muslim Beibytuly \n" "Language-Team: LANGUAGE \n" "Language: Kazakh\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s арқылы import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Импорт аяқталды, {} жаңа және {} жаңартылды {}." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Импортталған файлда қате кодтау бар: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s файлды оқып жатқанда кездесті: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Импорт" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Экспорт" #: admin.py:490 msgid "You must select an export format." msgstr "Сіз экспорт форматын таңдауыңыз керек." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Таңдалған %(verbose_name_plural)s экспорттаңыз" #: forms.py:10 msgid "File to import" msgstr "Импорттауға арналған файл" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Басты бет" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Жіберу" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Төменде импортталатын деректерді алдын ала қарау берілген. Егер сіз " "нәтижелерге қанағаттансаңыз, 'Импортты растау' түймесін басыңыз." #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Импортты растау" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Бұл импорттаушы келесі өрістерді импорттайды: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Қателер" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Жол нөмірі" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "Кейбір жолдар тексерілмеді" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Мүмкіндігінше деректеріңіздегі қателерді түзетіңіз, содан кейін жоғарыдағы " "пішінді қолданып қайта жүктеңіз." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "Қатар" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "Өріске қатысты емес" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Алдын-ала қарау" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Жаңа" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Өткізілді" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Жою" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Жаңарту" django-import-export-2.7.1/import_export/locale/nl/000077500000000000000000000000001416107567000224045ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/nl/LC_MESSAGES/000077500000000000000000000000001416107567000241715ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/nl/LC_MESSAGES/django.mo000066400000000000000000000045771416107567000260050ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!B?>UBj ~ / 0 /<B|V $ ,& S !]      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %s door import_export

%s tegengekomen tijden het lezen van het bestand: %s

Het geimporteerde bestand heeft de verkeerde encoding: %s

Hieronder is een voorvertoning van de data die geïmporteerd zal worden. Als u tevreden bent met het resultaat, klik dan op 'Accepteer de import'.Accepteer de importVerwijderenFoutenExporterenExporteer geselecteerde %(verbose_name_plural)sBestand om te importerenFormaatTerugImporterenImport is klaar met {} nieuwe en {} geupdate {}.Regel nummerNieuwNiet veld specifiekVerander alstublieft de volgende fouten in uw data waar mogelijk. Upload het bestand daarna nogmaals met het veld hierboven.VoorbeeldweergaveRegelOvergeslagenSommige regels zijn niet goedgekeurdIndienenDeze import zal de volgende velden toevoegenBijwerkenU moet een export formaat kiezen.django-import-export-2.7.1/import_export/locale/nl/LC_MESSAGES/django.po000066400000000000000000000075741416107567000260100ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Bram Janssen , 2019. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s door import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Import is klaar met {} nieuwe en {} geupdate {}." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Het geimporteerde bestand heeft de verkeerde encoding: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s tegengekomen tijden het lezen van het bestand: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importeren" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exporteren" #: admin.py:490 msgid "You must select an export format." msgstr "U moet een export formaat kiezen." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exporteer geselecteerde %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Bestand om te importeren" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formaat" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Terug" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Indienen" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Hieronder is een voorvertoning van de data die geïmporteerd zal worden. Als " "u tevreden bent met het resultaat, klik dan op 'Accepteer de import'." #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Accepteer de import" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Deze import zal de volgende velden toevoegen" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Fouten" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Regel nummer" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "Sommige regels zijn niet goedgekeurd" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Verander alstublieft de volgende fouten in uw data waar mogelijk. Upload het " "bestand daarna nogmaals met het veld hierboven." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "Regel" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "Niet veld specifiek" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Voorbeeldweergave" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nieuw" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Overgeslagen" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Verwijderen" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Bijwerken" django-import-export-2.7.1/import_export/locale/pl/000077500000000000000000000000001416107567000224065ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/pl/LC_MESSAGES/000077500000000000000000000000001416107567000241735ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000037041416107567000257760ustar00rootroot00000000000000 5"/Xh'=LSX/_ 0!|71{)9 <HM Va+i   %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %s przez import_export

%s napotkany podczas próby czytania pliku: %s

Zaimportowany plik ma złe kodowanie: %s

Poniżej znajdują się przykładowe dane do zaimportowania. Jeśli satysfakcjonuje Cię wynik, kliknij 'Potwierdź import'Potwierdź importUsuńBłędyEksportEksportuj wybrane %(verbose_name_plural)sPlik do importuFormatPowrótImportImport zakończony, z {} nowymi i {} zaktualizowanymi {}.Numer liniiNowyPodglądPominiętyWyślijZostaną zaimportowane następujące pola: ZaktualizowanyMusisz wybrać format eksportu.django-import-export-2.7.1/import_export/locale/pl/LC_MESSAGES/django.po000066400000000000000000000073511416107567000260030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Ludwik Trammer , 2015. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s przez import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Import zakończony, z {} nowymi i {} zaktualizowanymi {}." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Zaimportowany plik ma złe kodowanie: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s napotkany podczas próby czytania pliku: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Import" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Eksport" #: admin.py:490 msgid "You must select an export format." msgstr "Musisz wybrać format eksportu." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Eksportuj wybrane %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Plik do importu" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Powrót" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Wyślij" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Poniżej znajdują się przykładowe dane do zaimportowania. Jeśli " "satysfakcjonuje Cię wynik, kliknij 'Potwierdź import'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Potwierdź import" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Zostaną zaimportowane następujące pola: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Błędy" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Numer linii" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Podgląd" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nowy" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Pominięty" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Usuń" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Zaktualizowany" #~ msgid "Import finished" #~ msgstr "Zakończono importowanie" django-import-export-2.7.1/import_export/locale/pt_BR/000077500000000000000000000000001416107567000230015ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001416107567000245665ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000045531416107567000263740ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!GD7_=Ypx~->%6;_U #1 , 46      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2020-06-06 10:30-0500 Last-Translator: Daniel Pluth Language-Team: Language: pt_BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); X-Generator: Lokalize 20.04.1 %s através import_export

%s encontrado durante a leitura do arquivo: %s

O arquivo importado tem uma codificação errada: %s

Ver abaixo uma prévia dos dados a serem importados. Se você esta satisfeito com os resultados, clique em 'Confirmar importação'Confirmar importaçãoRemoverErrosExportarExportar %(verbose_name_plural)s selecionadosArquivo a ser importadoFormatoInícioImportarA importação foi completada com {} novas e {} atualizadas {}Número da linhaNovoCampo não é específicoPor favor corrigir os erros nos dados onde possível e recarregar os dados com o formato acima.PréviaLinhaNão usadosAlgumas linhas não foram validadasEnviarEste importador vai importar os seguintes campos:AtualizarVocê tem que selecionar um formato de exportação.django-import-export-2.7.1/import_export/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000075701416107567000264010ustar00rootroot00000000000000# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # # Dan , 2020. msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: 2020-06-06 10:30-0500\n" "Last-Translator: Daniel Pluth \n" "Language-Team: \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Lokalize 20.04.1\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s através import_export " #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "A importação foi completada com {} novas e {} atualizadas {}" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

O arquivo importado tem uma codificação errada: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s encontrado durante a leitura do arquivo: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importar" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportar" #: admin.py:490 msgid "You must select an export format." msgstr "Você tem que selecionar um formato de exportação." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s selecionados" #: forms.py:10 msgid "File to import" msgstr "Arquivo a ser importado" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Início" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Ver abaixo uma prévia dos dados a serem importados. Se você esta satisfeito " "com os resultados, clique em 'Confirmar importação'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Confirmar importação" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Este importador vai importar os seguintes campos:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Erros" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Número da linha" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "Algumas linhas não foram validadas" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Por favor corrigir os erros nos dados onde possível e recarregar os dados " "com o formato acima." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "Linha" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "Campo não é específico" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Prévia" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Novo" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Não usados" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Remover" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Atualizar" #~ msgid "Import finished" #~ msgstr "Importação finalizada" django-import-export-2.7.1/import_export/locale/ru/000077500000000000000000000000001416107567000224215ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ru/LC_MESSAGES/000077500000000000000000000000001416107567000242065ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000056071416107567000260150ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!AkS#2 ANG]  F4 L >_ ] v  D  C 2 AE      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %s через import_export

%s при попытке прочитать файл: %s

Импортированный файл имеет неправильную кодировку: %s

Ниже показано то, что будет импортировано. Нажмите 'Подтвердить импорт',если Вас устраивает результатПодтвердить импортУдаленоОшибкиЭкспортЭкспортировать выбранные %(verbose_name_plural)sФайл для импортаФорматГлавнаяИмпортИмпорт завершен, {} новых и {} обновлено.Номер строкиДобавленоНе относящиеся к конкретному полюПо возможности исправьте эти ошибки в своих данных, а затем повторно загрузите их, используя форму выше.ПредпросмотрСтрокаПропущеноНекоторые строки не прошли валидациюОтправитьБудут импортированы следующие поля: ОбновленоНеобходимо выбрать формат экспортаdjango-import-export-2.7.1/import_export/locale/ru/LC_MESSAGES/django.po000066400000000000000000000107021416107567000260100ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s через import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "Импорт завершен, {} новых и {} обновлено." #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

Импортированный файл имеет неправильную кодировку: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s при попытке прочитать файл: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Импорт" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Экспорт" #: admin.py:490 msgid "You must select an export format." msgstr "Необходимо выбрать формат экспорта" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Экспортировать выбранные %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Файл для импорта" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Главная" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Отправить" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Ниже показано то, что будет импортировано. Нажмите 'Подтвердить импорт',если " "Вас устраивает результат" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Подтвердить импорт" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Будут импортированы следующие поля: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Ошибки" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Номер строки" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "Некоторые строки не прошли валидацию" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "По возможности исправьте эти ошибки в своих данных, а затем повторно " "загрузите их, используя форму выше." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "Строка" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "Не относящиеся к конкретному полю" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Предпросмотр" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Добавлено" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Пропущено" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Удалено" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Обновлено" #~ msgid "Import finished" #~ msgstr "Импорт завершен" django-import-export-2.7.1/import_export/locale/sk/000077500000000000000000000000001416107567000224105ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/sk/LC_MESSAGES/000077500000000000000000000000001416107567000241755ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/sk/LC_MESSAGES/django.mo000066400000000000000000000030071416107567000257740ustar00rootroot00000000000000Lh*9@G'Nv 0!]ww   ,I\d jv '%    Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportLine numberNewPreviewSkippedSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; Nižšie je zobrazený náhľad importovaných dát. Ak je všetko v poriadku, kliknite na tlačidlo 'Potvrdiť import'Potvrdiť importVymazanýChybyExportovaťExportovať vybrané %(verbose_name_plural)sImportovať súborFormátDomovImportovaťČíslo riadkuNovýNáhľadPreskočenýOdoslaťBudú importované nasledujúce polia: AktualizovanýJe potrebné vybrať formát exportu.django-import-export-2.7.1/import_export/locale/sk/LC_MESSAGES/django.po000066400000000000000000000070341416107567000260030ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Juraj Bubniak , 2015. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "Importovať" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Exportovať" #: admin.py:490 msgid "You must select an export format." msgstr "Je potrebné vybrať formát exportu." #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportovať vybrané %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "Importovať súbor" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Formát" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Domov" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Odoslať" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Nižšie je zobrazený náhľad importovaných dát. Ak je všetko v poriadku, " "kliknite na tlačidlo 'Potvrdiť import'" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "Potvrdiť import" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Budú importované nasledujúce polia: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Chyby" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Číslo riadku" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Náhľad" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Nový" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Preskočený" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Vymazaný" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Aktualizovaný" #~ msgid "Import finished" #~ msgstr "Import dokončený" django-import-export-2.7.1/import_export/locale/tr/000077500000000000000000000000001416107567000224205ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/tr/LC_MESSAGES/000077500000000000000000000000001416107567000242055ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/tr/LC_MESSAGES/django.mo000066400000000000000000000046261416107567000260140ustar00rootroot00000000000000%`a5z/hIX_f'm/ _ jrv~0!A>P]Ex 3   4L]bsw  & >- l v      %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatHomeImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); %s vasıtasıyla import_export

Dosya okunurken %s hatası ile karşılaşıldı, okunan dosya adı: %s

İçe aktarılan dosyada yanlış kodlama bulunmaktadır: %s

Aşağıda içe aktarılacak verilerin önizlemesi verilmiştir. Sonuçlardan memnunsanız 'İçe aktarmayı onayla'yı tıklayın.İçe aktarmayı onaylaSilHatalarDışa aktarSeçililenleri dışa aktar %(verbose_name_plural)sİçe alınacak dosyaDosya biçimiAna sayfaİçe aktar{} yeni ve {} güncellenen {} ile içe aktarma bittiSatır numarasıYeniAlan olmayana özgüLütfen verilerinizdeki bu hataları olabildiğince düzeltin, sonra yukarıdaki formu kullanarak tekrar yükleyin.Ön izlemeSatırAtlandıBazı satırlar doğrulanamadıKaydetBu içe aktarıcı aşağıdaki alanları içe aktaracaktır: GüncelleBir dosya biçimi seçmelisinizdjango-import-export-2.7.1/import_export/locale/tr/LC_MESSAGES/django.po000066400000000000000000000076131416107567000260160ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s vasıtasıyla import_export" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "{} yeni ve {} güncellenen {} ile içe aktarma bitti" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

İçe aktarılan dosyada yanlış kodlama bulunmaktadır: %s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "" "

Dosya okunurken %s hatası ile karşılaşıldı, okunan dosya adı: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "İçe aktar" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "Dışa aktar" #: admin.py:490 msgid "You must select an export format." msgstr "Bir dosya biçimi seçmelisiniz" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Seçililenleri dışa aktar %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "İçe alınacak dosya" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "Dosya biçimi" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Ana sayfa" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "Kaydet" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Aşağıda içe aktarılacak verilerin önizlemesi verilmiştir. Sonuçlardan " "memnunsanız 'İçe aktarmayı onayla'yı tıklayın." #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "İçe aktarmayı onayla" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "Bu içe aktarıcı aşağıdaki alanları içe aktaracaktır: " #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "Hatalar" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "Satır numarası" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "Bazı satırlar doğrulanamadı" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Lütfen verilerinizdeki bu hataları olabildiğince düzeltin, sonra yukarıdaki " "formu kullanarak tekrar yükleyin." #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "Satır" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "Alan olmayana özgü" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "Ön izleme" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "Yeni" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "Atlandı" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "Sil" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "Güncelle" django-import-export-2.7.1/import_export/locale/zh_Hans/000077500000000000000000000000001416107567000233655ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/zh_Hans/LC_MESSAGES/000077500000000000000000000000001416107567000251525ustar00rootroot00000000000000django-import-export-2.7.1/import_export/locale/zh_Hans/LC_MESSAGES/django.mo000066400000000000000000000042261416107567000267550ustar00rootroot00000000000000%PQ5j/h9HOV']/ _U]ai0!=$%.J'ya '% MZa7hQ!(DKj$q     %s through import_export

%s encountered while trying to read file: %s

Imported file has a wrong encoding: %s

Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'Confirm importDeleteErrorsExportExport selected %(verbose_name_plural)sFile to importFormatImportImport finished, with {} new and {} updated {}.Line numberNewNon field specificPlease correct these errors in your data where possible, then reupload it using the form above.PreviewRowSkippedSome rows failed to validateSubmitThis importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: hao wang <173300430@qq.com> Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; %s 通过 django-import-export导入

%s 读取文件时遇到了冲突: %s

导入的文件编码有误:%s

以下是导入数据的预览。如果确认结果没有问题,可以点击 “确认导入”确认导入删除错误导出导出选中的 %(verbose_name_plural)s导入文件格式导入导入成功,新增{}条记录,更新{}条记录。行号新增没有指定的字段请使用上面的表单,纠正这些提示有错误的数据,并重新上传预览行忽略某些行验数据证失败提交此次将导入以下字段:更新您必须选择一个导出格式。django-import-export-2.7.1/import_export/locale/zh_Hans/LC_MESSAGES/django.po000066400000000000000000000073071416107567000267630ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-10-18 20:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: hao wang <173300430@qq.com>\n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: admin.py:158 #, python-format msgid "%s through import_export" msgstr "%s 通过 django-import-export导入" #: admin.py:164 msgid "Import finished, with {} new and {} updated {}." msgstr "导入成功,新增{}条记录,更新{}条记录。" #: admin.py:262 #, python-format msgid "

Imported file has a wrong encoding: %s

" msgstr "

导入的文件编码有误:%s

" #: admin.py:264 #, python-format msgid "

%s encountered while trying to read file: %s

" msgstr "

%s 读取文件时遇到了冲突: %s

" #: admin.py:295 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:10 msgid "Import" msgstr "导入" #: admin.py:429 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:7 msgid "Export" msgstr "导出" #: admin.py:490 msgid "You must select an export format." msgstr "您必须选择一个导出格式。" #: admin.py:513 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "导出选中的 %(verbose_name_plural)s" #: forms.py:10 msgid "File to import" msgstr "导入文件" #: forms.py:13 forms.py:41 forms.py:66 msgid "Format" msgstr "格式" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "" #: templates/admin/import_export/export.html:31 #: templates/admin/import_export/import.html:52 msgid "Submit" msgstr "提交" #: templates/admin/import_export/import.html:20 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "以下是导入数据的预览。如果确认结果没有问题,可以点击 “确认导入”" #: templates/admin/import_export/import.html:23 msgid "Confirm import" msgstr "确认导入" #: templates/admin/import_export/import.html:31 msgid "This importer will import the following fields: " msgstr "此次将导入以下字段:" #: templates/admin/import_export/import.html:61 #: templates/admin/import_export/import.html:90 msgid "Errors" msgstr "错误" #: templates/admin/import_export/import.html:72 msgid "Line number" msgstr "行号" #: templates/admin/import_export/import.html:82 msgid "Some rows failed to validate" msgstr "某些行验数据证失败" #: templates/admin/import_export/import.html:84 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "请使用上面的表单,纠正这些提示有错误的数据,并重新上传" #: templates/admin/import_export/import.html:89 msgid "Row" msgstr "行" #: templates/admin/import_export/import.html:116 msgid "Non field specific" msgstr "没有指定的字段" #: templates/admin/import_export/import.html:137 msgid "Preview" msgstr "预览" #: templates/admin/import_export/import.html:152 msgid "New" msgstr "新增" #: templates/admin/import_export/import.html:154 msgid "Skipped" msgstr "忽略" #: templates/admin/import_export/import.html:156 msgid "Delete" msgstr "删除" #: templates/admin/import_export/import.html:158 msgid "Update" msgstr "更新" #~ msgid "Import finished" #~ msgstr "导入完成" django-import-export-2.7.1/import_export/mixins.py000066400000000000000000000073521416107567000224240ustar00rootroot00000000000000from django.http import HttpResponse from django.utils.timezone import now from django.views.generic.edit import FormView from .formats import base_formats from .forms import ExportForm from .resources import modelresource_factory from .signals import post_export class BaseImportExportMixin: formats = base_formats.DEFAULT_FORMATS resource_class = None def get_resource_class(self): if not self.resource_class: return modelresource_factory(self.model) return self.resource_class def get_resource_kwargs(self, request, *args, **kwargs): return {} class BaseImportMixin(BaseImportExportMixin): def get_import_resource_class(self): """ Returns ResourceClass to use for import. """ return self.get_resource_class() def get_import_formats(self): """ Returns available import formats. """ return [f for f in self.formats if f().can_import()] def get_import_resource_kwargs(self, request, *args, **kwargs): return self.get_resource_kwargs(request, *args, **kwargs) class BaseExportMixin(BaseImportExportMixin): model = None def get_export_formats(self): """ Returns available export formats. """ return [f for f in self.formats if f().can_export()] def get_export_resource_class(self): """ Returns ResourceClass to use for export. """ return self.get_resource_class() def get_export_resource_kwargs(self, request, *args, **kwargs): return self.get_resource_kwargs(request, *args, **kwargs) def get_data_for_export(self, request, queryset, *args, **kwargs): resource_class = self.get_export_resource_class() return resource_class(**self.get_export_resource_kwargs(request, *args, **kwargs))\ .export(queryset, *args, **kwargs) def get_export_filename(self, file_format): date_str = now().strftime('%Y-%m-%d') filename = "%s-%s.%s" % (self.model.__name__, date_str, file_format.get_extension()) return filename class ExportViewMixin(BaseExportMixin): form_class = ExportForm def get_export_data(self, file_format, queryset, *args, **kwargs): """ Returns file_format representation for given queryset. """ data = self.get_data_for_export(self.request, queryset, *args, **kwargs) export_data = file_format.export_data(data) return export_data def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) return context def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['formats'] = self.get_export_formats() return kwargs class ExportViewFormMixin(ExportViewMixin, FormView): def form_valid(self, form): formats = self.get_export_formats() file_format = formats[ int(form.cleaned_data['file_format']) ]() if hasattr(self, 'get_filterset'): queryset = self.get_filterset(self.get_filterset_class()).qs else: queryset = self.get_queryset() export_data = self.get_export_data(file_format, queryset) content_type = file_format.get_content_type() # Django 1.7 uses the content_type kwarg instead of mimetype try: response = HttpResponse(export_data, content_type=content_type) except TypeError: response = HttpResponse(export_data, mimetype=content_type) response['Content-Disposition'] = 'attachment; filename="%s"' % ( self.get_export_filename(file_format), ) post_export.send(sender=None, model=self.model) return response django-import-export-2.7.1/import_export/resources.py000066400000000000000000001257061416107567000231330ustar00rootroot00000000000000import functools import logging import traceback from collections import OrderedDict from copy import deepcopy import django import tablib from diff_match_patch import diff_match_patch from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.management.color import no_style from django.core.paginator import Paginator from django.db import connections, router from django.db.models.fields.related import ForeignObjectRel from django.db.models.query import QuerySet from django.db.transaction import ( TransactionManagementError, savepoint, savepoint_commit, savepoint_rollback, ) from django.utils.encoding import force_str from django.utils.safestring import mark_safe from . import widgets from .fields import Field from .instance_loaders import ModelInstanceLoader from .results import Error, Result, RowResult from .utils import atomic_if_using_transaction if django.VERSION[0] >= 3: from django.core.exceptions import FieldDoesNotExist else: from django.db.models.fields import FieldDoesNotExist logger = logging.getLogger(__name__) # Set default logging handler to avoid "No handler found" warnings. logger.addHandler(logging.NullHandler()) def get_related_model(field): if hasattr(field, 'related_model'): return field.related_model # Django 1.6, 1.7 if field.rel: return field.rel.to class ResourceOptions: """ The inner Meta class allows for class-level configuration of how the Resource should behave. The following options are available: """ model = None """ Django Model class. It is used to introspect available fields. """ fields = None """ Controls what introspected fields the Resource should include. A whitelist of fields. """ exclude = None """ Controls what introspected fields the Resource should NOT include. A blacklist of fields. """ instance_loader_class = None """ Controls which class instance will take care of loading existing objects. """ import_id_fields = ['id'] """ Controls which object fields will be used to identify existing instances. """ export_order = None """ Controls export order for columns. """ widgets = None """ This dictionary defines widget kwargs for fields. """ use_transactions = None """ Controls if import should use database transactions. Default value is ``None`` meaning ``settings.IMPORT_EXPORT_USE_TRANSACTIONS`` will be evaluated. """ skip_unchanged = False """ Controls if the import should skip unchanged records. Default value is False """ report_skipped = True """ Controls if the result reports skipped rows. Default value is True """ clean_model_instances = False """ Controls whether ``instance.full_clean()`` is called during the import process to identify potential validation errors for each (non skipped) row. The default value is False. """ chunk_size = None """ Controls the chunk_size argument of Queryset.iterator or, if prefetch_related is used, the per_page attribute of Paginator. """ skip_diff = False """ Controls whether or not an instance should be diffed following import. By default, an instance is copied prior to insert, update or delete. After each row is processed, the instance's copy is diffed against the original, and the value stored in each ``RowResult``. If diffing is not required, then disabling the diff operation by setting this value to ``True`` improves performance, because the copy and comparison operations are skipped for each row. If enabled, then ``skip_row()`` checks do not execute, because 'skip' logic requires comparison between the stored and imported versions of a row. If enabled, then HTML row reports are also not generated (see ``skip_html_diff``). The default value is False. """ skip_html_diff = False """ Controls whether or not a HTML report is generated after each row. By default, the difference between a stored copy and an imported instance is generated in HTML form and stored in each ``RowResult``. The HTML report is used to present changes on the confirmation screen in the admin site, hence when this value is ``True``, then changes will not be presented on the confirmation screen. If the HTML report is not required, then setting this value to ``True`` improves performance, because the HTML generation is skipped for each row. This is a useful optimization when importing large datasets. The default value is False. """ use_bulk = False """ Controls whether import operations should be performed in bulk. By default, an object's save() method is called for each row in a data set. When bulk is enabled, objects are saved using bulk operations. """ batch_size = 1000 """ The batch_size parameter controls how many objects are created in a single query. The default is to create objects in batches of 1000. See `bulk_create() `_. This parameter is only used if ``use_bulk`` is True. """ force_init_instance = False """ If True, this parameter will prevent imports from checking the database for existing instances. Enabling this parameter is a performance enhancement if your import dataset is guaranteed to contain new instances. """ using_db = None """ DB Connection name to use for db transactions. If not provided, ``router.db_for_write(model)`` will be evaluated and if it's missing, DEFAULT_DB_ALIAS constant ("default") is used. """ class DeclarativeMetaclass(type): def __new__(cls, name, bases, attrs): declared_fields = [] meta = ResourceOptions() # If this class is subclassing another Resource, add that Resource's # fields. Note that we loop over the bases in *reverse*. This is # necessary in order to preserve the correct order of fields. for base in bases[::-1]: if hasattr(base, 'fields'): declared_fields = list(base.fields.items()) + declared_fields # Collect the Meta options options = getattr(base, 'Meta', None) for option in [option for option in dir(options) if not option.startswith('_') and hasattr(options, option)]: setattr(meta, option, getattr(options, option)) # Add direct fields for field_name, obj in attrs.copy().items(): if isinstance(obj, Field): field = attrs.pop(field_name) if not field.column_name: field.column_name = field_name declared_fields.append((field_name, field)) attrs['fields'] = OrderedDict(declared_fields) new_class = super().__new__(cls, name, bases, attrs) # Add direct options options = getattr(new_class, 'Meta', None) for option in [option for option in dir(options) if not option.startswith('_') and hasattr(options, option)]: setattr(meta, option, getattr(options, option)) new_class._meta = meta return new_class class Diff: def __init__(self, resource, instance, new): self.left = self._export_resource_fields(resource, instance) self.right = [] self.new = new def compare_with(self, resource, instance, dry_run=False): self.right = self._export_resource_fields(resource, instance) def as_html(self): data = [] dmp = diff_match_patch() for v1, v2 in zip(self.left, self.right): if v1 != v2 and self.new: v1 = "" diff = dmp.diff_main(force_str(v1), force_str(v2)) dmp.diff_cleanupSemantic(diff) html = dmp.diff_prettyHtml(diff) html = mark_safe(html) data.append(html) return data def _export_resource_fields(self, resource, instance): return [resource.export_field(f, instance) if instance else "" for f in resource.get_user_visible_fields()] class Resource(metaclass=DeclarativeMetaclass): """ Resource defines how objects are mapped to their import and export representations and handle importing and exporting data. """ def __init__(self): # The fields class attribute is the *class-wide* definition of # fields. Because a particular *instance* of the class might want to # alter self.fields, we create self.fields here by copying cls.fields. # Instances should always modify self.fields; they should not modify # cls.fields. self.fields = deepcopy(self.fields) # lists to hold model instances in memory when bulk operations are enabled self.create_instances = list() self.update_instances = list() self.delete_instances = list() @classmethod def get_result_class(self): """ Returns the class used to store the result of an import. """ return Result @classmethod def get_row_result_class(self): """ Returns the class used to store the result of a row import. """ return RowResult @classmethod def get_error_result_class(self): """ Returns the class used to store an error resulting from an import. """ return Error @classmethod def get_diff_class(self): """ Returns the class used to display the diff for an imported instance. """ return Diff def get_db_connection_name(self): if self._meta.using_db is None: return router.db_for_write(self._meta.model) else: return self._meta.using_db def get_use_transactions(self): if self._meta.use_transactions is None: return getattr(settings, 'IMPORT_EXPORT_USE_TRANSACTIONS', True) else: return self._meta.use_transactions def get_chunk_size(self): if self._meta.chunk_size is None: return getattr(settings, 'IMPORT_EXPORT_CHUNK_SIZE', 100) else: return self._meta.chunk_size def get_fields(self, **kwargs): """ Returns fields sorted according to :attr:`~import_export.resources.ResourceOptions.export_order`. """ return [self.fields[f] for f in self.get_export_order()] def get_field_name(self, field): """ Returns the field name for a given field. """ for field_name, f in self.fields.items(): if f == field: return field_name raise AttributeError("Field %s does not exists in %s resource" % ( field, self.__class__)) def init_instance(self, row=None): """ Initializes an object. Implemented in :meth:`import_export.resources.ModelResource.init_instance`. """ raise NotImplementedError() def get_instance(self, instance_loader, row): """ If all 'import_id_fields' are present in the dataset, calls the :doc:`InstanceLoader `. Otherwise, returns `None`. """ import_id_fields = [ self.fields[f] for f in self.get_import_id_fields() ] for field in import_id_fields: if field.column_name not in row: return return instance_loader.get_instance(row) def get_or_init_instance(self, instance_loader, row): """ Either fetches an already existing instance or initializes a new one. """ if not self._meta.force_init_instance: instance = self.get_instance(instance_loader, row) if instance: return (instance, False) return (self.init_instance(row), True) def get_import_id_fields(self): """ """ return self._meta.import_id_fields def get_bulk_update_fields(self): """ Returns the fields to be included in calls to bulk_update(). ``import_id_fields`` are removed because `id` fields cannot be supplied to bulk_update(). """ return [f for f in self.fields if f not in self._meta.import_id_fields] def bulk_create(self, using_transactions, dry_run, raise_errors, batch_size=None): """ Creates objects by calling ``bulk_create``. """ try: if len(self.create_instances) > 0: if not using_transactions and dry_run: pass else: self._meta.model.objects.bulk_create(self.create_instances, batch_size=batch_size) except Exception as e: logger.exception(e) if raise_errors: raise e finally: self.create_instances.clear() def bulk_update(self, using_transactions, dry_run, raise_errors, batch_size=None): """ Updates objects by calling ``bulk_update``. """ try: if len(self.update_instances) > 0: if not using_transactions and dry_run: pass else: self._meta.model.objects.bulk_update(self.update_instances, self.get_bulk_update_fields(), batch_size=batch_size) except Exception as e: logger.exception(e) if raise_errors: raise e finally: self.update_instances.clear() def bulk_delete(self, using_transactions, dry_run, raise_errors): """ Deletes objects by filtering on a list of instances to be deleted, then calling ``delete()`` on the entire queryset. """ try: if len(self.delete_instances) > 0: if not using_transactions and dry_run: pass else: delete_ids = [o.pk for o in self.delete_instances] self._meta.model.objects.filter(pk__in=delete_ids).delete() except Exception as e: logger.exception(e) if raise_errors: raise e finally: self.delete_instances.clear() def validate_instance(self, instance, import_validation_errors=None, validate_unique=True): """ Takes any validation errors that were raised by :meth:`~import_export.resources.Resource.import_obj`, and combines them with validation errors raised by the instance's ``full_clean()`` method. The combined errors are then re-raised as single, multi-field ValidationError. If the ``clean_model_instances`` option is False, the instances's ``full_clean()`` method is not called, and only the errors raised by ``import_obj()`` are re-raised. """ if import_validation_errors is None: errors = {} else: errors = import_validation_errors.copy() if self._meta.clean_model_instances: try: instance.full_clean( exclude=errors.keys(), validate_unique=validate_unique, ) except ValidationError as e: errors = e.update_error_dict(errors) if errors: raise ValidationError(errors) def save_instance(self, instance, using_transactions=True, dry_run=False): """ Takes care of saving the object to the database. Objects can be created in bulk if ``use_bulk`` is enabled. """ self.before_save_instance(instance, using_transactions, dry_run) if self._meta.use_bulk: if instance.pk: self.update_instances.append(instance) else: self.create_instances.append(instance) else: if not using_transactions and dry_run: # we don't have transactions and we want to do a dry_run pass else: instance.save() self.after_save_instance(instance, using_transactions, dry_run) def before_save_instance(self, instance, using_transactions, dry_run): """ Override to add additional logic. Does nothing by default. """ pass def after_save_instance(self, instance, using_transactions, dry_run): """ Override to add additional logic. Does nothing by default. """ pass def delete_instance(self, instance, using_transactions=True, dry_run=False): """ Calls :meth:`instance.delete` as long as ``dry_run`` is not set. If ``use_bulk`` then instances are appended to a list for bulk import. """ self.before_delete_instance(instance, dry_run) if self._meta.use_bulk: self.delete_instances.append(instance) else: if not using_transactions and dry_run: # we don't have transactions and we want to do a dry_run pass else: instance.delete() self.after_delete_instance(instance, dry_run) def before_delete_instance(self, instance, dry_run): """ Override to add additional logic. Does nothing by default. """ pass def after_delete_instance(self, instance, dry_run): """ Override to add additional logic. Does nothing by default. """ pass def import_field(self, field, obj, data, is_m2m=False, **kwargs): """ Calls :meth:`import_export.fields.Field.save` if ``Field.attribute`` is specified, and ``Field.column_name`` is found in ``data``. """ if field.attribute and field.column_name in data: field.save(obj, data, is_m2m, **kwargs) def get_import_fields(self): return self.get_fields() def import_obj(self, obj, data, dry_run, **kwargs): """ Traverses every field in this Resource and calls :meth:`~import_export.resources.Resource.import_field`. If ``import_field()`` results in a ``ValueError`` being raised for one of more fields, those errors are captured and reraised as a single, multi-field ValidationError.""" errors = {} for field in self.get_import_fields(): if isinstance(field.widget, widgets.ManyToManyWidget): continue try: self.import_field(field, obj, data, **kwargs) except ValueError as e: errors[field.attribute] = ValidationError( force_str(e), code="invalid") if errors: raise ValidationError(errors) def save_m2m(self, obj, data, using_transactions, dry_run): """ Saves m2m fields. Model instance need to have a primary key value before a many-to-many relationship can be used. """ if (not using_transactions and dry_run) or self._meta.use_bulk: # we don't have transactions and we want to do a dry_run # OR use_bulk is enabled (m2m operations are not supported for bulk operations) pass else: for field in self.get_import_fields(): if not isinstance(field.widget, widgets.ManyToManyWidget): continue self.import_field(field, obj, data, True) def for_delete(self, row, instance): """ Returns ``True`` if ``row`` importing should delete instance. Default implementation returns ``False``. Override this method to handle deletion. """ return False def skip_row(self, instance, original): """ Returns ``True`` if ``row`` importing should be skipped. Default implementation returns ``False`` unless skip_unchanged == True and skip_diff == False. If skip_diff is True, then no comparisons can be made because ``original`` will be None. When left unspecified, skip_diff and skip_unchanged both default to ``False``, and rows are never skipped. Override this method to handle skipping rows meeting certain conditions. Use ``super`` if you want to preserve default handling while overriding :: class YourResource(ModelResource): def skip_row(self, instance, original): # Add code here return super(YourResource, self).skip_row(instance, original) """ if not self._meta.skip_unchanged or self._meta.skip_diff: return False for field in self.get_import_fields(): try: # For fields that are models.fields.related.ManyRelatedManager # we need to compare the results if list(field.get_value(instance).all()) != list(field.get_value(original).all()): return False except AttributeError: if field.get_value(instance) != field.get_value(original): return False return True def get_diff_headers(self): """ Diff representation headers. """ return self.get_user_visible_headers() def before_import(self, dataset, using_transactions, dry_run, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def before_import_row(self, row, row_number=None, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def after_import_row(self, row, row_result, row_number=None, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def after_import_instance(self, instance, new, row_number=None, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def import_row(self, row, instance_loader, using_transactions=True, dry_run=False, raise_errors=False, **kwargs): """ Imports data from ``tablib.Dataset``. Refer to :doc:`import_workflow` for a more complete description of the whole import process. :param row: A ``dict`` of the row to import :param instance_loader: The instance loader to be used to load the row :param using_transactions: If ``using_transactions`` is set, a transaction is being used to wrap the import :param dry_run: If ``dry_run`` is set, or error occurs, transaction will be rolled back. """ skip_diff = self._meta.skip_diff row_result = self.get_row_result_class()() original = None try: self.before_import_row(row, **kwargs) instance, new = self.get_or_init_instance(instance_loader, row) self.after_import_instance(instance, new, **kwargs) if new: row_result.import_type = RowResult.IMPORT_TYPE_NEW else: row_result.import_type = RowResult.IMPORT_TYPE_UPDATE row_result.new_record = new if not skip_diff: original = deepcopy(instance) diff = self.get_diff_class()(self, original, new) if self.for_delete(row, instance): if new: row_result.import_type = RowResult.IMPORT_TYPE_SKIP if not skip_diff: diff.compare_with(self, None, dry_run) else: row_result.import_type = RowResult.IMPORT_TYPE_DELETE row_result.add_instance_info(instance) self.delete_instance(instance, using_transactions, dry_run) if not skip_diff: diff.compare_with(self, None, dry_run) else: import_validation_errors = {} try: self.import_obj(instance, row, dry_run, **kwargs) except ValidationError as e: # Validation errors from import_obj() are passed on to # validate_instance(), where they can be combined with model # instance validation errors if necessary import_validation_errors = e.update_error_dict(import_validation_errors) if self.skip_row(instance, original): row_result.import_type = RowResult.IMPORT_TYPE_SKIP else: self.validate_instance(instance, import_validation_errors) self.save_instance(instance, using_transactions, dry_run) self.save_m2m(instance, row, using_transactions, dry_run) row_result.add_instance_info(instance) if not skip_diff: diff.compare_with(self, instance, dry_run) if not skip_diff and not self._meta.skip_html_diff: row_result.diff = diff.as_html() self.after_import_row(row, row_result, **kwargs) except ValidationError as e: row_result.import_type = RowResult.IMPORT_TYPE_INVALID row_result.validation_error = e except Exception as e: row_result.import_type = RowResult.IMPORT_TYPE_ERROR # There is no point logging a transaction error for each row # when only the original error is likely to be relevant if not isinstance(e, TransactionManagementError): logger.debug(e, exc_info=e) tb_info = traceback.format_exc() row_result.errors.append(self.get_error_result_class()(e, tb_info, row)) if self._meta.use_bulk: # persist a batch of rows # because this is a batch, any exceptions are logged and not associated # with a specific row if len(self.create_instances) == self._meta.batch_size: self.bulk_create(using_transactions, dry_run, raise_errors, batch_size=self._meta.batch_size) if len(self.update_instances) == self._meta.batch_size: self.bulk_update(using_transactions, dry_run, raise_errors, batch_size=self._meta.batch_size) if len(self.delete_instances) == self._meta.batch_size: self.bulk_delete(using_transactions, dry_run, raise_errors) return row_result def import_data(self, dataset, dry_run=False, raise_errors=False, use_transactions=None, collect_failed_rows=False, rollback_on_validation_errors=False, **kwargs): """ Imports data from ``tablib.Dataset``. Refer to :doc:`import_workflow` for a more complete description of the whole import process. :param dataset: A ``tablib.Dataset`` :param raise_errors: Whether errors should be printed to the end user or raised regularly. :param use_transactions: If ``True`` the import process will be processed inside a transaction. :param collect_failed_rows: If ``True`` the import process will collect failed rows. :param rollback_on_validation_errors: If both ``use_transactions`` and ``rollback_on_validation_errors`` are set to ``True``, the import process will be rolled back in case of ValidationError. :param dry_run: If ``dry_run`` is set, or an error occurs, if a transaction is being used, it will be rolled back. """ if use_transactions is None: use_transactions = self.get_use_transactions() db_connection = self.get_db_connection_name() connection = connections[db_connection] supports_transactions = getattr(connection.features, "supports_transactions", False) if use_transactions and not supports_transactions: raise ImproperlyConfigured using_transactions = (use_transactions or dry_run) and supports_transactions if self._meta.batch_size is not None and (not isinstance(self._meta.batch_size, int) or self._meta.batch_size < 0): raise ValueError("Batch size must be a positive integer") with atomic_if_using_transaction(using_transactions, using=db_connection): return self.import_data_inner( dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, rollback_on_validation_errors, **kwargs) def import_data_inner( self, dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, rollback_on_validation_errors=False, **kwargs): result = self.get_result_class()() result.diff_headers = self.get_diff_headers() result.total_rows = len(dataset) db_connection = self.get_db_connection_name() if using_transactions: # when transactions are used we want to create/update/delete object # as transaction will be rolled back if dry_run is set sp1 = savepoint(using=db_connection) try: with atomic_if_using_transaction(using_transactions, using=db_connection): self.before_import(dataset, using_transactions, dry_run, **kwargs) except Exception as e: logger.debug(e, exc_info=e) tb_info = traceback.format_exc() result.append_base_error(self.get_error_result_class()(e, tb_info)) if raise_errors: raise instance_loader = self._meta.instance_loader_class(self, dataset) # Update the total in case the dataset was altered by before_import() result.total_rows = len(dataset) if collect_failed_rows: result.add_dataset_headers(dataset.headers) for i, row in enumerate(dataset.dict, 1): with atomic_if_using_transaction(using_transactions, using=db_connection): row_result = self.import_row( row, instance_loader, using_transactions=using_transactions, dry_run=dry_run, row_number=i, raise_errors=raise_errors, **kwargs ) result.increment_row_result_total(row_result) if row_result.errors: if collect_failed_rows: result.append_failed_row(row, row_result.errors[0]) if raise_errors: raise row_result.errors[-1].error elif row_result.validation_error: result.append_invalid_row(i, row, row_result.validation_error) if collect_failed_rows: result.append_failed_row(row, row_result.validation_error) if raise_errors: raise row_result.validation_error if (row_result.import_type != RowResult.IMPORT_TYPE_SKIP or self._meta.report_skipped): result.append_row_result(row_result) if self._meta.use_bulk: # bulk persist any instances which are still pending with atomic_if_using_transaction(using_transactions, using=db_connection): self.bulk_create(using_transactions, dry_run, raise_errors) self.bulk_update(using_transactions, dry_run, raise_errors) self.bulk_delete(using_transactions, dry_run, raise_errors) try: with atomic_if_using_transaction(using_transactions, using=db_connection): self.after_import(dataset, result, using_transactions, dry_run, **kwargs) except Exception as e: logger.debug(e, exc_info=e) tb_info = traceback.format_exc() result.append_base_error(self.get_error_result_class()(e, tb_info)) if raise_errors: raise if using_transactions: if dry_run or \ result.has_errors() or \ (rollback_on_validation_errors and result.has_validation_errors()): savepoint_rollback(sp1, using=db_connection) else: savepoint_commit(sp1, using=db_connection) return result def get_export_order(self): order = tuple(self._meta.export_order or ()) return order + tuple(k for k in self.fields if k not in order) def before_export(self, queryset, *args, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def after_export(self, queryset, data, *args, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def export_field(self, field, obj): field_name = self.get_field_name(field) method = getattr(self, 'dehydrate_%s' % field_name, None) if method is not None: return method(obj) return field.export(obj) def get_export_fields(self): return self.get_fields() def export_resource(self, obj): return [self.export_field(field, obj) for field in self.get_export_fields()] def get_export_headers(self): headers = [ force_str(field.column_name) for field in self.get_export_fields()] return headers def get_user_visible_headers(self): headers = [ force_str(field.column_name) for field in self.get_user_visible_fields()] return headers def get_user_visible_fields(self): return self.get_fields() def iter_queryset(self, queryset): if not isinstance(queryset, QuerySet): yield from queryset elif queryset._prefetch_related_lookups: # Django's queryset.iterator ignores prefetch_related which might result # in an excessive amount of db calls. Therefore we use pagination # as a work-around if not queryset.query.order_by: # Paginator() throws a warning if there is no sorting # attached to the queryset queryset = queryset.order_by('pk') paginator = Paginator(queryset, self.get_chunk_size()) for index in range(paginator.num_pages): yield from paginator.get_page(index + 1) else: yield from queryset.iterator(chunk_size=self.get_chunk_size()) def export(self, queryset=None, *args, **kwargs): """ Exports a resource. """ self.before_export(queryset, *args, **kwargs) if queryset is None: queryset = self.get_queryset() headers = self.get_export_headers() data = tablib.Dataset(headers=headers) for obj in self.iter_queryset(queryset): data.append(self.export_resource(obj)) self.after_export(queryset, data, *args, **kwargs) return data class ModelDeclarativeMetaclass(DeclarativeMetaclass): def __new__(cls, name, bases, attrs): new_class = super().__new__(cls, name, bases, attrs) opts = new_class._meta if not opts.instance_loader_class: opts.instance_loader_class = ModelInstanceLoader if opts.model: model_opts = opts.model._meta declared_fields = new_class.fields field_list = [] for f in sorted(model_opts.fields + model_opts.many_to_many): if opts.fields is not None and not f.name in opts.fields: continue if opts.exclude and f.name in opts.exclude: continue if f.name in declared_fields: continue field = new_class.field_from_django_field(f.name, f, readonly=False) field_list.append((f.name, field, )) new_class.fields.update(OrderedDict(field_list)) # add fields that follow relationships if opts.fields is not None: field_list = [] for field_name in opts.fields: if field_name in declared_fields: continue if field_name.find('__') == -1: continue model = opts.model attrs = field_name.split('__') for i, attr in enumerate(attrs): verbose_path = ".".join([opts.model.__name__] + attrs[0:i+1]) try: f = model._meta.get_field(attr) except FieldDoesNotExist as e: logger.debug(e, exc_info=e) raise FieldDoesNotExist( "%s: %s has no field named '%s'" % (verbose_path, model.__name__, attr)) if i < len(attrs) - 1: # We're not at the last attribute yet, so check # that we're looking at a relation, and move on to # the next model. if isinstance(f, ForeignObjectRel): model = get_related_model(f) else: if get_related_model(f) is None: raise KeyError( '%s is not a relation' % verbose_path) model = get_related_model(f) if isinstance(f, ForeignObjectRel): f = f.field field = new_class.field_from_django_field(field_name, f, readonly=True) field_list.append((field_name, field)) new_class.fields.update(OrderedDict(field_list)) return new_class class ModelResource(Resource, metaclass=ModelDeclarativeMetaclass): """ ModelResource is Resource subclass for handling Django models. """ DEFAULT_RESOURCE_FIELD = Field WIDGETS_MAP = { 'ManyToManyField': 'get_m2m_widget', 'OneToOneField': 'get_fk_widget', 'ForeignKey': 'get_fk_widget', 'DecimalField': widgets.DecimalWidget, 'DateTimeField': widgets.DateTimeWidget, 'DateField': widgets.DateWidget, 'TimeField': widgets.TimeWidget, 'DurationField': widgets.DurationWidget, 'FloatField': widgets.FloatWidget, 'IntegerField': widgets.IntegerWidget, 'PositiveIntegerField': widgets.IntegerWidget, 'BigIntegerField': widgets.IntegerWidget, 'PositiveSmallIntegerField': widgets.IntegerWidget, 'SmallIntegerField': widgets.IntegerWidget, 'SmallAutoField': widgets.IntegerWidget, 'AutoField': widgets.IntegerWidget, 'BigAutoField': widgets.IntegerWidget, 'NullBooleanField': widgets.BooleanWidget, 'BooleanField': widgets.BooleanWidget, } @classmethod def get_m2m_widget(cls, field): """ Prepare widget for m2m field """ return functools.partial( widgets.ManyToManyWidget, model=get_related_model(field)) @classmethod def get_fk_widget(cls, field): """ Prepare widget for fk and o2o fields """ return functools.partial( widgets.ForeignKeyWidget, model=get_related_model(field)) @classmethod def widget_from_django_field(cls, f, default=widgets.Widget): """ Returns the widget that would likely be associated with each Django type. Includes mapping of Postgres Array and JSON fields. In the case that psycopg2 is not installed, we consume the error and process the field regardless. """ result = default internal_type = "" if callable(getattr(f, "get_internal_type", None)): internal_type = f.get_internal_type() if internal_type in cls.WIDGETS_MAP: result = cls.WIDGETS_MAP[internal_type] if isinstance(result, str): result = getattr(cls, result)(f) else: try: from django.contrib.postgres.fields import ArrayField try: from django.db.models import JSONField except ImportError: from django.contrib.postgres.fields import JSONField except ImportError: # ImportError: No module named psycopg2.extras class ArrayField: pass class JSONField: pass if isinstance(f, ArrayField): return widgets.SimpleArrayWidget elif isinstance(f, JSONField): return widgets.JSONWidget return result @classmethod def widget_kwargs_for_field(self, field_name): """ Returns widget kwargs for given field_name. """ if self._meta.widgets: return self._meta.widgets.get(field_name, {}) return {} @classmethod def field_from_django_field(cls, field_name, django_field, readonly): """ Returns a Resource Field instance for the given Django model field. """ FieldWidget = cls.widget_from_django_field(django_field) widget_kwargs = cls.widget_kwargs_for_field(field_name) field = cls.DEFAULT_RESOURCE_FIELD( attribute=field_name, column_name=field_name, widget=FieldWidget(**widget_kwargs), readonly=readonly, default=django_field.default, ) return field def get_queryset(self): """ Returns a queryset of all objects for this model. Override this if you want to limit the returned queryset. """ return self._meta.model.objects.all() def init_instance(self, row=None): """ Initializes a new Django model. """ return self._meta.model() def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): """ Reset the SQL sequences after new objects are imported """ # Adapted from django's loaddata if not dry_run and any(r.import_type == RowResult.IMPORT_TYPE_NEW for r in result.rows): db_connection = self.get_db_connection_name() connection = connections[db_connection] sequence_sql = connection.ops.sequence_reset_sql(no_style(), [self._meta.model]) if sequence_sql: cursor = connection.cursor() try: for line in sequence_sql: cursor.execute(line) finally: cursor.close() def modelresource_factory(model, resource_class=ModelResource): """ Factory for creating ``ModelResource`` class for given Django model. """ attrs = {'model': model} Meta = type(str('Meta'), (object,), attrs) class_name = model.__name__ + str('Resource') class_attrs = { 'Meta': Meta, } metaclass = ModelDeclarativeMetaclass return metaclass(class_name, (resource_class,), class_attrs) django-import-export-2.7.1/import_export/results.py000066400000000000000000000114431416107567000226120ustar00rootroot00000000000000from collections import OrderedDict from django.core.exceptions import NON_FIELD_ERRORS from django.utils.encoding import force_str from tablib import Dataset class Error: def __init__(self, error, traceback=None, row=None): self.error = error self.traceback = traceback self.row = row class RowResult: IMPORT_TYPE_UPDATE = 'update' IMPORT_TYPE_NEW = 'new' IMPORT_TYPE_DELETE = 'delete' IMPORT_TYPE_SKIP = 'skip' IMPORT_TYPE_ERROR = 'error' IMPORT_TYPE_INVALID = 'invalid' valid_import_types = frozenset([ IMPORT_TYPE_NEW, IMPORT_TYPE_UPDATE, IMPORT_TYPE_DELETE, IMPORT_TYPE_SKIP, ]) def __init__(self): self.errors = [] self.validation_error = None self.diff = None self.import_type = None self.raw_values = {} self.object_id = None self.object_repr = None def add_instance_info(self, instance): if instance is not None: # Add object info to RowResult (e.g. for LogEntry) self.object_id = getattr(instance, "pk", None) self.object_repr = force_str(instance) class InvalidRow: """A row that resulted in one or more ``ValidationError`` being raised during import.""" def __init__(self, number, validation_error, values): self.number = number self.error = validation_error self.values = values try: self.error_dict = validation_error.message_dict except AttributeError: self.error_dict = {NON_FIELD_ERRORS: validation_error.messages} @property def field_specific_errors(self): """Returns a dictionary of field-specific validation errors for this row.""" return { key: value for key, value in self.error_dict.items() if key != NON_FIELD_ERRORS } @property def non_field_specific_errors(self): """Returns a list of non field-specific validation errors for this row.""" return self.error_dict.get(NON_FIELD_ERRORS, []) @property def error_count(self): """Returns the total number of validation errors for this row.""" count = 0 for error_list in self.error_dict.values(): count += len(error_list) return count class Result: def __init__(self, *args, **kwargs): super().__init__() self.base_errors = [] self.diff_headers = [] self.rows = [] # RowResults self.invalid_rows = [] # InvalidRow self.failed_dataset = Dataset() self.totals = OrderedDict([(RowResult.IMPORT_TYPE_NEW, 0), (RowResult.IMPORT_TYPE_UPDATE, 0), (RowResult.IMPORT_TYPE_DELETE, 0), (RowResult.IMPORT_TYPE_SKIP, 0), (RowResult.IMPORT_TYPE_ERROR, 0), (RowResult.IMPORT_TYPE_INVALID, 0)]) self.total_rows = 0 def valid_rows(self): return [ r for r in self.rows if r.import_type in RowResult.valid_import_types ] def append_row_result(self, row_result): self.rows.append(row_result) def append_base_error(self, error): self.base_errors.append(error) def add_dataset_headers(self, headers): self.failed_dataset.headers = headers + ["Error"] def append_failed_row(self, row, error): row_values = [v for (k, v) in row.items()] try: row_values.append(str(error.error)) except AttributeError: row_values.append(str(error)) self.failed_dataset.append(row_values) def append_invalid_row(self, number, row, validation_error): # NOTE: value order must match diff_headers order, so that row # values and column headers match in the UI when displayed values = tuple(row.get(col, "---") for col in self.diff_headers) self.invalid_rows.append( InvalidRow(number=number, validation_error=validation_error, values=values) ) def increment_row_result_total(self, row_result): if row_result.import_type: self.totals[row_result.import_type] += 1 def row_errors(self): return [(i + 1, row.errors) for i, row in enumerate(self.rows) if row.errors] def has_errors(self): """Returns a boolean indicating whether the import process resulted in any critical (non-validation) errors for this result.""" return bool(self.base_errors or self.row_errors()) def has_validation_errors(self): """Returns a boolean indicating whether the import process resulted in any validation errors for this result.""" return bool(self.invalid_rows) def __iter__(self): return iter(self.rows) django-import-export-2.7.1/import_export/signals.py000066400000000000000000000001561416107567000225500ustar00rootroot00000000000000from django.dispatch import Signal # Args: model post_export = Signal() # Args: model post_import = Signal() django-import-export-2.7.1/import_export/static/000077500000000000000000000000001416107567000220235ustar00rootroot00000000000000django-import-export-2.7.1/import_export/static/import_export/000077500000000000000000000000001416107567000247365ustar00rootroot00000000000000django-import-export-2.7.1/import_export/static/import_export/action_formats.js000066400000000000000000000013751416107567000303120ustar00rootroot00000000000000(function($) { $(document).ready(function() { var $actionsSelect, $formatsElement; if ($('body').hasClass('grp-change-list')) { // using grappelli $actionsSelect = $('#grp-changelist-form select[name="action"]'); $formatsElement = $('#grp-changelist-form select[name="file_format"]'); } else { // using default admin $actionsSelect = $('#changelist-form select[name="action"]'); $formatsElement = $('#changelist-form select[name="file_format"]').parent(); } $actionsSelect.change(function() { if ($(this).val() === 'export_admin_action') { $formatsElement.show(); } else { $formatsElement.hide(); } }); $actionsSelect.change(); }); })(django.jQuery); django-import-export-2.7.1/import_export/static/import_export/import.css000066400000000000000000000025401416107567000267630ustar00rootroot00000000000000.import-preview .errors { position: relative; } .validation-error-count { display: inline-block; background-color: #e40000; border-radius: 6px; color: white; font-size: 0.9em; position: relative; font-weight: bold; margin-top: -2px; padding: 0.2em 0.4em; } .validation-error-container { position: absolute; opacity: 0; pointer-events: none; background-color: #ffc1c1; padding: 14px 15px 10px; top: 25px; margin: 0 0 20px 0; width: 200px; z-index: 2; } table.import-preview tr.skip { background-color: #d2d2d2; } table.import-preview tr.new { background-color: #bdd8b2; } table.import-preview tr.delete { background-color: #f9bebf; } table.import-preview tr.update { background-color: #fdfdcf; } .import-preview td:hover .validation-error-count { z-index: 3; } .import-preview td:hover .validation-error-container { opacity: 1; pointer-events: auto; } .validation-error-list { margin: 0; padding: 0; } .validation-error-list li { list-style: none; margin: 0; } .validation-error-list > li > ul { margin: 8px 0; padding: 0; } .validation-error-list > li > ul > li { padding: 0; margin: 0 0 10px; line-height: 1.28em; } .validation-error-field-label { display: block; border-bottom: 1px solid #e40000; color: #e40000; text-transform: uppercase; font-weight: bold; font-size: 0.85em; } django-import-export-2.7.1/import_export/templates/000077500000000000000000000000001416107567000225325ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templates/admin/000077500000000000000000000000001416107567000236225ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templates/admin/import_export/000077500000000000000000000000001416107567000265355ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templates/admin/import_export/base.html000066400000000000000000000014251416107567000303370ustar00rootroot00000000000000{% extends "admin/base_site.html" %} {% load i18n admin_modify %} {% load admin_urls %} {% load static %} {% block extrastyle %}{{ block.super }}{% endblock %} {% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} {% if not is_popup %} {% block breadcrumbs %} {% endblock %} {% endif %} django-import-export-2.7.1/import_export/templates/admin/import_export/change_list.html000066400000000000000000000006071416107567000317060ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {# Original template renders object-tools only when has_add_permission is True. #} {# This hack allows sub templates to add to object-tools #} {% block object-tools %}
    {% block object-tools-items %} {% if has_add_permission %} {{ block.super }} {% endif %} {% endblock %}
{% endblock %} django-import-export-2.7.1/import_export/templates/admin/import_export/change_list_export.html000066400000000000000000000002731416107567000333060ustar00rootroot00000000000000{% extends "admin/import_export/change_list.html" %} {% block object-tools-items %} {% include "admin/import_export/change_list_export_item.html" %} {{ block.super }} {% endblock %} django-import-export-2.7.1/import_export/templates/admin/import_export/change_list_export_item.html000066400000000000000000000003171416107567000343230ustar00rootroot00000000000000{% load i18n %} {% load admin_urls %} {% if has_export_permission %}
  • {% trans "Export" %}
  • {% endif %} django-import-export-2.7.1/import_export/templates/admin/import_export/change_list_import.html000066400000000000000000000002731416107567000332770ustar00rootroot00000000000000{% extends "admin/import_export/change_list.html" %} {% block object-tools-items %} {% include "admin/import_export/change_list_import_item.html" %} {{ block.super }} {% endblock %} change_list_import_export.html000066400000000000000000000003761416107567000346250ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templates/admin/import_export{% extends "admin/import_export/change_list.html" %} {% block object-tools-items %} {% include "admin/import_export/change_list_import_item.html" %} {% include "admin/import_export/change_list_export_item.html" %} {{ block.super }} {% endblock %} django-import-export-2.7.1/import_export/templates/admin/import_export/change_list_import_item.html000066400000000000000000000002701416107567000343120ustar00rootroot00000000000000{% load i18n %} {% load admin_urls %} {% if has_import_permission %}
  • {% trans "Import" %}
  • {% endif %} django-import-export-2.7.1/import_export/templates/admin/import_export/export.html000066400000000000000000000013561416107567000307510ustar00rootroot00000000000000{% extends "admin/import_export/base.html" %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% block breadcrumbs_last %} {% trans "Export" %} {% endblock %} {% block content %}
    {% csrf_token %}
    {% for field in form %}
    {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.field.help_text %}

    {{ field.field.help_text|safe }}

    {% endif %}
    {% endfor %}
    {% endblock %} django-import-export-2.7.1/import_export/templates/admin/import_export/import.html000066400000000000000000000123011416107567000307320ustar00rootroot00000000000000{% extends "admin/import_export/base.html" %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% load static %} {% block extrastyle %}{{ block.super }}{% endblock %} {% block breadcrumbs_last %} {% trans "Import" %} {% endblock %} {% block content %} {% if confirm_form %}
    {% csrf_token %} {{ confirm_form.as_p }}

    {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %}

    {% else %}
    {% csrf_token %}

    {% trans "This importer will import the following fields: " %} {{ fields|join:", " }}

    {% for field in form %}
    {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.field.help_text %}

    {{ field.field.help_text|safe }}

    {% endif %}
    {% endfor %}
    {% endif %} {% if result %} {% if result.has_errors %}

    {% trans "Errors" %}

      {% for error in result.base_errors %}
    • {{ error.error }}
      {{ error.traceback|linebreaks }}
    • {% endfor %} {% for line, errors in result.row_errors %} {% for error in errors %}
    • {% trans "Line number" %}: {{ line }} - {{ error.error }}
      {{ error.row.values|join:", " }}
      {{ error.traceback|linebreaks }}
    • {% endfor %} {% endfor %}
    {% elif result.has_validation_errors %}

    {% trans "Some rows failed to validate" %}

    {% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}

    {% for field in result.diff_headers %} {% endfor %} {% for row in result.invalid_rows %} {% for field in row.values %} {% endfor %} {% endfor %}
    {% trans "Row" %} {% trans "Errors" %}{{ field }}
    {{ row.number }} {{ row.error_count }}
      {% for field_name, error_list in row.field_specific_errors.items %}
    • {{ field_name }}
        {% for error in error_list %}
      • {{ error }}
      • {% endfor %}
    • {% endfor %} {% if row.non_field_specific_errors %}
    • {% trans "Non field specific" %}
        {% for error in row.non_field_specific_errors %}
      • {{ error }}
      • {% endfor %}
    • {% endif %}
    {{ field }}
    {% else %}

    {% trans "Preview" %}

    {% for field in result.diff_headers %} {% endfor %} {% for row in result.valid_rows %} {% for field in row.diff %} {% endfor %} {% endfor %}
    {{ field }}
    {% if row.import_type == 'new' %} {% trans "New" %} {% elif row.import_type == 'skip' %} {% trans "Skipped" %} {% elif row.import_type == 'delete' %} {% trans "Delete" %} {% elif row.import_type == 'update' %} {% trans "Update" %} {% endif %} {{ field }}
    {% endif %} {% endif %} {% endblock %} django-import-export-2.7.1/import_export/templatetags/000077500000000000000000000000001416107567000232265ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templatetags/__init__.py000066400000000000000000000000001416107567000253250ustar00rootroot00000000000000django-import-export-2.7.1/import_export/templatetags/import_export_tags.py000066400000000000000000000005021416107567000275260ustar00rootroot00000000000000from diff_match_patch import diff_match_patch from django import template register = template.Library() @register.simple_tag def compare_values(value1, value2): dmp = diff_match_patch() diff = dmp.diff_main(value1, value2) dmp.diff_cleanupSemantic(diff) html = dmp.diff_prettyHtml(diff) return html django-import-export-2.7.1/import_export/tmp_storages.py000066400000000000000000000043421416107567000236200ustar00rootroot00000000000000import os import tempfile from uuid import uuid4 from django.core.cache import cache from django.core.files.base import ContentFile from django.core.files.storage import default_storage class BaseStorage: def __init__(self, name=None): self.name = name def save(self, data, mode='w'): raise NotImplementedError def read(self, read_mode='r'): raise NotImplementedError def remove(self): raise NotImplementedError class TempFolderStorage(BaseStorage): def open(self, mode='r'): if self.name: return open(self.get_full_path(), mode) else: tmp_file = tempfile.NamedTemporaryFile(delete=False) self.name = tmp_file.name return tmp_file def save(self, data, mode='w'): with self.open(mode=mode) as file: file.write(data) def read(self, mode='r'): with self.open(mode=mode) as file: return file.read() def remove(self): os.remove(self.get_full_path()) def get_full_path(self): return os.path.join( tempfile.gettempdir(), self.name ) class CacheStorage(BaseStorage): """ By default memcache maximum size per key is 1MB, be careful with large files. """ CACHE_LIFETIME = 86400 CACHE_PREFIX = 'django-import-export-' def save(self, data, mode=None): if not self.name: self.name = uuid4().hex cache.set(self.CACHE_PREFIX + self.name, data, self.CACHE_LIFETIME) def read(self, read_mode='r'): return cache.get(self.CACHE_PREFIX + self.name) def remove(self): cache.delete(self.name) class MediaStorage(BaseStorage): MEDIA_FOLDER = 'django-import-export' def save(self, data, mode=None): if not self.name: self.name = uuid4().hex default_storage.save(self.get_full_path(), ContentFile(data)) def read(self, read_mode='rb'): with default_storage.open(self.get_full_path(), mode=read_mode) as f: return f.read() def remove(self): default_storage.delete(self.get_full_path()) def get_full_path(self): return os.path.join( self.MEDIA_FOLDER, self.name ) django-import-export-2.7.1/import_export/utils.py000066400000000000000000000013351416107567000222500ustar00rootroot00000000000000from django.db import transaction class atomic_if_using_transaction: """Context manager wraps `atomic` if `using_transactions`. Replaces code:: if using_transactions: with transaction.atomic(using=using): return something() return something() """ def __init__(self, using_transactions, using): self.using_transactions = using_transactions if using_transactions: self.context_manager = transaction.atomic(using=using) def __enter__(self): if self.using_transactions: self.context_manager.__enter__() def __exit__(self, *args): if self.using_transactions: self.context_manager.__exit__(*args) django-import-export-2.7.1/import_export/widgets.py000066400000000000000000000343461416107567000225660ustar00rootroot00000000000000import json from datetime import date, datetime, time from decimal import Decimal import django from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone from django.utils.dateparse import parse_duration from django.utils.encoding import force_str, smart_str def format_datetime(value, datetime_format): # conditional logic to handle correct formatting of dates # see https://code.djangoproject.com/ticket/32738 if django.VERSION[0] >= 4: format = django.utils.formats.sanitize_strftime_format(datetime_format) return value.strftime(format) else: return django.utils.datetime_safe.new_datetime(value).strftime(datetime_format) class Widget: """ A Widget takes care of converting between import and export representations. This is achieved by the two methods, :meth:`~import_export.widgets.Widget.clean` and :meth:`~import_export.widgets.Widget.render`. """ def clean(self, value, row=None, *args, **kwargs): """ Returns an appropriate Python object for an imported value. For example, if you import a value from a spreadsheet, :meth:`~import_export.widgets.Widget.clean` handles conversion of this value into the corresponding Python object. Numbers or dates can be *cleaned* to their respective data types and don't have to be imported as Strings. """ return value def render(self, value, obj=None): """ Returns an export representation of a Python value. For example, if you have an object you want to export, :meth:`~import_export.widgets.Widget.render` takes care of converting the object's field to a value that can be written to a spreadsheet. """ return force_str(value) class NumberWidget(Widget): """ """ def is_empty(self, value): if isinstance(value, str): value = value.strip() # 0 is not empty return value is None or value == "" def render(self, value, obj=None): return value class FloatWidget(NumberWidget): """ Widget for converting floats fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return float(value) class IntegerWidget(NumberWidget): """ Widget for converting integer fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return int(Decimal(value)) class DecimalWidget(NumberWidget): """ Widget for converting decimal fields. """ def clean(self, value, row=None, *args, **kwargs): if self.is_empty(value): return None return Decimal(force_str(value)) class CharWidget(Widget): """ Widget for converting text fields. """ def render(self, value, obj=None): return force_str(value) class BooleanWidget(Widget): """ Widget for converting boolean fields. The widget assumes that ``True``, ``False``, and ``None`` are all valid values, as to match Django's `BooleanField `_. That said, whether the database/Django will actually accept NULL values will depend on if you have set ``null=True`` on that Django field. While the BooleanWidget is set up to accept as input common variations of "True" and "False" (and "None"), you may need to munge less common values to ``True``/``False``/``None``. Probably the easiest way to do this is to override the :func:`~import_export.resources.Resource.before_import_row` function of your Resource class. A short example:: from import_export import fields, resources, widgets class BooleanExample(resources.ModelResource): warn = fields.Field(widget=widgets.BooleanWidget()) def before_import_row(self, row, row_number=None, **kwargs): if "warn" in row.keys(): # munge "warn" to "True" if row["warn"] in ["warn", "WARN"]: row["warn"] = True return super().before_import_row(row, row_number, **kwargs) """ TRUE_VALUES = ["1", 1, True, "true", "TRUE", "True"] FALSE_VALUES = ["0", 0, False, "false", "FALSE", "False"] NULL_VALUES = ["", None, "null", "NULL", "none", "NONE", "None"] def render(self, value, obj=None): """ On export, ``True`` is represented as ``1``, ``False`` as ``0``, and ``None``/NULL as a empty string. Note that these values are also used on the import confirmation view. """ if value in self.NULL_VALUES: return "" return self.TRUE_VALUES[0] if value else self.FALSE_VALUES[0] def clean(self, value, row=None, *args, **kwargs): if value in self.NULL_VALUES: return None return True if value in self.TRUE_VALUES else False class DateWidget(Widget): """ Widget for converting date fields. Takes optional ``format`` parameter. """ def __init__(self, format=None): if format is None: if not settings.DATE_INPUT_FORMATS: formats = ("%Y-%m-%d",) else: formats = settings.DATE_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None if isinstance(value, date): return value for format in self.formats: try: return datetime.strptime(value, format).date() except (ValueError, TypeError): continue raise ValueError("Enter a valid date.") def render(self, value, obj=None): if not value: return "" return format_datetime(value, self.formats[0]) class DateTimeWidget(Widget): """ Widget for converting date fields. Takes optional ``format`` parameter. If none is set, either ``settings.DATETIME_INPUT_FORMATS`` or ``"%Y-%m-%d %H:%M:%S"`` is used. """ def __init__(self, format=None): if format is None: if not settings.DATETIME_INPUT_FORMATS: formats = ("%Y-%m-%d %H:%M:%S",) else: formats = settings.DATETIME_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None if isinstance(value, datetime): return value for format in self.formats: try: dt = datetime.strptime(value, format) if settings.USE_TZ: # make datetime timezone aware so we don't compare # naive datetime to an aware one dt = timezone.make_aware(dt, timezone.get_default_timezone()) return dt except (ValueError, TypeError): continue raise ValueError("Enter a valid date/time.") def render(self, value, obj=None): if not value: return "" if settings.USE_TZ: value = timezone.localtime(value) return format_datetime(value, self.formats[0]) class TimeWidget(Widget): """ Widget for converting time fields. Takes optional ``format`` parameter. """ def __init__(self, format=None): if format is None: if not settings.TIME_INPUT_FORMATS: formats = ("%H:%M:%S",) else: formats = settings.TIME_INPUT_FORMATS else: formats = (format,) self.formats = formats def clean(self, value, row=None, *args, **kwargs): if not value: return None if isinstance(value, time): return value for format in self.formats: try: return datetime.strptime(value, format).time() except (ValueError, TypeError): continue raise ValueError("Enter a valid time.") def render(self, value, obj=None): if not value: return "" return value.strftime(self.formats[0]) class DurationWidget(Widget): """ Widget for converting time duration fields. """ def clean(self, value, row=None, *args, **kwargs): if not value: return None try: return parse_duration(value) except (ValueError, TypeError): raise ValueError("Enter a valid duration.") def render(self, value, obj=None): if value is None: return "" return str(value) class SimpleArrayWidget(Widget): """ Widget for an Array field. Can be used for Postgres' Array field. :param separator: Defaults to ``','`` """ def __init__(self, separator=None): if separator is None: separator = ',' self.separator = separator super().__init__() def clean(self, value, row=None, *args, **kwargs): return value.split(self.separator) if value else [] def render(self, value, obj=None): return self.separator.join(str(v) for v in value) class JSONWidget(Widget): """ Widget for a JSON object (especially required for jsonb fields in PostgreSQL database.) :param value: Defaults to JSON format. The widget covers two cases: Proper JSON string with double quotes, else it tries to use single quotes and then convert it to proper JSON. """ def clean(self, value, row=None, *args, **kwargs): val = super().clean(value) if val: try: return json.loads(val) except json.decoder.JSONDecodeError: return json.loads(val.replace("'", "\"")) def render(self, value, obj=None): if value: return json.dumps(value) class ForeignKeyWidget(Widget): """ Widget for a ``ForeignKey`` field which looks up a related model using "natural keys" in both export and import. The lookup field defaults to using the primary key (``pk``) as lookup criterion but can be customised to use any field on the related model. Unlike specifying a related field in your resource like so… :: class Meta: fields = ('author__name',) …using a :class:`~import_export.widgets.ForeignKeyWidget` has the advantage that it can not only be used for exporting, but also importing data with foreign key relationships. Here's an example on how to use :class:`~import_export.widgets.ForeignKeyWidget` to lookup related objects using ``Author.name`` instead of ``Author.pk``:: from import_export import fields, resources from import_export.widgets import ForeignKeyWidget class BookResource(resources.ModelResource): author = fields.Field( column_name='author', attribute='author', widget=ForeignKeyWidget(Author, 'name')) class Meta: fields = ('author',) :param model: The Model the ForeignKey refers to (required). :param field: A field on the related model used for looking up a particular object. """ def __init__(self, model, field='pk', *args, **kwargs): self.model = model self.field = field super().__init__(*args, **kwargs) def get_queryset(self, value, row, *args, **kwargs): """ Returns a queryset of all objects for this Model. Overwrite this method if you want to limit the pool of objects from which the related object is retrieved. :param value: The field's value in the datasource. :param row: The datasource's current row. As an example; if you'd like to have ForeignKeyWidget look up a Person by their pre- **and** lastname column, you could subclass the widget like so:: class FullNameForeignKeyWidget(ForeignKeyWidget): def get_queryset(self, value, row): return self.model.objects.filter( first_name__iexact=row["first_name"], last_name__iexact=row["last_name"] ) """ return self.model.objects.all() def clean(self, value, row=None, *args, **kwargs): val = super().clean(value) if val: return self.get_queryset(value, row, *args, **kwargs).get(**{self.field: val}) else: return None def render(self, value, obj=None): if value is None: return "" attrs = self.field.split('__') for attr in attrs: try: value = getattr(value, attr, None) except (ValueError, ObjectDoesNotExist): # needs to have a primary key value before a many-to-many # relationship can be used. return None if value is None: return None return value class ManyToManyWidget(Widget): """ Widget that converts between representations of a ManyToMany relationships as a list and an actual ManyToMany field. :param model: The model the ManyToMany field refers to (required). :param separator: Defaults to ``','``. :param field: A field on the related model. Default is ``pk``. """ def __init__(self, model, separator=None, field=None, *args, **kwargs): if separator is None: separator = ',' if field is None: field = 'pk' self.model = model self.separator = separator self.field = field super().__init__(*args, **kwargs) def clean(self, value, row=None, *args, **kwargs): if not value: return self.model.objects.none() if isinstance(value, (float, int)): ids = [int(value)] else: ids = value.split(self.separator) ids = filter(None, [i.strip() for i in ids]) return self.model.objects.filter(**{ '%s__in' % self.field: ids }) def render(self, value, obj=None): ids = [smart_str(getattr(obj, self.field)) for obj in value.all()] return self.separator.join(ids) django-import-export-2.7.1/requirements/000077500000000000000000000000001416107567000203445ustar00rootroot00000000000000django-import-export-2.7.1/requirements/base.txt000066400000000000000000000001031416107567000220110ustar00rootroot00000000000000Django>=2.2 tablib[html,ods,xls,xlsx,yaml]>=3.0.0 diff-match-patch django-import-export-2.7.1/requirements/deploy.txt000066400000000000000000000000231416107567000223740ustar00rootroot00000000000000wheel zest.releaserdjango-import-export-2.7.1/requirements/docs.txt000066400000000000000000000000301416107567000220260ustar00rootroot00000000000000sphinx sphinx-rtd-theme django-import-export-2.7.1/requirements/test.txt000066400000000000000000000001321416107567000220600ustar00rootroot00000000000000isort psycopg2-binary mysqlclient coveralls chardet pytz memory-profiler django-extensionsdjango-import-export-2.7.1/runtests.sh000077500000000000000000000001141416107567000200430ustar00rootroot00000000000000PYTHONPATH=".:tests:$PYTHONPATH" django-admin test core --settings=settings django-import-export-2.7.1/setup.cfg000066400000000000000000000002241416107567000174400ustar00rootroot00000000000000[metadata] license_file = LICENSE [zest.releaser] create-wheel = yes python-file-with-version = import_export/__init__.py [isort] profile = black django-import-export-2.7.1/setup.py000066400000000000000000000034731416107567000173420ustar00rootroot00000000000000import os from setuptools import find_packages, setup VERSION = __import__("import_export").__version__ CLASSIFIERS = [ 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Software Development', ] install_requires = [ 'diff-match-patch', 'Django>=2.2', 'tablib[html,ods,xls,xlsx,yaml]>=3.0.0', ] with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: readme = f.read() setup( name="django-import-export", description="Django application and library for importing and exporting" " data with included admin integration.", long_description=readme, version=VERSION, author="Informatika Mihelac", author_email="bmihelac@mihelac.org", license='BSD License', platforms=['OS Independent'], url="https://github.com/django-import-export/django-import-export", project_urls={ "Documentation": "https://django-import-export.readthedocs.io/en/stable/", "Changelog": "https://django-import-export.readthedocs.io/en/stable/changelog.html", }, packages=find_packages(exclude=["tests"]), include_package_data=True, install_requires=install_requires, python_requires=">=3.6", classifiers=CLASSIFIERS, zip_safe=False, ) django-import-export-2.7.1/tests/000077500000000000000000000000001416107567000167635ustar00rootroot00000000000000django-import-export-2.7.1/tests/books-sample.csv000066400000000000000000000003331416107567000220730ustar00rootroot00000000000000id,name,author,author_email,imported,published,published_time,price,added,categories 12,Triangles,5,geo@met.ry,,2020-01-12,,12,,1 13,Rectangles,5,geo@met.ry,,2020-04-20,,20,,2 3,Rhombus,5,geo@met.ry,,2019-10-25,,5.5,,1 django-import-export-2.7.1/tests/bulk/000077500000000000000000000000001416107567000177205ustar00rootroot00000000000000django-import-export-2.7.1/tests/bulk/README.md000066400000000000000000000137561416107567000212130ustar00rootroot00000000000000## Bulk import testing This scripts outlines the steps used to profile bulk loading. The `bulk_import.py` script is used to profile run duration and memory during bulk load testing. ### Pre-requisites - [Docker](https://docker.com) - [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/command_ref.html) ### Test environment The following tests were run on the following platform: - Thinkpad T470 i5 processor (Ubuntu 18.04) - python 3.8.1 - Postgres 10 (docker container) ### Install dependencies ```bash # create venv and install django-import-export dependencies cd mkvirtualenv -p python3 djangoimportexport pip install -r requirements/base.txt -r requirements/test.txt ``` ### Create Postgres DB ```bash export IMPORT_EXPORT_TEST_TYPE=postgres export IMPORT_EXPORT_POSTGRESQL_USER=pguser export IMPORT_EXPORT_POSTGRESQL_PASSWORD=pguserpass export DJANGO_SETTINGS_MODULE=settings cd /tests # start a local postgres instance docker-compose -f bulk/docker-compose.yml up -d ./manage.py migrate ./manage.py test # only required if you want to login to the Admin site ./manage.py createsuperuser --username admin --email=email@example.com ``` ### Update settings In order to use the `runscript` command, add `django_extensions` to `settings.py` (`INSTALLED_APPS`). ### Running script ```bash # run creates, updates, and deletes ./manage.py runscript bulk_import # pass 'create', 'update' or 'delete' to run the single test ./manage.py runscript bulk_import --script-args create ``` ### Results - Used 20k book entries - Memory is reported as the peak memory value whilst running the test script #### bulk_create ##### Default settings - default settings - uses `ModelInstanceLoader` by default | Condition | Time (secs) | Memory (MB) | | ---------------------------------- | ----------- | ------------- | | `use_bulk=False` | 42.67 | 16.22 | | `use_bulk=True, batch_size=None` | 33.72 | 50.02 | | `use_bulk=True, batch_size=1000` | 33.21 | 11.43 | ##### Performance tweaks | use_bulk | batch_size | skip_diff | instance_loader | time (secs) | peak mem (MB) | | -------- | ---------- | --------- | -------------------- | ----------- | ------------- | | True | 1000 | True | force_init_instance | 9.60 | 9.4 | | True | 1000 | False | CachedInstanceLoader | 13.72 | 9.9 | | True | 1000 | True | CachedInstanceLoader | 16.12 | 9.5 | | True | 1000 | False | force_init_instance | 19.93 | 10.5 | | False | n/a | False | force_init_instance | 26.59 | 14.1 | | True | 1000 | False | ModelInstanceLoader | 28.60 | 9.7 | | True | 1000 | False | ModelInstanceLoader | 33.19 | 10.6 | | False | n/a | False | ModelInstanceLoader | 45.32 | 16.3 | (`force_init_instance`) means overriding `get_or_init_instance()` - this can be done when you know for certain that you are importing new rows: ```python def get_or_init_instance(self, instance_loader, row): return self._meta.model(), True ``` #### bulk_update ```bash ./manage.py runscript bulk_import --script-args update ``` ##### Default settings - `skip_diff = False` - `instance_loader_class = ModelInstanceLoader` | Condition | Time (secs) | Memory (MB) | | ---------------------------------- | ----------- | ------------- | | `use_bulk=False` | 82.28 | 9.33 | | `use_bulk=True, batch_size=None` | 92.41 | 202.26 | | `use_bulk=True, batch_size=1000` | 52.63 | 11.25 | ##### Performance tweaks - `skip_diff = True` - `instance_loader_class = CachedInstanceLoader` | Condition | Time (secs) | Memory (MB) | | ---------------------------------- | ----------- | ------------- | | `use_bulk=False` | 28.85 | 20.71 | | `use_bulk=True, batch_size=None` | 65.11 | 201.01 | | `use_bulk=True, batch_size=1000` | 21.56 | 21.25 | - `skip_diff = False` | Condition | Time (secs) | Memory (MB) | | ---------------------------------------- | ----------- | ------------- | | `use_bulk=True, batch_size=1000` | 9.26 | 8.51 | | `skip_html_diff=True, batch_size=1000` | 8.69 | 7.50 | | `skip_unchanged=True, batch_size=1000` | 5.42 | 7.34 | #### bulk delete ```bash ./manage.py runscript bulk_import --script-args delete ``` ##### Default settings - `skip_diff = False` - `instance_loader_class = ModelInstanceLoader` | Condition | Time (secs) | Memory (MB) | | ---------------------------------- | ----------- | ------------- | | `use_bulk=False` | 95.56 | 31.36 | | `use_bulk=True, batch_size=None` | 50.20 | 64.66 | | `use_bulk=True, batch_size=1000` | 43.77 | 33.123 | ##### Performance tweaks - `skip_diff = True` - `instance_loader_class = CachedInstanceLoader` | Condition | Time (secs) | Memory (MB) | | ---------------------------------- | ----------- | ------------- | | `use_bulk=False` | 61.66 | 31.94 | | `use_bulk=True, batch_size=None` | 14.08 | 39.40 | | `use_bulk=True, batch_size=1000` | 15.37 | 32.70 | ### Checking DB Note that the db is cleared down after each test run. You need to uncomment the `delete()` calls to be able to view data. ```bash ./manage.py shell_plus Book.objects.all().count() ``` ### Clear down Optional clear down of resources: ```bash # remove the test db container docker-compose -f bulk/docker-compose.yml down -v # remove venv deactivate rmvirtualenv djangoimportexport ``` ### References - https://hakibenita.com/fast-load-data-python-postgresql django-import-export-2.7.1/tests/bulk/docker-compose.yml000066400000000000000000000011741416107567000233600ustar00rootroot00000000000000version: '3.3' services: db: container_name: importexport_pgdb environment: IMPORT_EXPORT_TEST_TYPE: 'postgres' DB_HOST: 'db' DB_PORT: '5432' DB_NAME: 'import_export' IMPORT_EXPORT_POSTGRESQL_USER: ${IMPORT_EXPORT_POSTGRESQL_USER} IMPORT_EXPORT_POSTGRESQL_PASSWORD: ${IMPORT_EXPORT_POSTGRESQL_PASSWORD} POSTGRES_PASSWORD: ${IMPORT_EXPORT_POSTGRESQL_PASSWORD} image: postgres:10 restart: "no" ports: - 5432:5432 volumes: - ./docker/db/:/docker-entrypoint-initdb.d/ - local-db-data:/var/lib/postgresql/data volumes: local-db-data: driver: local django-import-export-2.7.1/tests/bulk/docker/000077500000000000000000000000001416107567000211675ustar00rootroot00000000000000django-import-export-2.7.1/tests/bulk/docker/db/000077500000000000000000000000001416107567000215545ustar00rootroot00000000000000django-import-export-2.7.1/tests/bulk/docker/db/init-user-db.sh000066400000000000000000000006501416107567000244130ustar00rootroot00000000000000#!/usr/bin/env bash set -e echo "init-user-db.sh" psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE USER $IMPORT_EXPORT_POSTGRESQL_USER WITH PASSWORD '$IMPORT_EXPORT_POSTGRESQL_PASSWORD'; CREATE DATABASE $DB_NAME ENCODING 'utf-8'; GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $IMPORT_EXPORT_POSTGRESQL_USER; ALTER USER $IMPORT_EXPORT_POSTGRESQL_USER CREATEDB; EOSQLdjango-import-export-2.7.1/tests/core/000077500000000000000000000000001416107567000177135ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/__init__.py000066400000000000000000000000001416107567000220120ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/admin.py000066400000000000000000000030601416107567000213540ustar00rootroot00000000000000from django.contrib import admin from import_export.admin import ExportActionModelAdmin, ImportExportMixin, ImportMixin from import_export.resources import ModelResource from .forms import CustomConfirmImportForm, CustomImportForm from .models import Author, Book, Category, Child, EBook class ChildAdmin(ImportMixin, admin.ModelAdmin): pass class BookResource(ModelResource): class Meta: model = Book def for_delete(self, row, instance): return self.fields['name'].clean(row) == '' class BookAdmin(ImportExportMixin, admin.ModelAdmin): list_display = ('name', 'author', 'added') list_filter = ['categories', 'author'] resource_class = BookResource class CategoryAdmin(ExportActionModelAdmin): pass class AuthorAdmin(ImportMixin, admin.ModelAdmin): pass class CustomBookAdmin(BookAdmin): """BookAdmin with custom import forms""" def get_import_form(self): return CustomImportForm def get_confirm_import_form(self): return CustomConfirmImportForm def get_form_kwargs(self, form, *args, **kwargs): # update kwargs with authors (from CustomImportForm.cleaned_data) if isinstance(form, CustomImportForm): if form.is_valid(): author = form.cleaned_data['author'] kwargs.update({'author': author.id}) return kwargs admin.site.register(Book, BookAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(Author, AuthorAdmin) admin.site.register(Child, ChildAdmin) admin.site.register(EBook, CustomBookAdmin) django-import-export-2.7.1/tests/core/exports/000077500000000000000000000000001416107567000214175ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/exports/books-dos.csv000066400000000000000000000000641416107567000240340ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-2.7.1/tests/core/exports/books-for-delete.csv000066400000000000000000000000511416107567000252710ustar00rootroot00000000000000id,name,author_email 1,,test@example.com django-import-export-2.7.1/tests/core/exports/books-mac.csv000066400000000000000000000000621416107567000240050ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-2.7.1/tests/core/exports/books-mac.tsv000066400000000000000000000000621416107567000240260ustar00rootroot00000000000000id name author_email 1 Some book test@example.com django-import-export-2.7.1/tests/core/exports/books-unicode.csv000066400000000000000000000000641416107567000246750ustar00rootroot00000000000000id,name,author_email 1,Some bookš,test@example.com django-import-export-2.7.1/tests/core/exports/books-unicode.tsv000066400000000000000000000000641416107567000247160ustar00rootroot00000000000000id name author_email 1 Some bookš test@example.com django-import-export-2.7.1/tests/core/exports/books.csv000066400000000000000000000000621416107567000232470ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-2.7.1/tests/core/exports/books.xls000066400000000000000000000130001416107567000232560ustar00rootroot00000000000000ࡱ;  Root Entry  !#  \pCalc Ba==@ 8@"1Arial1Arial1Arial1Arial1Cambria General                + ) , *    ` books,,TZR3  @@  Bidname author_email Some booktest@example.com y cc   d-C6?_%;*+&?'?('}'}?)'}'}?" d,, ` `? ` `?U } r;;   ~   PH0(  >@gg  FMicrosoft Excel 97-TabelleBiff8Oh+'0|8 @ L X d p0@@@@՜.+,D՜.+,\Root EntryF WorkbookCompObjIOle SummaryInformation(DocumentSummaryInformation8"tdjango-import-export-2.7.1/tests/core/exports/books.xlsx000066400000000000000000000114261416107567000234600ustar00rootroot00000000000000PKyS _rels/.relsJ1}{wDdЛH}a70u}{ZI~7CfGFoZ+{kW#VJ$cʪl n0\QX^:`dd{m]_dhVFw^F9W-(F/3ODSUNl/w{N([qTuŌVtys>(NGq0fp1yv*fXtE N5z ۖ) *dyVd+"أP9_ OQd X58 [\8G{UtKMRd:8)1hD~Fd6: p;|'t4dӟ$+'[4Cjϝ\;f>%woGPK+uPKyS xl/styles.xmlX]o0}߯B4m'BubTTiڃNw ~lmTJ^l9pmCtQm3<:12U _ϓ'KdFtkjY!2"PfKkOA`Ғ bTE%\ɕP4%q$qNAq$"֠T-|-p:] |jqGA+G#82p ]xHN9XA}عf^8'ڃZmঽZgx.kw>/X4nB݄"R-?+U 4q.4Gi B uIyeJ~]pN{܂fEZXt+wHRί7w*e3缷]HU:QN%mM:笐> 6[㈬Q4{i7EA~1te(K xפɬI LuLUgqlmdP{*TpSѶuj}>.TWj ޏ f[{3{3{3{3{3ۘ?dSn&;fKnN|9~c*3l{;*ZЮ[e">Gޫbɸeҏ %YǏ g#M鳤Tu9p& r x.Na:l 7PKj3PKySxl/worksheets/sheet1.xmlVMs8p4{6Ą2L%LUfZ2;U{U^II~ rZNrwkB{5#ADR`BfRŨgq[rlrm . M\']PzRsB( *3,)_BZ\:ZorQ6O|LA\֞.0WTR(h]NIE`UyKln+/#'>7dBZùdT'Γ[87]WBѸ oNͫEz#^QI8`eBdkUl}&R+gwIҮGࠞr5-.H j*PȅΞp(` (ܳl*Uѵܲ3r:iGvP #Q|LdUY_5cZ#<5.댵[gwgPKh<62 PKySxl/_rels/workbook.xml.relsMk0 FIc8AQ6ֵ~.[ eГH4C vV͢h%؀I/Z8Y.b;]JNɺQ/IӜS[n+Psc _{0 r3Rb+/2m=?"#ʯR6w _fz o ۭP~$E|PKOz%PKySxl/sharedStrings.xmlJ1D~E軓"K&+ziw&1ݳoě̱WUMåds&iayNth9d&ʉV պV%_޹]0̲ jvwD`">V|Ib/53Y~#` nê }ʛ - 7&ZQ/ԌCg}oPKHPKySdocProps/core.xmlmRo0~_A i,˖uݠ4)߯2|G]M:$QLТJWy[-Œn5ʻ\&Z /5`Q |vLl ԉ 4EޡnmC[Qq< 9C3&SczB ht4Ey`P.ƒֳ8NƮn2X|~F W%LX2ʠ_Q"<~?%i>XF|.gN`͉ 8Yz<<L4 ,īdg}뀡JMGmAqpSPKSPKySdocProps/app.xmlOk0 8 !-q;C6v ۠y7==n8ALڻW5)I;mx.7H(;;}R .&y+RmG+0x~'/g iS)Peq{B?2l03z- _Es"|?#]VYcӍ][ /Hz8kʆ[7PK<)zPKyS[Content_Types].xmlT;O0+"(vˀJځ(32%1{)TU )ɲラOv6ۨ&YL$ #rK/l:[ >^sR`E {j,h)p*fyF! L( w'/J[Ȃ,Xe88r~8l{|-?^H@,w#^-tCb̹ \a{I8OҦao-_YkP3e) X)PopkڮTqOmޒ y ]&icp!wϝ7;SD^[ j2~wPK6YWPKySf; _rels/.relsPKyS+uxl/workbook.xmlPKySj3 Txl/styles.xmlPKySh<62 Exl/worksheets/sheet1.xmlPKySOz%B xl/_rels/workbook.xml.relsPKySH\ xl/sharedStrings.xmlPKySSm docProps/core.xmlPKyS<)z docProps/app.xmlPKyS6YW'[Content_Types].xmlPK ?django-import-export-2.7.1/tests/core/exports/child.csv000066400000000000000000000000331416107567000232130ustar00rootroot00000000000000id,parent,name 1,1234,Some django-import-export-2.7.1/tests/core/fixtures/000077500000000000000000000000001416107567000215645ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/fixtures/book.json000066400000000000000000000026071416107567000234160ustar00rootroot00000000000000[ { "model": "core.author", "pk": 11, "fields": { "name": "George R. R. Martin", "birthday": "1948-09-20" } }, { "model": "core.author", "pk": 5, "fields": { "name": "Geo Metry", "birthday": "1950-12-20" } }, { "model": "core.book", "pk": 11, "fields": { "name": "A Game of Thrones", "author": 11, "author_email": "martin@got.com", "imported": false, "published": "1996-08-01", "published_time": "21:00", "price": 25.0, "categories": [1] } }, { "model": "core.book", "pk": 5, "fields": { "name": "Squares", "author": 5, "author_email": "geo@met.ry", "imported": false, "published": "1999-08-01", "published_time": "21:00", "price": 5.0, "categories": [2] } }, { "model": "core.book", "pk": 6, "fields": { "name": "Circles", "author": 11, "author_email": "geo@met.ry", "imported": false, "published": "2020-08-01", "published_time": "21:00", "price": 15.0, "categories": [2] } } ]django-import-export-2.7.1/tests/core/fixtures/category.json000066400000000000000000000004011416107567000242670ustar00rootroot00000000000000[ { "model": "core.category", "pk": 1, "fields": { "name": "Category 1" } }, { "model": "core.category", "pk": 2, "fields": { "name": "Category 2" } } ] django-import-export-2.7.1/tests/core/forms.py000066400000000000000000000010341416107567000214110ustar00rootroot00000000000000from django import forms from import_export.forms import ConfirmImportForm, ImportForm from .models import Author class AuthorFormMixin(forms.Form): author = forms.ModelChoiceField(queryset=Author.objects.all(), required=True) class CustomImportForm(AuthorFormMixin, ImportForm): """Customized ImportForm, with author field required""" pass class CustomConfirmImportForm(AuthorFormMixin, ConfirmImportForm): """Customized ConfirmImportForm, with author field required""" pass django-import-export-2.7.1/tests/core/migrations/000077500000000000000000000000001416107567000220675ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/migrations/0001_initial.py000066400000000000000000000064621416107567000245420ustar00rootroot00000000000000import core.models import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Author', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ('birthday', models.DateTimeField(auto_now_add=True)), ], ), migrations.CreateModel( name='Book', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='Book name')), ('author_email', models.EmailField(blank=True, max_length=75, verbose_name='Author email')), ('imported', models.BooleanField(default=False)), ('published', models.DateField(blank=True, null=True, verbose_name='Published')), ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.Author')), ], ), migrations.CreateModel( name='Category', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( name='Entry', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( name='Profile', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('is_private', models.BooleanField(default=True)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.CreateModel( name='WithDefault', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(blank=True, default='foo_bar', max_length=75, verbose_name='Default')), ], ), migrations.CreateModel( name='WithDynamicDefault', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(default=core.models.random_name, max_length=100, verbose_name='Dyn Default')), ], ), migrations.AddField( model_name='book', name='categories', field=models.ManyToManyField(blank=True, to='core.Category'), ), ] django-import-export-2.7.1/tests/core/migrations/0002_book_published_time.py000066400000000000000000000005561416107567000271170ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0001_initial'), ] operations = [ migrations.AddField( model_name='book', name='published_time', field=models.TimeField(blank=True, null=True, verbose_name='Time published'), ), ] django-import-export-2.7.1/tests/core/migrations/0003_withfloatfield.py000066400000000000000000000007341416107567000261140ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0002_book_published_time'), ] operations = [ migrations.CreateModel( name='WithFloatField', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('f', models.FloatField(blank=True, null=True)), ], ), ] django-import-export-2.7.1/tests/core/migrations/0004_bookwithchapters.py000066400000000000000000000025421416107567000264670ustar00rootroot00000000000000from django.db import migrations, models can_use_postgres_fields = False # Dummy fields chapters_field = models.Field() data_field = models.Field() try: from django.contrib.postgres.fields import ArrayField, JSONField chapters_field = ArrayField(base_field=models.CharField(max_length=100), default=list, size=None) data_field = JSONField(null=True) can_use_postgres_fields = True except ImportError: # We can't use ArrayField if psycopg2 is not installed - issue #1125 pass class Migration(migrations.Migration): dependencies = [ ('core', '0003_withfloatfield'), ] operations = [] pg_only_operations = [ migrations.CreateModel( name='BookWithChapters', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='Book name')), ('chapters', chapters_field), ('data', data_field) ], ), ] def apply(self, project_state, schema_editor, collect_sql=False): if can_use_postgres_fields and schema_editor.connection.vendor.startswith("postgres"): self.operations = self.operations + self.pg_only_operations return super().apply(project_state, schema_editor, collect_sql) django-import-export-2.7.1/tests/core/migrations/0005_addparentchild.py000066400000000000000000000017141416107567000260560ustar00rootroot00000000000000import django.db.models.deletion from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0004_bookwithchapters'), ] operations = [ migrations.CreateModel( name='Child', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( name='Parent', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ], ), migrations.AddField( model_name='child', name='parent', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Parent'), ), ] django-import-export-2.7.1/tests/core/migrations/0006_auto_20171130_0147.py000066400000000000000000000005301416107567000255050ustar00rootroot00000000000000from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0005_addparentchild'), ] operations = [ migrations.AlterField( model_name='category', name='name', field=models.CharField(max_length=100, unique=True), ), ] django-import-export-2.7.1/tests/core/migrations/0007_auto_20180628_0411.py000066400000000000000000000020731416107567000255200ustar00rootroot00000000000000import django.db.models.deletion from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('core', '0006_auto_20171130_0147'), ] operations = [ migrations.CreateModel( name='Person', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], ), migrations.CreateModel( name='Role', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), migrations.AddField( model_name='person', name='role', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Role'), ), ] django-import-export-2.7.1/tests/core/migrations/0008_auto_20190409_0846.py000066400000000000000000000011641416107567000255330ustar00rootroot00000000000000# Generated by Django 2.1.5 on 2019-04-09 06:46 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0007_auto_20180628_0411'), ] operations = [ migrations.CreateModel( name='EBook', fields=[ ], options={ 'proxy': True, 'indexes': [], }, bases=('core.book',), ), migrations.AddField( model_name='book', name='added', field=models.DateTimeField(blank=True, null=True), ), ] django-import-export-2.7.1/tests/core/migrations/0009_auto_20211111_0807.py000066400000000000000000000053241416107567000255130ustar00rootroot00000000000000# Generated by Django 3.2.9 on 2021-11-11 08:07 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('core', '0008_auto_20190409_0846'), ] operations = [ migrations.AlterField( model_name='author', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='book', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='category', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='child', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='entry', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='parent', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='person', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='profile', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='role', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='withdefault', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='withdynamicdefault', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), migrations.AlterField( model_name='withfloatfield', name='id', field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), ), ] django-import-export-2.7.1/tests/core/migrations/__init__.py000066400000000000000000000000001416107567000241660ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/models.py000066400000000000000000000055731416107567000215620ustar00rootroot00000000000000import random import string from django.core.exceptions import ValidationError from django.db import models class Author(models.Model): name = models.CharField(max_length=100) birthday = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name def full_clean(self, exclude=None, validate_unique=True): super().full_clean(exclude, validate_unique) if exclude is None: exclude = [] else: exclude = list(exclude) if 'name' not in exclude and self.name == '123': raise ValidationError({'name': "'123' is not a valid value"}) class Category(models.Model): name = models.CharField( max_length=100, unique=True, ) def __str__(self): return self.name class Book(models.Model): name = models.CharField('Book name', max_length=100) author = models.ForeignKey(Author, blank=True, null=True, on_delete=models.CASCADE) author_email = models.EmailField('Author email', max_length=75, blank=True) imported = models.BooleanField(default=False) published = models.DateField('Published', blank=True, null=True) published_time = models.TimeField('Time published', blank=True, null=True) price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) added = models.DateTimeField(blank=True, null=True) categories = models.ManyToManyField(Category, blank=True) def __str__(self): return self.name class Parent(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Child(models.Model): parent = models.ForeignKey(Parent, on_delete=models.CASCADE) name = models.CharField(max_length=100) def __str__(self): return '%s - child of %s' % (self.name, self.parent.name) class Profile(models.Model): user = models.OneToOneField('auth.User', on_delete=models.CASCADE) is_private = models.BooleanField(default=True) class Entry(models.Model): user = models.ForeignKey('auth.User', on_delete=models.CASCADE) class Role(models.Model): user = models.OneToOneField('auth.User', on_delete=models.CASCADE, null=True) class Person(models.Model): role = models.ForeignKey(Role, on_delete=models.CASCADE) class WithDefault(models.Model): name = models.CharField('Default', max_length=75, blank=True, default='foo_bar') def random_name(): chars = string.ascii_lowercase return ''.join(random.SystemRandom().choice(chars) for _ in range(100)) class WithDynamicDefault(models.Model): name = models.CharField('Dyn Default', max_length=100, default=random_name) class WithFloatField(models.Model): f = models.FloatField(blank=True, null=True) class EBook(Book): """Book proxy model to have a separate admin url access and name""" class Meta: proxy = True django-import-export-2.7.1/tests/core/templates/000077500000000000000000000000001416107567000217115ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/templates/core/000077500000000000000000000000001416107567000226415ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/templates/core/category_list.html000066400000000000000000000001011416107567000263670ustar00rootroot00000000000000{{ form }} {% for obj in object_list %} {{ obj }} {% endfor %} django-import-export-2.7.1/tests/core/tests/000077500000000000000000000000001416107567000210555ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/tests/__init__.py000066400000000000000000000000001416107567000231540ustar00rootroot00000000000000django-import-export-2.7.1/tests/core/tests/test_admin_integration.py000066400000000000000000000612031416107567000261630ustar00rootroot00000000000000import os.path from datetime import datetime from unittest import mock from unittest.mock import MagicMock import chardet import tablib from core.admin import ( AuthorAdmin, BookAdmin, BookResource, CustomBookAdmin, ImportMixin, ) from core.models import Author, Book, Category, EBook, Parent from django.contrib.admin.models import DELETION, LogEntry from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.core.files.uploadedfile import SimpleUploadedFile from django.http import HttpRequest from django.test.testcases import TestCase from django.test.utils import override_settings from django.utils.translation import gettext_lazy as _ from tablib import Dataset from import_export import formats from import_export.admin import ( ExportActionMixin, ExportActionModelAdmin, ExportMixin, ImportExportActionModelAdmin, ) from import_export.formats.base_formats import DEFAULT_FORMATS from import_export.tmp_storages import TempFolderStorage class ImportExportAdminIntegrationTest(TestCase): def setUp(self): user = User.objects.create_user('admin', 'admin@example.com', 'password') user.is_staff = True user.is_superuser = True user.save() self.client.login(username='admin', password='password') def test_import_export_template(self): response = self.client.get('/admin/core/book/') self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'admin/import_export/change_list_import_export.html') self.assertContains(response, _('Import')) self.assertContains(response, _('Export')) @override_settings(TEMPLATE_STRING_IF_INVALID='INVALID_VARIABLE') def test_import(self): # GET the import form response = self.client.get('/admin/core/book/import/') self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'admin/import_export/import.html') self.assertContains(response, 'form action=""') # POST the import form input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) self.assertIn('result', response.context) self.assertFalse(response.context['result'].has_errors()) self.assertIn('confirm_form', response.context) confirm_form = response.context['confirm_form'] data = confirm_form.initial self.assertEqual(data['original_file_name'], 'books.csv') response = self.client.post('/admin/core/book/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, _('Import finished, with {} new and {} updated {}.').format( 1, 0, Book._meta.verbose_name_plural) ) def test_delete_from_admin(self): # test delete from admin site (see #432) # create a book which can be deleted b = Book.objects.create(id=1) input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-for-delete.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) confirm_form = response.context['confirm_form'] data = confirm_form.initial response = self.client.post('/admin/core/book/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) # check the LogEntry was created as expected deleted_entry = LogEntry.objects.latest('id') self.assertEqual("delete through import_export", deleted_entry.change_message) self.assertEqual(DELETION, deleted_entry.action_flag) self.assertEqual(b.id, int(deleted_entry.object_id)) self.assertEqual("", deleted_entry.object_repr) @override_settings(TEMPLATE_STRING_IF_INVALID='INVALID_VARIABLE') def test_import_mac(self): # GET the import form response = self.client.get('/admin/core/book/import/') self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'admin/import_export/import.html') self.assertContains(response, 'form action=""') # POST the import form input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-mac.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) self.assertIn('result', response.context) self.assertFalse(response.context['result'].has_errors()) self.assertIn('confirm_form', response.context) confirm_form = response.context['confirm_form'] data = confirm_form.initial self.assertEqual(data['original_file_name'], 'books-mac.csv') response = self.client.post('/admin/core/book/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains(response, _('Import finished, with {} new and {} updated {}.').format( 1, 0, Book._meta.verbose_name_plural) ) def test_export(self): response = self.client.get('/admin/core/book/export/') self.assertEqual(response.status_code, 200) data = { 'file_format': '0', } date_str = datetime.now().strftime('%Y-%m-%d') response = self.client.post('/admin/core/book/export/', data) self.assertEqual(response.status_code, 200) self.assertTrue(response.has_header("Content-Disposition")) self.assertEqual(response['Content-Type'], 'text/csv') self.assertEqual( response['Content-Disposition'], 'attachment; filename="Book-{}.csv"'.format(date_str) ) def test_returns_xlsx_export(self): response = self.client.get('/admin/core/book/export/') self.assertEqual(response.status_code, 200) for i, f in enumerate(DEFAULT_FORMATS): if f().get_title() == 'xlsx': xlsx_index = i break else: self.fail('Unable to find xlsx format. DEFAULT_FORMATS: %r' % DEFAULT_FORMATS) data = {'file_format': str(xlsx_index)} response = self.client.post('/admin/core/book/export/', data) self.assertEqual(response.status_code, 200) self.assertTrue(response.has_header("Content-Disposition")) self.assertEqual(response['Content-Type'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') def test_import_export_buttons_visible_without_add_permission(self): # issue 38 - Export button not visible when no add permission original = BookAdmin.has_add_permission BookAdmin.has_add_permission = lambda self, request: False response = self.client.get('/admin/core/book/') BookAdmin.has_add_permission = original self.assertContains(response, _('Export')) self.assertContains(response, _('Import')) def test_import_buttons_visible_without_add_permission(self): # When using ImportMixin, users should be able to see the import button # without add permission (to be consistent with ImportExportMixin) original = AuthorAdmin.has_add_permission AuthorAdmin.has_add_permission = lambda self, request: False response = self.client.get('/admin/core/author/') AuthorAdmin.has_add_permission = original self.assertContains(response, _('Import')) self.assertTemplateUsed(response, 'admin/import_export/change_list.html') def test_import_file_name_in_tempdir(self): # 65 - import_file_name form field can be use to access the filesystem import_file_name = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') data = { 'input_format': "0", 'import_file_name': import_file_name, 'original_file_name': 'books.csv' } with self.assertRaises(FileNotFoundError): self.client.post('/admin/core/book/process_import/', data) def test_csrf(self): response = self.client.get('/admin/core/book/process_import/') self.assertEqual(response.status_code, 405) def test_import_log_entry(self): input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) confirm_form = response.context['confirm_form'] data = confirm_form.initial response = self.client.post('/admin/core/book/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) book = LogEntry.objects.latest('id') self.assertEqual(book.object_repr, "Some book") self.assertEqual(book.object_id, str(1)) def test_import_log_entry_with_fk(self): Parent.objects.create(id=1234, name='Some Parent') input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'child.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/child/import/', data) self.assertEqual(response.status_code, 200) confirm_form = response.context['confirm_form'] data = confirm_form.initial response = self.client.post('/admin/core/child/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) child = LogEntry.objects.latest('id') self.assertEqual(child.object_repr, 'Some - child of Some Parent') self.assertEqual(child.object_id, str(1)) def test_logentry_creation_with_import_obj_exception(self): # from https://mail.python.org/pipermail/python-dev/2008-January/076194.html def monkeypatch_method(cls): def decorator(func): setattr(cls, func.__name__, func) return func return decorator # Cause an exception in import_row, but only after import is confirmed, # so a failure only occurs when ImportMixin.process_import is called. class R(BookResource): def import_obj(self, obj, data, dry_run, **kwargs): if dry_run: super().import_obj(obj, data, dry_run, **kwargs) else: raise Exception @monkeypatch_method(BookAdmin) def get_resource_class(self): return R # Verify that when an exception occurs in import_row, when raise_errors is False, # the returned row result has a correct import_type value, # so generating log entries does not fail. @monkeypatch_method(BookAdmin) def process_dataset(self, dataset, confirm_form, request, *args, **kwargs): resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs)) return resource.import_data(dataset, dry_run=False, raise_errors=False, file_name=confirm_form.cleaned_data['original_file_name'], user=request.user, **kwargs) dataset = Dataset(headers=["id","name","author_email"]) dataset.append([1, "Test 1", "test@example.com"]) input_format = '0' content = dataset.csv f = SimpleUploadedFile("data.csv", content.encode(), content_type="text/csv") data = { "input_format": input_format, "import_file": f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) confirm_form = response.context['confirm_form'] data = confirm_form.initial response = self.client.post('/admin/core/book/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) def test_import_with_customized_forms(self): """Test if admin import works if forms are customized""" # We reuse import scheme from `test_import` to import books.csv. # We use customized BookAdmin (CustomBookAdmin) with modified import # form, which requires Author to be selected (from available authors). # Note that url is /admin/core/ebook/import (and not: ...book/import)! # We need at least a single author in the db to select from in the # admin import custom forms Author.objects.create(id=11, name='Test Author') # GET the import form response = self.client.get('/admin/core/ebook/import/') self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'admin/import_export/import.html') self.assertContains(response, 'form action=""') # POST the import form input_format = '0' filename = os.path.join(os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') with open(filename, "rb") as fobj: data = {'author': 11, 'input_format': input_format, 'import_file': fobj} response = self.client.post('/admin/core/ebook/import/', data) self.assertEqual(response.status_code, 200) self.assertIn('result', response.context) self.assertFalse(response.context['result'].has_errors()) self.assertIn('confirm_form', response.context) confirm_form = response.context['confirm_form'] self.assertIsInstance(confirm_form, CustomBookAdmin(EBook, 'ebook/import') .get_confirm_import_form()) data = confirm_form.initial self.assertEqual(data['original_file_name'], 'books.csv') response = self.client.post('/admin/core/ebook/process_import/', data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains( response, _('Import finished, with {} new and {} updated {}.').format( 1, 0, EBook._meta.verbose_name_plural) ) def test_get_skip_admin_log_attribute(self): m = ImportMixin() m.skip_admin_log = True self.assertTrue(m.get_skip_admin_log()) def test_get_tmp_storage_class_attribute(self): """Mock dynamically loading a class defined by an attribute""" target = "SomeClass" m = ImportMixin() m.tmp_storage_class = "tmpClass" with mock.patch("import_export.admin.import_string") as mock_import_string: mock_import_string.return_value = target self.assertEqual(target, m.get_tmp_storage_class()) def test_get_import_data_kwargs_with_form_kwarg(self): """ Test that if a the method is called with a 'form' kwarg, then it is removed and the updated dict is returned """ request = MagicMock(spec=HttpRequest) m = ImportMixin() kw = { "a": 1, "form": "some_form" } target = { "a": 1 } self.assertEqual(target, m.get_import_data_kwargs(request, **kw)) def test_get_import_data_kwargs_with_no_form_kwarg_returns_empty_dict(self): """ Test that if a the method is called with no 'form' kwarg, then an empty dict is returned """ request = MagicMock(spec=HttpRequest) m = ImportMixin() kw = { "a": 1, } target = {} self.assertEqual(target, m.get_import_data_kwargs(request, **kw)) def test_get_context_data_returns_empty_dict(self): m = ExportMixin() self.assertEqual(dict(), m.get_context_data()) def test_media_attribute(self): """ Test that the 'media' attribute of the ModelAdmin class is overridden to include the project-specific js file. """ mock_model = mock.MagicMock() mock_site = mock.MagicMock() class TestExportActionModelAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) m = TestExportActionModelAdmin() target_media = m.media self.assertEqual('import_export/action_formats.js', target_media._js[-1]) class TestImportExportActionModelAdmin(ImportExportActionModelAdmin): def __init__(self, mock_model, mock_site, error_instance): self.error_instance = error_instance super().__init__(mock_model, mock_site) def write_to_tmp_storage(self, import_file, input_format): mock_storage = MagicMock(spec=TempFolderStorage) mock_storage.read.side_effect = self.error_instance return mock_storage class ImportActionDecodeErrorTest(TestCase): mock_model = mock.Mock(spec=Book) mock_model.__name__ = "mockModel" mock_site = mock.MagicMock() mock_request = MagicMock(spec=HttpRequest) mock_request.POST = {'a': 1} mock_request.FILES = {} @mock.patch("import_export.admin.ImportForm") def test_import_action_handles_UnicodeDecodeError(self, mock_form): mock_form.is_valid.return_value = True b_arr = b'\x00\x00' m = TestImportExportActionModelAdmin(self.mock_model, self.mock_site, UnicodeDecodeError('codec', b_arr, 1, 2, 'fail!')) res = m.import_action(self.mock_request) self.assertEqual( "

    Imported file has a wrong encoding: \'codec\' codec can\'t decode byte 0x00 in position 1: fail!

    ", res.content.decode()) @mock.patch("import_export.admin.ImportForm") def test_import_action_handles_error(self, mock_form): mock_form.is_valid.return_value = True m = TestImportExportActionModelAdmin(self.mock_model, self.mock_site, ValueError("fail")) res = m.import_action(self.mock_request) self.assertRegex( res.content.decode(), r"

    ValueError encountered while trying to read file: .*

    ") class ExportActionAdminIntegrationTest(TestCase): def setUp(self): user = User.objects.create_user('admin', 'admin@example.com', 'password') user.is_staff = True user.is_superuser = True user.save() self.cat1 = Category.objects.create(name='Cat 1') self.cat2 = Category.objects.create(name='Cat 2') self.client.login(username='admin', password='password') def test_export(self): data = { 'action': ['export_admin_action'], 'file_format': '0', '_selected_action': [str(self.cat1.id)], } response = self.client.post('/admin/core/category/', data) self.assertContains(response, self.cat1.name, status_code=200) self.assertNotContains(response, self.cat2.name, status_code=200) self.assertTrue(response.has_header("Content-Disposition")) date_str = datetime.now().strftime('%Y-%m-%d') self.assertEqual( response['Content-Disposition'], 'attachment; filename="Category-{}.csv"'.format(date_str) ) def test_export_no_format_selected(self): data = { 'action': ['export_admin_action'], '_selected_action': [str(self.cat1.id)], } response = self.client.post('/admin/core/category/', data) self.assertEqual(response.status_code, 302) def test_get_export_data_raises_PermissionDenied_when_no_export_permission_assigned(self): request = MagicMock(spec=HttpRequest) class TestMixin(ExportMixin): model = Book def has_export_permission(self, request): return False m = TestMixin() with self.assertRaises(PermissionDenied): m.get_export_data('0', Book.objects.none(), request=request) class TestExportEncoding(TestCase): mock_request = MagicMock(spec=HttpRequest) mock_request.POST = {'file_format': 0} class TestMixin(ExportMixin): def __init__(self, test_str=None): self.test_str = test_str def get_data_for_export(self, request, queryset, *args, **kwargs): dataset = Dataset(headers=["id", "name"]) dataset.append([1, self.test_str]) return dataset def get_export_queryset(self, request): return list() def get_export_filename(self, request, queryset, file_format): return "f" def setUp(self): self.file_format = formats.base_formats.CSV() self.export_mixin = self.TestMixin(test_str="teststr") def test_to_encoding_not_set_default_encoding_is_utf8(self): self.export_mixin = self.TestMixin(test_str="teststr") data = self.export_mixin.get_export_data(self.file_format, list(), request=self.mock_request) csv_dataset = tablib.import_set(data) self.assertEqual("teststr", csv_dataset.dict[0]["name"]) def test_to_encoding_set(self): self.export_mixin = self.TestMixin(test_str="ハローワールド") data = self.export_mixin.get_export_data(self.file_format, list(), request=self.mock_request, encoding="shift-jis") encoding = chardet.detect(bytes(data))["encoding"] self.assertEqual("SHIFT_JIS", encoding) def test_to_encoding_set_incorrect(self): self.export_mixin = self.TestMixin() with self.assertRaises(LookupError): self.export_mixin.get_export_data(self.file_format, list(), request=self.mock_request, encoding="bad-encoding") def test_to_encoding_not_set_for_binary_file(self): self.export_mixin = self.TestMixin(test_str="teststr") self.file_format = formats.base_formats.XLSX() data = self.export_mixin.get_export_data(self.file_format, list(), request=self.mock_request) binary_dataset = tablib.import_set(data) self.assertEqual("teststr", binary_dataset.dict[0]["name"]) @mock.patch("import_export.admin.ImportForm") def test_export_action_to_encoding(self, mock_form): mock_form.is_valid.return_value = True self.export_mixin.to_encoding = "utf-8" with mock.patch("import_export.admin.ExportMixin.get_export_data") as mock_get_export_data: self.export_mixin.export_action(self.mock_request) encoding_kwarg = mock_get_export_data.call_args_list[0][1]["encoding"] self.assertEqual("utf-8", encoding_kwarg) @mock.patch("import_export.admin.ImportForm") def test_export_admin_action_to_encoding(self, mock_form): class TestExportActionMixin(ExportActionMixin): def get_export_filename(self, request, queryset, file_format): return "f" self.mock_request.POST = {'file_format': '1'} self.export_mixin = TestExportActionMixin() self.export_mixin.to_encoding = "utf-8" mock_form.is_valid.return_value = True with mock.patch("import_export.admin.ExportMixin.get_export_data") as mock_get_export_data: self.export_mixin.export_admin_action(self.mock_request, list()) encoding_kwarg = mock_get_export_data.call_args_list[0][1]["encoding"] self.assertEqual("utf-8", encoding_kwarg)django-import-export-2.7.1/tests/core/tests/test_base_formats.py000066400000000000000000000144321416107567000251370ustar00rootroot00000000000000import os import unittest from unittest import mock import tablib from django.test import TestCase from django.utils.encoding import force_str from tablib.core import UnsupportedFormat from import_export.formats import base_formats class FormatTest(TestCase): def setUp(self): self.format = base_formats.Format() @mock.patch('import_export.formats.base_formats.HTML.get_format', side_effect=ImportError) def test_format_non_available1(self, mocked): self.assertFalse(base_formats.HTML.is_available()) @mock.patch('import_export.formats.base_formats.HTML.get_format', side_effect=UnsupportedFormat) def test_format_non_available2(self, mocked): self.assertFalse(base_formats.HTML.is_available()) def test_format_available(self): self.assertTrue(base_formats.CSV.is_available()) def test_get_title(self): self.assertEqual("", str(self.format.get_title())) def test_create_dataset_NotImplementedError(self): with self.assertRaises(NotImplementedError): self.format.create_dataset(None) def test_export_data_NotImplementedError(self): with self.assertRaises(NotImplementedError): self.format.export_data(None) def test_get_extension(self): self.assertEqual("", self.format.get_extension()) def test_get_content_type(self): self.assertEqual("application/octet-stream", self.format.get_content_type()) def test_is_available_default(self): self.assertTrue(self.format.is_available()) def test_can_import_default(self): self.assertFalse(self.format.can_import()) def test_can_export_default(self): self.assertFalse(self.format.can_export()) class XLSTest(TestCase): def setUp(self): self.format = base_formats.XLS() def test_binary_format(self): self.assertTrue(self.format.is_binary()) def test_import(self): filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.xls') with open(filename, self.format.get_read_mode()) as in_stream: self.format.create_dataset(in_stream.read()) class XLSXTest(TestCase): def setUp(self): self.format = base_formats.XLSX() self.filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.xlsx') def test_binary_format(self): self.assertTrue(self.format.is_binary()) def test_import(self): with open(self.filename, self.format.get_read_mode()) as in_stream: dataset = self.format.create_dataset(in_stream.read()) result = dataset.dict self.assertEqual(1, len(result)) row = result.pop() self.assertEqual(1, row["id"]) self.assertEqual("Some book", row["name"]) self.assertEqual("test@example.com", row["author_email"]) self.assertEqual(4, row["price"]) @mock.patch("openpyxl.load_workbook") def test_that_load_workbook_called_with_required_args(self, mock_load_workbook): self.format.create_dataset(b"abc") mock_load_workbook.assert_called_with(unittest.mock.ANY, read_only=True, data_only=True) class CSVTest(TestCase): def setUp(self): self.format = base_formats.CSV() self.dataset = tablib.Dataset(headers=['id', 'username']) self.dataset.append(('1', 'x')) def test_import_dos(self): filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-dos.csv') with open(filename, self.format.get_read_mode()) as in_stream: actual = in_stream.read() expected = 'id,name,author_email\n1,Some book,test@example.com\n' self.assertEqual(actual, expected) def test_import_mac(self): filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-mac.csv') with open(filename, self.format.get_read_mode()) as in_stream: actual = in_stream.read() expected = 'id,name,author_email\n1,Some book,test@example.com\n' self.assertEqual(actual, expected) def test_import_unicode(self): # importing csv UnicodeEncodeError 347 filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-unicode.csv') with open(filename, self.format.get_read_mode()) as in_stream: data = force_str(in_stream.read()) base_formats.CSV().create_dataset(data) def test_export_data(self): res = self.format.export_data(self.dataset) self.assertEqual("id,username\r\n1,x\r\n", res) def test_get_extension(self): self.assertEqual("csv", self.format.get_extension()) def test_content_type(self): self.assertEqual("text/csv", self.format.get_content_type()) def test_can_import(self): self.assertTrue(self.format.can_import()) def test_can_export(self): self.assertTrue(self.format.can_export()) class TSVTest(TestCase): def setUp(self): self.format = base_formats.TSV() def test_import_mac(self): filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-mac.tsv') with open(filename, self.format.get_read_mode()) as in_stream: actual = in_stream.read() expected = 'id\tname\tauthor_email\n1\tSome book\ttest@example.com\n' self.assertEqual(actual, expected) def test_import_unicode(self): # importing tsv UnicodeEncodeError filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books-unicode.tsv') with open(filename, self.format.get_read_mode()) as in_stream: data = force_str(in_stream.read()) base_formats.TSV().create_dataset(data) class TextFormatTest(TestCase): def setUp(self): self.format = base_formats.TextFormat() def test_get_read_mode(self): self.assertEqual('r', self.format.get_read_mode()) def test_is_binary(self): self.assertFalse(self.format.is_binary())django-import-export-2.7.1/tests/core/tests/test_fields.py000066400000000000000000000051641416107567000237420ustar00rootroot00000000000000from datetime import date from django.test import TestCase from import_export import fields class Obj: def __init__(self, name, date=None): self.name = name self.date = date class FieldTest(TestCase): def setUp(self): self.field = fields.Field(column_name='name', attribute='name') self.row = { 'name': 'Foo', } self.obj = Obj(name='Foo', date=date(2012, 8, 13)) def test_clean(self): self.assertEqual(self.field.clean(self.row), self.row['name']) def test_clean_raises_KeyError(self): self.field.column_name = 'x' with self.assertRaisesRegex(KeyError, "Column 'x' not found in dataset. Available columns are: \\['name'\\]"): self.field.clean(self.row) def test_export(self): self.assertEqual(self.field.export(self.obj), self.row['name']) def test_save(self): self.row['name'] = 'foo' self.field.save(self.obj, self.row) self.assertEqual(self.obj.name, 'foo') def test_save_follow(self): class Test: class name: class follow: me = 'bar' test = Test() field = fields.Field(column_name='name', attribute='name__follow__me') row = {'name': 'foo'} field.save(test, row) self.assertEqual(test.name.follow.me, 'foo') def test_following_attribute(self): field = fields.Field(attribute='other_obj__name') obj2 = Obj(name="bar") self.obj.other_obj = obj2 self.assertEqual(field.export(self.obj), "bar") def test_default(self): field = fields.Field(default=1, column_name='name') self.assertEqual(field.clean({'name': None}), 1) def test_default_falsy_values(self): field = fields.Field(default=1, column_name='name') self.assertEqual(field.clean({'name': 0}), 0) def test_default_falsy_values_without_default(self): field = fields.Field(column_name='name') self.assertEqual(field.clean({'name': 0}), 0) def test_saves_null_values(self): field = fields.Field(column_name='name', attribute='name', saves_null_values=False) row = { 'name': None, } field.save(self.obj, row) self.assertEqual(self.obj.name, 'Foo') self.field.save(self.obj, row) self.assertIsNone(self.obj.name) def test_repr(self): self.assertEqual(repr(self.field), '') self.field.column_name = None self.assertEqual(repr(self.field), '') django-import-export-2.7.1/tests/core/tests/test_instance_loaders.py000066400000000000000000000053221416107567000260050ustar00rootroot00000000000000import tablib from core.models import Book from django.test import TestCase from import_export import instance_loaders, resources class BaseInstanceLoaderTest(TestCase): def test_get_instance(self): instance_loader = instance_loaders.BaseInstanceLoader(None) with self.assertRaises(NotImplementedError): instance_loader.get_instance(None) class ModelInstanceLoaderTest(TestCase): def setUp(self): self.resource = resources.modelresource_factory(Book)() def test_get_instance_returns_None_when_params_is_empty(self): # setting an empty array of import_id_fields will mean # that 'params' is never set self.resource._meta.import_id_fields = [] instance_loader = instance_loaders.ModelInstanceLoader(self.resource) self.assertIsNone(instance_loader.get_instance([])) class CachedInstanceLoaderTest(TestCase): def setUp(self): self.resource = resources.modelresource_factory(Book)() self.dataset = tablib.Dataset(headers=['id', 'name', 'author_email']) self.book = Book.objects.create(name="Some book") self.book2 = Book.objects.create(name="Some other book") row = [str(self.book.pk), 'Some book', 'test@example.com'] self.dataset.append(row) self.instance_loader = instance_loaders.CachedInstanceLoader( self.resource, self.dataset) def test_all_instances(self): self.assertTrue(self.instance_loader.all_instances) self.assertEqual(len(self.instance_loader.all_instances), 1) self.assertEqual(list(self.instance_loader.all_instances), [self.book.pk]) def test_get_instance(self): obj = self.instance_loader.get_instance(self.dataset.dict[0]) self.assertEqual(obj, self.book) class CachedInstanceLoaderWithAbsentImportIdFieldTest(TestCase): """Ensure that the cache is empty when the PK field is absent in the inbound dataset. """ def setUp(self): self.resource = resources.modelresource_factory(Book)() self.dataset = tablib.Dataset(headers=['name', 'author_email']) self.book = Book.objects.create(name="Some book") self.book2 = Book.objects.create(name="Some other book") row = ['Some book', 'test@example.com'] self.dataset.append(row) self.instance_loader = instance_loaders.CachedInstanceLoader( self.resource, self.dataset) def test_all_instances(self): self.assertEqual(self.instance_loader.all_instances, {}) self.assertEqual(len(self.instance_loader.all_instances), 0) def test_get_instance(self): obj = self.instance_loader.get_instance(self.dataset.dict[0]) self.assertEqual(obj, None) django-import-export-2.7.1/tests/core/tests/test_invalidrow.py000066400000000000000000000036071416107567000246520ustar00rootroot00000000000000from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.test import TestCase from import_export.results import InvalidRow class InvalidRowTest(TestCase): def setUp(self): # Create a ValidationError with a mix of field-specific and non-field-specific errors self.non_field_errors = ValidationError(['Error 1', 'Error 2', 'Error 3']) self.field_errors = ValidationError({ 'name': ['Error 4', 'Error 5'], 'birthday': ['Error 6', 'Error 7'], }) combined_error_dict = self.non_field_errors.update_error_dict( self.field_errors.error_dict.copy() ) e = ValidationError(combined_error_dict) # Create an InvalidRow instance to use in tests self.obj = InvalidRow( number=1, validation_error=e, values=['ABC', '123'] ) def test_error_count(self): self.assertEqual(self.obj.error_count, 7) def test_non_field_specific_errors(self): result = self.obj.non_field_specific_errors self.assertIsInstance(result, list) self.assertEqual(result, ['Error 1', 'Error 2', 'Error 3']) def test_field_specific_errors(self): result = self.obj.field_specific_errors self.assertIsInstance(result, dict) self.assertEqual(len(result), 2) self.assertEqual(result['name'], ['Error 4', 'Error 5']) self.assertEqual(result['birthday'], ['Error 6', 'Error 7']) def test_creates_error_dict_from_error_list_if_validation_error_only_has_error_list(self): obj = InvalidRow( number=1, validation_error=self.non_field_errors, values=[] ) self.assertIsInstance(obj.error_dict, dict) self.assertIn(NON_FIELD_ERRORS, obj.error_dict) self.assertEqual(obj.error_dict[NON_FIELD_ERRORS], ['Error 1', 'Error 2', 'Error 3']) django-import-export-2.7.1/tests/core/tests/test_mixins.py000066400000000000000000000161241416107567000240010ustar00rootroot00000000000000from unittest import mock from unittest.mock import MagicMock from core.models import Book, Category from django.http import HttpRequest from django.test.testcases import TestCase from django.urls import reverse from import_export import formats, forms, mixins class ExportViewMixinTest(TestCase): class TestExportForm(forms.ExportForm): cleaned_data = dict() def setUp(self): self.url = reverse('export-category') self.cat1 = Category.objects.create(name='Cat 1') self.cat2 = Category.objects.create(name='Cat 2') self.form = ExportViewMixinTest.TestExportForm(formats.base_formats.DEFAULT_FORMATS) self.form.cleaned_data["file_format"] = "0" def test_get(self): response = self.client.get(self.url) self.assertContains(response, self.cat1.name, status_code=200) self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_post(self): data = { 'file_format': '0', } response = self.client.post(self.url, data) self.assertContains(response, self.cat1.name, status_code=200) self.assertTrue(response.has_header("Content-Disposition")) self.assertEqual(response['Content-Type'], 'text/csv') def test_get_response_raises_TypeError_when_content_type_kwarg_used(self): """ Test that HttpResponse is instantiated using the correct kwarg. """ content_type = "text/csv" class TestMixin(mixins.ExportViewFormMixin): def __init__(self): self.model = MagicMock() self.request = MagicMock(spec=HttpRequest) self.model.__name__ = "mockModel" def get_queryset(self): return MagicMock() m = TestMixin() with mock.patch("import_export.mixins.HttpResponse") as mock_http_response: # on first instantiation, raise TypeError, on second, return mock mock_http_response.side_effect = [TypeError(), mock_http_response] m.form_valid(self.form) self.assertEqual(content_type, mock_http_response.call_args_list[0][1]["content_type"]) self.assertEqual(content_type, mock_http_response.call_args_list[1][1]["mimetype"]) def test_implements_get_filterset(self): """ test that if the class-under-test defines a get_filterset() method, then this is called as required. """ class TestMixin(mixins.ExportViewFormMixin): mock_get_filterset_call_count = 0 mock_get_filterset_class_call_count = 0 def __init__(self): self.model = MagicMock() self.request = MagicMock(spec=HttpRequest) self.model.__name__ = "mockModel" def get_filterset(self, filterset_class): self.mock_get_filterset_call_count += 1 return MagicMock() def get_filterset_class(self): self.mock_get_filterset_class_call_count += 1 return MagicMock() m = TestMixin() res = m.form_valid(self.form) self.assertEqual(200, res.status_code) self.assertEqual(1, m.mock_get_filterset_call_count) self.assertEqual(1, m.mock_get_filterset_class_call_count) class BaseImportMixinTest(TestCase): def test_get_import_formats(self): class Format(object): def __init__(self, id, can_import): self.id = id self.val = can_import def can_import(self): return self.val class CanImportFormat(Format): def __init__(self): super().__init__(1, True) class CannotImportFormat(Format): def __init__(self): super().__init__(2, False) m = mixins.BaseImportMixin() m.formats = [CanImportFormat, CannotImportFormat] formats = m.get_import_formats() self.assertEqual(1, len(formats)) self.assertEqual('CanImportFormat', formats[0].__name__) class MixinModelAdminTest(TestCase): """ Tests for regression where methods in ModelAdmin with BaseImportMixin / BaseExportMixin do not get called. see #1315. """ request = MagicMock(spec=HttpRequest) class BaseImportModelAdminTest(mixins.BaseImportMixin): call_count = 0 def get_resource_class(self): self.call_count += 1 def get_resource_kwargs(self, request, *args, **kwargs): self.call_count += 1 class BaseExportModelAdminTest(mixins.BaseExportMixin): call_count = 0 def get_resource_class(self): self.call_count += 1 def get_resource_kwargs(self, request, *args, **kwargs): self.call_count += 1 def test_get_import_resource_class_calls_self_get_resource_class(self): admin = self.BaseImportModelAdminTest() admin.get_import_resource_class() self.assertEqual(1, admin.call_count) def test_get_import_resource_kwargs_calls_self_get_resource_kwargs(self): admin = self.BaseImportModelAdminTest() admin.get_import_resource_kwargs(self.request) self.assertEqual(1, admin.call_count) def test_get_export_resource_class_calls_self_get_resource_class(self): admin = self.BaseExportModelAdminTest() admin.get_export_resource_class() self.assertEqual(1, admin.call_count) def test_get_export_resource_kwargs_calls_self_get_resource_kwargs(self): admin = self.BaseExportModelAdminTest() admin.get_export_resource_kwargs(self.request) self.assertEqual(1, admin.call_count) class BaseExportMixinTest(TestCase): class TestBaseExportMixin(mixins.BaseExportMixin): def get_export_resource_kwargs(self, request, *args, **kwargs): self.args = args self.kwargs = kwargs return super().get_resource_kwargs(request, *args, **kwargs) def test_get_data_for_export_sets_args_and_kwargs(self): """ issue 1268 Ensure that get_export_resource_kwargs() handles the args and kwargs arguments. """ request = MagicMock(spec=HttpRequest) m = self.TestBaseExportMixin() m.model = Book target_args = (1,) target_kwargs = {"a": 1} m.get_data_for_export(request, Book.objects.none(), *target_args, **target_kwargs) self.assertEqual(m.args, target_args) self.assertEqual(m.kwargs, target_kwargs) def test_get_export_formats(self): class Format(object): def __init__(self, can_export): self.val = can_export def can_export(self): return self.val class CanExportFormat(Format): def __init__(self): super().__init__(True) class CannotExportFormat(Format): def __init__(self): super().__init__(False) m = mixins.BaseExportMixin() m.formats = [CanExportFormat, CannotExportFormat] formats = m.get_export_formats() self.assertEqual(1, len(formats)) self.assertEqual('CanExportFormat', formats[0].__name__) django-import-export-2.7.1/tests/core/tests/test_permissions.py000066400000000000000000000077421416107567000250530ustar00rootroot00000000000000import os.path from django.contrib.auth.models import Permission, User from django.test.testcases import TestCase from django.test.utils import override_settings class ImportExportPermissionTest(TestCase): def setUp(self): user = User.objects.create_user('admin', 'admin@example.com', 'password') user.is_staff = True user.is_superuser = False user.save() self.user = user self.client.login(username='admin', password='password') def set_user_book_model_permission(self, action): permission = Permission.objects.get(codename="%s_book" % action) self.user.user_permissions.add(permission) @override_settings(IMPORT_EXPORT_IMPORT_PERMISSION_CODE='change') def test_import(self): # user has no permission to import response = self.client.get('/admin/core/book/import/') self.assertEqual(response.status_code, 403) # POST the import form input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 403) response = self.client.post('/admin/core/book/process_import/', {}) self.assertEqual(response.status_code, 403) # user has sufficient permission to import self.set_user_book_model_permission('change') response = self.client.get('/admin/core/book/import/') self.assertEqual(response.status_code, 200) # POST the import form input_format = '0' filename = os.path.join( os.path.dirname(__file__), os.path.pardir, 'exports', 'books.csv') with open(filename, "rb") as f: data = { 'input_format': input_format, 'import_file': f, } response = self.client.post('/admin/core/book/import/', data) self.assertEqual(response.status_code, 200) confirm_form = response.context['confirm_form'] data = confirm_form.initial response = self.client.post('/admin/core/book/process_import/', data) self.assertEqual(response.status_code, 302) @override_settings(IMPORT_EXPORT_EXPORT_PERMISSION_CODE='change') def test_import_with_permission_set(self): response = self.client.get('/admin/core/book/export/') self.assertEqual(response.status_code, 403) data = {'file_format': '0'} response = self.client.post('/admin/core/book/export/', data) self.assertEqual(response.status_code, 403) self.set_user_book_model_permission('change') response = self.client.get('/admin/core/book/export/') self.assertEqual(response.status_code, 200) data = {'file_format': '0'} response = self.client.post('/admin/core/book/export/', data) self.assertEqual(response.status_code, 200) @override_settings(IMPORT_EXPORT_EXPORT_PERMISSION_CODE='add') def test_check_export_button(self): self.set_user_book_model_permission('change') response = self.client.get('/admin/core/book/') widget = "import_link" self.assertIn(widget, response.content.decode()) widget = "export_link" self.assertNotIn(widget, response.content.decode()) @override_settings(IMPORT_EXPORT_IMPORT_PERMISSION_CODE='add') def test_check_import_button(self): self.set_user_book_model_permission('change') response = self.client.get('/admin/core/book/') widget = "import_link" self.assertNotIn(widget, response.content.decode()) widget = "export_link" self.assertIn(widget, response.content.decode()) django-import-export-2.7.1/tests/core/tests/test_resources.py000066400000000000000000002260371416107567000245120ustar00rootroot00000000000000import json from collections import OrderedDict from copy import deepcopy from datetime import date from decimal import Decimal, InvalidOperation from unittest import mock, skip, skipIf, skipUnless import django import tablib from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.paginator import Paginator from django.db import IntegrityError from django.db.models import Count from django.db.utils import ConnectionDoesNotExist from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature from django.utils.encoding import force_str from django.utils.html import strip_tags from import_export import fields, resources, results, widgets from import_export.instance_loaders import ModelInstanceLoader from import_export.resources import Diff from ..models import ( Author, Book, Category, Entry, Person, Profile, Role, WithDefault, WithDynamicDefault, WithFloatField, ) if django.VERSION[0] >= 3: from django.core.exceptions import FieldDoesNotExist else: from django.db.models.fields import FieldDoesNotExist class MyResource(resources.Resource): name = fields.Field() email = fields.Field() extra = fields.Field() class Meta: export_order = ('email', 'name') class ResourceTestCase(TestCase): def setUp(self): self.my_resource = MyResource() def test_fields(self): """Check that fields were determined correctly """ # check that our fields were determined self.assertIn('name', self.my_resource.fields) # check that resource instance fields attr isn't link to resource cls # fields self.assertFalse( MyResource.fields is self.my_resource.fields ) # dynamically add new resource field into resource instance self.my_resource.fields.update( OrderedDict([ ('new_field', fields.Field()), ]) ) # check that new field in resource instance fields self.assertIn( 'new_field', self.my_resource.fields ) # check that new field not in resource cls fields self.assertNotIn( 'new_field', MyResource.fields ) def test_field_column_name(self): field = self.my_resource.fields['name'] self.assertIn(field.column_name, 'name') def test_meta(self): self.assertIsInstance(self.my_resource._meta, resources.ResourceOptions) @mock.patch("builtins.dir") def test_new_handles_null_options(self, mock_dir): # #1163 - simulates a call to dir() returning additional attributes mock_dir.return_value = ['attrs'] class A(MyResource): pass A() def test_get_export_order(self): self.assertEqual(self.my_resource.get_export_headers(), ['email', 'name', 'extra']) # Issue 140 Attributes aren't inherited by subclasses def test_inheritance(self): class A(MyResource): inherited = fields.Field() class Meta: import_id_fields = ('email',) class B(A): local = fields.Field() class Meta: export_order = ('email', 'extra') resource = B() self.assertIn('name', resource.fields) self.assertIn('inherited', resource.fields) self.assertIn('local', resource.fields) self.assertEqual(resource.get_export_headers(), ['email', 'extra', 'name', 'inherited', 'local']) self.assertEqual(resource._meta.import_id_fields, ('email',)) def test_inheritance_with_custom_attributes(self): class A(MyResource): inherited = fields.Field() class Meta: import_id_fields = ('email',) custom_attribute = True class B(A): local = fields.Field() resource = B() self.assertEqual(resource._meta.custom_attribute, True) def test_get_use_transactions_defined_in_resource(self): class A(MyResource): class Meta: use_transactions = True resource = A() self.assertTrue(resource.get_use_transactions()) def test_get_field_name_raises_AttributeError(self): err = "Field x does not exists in resource" with self.assertRaisesRegex(AttributeError, err): self.my_resource.get_field_name('x') def test_init_instance_raises_NotImplementedError(self): with self.assertRaises(NotImplementedError): self.my_resource.init_instance([]) class AuthorResource(resources.ModelResource): books = fields.Field( column_name='books', attribute='book_set', readonly=True, ) class Meta: model = Author export_order = ('name', 'books') class BookResource(resources.ModelResource): published = fields.Field(column_name='published_date') class Meta: model = Book exclude = ('imported', ) class BookResourceWithLineNumberLogger(BookResource): def __init__(self, *args, **kwargs): self.before_lines = [] self.after_lines = [] return super().__init__(*args, **kwargs) def before_import_row(self,row, row_number=None, **kwargs): self.before_lines.append(row_number) def after_import_row(self, row, row_result, row_number=None, **kwargs): self.after_lines.append(row_number) class CategoryResource(resources.ModelResource): class Meta: model = Category class ProfileResource(resources.ModelResource): class Meta: model = Profile exclude = ('user', ) class WithDefaultResource(resources.ModelResource): class Meta: model = WithDefault fields = ('name',) class HarshRussianWidget(widgets.CharWidget): def clean(self, value, row=None, *args, **kwargs): raise ValueError("Ова вриједност је страшна!") class AuthorResourceWithCustomWidget(resources.ModelResource): class Meta: model = Author @classmethod def widget_from_django_field(cls, f, default=widgets.Widget): if f.name == 'name': return HarshRussianWidget result = default internal_type = f.get_internal_type() if callable(getattr(f, "get_internal_type", None)) else "" if internal_type in cls.WIDGETS_MAP: result = cls.WIDGETS_MAP[internal_type] if isinstance(result, str): result = getattr(cls, result)(f) return result class ModelResourceTest(TestCase): def setUp(self): self.resource = BookResource() self.book = Book.objects.create(name="Some book") self.dataset = tablib.Dataset(headers=['id', 'name', 'author_email', 'price']) row = [self.book.pk, 'Some book', 'test@example.com', "10.25"] self.dataset.append(row) def test_default_instance_loader_class(self): self.assertIs(self.resource._meta.instance_loader_class, ModelInstanceLoader) def test_fields(self): fields = self.resource.fields self.assertIn('id', fields) self.assertIn('name', fields) self.assertIn('author_email', fields) self.assertIn('price', fields) def test_fields_foreign_key(self): fields = self.resource.fields self.assertIn('author', fields) widget = fields['author'].widget self.assertIsInstance(widget, widgets.ForeignKeyWidget) self.assertEqual(widget.model, Author) def test_fields_m2m(self): fields = self.resource.fields self.assertIn('categories', fields) def test_excluded_fields(self): self.assertNotIn('imported', self.resource.fields) def test_init_instance(self): instance = self.resource.init_instance() self.assertIsInstance(instance, Book) def test_default(self): self.assertEqual(WithDefaultResource.fields['name'].clean({'name': ''}), 'foo_bar') def test_get_instance(self): instance_loader = self.resource._meta.instance_loader_class( self.resource) self.resource._meta.import_id_fields = ['id'] instance = self.resource.get_instance(instance_loader, self.dataset.dict[0]) self.assertEqual(instance, self.book) def test_get_instance_import_id_fields(self): class BookResource(resources.ModelResource): name = fields.Field(attribute='name', widget=widgets.CharWidget()) class Meta: model = Book import_id_fields = ['name'] resource = BookResource() instance_loader = resource._meta.instance_loader_class(resource) instance = resource.get_instance(instance_loader, self.dataset.dict[0]) self.assertEqual(instance, self.book) def test_get_instance_import_id_fields_with_custom_column_name(self): class BookResource(resources.ModelResource): name = fields.Field(attribute='name', column_name='book_name', widget=widgets.CharWidget()) class Meta: model = Book import_id_fields = ['name'] dataset = tablib.Dataset(headers=['id', 'book_name', 'author_email', 'price']) row = [self.book.pk, 'Some book', 'test@example.com', "10.25"] dataset.append(row) resource = BookResource() instance_loader = resource._meta.instance_loader_class(resource) instance = resource.get_instance(instance_loader, dataset.dict[0]) self.assertEqual(instance, self.book) def test_get_instance_usually_defers_to_instance_loader(self): self.resource._meta.import_id_fields = ['id'] instance_loader = self.resource._meta.instance_loader_class( self.resource) with mock.patch.object(instance_loader, 'get_instance') as mocked_method: row = self.dataset.dict[0] self.resource.get_instance(instance_loader, row) # instance_loader.get_instance() should have been called mocked_method.assert_called_once_with(row) def test_get_instance_when_id_fields_not_in_dataset(self): self.resource._meta.import_id_fields = ['id'] # construct a dataset with a missing "id" column dataset = tablib.Dataset(headers=['name', 'author_email', 'price']) dataset.append(['Some book', 'test@example.com', "10.25"]) instance_loader = self.resource._meta.instance_loader_class(self.resource) with mock.patch.object(instance_loader, 'get_instance') as mocked_method: result = self.resource.get_instance(instance_loader, dataset.dict[0]) # Resource.get_instance() should return None self.assertIs(result, None) # instance_loader.get_instance() should NOT have been called mocked_method.assert_not_called() def test_get_export_headers(self): headers = self.resource.get_export_headers() self.assertEqual(headers, ['published_date', 'id', 'name', 'author', 'author_email', 'published_time', 'price', 'added', 'categories', ]) def test_export(self): with self.assertNumQueries(2): dataset = self.resource.export(Book.objects.all()) self.assertEqual(len(dataset), 1) def test_export_iterable(self): with self.assertNumQueries(2): dataset = self.resource.export(list(Book.objects.all())) self.assertEqual(len(dataset), 1) def test_export_prefetch_related(self): with self.assertNumQueries(3): dataset = self.resource.export(Book.objects.prefetch_related("categories").all()) self.assertEqual(len(dataset), 1) def test_iter_queryset(self): qs = Book.objects.all() with mock.patch.object(qs, "iterator") as mocked_method: list(self.resource.iter_queryset(qs)) mocked_method.assert_called_once_with(chunk_size=100) def test_iter_queryset_prefetch_unordered(self): qsu = Book.objects.prefetch_related("categories").all() qso = qsu.order_by('pk').all() with mock.patch.object(qsu, "order_by") as mocked_method: mocked_method.return_value = qso list(self.resource.iter_queryset(qsu)) mocked_method.assert_called_once_with("pk") def test_iter_queryset_prefetch_ordered(self): qs = Book.objects.prefetch_related("categories").order_by('pk').all() with mock.patch("import_export.resources.Paginator", autospec=True) as p: p.return_value = Paginator(qs, 100) list(self.resource.iter_queryset(qs)) p.assert_called_once_with(qs, 100) def test_iter_queryset_prefetch_chunk_size(self): class B(BookResource): class Meta: chunk_size = 1000 paginator = "import_export.resources.Paginator" qs = Book.objects.prefetch_related("categories").order_by('pk').all() with mock.patch(paginator, autospec=True) as mocked_obj: mocked_obj.return_value = Paginator(qs, 1000) list(B().iter_queryset(qs)) mocked_obj.assert_called_once_with(qs, 1000) def test_get_diff(self): diff = Diff(self.resource, self.book, False) book2 = Book(name="Some other book") diff.compare_with(self.resource, book2) html = diff.as_html() headers = self.resource.get_export_headers() self.assertEqual(html[headers.index('name')], 'Some ' 'other book') self.assertFalse(html[headers.index('author_email')]) @skip("See: https://github.com/django-import-export/django-import-export/issues/311") def test_get_diff_with_callable_related_manager(self): resource = AuthorResource() author = Author(name="Some author") author.save() author2 = Author(name="Some author") self.book.author = author self.book.save() diff = Diff(self.resource, author, False) diff.compare_with(self.resource, author2) html = diff.as_html() headers = resource.get_export_headers() self.assertEqual(html[headers.index('books')], 'core.Book.None') def test_import_data(self): result = self.resource.import_data(self.dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 1) self.assertTrue(result.rows[0].diff) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_UPDATE) instance = Book.objects.get(pk=self.book.pk) self.assertEqual(instance.author_email, 'test@example.com') self.assertEqual(instance.price, Decimal("10.25")) @mock.patch("import_export.resources.connections") def test_raised_ImproperlyConfigured_if_use_transactions_set_when_transactions_not_supported(self, mock_db_connections): class Features(object): supports_transactions = False class DummyConnection(object): features = Features() dummy_connection = DummyConnection() mock_db_connections.__getitem__.return_value = dummy_connection with self.assertRaises(ImproperlyConfigured): self.resource.import_data( self.dataset, use_transactions=True, ) def test_importing_with_line_number_logging(self): resource = BookResourceWithLineNumberLogger() result = resource.import_data(self.dataset, raise_errors=True) self.assertEqual(resource.before_lines, [1]) self.assertEqual(resource.after_lines, [1]) def test_import_data_raises_field_specific_validation_errors(self): resource = AuthorResource() dataset = tablib.Dataset(headers=['id', 'name', 'birthday']) dataset.append(['', 'A.A.Milne', '1882test-01-18']) result = resource.import_data(dataset, raise_errors=False) self.assertTrue(result.has_validation_errors()) self.assertIs(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_INVALID) self.assertIn('birthday', result.invalid_rows[0].field_specific_errors) def test_collect_failed_rows(self): resource = ProfileResource() headers = ['id', 'user'] # 'user' is a required field, the database will raise an error. row = [None, None] dataset = tablib.Dataset(row, headers=headers) result = resource.import_data( dataset, dry_run=True, use_transactions=True, collect_failed_rows=True, ) self.assertEqual( result.failed_dataset.headers, ['id', 'user', 'Error'] ) self.assertEqual(len(result.failed_dataset), 1) # We can't check the error message because it's package- and version-dependent def test_row_result_raise_errors(self): resource = ProfileResource() headers = ['id', 'user'] # 'user' is a required field, the database will raise an error. row = [None, None] dataset = tablib.Dataset(row, headers=headers) with self.assertRaises(IntegrityError): resource.import_data( dataset, dry_run=True, use_transactions=True, raise_errors=True, ) def test_collect_failed_rows_validation_error(self): resource = ProfileResource() row = ['1'] dataset = tablib.Dataset(row, headers=['id']) with mock.patch("import_export.resources.Field.save", side_effect=ValidationError("fail!")): result = resource.import_data( dataset, dry_run=True, use_transactions=True, collect_failed_rows=True, ) self.assertEqual( result.failed_dataset.headers, ['id', 'Error'] ) self.assertEqual(1, len(result.failed_dataset), ) self.assertEqual('1', result.failed_dataset.dict[0]['id']) self.assertEqual("{'__all__': ['fail!']}", result.failed_dataset.dict[0]['Error']) def test_row_result_raise_ValidationError(self): resource = ProfileResource() row = ['1'] dataset = tablib.Dataset(row, headers=['id']) with mock.patch("import_export.resources.Field.save", side_effect=ValidationError("fail!")): with self.assertRaisesRegex(ValidationError, "{'__all__': \\['fail!'\\]}") : resource.import_data( dataset, dry_run=True, use_transactions=True, raise_errors=True, ) def test_import_data_handles_widget_valueerrors_with_unicode_messages(self): resource = AuthorResourceWithCustomWidget() dataset = tablib.Dataset(headers=['id', 'name', 'birthday']) dataset.append(['', 'A.A.Milne', '1882-01-18']) result = resource.import_data(dataset, raise_errors=False) self.assertTrue(result.has_validation_errors()) self.assertIs(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_INVALID) self.assertEqual( result.invalid_rows[0].field_specific_errors['name'], ["Ова вриједност је страшна!"] ) def test_model_validation_errors_not_raised_when_clean_model_instances_is_false(self): class TestResource(resources.ModelResource): class Meta: model = Author clean_model_instances = False resource = TestResource() dataset = tablib.Dataset(headers=['id', 'name']) dataset.append(['', '123']) result = resource.import_data(dataset, raise_errors=False) self.assertFalse(result.has_validation_errors()) self.assertEqual(len(result.invalid_rows), 0) def test_model_validation_errors_raised_when_clean_model_instances_is_true(self): class TestResource(resources.ModelResource): class Meta: model = Author clean_model_instances = True export_order = ['id', 'name', 'birthday'] # create test dataset # NOTE: column order is deliberately strange dataset = tablib.Dataset(headers=['name', 'id']) dataset.append(['123', '1']) # run import_data() resource = TestResource() result = resource.import_data(dataset, raise_errors=False) # check has_validation_errors() self.assertTrue(result.has_validation_errors()) # check the invalid row itself invalid_row = result.invalid_rows[0] self.assertEqual(invalid_row.error_count, 1) self.assertEqual( invalid_row.field_specific_errors, {'name': ["'123' is not a valid value"]} ) # diff_header and invalid_row.values should match too self.assertEqual( result.diff_headers, ['id', 'name', 'birthday'] ) self.assertEqual( invalid_row.values, ('1', '123', '---') ) def test_known_invalid_fields_are_excluded_from_model_instance_cleaning(self): # The custom widget on the parent class should complain about # 'name' first, preventing Author.full_clean() from raising the # error as it does in the previous test class TestResource(AuthorResourceWithCustomWidget): class Meta: model = Author clean_model_instances = True resource = TestResource() dataset = tablib.Dataset(headers=['id', 'name']) dataset.append(['', '123']) result = resource.import_data(dataset, raise_errors=False) self.assertTrue(result.has_validation_errors()) self.assertEqual(result.invalid_rows[0].error_count, 1) self.assertEqual( result.invalid_rows[0].field_specific_errors, {'name': ["Ова вриједност је страшна!"]} ) def test_import_data_error_saving_model(self): row = list(self.dataset.pop()) # set pk to something that would yield error row[0] = 'foo' self.dataset.append(row) result = self.resource.import_data(self.dataset, raise_errors=False) self.assertTrue(result.has_errors()) self.assertTrue(result.rows[0].errors) actual = result.rows[0].errors[0].error self.assertIsInstance(actual, (ValueError, InvalidOperation)) self.assertIn(str(actual), {"could not convert string to float", "[]"}) def test_import_data_delete(self): class B(BookResource): delete = fields.Field(widget=widgets.BooleanWidget()) def for_delete(self, row, instance): return self.fields['delete'].clean(row) row = [self.book.pk, self.book.name, '1'] dataset = tablib.Dataset(*[row], headers=['id', 'name', 'delete']) result = B().import_data(dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_DELETE) self.assertFalse(Book.objects.filter(pk=self.book.pk)) def test_save_instance_with_dry_run_flag(self): class B(BookResource): def before_save_instance(self, instance, using_transactions, dry_run): super().before_save_instance(instance, using_transactions, dry_run) if dry_run: self.before_save_instance_dry_run = True else: self.before_save_instance_dry_run = False def save_instance(self, instance, using_transactions=True, dry_run=False): super().save_instance(instance, using_transactions, dry_run) if dry_run: self.save_instance_dry_run = True else: self.save_instance_dry_run = False def after_save_instance(self, instance, using_transactions, dry_run): super().after_save_instance(instance, using_transactions, dry_run) if dry_run: self.after_save_instance_dry_run = True else: self.after_save_instance_dry_run = False resource = B() resource.import_data(self.dataset, dry_run=True, raise_errors=True) self.assertTrue(resource.before_save_instance_dry_run) self.assertTrue(resource.save_instance_dry_run) self.assertTrue(resource.after_save_instance_dry_run) resource.import_data(self.dataset, dry_run=False, raise_errors=True) self.assertFalse(resource.before_save_instance_dry_run) self.assertFalse(resource.save_instance_dry_run) self.assertFalse(resource.after_save_instance_dry_run) @mock.patch("core.models.Book.save") def test_save_instance_noop(self, mock_book): book = Book.objects.first() self.resource.save_instance(book, using_transactions=False, dry_run=True) self.assertEqual(0, mock_book.call_count) @mock.patch("core.models.Book.save") def test_delete_instance_noop(self, mock_book): book = Book.objects.first() self.resource.delete_instance(book, using_transactions=False, dry_run=True) self.assertEqual(0, mock_book.call_count) def test_delete_instance_with_dry_run_flag(self): class B(BookResource): delete = fields.Field(widget=widgets.BooleanWidget()) def for_delete(self, row, instance): return self.fields['delete'].clean(row) def before_delete_instance(self, instance, dry_run): super().before_delete_instance(instance, dry_run) if dry_run: self.before_delete_instance_dry_run = True else: self.before_delete_instance_dry_run = False def delete_instance(self, instance, using_transactions=True, dry_run=False): super().delete_instance(instance, using_transactions, dry_run) if dry_run: self.delete_instance_dry_run = True else: self.delete_instance_dry_run = False def after_delete_instance(self, instance, dry_run): super().after_delete_instance(instance, dry_run) if dry_run: self.after_delete_instance_dry_run = True else: self.after_delete_instance_dry_run = False resource = B() row = [self.book.pk, self.book.name, '1'] dataset = tablib.Dataset(*[row], headers=['id', 'name', 'delete']) resource.import_data(dataset, dry_run=True, raise_errors=True) self.assertTrue(resource.before_delete_instance_dry_run) self.assertTrue(resource.delete_instance_dry_run) self.assertTrue(resource.after_delete_instance_dry_run) resource.import_data(dataset, dry_run=False, raise_errors=True) self.assertFalse(resource.before_delete_instance_dry_run) self.assertFalse(resource.delete_instance_dry_run) self.assertFalse(resource.after_delete_instance_dry_run) def test_relationships_fields(self): class B(resources.ModelResource): class Meta: model = Book fields = ('author__name',) author = Author.objects.create(name="Author") self.book.author = author resource = B() result = resource.fields['author__name'].export(self.book) self.assertEqual(result, author.name) def test_dehydrating_fields(self): class B(resources.ModelResource): full_title = fields.Field(column_name="Full title") class Meta: model = Book fields = ('author__name', 'full_title') def dehydrate_full_title(self, obj): return '%s by %s' % (obj.name, obj.author.name) author = Author.objects.create(name="Author") self.book.author = author resource = B() full_title = resource.export_field(resource.get_fields()[0], self.book) self.assertEqual(full_title, '%s by %s' % (self.book.name, self.book.author.name)) def test_widget_format_in_fk_field(self): class B(resources.ModelResource): class Meta: model = Book fields = ('author__birthday',) widgets = { 'author__birthday': {'format': '%Y-%m-%d'}, } author = Author.objects.create(name="Author") self.book.author = author resource = B() result = resource.fields['author__birthday'].export(self.book) self.assertEqual(result, str(date.today())) def test_widget_kwargs_for_field(self): class B(resources.ModelResource): class Meta: model = Book fields = ('published',) widgets = { 'published': {'format': '%d.%m.%Y'}, } resource = B() self.book.published = date(2012, 8, 13) result = resource.fields['published'].export(self.book) self.assertEqual(result, "13.08.2012") def test_foreign_keys_export(self): author1 = Author.objects.create(name='Foo') self.book.author = author1 self.book.save() dataset = self.resource.export(Book.objects.all()) self.assertEqual(dataset.dict[0]['author'], author1.pk) def test_foreign_keys_import(self): author2 = Author.objects.create(name='Bar') headers = ['id', 'name', 'author'] row = [None, 'FooBook', author2.pk] dataset = tablib.Dataset(row, headers=headers) self.resource.import_data(dataset, raise_errors=True) book = Book.objects.get(name='FooBook') self.assertEqual(book.author, author2) def test_m2m_export(self): cat1 = Category.objects.create(name='Cat 1') cat2 = Category.objects.create(name='Cat 2') self.book.categories.add(cat1) self.book.categories.add(cat2) dataset = self.resource.export(Book.objects.all()) self.assertEqual(dataset.dict[0]['categories'], '%d,%d' % (cat1.pk, cat2.pk)) def test_m2m_import(self): cat1 = Category.objects.create(name='Cat 1') headers = ['id', 'name', 'categories'] row = [None, 'FooBook', str(cat1.pk)] dataset = tablib.Dataset(row, headers=headers) self.resource.import_data(dataset, raise_errors=True) book = Book.objects.get(name='FooBook') self.assertIn(cat1, book.categories.all()) def test_m2m_options_import(self): cat1 = Category.objects.create(name='Cat 1') cat2 = Category.objects.create(name='Cat 2') headers = ['id', 'name', 'categories'] row = [None, 'FooBook', "Cat 1|Cat 2"] dataset = tablib.Dataset(row, headers=headers) class BookM2MResource(resources.ModelResource): categories = fields.Field( attribute='categories', widget=widgets.ManyToManyWidget(Category, field='name', separator='|') ) class Meta: model = Book resource = BookM2MResource() resource.import_data(dataset, raise_errors=True) book = Book.objects.get(name='FooBook') self.assertIn(cat1, book.categories.all()) self.assertIn(cat2, book.categories.all()) def test_related_one_to_one(self): # issue #17 - Exception when attempting access something on the # related_name user = User.objects.create(username='foo') profile = Profile.objects.create(user=user) Entry.objects.create(user=user) Entry.objects.create(user=User.objects.create(username='bar')) class EntryResource(resources.ModelResource): class Meta: model = Entry fields = ('user__profile', 'user__profile__is_private') resource = EntryResource() dataset = resource.export(Entry.objects.all()) self.assertEqual(dataset.dict[0]['user__profile'], profile.pk) self.assertEqual(dataset.dict[0]['user__profile__is_private'], '1') self.assertEqual(dataset.dict[1]['user__profile'], '') self.assertEqual(dataset.dict[1]['user__profile__is_private'], '') def test_empty_get_queryset(self): # issue #25 - Overriding queryset on export() fails when passed # queryset has zero elements dataset = self.resource.export(Book.objects.none()) self.assertEqual(len(dataset), 0) def test_import_data_skip_unchanged(self): def attempted_save(instance, real_dry_run): self.fail('Resource attempted to save instead of skipping') # Make sure we test with ManyToMany related objects cat1 = Category.objects.create(name='Cat 1') cat2 = Category.objects.create(name='Cat 2') self.book.categories.add(cat1) self.book.categories.add(cat2) dataset = self.resource.export() # Create a new resource that attempts to reimport the data currently # in the database while skipping unchanged rows (i.e. all of them) resource = deepcopy(self.resource) resource._meta.skip_unchanged = True # Fail the test if the resource attempts to save the row resource.save_instance = attempted_save result = resource.import_data(dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), len(dataset)) self.assertTrue(result.rows[0].diff) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) # Test that we can suppress reporting of skipped rows resource._meta.report_skipped = False result = resource.import_data(dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 0) def test_before_import_access_to_kwargs(self): class B(BookResource): def before_import(self, dataset, using_transactions, dry_run, **kwargs): if 'extra_arg' in kwargs: dataset.headers[dataset.headers.index('author_email')] = 'old_email' dataset.insert_col(0, lambda row: kwargs['extra_arg'], header='author_email') resource = B() result = resource.import_data(self.dataset, raise_errors=True, extra_arg='extra@example.com') self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 1) instance = Book.objects.get(pk=self.book.pk) self.assertEqual(instance.author_email, 'extra@example.com') def test_before_import_raises_error(self): class B(BookResource): def before_import(self, dataset, using_transactions, dry_run, **kwargs): raise Exception('This is an invalid dataset') resource = B() with self.assertRaises(Exception) as cm: resource.import_data(self.dataset, raise_errors=True) self.assertEqual("This is an invalid dataset", cm.exception.args[0]) def test_after_import_raises_error(self): class B(BookResource): def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): raise Exception('This is an invalid dataset') resource = B() with self.assertRaises(Exception) as cm: resource.import_data(self.dataset, raise_errors=True) self.assertEqual("This is an invalid dataset", cm.exception.args[0]) def test_link_to_nonexistent_field(self): with self.assertRaises(FieldDoesNotExist) as cm: class BrokenBook1(resources.ModelResource): class Meta: model = Book fields = ('nonexistent__invalid',) self.assertEqual("Book.nonexistent: Book has no field named 'nonexistent'", cm.exception.args[0]) with self.assertRaises(FieldDoesNotExist) as cm: class BrokenBook2(resources.ModelResource): class Meta: model = Book fields = ('author__nonexistent',) self.assertEqual("Book.author.nonexistent: Author has no field named " "'nonexistent'", cm.exception.args[0]) def test_link_to_nonrelation_field(self): with self.assertRaises(KeyError) as cm: class BrokenBook1(resources.ModelResource): class Meta: model = Book fields = ('published__invalid',) self.assertEqual("Book.published is not a relation", cm.exception.args[0]) with self.assertRaises(KeyError) as cm: class BrokenBook2(resources.ModelResource): class Meta: model = Book fields = ('author__name__invalid',) self.assertEqual("Book.author.name is not a relation", cm.exception.args[0]) def test_override_field_construction_in_resource(self): class B(resources.ModelResource): class Meta: model = Book fields = ('published',) @classmethod def field_from_django_field(self, field_name, django_field, readonly): if field_name == 'published': return {'sound': 'quack'} B() self.assertEqual({'sound': 'quack'}, B.fields['published']) def test_readonly_annotated_field_import_and_export(self): class B(BookResource): total_categories = fields.Field('total_categories', readonly=True) class Meta: model = Book skip_unchanged = True cat1 = Category.objects.create(name='Cat 1') self.book.categories.add(cat1) resource = B() # Verify that the annotated field is correctly exported dataset = resource.export( Book.objects.annotate(total_categories=Count('categories'))) self.assertEqual(int(dataset.dict[0]['total_categories']), 1) # Verify that importing the annotated field raises no errors and that # the rows are skipped result = resource.import_data(dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), len(dataset)) self.assertEqual( result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) def test_follow_relationship_for_modelresource(self): class EntryResource(resources.ModelResource): username = fields.Field(attribute='user__username', readonly=False) class Meta: model = Entry fields = ('id', ) def after_save_instance(self, instance, using_transactions, dry_run): if not using_transactions and dry_run: # we don't have transactions and we want to do a dry_run pass else: instance.user.save() user = User.objects.create(username='foo') entry = Entry.objects.create(user=user) row = [ entry.pk, 'bar', ] self.dataset = tablib.Dataset(headers=['id', 'username']) self.dataset.append(row) result = EntryResource().import_data( self.dataset, raise_errors=True, dry_run=False) self.assertFalse(result.has_errors()) self.assertEqual(User.objects.get(pk=user.pk).username, 'bar') def test_import_data_dynamic_default_callable(self): class DynamicDefaultResource(resources.ModelResource): class Meta: model = WithDynamicDefault fields = ('id', 'name',) self.assertTrue(callable(DynamicDefaultResource.fields['name'].default)) resource = DynamicDefaultResource() dataset = tablib.Dataset(headers=['id', 'name', ]) dataset.append([1, None]) dataset.append([2, None]) resource.import_data(dataset, raise_errors=False) objs = WithDynamicDefault.objects.all() self.assertNotEqual(objs[0].name, objs[1].name) def test_float_field(self): #433 class R(resources.ModelResource): class Meta: model = WithFloatField resource = R() dataset = tablib.Dataset(headers=['id', 'f', ]) dataset.append([None, None]) dataset.append([None, '']) resource.import_data(dataset, raise_errors=True) self.assertEqual(WithFloatField.objects.all()[0].f, None) self.assertEqual(WithFloatField.objects.all()[1].f, None) def test_get_db_connection_name(self): class BookResource(resources.ModelResource): class Meta: using_db = 'other_db' self.assertEqual(BookResource().get_db_connection_name(), 'other_db') self.assertEqual(CategoryResource().get_db_connection_name(), 'default') def test_import_data_raises_field_for_wrong_db(self): class BookResource(resources.ModelResource): class Meta: using_db = 'wrong_db' with self.assertRaises(ConnectionDoesNotExist): BookResource().import_data(self.dataset) class ModelResourceTransactionTest(TransactionTestCase): @skipUnlessDBFeature('supports_transactions') def test_m2m_import_with_transactions(self): resource = BookResource() cat1 = Category.objects.create(name='Cat 1') headers = ['id', 'name', 'categories'] row = [None, 'FooBook', str(cat1.pk)] dataset = tablib.Dataset(row, headers=headers) result = resource.import_data( dataset, dry_run=True, use_transactions=True ) row_diff = result.rows[0].diff fields = resource.get_fields() id_field = resource.fields['id'] id_diff = row_diff[fields.index(id_field)] # id diff should exist because in rollbacked transaction # FooBook has been saved self.assertTrue(id_diff) category_field = resource.fields['categories'] categories_diff = row_diff[fields.index(category_field)] self.assertEqual(strip_tags(categories_diff), force_str(cat1.pk)) # check that it is really rollbacked self.assertFalse(Book.objects.filter(name='FooBook')) @skipUnlessDBFeature('supports_transactions') def test_m2m_import_with_transactions_error(self): resource = ProfileResource() headers = ['id', 'user'] # 'user' is a required field, the database will raise an error. row = [None, None] dataset = tablib.Dataset(row, headers=headers) result = resource.import_data( dataset, dry_run=True, use_transactions=True ) # Ensure the error raised by the database has been saved. self.assertTrue(result.has_errors()) # Ensure the rollback has worked properly. self.assertEqual(Profile.objects.count(), 0) @skipUnlessDBFeature('supports_transactions') def test_integrity_error_rollback_on_savem2m(self): # savepoint_rollback() after an IntegrityError gives # TransactionManagementError (#399) class CategoryResourceRaisesIntegrityError(CategoryResource): def save_m2m(self, instance, *args, **kwargs): # force raising IntegrityError Category.objects.create(name=instance.name) resource = CategoryResourceRaisesIntegrityError() headers = ['id', 'name'] rows = [ [None, 'foo'], ] dataset = tablib.Dataset(*rows, headers=headers) result = resource.import_data( dataset, use_transactions=True, ) self.assertTrue(result.has_errors()) def test_rollback_on_validation_errors_false(self): """ Should create only one instance as the second one raises a ``ValidationError`` """ resource = AuthorResource() headers = ['id', 'name', 'birthday'] rows = [ ['', 'A.A.Milne', ''], ['', '123', '1992test-01-18'], # raises ValidationError ] dataset = tablib.Dataset(*rows, headers=headers) result = resource.import_data( dataset, use_transactions=True, rollback_on_validation_errors=False, ) # Ensure the validation error raised by the database has been saved. self.assertTrue(result.has_validation_errors()) # Ensure that valid row resulted in an instance created. self.assertEqual(Author.objects.count(), 1) def test_rollback_on_validation_errors_true(self): """ Should not create any instances as the second one raises a ``ValidationError`` and ``rollback_on_validation_errors`` flag is set """ resource = AuthorResource() headers = ['id', 'name', 'birthday'] rows = [ ['', 'A.A.Milne', ''], ['', '123', '1992test-01-18'], # raises ValidationError ] dataset = tablib.Dataset(*rows, headers=headers) result = resource.import_data( dataset, use_transactions=True, rollback_on_validation_errors=True, ) # Ensure the validation error raised by the database has been saved. self.assertTrue(result.has_validation_errors()) # Ensure the rollback has worked properly, no instances were created. self.assertFalse(Author.objects.exists()) class ModelResourceFactoryTest(TestCase): def test_create(self): BookResource = resources.modelresource_factory(Book) self.assertIn('id', BookResource.fields) self.assertEqual(BookResource._meta.model, Book) @skipUnless( 'postgresql' in settings.DATABASES['default']['ENGINE'], 'Run only against Postgres') class PostgresTests(TransactionTestCase): # Make sure to start the sequences back at 1 reset_sequences = True def test_create_object_after_importing_dataset_with_id(self): dataset = tablib.Dataset(headers=['id', 'name']) dataset.append([1, 'Some book']) resource = BookResource() result = resource.import_data(dataset) self.assertFalse(result.has_errors()) try: Book.objects.create(name='Some other book') except IntegrityError: self.fail('IntegrityError was raised.') if 'postgresql' in settings.DATABASES['default']['ENGINE']: from django.contrib.postgres.fields import ArrayField from django.db import models try: from django.db.models import JSONField except ImportError: from django.contrib.postgres.fields import JSONField class BookWithChapters(models.Model): name = models.CharField('Book name', max_length=100) chapters = ArrayField(models.CharField(max_length=100), default=list) data = JSONField(null=True) class BookWithChaptersResource(resources.ModelResource): class Meta: model = BookWithChapters fields = ( 'id', 'name', 'chapters', 'data', ) class TestExportArrayField(TestCase): def test_exports_array_field(self): dataset_headers = ["id", "name", "chapters"] chapters = ["Introduction", "Middle Chapter", "Ending"] dataset_row = ["1", "Book With Chapters", ",".join(chapters)] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_with_chapters_resource = resources.modelresource_factory(model=BookWithChapters)() result = book_with_chapters_resource.import_data(dataset, dry_run=False) self.assertFalse(result.has_errors()) book_with_chapters = list(BookWithChapters.objects.all())[0] self.assertListEqual(book_with_chapters.chapters, chapters) class TestImportArrayField(TestCase): def setUp(self): self.resource = BookWithChaptersResource() self.chapters = ["Introduction", "Middle Chapter", "Ending"] self.book = BookWithChapters.objects.create(name='foo') self.dataset = tablib.Dataset(headers=['id', 'name', 'chapters']) row = [self.book.id, 'Some book', ",".join(self.chapters)] self.dataset.append(row) def test_import_of_data_with_array(self): self.assertListEqual(self.book.chapters, []) result = self.resource.import_data(self.dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 1) self.book.refresh_from_db() self.assertEqual(self.book.chapters, self.chapters) class TestExportJsonField(TestCase): def setUp(self): self.json_data = {"some_key": "some_value"} self.book = BookWithChapters.objects.create(name='foo', data=self.json_data) def test_export_field_with_appropriate_format(self): resource = resources.modelresource_factory(model=BookWithChapters)() result = resource.export(BookWithChapters.objects.all()) assert result[0][3] == json.dumps(self.json_data) class TestImportJsonField(TestCase): def setUp(self): self.resource = BookWithChaptersResource() self.data = {"some_key": "some_value"} self.json_data = json.dumps(self.data) self.book = BookWithChapters.objects.create(name='foo') self.dataset = tablib.Dataset(headers=['id', 'name', 'data']) row = [self.book.id, 'Some book', self.json_data] self.dataset.append(row) def test_sets_json_data_when_model_field_is_empty(self): self.assertIsNone(self.book.data) result = self.resource.import_data(self.dataset, raise_errors=True) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 1) self.book.refresh_from_db() self.assertEqual(self.book.data, self.data) class ForeignKeyWidgetFollowRelationship(TestCase): def setUp(self): self.user = User.objects.create(username='foo') self.role = Role.objects.create(user=self.user) self.person = Person.objects.create(role=self.role) def test_export(self): class MyPersonResource(resources.ModelResource): role = fields.Field( column_name='role', attribute='role', widget=widgets.ForeignKeyWidget(Role, field='user__username') ) class Meta: model = Person fields = ['id', 'role'] resource = MyPersonResource() dataset = resource.export(Person.objects.all()) self.assertEqual(len(dataset), 1) self.assertEqual(dataset[0][0], 'foo') self.role.user = None self.role.save() resource = MyPersonResource() dataset = resource.export(Person.objects.all()) self.assertEqual(len(dataset), 1) self.assertEqual(dataset[0][0], None) class ManyRelatedManagerDiffTest(TestCase): fixtures = ["category", "book"] def setUp(self): pass def test_related_manager_diff(self): dataset_headers = ["id", "name", "categories"] dataset_row = ["1", "Test Book", "1"] original_dataset = tablib.Dataset(headers=dataset_headers) original_dataset.append(dataset_row) dataset_row[2] = "2" changed_dataset = tablib.Dataset(headers=dataset_headers) changed_dataset.append(dataset_row) book_resource = BookResource() export_headers = book_resource.get_export_headers() add_result = book_resource.import_data(original_dataset, dry_run=False) expected_value = '1' self.check_value(add_result, export_headers, expected_value) change_result = book_resource.import_data(changed_dataset, dry_run=False) expected_value = '12' self.check_value(change_result, export_headers, expected_value) def check_value(self, result, export_headers, expected_value): self.assertEqual(len(result.rows), 1) diff = result.rows[0].diff self.assertEqual(diff[export_headers.index("categories")], expected_value) @mock.patch("import_export.resources.Diff", spec=True) class SkipDiffTest(TestCase): """ Tests that the meta attribute 'skip_diff' means that no diff operations are called. 'copy.deepcopy' cannot be patched at class level because it causes interferes with ``resources.Resource.__init__()``. """ def setUp(self): class _BookResource(resources.ModelResource): class Meta: model = Book skip_diff = True self.resource = _BookResource() self.dataset = tablib.Dataset(headers=['id', 'name', 'birthday']) self.dataset.append(['', 'A.A.Milne', '1882test-01-18']) def test_skip_diff(self, mock_diff): with mock.patch("import_export.resources.deepcopy") as mock_deep_copy: self.resource.import_data(self.dataset) mock_diff.return_value.compare_with.assert_not_called() mock_diff.return_value.as_html.assert_not_called() mock_deep_copy.assert_not_called() def test_skip_diff_for_delete_new_resource(self, mock_diff): class BookResource(resources.ModelResource): class Meta: model = Book skip_diff = True def for_delete(self, row, instance): return True resource = BookResource() with mock.patch("import_export.resources.deepcopy") as mock_deep_copy: resource.import_data(self.dataset) mock_diff.return_value.compare_with.assert_not_called() mock_diff.return_value.as_html.assert_not_called() mock_deep_copy.assert_not_called() def test_skip_diff_for_delete_existing_resource(self, mock_diff): book = Book.objects.create() class BookResource(resources.ModelResource): class Meta: model = Book skip_diff = True def get_or_init_instance(self, instance_loader, row): return book, False def for_delete(self, row, instance): return True resource = BookResource() with mock.patch("import_export.resources.deepcopy") as mock_deep_copy: resource.import_data(self.dataset, dry_run=True) mock_diff.return_value.compare_with.assert_not_called() mock_diff.return_value.as_html.assert_not_called() mock_deep_copy.assert_not_called() def test_skip_diff_for_delete_skip_row_not_enabled_new_object(self, mock_diff): class BookResource(resources.ModelResource): class Meta: model = Book skip_diff = False def for_delete(self, row, instance): return True resource = BookResource() with mock.patch("import_export.resources.deepcopy") as mock_deep_copy: resource.import_data(self.dataset, dry_run=True) self.assertEqual(1, mock_diff.return_value.compare_with.call_count) self.assertEqual(1, mock_deep_copy.call_count) def test_skip_row_returns_false_when_skip_diff_is_true(self, mock_diff): class BookResource(resources.ModelResource): class Meta: model = Book skip_unchanged = True skip_diff = True resource = BookResource() with mock.patch('import_export.resources.Resource.get_import_fields') as mock_get_import_fields: resource.import_data(self.dataset, dry_run=True) self.assertEqual(2, mock_get_import_fields.call_count) class SkipHtmlDiffTest(TestCase): def test_skip_html_diff(self): class BookResource(resources.ModelResource): class Meta: model = Book skip_html_diff = True resource = BookResource() self.dataset = tablib.Dataset(headers=['id', 'name', 'birthday']) self.dataset.append(['', 'A.A.Milne', '1882test-01-18']) with mock.patch('import_export.resources.Diff.as_html') as mock_as_html: resource.import_data(self.dataset, dry_run=True) mock_as_html.assert_not_called() class BulkTest(TestCase): def setUp(self): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True self.resource = _BookResource() rows = [('book_name',)] * 10 self.dataset = tablib.Dataset(*rows, headers=['name']) def init_update_test_data(self): [Book.objects.create(name='book_name') for _ in range(10)] self.assertEqual(10, Book.objects.count()) rows = Book.objects.all().values_list('id', 'name') updated_rows = [(r[0], 'UPDATED') for r in rows] self.dataset = tablib.Dataset(*updated_rows, headers=['id', 'name']) class BulkCreateTest(BulkTest): @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_does_not_call_object_save(self, mock_bulk_create): with mock.patch('core.models.Book.save') as mock_obj_save: self.resource.import_data(self.dataset) mock_obj_save.assert_not_called() mock_bulk_create.assert_called_with(mock.ANY, batch_size=None) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_batch_size_of_5(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 5 resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(2, mock_bulk_create.call_count) mock_bulk_create.assert_called_with(mock.ANY, batch_size=5) self.assertEqual(10, result.total_rows) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_no_batch_size(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = None resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(1, mock_bulk_create.call_count) mock_bulk_create.assert_called_with(mock.ANY, batch_size=None) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["new"]) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_called_dry_run(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = None resource = _BookResource() result = resource.import_data(self.dataset, dry_run=True) self.assertEqual(1, mock_bulk_create.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["new"]) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_not_called_when_not_using_transactions(self, mock_bulk_create): class _BookResource(resources.ModelResource): def import_data(self, dataset, dry_run=False, raise_errors=False, use_transactions=None, collect_failed_rows=False, **kwargs): # override so that we can enforce not using_transactions using_transactions = False return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs) class Meta: model = Book use_bulk = True resource = _BookResource() resource.import_data(self.dataset, dry_run=True) mock_bulk_create.assert_not_called() @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_batch_size_of_4(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 4 resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(3, mock_bulk_create.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["new"]) def test_no_changes_for_errors_if_use_transactions_enabled(self): with mock.patch('import_export.results.Result.has_errors') as mock_has_errors: mock_has_errors.return_val = True self.resource.import_data(self.dataset) self.assertEqual(0, Book.objects.count()) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_use_bulk_disabled(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = False resource = _BookResource() result = resource.import_data(self.dataset) mock_bulk_create.assert_not_called() self.assertEqual(10, Book.objects.count()) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["new"]) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_bad_batch_size_value(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 'a' resource = _BookResource() with self.assertRaises(ValueError): resource.import_data(self.dataset) mock_bulk_create.assert_not_called() @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_negative_batch_size_value(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = -1 resource = _BookResource() with self.assertRaises(ValueError): resource.import_data(self.dataset) mock_bulk_create.assert_not_called() @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_oversized_batch_size_value(self, mock_bulk_create): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 100 resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(1, mock_bulk_create.call_count) mock_bulk_create.assert_called_with(mock.ANY, batch_size=None) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["new"]) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_logs_exception(self, mock_bulk_create): e = ValidationError("invalid field") mock_bulk_create.side_effect = e class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 100 resource = _BookResource() with mock.patch("logging.Logger.exception") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e) self.assertEqual(1, mock_exception.call_count) @mock.patch('core.models.Book.objects.bulk_create') def test_bulk_create_raises_exception(self, mock_bulk_create): mock_bulk_create.side_effect = ValidationError("invalid field") class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 100 resource = _BookResource() with self.assertRaises(ValidationError): resource.import_data(self.dataset, raise_errors=True) def test_m2m_not_called_for_bulk(self): mock_m2m_widget = mock.Mock(spec=widgets.ManyToManyWidget) class BookM2MResource(resources.ModelResource): categories = fields.Field( attribute='categories', widget=mock_m2m_widget ) class Meta: model = Book use_bulk = True resource = BookM2MResource() self.dataset.append_col(["Cat 1|Cat 2"] * 10, header="categories") resource.import_data(self.dataset, raise_errors=True) mock_m2m_widget.assert_not_called() def test_force_init_instance(self): class _BookResource(resources.ModelResource): def get_instance(self, instance_loader, row): raise AssertionError("should not be called") class Meta: model = Book force_init_instance = True resource = _BookResource() self.assertIsNotNone(resource.get_or_init_instance(ModelInstanceLoader(resource), self.dataset[0])) @skipIf(django.VERSION[0] == 2 and django.VERSION[1] < 2, "bulk_update not supported in this version of django") class BulkUpdateTest(BulkTest): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True fields = ('id', 'name') import_id_fields = ('id',) def setUp(self): super().setUp() self.init_update_test_data() self.resource = self._BookResource() def test_bulk_update(self): result = self.resource.import_data(self.dataset) [self.assertEqual('UPDATED', b.name) for b in Book.objects.all()] self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["update"]) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_batch_size_of_4(self, mock_bulk_update): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 4 resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(3, mock_bulk_update.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["update"]) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_batch_size_of_5(self, mock_bulk_update): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = 5 resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(2, mock_bulk_update.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["update"]) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_no_batch_size(self, mock_bulk_update): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True batch_size = None resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(1, mock_bulk_update.call_count) mock_bulk_update.assert_called_with(mock.ANY, mock.ANY, batch_size=None) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["update"]) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_not_called_when_not_using_transactions(self, mock_bulk_update): class _BookResource(resources.ModelResource): def import_data(self, dataset, dry_run=False, raise_errors=False, use_transactions=None, collect_failed_rows=False, **kwargs): # override so that we can enforce not using_transactions using_transactions = False return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs) class Meta: model = Book use_bulk = True resource = _BookResource() resource.import_data(self.dataset, dry_run=True) mock_bulk_update.assert_not_called() @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_called_for_dry_run(self, mock_bulk_update): self.resource.import_data(self.dataset, dry_run=True) self.assertEqual(1, mock_bulk_update.call_count) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_not_called_when_use_bulk_disabled(self, mock_bulk_update): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = False resource = _BookResource() result = resource.import_data(self.dataset) self.assertEqual(10, Book.objects.count()) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["update"]) mock_bulk_update.assert_not_called() @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_logs_exception(self, mock_bulk_update): e = ValidationError("invalid field") mock_bulk_update.side_effect = e class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True resource = _BookResource() with mock.patch("logging.Logger.exception") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e) self.assertEqual(1, mock_exception.call_count) @mock.patch('core.models.Book.objects.bulk_update') def test_bulk_update_raises_exception(self, mock_bulk_update): e = ValidationError("invalid field") mock_bulk_update.side_effect = e class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True resource = _BookResource() with self.assertRaises(ValidationError) as raised_exc: resource.import_data(self.dataset, raise_errors=True) self.assertEqual(e, raised_exc) class BulkDeleteTest(BulkTest): class DeleteBookResource(resources.ModelResource): def for_delete(self, row, instance): return True class Meta: model = Book use_bulk = True def setUp(self): super().setUp() self.resource = self.DeleteBookResource() self.init_update_test_data() @mock.patch("core.models.Book.delete") def test_bulk_delete_use_bulk_is_false(self, mock_obj_delete): class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = False self.resource = _BookResource() self.resource.import_data(self.dataset) self.assertEqual(10, mock_obj_delete.call_count) @mock.patch("core.models.Book.objects") def test_bulk_delete_batch_size_of_4(self, mock_obj_manager): class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = True batch_size = 4 self.resource = _BookResource() result = self.resource.import_data(self.dataset) self.assertEqual(3, mock_obj_manager.filter.return_value.delete.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["delete"]) @mock.patch("core.models.Book.objects") def test_bulk_delete_batch_size_of_5(self, mock_obj_manager): class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = True batch_size = 5 self.resource = _BookResource() result = self.resource.import_data(self.dataset) self.assertEqual(2, mock_obj_manager.filter.return_value.delete.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["delete"]) @mock.patch("core.models.Book.objects") def test_bulk_delete_batch_size_is_none(self, mock_obj_manager): class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = True batch_size = None self.resource = _BookResource() result = self.resource.import_data(self.dataset) self.assertEqual(1, mock_obj_manager.filter.return_value.delete.call_count) self.assertEqual(10, result.total_rows) self.assertEqual(10, result.totals["delete"]) @mock.patch("core.models.Book.objects") def test_bulk_delete_not_called_when_not_using_transactions(self, mock_obj_manager): class _BookResource(self.DeleteBookResource): def import_data(self, dataset, dry_run=False, raise_errors=False, use_transactions=None, collect_failed_rows=False, **kwargs): # override so that we can enforce not using_transactions using_transactions = False return self.import_data_inner(dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs) class Meta: model = Book use_bulk = True resource = _BookResource() resource.import_data(self.dataset, dry_run=True) self.assertEqual(0, mock_obj_manager.filter.return_value.delete.call_count) @mock.patch("core.models.Book.objects") def test_bulk_delete_called_for_dry_run(self, mock_obj_manager): self.resource.import_data(self.dataset, dry_run=True) self.assertEqual(1, mock_obj_manager.filter.return_value.delete.call_count) @mock.patch("core.models.Book.objects") def test_bulk_delete_logs_exception(self, mock_obj_manager): e = Exception("invalid") mock_obj_manager.filter.return_value.delete.side_effect = e class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = True resource = _BookResource() with mock.patch("logging.Logger.exception") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e) self.assertEqual(1, mock_exception.call_count) @mock.patch("core.models.Book.objects") def test_bulk_delete_raises_exception(self, mock_obj_manager): e = Exception("invalid") mock_obj_manager.filter.return_value.delete.side_effect = e class _BookResource(self.DeleteBookResource): class Meta: model = Book use_bulk = True resource = _BookResource() with self.assertRaises(Exception) as raised_exc: resource.import_data(self.dataset, raise_errors=True) self.assertEqual(e, raised_exc) django-import-export-2.7.1/tests/core/tests/test_results.py000066400000000000000000000041001416107567000241620ustar00rootroot00000000000000from core.models import Book from django.core.exceptions import ValidationError from django.test.testcases import TestCase from tablib import Dataset from import_export.results import Error, Result, RowResult class ResultTest(TestCase): def setUp(self): self.result = Result() headers = ['id', 'book_name'] rows = [(1, 'Some book')] self.dataset = Dataset(*rows, headers=headers) def test_add_dataset_headers(self): target = ['Error'] self.result.add_dataset_headers([]) self.assertEqual(target, self.result.failed_dataset.headers) def test_result_append_failed_row_with_ValidationError(self): target = [[1, 'Some book', "['some error']"]] self.result.append_failed_row(self.dataset.dict[0], ValidationError('some error')) self.assertEqual(target, self.result.failed_dataset.dict) def test_result_append_failed_row_with_wrapped_error(self): target = [[1, 'Some book', "['some error']"]] row_result = RowResult() error = Error(ValidationError('some error')) row_result.errors = [error] self.result.append_failed_row(self.dataset.dict[0], row_result.errors[0]) self.assertEqual(target, self.result.failed_dataset.dict) def test_add_instance_info_null_instance(self): row_result = RowResult() row_result.add_instance_info(None) self.assertEqual(None, row_result.object_id) self.assertEqual(None, row_result.object_repr) def test_add_instance_info_no_instance_pk(self): row_result = RowResult() row_result.add_instance_info(Book()) self.assertEqual(None, row_result.object_id) self.assertEqual("", row_result.object_repr) def test_add_instance_info(self): class BookWithObjectRepr(Book): def __str__(self): return self.name row_result = RowResult() row_result.add_instance_info(BookWithObjectRepr(pk=1, name="some book")) self.assertEqual(1, row_result.object_id) self.assertEqual("some book", row_result.object_repr)django-import-export-2.7.1/tests/core/tests/test_tmp_storages.py000066400000000000000000000052021416107567000251740ustar00rootroot00000000000000import os from django.core.cache import cache from django.core.files.storage import default_storage from django.test import TestCase from import_export.tmp_storages import ( BaseStorage, CacheStorage, MediaStorage, TempFolderStorage, ) class TestBaseStorage(TestCase): def setUp(self): self.storage = BaseStorage() def test_save(self): with self.assertRaises(NotImplementedError): self.storage.save(None) def test_read(self): with self.assertRaises(NotImplementedError): self.storage.read() def test_remove(self): with self.assertRaises(NotImplementedError): self.storage.remove() class TempStoragesTest(TestCase): def setUp(self): self.test_string = b""" id,name,author,author_email,imported,published,price,categories 2,Bar,1,,0,,, 1,Foo,,,0,,, """ def test_temp_folder_storage(self): tmp_storage = TempFolderStorage() tmp_storage.save(self.test_string) name = tmp_storage.name tmp_storage = TempFolderStorage(name=name) self.assertEqual(self.test_string.decode(), tmp_storage.read()) self.assertTrue(os.path.isfile(tmp_storage.get_full_path())) tmp_storage.remove() self.assertFalse(os.path.isfile(tmp_storage.get_full_path())) def test_cache_storage(self): tmp_storage = CacheStorage() tmp_storage.save(self.test_string) name = tmp_storage.name tmp_storage = CacheStorage(name=name) self.assertEqual(self.test_string, tmp_storage.read()) self.assertNotEqual(cache.get(tmp_storage.CACHE_PREFIX, tmp_storage.name), None) tmp_storage.remove() self.assertEqual(cache.get(tmp_storage.name), None) def test_media_storage(self): tmp_storage = MediaStorage() tmp_storage.save(self.test_string) name = tmp_storage.name tmp_storage = MediaStorage(name=name) self.assertEqual(self.test_string, tmp_storage.read()) self.assertTrue(default_storage.exists(tmp_storage.get_full_path())) tmp_storage.remove() self.assertFalse(default_storage.exists(tmp_storage.get_full_path())) def test_media_storage_read_mode(self): # issue 416 - MediaStorage does not respect the read_mode parameter. test_string = self.test_string.replace(b'\n', b'\r') tmp_storage = MediaStorage() tmp_storage.save(test_string) name = tmp_storage.name tmp_storage = MediaStorage(name=name) self.assertEqual(self.test_string.decode(), tmp_storage.read(read_mode='r')) django-import-export-2.7.1/tests/core/tests/test_widgets.py000066400000000000000000000346771416107567000241550ustar00rootroot00000000000000from datetime import date, datetime, time, timedelta from decimal import Decimal from unittest import mock import pytz from core.models import Author, Category from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from import_export import widgets class BooleanWidgetTest(TestCase): def setUp(self): self.widget = widgets.BooleanWidget() def test_clean(self): self.assertTrue(self.widget.clean("1")) self.assertTrue(self.widget.clean(1)) self.assertTrue(self.widget.clean("TRUE")) self.assertTrue(self.widget.clean("True")) self.assertTrue(self.widget.clean("true")) self.assertFalse(self.widget.clean("0")) self.assertFalse(self.widget.clean(0)) self.assertFalse(self.widget.clean("FALSE")) self.assertFalse(self.widget.clean("False")) self.assertFalse(self.widget.clean("false")) self.assertEqual(self.widget.clean(""), None) self.assertEqual(self.widget.clean("NONE"), None) self.assertEqual(self.widget.clean("None"), None) self.assertEqual(self.widget.clean("none"), None) self.assertEqual(self.widget.clean("NULL"), None) self.assertEqual(self.widget.clean("null"), None) def test_render(self): self.assertEqual(self.widget.render(True), "1") self.assertEqual(self.widget.render(False), "0") self.assertEqual(self.widget.render(None), "") class DateWidgetTest(TestCase): def setUp(self): self.date = date(2012, 8, 13) self.widget = widgets.DateWidget('%d.%m.%Y') def test_render(self): self.assertEqual(self.widget.render(self.date), "13.08.2012") def test_render_none(self): self.assertEqual(self.widget.render(None), "") def test_render_datetime_safe(self): """datetime_safe is supposed to be used to support dates older than 1000""" self.date = date(10, 8, 2) self.assertEqual(self.widget.render(self.date), "02.08.0010") def test_clean(self): self.assertEqual(self.widget.clean("13.08.2012"), self.date) def test_clean_returns_None_for_empty_value(self): self.assertIsNone(self.widget.clean(None)) def test_clean_returns_date_when_date_passed(self): self.assertEqual(self.date, self.widget.clean(self.date)) def test_clean_raises_ValueError(self): self.widget = widgets.DateWidget('x') with self.assertRaisesRegex(ValueError, "Enter a valid date."): self.widget.clean('2021-05-01') @override_settings(USE_TZ=True) def test_use_tz(self): self.assertEqual(self.widget.render(self.date), "13.08.2012") self.assertEqual(self.widget.clean("13.08.2012"), self.date) @override_settings(DATE_INPUT_FORMATS=None) def test_default_format(self): self.widget = widgets.DateWidget() self.assertEqual(("%Y-%m-%d",), self.widget.formats) class DateTimeWidgetTest(TestCase): def setUp(self): self.datetime = datetime(2012, 8, 13, 18, 0, 0) self.widget = widgets.DateTimeWidget('%d.%m.%Y %H:%M:%S') def test_render(self): self.assertEqual(self.widget.render(self.datetime), "13.08.2012 18:00:00") def test_render_none(self): self.assertEqual(self.widget.render(None), "") def test_clean(self): self.assertEqual(self.widget.clean("13.08.2012 18:00:00"), self.datetime) @override_settings(USE_TZ=True, TIME_ZONE='Europe/Ljubljana') def test_use_tz(self): utc_dt = timezone.make_aware(self.datetime, pytz.UTC) self.assertEqual(self.widget.render(utc_dt), "13.08.2012 20:00:00") self.assertEqual(self.widget.clean("13.08.2012 20:00:00"), utc_dt) @override_settings(DATETIME_INPUT_FORMATS=None) def test_default_format(self): self.widget = widgets.DateTimeWidget() self.assertEqual(("%Y-%m-%d %H:%M:%S",), self.widget.formats) def test_clean_returns_datetime_when_datetime_passed(self): self.assertEqual(self.datetime, self.widget.clean(self.datetime)) def test_render_datetime_safe(self): """datetime_safe is supposed to be used to support dates older than 1000""" self.datetime = datetime(10, 8, 2) self.assertEqual(self.widget.render(self.datetime), "02.08.0010 00:00:00") class DateWidgetBefore1900Test(TestCase): """https://github.com/django-import-export/django-import-export/pull/94""" def setUp(self): self.date = date(1868, 8, 13) self.widget = widgets.DateWidget('%d.%m.%Y') def test_render(self): self.assertEqual(self.widget.render(self.date), "13.08.1868") def test_clean(self): self.assertEqual(self.widget.clean("13.08.1868"), self.date) class DateTimeWidgetBefore1900Test(TestCase): def setUp(self): self.datetime = datetime(1868, 8, 13) self.widget = widgets.DateTimeWidget('%d.%m.%Y') def test_render(self): self.assertEqual("13.08.1868", self.widget.render(self.datetime)) def test_clean(self): self.assertEqual(self.datetime, self.widget.clean("13.08.1868")) class TimeWidgetTest(TestCase): def setUp(self): self.time = time(20, 15, 0) self.widget = widgets.TimeWidget('%H:%M:%S') def test_render(self): self.assertEqual(self.widget.render(self.time), "20:15:00") def test_render_none(self): self.assertEqual(self.widget.render(None), "") def test_clean(self): self.assertEqual(self.widget.clean("20:15:00"), self.time) @override_settings(TIME_INPUT_FORMATS=None) def test_default_format(self): self.widget = widgets.TimeWidget() self.assertEqual(("%H:%M:%S",), self.widget.formats) def test_clean_raises_ValueError(self): self.widget = widgets.TimeWidget('x') with self.assertRaisesRegex(ValueError, "Enter a valid time."): self.widget.clean("20:15:00") def test_clean_returns_time_when_time_passed(self): self.assertEqual(self.time, self.widget.clean(self.time)) class DurationWidgetTest(TestCase): def setUp(self): self.duration = timedelta(hours=1, minutes=57, seconds=0) self.widget = widgets.DurationWidget() def test_render(self): self.assertEqual(self.widget.render(self.duration), "1:57:00") def test_render_none(self): self.assertEqual(self.widget.render(None), "") def test_render_zero(self): self.assertEqual(self.widget.render(timedelta(0)), "0:00:00") def test_clean(self): self.assertEqual(self.widget.clean("1:57:00"), self.duration) def test_clean_none(self): self.assertEqual(self.widget.clean(""), None) def test_clean_zero(self): self.assertEqual(self.widget.clean("0:00:00"), timedelta(0)) @mock.patch("import_export.widgets.parse_duration", side_effect=ValueError("err")) def test_clean_raises_ValueError(self, _): with self.assertRaisesRegex(ValueError, "Enter a valid duration."): self.widget.clean("x") class FloatWidgetTest(TestCase): def setUp(self): self.value = 11.111 self.widget = widgets.FloatWidget() def test_clean(self): self.assertEqual(self.widget.clean(11.111), self.value) def test_render(self): self.assertEqual(self.widget.render(self.value), self.value) def test_clean_string_zero(self): self.assertEqual(self.widget.clean("0"), 0.0) self.assertEqual(self.widget.clean("0.0"), 0.0) def test_clean_empty_string(self): self.assertEqual(self.widget.clean(""), None) self.assertEqual(self.widget.clean(" "), None) self.assertEqual(self.widget.clean("\r\n\t"), None) class DecimalWidgetTest(TestCase): def setUp(self): self.value = Decimal("11.111") self.widget = widgets.DecimalWidget() def test_clean(self): self.assertEqual(self.widget.clean("11.111"), self.value) self.assertEqual(self.widget.clean(11.111), self.value) def test_render(self): self.assertEqual(self.widget.render(self.value), self.value) def test_clean_string_zero(self): self.assertEqual(self.widget.clean("0"), Decimal("0")) self.assertEqual(self.widget.clean("0.0"), Decimal("0")) def test_clean_empty_string(self): self.assertEqual(self.widget.clean(""), None) self.assertEqual(self.widget.clean(" "), None) self.assertEqual(self.widget.clean("\r\n\t"), None) class IntegerWidgetTest(TestCase): def setUp(self): self.value = 0 self.widget = widgets.IntegerWidget() self.bigintvalue = 163371428940853127 def test_clean_integer_zero(self): self.assertEqual(self.widget.clean(0), self.value) def test_clean_big_integer(self): self.assertEqual(self.widget.clean(163371428940853127), self.bigintvalue) def test_clean_string_zero(self): self.assertEqual(self.widget.clean("0"), self.value) self.assertEqual(self.widget.clean("0.0"), self.value) def test_clean_empty_string(self): self.assertEqual(self.widget.clean(""), None) self.assertEqual(self.widget.clean(" "), None) self.assertEqual(self.widget.clean("\n\t\r"), None) class ForeignKeyWidgetTest(TestCase): def setUp(self): self.widget = widgets.ForeignKeyWidget(Author) self.author = Author.objects.create(name='Foo') def test_clean(self): self.assertEqual(self.widget.clean(self.author.id), self.author) def test_clean_empty(self): self.assertEqual(self.widget.clean(""), None) def test_render(self): self.assertEqual(self.widget.render(self.author), self.author.pk) def test_render_empty(self): self.assertEqual(self.widget.render(None), "") def test_clean_multi_column(self): class BirthdayWidget(widgets.ForeignKeyWidget): def get_queryset(self, value, row): return self.model.objects.filter( birthday=row['birthday'] ) author2 = Author.objects.create(name='Foo') author2.birthday = "2016-01-01" author2.save() birthday_widget = BirthdayWidget(Author, 'name') row = {'name': "Foo", 'birthday': author2.birthday} self.assertEqual(birthday_widget.clean("Foo", row), author2) def test_render_handles_value_error(self): class TestObj(object): @property def attr(self): raise ValueError("some error") t = TestObj() self.widget = widgets.ForeignKeyWidget(mock.Mock(), "attr") self.assertIsNone(self.widget.render(t)) class ManyToManyWidget(TestCase): def setUp(self): self.widget = widgets.ManyToManyWidget(Category) self.widget_name = widgets.ManyToManyWidget(Category, field="name") self.cat1 = Category.objects.create(name='Cat úňíčóďě') self.cat2 = Category.objects.create(name='Cat 2') def test_clean(self): value = "%s,%s" % (self.cat1.pk, self.cat2.pk) cleaned_data = self.widget.clean(value) self.assertEqual(len(cleaned_data), 2) self.assertIn(self.cat1, cleaned_data) self.assertIn(self.cat2, cleaned_data) def test_clean_field(self): value = "%s,%s" % (self.cat1.name, self.cat2.name) cleaned_data = self.widget_name.clean(value) self.assertEqual(len(cleaned_data), 2) self.assertIn(self.cat1, cleaned_data) self.assertIn(self.cat2, cleaned_data) def test_clean_field_spaces(self): value = "%s, %s" % (self.cat1.name, self.cat2.name) cleaned_data = self.widget_name.clean(value) self.assertEqual(len(cleaned_data), 2) self.assertIn(self.cat1, cleaned_data) self.assertIn(self.cat2, cleaned_data) def test_clean_typo(self): value = "%s," % self.cat1.pk cleaned_data = self.widget.clean(value) self.assertEqual(len(cleaned_data), 1) self.assertIn(self.cat1, cleaned_data) @mock.patch("core.models.Category.objects.none") def test_clean_handles_None_value(self, mock_none): self.widget.clean(None) self.assertEqual(1, mock_none.call_count) def test_int(self): value = self.cat1.pk cleaned_data = self.widget.clean(value) self.assertEqual(len(cleaned_data), 1) self.assertIn(self.cat1, cleaned_data) def test_float(self): value = float(self.cat1.pk) cleaned_data = self.widget.clean(value) self.assertEqual(len(cleaned_data), 1) self.assertIn(self.cat1, cleaned_data) def test_render(self): self.assertEqual(self.widget.render(Category.objects.order_by('id')), "%s,%s" % (self.cat1.pk, self.cat2.pk)) self.assertEqual(self.widget_name.render(Category.objects.order_by('id')), "%s,%s" % (self.cat1.name, self.cat2.name)) class JSONWidgetTest(TestCase): def setUp(self): self.value = {"value": 23} self.widget = widgets.JSONWidget() def test_clean(self): self.assertEqual(self.widget.clean('{"value": 23}'), self.value) def test_render(self): self.assertEqual(self.widget.render(self.value), '{"value": 23}') def test_clean_single_quoted_string(self): self.assertEqual(self.widget.clean("{'value': 23}"), self.value) self.assertEqual(self.widget.clean("{'value': null}"), {'value': None}) def test_clean_none(self): self.assertEqual(self.widget.clean(None), None) self.assertEqual(self.widget.clean('{"value": null}'), {'value': None}) def test_render_none(self): self.assertEqual(self.widget.render(None), None) self.assertEqual(self.widget.render(dict()), None) self.assertEqual(self.widget.render({"value": None}), '{"value": null}') class SimpleArrayWidgetTest(TestCase): def setUp(self): self.value = {"value": 23} self.widget = widgets.SimpleArrayWidget() def test_default_separator(self): self.assertEqual(',', self.widget.separator) def test_arg_separator(self): self.widget = widgets.SimpleArrayWidget('|') self.assertEqual('|', self.widget.separator) def test_clean_splits_str(self): s = "a,b,c" self.assertEqual(["a", "b", "c"], self.widget.clean(s)) def test_clean_returns_empty_list_for_empty_arg(self): s = '' self.assertEqual([], self.widget.clean(s)) def test_render(self): v = ["a", "b", "c"] s = "a,b,c" self.assertEqual(s, self.widget.render(v)) django-import-export-2.7.1/tests/core/views.py000066400000000000000000000003051416107567000214200ustar00rootroot00000000000000from django.views.generic.list import ListView from import_export import mixins from . import models class CategoryExportView(mixins.ExportViewFormMixin, ListView): model = models.Category django-import-export-2.7.1/tests/manage.py000077500000000000000000000005001416107567000205630ustar00rootroot00000000000000#!/usr/bin/env python3 import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.path.pardir)) if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-import-export-2.7.1/tests/scripts/000077500000000000000000000000001416107567000204525ustar00rootroot00000000000000django-import-export-2.7.1/tests/scripts/__init__.py000066400000000000000000000000001416107567000225510ustar00rootroot00000000000000django-import-export-2.7.1/tests/scripts/bulk_import.py000066400000000000000000000114351416107567000233570ustar00rootroot00000000000000""" Helper module for testing bulk imports. See tests/bulk/README.md """ import time from functools import wraps import tablib from memory_profiler import memory_usage from import_export import resources from import_export.instance_loaders import CachedInstanceLoader from core.models import Book # isort:skip # The number of rows to be created on each profile run. # Increase this value for greater load testing. NUM_ROWS = 10000 class _BookResource(resources.ModelResource): class Meta: model = Book fields = ('id', 'name', 'author_email', 'price') use_bulk = True batch_size = 1000 skip_unchanged = True #skip_diff = True # This flag can speed up imports # Cannot be used when performing updates # force_init_instance = True instance_loader_class = CachedInstanceLoader def profile_duration(fn): @wraps(fn) def inner(*args, **kwargs): # Measure duration t = time.perf_counter() retval = fn(*args, **kwargs) elapsed = time.perf_counter() - t print(f'Time {elapsed:0.4}') return inner def profile_mem(fn): @wraps(fn) def inner(*args, **kwargs): # Measure memory mem, retval = memory_usage((fn, args, kwargs), retval=True, timeout=200, interval=1e-7) print(f'Memory {max(mem) - min(mem)}') return retval return inner @profile_duration def do_import_duration(resource, dataset): resource.import_data(dataset) @profile_mem def do_import_mem(resource, dataset): resource.import_data(dataset) def do_create(): print("\ndo_create()") # clearing down existing objects Book.objects.all().delete() rows = [('', 'Some new book', 'email@example.com', '10.25')] * NUM_ROWS dataset = tablib.Dataset(*rows, headers=['id', 'name', 'author_email', 'price']) book_resource = _BookResource() do_import_duration(book_resource, dataset) do_import_mem(book_resource, dataset) # Book objects are created once for the 'duration' run, and once for the 'memory' run assert Book.objects.count() == NUM_ROWS * 2 Book.objects.all().delete() def do_update(): print("\ndo_update()") # clearing down existing objects Book.objects.all().delete() rows = [('', 'Some new book', 'email@example.com', '10.25')] * NUM_ROWS books = [Book(name=r[1], author_email=r[2], price=r[3]) for r in rows] # run 'update' - there must be existing rows in the DB... # i.e. so they can be updated Book.objects.bulk_create(books) assert NUM_ROWS == Book.objects.count() # find the ids, so that we can perform the update all_books = Book.objects.all() rows = [(b.id, b.name, b.author_email, b.price) for b in all_books] dataset = tablib.Dataset(*rows, headers=['id', 'name', 'author_email', 'price']) book_resource = _BookResource() do_import_duration(book_resource, dataset) do_import_mem(book_resource, dataset) assert NUM_ROWS == Book.objects.count() Book.objects.all().delete() def do_delete(): class _BookResource(resources.ModelResource): def for_delete(self, row, instance): return True class Meta: model = Book fields = ('id', 'name', 'author_email', 'price') use_bulk = True batch_size = 1000 skip_diff = True instance_loader_class = CachedInstanceLoader print("\ndo_delete()") # clearing down existing objects Book.objects.all().delete() rows = [('', 'Some new book', 'email@example.com', '10.25')] * NUM_ROWS books = [Book(name=r[1], author_email=r[2], price=r[3]) for r in rows] # deletes - there must be existing rows in the DB... # i.e. so they can be deleted Book.objects.bulk_create(books) assert NUM_ROWS == Book.objects.count() all_books = Book.objects.all() rows = [(b.id, b.name, b.author_email, b.price) for b in all_books] dataset = tablib.Dataset(*rows, headers=['id', 'name', 'author_email', 'price']) book_resource = _BookResource() do_import_duration(book_resource, dataset) assert 0 == Book.objects.count() # recreate rows which have just been deleted Book.objects.bulk_create(books) assert NUM_ROWS == Book.objects.count() all_books = Book.objects.all() rows = [(b.id, b.name, b.author_email, b.price) for b in all_books] dataset = tablib.Dataset(*rows, headers=['id', 'name', 'author_email', 'price']) do_import_mem(book_resource, dataset) assert 0 == Book.objects.count() def run(*args): if len(args) > 0: arg = args[0].lower() if arg == "create": do_create() if arg == "update": do_update() if arg == "delete": do_delete() else: do_create() do_update() do_delete()django-import-export-2.7.1/tests/settings.py000066400000000000000000000054521416107567000212030ustar00rootroot00000000000000import os import sys import django INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.sites', 'import_export', 'core', ] SITE_ID = 1 ROOT_URLCONF = "urls" DEBUG = True STATIC_URL = '/static/' SECRET_KEY = '2n6)=vnp8@bu0om9d05vwf7@=5vpn%)97-!d*t4zq1mku%0-@j' MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': ( 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.request', ), }, }, ] if django.VERSION >= (3, 2): DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' if os.environ.get('IMPORT_EXPORT_TEST_TYPE') == 'mysql-innodb': IMPORT_EXPORT_USE_TRANSACTIONS = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'import_export', 'USER': os.environ.get('IMPORT_EXPORT_MYSQL_USER', 'root'), 'PASSWORD': os.environ.get('IMPORT_EXPORT_MYSQL_PASSWORD', 'password'), 'HOST': '127.0.0.1', 'PORT': 3306, 'TEST': { 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', } } } elif os.environ.get('IMPORT_EXPORT_TEST_TYPE') == 'postgres': IMPORT_EXPORT_USE_TRANSACTIONS = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'import_export', 'USER': os.environ.get('IMPORT_EXPORT_POSTGRESQL_USER'), 'PASSWORD': os.environ.get('IMPORT_EXPORT_POSTGRESQL_PASSWORD'), 'HOST': 'localhost', 'PORT': 5432 } } else: if 'test' in sys.argv: database_name = '' else: database_name = os.path.join(os.path.dirname(__file__), 'database.db') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': database_name, } } LOGGING = { 'version': 1, 'disable_existing_loggers': True, 'handlers': { 'console': { 'class': 'logging.NullHandler' } }, 'root': { 'handlers': ['console'], }} USE_TZ = False django-import-export-2.7.1/tests/urls.py000066400000000000000000000007371416107567000203310ustar00rootroot00000000000000from core import views from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import path from django.views.generic import RedirectView admin.autodiscover() urlpatterns = [ path('', RedirectView.as_view(url='/admin/'), name="admin-site"), path('admin/', admin.site.urls), path('export/category/', views.CategoryExportView.as_view(), name='export-category'), ] urlpatterns += staticfiles_urlpatterns() django-import-export-2.7.1/tox.ini000066400000000000000000000015621416107567000171400ustar00rootroot00000000000000[tox] envlist = isort {py36,py37,py38,py39,py310}-django22-tablib{dev,stable} {py36,py37,py38,py39,py310}-django31-tablib{dev,stable} {py36,py37,py38,py39,py310}-django32-tablib{dev,stable} {py38,py39,py310}-django40-tablib{dev,stable} {py38,py39,py310}-djangomain-tablib{dev,stable} [testenv] commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning {toxinidir}/tests/manage.py test core deps = tablibdev: -egit+https://github.com/jazzband/tablib.git#egg=tablib tablibstable: tablib django22: Django>=2.2,<3.0 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/test.txt [testenv:isort] skip_install = True deps = isort commands = isort --check-only import_export tests