pax_global_header00006660000000000000000000000064145351176650014527gustar00rootroot0000000000000052 comment=e71cd439a69ff27b886d9d5923b1713851694859 django-import-export-3.3.4/000077500000000000000000000000001453511766500156275ustar00rootroot00000000000000django-import-export-3.3.4/.editorconfig000066400000000000000000000006371453511766500203120ustar00rootroot00000000000000# https://editorconfig.org/ root = true [*] indent_style = space indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf charset = utf-8 # Docstrings and comments use max_line_length = 79 [*.py] max_line_length = 88 # Use 2 spaces for the HTML files [*.html] indent_size = 2 # Makefiles always use tabs for indentation [Makefile] indent_style = tab [*.yml] indent_size = 2django-import-export-3.3.4/.git-blame-ignore-revs000066400000000000000000000004631453511766500217320ustar00rootroot00000000000000# Run this command to always ignore formatting commits in `git blame` # git config blame.ignoreRevsFile .git-blame-ignore-revs # https://www.stefanjudis.com/today-i-learned/how-to-exclude-commits-from-git-blame/ # Reformat files according to linting rules (#1577) dc025e1c3dfea741120c11188fc7f8959df79e77 django-import-export-3.3.4/.github/000077500000000000000000000000001453511766500171675ustar00rootroot00000000000000django-import-export-3.3.4/.github/FUNDING.yml000066400000000000000000000001651453511766500210060ustar00rootroot00000000000000# These are supported funding model platforms github: [django-import-export] open_collective: django-import-export django-import-export-3.3.4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001453511766500213525ustar00rootroot00000000000000django-import-export-3.3.4/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000012601453511766500240430ustar00rootroot00000000000000--- 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-3.3.4/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000007501453511766500251010ustar00rootroot00000000000000**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-3.3.4/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000011471453511766500235460ustar00rootroot00000000000000--- name: Question about: Do you have a general question? title: '' labels: question assignees: '' --- django-import-export-3.3.4/.github/pull_request_template.md000066400000000000000000000003471453511766500241340ustar00rootroot00000000000000**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-3.3.4/.github/stale.yml000066400000000000000000000026311453511766500210240ustar00rootroot00000000000000# 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-3.3.4/.github/workflows/000077500000000000000000000000001453511766500212245ustar00rootroot00000000000000django-import-export-3.3.4/.github/workflows/django-import-export-ci.yml000066400000000000000000000052721453511766500264370ustar00rootroot00000000000000name: django-import-export CI on: push: branches: - main pull_request: branches: - main jobs: test: runs-on: ubuntu-latest env: DB_NAME: import_export IMPORT_EXPORT_POSTGRESQL_USER: postgres IMPORT_EXPORT_POSTGRESQL_PASSWORD: somepass IMPORT_EXPORT_MYSQL_USER: root IMPORT_EXPORT_MYSQL_PASSWORD: root COVERAGE: 1 strategy: fail-fast: true matrix: python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' services: postgres: image: postgres env: POSTGRES_USER: ${{ env.IMPORT_EXPORT_POSTGRESQL_USER }} POSTGRES_PASSWORD: ${{ env.IMPORT_EXPORT_POSTGRESQL_PASSWORD }} POSTGRES_DB: ${{ env.DB_NAME }} ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Set up MySQL run: > sudo /etc/init.d/mysql start mysql -e 'CREATE DATABASE ${{ env.DB_NAME }};' -u${{ env.IMPORT_EXPORT_MYSQL_USER }} -p${{ env.IMPORT_EXPORT_MYSQL_PASSWORD }} - name: Check out repository code uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install Dependencies run: | python -m pip install --upgrade pip pip install tox coverage coveralls - name: Run tox targets for ${{ matrix.python-version }} (sqlite) run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) - name: Run tox targets for ${{ matrix.python-version }} (postgres) run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) env: IMPORT_EXPORT_TEST_TYPE: postgres - name: Run tox targets for ${{ matrix.python-version }} (mysql) run: tox run -f py$(echo ${{ matrix.python-version }} | tr -d .) env: IMPORT_EXPORT_TEST_TYPE: mysql-innodb - name: Combine test coverage run: coverage combine - name: Upload coverage data to coveralls.io run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ 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-3.3.4/.github/workflows/pre-commit.yml000066400000000000000000000004751453511766500240310ustar00rootroot00000000000000on: pull_request: push: branches: - main jobs: main: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: 3.x - uses: pre-commit/action@v3.0.0 - uses: pre-commit-ci/lite-action@v1.0.1 if: always() django-import-export-3.3.4/.gitignore000066400000000000000000000002731453511766500176210ustar00rootroot00000000000000*.log *.pot *.pyc local_settings.py docs/_build build/ dist/ /django-import-export/ *.egg-info/ .tox/ .idea/ *.python-version .coverage *.sw[po] # IDE support .vscode tests/database.db django-import-export-3.3.4/.pre-commit-config.yaml000066400000000000000000000004071453511766500221110ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 23.10.1 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: - id: flake8 django-import-export-3.3.4/.readthedocs.yaml000066400000000000000000000007471453511766500210660ustar00rootroot00000000000000# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: "ubuntu-22.04" tools: python: "3.11" sphinx: configuration: docs/conf.py # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: requirements/docs.txt django-import-export-3.3.4/AUTHORS000066400000000000000000000070051453511766500167010ustar00rootroot00000000000000The 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 * rpsands (Ryan P. Sands) * 2ykwang (Yeongkwang Yang) * KamilRizatdinov (Kamil Rizatdinov) * Mark Walker * shimakaze-git * frgmt * vanschelven (Klaas van Schelven) * HaPyTeX (Willem Van Onsem) * nikhaldi (Nik Haldimann) * TheRealVizard (Eduardo Leyva) * 1gni5 (Jules Ducange) * mpasternak (Michał Pasternak) * nikatlas (Nikos Atlas) * cocorocho (Erkan Çoban) * bdnettleton (Brian Nettleton) * Ptosiek (Antonin) * samupl (Jakub Szafrański) * smunoz-ml (Santiago Muñoz) * carlosal0ns0 (Carlos Alonso) * travenin (Lauri Virtanen) * christophehenry (Christophe Henry) * bgelov (Oleg Belov) * EricOuma (Eric Ouma) django-import-export-3.3.4/CODE_OF_CONDUCT.md000066400000000000000000000126531453511766500204350ustar00rootroot00000000000000 # 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-3.3.4/CONTRIBUTING.md000066400000000000000000000003001453511766500200510ustar00rootroot00000000000000# Contributing Thanks for the interest! See the [contributor documentation][contribute] to get started. [contribute]: https://django-import-export.readthedocs.io/en/latest/contributing.html django-import-export-3.3.4/LICENSE000066400000000000000000000024771453511766500166460ustar00rootroot00000000000000Copyright (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-3.3.4/MANIFEST.in000066400000000000000000000002611453511766500173640ustar00rootroot00000000000000include LICENSE include AUTHORS include README.rst recursive-include import_export/templates * recursive-include import_export/locale * recursive-include import_export/static * django-import-export-3.3.4/Makefile000066400000000000000000000036031453511766500172710ustar00rootroot00000000000000.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/ 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 combine 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-3.3.4/README.rst000066400000000000000000000032731453511766500173230ustar00rootroot00000000000000==================== 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 .. image:: https://static.pepy.tech/personalized-badge/django-import-export?period=month&units=international_system&left_color=black&right_color=blue&left_text=Downloads/month :target: https://pepy.tech/project/django-import-export django-import-export is a Django application and library for importing and exporting data from a variety of formats. Includes Django Admin site integration. * 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/ django-import-export-3.3.4/RELEASE.md000066400000000000000000000041411453511766500172310ustar00rootroot00000000000000## Release process #### Pre release - Set up `.pypirc` file to reference both pypi and testpypi. #### Release - Ensure that all code has been committed and integration tests have run on Github. - If pushing directly to `main` branch, ensure this is done on the correct remote repo. - `make messages` is intended to be run now to keep the translation files up-to-date. - Run this if there have been any translations updates for the release. It is recommended to run this prior to any minor release. - This creates updates to all translation files so there is no need to commit these unless there have been any translation changes. - If 'no module named settings' error is seen, try unsetting `DJANGO_SETTINGS_MODULE` environment variable. ```bash # check out clean version # all git operations will be run against this source repo git clone git@github.com:django-import-export/django-import-export.git django-import-export-rel cd django-import-export-rel # checkout any feature branch at this point # git checkout develop python3 -m venv venv source venv/bin/activate pip install -U pip setuptools wheel pip install --exists-action=w --no-cache-dir -r requirements/deploy.txt # zest.releaser pre-release # (you can set the correct version in this step) prerelease ``` #### Perform the release For the first pass you may choose not to upload only to testpypi (not pypi) so that you can check the release. You can check the release by manually downloading the files from testPyPI and checking the contents. Once the test file have been checked, run again to upload to PyPI. ```bash release # resets the version and pushes changes to origin postrelease # remove the rel copy - no longer required deactivate cd .. rm -rf django-import-export-rel ``` #### Add Release to Github - Go to [Github releases](https://github.com/django-import-export/django-import-export/releases) - Click 'Draft a new release' - Enter the version number (e.g. 3.1.0) - Select the correct tag - Publish the release ### Check readthedocs Login to [readthedocs.org](https://readthedocs.org) to check that the build ran OK (click on 'Builds' tab). django-import-export-3.3.4/SECURITY.md000066400000000000000000000003661453511766500174250ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability **Please report security issues by emailing djangoimportexport@gmail.com**. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure. django-import-export-3.3.4/docs/000077500000000000000000000000001453511766500165575ustar00rootroot00000000000000django-import-export-3.3.4/docs/Makefile000066400000000000000000000127651453511766500202320ustar00rootroot00000000000000# 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-3.3.4/docs/_static/000077500000000000000000000000001453511766500202055ustar00rootroot00000000000000django-import-export-3.3.4/docs/_static/images/000077500000000000000000000000001453511766500214525ustar00rootroot00000000000000django-import-export-3.3.4/docs/_static/images/custom-export-form.png000066400000000000000000000704461453511766500257650ustar00rootroot00000000000000PNG  IHDR3xpIDATx^w׽{뼸&++7+9''d%\'q| xy4yg!1 !HČ 43HߢT-ԭYYT]]]]*>{׮{wsCDDDDߢǁDDDDq qHGDDDDǁDDDDq qHGDDDDG κyϥ粻*=UWSG݊ )刈8< {ܵ{m)K8RJX}ox;yʝƂh|֕n~/|,x↲eZ7m{YJ"""P%wZv+=\9pƇ~Q}9eվW#eu2eY߹k}юS)s垊:DDDġ3=|ۺgNww=w{x-;^.46 )/7{kxot;̯q3z`<';:]hMR=tooXﮆq5TmFO댾2_c ZI ڗ}7vsɁ>|޿U2Z܂>} MwwGS~i*屮R..rkNeN7榛9V~? .tJ. {5,Tuz2WS(л.-iSn"""PL>o1o8 AfsV6at+j+Xz42<7I= _],Q]ew?hZhϾ'} ADD0U@ "~z;? wBA+y} KS5>HF󽭥~ c&57݆ԣ-Իl?n{ExXITpJB4"nzd>{o/l Ba?-[+ B<]@}0}oZ{*k_ z5oW4Ge {] ]=%q4L2gt8OA RC t(i? 4 /{=T )_WTHU#8y i~WS Vмr/h ꭍn8y Аh5qfWiVT~ٕ /z ]-HjBx0`%:s(_h,M^tc—x+_jzVnPϖͺ+7@ vW <4)ꋂ> -UcMo@ְ#ٕ.DDDġ_7W2ovf_9VET.28 Qo.\dm t[aJ[*.=5o=邿^@e67]Щ퍎jC}~ e=g:wٽZ/lR+}+}iBܫioBW{t}?ES^ 0'ӆ,m(To7vk^Wu7_uש_CtUH?ͫSz j\?i"""bD꡷a /Z.oUSf4^荭goxznhAS\U5 >02=__۪ǟuI]hzkY9h>ts~+_]OQ={c9WftC U2]v3y8 ٪>㞵>4G挄Ǯ!=Su2 ʤ^#q+XϦ9_8i =hh_?u@cEDDDƜ cpGDDDdqIGDDDDǁDDDDq qHGDDDDǁDDDDq qHGDDDDǁ"!-Q >O >O >- >>|8d BGD$ /DLD?@1 2*ʕ+ڵUVVerj7}tܜRgϞu}[rw .͛7cǎqHrӦMs_~V8j̚5+uٸ{nlHyy9F#/\Нd[BBHP2/..v.]CtD RH~V`U:=ïk5^kphȌ>֯_Օ kږU ׶n۶k~>|9s.g_x1e?mڴiSxYb/h[>3]׶zGhY5XTϦeHжom;rHP~~p%ˈibuVgzvggWF Z! ^F! -OU޽{ݝ;wRUZe ZV0+'浏Lې#={'HJ ~;h(WhO:5*_Cb;ЫmA[e(R `;vuQ0$M[)?'z훰C ׼G---)C?@h{5V]aSR!\e6FϞ׼ulp5E_nMP(\ClY ]ոP}6_ zݰ)zԩh^h_(k[4_7G_' (DDqѢE8c _5Fᦿ>^}_t(`$ˈI)2tp^+(+Hp p`=2VvZ_~ 6۫zo[#"lO2 ZBKJJ6{Ι3'~@҂ݛ0PzRmSf5͚5Ѥа#-R^[1 2Ԙu$MQxk7?POz˄ü@d(iB6tG_{~ng;2F0 .4b6eԂXR=ӧO!\?(K׋?T5I}[,_WtAǾi*<^54'!b-2===ї)EGıg.33a„P oXI>}>}ڗ7>ĉ'~kC>=4BnWݍ*~z̙ȵ~58`@ȕ} ffYhµzم~OD}w}_V~Яl[V?%Ke ЏFa+=/跼Ҩ\!uMߺu+6Q{y r{mC:%7u#r:?$FOzܹ~+X+|=B3 IJ{/ 0Rws "ͥ?1a }ȶ0/^p!~ PәWUUzPt7DL| 3uTew:֯_uKGz=G DL| J?^_ {O y7GV,Շߩz #bbcs.]o^7\z5(Ns Atن_e@W`AG̥௛{LQzz?n:>7(4G~V\gĉ~,e5_T{n@1h(h~@ DL\ zgRCylMF\ ѐzz >+wiΘ1#*~7cB m#!oG zjwC`eÿ -z~ I'@|11kׯn߾ gG;N׌}Sq|oI7ٔ\s+5$A_oBckxRpOw==S޻_'oy^2s)vZsWS@Pj|]}>ot}Q_WVPs~M'=v%wW y^[y?elI~WPH4'Xt|:tAKM?r7}o}_ŸT1M wZݶ`^ET5? +/e*u궓n4O^早~ KWR ok}d/xӇvG?ÿ';]i}_fo}!(2 s ^p/oM#>Gs ܑgMpL;23}o ֣jشg|ῃYW{mzdd{]1 h?u@ v#9o*EF4И{)˙BT mF>zhhHkW^}igWkm^zU607ekߎBl$H*h a3߮: ._}Qq uiLa;Z^襾!2`at k3#~ V`V=bɱ`]i6Mt_CֈʋJX?@|$Ӑ kJ/#VŠh.c >MGC9Ppgwuϛ#9௞ijh,>Լ/Ty0aNDXXv5׋h 6MXH`;\6/},_njq]/V.V% / zXXXwElj͏w%, > wQڴ+ 6G wcQԫH^].l̺]N4mCTf s>z P[FhFe½Cyhֽt꣱icWK7;g&(_/kDweh~W#/' ^5#=R 'ӌlKۥ7௱'I /kb}$cC~9*2Cn6{ض 2E0mytC\௠.~hIѠ-?<& +=NF=ZQn=4o=D''  vs9@cFÝnl`8m^h5z}.G֝{+T}4  g-~zu?n2oƍk߹'<{[;;:`D%G"_a^r {pZCK @cFÝ~ο0]8ਞ`PXգU7}n;}v}UC3z#l4I +rGr5h4/^fU Ro6Gttd8Km3>o?ݺoGXgG #bbI?"&& > #bbcH`$|H 'A|H 'A|H 'A|H 'A|H 'A35ws*p>ă{nÑ IWvy]`b*Z“njhmυi d&f\k0HeOrU 7Uz_?F|X@ g.#UunվlƆ` %߫V$_zwA[PS/yԗiZZW>Y84lÿwlszUcgܯ?^~>e{NPear`]c L5E_0ss7]w>Lףnڊ]xnJ/:~cD|0xkіhU?RÂPϤXc+/y5 -oaU{|teYk<ۖz|?h 0lt:tu jAƶWZW*/_wkE{$mA;Kry , pq19?cAsǽh;vI`|= py_VWfm=cѨȫ%{ cD@`0yLOzSA0&- !5(s!DLs7y~9C8pҾj V7Z:|j)D$Ջn<)TXH5_ejyq5W<~po81é2F=bCP0&?|R5 >kZ7˯/5 F<`Hk (*(sʃ E_U×i\9!98dQ м_ ɠ_cmE}۰5vmg}Y7S]Uo:tUO3V_eKG~4WVVSN:@"$;`n!To #P @hCzGO}XNet߀௧2 2vA=ѡ>j /zoxȝFjhh jsϞsʭ>5tO:^{5ꫯ>,X={}bg$ 5͵٠~WEb_z? uH>Cq 5f'I_XcG oq,~>2Pv˖-so_n֬YJÇ}_}UPyf]QQ{E\ϳ+7ot|0a_joeU_ ø zcC 9r|E~+P_(+>}Ը)S{)+曮+ruuun׮]~~ҤI;v[xW>}wǏu'Otn֭>߸q mG}Է0.wv>QWwo 1]޽|~PZ8qoh뚛mذ:::@~Cȷ󉂿B#?XWTv̙ gHW'o۷o>tʣ ??v>4zKKK2}v𰞡VΝ;}CChH 1|)3g1W^uMMMO?] 2ew?_UUfi9? 1|2PW1crԩ~|թL7憗) 6_A֭v}`3m4_w^_o1'O<~4к=Poւw !@bp>?$RGD) 5>qI?"&& > #bbI?"&fW[[]YYYުϧϩ 3BqSSV|܁O_= }^}n =Qy 7 #晻j٧֍5)kxd~n\$_x;Q}_Ygg;XQ/6,sbuYpm9v:t]}d=ۆK'_O1a?\)9{߱/7]]}Ǫ޶u#7 :JG $S.S{-9}kfm) z֯ƃ]5ewM6\6uԐ8j[XJ{~ZߖTljJ\wT51^?7@.Xp|WRNsS 1SRʶ!/zr-StGW?,,2# ɒX7͹B7Fmkp7O)7@Pc<` D-mXj :Z>5 ֺgiO4]~=---uΝ1cFeFp(l>7$Kǯ5uMMMFWr?$F>O:::|pss@n@ȷBz5=_$H 'A|H 'A|H 'A|H 'A|H 'A|H 'A|H\8hA?$ @bp>?$ @bp>?$ @bp>?$ @bp>?$ @bp>?$ @bp>?$ @bp>!w7 "[?@|11 AG$DLL?@|11 AGۿA|y[뭷4i/S2e[`/Wѣ߿ꫠnӧܹs7tk֬z!H۷ow> ă-%nR)ua?Q=TRk>oQݾ}:CEEE77o+ߺu˗)1n:WWWΝ;::5>#wE?H__XcƌڇB_VSSl mO/_ѓ<-suM'-q'm1ۅo_\\ ߻wϯ… ʕ+~Z^k[6j>X𯿫z#*sZg^!ߖu}w>>>ןkoӖ|'u5.Z:˼Io!T^a?5 _?i}*_ѣ Ov8 _$Kk2=o9]5P^t2Z. "&ow}ׇjކh̽¼q2c௡?CF ?DI$[xo?dgnAT^W6[S?_勏GCʾk9{-s}z1^)볤?0a6 wu@!`A]L6/൙~#@=w /^/._~͞=۵+A"/k~. 󱆀y@ux O_M al=1>)oܸ1a~ڵ`|z]*!< j kϟ?x=>zѰӧO=EHe`|HWcd/7? 7ې tcku2G.g\YmAsTt<_ʸ(]D;/k~dzd_kWx>־PF@@5$A"_jyɠwyȟw47{_cշ2߸kJ]eP!nP b#z=zz99݀kOc:z=S % /ۅIĂΣ?Ѻ)q O#W?@|11 AG$DLL?@|11-wttZW^^G>{011)+)555h5@{W 30{p!#bbSW,=0{p!#bbSטlтC #A>Of0O&|`$ @b` / t>!h F?$F>Of0Ol766cǎ~mWWWy𨬬t\{{{ Fl_ @b$`K/={DŶm܏cNB„ Q&@:t>6V-CcE.0Xt>6իWoჹ_OO|M~򓟸﮴4l~ v֬Y~y9s='O_?]xoUUU_ӧOv>7ntuu?Ν;ݯ~~4WQWJLuSt`( 1|m0 I&p}!9Νkjj{(;;;pWWּ[_gyfwϯ]և_=zDIIV#E[|{}WgΜq}}uuOXteF/t} ݜ:t>6Cw=#l2_mpxڵ̰1a.#5[ZZׯ{XjUt명۱c`=.]_@_dHGn]a+KO,[^4c+8s1Z0nXly(7>gx/Z婹zshqZF|$pW0VOud_!6FGGWh )>Ba_pS A=(PQXX40z^o+9 ~ wls?4hL\k;wٯG P|?:Ux_fS\]HO$`f_\oݺuѣGNay0citDs$g^bEPwqD/t$;> B,t%ȀrAZWoz f߿ïn#1uëG3 +!D:+3f7{T= _|Epc |@e.ÿz hc/~ȏuZһs+,uswqvaNֵl ߠ`.-}̗k}{4?3Yל98=f*p1豛b?f~1utt!?^:^/&K8?їy湷z+xڠА j:aÆ~_mZFOғm\M00~ґhGO'H;R1oP^IK}/glf83 6ka?!5zʺA׍nz<.[LJ$5mW6``~HX@-gі'ݞ2׉o}ї5uސmպ^D1K8WgmY޾j8 V7Z:|2 +/!#zvׯ_VC~HXW W8hPضqW<~_CWګ~^]TtP@ƞ|Rz! ?{y;5 >k:\W}[y$`#A"&06D`kq Mkі௛U|}טd=\w41"]uâBa?cl atlNYOQcFyv㼎 }P9]qa@y~HH[Wxϗk聆Z˷@_.0 :}|E郿͇+ ROtϪN0vX~9S| ݔc+A=67w=ܳr+WכF_0$A7x DwwOhh1  5 L/qOF=ֲ;*^cXØD׍#W~ [t>kWV! =I+} #' gUPӘlO~)O >O >O >OcHqxR!"%DLL?@|11 AG̅1W%#bb|@G$DLL?@|11 AG3wqOk#࿿r>w[~=hmKYn)88}ٔRN,Q`n n-NGmtn\PYw)$Gbf)s}h.;xЯ|o!C RܾۜR/WXɢʋ>ZN?@|$l{{?Β)E?Wo%)a Ibw_8u ʠdu{uj+,qv]]ױ4gg٠.Wv/1jMY_aк회Ǻi*Ny_m/=ẖv7uvb|pYcGǐڹ;cĂ.9{ o.5+7}P8Szk,:/8]_EVKw OR1ltogg?t,\~:^_W|.85_ |ʝP_OG?z j.zptO׫^ 7ۋy m.y$xo+[}褻zlniMF\P=zL_כغZ 2X>'G {Gb?<ԧ0j(Ý~Z(P1w/}7|tԴSӾ&ˆ.ֈVRW7@D?;p0ߴO+Ǐfؘ2T 1W$@bD_uuuܠ+ 1r|~1$@bD#"b>Hȅ1zDDDE p>!#"bHȅ E?$F.O/} ݜ:p>!#"bhԹMnSnM)wʭ"% ~ u7%UtwwWE<5Wo.-NHOF ?""拉_᧓f|``3/hU?>~◫jW0Rфbbc>_߻³DuNj}٦cgx9C Ʒ 17M$Cwxﴴ7>_>r~1u~H8{gKܱ}yEUorr2Xw"ϧu(u_y0?GWSA5nƆ~5,B;e?v|ujKvk.4`oVa=vVy5|^pw_~0d!#"bHtmV!Ŭr  ꕴK}|뮟ו+|+.-]5 PUTzw 7puݢ}5s~oO۠qϚ?v0 6gٷoJەgǦ j\] hծuV]&/~6yޞ~CDD  e?h ֠'ֽ_do}ou@m]~~k|=]xg6?o-oݣy5n.^oӺrp20R'_O1a?\)k]{LnaWvWׂntӃW8 #q>mb ٩EGD 7t=a½aE4xkʲR](ش2 ^u) -a>Z—+4U Sn84?k7wUWzM+LOY)HOF`G-XDDH6LBPPpxay·S svn4Vh8mм{[yjt#D"Sltn8Rѯ^{Ydnv]UwRGO{+hMV|5t#eǯ=B,ic 8xob_*(Az w   *;tGR7ZB*ؘ~w=U{\U z=!Ș0XMņ d($-fsAF+w,Dc@O1tLWҢ؍jh (sϞ޳!y:ftO<dKDDC!?a4N>< ="{#誁+P~$LOQ@+E?bѓ`n{R Wp;ǒ 5{hOF2F%""1B=֛oCх٢uںN

!'ct_""W_WRCr=5hn~YrȒ\81/qc\81/q 1r|BODDKȅ ?/#'InrJѪZӷgu,ۖ5_i\qu[Sp-s^&|C嵞GfsF?U~;4=.oYlKDDH\8$۠Ǩw>Mͭx[_^r^^_7o >KǧkQmHYg4^}喟W#D&Ǹs)DDKȅIR_= goez ^W~|7,e j_k?xYC!êaR~JJPKDDH\8$Ŋ%5O%o6ewk|_}ߏr`9P 1r|Tqq z~\fwss5Kn{,௡< [{mDwُW>\FGD+AD߻6ٍ WSUo C}tB3?N/nR'&#"DLLf? nU|;]&?.K 5!@u7uT< DIϬ[{`[_tʗ}_vy=G7ꪀ;e,?{߮l>7?gl넞ٯ2?"b~HG$cc򍒚~?t~hxζʠ^WhK2/Uo ;Jf ecY?"&&p'_nM u\5/q11 DDKG$c&/KDDDLL?f2/q11 DDӵ-Gf7Z7$c&o;pí8PV,q/]WfSko?]^pQz\쾲s)M|})uĂFXrtJyT5:W{-SILF%""ĂºO'ǫv:-ƃL 6~O>s/`irF_=)uP#6ԅmikU\lHE}}<dt_""M,^\W#g/_ؗ+_uYݗ8|   Ѻ@|oCh l(*'u%;W5eǺS|pg fcAW4w_o;6` F^M_o/uR-`X'B?"HSz וC2Vj;uf Z]Az:Β)ۀk.fǀ aiZߖWa^DDıg"_2 9' 5rz|{OiUo_ի?d6_W}5kyVt%@_Z/t㽾R%#""= i_Vٶ3~+hw{`o_5<_z~"MG*qm7o?t;5N?lrWoÀ㏈86M,K=6P[!A*0l>Zk5LGmP8}y9"gn.k8Zu)esM=Mꇼt\$Q{nU#`DD1h͵!?#2ttth9"͵wj c"#DLL?""b|11 IG$#""'3ccc7-#""E ]K OBDܗ Ǿ_៞DDe ¿z6#""DLLO RGD) +CD #bbI?"&& > #bbI?"&& > #bbgkxͩ7Z7$Gb>.MYU߿[R>RtbuYw(ٙRIIj㑔:Ys)$Gbws?4;św,_.GG^?b5 1xxJ]X5ε\Օ:Y\}GIDϧqQIʲG{q:~k|>->, 7I4y)`p9Y[׻O/-O3vu'כֹ?ףa _ӭ'܄Ko>[9ѫ  j]&IᰟɺR^kܼ%nM?\?c@ߦ ym2_.ܽ0I4+D>XQ{0e9` PECy6W>T -{J77}Ê,+Ѡe.uZvrmu ImDLL7MǶƮjZ/GDž_ ˕U;/G_ Et9i;^~z&#yk^ h%+}0ѴaW?k(2~,Vu-ŮaIHI|= pyFwQZW4mW*/ ntӃ,ٻ0I4kMwB!pkZ\늮_=.kiʾ\ۡ!CZ@xX3u鴆{+:&5o˫lOoəj$GL# {n.`?7ӳf^_|c Rȍ޾ ͽϥ}{ֵs՟nYYӴ{@QFo_FoRl|"/g};?ܜFd?~D5;h~fW:g,[ϧmw/i{fioN||Χ'KbcǛ^^G_I'XБkϳͿs1׿.m+Ζ.߸cu1oݜDZ?Mz{e?v>T` RqKk\G2) Sqg*(%Ƕ'~*Ӷ7_iHf\>>cq{R(f 5.~8,M._ (}O?ž_t]{g)>y}(YԱ著lk\ws@QWl8g#Q7 "xxXrYq;o''cg-C, ^ķ'Oy2g.>9^>X}Y)}50Wl?]_l,;ErF\dTJ;%=U4r9糎9qv_1;ErF\!nţ8scķWHDDz([G ~Ez?_>8<7껫k {jvH.|Xۊo?KGxOx1Xžyۇ<i_]DwcY~K[/S>>;y}*SbY|]#ϝ/:7.ԍ1eh; M;uyZP0rB7[xSb([`%Fy>hƧo"(=(xH"Scp>͉ظ&mh|)mً>vJ.ӀT-킝E"wbѾM8/ny񺘂>H.p\4(K?Ly8}(1:^85qcN&=tALﶷ8/;ř>q!s{[31?rEීZدobJ[6~ ~skmʩV%qǐ^Y'"@9Պ܏w6_gS)돞:ݬ۵?=>K/l9{|Y3kUsmH(PN׌i[QsV5GON<؅ypfM\%=oSٖ߉uppbD\(Jbi[YykI+tfL7vmgy#'GO^,~kmkE\(J>{.MYs_K9C)nk^x}[qWZwD:"R7?S?2mv>s7C)!inѦ͉ӗps%"@9ՊN8;?C)Mi7W7L"R.?S?tSzW-{iw,+5.O3mَT)\gΞ.?X7]~sƿ4\v,[ՎG.j~8āwG߾fʻk.+_w9F*?rsc^^{'K;,Zi?%͇h>s6M e/0kV'tm*- mvsI\wM|E¿tg͊;:]=Wuj9H1צJQ,,DAȉ91'v/8FۀXdUtXa-L~']+oܞCN>qKQy:?.}v?;w#PźqF?ez1-Zt%d?3@sWn]ZĔG`90G⛀Ng1.E-ۉb`pUl,QbA)A1'xe*([ۙZ`߂~Ç_Jӑ&i{ڟqg,H<.W`R^ '6wǷ1?Ekb?/MK4 s`dRC\$By8rTZ]g7HQ]}i]lOF]| ~[\pMԞ<(w/'OYTg1?$]zEL 3m/Ʒo[\N_a?;U x zn](G1@9?PQj'PTc<r Ox(@5(G1@9C*""!`<r Ox(@5(G1@9?PQj'PTc<r Ox(@5(G1@9?PQj'PTc<r Ox(@5(G1@9?PQj'PTc<r Ox(@5(G1@9?PQj'PTc<r Ox(@5(G1@9?PQj'PTc<r Ox(@5(G1@9{CsMIENDB`django-import-export-3.3.4/docs/_static/images/custom-import-form.png000066400000000000000000002577131453511766500257620ustar00rootroot00000000000000PNG  IHDRECXf iCCPICC ProfileHP}$@ %&H'Z(t@B 1$ؑX *"*EĂEb .lX aw߼杙s9wgP8V S$DA"-qbfDD(@mAQi3^F@('󲹙(GSX"BKqnCY]qNqN9ё,0dG YshD<y23x(B<^:*x,Ff38ߖ!a.@zezӳB,J>BD dA1Sf%N1؛17tSlE);zQS,ɊTJS̑L(>[Q?O79عS2R%H|Qt3r^![W*R3=?_ĜΉQ䋥^E>?#PΉR앢q##p\\!XYea@JgOgvOM4 ӱuh9(\pojt QY @QL3Ì` u 169A4H@&`X A1 508 )p\@7 9x CiC)d 9@ BH(JR!$V@bBItA}0#0V`3x6̀p /S%p\oja9Eh" @XH8 dR!HҊt 79ah: `%ULӄÌ`b)X]5cSK2 Emę\qA\n9 ׈kqx<^oć9x)?@P"DO(#n cDU)ѝN7[׉1dN$EHkIEC%%%#%7yJB5JJG.+)}$,|FG~CP(>DRK9OyLLSUf+W+W*7)P~BT1Ua,RS)S9r]*QLQ]ZzR(FS3%:^L_VOZ? X4.mm?"m@nVOS/V?ޥ>QqZChi5347iռi 3fܘ^k_HQ'mvfG:+y:Kuv\y1S}L̢Ggׅut#u ;B_SG?Ma`Ygt :A/_ v5=2&3S07oJ4e Lwv7373[ol6de637hAXbQmqgɰLem[9[ *[.B]=ffUϺkCajچ6۾m2;q9e{`flojʁPpˑڱ񕓵iS39ysWK˰kk]:#Q¸uu[v6uCsi){%y%6xW{?1dZ2Ә/}|%'|߳Y+Ym~___Ԁ@mAؠ-Awzl.=2B9$*"IU$5  p\p()Gᏽ{6UUt?9P|AAyMdͅZ:ݺMp~ЂC݇44ml,>Ȏ<9;GCck8nzDQԴiY,oIh9|գ/ 581 1024 Screenshot r@IDATx|TUOHoJ{G**6Ăuuu]{]v׵7PDz]z'@h!H%!w^^f&w3}s9 SB!      (*͑ h4E       @ y$@$@$@$@$@$@$@jDȐvGVqxHHHHHH x.,%LAq_s9 2dPh{$e=^@irUR-| Q΁$|5/4iU'S.|D~XT]B\8}     b:])eX7Lޝ+uHTgwT+ d|Heh%WyyBOrBZ>&;tK@e7䓯c Te'7')Ԃ\)bnzw+qDV?`/<*uYu)Y~^Znw4W\_E>[qXIׅ y$|HHHHHJ\?Y'&ĿJHIyd^iVwtFc X!P|{jSޤ7.     ڳK<z_p/T =!meJ9EjߞHxIA,z|ۡA {Lp|6qY6IԼ]Ii~ʭl%Z=;eQ~T=߃H5v,Ϣ8{"E}7 x( [**uQX1ϦzoE窪 <{neﺝ[ʝ:?=tR2ve y1\5o(⵪S$t`KPJx},LfLpڸXiJ_5R+Xus6R>:{N[5#zL % Te;"U1qL=F H'(bݞP߻u[(z<5&qn{P~!tf:~+/@eXQeܱ{Qok<&    =>*s\nS+lZYZ 﫸|+P߽iV|@*?wcEYLq΢ +˿s[B!}OWlQJyZ8ϨUs6+W)(k0\ϜnJVeZxCSJ~=;Guj|tYb@{{s޾~`Z}v%QJmQvGk;Ptb8@*Όc- 31 Wj؞ \ܝˎ&G(}UgF$ջeJLqgQ10r?ap|߮f9 Yb!H*W 9v@t,XWkRO}JB"C%+SY1wUsun!]Hw5=$M)XY5F$3qԮRuw?uXq[eVLmgԃoSގS4J׉ۨtIx>i_5ҝ,<k$@$@$@$@$@Ai)m?ɒlޟʿ-KL%꞊ @zWd{ 2CGboX@w+ aێYVc `Go ,]U6Ź<⌳tHӞ$ Lx&z,'    (|b9&futm5ܴ^%!歶ck@g\vAtW}~OyF =#="I!%X60On^Gʃ<6|=eJp8Hs7]l(Rga09̹ol[`u' J%[     M ^o׊!zgRp#^>T,)zg!*٠U@c'0RPױKtBP[yW#KjnhmY}T^|Wc8}o3\crm6wP/nD+EYlwϗq Z*K} 5G$yhF-[)jqP+y ZHdjV0 @ey%y ?;k~2 3Wͼ}- _VGk)FƻcǸYY ]"ej Z&ޓhϽcҕGJ%:Jm OHHHHH|pܽ]* .pGL! 7|컾Q ~;0>kd_U_gʞ(Plz=0U zy{]3ϲJԘ )WO8IG$x)SS1xrPHHHHHH3~#soL,48mUX9/N;Wyk9!DOD?KP)γ {W>؆HHHHH|(WKXV΅x.y3dio^q ߹7 %5J)3S4V?!_{ƶnB~Z\C HG#+~8c8& @&NqXC &>gOtΦʲG>/Io`!C !!HHHHHHrJ斦%YZYGQHHHHHHH\y<~_s"qIHHHHHU@HHHHHHH |HHHHHHH"'NըxKOSCaH)"BuR%Q}@DWuELZŬCÚ]=S *4\BC!B֧e]dUnE(!AFZԩ&*,iSE+䯌_z+9J =!]E:hdٰ'9VۛBeO{-ᄚ6aYzbܭIrW?.ʵ[SVibŽqrRW2FWCs}s ]Vu3%<1緕hߴP `P]7fg:A( Tc /V}go Av'UUB8ve@^($@$@$@$@$@$PPz<|i|Ua(l(PwUVntR_)PVPV^-l0H@+ӽi l@uD%l<#`HW1׷Zᬪ60|4}_(۷tT uePoR|ëa@2Mfg̀7kxKr`|Ka^xG櫱+xq mUOjDcgzj+`,@ٖ}+F&\o n/hP?#x5PHHHHHH,L_ZjX]ݛ|UqZm.3+8*dBvVr i?&]rٶ t?(v3=V̹ߡҍ+yn(vٮe+Xv&v\*JŒ$Xy0U+tk^_O mj+θ_ƁJ¦Ο0oeh(h(왠/ HČe:WF'A;RPHUl {~)O#єHHHHHʒy@Y*>(p݆TV ;F)gݎ@{CMg(| \!p ߕt&e{~3+oS?Z{%gB0wD38( 6NOL]g͵¾s & CEZ/uAJ0<٤r=d>vL T8ۅrlU0i%0(c$KyT@@!     o!-UK^gÒNx P6ZJ]:w^\킭HrunC V)@qUg({o^])XAw~[)Ċy`X.kφ*qrHVѴ%fz<j|}{&f ֞0$!愜0@L@2 HHHHHVdZwR\ӷ+"*\P&"U}wV1UT2;ĸ:[8rSn}5T ]\ ?삜G=bk)^*%[u칵~[i*UAW ;JvA> vrSv]WTmlݯWvj$NMaat!*;?٭+裰gb9oc0|`'$h{1 0ffDƅDڡº3,'     }nߩL#{PJpBxߨpAwҝ''H&8J)V(G*hq'ZFl)lcŀr:cJ$ӫr9]v$I K dt|?oܠ0n&G9?c.YEګm6*WlQ}[ bڝڰs#<SHfb+w~a Ȁ['gSU{~ M2CCKƋ΍ceƚ( @{EvݤT,αVRzq@v#`gBZ *'LN?P[eU~$ʾ] VΤژ2w7ưSBo1n/r8c @qx@.%)Xŧ$J!VN:iMvWR®PEA.ݰ&pw_ )׭xHHHHH$^gB$@2!$U|yB$@$@$@$@$@^D=#RN >     u' h ן O$@$@$@$@$@$@HB$@$@$@$@$@$@N_? x@ :| r$@$@$@$@$@$@$<*$@$@$@$@$@$@$h' N)*:/ %""G$@$@$@$@$P(,V%(];{'    L y$@$@$@$@$@$@ YNIHHHHH*0*筓 T4Tg;%     JWXQݰ NnݺoHHHHJ@06mڔ,3 @'nݺrA    8p.sl      (#4hC$@$@$@$@$@$@ Y}ԩIHHHHH* XZZ_^N<)5ZjUN8!ϗ.]HxxWmժU2{lINNjժm&qF_䩧yr$@$@$@$@$@$@C=&L eɒ%Z~ X%!!XD kE?(Vߦѱc|ٲe>F퍠)7cJʕs:VM;3(/88tQ۴E\uyh6v۷o3x`]qƺO{=Wcƀ7u^ [< @!PZQ"z%!!!Z1:䧟~W\q믿zPtv*_ϗ3gh xU?2b{Rsnݪ)))z\{'/-ܢ94iDmۦj߾\yx޽_HVVVtLKꫯ/^,SL(Æ a`ŊW_}U $lnGUo=E]$xvF͛; 8^ RjUHBiB }vɓO>G}e VO0`ֱcG1FT~Zq7u ^X >ƃbj tAcJ>fҤI?2tPYz6ԩSG*cƌco}X՟8qS~='0bnN:ȑ#uc=&weϴQ~ir1la /j믿^.r_]vi@MO?[WRE+XG;\} e=((HmV+쨷n:u@inРԯ__oOo_|@YF=$>>^HܹS7L?[׭01H7|pA`=<W:v6C !0~ #-B!     (_J5b#)ߧ~W"l.6mڤaŕk+7o /Y5mԫQF:fÇB]N +8p΁`pqc@v`1cHHHHHH(U2/\PUem~ر-m6l wy@FoarVXG¸q:ȬDGGkڮ{&Û yxҟ:kw}{Zj`c      ATCC y\V-Ug(p7YQn&+W|x X }$;2C:wǃ>v0|Ϟ=z9sh( Vg͚C~z}o1'$D@$3[l2_h֬fg#3iڵz8xJ dF $*,4XHHHHHHKvzݻwˇ~3C!ٳu~le }]"l'LZ`dG B0U`,h{x*Xc&G=}vpe@!ChozK@\uUPw㺚} W[#;ZY?v0Fl YRHHHHHH|Z[/ܱR6mڸPJqh PvPD qCfwc!a! 0GE;;;[ʽ3㛹:kS2h1_]?y=LYQ$ $ÄtRHHHHHJ@N(2sluGәlo{{k](P=9 zru?0o^CV>o(%c\(qs2b9IHHHHH|@5ώ . t6m3!_%Nڰh"3ߪU+royМUHHHHHHHY@;&(|)!JSN>uB!    _ PI}H$@E%AǏ/۷o/jS{1?v Y(QHH|-[NÇIHHH %P"z $@$hRHHHHHJ@JgjH|+N >㏲aIII uҽ{:؎ۅ֪UKZi֬Fa۷37n,Gի;ꚃŋ˲etSց1G)3Udz _z~6m5kʰa_~4]'>TZў$@$@$@$@O%gsq6]- hߓ\FƁg}V $|7ZwԫW/ybx#ضk"ԩ^;qD(. "nСu?siӦ;wbbbرck<    (_ @z sLn˧\IÆ nʁު"VaxDFFE]d7{G{,Y$ߵ[n%] @&P"/voHT WEָ{3baVV߳( r\Re [nm RN]VA9HHHHb(ƊɁwM$@^O[1,. ]^    Ey$@>Lqp*dGA |HHG޽[MџB$@$@$@$@ @ 2|Bl㇘#}]97}HPPIu!   A4CI7lꫯ%))Im&/.s|޽I}]>} رceѢEăƶ-[l3xa!$@$@$@$P.oHJL]ky'?Jڵݠ?.}?kY`ԨQҽ{wG]sj]v2eۥsΦIHHHSJ>emIH@6P^+|駲k.y4cUł#ddbzzdffJ5n/- x7zxH*8ի{D+%a8Q      (>r$#[&ڑ1.?(cP{a8!9ʭeі}2_kټ/YlܹQk-ZP'"$/H_UPUgS&E5[ʷ6HtB{wH<!j +Kfv,ߑ(kv{Pƀ6A_In>ILɐ_o>-J]5otTiYtk+Qrh^  e˲mun,'Մ&<AZQ޿QR3doUo]_~YYd:9;9M,Qswv h+7 軒UjV:6rծ!um*\+=pp&m`9E]x|UnNsV/;{9uNu'jHV1%JCh[e(_~溑c}ۼWn!w3Y<w:K TD^]9L*CÚb^RάR=2TEHJqTC @yiųVTT 6#A?[)n:JQ/Df+$09nUP*(L({C;5m?ݚ(bt e0<`w'cfUM%lڛ,כdvHHHHH@pe1~IZP+յd} I)XY@Ibh=Ri4m[_U:.cAp^_#{5q12}.GmƐ2]G+HeZчg mCJQY1VC)\M.QV/JJ#Wç3zp~xF7V\@EW^ 3.p*;˕=Ws֛+#Q6QJ5D6=,@v2=i:g<'xψu >*Y<9X~oqKFkkԊ=@>e<|7“!S"    P/c$ ò79M:7ugSpTRyX 0bJ`Ċ/>V;UuPO 7XUjK칹7a†=y(ExZ_VXzl*bhS?ZLJv e2&JnVq?0lЀIp%]Tz|ޢEޕkUW{Pe4]Ub?>˶V=`T\)TA2Zjau~xrH|JQY$@$@$@$@$@Gk Xj.b |JWR$W;C=!b쭂xn{jj]G2:!a!#:,jocgr<*a&ŝ r VEHW` $; 1ƄdDf[1&ȷ`-b=fJDŽ@ӢN5^oM\*,xbOWF`I9??gpx7kUL?on(P~E{Q[IHHHHHxEHx ">'TB8;y-KR;Sn n$3^+v\cg Vy<ox( 1C{7 eξR]g:=񢵮r(Y]xn}[p'ȊԲn^HpcQ/yRvl0b;X੘w|[H9XOZ1r\.Uӹ0U<}X۸:.GQ1YN$@$@$@$@$[A{Z#K>$[tk|w*G1aW(;U}K# \|ϙ"  1&v]QiH8 r;`[xpFVTkxx8sw܍k$@$@$@$@$@E+ p4ݡ!Bɂ a]=acv/Vɭ+hXw}\*a]t92/>QV}HLQyI}[ϡ[m0CCexhb~nPꬲD)μsN${5 ]<[n0 1.^T'V3֪m ZxT'x̙3G`2dH͙ WI$󈌌YʸqdݺuҡC9%<<\N:%XFVZ۷oJ >}Hǎ% W̚5K^|ɒ%\r%:V_~ҭ[7<ЦgϞ2m4sڵdeeyXB*W,f͚h?uTA)Sy_^233e/7pm6=B%t{hݺn?&L\|źlڵl2m`@(pm꺞/kga `~ۧ!g]_ޱk\ ڵ3 ?=zHΝݾ.q6l)4UGF)Ѻرch~'NxC ~g8U\xLcܹ~#""{ %MÏ?׌eO8CMpuѿms xn|;۷c>x($@$@$P<_:)Yq, qC_+;g ~իꫯ(Pr.{3>^{ nP0Fo믿CJ?€6C!@)k+pG}0mEF7|Ӝjߗ]{T];G]O˚!{^ӌCs/꺫 m~= m1 f.v#[owyGgw<.|Ex6o~\='PGPa ae7 Ƃ1*԰;hqZ±]Pv=7- LJIOOחax!B$@$@eMSY2^Y_v;$mՖcR$@!073uoiP.VnMlߤ6N\  M?&w0f4綳rr{: V{ⴒR(b& p;3s'xts(P Gw4mT~aaESNzePʰpakUbȠAt.x-@@Ik>APO> ]pa?P< Xɾkeګ!88Xğf7ghѢCs_uqrne/o}3f~o+Pα2q /{`w^9sV~Ey睧aÆ9 Y3oFs yf)V|A 4o\31-pVE}@?w(Oõsq7 1ؓtDFq yre2FH m{B^vTJm㒒qje}W$mڴg(pNII)נ}cܵ_}@Ir*yP.j(ew{(Y0@̺q֕lc2<=F\ѲeK=3;BUaCy*I)k7v(gsޯM6nCy(ظqGy8lժU2`zժU2 0z(0!'5j/c=;;[iР.O_kt;i`lnx5%>aox9+S }+3sGOlKk7L5nXǻC F(}`{ɉ1NYcT(/v*8^qk XH$@$@eD+Bv8,x W-<%"M)M/PHjU,S喋zj\41մ r*דe=ȁҫM#yy2sf(z5JjF6(Ԯ^EScGEY+uyHPߩع[b='CeN`u_긷XOWЍ sY7Gfڢ񛥸&0i$ p?>^zNejJQX.P{c1΅ A1F``B*4VT.q/^(>\εaY pdz`9B {Ysx,ZHoLByg \瑀1E} M(j_p7bC~Q/+8G(!|B$@$@犀Wxlݛ'.V_%y!7_"a!>P%<4Hy ^9\v%&˷XItYܬTʠCbFJ#P\'ȏN$4]CJQڵ4rbٺ޻?$eenFn蔷=9߫n@HX{D( Va,!lg# ֜lg)GXu_ҷ^BXTݬ[])R 1^wvl58^XYQڲR1&.FҞ??,D kMwmyHHJW)7~ʜ[Kz1FG>j݌ u X\`ƊZ~#BV je`5yA^IdTF4xX6׮qy*U3U,Q '{ˆڋT:Xч[2Wϟ Ϯ4["`ر:oA]glgXF$@$@$@$Ph(YHnݺɥ^*wm={7߬+w}$%%9|gڅ^(qdeeM7$X뭷Lq &Fwn1/III&M$#G .@.bO}Ο?_ǠAgϞ+h^Ѻnzz>袋9Bp}̘12dٻw_^qPߖ\]F#Fwg4pL$@$@$@$@BRNI;رcuq?}C_vKZhWkd„ s'\r@?`a6ly}KZaNMM;vȌ3ty6mt=#VBKx*`U"\࣏>:; f`zjS뾠W^]{',X@ۧXK>}$@$@$@$@L9J0'(8ny"۶m pӦM=zN5knkI&ob\_uz6m$ aHj=y(7x6,`|x@q hG_/Pޱ?p~Q \nݺ:d~̙3GM&1111 ̮&_Wp/ oa=c,C$@$@$@$PP;L?-Nx B]8yJT!BYS2Iȼ-˚-Cuy@$p'OMgg.>JN͔(:[n~'|R:[[nչL>CIGb(XcǎX^xAeڛ|̸;8{qrq9`@"G}T$qJ=a@?ngs0P1W1b`ر>sf^H!L!((Hp?tIHHHJZ ӌ9!Uy!>_<{ź0A)|V?{:͟$U_;݉ri2_뭟um$ڼ0`^ͪ2{+p]D~R_kX*I ߸kOUi_ʡ _)I3{txм49p8Uj'/|;Us7_,G? Ntl*篕F_(ŭt7rWu|<##C'blF ?2^`\ Ƭ\XcU #^"#=7 l&ol'>    #3Dzs4cYZGP00/W(Y9/`c๛JZ?ʲT\nRp]6.+*'3[u\ xrOMCKy }7ep;yBn%aAr2AN'Oxx;O]uK `p&kQ>     'U/.}#ɻjWd#G('G ]yH2d#8?yP@Rq+$Ȣ ;?]Z8]?$U*g#g o L\/tUCGuH Bmɖ$iS% C^a~:vxÂJؠV!UC+zSaL_݌Q2T}Wl+XrT:F6INɈ>e]D&l ƃ-jh`w8 YK:, !0mf{G7ceiW[[#UV;lʫА9ˏWI\ygu     2%!$}Pc2/X"?zHJ| Okt4]t rG֢?="Âݤ^%2t: I`ew Nd+ hs_Og,୨>N. 0@_QlAQCPB[ye{ι}9s3^kC|^;4RϽZZ!?`ߞ!c(I?J|,K~ тbd>?.y N a20ၓ53d=cTD@D@D@D@@ "ss" " " " E@R :Kydyud(02jHD0Xh}̙3m?Jɓm1Æ KS?lԨQgԩ6x`6mZmyЯ_?3gN^dyl۶ͱZvN),'L؋/\ѣGLqFӧe ,1c/+Ǎgny~ժU/i_.0ݲeKtgfLI2'9|rb%7پbLL.@TAD b͟СCfBί:C*X֧ꊯf͚͘1#T]1bĈ ׍FYO3lˋ*yvW͛޳%KdgeR1~1>~}|e6Kxvq}7l+e>%<֭[g|A33&$Z ş={;1{l_d=OB]/YPt<( tb$yKF>:o3N~'e]qIV-\!C9=ŊG}4/ɓ< {}nٲ8p1ծ]VWVPzHp=EL9n! ׯAne˖͐<3U3\ڝ"ٞPeVH%x=cȕ*U޽{g(>.?ŲޢE i^Tzbzi~Qc9;0֭[]IWv/_vN2emڴƏo\sVzu[ܾ׽6m.2\rv!mtnv⋭I&n?z+f'xM4͛&xI+x|nߋ.ڴi矻Wx֯_m/2լYӅm\8 ;￝҃K{ɒ%]z{ fCMrϓs\oA( UV >s2ͷn;~?u>|KCG"~K+gݵ8蠃O<-Ҿ}{իW"6zǎ>>||A;>2έtҮ]y啶pB^BzfO=T{x,.]*UOl]}I'O\k/nfVq痺4kv' yWݽZQZ϶ &؜k,>~{)tMNG[ӶmHڇuQvGmm>5NC˭` )VAqi͝;?x)4Q}f' xp)W`%xmćY_Ƴɵ\ygDF`B`Lw}r)_N)+뮻.5IȬ>?ލO2e\ۺ]C8=n~Gן([1=&QɏEYy#?uqQ)4h4Q N){;'+XS@T6" Es=)'|ʾ+嗻ޱFŨSNZ^~)(W\qY&CYDEx0瞆{/ż'n[WE?!/T/3 WE~^g_^xQ\N9bEz6(h;vtrR_9&/{VF ((ܰ=]}'a۽yydbN:/Bg/]VN88xa ֣Lr ~K/RwG)ez$bыv}ApDRVqOHxyFGFA8D$n+ >vgy)(\3movFz1{ꩧ\1QB^!1$l_,>lK$U &H[=S']j"Q=s]cg `<ꪈ,qfՑq8G|/2 I̽9{~Kzz-gM[,#{ kt xG\C'`Cj%\SdZPEWj^2z=i b hխ[׽hbȍ{pޙ t}9V /?(\gzwFJ2y$Glu7D<H$b@y O~ιk׮ 1h/(\.Fn@Qmv jHyfw 3rqǹKX9wi)!+7SQY|h2G ?]w5V: Aߧ1&q{(3z:>Y42OChxkqmIf>0 c K/(aeK^r"~xI~ʏ4K3/ f_N?t:S Hx<1bϿ-'=3'm.Ov9ÌAwKĎ/7P^Knp^hWԭuRys9:]ϯD6p(p4Dyfe[k;6ђF2F1b #ֳ<=&QcSdVɏwʓ0Kne-A/" ;:==^P XTz61y%_Y /zwc{}LV P29^hjc# /k~#3OL$1`a|8~9) %@0!+y&b.F;9O'aN]58ڽ٧I4//Yic}P鑦We ^I\"I'oz"ɓ EH)S>(uwF&zl<>2Qw696?_Be/0ިox9;?((mqcw 3vyse;}i 1Īq3\RD++ϯx4Q~#[LX7(O᪍eGAꍠ3睕kƚFg73 r#iϖ̽ސ}g"c 1H~/} 1;MfʠO\Cqy"ybÃܟx WNQļ ]<8/KC~S_F'H8f"~8QNBc^ȃh scI=) ;?Re?xv0Qwx1d`GP(f``cz衑px3uꫯvme~O'YiNp>A$pU:S0Z7!(*/{7H_Ò=KF8Ò$b- {3ק;X4XC܊_t}=cXYyrK>_?]n>L +R(*P) +ianQJY&QۍWgsڦ2u<͏ )/4V&=xF)wU:[&/% $7aՎeMWDh( <*=aX,Tp\o@I:?F.!dSRErqi) r-gN{*֑rJsZIhQ~2I4+71 DL d\" (*WZ))@jQD@D@D@DH"xAt" " " " " " "dSD@D@D@D@D@D@ IvAT2U)" " " " " " IF@$ *$# @]GD@D@D@D@D@D@ AUy@ .#" " " " " " A@M|>6l`+WvI.;U^K*Ty fVZxעzZgHcE 216mڔ?G·k D+eH1`}=oӼ@#MHD@o ~g_|S~ꫯO?毼Jի'ʕ+]Νzvjtnn ' sn\rۆGDα_~뮻Zƍ#c=kK.ֽ{w[쩧2pg\*/[ʍ?C=wLknrM1xE<8Yd]}v!8&Mr7nh{Ν;aw?>믿p[nݬo߾mG} y/xPON>d;S1cc\K/5B_`yq^yw\Ow}e;7 e~ }Yדּr*fD@D@D@ % eI73g1){챇s9Őb+/#Gt9ǎ2zQQPZjeÇwy[~}kРSY'W\cL pGx~7H1}];c ~l}Y=ۉ'gtX0 ãD= _nС!7Xm?.͚5sJy cW_ >hucBX>#㏭VZOZҥÛٳ]<ƀӧ[>}z? \p=:rL `ʔ)n~ 'p'tbؘ? 4z-7wa{׻t'" " "P8 (p^W@>@y ly|ڛoJ@Om؝o 6 ևI==[lN:[z<#X~zQ04ibK?i,x ߺqP%g66?KmY9<87\?W:ue'4#QF9%zh$Mx((pEGP `L +l@0p-Z8/Q؆2MY A첋Oz1"!9z,\ЭgkvH]cVia| F9aKW`77XD0`=LÅi۶yVƽE/>b{1" %Kt1v٘|1+}7L4쮞 %тK;q <-B,!vxxux7|J2J>a_@1M?Ϋ3t^|~_C*&O쒿vy9x+:@( i0ZďᏣ@" @ẞ:Lbܹ|~MuGһ>X͚5:ܸ'yzW q^NO6zI~7|ܜ8QlSOW݆?eҍ`A{u۠^zn7ཀn!q'?O1#2XЗ /?ܨ|9pư`T0̚5+Ci@ P@ _<]D`@$v:,zo&N 6^fyfŠIlԩSX~`q^i3fs^ǁOi~1Sz10_`^GL8|̺8s @@=cOD@D@D( .Z DJ""   i7 @)c5zGmX( p εۻJuY^85*>:'G||abc@\}8c!&-^b)s̵d?J*39$" " " B 5$X?e^A|ײKGy"|B.g&[,0R3Tq)Α[_D@D@R sTR&@9ԋ7"}Zk޼)zME@D@D@D@0B$ ,UK@k3(B$>Ê2@~p"O@E @Q @Q:G"O@"_@D@D@D@D@D@D((\e@' @ "|otי3gĉ3M" " " " "do_DP6m϶lْj}]4(" " " " A O>X1D@D tID@D@D@D@D U*WJH ˖-{̕RJֻw 5j6k,ۼyh"C1bIгgO2s1valnj_}=V^me˖N;y'L26mdǏk^~e^r-nk ,Me]f+WNVD@D@D@D E( E.)"P=\{m 5a{WL2v[˖-3 ɓ'?o:uC9c֭sY?X+5kD~C +Wt.sO#|aƍ@Am۶n/_n~v" " " " )B i<3;})kV+&ǜ6ﭶa=*٢m-6g6L) NLj6m>u_6EME@D !RJYV\O{(Э[m7n\LCA}۶m]r%N_jW~ڵ6l0;s.ҥKK/u<ze;/rYn\zlNIc8kiW_[Jlti^z/jJ7۬dbV50 AhZhs>үTD@rD>:G'کZjN' Ξ=ͷoM5oʗ/o%J|3f8Ozȯr۷obXe%" " " " @Rv[)[9h-]ݮ" " " " @R7S!'mk[ň6|t׭V<2~_[ls?nۧRؽT6niI"Sv3M_ұwdB8|3>@Z==O=TP{Ξ~i={ #oq2}׍-@A ̨i.A;N:ߦl΁?uGuA'Jiƀ13f-fkwl]vlSћC_ (n+@/|ˑ; +5|"5Wj׮u5lE@D@D@D@D 6`P\(%1>Eg;cଏOO8ˤçdv4 'M]G~;l8`=zXCiE@D@D@D@D 'b pq<r44CPxDD@D@D@D0H0Pb3)vHO )BIK" " " " " " " yM@&D@D@D@D@D@D@D EE& @^U~" " " " " " "dH‹"@^ *?HB2$EQD@D@D@D@D@D@D D$!H" " " " " " "m$7.Zj׮,8"@@U" " " " " " "4dHK@ *gH2$ͥPAD@D@D@D@D@D@D l$ R " " " " " " "d?YD@D@D@D@D@D@ Is)T?2[," " " " " " IC =׾>.^cH󗬴^nyЎ"P 3c3u&3WbCd+֬wqڼ1k@H*m[2nK>[|6oj(ߴYǹiVyІ~k[nKۚlm[@kS𓈀<%wޡ3y_sz׬h˕q JDbŭX12ɳtGZѫݱ8FxƄm۷)f7oqi*/ey[23"P sўMi,).]֪UضHaf)i$uق lժU|%TD u/^*Wluֵc?SlTRH ߏp^~7Վ꼻[ww7ú}cعu~6ߙUA=kS[ul-{?3t˖[zb{4o?l/y oZwm5#Ռ6 õ-`>l`׶n&+`YbslTY"?ekР5iJ(\TiD lݺՖ-[U˖-eH@Q 4!k2qI{oHt!8kHzq;7U)_6G?:eJrֲam0<~ڼq /`Xk^_x_v˙vQ;W<9H.>"fm7rkxth5ٝ'X`'jTZNmF:@U*ک>#4#"p^zREDvE@Hv/Xʞl/tCT&Yڭ+UUX7M&ZhUrשlOݺ6o @^ Wa(>HFvE^WƜ2~R%p. J % g8mJ""FnT#@}ID@D $Oۏh2`_vT4g;\pDqb0v2}óq?/{pFײlu?snjyn ڠٽi]gϙ \?F Zi` io1Կ5'" " " " "PP@/?>p5sH|C0ߨ)#Sl"= 0#i=CO&aklfȠݲ<+z3x~׈@DZ!w'z΀aZ&u]ɠVw*'9̆O d߯uQ#{ǖ1l4/" " " " "$# 8FܾGscz#{W} wJN("nAXd%W< No $Sg2pGҴ2h0_nĤYv[ߺfvƠ> gW>QģoW" " " " " CXປ=^hծ]p|-\RNُ.&n7oq$kr`ȊH%|IЀܞÊ5ia4Q66-mr*}?jN +ѣG^{iz}u^;寣&!{-'˪d7})+(8r~ X3DDp"c)S:vhժUIΙ3.B{wVZ<Xu֨Qti " " "5zC'l4hPd*UX6m I"J;f͚Ϙ1쬳)mٲŖ-[fL_~{o?۰ap}\" " "  6@N ]XJrʶd۸qu.` c8O %\br;;c/zV>+V+Zɒ_3P׬YcUV[t!DŃzz1 {O?m[ǿM69-Z֭[k[nuۨO<[O<1G[nmGi ]tYҾC~'ݺݻ; 7`tDe[o~~?>`;餓#p1cƸMȾ~vav衇٦"@L<&T 4 VE@D J 2}t{#J)Y/tw3ΰ=)1QBꪫ[6=#G/r;uWyq| /؄ \^Z;REl6\馛;駟_"0dכM/? 5\ugܸqvw[Æ wǬiӦSOj/({s7\}"8*Ď;8 5=(:Fr:H^k5.̈0{.y' <MqgFSi 3g:}ԩzlT~};ꨣ eճgOI&q1=?1v)2{lWgEp6B~-[Y mPK1veAϬnc ʏA{΃%f}PkSܣ{Ih*"**WJHIjGEtܞt*(09SL(>+zȑn3zܒG+_|pY3;!(y([r-Zpev;_$ȏ0~x [n%:X^u߾}ĉ/͛7w~W.:Nv?AL[⸸{;pJ:nO@|2%ߟ0& 0r\}n 뮑uI {|W@jJ)"P PYDtڵkz܀9wEKRlֶܹm[+G= $UN׋ŬŠc+y~zl7;ȂfD #`zQxGih{/~>͂gmfoѫ`j߾'̀0\9-|Bv^/$c8hܸq²b@X~}tv1)`~`CtaקOje(};+XD(P@Q:W'@if|O?ue"? e}(bq'h/~%zk)W\艥G׏bΧe bf%)06͟?ߍ@y0t+<#'ݝc9ƹcXD<5{meb 1{\ 5](3|=e!TfN]EYYَ>+ƷXA)ϢE96_`<1>(m4nZsݥkd<L{@ )Sƹ?~n@?>1F/'n[oٹobW091(',2V<=ontrb J^ x(4h}f =\ᮌu=>|SGR<~>GA(ܸOt/qzzIK3Wz袋`mGzƥݻ˓6rpO(_'*+^6#xW~Cǡ=7n />cH{b _J, -"P  J>T1`qx9B#P'Zs<|Nd w̽~CE葧7g x7]Hydխ[E߿&wM_ Qz!9,!Wcp@Q "((.|c7j>?_Jy]xzq -7K,":B{Ʉ'*+J? =;Yzi#F 6I!_ԙ<1G@$P |F %#ZYBi`cD ΦM~0`Tۣɇ(CPiZRiSWj]/V < ESD [XK'-=z3نrU%\"u" " " "2%UBl \D@D@D@D@D@D@R xTf& LE@D@D@D@D@D@D  WMe< |R""hW05o*7! @pT." " "r.eR*$;K"" F@d"*uܹsm( :L&@?vE@(lRyD@D@D@ @e˖`l۶`@!%?=+ڗDD@ vET(@()͛7/#P" " " ;@RVYo#&ώ0(YmRjWYәa/}Ԯ]xDfD Y9Vig7:'2n<+^kV/>o0Xk7=Ck75>pkVw ۵BD@D@D@D@D`$`- jV{=ʕ))7o[@a#w)Y{ 8?F1~ӭDOh_mb ?#~'b2vۙmJW؏cJW6ٺK+Zk۷3[ضu[ϧ`ݪR""/f7ٲUl[a[nc8E@D@D@D@D@v"_TIۻeC8y"[1Mٟd=OajݸF nG#'9Db֫^v~m|nBrK}mI`$ؿmS <`H&Sry;=kӂE?lZ7-۶mkun$aevl]>io]*# r^D2֌@ $`eBNpj;uS,:wS6ԩ\>8֮<~$vD}{^`Ժq ~tz5c_OARo*mA~co~~eK< rP͈N"@̿w.cqG Fii:m?ٲz09"濘=^?& ;j_:mZ"" " " " "s $U6hv͂WCp= X<`bw%"PT 5֮9ty3@:h7g2T쾺1*iؐ}pO~g f_>HD@D@D@D@D@ @Rx Cp+#}DoQ|vͭL̞m|zo9}[ar3*92 `X$8[G~c;}[5 :Be'@ HBozt-w︛ mܼ6=~78aWK;dB`p3d" " " " " B@rsEYժ03_h 7*@guAOID0cd6@ve*hjo Fr@Y+VXڵ\D@D@D@D@D@A 87%H1`)T$=Be" È9 J# @:ZI@y]uV" " " " " " " phAD@D@D@D@D@D@ ' uY@:2á(d(Ug%" " " " " " D@D@D@D@D@D@Dpp^W#P2RVX=@A@ڵ :@!'hѢB~:=y" " " " " " "P`d(0:<y# Z>}l֬Yy]ʣqv'Z:uydE@D@R wTbs(Z38#.3O~:?'*z;vY<&ω"wu" "dH˦B@=^J-" " " ;mvzT# @]3XD@D@vM6و#駟v'#ׯ >.<ƚׯa{a^XJrʑӬZ~F/>c|> e`Æ 4YAqO+ҥw ;+ʯ4Cӧۙg_PyLK.)SؤI"5j<)d3fXz_wż⋭x sFD||1mݺ]9ܹ(QXH֬Yc%Ke&JfxvB^3KTfۡB >yd8\/fD@D@rD zY,֌K܇޶>o g1]`I9 ~Ke|zLwT("OfU6ojz_8tMrm^mk0ym ~EQN8KGo?zێ:꨸H0Kpux=~E>6/ڤǏ_󏵍<n<8VfL\pn_s?~vm#?cjժnSNuM~[nְaC mѢEkv#cǺmԛ[n~~͝;mClvsQ >5?u~WҥZ*:s@G)3 GٸqbFp?蠃np$:1ѣGo|+;Xr`=P2r;#?98F]~Y{1O׮]savO' o ss OD@D afկlkBq׆v=C0EixNrFz/K/ui9 0T>hCowF=K/̙3 :Z`=pa -p,z?sgtFk!e8uxb[d|Def Xy{=%P&q=0h*" ";IN.[~5o?SQ"/_m|)$hݸpJwjuCpsCphߝDv1[ϽZt8ҥenu;.^a}Μָvu_*2".\mFLi-G.=ަqK_{~u^ (PT`zm;ʠ"uٽm z Xzk[ɨ˕U+?9( (\qyX;Es${]w^x)=Cy⥝<)I(E(;3X\93~w%u⼽B2@y9-[p`8@A%‹>͛7w=(3{VLF$9 "d&(~\:+zy\c@tx ygz 0J*F9rI Gx t-@HPn]@][4" v`Dh۶#~'M0.kтM6n=1lė*U*p\Y|,cཱྀW9\3 'lwa/H;cS#pdݾ댕$y 05w6-Վ ;ύ,NG^(ߓf-/`llqf^(;AOR%K~ب)žXu|Rm6a1PNiZw[vZ?Ү=H:nߢ3x%]넿m)VFUWKe܆q }G2Ռ!bA#`\`f5F>}lK;۲z`B7s~V,XozfPO ?uH"6Co"yE~1Ôxjd^mժsF@IDATQf+܂ q[fiܞ)GE!CI+l5^QҲA%l6ޯʹܮ]2qɔr# nAWNMf۷OW>\>MF"kԨ<<0 zc^@PX +amg K<I`z]}nbhXbŊ ~[fS |pB~ Lk֬E[CŠ9 븮^hxLdVf0xpl<[oup`1clݽ/:ߦ@ $ zɭgt">~cwx'[a4{eѹM3[ .8a<#(7vMz[CfJz>O5-pQ]\usW]^EF5(#0Bs Jrv׹GX.@blL@ =J5zu˵?g,6jѮS`Z4=/eKe}=όh&%+B޴iSF6d2ၞEv٠2Z8‹W|~E sq+|| wera 0aRwKyBpYQSq#袋\#BG>MMn^ pe6ūA8Am# r"~<<8DR~}ǙA 5gVf(wN>dcxyyp< (xIaY5͝pEWsg՟zKfaw'O$\:3<=T/֌!4RDqPz5 <|P1P6Gz.)Nf䖏دmDayi*Aqb`S| _H$A32J }w,^azi/yMmm(z3'> AB5;veJ..^`{Kicu\q)gK L(5H$P:|>ˇ. e <}`x ؇4ӻ 9ʁ|>}\wW|s/`>C[BpǛrQsLHCFv`R~衇 Ε2p-0. Qq'pOAC^\S !ID@D@@R"1nа˺}+7a_hxqO2=T5Tp=Gvjk)+jTFNzg@x>Ì7BH?RZhb~W'Da7A|{a1Ow+c\'wO{w?f̳>t7ƿsg"/w$m$x&v7adK)=j(( ?gbK`^zFyZ\l/ńSb8gC> vfє< >nz_|, 뢽=0l!3@% DPHGw-cRwɻTRYf{|h[hīİ[vdIX(912t2|H˃7VPqxpLc z W1N ~})uur7z s ~ DaРpp7L\ ^?0"rbDH|Q"> Ȱ00De&4<)~Po$DTD@D w=wj~8|vO}.>:)6Eqd{Ngzi)$]Բ/9) >F?(Ww`[ J{@ySH `CG z9ecʎ8gl._8WD @_eå3깏p`ςQ̿s{[xS ~DZ +!<=[: n F/n_` J;ug0*F^Y+> \ MuB#c`  _ೃt JNݰ)G$W2ӲXC*F0x51eE%yօ MRI<זK}9zƣǏ˫_Mp RYCG$9-k \V^+1HHHHH]e O'Zn*2{ifb_O;bQt:\B\jĉ#>i hH $~d&e{k8HHHHH@PXێȇؒiR#^|{9~8x_eRDqwqKz^,8oLi eύ$㈰P xd%,l(!=;+ û |$뼕Ҧa 9vmٸc<$I;| (&KIltw{j:l;'$@$@$@$@$@G C =ȚmlVtE#(>xZO<$%_N[h-e=?DJ ̸tnDJ:2ִϳ Z[sL#Akuڿ wRsIfk=ٝjݩqW"{SҤ\d-ơV8:bqiؼs]T.j^W~s$Kl$@$@$@$@$@D ( ѥψaY3 6\AM7xq=*ѥS7] v͞”4/jUSE?!+ƛט%7pի+Ww73w&;ۦgsBTiqaSwOHHHHH@Ppj0v w[wŵ+w'|>boak1^XI}bLi^h(#ϴXR` fTKR]c? F51j~WFMeJ/.Mf̶53~/mS?Voe6,L9 @P5Bæaa1'pp\F'"wKUТ^5~a3Y!Q>))Ibb\}iGb*16.SʋRg1kKI/3 $P L{cߎ(7VבӰ~iq#EiӦZ;a&ҥKyd @!Bb cw(e?K<(W3 nfU$@Mk oluo4  $4K}Ȋ+/J*I׮]mY[oUj׮-ϗGʕW^)5ѣG[@ӦM^})%p,$$$w!Ww}W:v(O>|ײd2d%\"tOH +cǎ={zYEҥ.]ԝץK{݇N@g$@$@_/& :uL>ǃ>hCp vܹ2sLM6zK :w,˖-ݻK/8hhժvVF1RbŊYkժ'$  zUCG#=}  ((K,($bIIA|w5LW-Vczq84kg{pp8voxA>իWڞX_=x[_~IHH L{! `'P\9+w-*TvZ{ǦJnlsY''N(GuIHݒ ]͚53>3ݻ`ƍv?_kB@bbHp֬Yra{m|bVtt$ Xr@A  @ tlHH 3x~}7]ts ] AéxbyZhh:}Q&OlHHHH b'}y$! `pT i> +yr8Q C#'/\/HH {\= @@ @"M/}|M     pwlHHHHHHAx$@$@$@$@$@$@$;:|gHHHHHHH 7H$@$@$@$@$@$@Mi%}:pDy,N$Pz_O 0c/iI([W:&9 ͯ_:`&ٹ}ks8z줼4zc~8G8x0~U;{Ĵ@$\d޽\7HHH (!]SC/m\&<[LI;&ŠK ab>i-{HPyLً^z)CD aNSH`^VMʝ =+[&A,~3$@AF >>^VZe\rv&F!n*!  #λ{pV2DZ7,dصfk-r[rE8{_;h)nF.o\ݾL?_c* q̔zR!:^ vq_ޏdC6I(ԫd@ж@A%0$igD'TR&4G+%-L6'eJؘߣ S^\XAE&BO "" D+O(N;L??HH*r`/͔c exCҸF1E>ut;2g8+ ueY<%Q+`!ɩdS!VyZO OUcKm//ӛt"LdܟҷKUw=@0y_MNv_w©fRcf󟭖fU-v6̓ƁI6OADަ4+{v,e]b Bĵk;- @P}S>6!Uvo#y6XvՖ2c[|@I;heYq{M:X#w_UýY6-@ϙ.lZަ dz9vin6.oH1 A2Rmeܙ>0k0^UIHHHH@PXxoPuOTJmO8IukVrMaFxP*/ekþ# t?һ zث`ȭ\CɌC !ٯ!li{h@쇕, SqeE+{FrAAg<'    _A8G\U-c7KK?d}15 άAwҰ9?B\ٌ5ʺt_6yc&!rW:fLg2'no䏠#d g,|g|CµߵIH>`7%Ix N7s^82yF$@$@$@$@$=XK }c4[To5Xйyid dʹ&\]CN&S_ F|޶)B ' FKqaKg%f$FQ%|"sVlps̔mҹE|e~HHHHH@uBprͺjvD6qr90=aT_ǧ1/f3~yu`i4׳M= .kkzV5 xtɿMp5Bj6χ+e'UI'7TuEvF߈mkiOrx`|    (@v"))IsXڿŰRŭw|8av&:cc  f@  ciϰ,(ΚI_II'=sReH|'pgfNƟ8/f"A5i8ײC]{ 2 oi$@$@s }ۻ*C@)S2' Ž l@@ra  @ЇCޡ x\C#@{փhG>#D uq $@$@K6x HH|"(;S W87tgz\k ^ @ h^oHHC;+S4CYuhyǎuf&>A]\k?8 k?8s}jKHc  _; j\#]E z3! _]5b}N[Ѧ#Ogh:c   |: }4a*TlW(kӸVt\#e=T!FY>EQOq z_%q}x7xΜ!ǹC9}NBY  #@@ޱfO$@$@$3[Xř h./q li:P)e9.-mePSGPWڧc_< ixvzr8ڂ^#HH t3#v. @^hQ *TLĻ.S)TXG[zV1.q\kYQHTuPAڟZٿ;) %lCk[t {EmsދG(i苁HH p2 &&&pwȖI g僒Pa:S!01b \k^k_(<ֶ ?t>Q>4??9>搦yjHG@=:8g  ;~q'  (ZT!FPQ ы4\9Dkt9tlہ(C= i*;?F@9o#i*yڎ(zڟl~5bgy?sgHGw9Ql{w6q RJI2e{w|C9'Q!M ?`   :Õ _@ 9 ]:ڪeTC1={$-5U"##|ٲRosb[#Egx(znťɳ (!!ߑz'L}r^tG)--M%""B N؟}UZ>sİ)  @`e.a$P` (jo{&NasMƹ"Obm۶Id1Qv  -ON6RjU =QMC64@$@$clHHB)T()5B HC9`ޒft5*55[ |mDEIxX޽^6{FQGel1]0 lHHF9BHcU  B!\+~ 166#:@Xo/W6 vs(u!s-s  , !=5 '*IuT y^­M!8SjP;=\hC]1& [!  ~}#*R|Q8ٜOfH K;B8MnnQvփ=sV  / S,9GC!s*C@a忻cK$5x O0^a(:ZNm   dU2g&>][_k/jׯ7 ?/="&,^M~*:G܁t>q|& N@G@Uy(k&+=wAyH@<Kp TV(@:HC jAغ{@@<>|=v\R=]$+pfmLX"s?^j49np. 9-kч $@9#!A< tTE# cLyEpnum^kyiHHC (9}( V|'-waI?rL/3i^4E?n&[ #(jGޓihЀ,2IWܕ6797-߹zɘH  p r3@>CLpBBAyD $5UjW[9a8G8nݐ @ P W-\Uq@#tjVGɸl,Ǖ]'r7"A2R.2¦ jlJg֪,^v|4q|Mgsee2v"[?/e?ټC7)Sl}@^;Ev?hWXV,Jl_筰xH1ӹ\q~#gin$h߸w2Y2,?(+!=;+ 7_答6 kȱǍjظc<$I;|D`KIltw{N/Yk{@$sD*|T06q@FY4`Kc&[ 䏥Ch0ϳ3`6%IÙeN뚣޺Si(KoL29t8cfcpqi\#[E*m֭2sL'|ҧ6XP)1i tv:4ߗ'rC~gcQv{ājU-NO/J$@$hAXveܭuCE={=Yz=?a!ʓfPϋd-ݤ *ټVl_m~񎫬?l6iUܾDh ƞ?0[/Gz]d0[a=vd3^Jq_w7sz#sU{yrMs/1aުVt'^bxqJ7]:\7xn-Y8,\-%qxiff|նcO l֫Zhrua ffx駟͛s`˖-2|pÇ%%%%YPQqX3O(e̙#ݷ8--:ԁv Ԟf5 jj:cV   Ĕc {h$Fjŗ1";,#'f_k f `B\C="_iCKӪ16޸clݵߞ'ĕq #!0Qj@ʭ؜$U*D< 9\12r޺0-Ϙ@zt3 9_e0@h2j,[lҮ7KNM;1k;϶Voelgr޿_ފ':v }Fp\:0(]yt'''KDD~Jtt#RB;E;UV8B>PWk=rt_xVITTTnb"F@6vǐW(M'h0 Bkd}Laz5{c]rǍ qJ3pRa6ط:ߜo6L ڠ=0c +֯j;v-·*ʕ;?~rɓmڠAn={gd۶m֡УG"uȑ#ݳڷo/7xzĈc9pI׮]>^{[|3*{ "ܵ'#80KD!(UPYWiW_}\ԩSVՙN)/*@R[֖Q(`  r;&/tMc"  bG: `6g6[)ɶ{LG Sl)MkUqׅб* 6|꣟3Giʹ M  _ޱYm:skWz;}ZI-Yn8{yŦ3n6h-ΎoWYG2U6Ԭ,T*73S e4_O_,ؠK2S.01ަM3f,X 8@׭[ W^yŎN41ܢE oqYr:uH>}dŊ_JJG_zԮ]} 7ؙ򇀊NBI>bC:'h9eQ׽[6_[=!A80p:,Z(S~F$,,,_1Kaj~'4T4NZ5kug|0WC6q\: 9bg[?HH|& V܌>j2ᄇ 2k1!`?uJTDݩbi#!.\lNgn^qM;!3gg>D֛=S 6*O~ _eA:?|`sC IЫS 3;av Z/Xo0_8WjDZ~@n _\&:7ΦLg? fcJkg]owؤo/ȏիW)\pK&Mdڴigt@)W^yױcG??ܹsƃGhڴuB GtEx*pDEkD*FPJdMSL>*o3V^>BJ)C]۹(;W3ƙ?}6,=HK?*anWO̹7wm-7]Jh)dvUmC~GGu81E voŰ7@vvf?p:po9`:_mԄq94Tu|˖mَ?$,CkUV/jCBIIS!O@ @Y'Tŋ x%phwwأ:jR[Wvy툉$@$@gM@ǗJiYkaNgCbU*)}:pl`Y G2m?15\c( ǀ)SBħD.4k޽{=6nhV3,Y" HQTL8p {ȩ @[@IDAT&~-ia`&'`o_{U|aS~vPM(|!8];sطر2Ԟ51 o uN﯀vc;$@E6Pi|:t`YpBym+ʡCϔV^ݎjWc7π?5@=ʐ!C]tq!D/7aCVZv&#w* UXHByEgZN|٧(ː@N٤%lRg@Z3!#vu6}% )b d^RR{, 1Hr@ksPPIMMRJ?v=b$<8SRRۙ<!Hsܩ #),!\#1U#S 37H*Ug a7FCW[GA1 oןoXH|&YGgca|: ϳ=^s}GW;@$@4:Z쵨PCv48<-tot   ;C@$@$@~#ab^5M^kG& @ Ѥ#:L88WX!H d,OQ[E T[U;B#;ZH D$@$@$P `QTCPH4P.N #SBiƘHHw~q $@$@$@8p¡^U;^Zq%C(l |/?!48HHC/8 [!(I@ ҀsWD ~:kC]0&=/ }ujcz*{\rIHC.Upd+$@$@$D8 p\Z?p8GNQ@Hc[lV]jNg$@$Px e@'#  'b =c4UupӬq THB~ Ö-[dyr,;hРs9nΜ9#?},I\l4kTOgB)a)Y+u #MaZʛg/$@$P P/HHP޾Lzrz4L^!<#&&mI;K.IXX_oAHҥ嗟'X!l1cd/dפuV,=%,4ԝUUal}m}/]/*;Rj׮mʄ3A/'J&M'O٢2xN6aǰa$8G>\;^a @. hB$@$@"b?!TP FN!phq:r ۶mWRcOV&  Th(5OcS :TH3Oțo-FإGN堙Q{kG'2lثkz!;ꞹ+<+W]% Zi~v~޽䭷޶9.-Fp60dO $堂9m4د#4Gu~-g#sIHrJG6ogh)ɬb2H2'pP1EtH:r[P֭[~:v5/P#̽E)eSRR$8r3%Yl' ~?B3 Lw}rͺv x}꤂M#sȃ #FP['W  vi @"' D+$9ӫ!Oi;_ʕ+g_V!nvZBYV3Fԭ#w`wRJY#@]8ݪmk3$@$@g"@1H8!1"BiqC:hi~֭-">=&K%*Ӱ0kHw@'V[9ipkc   XHHN)x  4ƹ:фHSGq0FiӦ5Pk q#]q,<`9@$@$.VVHHH| ?AHt@BaAA\ê$pU{#b[t^i-g}@$@$^0HH %#KEOґQ\qv@^\)lY(lyj8{F=kz͘HH7k  '&#E!|UBJ}̏MMjxz=Gv),'ڿ  / ? "QAb b ZE- a?AتSæ1E3vz:#@0& 3d $@$@$WCHMNb 9>ʫhB8 psknjW öqP P!sM4*Cyo3QjOчվ|u -m (2v_)ˇ$  'bHE"%SB iTHi@?-[dyr iРs9/ϙ;W9b Rj",q Hdd;-uˎҾ};fϞ-RNgRlY喛϶YWCEة|Mùn jZ1 @ {vI$@$@~'XGJULA,eL~m|miڴi+WG:^yLy9xAү__9eK[ o ^Uz [GCWlqkAC?@:2 EGB$@$@~!H4 MXRV{w^ٙ$[:mLm"Ə( "^n(2WX)O)i٢[̻W|DU4a;k/r|\xgS<7lᕡCm~Z!Ylyt6xxu0 ΔGq hTҲe 0A7TGPgle.ѯCz?O$@$A32XHHDxB\nPNF mmfRǞxˆ4{]:f'zizjc7$>'eMN[~`Ba{ƍmw6=9F|bsNyl d͚52((WTV1CN&;te<L:Nj*rQNŲ~tdعkݭ酈_hFÏ{,?3ֵ=LqGeoظA'kQf ; wܹR%}z@{l/kת-M41WR`kw7 FCC2gҮ]wF\w˺]&k|Vᥗ_%Kڑ}pp(vgw'LcUQ`ø: %K<2Vw9 oXHHH@ @@APQB (Q-A'8q}u2E$|RJqry]_^|rkaDU˻Cnv~ºs_e DnoҥK;Bb ~dgY#kԮUK7_&!nߛ%Cf?gGfsJqq2e仮Yr؇FU#FBvqjZ/7HH༪"$@$@ ! *R*Pysbچc}]wuX?s|G=zt]oN>#ݽ3rڶKXX?QMH.|,5f֮?cv=-pJ 6o,#Ou kOM;oKn-nѢ7z]~h[yg ̜5UmBuY_Ml_;w$Z BfVo_.-˘HHwtΐ- @Qz~1Ds4/ݺ 6c>ewH=dW_4X˯R6}7o~ ~d d6 Cv3@oPO)pYAYXǮl0nӦr}^rC|)| 'K/;?ߧO;{>5F}kgs̵?j֨!ؑAR|3rskoQp'޽{J64dUG}]{t ~#hp-ǘHHw?> K2tv @Q&=V(*uL iU-N[lzuj[9]/%%E"##kn>ilTi`sY>T>ݗ ?K}iYXڐ03*7}YNWn; Kp<İ!~zW~HH0Ȼf|6  kk$\`9^#皮m2F+Wζ ?*I cDD>k.9~X}dg (珐S/ר(od{ϝ"&Ϙyjjxo`kpL F9MGjÚXHHpVXHHK@Ŧw\4FU,!FPwk WXvu[g@ '+{"{E6>Նqkg  / !" A*.[j׮-8`pF v3Ԇ;VGh{J Ltƻ& (T@9UD4  רsr <"pRİWՂզ,@m:   @x|H  B@W!TC(iAu 3H 3 #\c bg@mk    Ҷ^eIGedN6*UGʘ?WEMK*>HLDtj\oZ%;IDXIi_ԫ\$v>di@$@$@AbI<Ll':pj$& "_U=[9l=?nݐ @&h#o+ٺG+#W%CIR\Ǘ-m_Ď}Vµ5*JJYcK3q1S:sCG\ XJ[vKPiY)._Z,s  I*T@Fb e4izS* @yI@N):`8 #\m $@$@!zet0/Gv'O6[ i{_m@hR$󇷴R%\pzL[R'*IHH @4A,!kAkآ *ògWQ9H/NIkgub5nZ/7FHH r3 Gj]ּ +ayGOXvOHؾ5qT"Y`s3 6*B>9c8BCC}j;!HkG}Giq=`Ú2#  ݾ+Lwb;U6Kz> غ'E>ܶ:r - (cGFHSq4 8G:Ē &g d#)I̝.F|9*B[loNYl9s&Ӥbtj@&,\'fŊ@$@9!Wl1@xB?*T(i:J фzp@Z>/yǏ;cwk֬LW32io_9>{Ҏ$ҥ5S-O@$@$@D)UP}"B @U49|ӰbJyOI-䝷ߒV'OiYwƦg̐ɈI瞳_'M{Tx`L0AP3ݻWvZj%={^/sMOK;$˗-CeW2|Ν6q L0Ά^x^F|G#~ͬK/h/{O:\p&Mû{*]ƏаB7}hWMbɊ2iz&jǚ4  %y,B$@$@=bF#FHG PFt\k8v~n!h᜘݂:S8qn]&W_uaUL2;!܅, %/G/O^iS&˘1_^x;إ1q:PF! @0lHHΞ #%FPk *PA5?P%KV͛7կWv׮m;YdX?nxֽm۶~<]fsƍ?TWYGѣ{2>+T+~p~B%J`>ލ~uR#etSK!yjE) p @  #NB8(R!sgAs@v5ߛ"wymR|%{y]?6UX*隧<=[7͚Q@H<S'{T|:[>23d=l֕:M ۵m#{-Ǭ7 ڇgwm+:@mgƁ4VQNm~' "P&IfTś!(no_Ӭu'y111z{B@GEĐ'^G(΀-[H 쵶8q#s#>g__`eϦ_Èf.p W FBUf3)NT@wGku>FV|& k dC@b$854Z^E#k# rYFs+ 6`pT)Z6r8me  !   ¡_D*,FTtQ9wȖH {j;PUD" Fy8g#sIHrJb9  #=I5pr#XP/$$c V5>h ZdIktج:ޝ`E  p& @XrNWѯHcjxBYRǘ@H ө6B_g Ye`j ~FHHH?t^Œ 'z=!N\kַ AyDT`jj:UD{QGl   ~i @5GGE! HB*xGRlڴIo:nRRRd%2aaaҰAtewKt7~ͷk6hgG{Lv[w&ZWeYQ-5SR4,ICS+)\eQ\[r7- uZeK!(*H,E..s^.p|ٞ\x翼QK9sw97N>eˢ"j=kGJakE}LLZN-gLLLj$h+k3RFta(UN@.ǓO>]w}n]+iܸqona66fϮKoGy RؒuuuqW>%&ޫwXSW;nQ65?2dH.*㕾^e"ic-jJ!뙵g5-ϺL p>I0hmj`Ҥi(} 4`eYo.92yA]@"0rM1ز[m=蔾+s=y/y 䫆'n20X8!1@JF;LL*;8ak\p˽qEoǸψgIϿRn::n͵oq/O_V[ǜ lea3X9ĒNp+5~uYgϞqG塾DZKk>&ώiӧ.MƟy\Gs#_|)&G~8?swGz(N:Wglи{sY{f&u?>Zal(s/{eq @[#&L6+#>qѽ[xiWcA['.~:8ufS`Ɲd*_cK 8w,\TC0o ؘ MzMV☻ ni&$C,I̓F$qGqՔ4-&~Xӧii:z?㏏7j V}#6qXhqmPbo.X^ 88>Öe&8&7rLt9sD/)ۧ;ruL9'ytGxž=Tf&< BHeJ%Z믿U͝;7_nnvX~ l_KO'˶[Zlx_MS4oοڱj n!x`iMʼn' C9,u])n9Xߘ?yεZ00h161:q-g_m\1Gے،tr{[ .}1}&Q l{Q*^@wn\}t{<15]c7NW9 q?1c^9gyqr|ү{aIhJ'qf(\|&`& _=KVfs=뒯ɷ)ѭQ 6ҵ2394 5`ٷ.\j~-;K~ +CEOF>)Evڮ1?ZҚ|WcukgΜן'bkm}Վe:r jjsE00>DBHWGsh#@"IB_"hboFΧ= Ԋ6ϘOk>+F8Um:60oT[200pG\I,!$'D=(usĬtK6 گ`ZMƌFmX?g*_inֹZ|&`5 WjБ400hD6%! ďK~eV?/1y|gm)r7Қ['dXS?s?LLB*ӦM`܈ IFiDnGD!ɔIGZ~+"('NQG1y%ɣ_@h?zS&ĔxcQ_}vKaU [s|B\sJ\iΕGH?R_2l&`&`'P ~UdnL`#ĵn}+ ȕ]"<X_RG/b8mO?jS"NmKv%as\mSF>0Ҫvs?LLJ*U300B# (A%,AW>s'<Ӯ[u!ž1|WX㫱ЇX)Q:p_j[R:)yyTi00 o500@I(#MR\!cq|W_fG\Sr"M>Fi2<]V~ROM8v9Fʴ|h|5o̧xך#LG昺-qRb 1E!#S{#SC7qm -ԗ-ʩGROuԷW_ʔƪa%"타Cl!$ %pS ' %:KĕiƂLa)MW;Sw ;b ;q&ڂ5a%rOS~l2ƏF9LLZ7Z{1 zuuul rz>vB@IJ[K ? -#P&FX!RQF>u?!џڤor5Q9`ZLLZ7Z{2/}ܹswS)CJ&PS÷SyW)_BO iƊIUOʲcGy+^?Q?S_ @@vo&`{gS(J!s.E>#CҊK)e|U2SV6ՎPjOm-vV+^ge/? #5= @ 1DqZ)uĘD5f -렝#_>j#%I m׺1_Z{D9e6005Gw9LLZL-+LX)IS0'_q=#IkiH G?Z/k湩zf@Cʟ&`&`k7ui&`&`5"ЖȖ(+DeJ'7ՖD6OFRτW.IOu||%a9&`&`O`o?h&`&`&PEe54]}HWyjO? yҔi!~&Tƃ*W=߰kޙ'9f&`& x`ρG`&`&`5!CK6 #!'.Rjc@P fm&`&`mm1zT&`&`&`-$f3C"Q@8W2ՕP'P>!m.!퉿HSs'&`&`mhSす @ Hӓy0%O¯,_Q5>~q oчI[xl&`&`?=b0000000f@3ALLLLLL3oMLLLLLLLPv3000000Ly 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-3.3.4/docs/_static/images/select-for-export.png000066400000000000000000000530661453511766500255540ustar00rootroot00000000000000PNG  IHDR ݝ= iCCPICC ProfileHTSϽ!B5&H'ZFHB%@##02X * bA@ 6,03oY'[;YfIl( M%pȇ؜L#44 6nh߲WpyP4O )G$UWfaE12 ·?cӜ0˟gr"˜SIlIӲ9|iVB@nii\#lOOKj&Hk|)Ϟe^LQ*{yR%s=EJ!^) CXɟ$_s2qse{Hq%Ŋc^wäL}%)x%=ق9L aJbIt~si9%ݛ'=;{~~1_33F:=)eyJ{RCT_i<3;\7 y J0:ǀ l ɚ>3]J,'eţ4+;)c螏%poJ|ɑ[@1D=COa9Ԁ6&<72g`9$`%X6PԀ@+8 ΃ˠ#9A8 Q!5H2!AP BBh *JJhT CW^4CO0 &l/0#e0΀s|x+\‡< ÃsxP2(e.EG1Q!8T"JZ*DjQMvTjES4퇎Dsubt%݂BObM9`L9 $3ybXG6]-6c;a$SÙ\q!86. Wۍ;;Í>e:x>/ >(~ O0$8B\*B p0B"RDWb1XAl"^">$ѓqY,# S!sT̐GɌ$-%IH[IH{7d2وA#gȏd,Yz*>ٗr9C9r\rr7^luUORkJ%RLi\) .( SQT}*ʡn^(bYɊEG{'씢r( *Yʩ%ǔ(RRaT4WPP6~RymWkU{V7S_R}%.Bc5aM30՚5kNjikjvk]z]}V{\#)9DcRi ]M]?]>)=cH>};0~~XV=kR@x@e@@q`{#aa05Bv< 5e1vqOìքuSW7(xi)쌒ZU>+4z0faژXXAl[..*`%;,_Z2e9ˮ.W_ 1 !Zd+:a?F\f㚡Au_~dč)ͳ+{)zS{V|k,-첹{{nٽk!ZUQybNĭ=%%{a R[:#hGK;ە"hm{ϕIUU՚[뱷Fӏk5-ߏݟ遨]??~CCuau4JFI᥇o:dѴY(8*9 8y~ԓ-P˪֤ضS:]ObˡӺ()9K<۹s;Wt>s{.\r.F׹+WN_uzZkCwu'dCO m7n.=w׭˷Y{Dw`^WO=ǚk3ya̐'O s:c6c}o>[lԋ?(T4yyO?OLڛCovNN>~n}u>EZ_~KMg Yp"^ &%ZzƠY?C?ޞ17-}I 7 AH 9;ѧ-+̘(/%?lVez 7/ hbeXIfMM*i&P ASCIIScreenshotb7o=iTXtXML:com.adobe.xmp 225 Screenshot 522 1 w@IDATxEڇ_r9J%) P1ggpyg8xgÄ"3 Ys~;.q/Tu[oU721 P3UT[(X0|p+SqV~}+V$h™)Z~ ^FD@D@ 7lY/^l((¬+-[dZz3&L`7o֭[[I&źν'N4~ӦM3 '`X7x |?:ٸq}G6}t+Qzv[5'MdSNի%ZZ} }3e˖v'fްaE9^zJ*Xjʹmݺƍ:w\k֬ʆ"H͛PV,J͛7>6l׭o_~1=cڵkg _|aĽm6>8;#;^zWXSN9ŕ#88ٵk[j,`ƍ; LRlYk׮ue8^@:(@-0l۷oo+W ~qg݀Ā0tPkڴidXr9 ~I^cC%+,Yg``"mۺtfڵk5yU^'|'gsq 0ɰaìs`}Ll À(3`d2yd{'?)>o&ʊOe1G( iWҥKLEЧ{? X٣0j(Wu5~PPYB(C=dk@%{ViS|O8e E('(L#{Mho J ,G E]Sؑ>PʕsGD@Ҏ@0#LZ@UW]q5d'#3_A" n#aBm܃ݹO2 :;ftɹ?#{b- dk$wI駟qCwGv~`w{)"yu2p@wu2y=o/tk< 1Wo_2:4nƌ뮻.^}٩@AqoV o0F(|`w3{'Q/bč@sy'0`Ch=ܶ2E (GSM7f\X|Iw3Hfa^uϬYe̺w,̬1d^f˯L3afzr3oL9fs?kLX.,l3tg7?;segg , FDvºȃ?yE-;bgX;KYୗh]r-n)@D# zʾ-Ph'C`_W& (q|Z>6 XϾ=v9]lmf sa׽{wsU<6 Kv}EQfϲ^f3IA aUa7%05XIR WN⣾bw?Faa?`ce"k3mw3 @蠑hKIC'" "P XQs>S/:}f~׬/{@i87$^lDP|֬Y3 9  kzw%'96_.: o*Y<^y78X`g . ]E /EvgVx͌]DHZQ`օࢋ.r;cag9 ;nrfwf-;!Y:?L9c}_$vǛ {wʔa6g}HI^ DomD/k V>`)0 iWRQXT(oX ǨҊDD@D`(эYߠAku{}ƚ307l?c6g6 ~ayvem̂7ߌg3H@yÂA7/Q XW"(QHvº~r:k _ `@!~C|ld!H愿-r|f躎Hp²JXhOl̩s1=snQv^ h&Fٝa+D@DH; T >tt3[c 6aI `Ġîr3p_wK.{7%ǝPP?|!C܌#ayMsMl-}:EAay`!%axXz`JEv2G7Oxǟbm$Ol$P('e͕ o`N oTD[ aI{hkGqbafݺusayK&SGNaI?Ph/Y>Єw`wxm BXn!:2pdŜD 1=? >POX}XN:ɂo 28VRX ^"0s99Kz p>;#_D%c ;$.(?7o DXqƽ ִ; nJ_AAG iR_E^|_umrDr|Q: ^Q yq/m^|$X².&3m~.fM$l$\2qg'ltc3C0 |#U&X7XD5:lkyABQy嗍&?!Z($.ZD@ 3+ {'%.ޢ`%[|!Z(dTB) ЅVn,dUAi>CM.DHQIQ$~m`C'y{*($f" " "2nK[ *@,RbQ# EA AD@D@D .) qCD@D@D@ڀ@\R⢑(E#) j" " " q HQF" " " RD@D@D@űsLW[l%U*$K<ӦM ZխDRtعsopB;PDu\~}R1x+V̵49?&Dk׮ŋE~E^) ٬Yf[Zy@Xbt5o<s)s ZjGOˬnڣ͖ N لyFfs)s7:ﯳ:sӡ}e((c" @x+ .\ nk^ǢO@KEUB"Dر?vKtxa~r{rʕ+瞳{v3 6<؆ b˖-f_GeξD{)C=޾KI "UEa͚5UjEi_}UZ' P/١Ƅ7o}1mǭ[N9[hQn's@ORuyKoرc6pHs;쥗^roNd~¿[`;b.>>c0M2%'IŽgҥ6uԸz/nO6|t8fϞm K| :jԨa;vLr4A;wnAV䚢>rH}vqzݻ?-TG4hnݺVaÆ{w+B o[ l_T)7nu!f/]uU1p# w@yQ:Tٗ: h Y4JŸg;=gqpx:9naw_#ֻn5ko߾6~x@.RXr-n e&aaҥKd{,!O믎ϡjgeʔqRٻ w+º@ʒչk-[t=Ý?gțnzeGy?Gd~g~Y\S&Nh 4^xeWT@T&Qpv嗻0 ʻ'֭[oY5&bŒd˖-֩S'*`$*.N:6-}+M>͕אXy SO.( 74uSN9͜ IovLLkӦ SvW^q&M_ Lq~G;Oٲeqht:ʮQꅺ_5C0#>î_/:+L(3u+I~x3#Vxa9ftP$G}ʀ? y߇8zhW(x 8qzC^{͏ AXxD8|l;2ڠA\a#mg*YIecM) V]Fm e]?P%4eڻ%[va@yxiW)]fO_Wg uC{D)X@\/a]뮻vӦM]Ch^IgSOH0pƷ.J*}Wʕ+^1CO[b `ߧ`RժU]_F=\^s5`k{py Do]py%8M3>k @sA92СRY4&/7o6֪d:u Q^ɇWcSb" y]pNO?:/MN8?3w? V. 9Vc=ft6@R?%s㜇-h1 |̫v+QwC!n#_=DhW(bH"? Dd0/wuCB~{wNzmKJraVQiC|۪U-Zgp?\NY`j(6lpG=N2tvH~C:j" J3T#}F][ qS Q(˾aG?1(Uɒ%]yXa'YxsҤFg`CgC"m>|3sW,'(xc m>OF$&,2QG{MD nċg%daGhGCi_|y.h (φ8+RrŢ@%xM~5F ,Aa6MGOCy doL ~F@cb fL ((X.Dpi($y! :aa bWL:M>`JXwCEfHa ,]Sg'|qa :@H3s9縎:%~NݡGoRπTE}a1` ` yyEM1#me%h( 6"+A#Gai(ZaGI"(%MLt( t&K{I'9(lԊ%Xb @eqƮS(o*o$fU:S6ʅ,z,$}ZuDA!<@Ú 85e:ݠٗ: x`"D_5? >z@)˄E}\:1i|/^!`m;O,LLcY;Y"-Sf\|! k2!po?l#X慸x(ss%|r+b oq%3}vPp`Ћ^ˏUD %xv"<y$!/t<`$Qn‚W|pgc?Cf9BI?Rf~^<ȌvtQyВ뗁9s~AP4 4wL4fY E(30BHQ/OPÝb6g›1wؽ:>?fZ t$42rdggGxp|r|f8#ќ&D># Ju{hxUH56&Y̷,[$% %GfYA%+( fYjFN?IӢ_?G~?l /ea7}<O?= ᝬ@EED(S,a)?e_Pyo1&aXVe8&8̅bF^D!EZ[%PPbI7z&fLh^a*ܘ0?nL~0Wl#jl*e3 =6}!Nߏ{(0N!L|ҙWxca\[ ޺yL`qA>;ix  _j<ʄJ?<ֳ!|x[ǔ+ l$8j@:bptbe0"߽+D0\ƅvIϦPx r.v1ϯ͓gob{ P:6ـÞt.P?h%!vGA^9g#IncPZ0/hz@XKC1 2 e%oFK0<+A`3K:tX/2|̌=10O,aFxH&1k%?qx=?Y̘ aĽ)Y=Gy  M8:JL|:I0Q&[L4tS1@f ])=sM3,$( ^1&<4!J o 2@4ϟG}9ҾR x0:,YYUx8g"E5{Q sT~^XG!\y`" c,(*aep,yaA,c n8x5}Rz @I)#(xJKL?7MGVqfu f410We3`;#,٬#f2]kgπl`w9`c`4`(.71"io-G`SX?3k̅=دK0c y|%H#iQ?{ek{!#F2~O|&wD'>s8XF0g,$X\ 2@F1GBځR16sH{|s_ GlVNH/:Fik%:׿|L &X2¿kkofχ ޢt8}ZI;V7H9GnСj)Ѫ2{FSń/E.bvCKF k0ho=-3}2VfvJ_r {I+줍Zfͧƻ'Q}:2Xam4yJu1QF,TAb7>ya-a7}nuHC襞h7eE"zKgOI l.5fxm Xy 'LP`'Q}b1/Mm $Pj) 6G:'/wuLRY *̸ss3mf< 6cYnxY:<,Wrx+L؍th_ؤl>3O6f3=QÄaw~de}_E@^/eާR5:^c㡑@A @[M6iE]s)3AXG1ޟ#~:d|̘]bAx^]s6@ID `fG'~+0:̯2'ܬkŝ(/." "  t(s" " "(/." " ]=ʜ/) _@& E@W2'" " K@BW" " "P HQ(գ̉@hR t(s" " "O-[9U" " " yN O8oݺ53E@D@D@򗀖RM@BeND@D@E@D@D@PG% E!+u((QD@D@D  HQ_J]D@D@ 4?l)VXa͝;7-M4={ZZU'.ڷoo;wNX#F؄ .KVD@D@D@@ʗ$$$PT%cy{,JQD@D@D)W իW[VȰ+Wڮ] [є_g)ߣ9JAk׮+SŭYfvWXѣ>͛7_o+V{Ý@ڵK.Ċ+f>>ێ:([~{VreۭDx^uBjԨh}=䓑p(-w}Ɲ4ڷo_֭{΍?XcqkԨNj׮mwygI" " "4"iQ 4n 3gδ~/X eʔ;ώ=X[x %СCqlǏw5ć|Ν:e#$`xW2ݷg_/SLkưa"x.]څVݻg~'tR$@v IЩS';Cun |n~7[j՜Jn((6?3{z'oqoj~oے%K즛n(#x?h￿('2qDYt6vhowݥKSNL?9@viEal2>}UTɎ;8f֠ALzr_͚5mƍֵk׈;vhsqq-[_ںu\X7I iӦgY6m F>Yzѣ-\а *U*CqV*JM%-Z%0dɥ_pL;v*U= }Xe˖F`u@›Qfaw碋.2^z+W5l-1gX>hBrZ`ҥK[Q\D@D@%P6nUV駟:.ϟjڴi֧N_3{aku[ocQ<*[fMdȒcY~9VU'|HDK.BWfMⅉuD@D@D @Vx+=ld3#6c ߿17Xr_|`nٲ9,/e޼yro1&aff͊y. fKɓݏ@(0bFOKsN >^_D71^|Ν=fȧyW_}7ze۶ms&oUh۶m$9sgo*p4o{_VaOYfjoO;[U(9%" " " @d, /_S:6mqH&|DWre΂!FD@DHHC"*WP:JKKZnO<[ WlK7żDbvA5~Sm-ucˎlm)dً7U[Y2%s얧[= (ϼ?ݾ̞5]>frSE%w1~qGN=vA0%f3k ]7k^kqUT:r/?'zjMW:ٵ+PpWstDyWU*_ʗ]o|>Fm|EǫkHD Wpy+ /qOվ5{F7mvMJVv, ayY{ɪͶm.藍@) z-/̝u*?dNQڦժVƹ3d1GNZ.ԝܙa/ i?NZi;eV=`fҠ6"oڲoXɮvQ:~:[JO7Äֽ7.[n}Tj`?Cu;6i A 3ݦyK7_Wج @|4+||G0xseZ; @e@K7⎝ZհՆM\am3UuEtDlWˆ-m(%+5E ӌ>66$ Ӱ}G-Z f?>.SW}M`AlŒſr *#oGxie9p yQHѦLE@D@D @* $~1:vݳ6[gZ1?gr} C{֩^Z|k Wlv?lrIZ[ ko˔0{F}%t/eRV_P+} MMU06=2p?L.N6uQ,1xa]NCZTAC9gW&IGs#({儍쳠L ofnHvCN2{DZ *`wt킇NqN}Y* Qzw'΃Ue2Hltܰi/뎝[t$, +\lzl(9KzF|1rI r$6!`E$٫^RעV¦;fxqmؼo5KT3~K&:dx%V|rHQȊD@D@D $9 _D@D@ҙt}]D@D@E@D@D HQHWE@D@D ) [D@D@ҙt}]D@D@E@D@D HQHWE@D@D ) [D@D@ҙ@)Բeҙ." " iI O8oݺ5-" " "ε@R3) \*$ E! y@:ε@R3) \*$ ^b}6w܈[&MXϞ=VZ]D@D@D  K}[ΝkĈ6aU', ( pXKzeҋD\QĜO'cεZm[ 1&/h>-m]8{M&|FNYvuęvWcDt"" "RG!Q>WV\{oJVmaV%f; '!ʗ-Woe'ϱ]vUT Wr>aŀ8ʖ.e^u5UժW.ﮫ(QE΢Pp7AMv]R{Z( KK$&'ۭԤqǥvs+y+" "P9BVwet26mf .֬~-[avHFb0k {`܀omERqJY%aYl}if+.ۅ'vYo~4k]Лug[Պ#=6n|w}ۋqZtpX2z yB+zvN:?C^K+5Nj%˔*Q>6oN?}8ǥk/ ]%bȘVB9;[{;A.홋[O%Eِ`3qi[ʞ xbֱ~veϣB kPD@D@ #Ib2ìqSivrֆ2p+lŚ~5=/Syg jWv7w7C ( SZU+٧#&9Z v!{kT`'ڙÑv`9ֺi˜%`X!n`pG}:5 PxP0P,PYfCtծ^ɕgcDZkVFxw死wϝpŧev ɳˆ(, U/<ي3S03#uWG=0LjV7d5F@o:x7w@3P> &ukCK0؎u-_NտP#3pŧ{~LX( ޝ[7n=eSx=+s AѸAcNx(.Y {DP|<|M1J,,3žu8KJe>︎VXZ駎@a%6’Uk\܀p ^QhRf߿ТQmM72fTwHU#7 ( ;kœ08m݆BQ_F M ʹvG$ jVsǭvo6VwGdW~A9=(rND@D@ $Qjdъ݃c+`}@ OX` 7"B0F``\\0ֽܣL+>:S`h){b Jnl^[򖘛+cݚ֫NQ X <ؑm[ܥ+Ǯ;QD@DHФI㋋|tpOlؼ ɼFu~f}6`0sC{jcun $<7nF:jRĽw ^ٵgVظ[xqV\Y+Q\dn.ma?e^sY%_9>(GW=V1#urvIDATG~ GD@DH0pFI |a2E+Xp Ǻ7/6( m/F)nѲq].Nk>qG;ȆOS /^+ŊY Wa^kƭWI[z}}>ivƑDXiR/A)(%{z>5_3؏Pz =t(^ID@D@ _ؼ)ؕ;.%K{0aw6aaD(;l ̉ټuLIy!,]<7Vf(x7%}gyN1 {4ʖ*eeJ\7I(" " Ҳ<%ܳ#|Ca_$@M@D@D@D >) GD@D@Ҟo " "  $e5k񣓏@Q" BQMED@D@RL /38]E'" " "PȢP*IY" E!+]((JRE@D@D HQ/JWD@D@ ) E/RB@@B!$eQD@D@"tE@D@DP*IY" E!+]((JRE@D@D HQ/JWD@D@ ) E/RB@@B!$eQD@D@@) .ې!Clٲeq{ٰaǨQwٗ(t5QÇۛoi 4M6o7tjjf̘a{aҥ6uTD8D@D@D - 䊢ډ'hguSxOc* iI]BB ֭[K.ֽ{wXbvڸq"Yv?9sXڵW^־}{~Νַo_?~رu饗ZŊ?/X^nݺ qF[rH>}lѢEV\9ѣw " " iL {ʔ)cwUVaEq1bi&.cZ˖-5k K,q_udp |r{ᇝK O=5n݋[oĉ3@8J\wuVT)y|sαŋ3ݧ Hw)(ڵ˞y+^M4389]r%?nݺqXvW_mwZ&L]ta , (&m۶uã>j6l;ʖ-7ol۶m۷[׮]y@@- >f6(6|75jdW[`IaÆ Xn_S| ٳz%?϶n:w6`\T6(ɓ,FYr~}rŊ5J*`eʕN:Nޟov>,ܢE {N;ͽ9f̘ND@D@D@rEQ`s?h, ~VӧOC _Zn,R[ ,06 @ zj裏u&?cǎE`o I&%[oսQtiAػ CXDҏyS~,yd"L?o<(0X{vQG(M[)Fmwo3p ',F1` H{Y6c{!O!M ¾ea 29aު%Ƚj@fJQȜ5]7`R p(k" " "(w (}((QD@D@D HQP" " "P HQ(@~^/BIENDB`django-import-export-3.3.4/docs/advanced_usage.rst000066400000000000000000001105651453511766500222520ustar00rootroot00000000000000============== Advanced usage ============== 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. Fields are generated automatically by introspection on the declared model class. The field defines the relationship between the resource we are importing (for example, a csv row) and the instance we want to update. Typically, the row data will map onto a single model instance. The row data will be set onto model instance attributes (including instance relations) during the import process. In a simple case, the name of the row headers will map exactly onto the names of the model attributes, and the import process will handle this mapping. In more complex cases, model attributes and row headers may differ, and we will need to declare explicitly declare this mapping. See :ref:`field_declaration` for more information. Declare import fields --------------------- You can optionally use the ``fields`` declaration to affect which fields are handled during import. To affect which model fields will be included in a 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') .. _field_declaration: Model relations --------------- 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',) This example declares that the ``Author.name`` value (which has a foreign key relation to ``Book``) will appear in the export. Note that declaring the relationship using this syntax sets ``field`` as readonly, meaning this field will be skipped when importing data. To understand how to import model relations, see :ref:`import_model_relations`. Explicit field declaration -------------------------- We can declare fields explicitly to give us more control over the relationship between the row and the model attribute. In the example below, we use the ``attribute`` kwarg to define the model attribute, and ``column_name`` to define the column name (i.e. row header):: from import_export.fields import Field class BookResource(resources.ModelResource): published = Field(attribute='published', column_name='published_date') class Meta: model = Book .. seealso:: :doc:`/api_fields` Available field types and options. Custom workflow based on import values -------------------------------------- You can extend the import process to add workflow based on changes to persisted model instances. For example, suppose you are importing a list of books and you require additional workflow on the date of publication. In this example, we assume there is an existing unpublished book instance which has a null 'published' field. There will be a one-off operation to take place on the date of publication, which will be identified by the presence of the 'published' field in the import file. To achieve this, we need to test the existing value taken from the persisted instance (i.e. prior to import changes) against the incoming value on the updated instance. Both ``instance`` and ``original`` are attributes of :class:`~import_export.results.RowResult`. You can override the :meth:`~import_export.resources.Resource.after_import_row` method to check if the value changes:: class BookResource(resources.ModelResource): def after_import_row(self, row, row_result, **kwargs): if getattr(row_result.original, "published") is None \ and getattr(row_result.instance, "published") is not None: # import value is different from stored value. # exec custom workflow... class Meta: model = Book store_instance = True .. note:: * The ``original`` attribute will be null if :attr:`~import_export.resources.ResourceOptions.skip_diff` is True. * The ``instance`` attribute will be null if :attr:`~import_export.resources.ResourceOptions.store_instance` is False. Field widgets ============= A widget is an object associated with each field declaration. The widget has two roles: 1. Transform the raw import data into a python object which is associated with the instance (see :meth:`.clean`). 2. Export persisted data into a suitable export format (see :meth:`.render`). There are widgets associated with character data, numeric values, dates, foreign keys. You can also define your own widget and associate it with the field. A :class:`~import_export.resources.ModelResource` creates fields with a default widget for a given field type via instrospection. If the widget should be initialized with different arguments, this can be done via an explicit declaration or via the widgets dict. For example, 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): published = Field(attribute='published', column_name='published_date', widget=DateWidget(format='%d.%m.%Y')) class Meta: model = Book Alternatively, widget parameters can be overridden using the widgets dict declaration:: class BookResource(resources.ModelResource): class Meta: model = Book widgets = { 'published': {'format': '%d.%m.%Y'}, } .. seealso:: :doc:`/api_widgets` available widget types and options. .. _import_model_relations: Importing model relations ========================= If you are importing data for a model instance which has a foreign key relationship to another model then import-export can handle the lookup and linking to the related model. Foreign Key relations --------------------- ``ForeignKeyWidget`` allows you to declare a reference to a related model. For example, if we are importing a 'book' csv file, then we can have a single field which references an author by name. :: id,title,author 1,The Hobbit, J. R. R. Tolkien We would have to declare our ``BookResource`` to use the author name as the foreign key reference:: 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, field='name')) class Meta: model = Book fields = ('author',) By default, ``ForeignKeyWidget`` will use 'pk' as the lookup field, hence we have to pass 'name' as the lookup field. This relies on 'name' being a unique identifier for the related model instance, meaning that a lookup on the related table using the field value will return exactly one result. This is implemented as a ``Model.objects.get()`` query, so if the instance in not uniquely identifiable based on the given arg, then the import process will raise either ``DoesNotExist`` or ``MultipleObjectsReturned`` errors. See also :ref:`advanced_usage:Creating non existent relations`. Refer to the :class:`~.ForeignKeyWidget` documentation for more detailed information. Many-to-many relations ---------------------- ``ManyToManyWidget`` allows you to import m2m references. For example, we can import associated categories with our book import. The categories refer to existing data in a ``Category`` table, and are uniquely referenced by category name. We use the pipe separator in the import file, which means we have to declare this in the ``ManyToManyWidget`` declaration. :: id,title,categories 1,The Hobbit,Fantasy|Classic|Movies :: class BookResource(resources.ModelResource): categories = fields.Field( column_name='categories', attribute='categories', widget=widgets.ManyToManyWidget(Category, field='name', separator='|') ) class Meta: model = Book Creating non existent relations ------------------------------- The examples above rely on the relation data being present prior to the import. It is a common use-case to create the data if it does not already exist. It is possible to achieve this as follows:: class BookResource(resources.ModelResource): def before_import_row(self, row, **kwargs): author_name = row["author"] Author.objects.get_or_create(name=author_name, defaults={"name": author_name}) class Meta: model = Book The code above can be adapted to handle m2m relationships. You can also achieve similar by subclassing the widget :meth:`~import_export.widgets.ForeignKeyWidget.clean` method to create the object if it does not already exist. Customize relation lookup ------------------------- The ``ForeignKeyWidget`` and ``ManyToManyWidget`` widgets will look for relations by searching the entire relation table for the imported value. This is implemented in the :meth:`~import_export.widgets.ForeignKeyWidget.get_queryset` method. For example, for an ``Author`` relation, the lookup calls ``Author.objects.all()``. In some cases, you may want to customize this behaviour, and it can be a requirement to pass dynamic values in. For example, suppose we want to look up authors associated with a certain publisher id. We can achieve this by passing the publisher id into the ``Resource`` constructor, which can then be passed to the widget:: class BookResource(resources.ModelResource): def __init__(self, publisher_id): super().__init__() self.fields["author"] = fields.Field( attribute="author", column_name="author", widget=AuthorForeignKeyWidget(publisher_id), ) The corresponding ``ForeignKeyWidget`` subclass:: class AuthorForeignKeyWidget(ForeignKeyWidget): model = Author field = 'name' def __init__(self, publisher_id, **kwargs): super().__init__(self.model, field=self.field, **kwargs) self.publisher_id = publisher_id def get_queryset(self, value, row, *args, **kwargs): return self.model.objects.filter(publisher_id=self.publisher_id) Then if the import was being called from another module, we would pass the ``publisher_id`` into the Resource:: >>> resource = BookResource(publisher_id=1) If you need to pass dynamic values to the Resource from an `Admin integration`_, refer to :ref:`advanced_usage:How to dynamically set resource values`. Django Natural Keys ------------------- The ``ForeignKeyWidget`` also supports using Django's natural key functions. A manager class with the ``get_by_natural_key`` function is required for importing foreign key relationships by the field model's natural key, and the model must have a ``natural_key`` function that can be serialized as a JSON list in order to export data. The primary utility for natural key functionality is to enable exporting data that can be imported into other Django environments with different numerical primary key sequences. The natural key functionality enables handling more complex data than specifying either a single field or the PK. The example below illustrates how to create a field on the ``BookResource`` that imports and exports its author relationships using the natural key functions on the ``Author`` model and modelmanager. The resource _meta option ``use_natural_foreign_keys`` enables this setting for all Models that support it. :: from import_export.fields import Field from import_export.widgets import ForeignKeyWidget class AuthorManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) class Author(models.Model): objects = AuthorManager() name = models.CharField(max_length=100) birthday = models.DateTimeField(auto_now_add=True) def natural_key(self): return (self.name,) # Only the author field uses natural foreign keys. class BookResource(resources.ModelResource): author = Field( column_name = "author", attribute = "author", widget = ForeignKeyWidget(Author, use_natural_foreign_keys=True) ) class Meta: model = Book # All widgets with foreign key functions use them. class BookResource(resources.ModelResource): class Meta: model = Book use_natural_foreign_keys = True Read more at `Django Serialization `_. Create or update model instances ================================ When you are importing a file using import-export, the file is processed row by row. For each row, the import process is going to test whether the row corresponds to an existing stored instance, or whether a new instance is to be created. If an existing instance is found, then the instance is going to be *updated* with the values from the imported row, otherwise a new row will be created. In order to test whether the instance already exists, import-export needs to use a field (or a combination of fields) in the row being imported. The idea is that the field (or fields) will uniquely identify a single instance of the model type you are importing. To define which fields identify an instance, use the ``import_id_fields`` meta attribute. You can use this declaration to indicate which field (or fields) should be used to uniquely identify the row. If you don't declare ``import_id_fields``, then a default declaration is used, in which there is only one field: 'id'. For example, you can use the 'isbn' number instead of 'id' to uniquely identify a Book as follows:: class BookResource(resources.ModelResource): class Meta: model = Book import_id_fields = ('isbn',) fields = ('isbn', 'name', 'author', 'price',) .. note:: If setting ``import_id_fields``, you must ensure that the data can uniquely identify a single row. If the chosen field(s) select more than one row, then a ``MultipleObjectsReturned`` exception will be raised. If no row is identified, then ``DoesNotExist`` exception will be raised. Access instances after import ============================= Access instance summary data ---------------------------- The instance pk and representation (i.e. output from ``repr()``) can be accessed after import:: rows = [ (1, 'Lord of the Rings'), ] dataset = tablib.Dataset(*rows, headers=['id', 'name']) resource = BookResource() result = resource.import_data(dataset) for row_result in result: print("%d: %s" % (row_result.object_id, row_result.object_repr)) Access full instance data ------------------------- All 'new', 'updated' and 'deleted' instances can be accessed after import if the :attr:`~import_export.resources.ResourceOptions.store_instance` meta attribute is set. For example, this snippet shows how you can retrieve persisted row data from a result:: class BookResourceWithStoreInstance(resources.ModelResource): class Meta: model = Book store_instance = True rows = [ (1, 'Lord of the Rings'), ] dataset = tablib.Dataset(*rows, headers=['id', 'name']) resource = BookResourceWithStoreInstance() result = resource.import_data(dataset) for row_result in result: print(row_result.instance.pk) Handling duplicate data ======================= If an existing instance is identified during import, then the existing instance will be updated, regardless of whether the data in the import row is the same as the persisted data or not. You can configure the import process to skip the row if it is duplicate by using setting ``skip_unchanged``. If ``skip_unchanged`` is enabled, then the import process will check each defined import field and perform a simple comparison with the existing instance, and if all comparisons are equal, then the row is skipped. Skipped rows are recorded in the row ``Result`` object. You can override the :meth:`~.skip_row` method to have full control over the skip row implementation. Also, the ``report_skipped`` option controls whether skipped records appear in the import ``Result`` object, and whether skipped records will show in the import preview page in the Admin UI:: class BookResource(resources.ModelResource): class Meta: model = Book skip_unchanged = True report_skipped = False fields = ('id', 'name', 'price',) .. seealso:: :doc:`/api_resources` How to set a value on all imported instances prior to persisting ================================================================ You may have a use-case where you need to set the same value on each instance created during import. For example, it might be that you need to set a value read at runtime on all instances during import. You can define your resource to take the associated instance as a param, and then set it on each import instance:: class BookResource(ModelResource): def __init__(self, publisher_id): self.publisher_id = publisher_id def before_save_instance(self, instance, using_transactions, dry_run): instance.publisher_id = self.publisher_id class Meta: model = Book See also :ref:`advanced_usage:How to dynamically set resource values`. 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 It is also possible to pass a method name in to the :meth:`~import_export.fields.Field` constructor. If this method name is supplied, then that method will be called as the 'dehydrate' method. Filtering querysets during export ================================= You can use :meth:`~import_export.resources.Resource.filter_export` to filter querysets during export. See also `Customize admin export forms`_. 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 ================= One of the main features of import-export is the support for integration with the `Django Admin site `_. This provides a convenient interface for importing and exporting Django objects. Please install and run the :ref:`example application` to become familiar with Admin integration. Integrating import-export with your application requires extra configuration. 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_classes = [BookResource] admin.site.register(Book, BookAdmin) Once this configuration is present (and server is restarted), 'import' and 'export' buttons will be presented to the user. Clicking each button will open a workflow where the user can select the type of import or export. You can assign multiple resources to the ``resource_classes`` attribute. These resources will be presented in a select dropdown in the UI. .. _change-screen-figure: .. figure:: _static/images/django-import-export-change.png A screenshot of the change view with Import and Export buttons. Exporting via admin action -------------------------- Another approach to exporting data is by subclassing :class:`~import_export.admin.ExportActionModelAdmin` 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 ExportActionModelAdmin class BookAdmin(ExportActionModelAdmin): 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``. 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`. By default, import is a two step process, though it can be configured to be a single step process (see :ref:`IMPORT_EXPORT_SKIP_ADMIN_CONFIRM`). The two step process is: 1. Select the file and format for import. 2. Preview the import data and confirm import. .. _confirm-import-figure: .. 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. Import confirmation ------------------- To support import confirmation, uploaded data is written to temporary storage after step 1 (:ref:`choose file`), and read back for final import after step 2 (:ref:`import confirmation`). There are three mechanisms for temporary storage. #. Temporary file storage on the host server (default). This is suitable for development only. Use of temporary filesystem storage is not recommended for production sites. #. The `Django cache `_. #. `Django storage `_. To modify which storage mechanism is used, please refer to the setting :ref:`IMPORT_EXPORT_TMP_STORAGE_CLASS`. Temporary resources are removed when data is successfully imported after the confirmation step. Your choice of temporary storage will be influenced by the following factors: * Sensitivity of the data being imported. * Volume and frequency of uploads. * File upload size. * Use of containers or load-balanced servers. .. warning:: If users do not complete the confirmation step of the workflow, or if there are errors during import, then temporary resources may not be deleted. This will need to be understood and managed in production settings. For example, using a cache expiration policy or cron job to clear stale resources. 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 your customized form(s), change the respective attributes on your ``ModelAdmin`` class: * :attr:`~import_export.admin.ImportMixin.import_form_class` * :attr:`~import_export.admin.ImportMixin.confirm_form_class` 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. .. note:: Importing an E-Book using the :ref:`example application` demonstrates this. .. figure:: _static/images/custom-import-form.png A screenshot of a customized import view. Customize forms (for example see ``tests/core/forms.py``):: 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`` (for example see ``tests/core/admin.py``):: class CustomBookAdmin(ImportMixin, admin.ModelAdmin): resource_classes = [BookResource] import_form_class = CustomImportForm confirm_form_class = CustomConfirmImportForm def get_confirm_form_initial(self, request, import_form): initial = super().get_confirm_form_initial(request, import_form) # Pass on the `author` value from the import form to # the confirm form (if provided) if import_form: initial['author'] = import_form.cleaned_data['author'] return initial admin.site.register(Book, CustomBookAdmin) To further customize the import forms, you might like to consider overriding the following :class:`~import_export.admin.ImportMixin` methods: * :meth:`~import_export.admin.ImportMixin.get_import_form_class` * :meth:`~import_export.admin.ImportMixin.get_import_form_kwargs` * :meth:`~import_export.admin.ImportMixin.get_import_form_initial` * :meth:`~import_export.admin.ImportMixin.get_confirm_form_class` * :meth:`~import_export.admin.ImportMixin.get_confirm_form_kwargs` For example, to pass extract form values (so that they get passed to the import process):: def get_import_data_kwargs(self, request, *args, **kwargs): """ Return form data as kwargs for import_data. """ form = kwargs.get('form') if form: return form.cleaned_data return {} The parameters can then be read from ``Resource`` methods, such as: * :meth:`~import_export.resources.Resource.before_import` * :meth:`~import_export.resources.Resource.before_import_row` .. seealso:: :doc:`/api_admin` available mixins and options. Customize admin export forms ---------------------------- It is also possible to add fields to the export form so that export data can be filtered. For example, we can filter exports by Author. .. figure:: _static/images/custom-export-form.png A screenshot of a customized export view. Customize forms (for example see ``tests/core/forms.py``):: class CustomExportForm(AuthorFormMixin, ExportForm): """Customized ExportForm, with author field required.""" author = forms.ModelChoiceField( queryset=Author.objects.all(), required=True) Customize ``ModelAdmin`` (for example see ``tests/core/admin.py``):: class CustomBookAdmin(ImportMixin, ImportExportModelAdmin): resource_classes = [EBookResource] export_form_class = CustomExportForm def get_export_resource_kwargs(self, request, *args, **kwargs): export_form = kwargs["export_form"] if export_form: return dict(author_id=export_form.cleaned_data["author"].id) return {} admin.site.register(Book, CustomBookAdmin) Create a Resource subclass to apply the filter (for example see ``tests/core/admin.py``):: class EBookResource(ModelResource): def __init__(self, **kwargs): super().__init__() self.author_id = kwargs.get("author_id") def filter_export(self, queryset, *args, **kwargs): return queryset.filter(author_id=self.author_id) class Meta: model = EBook In this example, we can filter an EBook export using the author's name. 1. Create a custom form which defines 'author' as a required field. 2. Create a 'CustomBookAdmin' class which defines a :class:`~import_export.resources.Resource`, and overrides :meth:`~import_export.mixins.BaseExportMixin.get_export_resource_kwargs`. This ensures that the author id will be passed to the :class:`~import_export.resources.Resource` constructor. 3. Create a :class:`~import_export.resources.Resource` which is instantiated with the ``author_id``, and can filter the queryset as required. Using multiple resources ------------------------ It is possible to set multiple resources both to import and export `ModelAdmin` classes. The ``ImportMixin``, ``ExportMixin``, ``ImportExportMixin`` and ``ImportExportModelAdmin`` classes accepts subscriptable type (list, tuple, ...) as ``resource_classes`` parameter. The subscriptable could also be returned from one of the following: * :meth:`~import_export.mixins.BaseImportExportMixin.get_resource_classes` * :meth:`~import_export.mixins.BaseImportMixin.get_import_resource_classes` * :meth:`~import_export.mixins.BaseExportMixin.get_export_resource_classes` If there are multiple resources, the resource chooser appears in import/export admin form. The displayed name of the resource can be changed through the `name` parameter of the `Meta` class. Use multiple resources:: from import_export import resources from core.models import Book class BookResource(resources.ModelResource): class Meta: model = Book class BookNameResource(resources.ModelResource): class Meta: model = Book fields = ['id', 'name'] name = "Export/Import only book names" class CustomBookAdmin(ImportMixin, admin.ModelAdmin): resource_classes = [BookResource, BookNameResource] .. _dynamically_set_resource_values: How to dynamically set resource values -------------------------------------- There are a few use cases where it is desirable to dynamically set values in the `Resource`. For example, suppose you are importing via the Admin console and want to use a value associated with the authenticated user in import queries. Suppose the authenticated user (stored in the ``request`` object) has a property called ``publisher_id``. During import, we want to filter any books associated only with that publisher. First of all, override the ``get_import_resource_kwargs()`` method so that the request user is retained:: class BookAdmin(ImportExportMixin, admin.ModelAdmin): # attribute declarations not shown def get_import_resource_kwargs(self, request, *args, **kwargs): kwargs = super().get_resource_kwargs(request, *args, **kwargs) kwargs.update({"user": request.user}) return kwargs Now you can add a constructor to your ``Resource`` to store the user reference, then override ``get_queryset()`` to return books for the publisher:: class BookResource(ModelResource): def __init__(self, user): self.user = user def get_queryset(self): return self._meta.model.objects.filter(publisher_id=self.user.publisher_id) class Meta: model = Book Select items for export ----------------------- It's possible to configure the Admin UI so that users can select which items they want to export: .. image:: _static/images/select-for-export.png :alt: Select items for export To do this, simply declare an Admin instance which includes :class:`~import_export.admin.ExportActionMixin`:: class BookAdmin(ImportExportModelAdmin, ExportActionMixin): # additional config can be supplied if required pass Then register this Admin:: admin.site.register(Book, BookAdmin) Note that the above example refers specifically to the :ref:`example application`, you'll have to modify this to refer to your own Model instances. .. _interoperability: Interoperability with 3rd party libraries ----------------------------------------- import_export extends the Django Admin interface. There is a possibility that clashes may occur with other 3rd party libraries which also use the admin interface. django-admin-sortable2 ^^^^^^^^^^^^^^^^^^^^^^ Issues have been raised due to conflicts with setting `change_list_template `_. There is a workaround listed `here `_. Also, refer to `this issue `_. If you want to patch your own installation to fix this, a patch is available `here `_. django-polymorphic ^^^^^^^^^^^^^^^^^^ Refer to `this issue `_. template skipped due to recursion issue ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Refer to `this issue `_. .. _admin_security: Security -------- Enabling the Admin interface means that you should consider the security implications. Some or all of the following points may be relevant: Is there potential for untrusted imports? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * What is the source of your import file? * Is this coming from an external source where the data could be untrusted? * Could source data potentially contain malicious content such as script directives or Excel formulae? * Even if data comes from a trusted source, is there any content such as HTML which could cause issues when rendered in a web page? What is the potential risk for exported data? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * If there is malicious content in stored data, what is the risk of exporting this data? * Could untrusted input be executed within a spreadsheet? * Are spreadsheets sent to other parties who could inadvertently execute malicious content? * Could data be exported to other formats, such as CSV, TSV or ODS, and then opened using Excel? * Could any exported data be rendered in HTML? For example, csv is exported and then loaded into another web application. In this case, untrusted input could contain malicious code such as active script content. Mitigating security risks ^^^^^^^^^^^^^^^^^^^^^^^^^ By default, import-export does not sanitize or process imported data. Malicious content, such as script directives, can be imported into the database, and can be exported without any modification. You can optionally configure import-export to sanitize data on export. There are two settings which enable this: #. :ref:`IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT` #. :ref:`IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT` .. warning:: Enabling these settings only sanitizes data exported using the Admin Interface. If exporting data :ref:`programmatically`, then you will need to apply your own sanitization. Limiting the available import or export types can be considered. This can be done using either of the following settings: #. :ref:`IMPORT_EXPORT_FORMATS` #. :ref:`IMPORT_FORMATS` #. :ref:`EXPORT_FORMATS` You should in all cases review `Django security documentation `_ before deploying a live Admin interface instance. Please refer to `SECURITY.md `_ for details on how to escalate security issues. django-import-export-3.3.4/docs/api_admin.rst000066400000000000000000000002641453511766500212340ustar00rootroot00000000000000===== 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-3.3.4/docs/api_fields.rst000066400000000000000000000001151453511766500214050ustar00rootroot00000000000000====== Fields ====== .. autoclass:: import_export.fields.Field :members: django-import-export-3.3.4/docs/api_forms.rst000066400000000000000000000001601453511766500212650ustar00rootroot00000000000000===== Forms ===== .. module:: import_export.forms .. autoclass:: ImportForm .. autoclass:: ConfirmImportForm django-import-export-3.3.4/docs/api_instance_loaders.rst000066400000000000000000000003131453511766500234540ustar00rootroot00000000000000================ Instance loaders ================ .. module:: import_export.instance_loaders .. autoclass:: BaseInstanceLoader .. autoclass:: ModelInstanceLoader .. autoclass:: CachedInstanceLoader django-import-export-3.3.4/docs/api_resources.rst000066400000000000000000000006601453511766500221560ustar00rootroot00000000000000========= 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-3.3.4/docs/api_results.rst000066400000000000000000000004611453511766500216440ustar00rootroot00000000000000======= Results ======= .. currentmodule:: import_export.results Result ------ .. autoclass:: import_export.results.Result :members: RowResult --------- .. autoclass:: import_export.results.RowResult :members: InvalidRow --------- .. autoclass:: import_export.results.InvalidRow :members: django-import-export-3.3.4/docs/api_tmp_storages.rst000066400000000000000000000006311453511766500226510ustar00rootroot00000000000000================== 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-3.3.4/docs/api_widgets.rst000066400000000000000000000015301453511766500216070ustar00rootroot00000000000000======= Widgets ======= .. autoclass:: import_export.widgets.Widget :members: .. autoclass:: import_export.widgets.NumberWidget :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-3.3.4/docs/bulk_import.rst000066400000000000000000000062501453511766500216430ustar00rootroot00000000000000============= Bulk imports ============= import-export provides a 'bulk mode' to improve the performance of importing large datasets. In normal operation, 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 returned as critical (non-validation) errors (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. Testing ======= Scripts are provided to enable testing and benchmarking of bulk imports. See :ref:`testing:Bulk testing`.django-import-export-3.3.4/docs/celery.rst000066400000000000000000000003331453511766500205730ustar00rootroot00000000000000=========== 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-3.3.4/docs/changelog.rst000066400000000000000000000604671453511766500212550ustar00rootroot00000000000000Changelog ========= 3.3.4 (2023-12-09) ------------------ - Added support for django5 (#1634) - Show list of exported fields in Admin UI (#1685) - Added `CONTRIBUTING.md` - Added support for python 3.12 (#1698) - Update Finnish translations (#1701) 3.3.3 (2023-11-11) ------------------ - :meth:`~import_export.admin.ExportActionMixin.export_admin_action` can be overridden by subclassing it in the ``ModelAdmin`` (#1681) 3.3.2 (2023-11-09) ------------------ - Updated Spanish translations (#1639) - Added documentation and tests for retrieving instance information after import (#1643) - :meth:`~import_export.widgets.NumberWidget.render` returns ``None`` as empty string if ``coerce_to_string`` is True (#1650) - Updated documentation to describe how to select for export in Admin UI (#1670) - Added catch for django5 deprecation warning (#1676) - Updated and compiled message files (#1678) 3.3.1 (2023-09-14) ------------------ - Added `.readthedocs.yaml` (#1625) 3.3.0 (2023-09-14) ------------------ Deprecations ############ - Remove 'escape output' deprecation (#1618) - Removal of deprecated :ref:`IMPORT_EXPORT_ESCAPE_OUTPUT_ON_EXPORT`. - Deprecation of :ref:`IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT`. Refer to :ref:`installation` docs. Enhancements ############ - Refactoring and fix to support filtering exports (#1579) - Store ``instance`` and ``original`` object in :class:`~import_export.results.RowResult` (#1584) - Add customizable blocks in import.html (#1598) - Include 'allowed formats' settings (#1606) - Add kwargs to enable CharWidget to return values as strings (#1623) Internationalization #################### - Add Finnish translation (#1588) - Updated ru translation (#1604) - Fixed badly formatted translation string (#1622) - Remove 'escape output' deprecation (#1618) Fixes ##### - Do not decode bytes when writing to MediaStorage (#1615) - Fix for cache entries not removed (#1621) Development ########### - Added support for Django 4.2 (#1570) - Add automatic formatting and linting (#1571) - removed duplicate admin integration tests (#1616) - Removed support for python3.7 and django4.0 (past EOL) (#1618) Documentation ############# - Updated documentation for interoperability with third party libraries (#1614) 3.2.0 (2023-04-12) ------------------ - Escape formulae on export to XLSX (#1568) - This includes deprecation of :ref:`IMPORT_EXPORT_ESCAPE_OUTPUT_ON_EXPORT`. Refer to :ref:`installation` for alternatives. - :meth:`import_export.formats.TablibFormat.export()`: ``escape_output`` flag now deprecated in favour of ``escape_html`` and ``escape_formulae``. - Refactor methods so that ``args`` are declared correctly (#1566) - This includes deprecations to be aware of if you have overridden :meth:`~import_export.resources.Resource.export` or :class:`~import_export.forms.ImportExportFormBase`. - ``export()``: If passing ``queryset`` as the first arg, ensure this is passed as a named parameter. - ``ImportExportFormBase``: If passing ``resources`` to ``__init__`` as the first arg, ensure this is passed as a named parameter. - Updated ``setup.py`` (#1564) - Added ``SECURITY.md`` (#1563) - Updated FAQ to include workaround for `RelatedObjectDoesNotExist` exception (#1562) - Prevent error comparing m2m field of the new objects (#1560) - Add documentation for passing data from admin form to Resource (#1555) - Added new translations to Spanish and Spanish (Argentina) (#1552) - Pass kwargs to import_set function (#1448) 3.1.0 (2023-02-21) ------------------ - Add optional dehydrate method param (#1536) - ``exceptions`` module has been undeprecated - Updated DE translation (#1537) - Add option for single step import via Admin Site (#1540) - Add support for m2m add (#1545) - collect errors on bulk operations (#1541) - this change causes bulk import errors to be logged at DEBUG level not EXCEPTION. - Improve bulk import performance (#1539) - ``raise_errors`` has been deprecated as a kwarg in ``import_row()`` - Reduce memory footprint during import (#1542) - documentation updates (#1533) - add detailed format parameter docstrings to ``DateWidget`` and ``TimeWidget`` (#1532) - tox updates (#1534) - fix xss vulnerability in html export (#1546) 3.0.2 (2022-12-13) ------------------ - Support Python 3.11 (#1508) - use ``get_list_select_related`` in ``ExportMixin`` (#1511) - bugfix: handle crash on start-up when ``change_list_template`` is a property (#1523) - bugfix: include instance info in row result when row is skipped (#1526) - bugfix: add ``**kwargs`` param to ``Resource`` constructor (#1527) 3.0.1 (2022-10-18) ------------------ - Updated ``django-import-export-ci.yml`` to fix node.js deprecation - bugfix: ``DateTimeWidget.clean()`` handles tz aware datetime (#1499) - Updated translations for v3.0.0 release (#1500) 3.0.0 (2022-10-18) ------------------ Breaking changes ################ This release makes some minor changes to the public API. If you have overridden any methods from the ``resources`` or ``widgets`` modules, you may need to update your implementation to accommodate these changes. - Check value of ``ManyToManyField`` in ``skip_row()`` (#1271) - This fixes an issue where ManyToMany fields are not checked correctly in ``skip_row()``. This means that ``skip_row()`` now takes ``row`` as a mandatory arg. If you have overridden ``skip_row()`` in your own implementation, you will need to add ``row`` as an arg. - Bug fix: validation errors were being ignored when ``skip_unchanged`` is set (#1378) - If you have overridden ``skip_row()`` you can choose whether or not to skip rows if validation errors are present. The default behavior is to not to skip rows if there are validation errors during import. - Use 'create' flag instead of instance.pk (#1362) - ``import_export.resources.save_instance()`` now takes an additional mandatory argument: ``is_create``. If you have overridden ``save_instance()`` in your own code, you will need to add this new argument. - ``widgets``: Unused ``*args`` params have been removed from method definitions. (#1413) - If you have overridden ``clean()`` then you should update your method definition to reflect this change. - ``widgets.ForeignKeyWidget`` / ``widgets.ManyToManyWidget``: The unused ``*args`` param has been removed from ``__init__()``. If you have overridden ``ForeignKeyWidget`` or ``ManyToManyWidget`` you may need to update your implementation to reflect this change. - Admin interface: Modified handling of import errors (#1306) - Exceptions raised during the import process are now presented as form errors, instead of being wrapped in a \ tag in the response. If you have any custom logic which uses the error written directly into the response, then this may need to be changed. - ImportForm: improve compatibility with previous signature (#1434) - Previous ``ImportForm`` implementation was based on Django's ``forms.Form``, if you have any custom ImportForm you now need to inherit from ``import_export.forms.ImportExportFormBase``. - Allow custom ``change_list_template`` in admin views using mixins (#1483) - If you are using admin mixins from this library in conjunction with code that overrides ``change_list_template`` (typically admin mixins from other libraries such as django-admin-sortable2 or reversion), object tools in the admin change list views may render differently now. - If you have created a custom template which extends any import_export template, then this may now cause a recursion error (see #1514) - ``import.html``: Added blocks to import template (#1488) - If you have made customizations to the import template then you may need to refactor these after the addition of block declarations. Deprecations ############ This release adds some deprecations which will be removed in a future release. - Add support for multiple resources in ModelAdmin. (#1223) - The ``*Mixin.resource_class`` accepting single resource has been deprecated and the new ``*Mixin.resource_classes`` accepting subscriptable type (list, tuple, ...) has been added. - Same applies to all of the ``get_resource_class``, ``get_import_resource_class`` and ``get_export_resource_class`` methods. - Deprecated ``exceptions.py`` (#1372) - Refactored form-related methods on ``ImportMixin`` / ``ExportMixin`` (#1147) - The following are deprecated: - ``get_import_form()`` - ``get_confirm_import_form()`` - ``get_form_kwargs()`` - ``get_export_form()`` Enhancements ############ - Default format selections set correctly for export action (#1389) - Added option to store raw row values in each row's ``RowResult`` (#1393) - Add natural key support to ``ForeignKeyWidget`` (#1371) - Optimised default instantiation of ``CharWidget`` (#1414) - Allow custom ``change_list_template`` in admin views using mixins (#1483) - Added blocks to import template (#1488) - improve compatibility with previous ImportForm signature (#1434) - Refactored form-related methods on ``ImportMixin`` / ``ExportMixin`` (#1147) - Include custom form media in templates (#1038) - Remove unnecessary files generated when running tox locally (#1426) Fixes ##### - Fixed Makefile coverage: added ``coverage combine`` - Fixed handling of LF character when using ``CacheStorage`` (#1417) - bugfix: ``skip_row()`` handles M2M field when UUID pk used - Fix broken link to tablib formats page (#1418) - Fix broken image ref in ``README.rst`` - bugfix: ``skip_row()`` fix crash when model has m2m field and none is provided in upload (#1439) - Fix deprecation in example application: Added support for transitional form renderer (#1451) Development ########### - Increased test coverage, refactored CI build to use tox (#1372) Documentation ############# - Clarified issues around the usage of temporary storage (#1306) 2.9.0 (2022-09-14) ------------------ - Fix deprecation in example application: Added support for transitional form renderer (#1451) - Escape HTML output when rendering decoding errors (#1469) - Apply make_aware when the original file contains actual datetimes (#1478) - Automatically guess the format of the file when importing (#1460) 2.8.0 (2022-03-31) ------------------ - Updated import.css to support dark mode (#1318) - Fix crash when import_data() called with empty Dataset and ``collect_failed_rows=True`` (#1381) - Improve Korean translation (#1402) - Update example subclass widget code (#1407) - Drop support for python3.6, django 2.2, 3.0, 3.1 (#1408) - Add get_export_form() to ExportMixin (#1409) 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-3.3.4/docs/conf.py000066400000000000000000000072031453511766500200600ustar00rootroot00000000000000import os import sys import django # 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" django.setup() # -- General configuration ----------------------------------------------------- # 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", "sphinx.ext.autosectionlabel", ] autosectionlabel_prefix_document = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "django-import-export" copyright = "2012–2023, 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 name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- 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" # 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"] # Output file base name for HTML help builder. htmlhelp_basename = "django-import-export" # -- Options for LaTeX output -------------------------------------------------- # 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", ), ] # -- 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, ) ] # -- 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-3.3.4/docs/contributing.rst000066400000000000000000000124211453511766500220200ustar00rootroot00000000000000.. _contributing: ############ Contributing ############ 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 `Code of Conduct `_. 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 main branch should always be stable, production-ready & passing all tests. .. _question: Questions --------- Please check the :ref:`common issues ` section of the :doc:`FAQ ` to see if your question already has an answer. For general questions about usage, we recommend posting to Stack Overflow, using the `django-import-export `_ tag. Please search existing answers to see if any match your problem. If not, post a new question including as much relevant detail as you can. See `how to ask `_ for more details. For questions about the internals of the library, please raise an `issue `_ and use the 'question' workflow. * First check to see if there is an existing issue which answers your question. * Remember to include as much detail as you can so that your question is answered in a timely manner. Guidelines For Reporting An Issue/Feature ----------------------------------------- So you've found a bug or have a great idea for a feature. Here are 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 `_ or `pull request `_ for the bug/feature. * 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 any 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 and fix the problem. Guidelines For Contributing Code -------------------------------- If you're ready to take the plunge and contribute back some code or documentation please consider the following: * Search existing issues and PRs to see if there are already any similar proposals. * For substantial changes, we recommend raising a question_ first so that we can offer any advice or pointers based on previous experience. 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 and you feel you have a better fix, please take note of the issue number and 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 and referencing any related issues/pull requests. * We recommend setting up your editor to automatically indicate non-conforming styles (see `Development`_). 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`_). * 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. .. _`AUTHORS`: https://github.com/django-import-export/django-import-export/blob/main/AUTHORS Development ----------- * All files should be formatted using the black auto-formatter. This will be run by pre-commit if configured. * The project repository includes an ``.editorconfig`` file. We recommend using a text editor with EditorConfig support to avoid indentation and whitespace issues. * We allow up to 88 characters as this is the line length used by black. This check is included when you run flake8. Documentation, comments, and docstrings should be wrapped at 79 characters, even though PEP 8 suggests 72. * To install pre-commit:: python -m pip install pre-commit Then run:: pre-commit install django-import-export-3.3.4/docs/faq.rst000077500000000000000000000165301453511766500200700ustar00rootroot00000000000000========================== Frequently Asked Questions ========================== What's the best way to communicate a problem, question, or suggestion? ====================================================================== To submit a feature, to report a bug, or to ask a question, please refer our :doc:`contributing guidelines `. How can I help? =============== We welcome contributions from the community. You can help in the following ways: * Reporting bugs or issues. * Answering questions which arise on Stack Overflow or as Github issues. * Providing translations for UI text. * Suggesting features or changes. We encourage you to read the :doc:`contributing guidelines `. .. _common_issues: Common issues ============= key error 'id' in ``get_import_id_fields()`` -------------------------------------------- When attempting to import, this error can be seen. This indicates that the ``Resource`` has not been configured correctly, and the import logic fails. Specifically, the import process is looking for an instance field called ``id`` and there is no such field in the import. See :ref:`advanced_usage:Create or update model instances`. How to handle double-save from Signals -------------------------------------- This issue can apply if you have implemented post-save :ref:`advanced_usage:signals`, and you are using the import workflow in the Admin interface. You will find that the post-save signal is called twice for each instance. The reason for this is that the model ``save()`` method is called twice: once for the 'confirm' step and once for the 'import' step. The call to ``save()`` during the 'confirm' step is necessary to prove that the object will be saved successfully, or to report any exceptions in the Admin UI if save failed. After the 'confirm' step, the database transaction is rolled back so that no changes are persisted. Therefore there is no way at present to stop ``save()`` being called twice, and there will always be two signal calls. There is a workaround, which is to set a temporary flag on the instance being saved:: class BookResource(resources.ModelResource): def before_save_instance(self, instance, using_transactions, dry_run): # during 'confirm' step, dry_run is True instance.dry_run = dry_run class Meta: model = Book fields = ('id', 'name') Your signal receiver can then include conditional logic to handle this flag:: @receiver(post_save, sender=Book) def my_callback(sender, **kwargs): instance = kwargs["instance"] if getattr(instance, "dry_run"): # no-op if this is the 'confirm' step return else: # your custom logic here # this will be executed only on the 'import' step pass Further discussion `here `_ and `here `_. How to dynamically set resource values -------------------------------------- There can be use cases where you need a runtime or user supplied value to be passed to a Resource. See :ref:`dynamically_set_resource_values`. How to set a value on all imported instances prior to persisting ---------------------------------------------------------------- If you need to set the same value on each instance created during import then refer to :ref:`advanced_usage:How to set a value on all imported instances prior to persisting`. How to export from more than one table -------------------------------------- In the usual configuration, a ``Resource`` maps to a single model. If you want to export data associated with relations to that model, then these values can be defined in the ``fields`` declaration. See :ref:`advanced_usage:Model relations`. How to import imagefield in excel cell -------------------------------------- Please refer to `this issue `_. How to hide stack trace in UI error messages -------------------------------------------- Please refer to `this issue `_. Ids incremented twice during import ----------------------------------- When importing using the Admin site, it can be that the ids of the imported instances are different from those show in the preview step. This occurs because the rows are imported during 'confirm', and then the transaction is rolled back prior to the confirm step. Database implementations mean that sequence numbers may not be reused. Consider enabling :ref:`IMPORT_EXPORT_SKIP_ADMIN_CONFIRM` as a workaround. See `this issue `_ for more detailed discussion. Not Null constraint fails when importing blank Charfield -------------------------------------------------------- See `this issue `_. Foreign key is null when importing ---------------------------------- It is possible to reference model relations by defining a field with the double underscore syntax. For example:: fields = ("author__name") This means that during export, the relation will be followed and the referenced field will be added correctly to the export. This does not work during import because the reference may not be enough to identify the correct relation instance. :class:`~import_export.widgets.ForeignKeyWidget` should be used during import. See the documentation explaining :ref:`advanced_usage:Foreign Key relations`. How to customize export data ---------------------------- See the following responses on StackOverflow: * https://stackoverflow.com/a/55046474/39296 * https://stackoverflow.com/questions/74802453/export-only-the-data-registered-by-the-user-django-import-export How to set export file encoding ------------------------------- If export produces garbled or unexpected output, you may need to set the export encoding. See `this issue `_. How to create relation during import if it does not exist --------------------------------------------------------- See :ref:`advanced_usage:Creating non existent relations`. How to handle large file uploads --------------------------------- If uploading large files, you may encounter time-outs. See :ref:`Celery:Using celery to perform imports` and :ref:`bulk_import:Bulk imports`. How to use field other than `id` in Foreign Key lookup ------------------------------------------------------ See :ref:`advanced_usage:Foreign key relations`. ``RelatedObjectDoesNotExist`` exception during import ----------------------------------------------------- This can occur if a model defines a ``__str__()`` method which references a primary key or foreign key relation, and which is ``None`` during import. There is a workaround to deal with this issue. Refer to `this comment `_. 'failed to assign change_list_template attribute' warning in logs ----------------------------------------------------------------- This indicates that the change_list_template attribute could not be set, most likely due to a clash with a third party library. Refer to :ref:`interoperability`. django-import-export-3.3.4/docs/getting_started.rst000066400000000000000000000111721453511766500225020ustar00rootroot00000000000000=============== Getting started =============== Introduction ============ This section describes how to get started with import-export. We'll use the :ref:`example application` as a guide. import-export can be used programmatically as described here, or it can be integrated with the :ref:`Django Admin interface`. Test data ========= There are sample files which can be used to test importing data in the `tests/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 a resource =============================== To integrate 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 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 (or combination of fields) which uniquely identifies an instance always needs to be present. This is so that the import process can manage creates / updates. In this case, we use ``id``. For more information, see :ref:`advanced_usage:Create or update model instances`. 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 .. _exporting_data: Exporting data ============== Now that we have defined a :class:`~import_export.resources.ModelResource` class, we can export books:: >>> from core.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 .. warning:: Data exported programmatically is not sanitized for malicious content. You will need to understand the implications of this and handle accordingly. See :ref:`admin_security`. django-import-export-3.3.4/docs/import_workflow.rst000066400000000000000000000137521453511766500225650ustar00rootroot00000000000000==================== 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. 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-3.3.4/docs/index.rst000066400000000000000000000024031453511766500204170ustar00rootroot00000000000000====================== Django import / export ====================== django-import-export is a Django application and library for importing and exporting data with included admin integration. **Features:** * Import from / Export to multiple file formats * Manage import / export of object relations, data types * Handle create / update / delete / skip during imports * Extensible API * Support multiple formats (Excel, CSV, JSON, ... and everything else that `tablib`_ supports) * Bulk import * Admin integration for importing / exporting * Preview import changes * 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 advanced_usage import_workflow bulk_import celery testing faq 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 .. toctree:: :maxdepth: 2 :caption: Developers contributing .. _`tablib`: https://github.com/jazzband/tablib django-import-export-3.3.4/docs/installation.rst000066400000000000000000000147611453511766500220230ustar00rootroot00000000000000============================== 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.html 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 ``True``. 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: ``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. ``IMPORT_EXPORT_SKIP_ADMIN_CONFIRM`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If ``True``, no import confirmation page will be presented to the user in the Admin UI. The file will be imported in a single step. By default, the import will occur in a transaction. If the import causes any runtime errors (including validation errors), then the errors are presented to the user and then entire transaction is rolled back. Note that if you disable transaction support via configuration (or if your database does not support transactions), then validation errors will still be presented to the user but valid rows will have imported. ``IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If set to ``True``, strings will be HTML escaped. By default this is ``False``. This is deprecated and will be removed in a future release. Future releases will escape strings by default. ``IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If set to ``True``, strings will be sanitized by removing any leading '=' character. This is to prevent execution of Excel formulae. By default this is ``False``. ``IMPORT_EXPORT_FORMATS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A list that defines which file formats will be allowed during imports and exports. Defaults to ``import_export.formats.base_formats.DEFAULT_FORMATS``. The values must be those provided in ``import_export.formats.base_formats`` e.g .. code-block:: python # settings.py from import_export.formats.base_formats import XLSX IMPORT_EXPORT_FORMATS = [XLSX] ``IMPORT_FORMATS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A list that defines which file formats will be allowed during imports. Defaults to ``IMPORT_EXPORT_FORMATS``. The values must be those provided in ``import_export.formats.base_formats`` e.g .. code-block:: python # settings.py from import_export.formats.base_formats import CSV, XLSX IMPORT_FORMATS = [CSV, XLSX] ``EXPORT_FORMATS`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A list that defines which file formats will be allowed during exports. Defaults to ``IMPORT_EXPORT_FORMATS``. The values must be those provided in ``import_export.formats.base_formats`` e.g .. code-block:: python # settings.py from import_export.formats.base_formats import XLSX EXPORT_FORMATS = [XLSX] .. _exampleapp: 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 author.json category.json book.json ./manage.py runserver Go to http://127.0.0.1:8000 For example import files, see :ref:`getting_started:Test data`. django-import-export-3.3.4/docs/make.bat000066400000000000000000000120061453511766500201630ustar00rootroot00000000000000@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-3.3.4/docs/testing.rst000066400000000000000000000040061453511766500207660ustar00rootroot00000000000000Testing ======= All tests can be run using `tox `_ simply by running the `tox` command. By default, tests are run against a local sqlite2 instance. `pyenv `_ can be used to manage multiple python installations. MySql / Postgres tests ###################### By using Docker, you can also run tests against either a MySQL db and/or Postgres. The ``IMPORT_EXPORT_TEST_TYPE`` must be set according to the type of tests you wish to run. Set to 'postgres' for postgres tests, and 'mysql-innodb' for mysql tests. If this environment variable is blank (or is any other value) then the default sqlite2 db will be used. This process is scripted in ``runtests.sh``. Assuming that you have docker installed on your system, running ``runtests.sh`` will run tox against sqlite2, mysql and postgres. You can edit this script to customise testing as you wish. Note that this is the process which is undertaken by CI builds. Coverage ######## Coverage data is written in parallel mode by default (defined in ``setup.cfg``). After a tox run, you can view coverage data as follows: .. code-block:: bash # combine all coverage data generated by tox into one file coverage combine # produce an HTML coverage report coverage html Check the output of the above commands to locate the coverage HTML file. Bulk testing ############ There is a helper script available to generate and profile bulk loads. See ``scripts/bulk_import.py``. You can use this script by configuring environment variables as defined above, and then installing and running the test application. In order to run the helper script, you will need to install ``requirements/test.txt``, and then add `django-extensions` to `settings.py` (`INSTALLED_APPS`). You can then run the script as follows: .. code-block:: 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 django-import-export-3.3.4/import_export/000077500000000000000000000000001453511766500205425ustar00rootroot00000000000000django-import-export-3.3.4/import_export/__init__.py000066400000000000000000000000261453511766500226510ustar00rootroot00000000000000__version__ = "3.3.4" django-import-export-3.3.4/import_export/admin.py000066400000000000000000001066331453511766500222150ustar00rootroot00000000000000import logging import warnings import 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.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, ImportExportFormBase, 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 from .utils import original logger = logging.getLogger(__name__) class ImportExportMixinBase: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.init_change_list_template() def init_change_list_template(self): # Store already set change_list_template to allow users to independently # customize the change list object tools. This treats the cases where # `self.change_list_template` is `None` (the default in `ModelAdmin`) or # where `self.import_export_change_list_template` is `None` as falling # back on the default templates. if getattr(self, "change_list_template", None): self.base_change_list_template = self.change_list_template else: self.base_change_list_template = "admin/change_list.html" try: self.change_list_template = getattr( self, "import_export_change_list_template", None ) except AttributeError: logger.warning("failed to assign change_list_template attribute") if self.change_list_template is None: self.change_list_template = self.base_change_list_template def get_model_info(self): app_label = self.model._meta.app_label return (app_label, self.model._meta.model_name) def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} extra_context["base_change_list_template"] = self.base_change_list_template return super().changelist_view(request, extra_context) 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 import_export_change_list_template = "admin/import_export/change_list_import.html" #: template for import view import_template_name = "admin/import_export/import.html" #: form class to use for the initial import step import_form_class = ImportForm #: form class to use for the confirm import step confirm_form_class = ConfirmImportForm #: import data encoding from_encoding = "utf-8-sig" 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 if getattr(self.get_confirm_import_form, "is_original", False): confirm_form = self.create_confirm_form(request) else: 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"]) ](encoding=self.from_encoding) encoding = None if input_format.is_binary() else self.from_encoding tmp_storage_cls = self.get_tmp_storage_class() tmp_storage = tmp_storage_cls( name=confirm_form.cleaned_data["import_file_name"], encoding=encoding, read_mode=input_format.get_read_mode(), ) data = tmp_storage.read() 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, rollback_on_validation_errors=False, **kwargs, ): res_kwargs = self.get_import_resource_kwargs( request, *args, form=confirm_form, **kwargs ) resource = self.choose_import_resource_class(confirm_form)(**res_kwargs) imp_kwargs = self.get_import_data_kwargs( request, *args, form=confirm_form, **kwargs ) return resource.import_data( dataset, dry_run=False, file_name=confirm_form.cleaned_data.get("original_file_name"), user=request.user, rollback_on_validation_errors=True, **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 ): with warnings.catch_warnings(): warnings.filterwarnings( "ignore", category=PendingDeprecationWarning ) 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 {} @original def get_import_form(self): """ .. deprecated:: 3.0 Use :meth:`~import_export.admin.ImportMixin.get_import_form_class` or set the new :attr:`~import_export.admin.ImportMixin.import_form_class` attribute. """ warnings.warn( "ImportMixin.get_import_form() is deprecated and will be removed in " "a future release. Please use get_import_form_class() instead.", category=DeprecationWarning, ) return self.import_form_class @original def get_confirm_import_form(self): """ .. deprecated:: 3.0 Use :func:`~import_export.admin.ImportMixin.get_confirm_form_class` or set the new :attr:`~import_export.admin.ImportMixin.confirm_form_class` attribute. """ warnings.warn( "ImportMixin.get_confirm_import_form() is deprecated " "and will be removed in a future release. " "Please use get_confirm_form_class() instead.", category=DeprecationWarning, ) return self.confirm_form_class @original def get_form_kwargs(self, form, *args, **kwargs): """ .. deprecated:: 3.0 Use :meth:`~import_export.admin.ImportMixin.get_import_form_kwargs` or :meth:`~import_export.admin.ImportMixin.get_confirm_form_kwargs` instead, depending on which form you wish to customize. """ warnings.warn( "ImportMixin.get_form_kwargs() is deprecated and will be removed " "in a future release. Please use get_import_form_kwargs() or " "get_confirm_form_kwargs() instead.", category=DeprecationWarning, ) return kwargs def create_import_form(self, request): """ .. versionadded:: 3.0 Return a form instance to use for the 'initial' import step. This method can be extended to make dynamic form updates to the form after it has been instantiated. You might also look to override the following: * :meth:`~import_export.admin.ImportMixin.get_import_form_class` * :meth:`~import_export.admin.ImportMixin.get_import_form_kwargs` * :meth:`~import_export.admin.ImportMixin.get_import_form_initial` * :meth:`~import_export.mixins.BaseImportMixin.get_import_resource_classes` """ formats = self.get_import_formats() form_class = self.get_import_form_class(request) kwargs = self.get_import_form_kwargs(request) if not issubclass(form_class, ImportExportFormBase): warnings.warn( "The ImportForm class must inherit from ImportExportFormBase, " "this is needed for multiple resource classes to work properly. ", category=DeprecationWarning, ) return form_class(formats, **kwargs) return form_class( formats, resources=self.get_import_resource_classes(), **kwargs ) def get_import_form_class(self, request): """ .. versionadded:: 3.0 Return the form class to use for the 'import' step. If you only have a single custom form class, you can set the ``import_form_class`` attribute to change this for your subclass. """ # TODO: Remove following conditional when get_import_form() is removed if not getattr(self.get_import_form, "is_original", False): warnings.warn( "ImportMixin.get_import_form() is deprecated and will be " "removed in a future release. Please use the new " "'import_form_class' attribute to specify a custom form " "class, or override the get_import_form_class() method if " "your requirements are more complex.", category=DeprecationWarning, ) return self.get_import_form() # Return the class attribute value return self.import_form_class def get_import_form_kwargs(self, request): """ .. versionadded:: 3.0 Return a dictionary of values with which to initialize the 'import' form (including the initial values returned by :meth:`~import_export.admin.ImportMixin.get_import_form_initial`). """ return { "data": request.POST or None, "files": request.FILES or None, "initial": self.get_import_form_initial(request), } def get_import_form_initial(self, request): """ .. versionadded:: 3.0 Return a dictionary of initial field values to be provided to the 'import' form. """ return {} def create_confirm_form(self, request, import_form=None): """ .. versionadded:: 3.0 Return a form instance to use for the 'confirm' import step. This method can be extended to make dynamic form updates to the form after it has been instantiated. You might also look to override the following: * :meth:`~import_export.admin.ImportMixin.get_confirm_form_class` * :meth:`~import_export.admin.ImportMixin.get_confirm_form_kwargs` * :meth:`~import_export.admin.ImportMixin.get_confirm_form_initial` """ form_class = self.get_confirm_form_class(request) kwargs = self.get_confirm_form_kwargs(request, import_form) return form_class(**kwargs) def get_confirm_form_class(self, request): """ .. versionadded:: 3.0 Return the form class to use for the 'confirm' import step. If you only have a single custom form class, you can set the ``confirm_form_class`` attribute to change this for your subclass. """ # TODO: Remove following conditional when get_confirm_import_form() is removed if not getattr(self.get_confirm_import_form, "is_original", False): warnings.warn( "ImportMixin.get_confirm_import_form() is deprecated and will " "be removed in a future release. Please use the new " "'confirm_form_class' attribute to specify a custom form " "class, or override the get_confirm_form_class() method if " "your requirements are more complex.", category=DeprecationWarning, ) return self.get_confirm_import_form() # Return the class attribute value return self.confirm_form_class def get_confirm_form_kwargs(self, request, import_form=None): """ .. versionadded:: 3.0 Return a dictionary of values with which to initialize the 'confirm' form (including the initial values returned by :meth:`~import_export.admin.ImportMixin.get_confirm_form_initial`). """ if import_form: # When initiated from `import_action()`, the 'posted' data # is for the 'import' form, not this one. data = None files = None else: data = request.POST or None files = request.FILES or None return { "data": data, "files": files, "initial": self.get_confirm_form_initial(request, import_form), } def get_confirm_form_initial(self, request, import_form): """ .. versionadded:: 3.0 Return a dictionary of initial field values to be provided to the 'confirm' form. """ if import_form is None: return {} return { "import_file_name": import_form.cleaned_data[ "import_file" ].tmp_storage_name, "original_file_name": import_form.cleaned_data["import_file"].name, "input_format": import_form.cleaned_data["input_format"], "resource": import_form.cleaned_data.get("resource", ""), } 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): encoding = None if not input_format.is_binary(): encoding = self.from_encoding tmp_storage_cls = self.get_tmp_storage_class() tmp_storage = tmp_storage_cls( encoding=encoding, read_mode=input_format.get_read_mode() ) data = bytes() for chunk in import_file.chunks(): data += chunk tmp_storage.save(data) return tmp_storage def add_data_read_fail_error_to_form(self, form, e): exc_name = repr(type(e).__name__) msg = _( "%(exc_name)s encountered while trying to read file. " "Ensure you have chosen the correct format for the file." ) % {"exc_name": exc_name} form.add_error("import_file", msg) 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 are no errors, 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() if getattr(self.get_form_kwargs, "is_original", False): # Use new API import_form = self.create_import_form(request) else: form_class = self.get_import_form_class(request) form_kwargs = self.get_form_kwargs(form_class, *args, **kwargs) if issubclass(form_class, ImportExportFormBase): import_form = form_class( import_formats, request.POST or None, request.FILES or None, resources=self.get_import_resource_classes(), **form_kwargs, ) else: warnings.warn( "The ImportForm class must inherit from ImportExportFormBase, " "this is needed for multiple resource classes to work properly. ", category=DeprecationWarning, ) import_form = form_class( import_formats, request.POST or None, request.FILES or None, **form_kwargs, ) resources = list() if request.POST and import_form.is_valid(): input_format = import_formats[ int(import_form.cleaned_data["input_format"]) ]() if not input_format.is_binary(): input_format.encoding = self.from_encoding import_file = import_form.cleaned_data["import_file"] if getattr(settings, "IMPORT_EXPORT_SKIP_ADMIN_CONFIRM", False): # This setting means we are going to skip the import confirmation step. # Go ahead and process the file for import in a transaction # If there are any errors, we roll back the transaction. # rollback_on_validation_errors is set to True so that we rollback on # validation errors. If this is not done validation errors would be # silently skipped. data = bytes() for chunk in import_file.chunks(): data += chunk try: dataset = input_format.create_dataset(data) except Exception as e: self.add_data_read_fail_error_to_form(import_form, e) if not import_form.errors: result = self.process_dataset( dataset, import_form, request, *args, raise_errors=False, rollback_on_validation_errors=True, **kwargs, ) if not result.has_errors() and not result.has_validation_errors(): return self.process_result(result, request) else: context["result"] = result else: # 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) # allows get_confirm_form_initial() to include both the # original and saved file names from form.cleaned_data import_file.tmp_storage_name = tmp_storage.name try: # then read the file, using the proper format-specific mode # warning, big files may exceed memory data = tmp_storage.read() dataset = input_format.create_dataset(data) except Exception as e: self.add_data_read_fail_error_to_form(import_form, e) if not import_form.errors: # prepare kwargs for import data, if needed res_kwargs = self.get_import_resource_kwargs( request, *args, form=import_form, **kwargs ) resource = self.choose_import_resource_class(import_form)( **res_kwargs ) resources = [resource] # prepare additional kwargs for import_data, if needed imp_kwargs = self.get_import_data_kwargs( request, *args, form=import_form, **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(): if getattr(self.get_form_kwargs, "is_original", False): # Use new API context["confirm_form"] = self.create_confirm_form( request, import_form=import_form ) else: confirm_form_class = self.get_confirm_form_class(request) initial = self.get_confirm_form_initial( request, import_form ) context["confirm_form"] = confirm_form_class( initial=self.get_form_kwargs( form=import_form, **initial ) ) else: res_kwargs = self.get_import_resource_kwargs( request, *args, form=import_form, **kwargs ) resource_classes = self.get_import_resource_classes() resources = [ resource_class(**res_kwargs) for resource_class in resource_classes ] context.update(self.admin_site.each_context(request)) context["title"] = _("Import") context["form"] = import_form context["opts"] = self.model._meta context["media"] = self.media + import_form.media context["fields_list"] = [ ( resource.get_display_name(), [f.column_name for f in resource.get_user_visible_fields()], ) for resource in resources ] 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 import_export_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 #: form class to use for the initial import step export_form_class = ExportForm 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_select_related = self.get_list_select_related(request) 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": 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, } 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, escape_html=self.should_escape_html, escape_formulae=self.should_escape_formulae, ) 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 {} @original def get_export_form(self): """ .. deprecated:: 3.0 Use :meth:`~import_export.admin.ExportMixin.get_export_form_class` or set the new :attr:`~import_export.admin.ExportMixin.export_form_class` attribute. """ warnings.warn( "ExportMixin.get_export_form() is deprecated and will " "be removed in a future release. Please use the new " "'export_form_class' attribute to specify a custom form " "class, or override the get_export_form_class() method if " "your requirements are more complex.", category=DeprecationWarning, ) return self.export_form_class def get_export_form_class(self): """ Get the form class used to read the export format. """ return self.export_form_class def export_action(self, request, *args, **kwargs): if not self.has_export_permission(request): raise PermissionDenied if getattr(self.get_export_form, "is_original", False): form_type = self.get_export_form_class() else: form_type = self.get_export_form() formats = self.get_export_formats() form = form_type( formats, request.POST or None, resources=self.get_export_resource_classes() ) 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, export_form=form, ) 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 context["fields_list"] = [ ( res.get_display_name(), [ field.column_name for field in res(model=self.model).get_user_visible_fields() ], ) for res in self.get_export_resource_classes() ] 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 import_export_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. import_export_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: for i, f in enumerate(formats): choices.append((str(i), f().get_title())) if len(formats) > 1: choices.insert(0, ("", "---")) 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=( type(self).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-3.3.4/import_export/exceptions.py000066400000000000000000000003121453511766500232710ustar00rootroot00000000000000class 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-3.3.4/import_export/fields.py000066400000000000000000000126201453511766500223630ustar00rootroot00000000000000from django.core.exceptions import ObjectDoesNotExist from django.db.models.fields import NOT_PROVIDED from django.db.models.manager import Manager from . import widgets from .exceptions import FieldError 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, or transform the value during import. :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 :param dehydrate_method: Lets you choose your own method for dehydration rather than using `dehydrate_{field_name}` syntax. :param m2m_add: changes save of this field to add the values, if they do not exist, to a ManyToMany field instead of setting all values. Only useful if field is a ManyToMany field. """ empty_values = [None, ""] def __init__( self, attribute=None, column_name=None, widget=None, default=NOT_PROVIDED, readonly=False, saves_null_values=True, dehydrate_method=None, m2m_add=False, ): 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 self.dehydrate_method = dehydrate_method self.m2m_add = m2m_add 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: if self.m2m_add: getattr(obj, attrs[-1]).add(*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) def get_dehydrate_method(self, field_name=None): """ Returns method name to be used for dehydration of the field. Defaults to `dehydrate_{field_name}` """ DEFAULT_DEHYDRATE_METHOD_PREFIX = "dehydrate_" if not self.dehydrate_method and not field_name: raise FieldError("Both dehydrate_method and field_name are not supplied.") return self.dehydrate_method or DEFAULT_DEHYDRATE_METHOD_PREFIX + field_name django-import-export-3.3.4/import_export/formats/000077500000000000000000000000001453511766500222155ustar00rootroot00000000000000django-import-export-3.3.4/import_export/formats/__init__.py000066400000000000000000000000001453511766500243140ustar00rootroot00000000000000django-import-export-3.3.4/import_export/formats/base_formats.py000066400000000000000000000144501453511766500252400ustar00rootroot00000000000000import html import warnings import tablib from tablib.formats import registry 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 __init__(self, encoding=None): self.encoding = encoding def get_format(self): """ Import and returns tablib module. """ if not self.TABLIB_MODULE: raise AttributeError("TABLIB_MODULE must be defined") 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(), **kwargs) def export_data(self, dataset, **kwargs): if kwargs.pop("escape_html", None): self._escape_html(dataset) if kwargs.pop("escape_formulae", None): self._escape_formulae(dataset) 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") def _escape_html(self, dataset): for _ in dataset: row = dataset.lpop() row = [html.escape(str(cell)) for cell in row] dataset.append(row) def _escape_formulae(self, dataset): def _do_escape(s): return s.replace("=", "", 1) if s.startswith("=") else s for _ in dataset: row = dataset.lpop() row = [_do_escape(str(cell)) for cell in row] dataset.append(row) class TextFormat(TablibFormat): def create_dataset(self, in_stream, **kwargs): if isinstance(in_stream, bytes) and self.encoding: in_stream = in_stream.decode(self.encoding) return super().create_dataset(in_stream, **kwargs) 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" 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" def export_data(self, dataset, **kwargs): return super().export_data(dataset, **kwargs) 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 def export_data(self, dataset, **kwargs): # #1698 temporary catch for deprecation warning in openpyxl # this catch block must be removed when openpyxl updated with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) return super().export_data(dataset, **kwargs) #: 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-3.3.4/import_export/forms.py000066400000000000000000000074551453511766500222550ustar00rootroot00000000000000import os.path import warnings from django import forms from django.conf import settings from django.contrib.admin.helpers import ActionForm from django.utils.translation import gettext_lazy as _ class ImportExportFormBase(forms.Form): resource = forms.ChoiceField( label=_("Resource"), choices=(), required=False, ) def __init__(self, *args, resources=None, **kwargs): super().__init__(*args, **kwargs) if len(args) == 1 and resources is None: # issue 1565: definition of __init__() was incorrect # this logic included to aid backwards compatibility, # for cases where users are calling with the original form. # this check can be removed in a future release warnings.warn( "'resources' must be supplied as a named parameter", category=DeprecationWarning, ) resources = args if resources and len(resources) > 1: resource_choices = [] for i, resource in enumerate(resources): resource_choices.append((i, resource.get_display_name())) self.fields["resource"].choices = resource_choices else: del self.fields["resource"] class ImportForm(ImportExportFormBase): import_file = forms.FileField(label=_("File to import")) input_format = forms.ChoiceField( label=_("Format"), choices=(), ) def __init__(self, import_formats, *args, **kwargs): resources = kwargs.pop("resources", None) super().__init__(*args, resources=resources, **kwargs) choices = [(str(i), f().get_title()) for i, f in enumerate(import_formats)] if len(import_formats) > 1: choices.insert(0, ("", "---")) self.fields["import_file"].widget.attrs["class"] = "guess_format" self.fields["input_format"].widget.attrs["class"] = "guess_format" self.fields["input_format"].choices = choices @property def media(self): extra = "" if settings.DEBUG else ".min" return forms.Media( js=( f"admin/js/vendor/jquery/jquery{extra}.js", "admin/js/jquery.init.js", "import_export/guess_format.js", ) ) 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()) resource = forms.CharField(widget=forms.HiddenInput(), required=False) def clean_import_file_name(self): data = self.cleaned_data["import_file_name"] data = os.path.basename(data) return data class ExportForm(ImportExportFormBase): file_format = forms.ChoiceField( label=_("Format"), choices=(), ) def __init__(self, formats, *args, **kwargs): resources = kwargs.pop("resources", None) super().__init__(*args, resources=resources, **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-3.3.4/import_export/instance_loaders.py000066400000000000000000000040511453511766500244310ustar00rootroot00000000000000class 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-3.3.4/import_export/locale/000077500000000000000000000000001453511766500220015ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ar/000077500000000000000000000000001453511766500224035ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ar/LC_MESSAGES/000077500000000000000000000000001453511766500241705ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ar/LC_MESSAGES/django.mo000066400000000000000000000033261453511766500257730ustar00rootroot00000000000000Lh*9@G'Nv 0!c  1 ' 0 = HJS ,    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; فيما يلي إستعراض للبيانات التي سيتم إستيرادها. إذا كنت راضيا عن النتائج, انقر على 'تأكيد الإستيراد'تأكيد الإستيرادحذفأخطاءتصديرتصدير %(verbose_name_plural)s المحددةملف للإستيرادتنسيقالرئيسيةإستبرادرقم الصطرجديدمعاينةتجاهلإرسالهذا المستورد سوف يستورد الحقول التالية : تحديثيجب تحديد تنسيق التصدير.django-import-export-3.3.4/import_export/locale/ar/LC_MESSAGES/django.po000066400000000000000000000072721453511766500260020ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "إستبراد" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "تصدير" #: admin.py:892 msgid "You must select an export format." msgstr "يجب تحديد تنسيق التصدير." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "تصدير %(verbose_name_plural)s المحددة" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "ملف للإستيراد" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "تنسيق" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "الرئيسية" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "إرسال" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "تأكيد الإستيراد" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "هذا المستورد سوف يستورد الحقول التالية : " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "أخطاء" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "رقم الصطر" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "معاينة" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "جديد" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "تجاهل" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "حذف" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "تحديث" django-import-export-3.3.4/import_export/locale/bg/000077500000000000000000000000001453511766500223715ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/bg/LC_MESSAGES/000077500000000000000000000000001453511766500241565ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/bg/LC_MESSAGES/django.mo000066400000000000000000000040261453511766500257570ustar00rootroot00000000000000lhcry'/  !0(Y!`X+   H%$n  [ 9@ObFqN     %s through import_exportBelow 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Отдолу виждате преглед на данните за импортиране. Ако сте доволни от резултата, изберете 'Потвърди импортирането'Потвърди импортиранетоИзтритГрешкиЕкспортиранеЕкспортиране на избраните %(verbose_name_plural)sФайл за импортиранеФорматНачалоИмпортиранеИмпортирането е завършено, с {} нови и {} обновени {}.Номер на редаНовПрегледПропуснатИзпълниЩе бъдат импортирани следните полета: ОбновенТрябва да изберете формат за експортиране.django-import-export-3.3.4/import_export/locale/bg/LC_MESSAGES/django.po000066400000000000000000000076261453511766500257730ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s чрез import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Импортирането е завършено, с {} нови и {} обновени {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Импортиране" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Експортиране" #: admin.py:892 msgid "You must select an export format." msgstr "Трябва да изберете формат за експортиране." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Експортиране на избраните %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Файл за импортиране" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Начало" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Изпълни" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Потвърди импортирането" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Ще бъдат импортирани следните полета: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Грешки" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Номер на реда" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Преглед" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Нов" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Пропуснат" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Изтрит" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Обновен" django-import-export-3.3.4/import_export/locale/ca/000077500000000000000000000000001453511766500223645ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ca/LC_MESSAGES/000077500000000000000000000000001453511766500241515ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ca/LC_MESSAGES/django.mo000066400000000000000000000030141453511766500257460ustar00rootroot00000000000000Lh*9@G'Nv 0!B\ -J[bhq 2 *    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ó 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-3.3.4/import_export/locale/ca/LC_MESSAGES/django.po000066400000000000000000000067611453511766500257650ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importar" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportar" #: admin.py:892 msgid "You must select an export format." msgstr "Heu de seleccionar un format d'exportació" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionats" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Arxiu a importar" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inici" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Confirmar importació" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Aquest importador importarà els següents camps: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Errors" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Número de línia" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Vista prèvia" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nou" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Omès" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Esborrar" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Actualitzar" django-import-export-3.3.4/import_export/locale/cs/000077500000000000000000000000001453511766500224065ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/cs/LC_MESSAGES/000077500000000000000000000000001453511766500241735ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/cs/LC_MESSAGES/django.mo000066400000000000000000000031771453511766500260020ustar00rootroot00000000000000lhcry'/  !0(Y!`=uL \fl&s2  (' P"\     %s through import_exportBelow 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_exportNíž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-3.3.4/import_export/locale/cs/LC_MESSAGES/django.po000066400000000000000000000067601453511766500260060ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s skrz import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Import dokončen, {} nové a {} aktualizované {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Import" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Export" #: admin.py:892 msgid "You must select an export format." msgstr "Musíte vybrat formát pro export." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Vybrán export %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Soubor k importu" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formát" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Domů" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Odeslat" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Provést import" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Budou importována následující pole: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Chyby" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Číslo řádku" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Náhled" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nové" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Přeskočené" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Smazání" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Aktualizace" django-import-export-3.3.4/import_export/locale/de/000077500000000000000000000000001453511766500223715ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/de/LC_MESSAGES/000077500000000000000000000000001453511766500241565ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/de/LC_MESSAGES/django.mo000066400000000000000000000046721453511766500257660ustar00rootroot00000000000000%`kahO^el's/ _px0!J V 0   = \im|    *& Q +Z  ,      %(exc_name)s encountered while trying to read file. Ensure you have chosen the correct format for the file.%s through import_exportBelow 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.PreviewResourceRowSkippedSome 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: 2022-10-17 17:42+0200 Last-Translator: Jannes Blobel 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 3.1.1 %(exc_name)s beim Versuch, die Datei zu lesen ist aufgetretten. Stellen Sie sicher, dass du das richtige Format für deine Datei gewählt hast%s durch import_exportUnten 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 DateiDateiformatStartImportierenImport fertiggestellt, mit {} neuen und {} aktualisierten {}.ZeilennummerNeuNicht feldspezifischBitte korrigiere falls möglich diese Fehler in deiner Datei und lade sie anschließend erneut mit dem obigen Formular hoch.VorschauRessourceZeileÜbersprungenDie Validierung einiger Zeilen schlug fehlAbsendenEs werden die folgenden Felder importiert: UpdateEs muss ein Exportformat ausgewählt werden.django-import-export-3.3.4/import_export/locale/de/LC_MESSAGES/django.po000066400000000000000000000076521453511766500257720ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\n" "PO-Revision-Date: 2022-10-17 17:42+0200\n" "Last-Translator: Jannes Blobel \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 3.1.1\n" #: admin.py:239 #, python-format msgid "%s through import_export" msgstr "%s durch import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Import fertiggestellt, mit {} neuen und {} aktualisierten {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" "%(exc_name)s beim Versuch, die Datei zu lesen ist aufgetretten. Stellen Sie " "sicher, dass du das richtige Format für deine Datei gewählt hast" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importieren" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportieren" #: admin.py:892 msgid "You must select an export format." msgstr "Es muss ein Exportformat ausgewählt werden." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Ausgewählte %(verbose_name_plural)s exportieren" #: forms.py:12 msgid "Resource" msgstr "Ressource" #: forms.py:40 msgid "File to import" msgstr "Zu importierende Datei" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Dateiformat" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Start" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Absenden" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Import bestätigen" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Es werden die folgenden Felder importiert: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Fehler" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Zeilennummer" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Die Validierung einiger Zeilen schlug fehl" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Bitte korrigiere falls möglich diese Fehler in deiner Datei und lade sie " "anschließend erneut mit dem obigen Formular hoch." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "Zeile" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Nicht feldspezifisch" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Vorschau" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Neu" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Übersprungen" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Löschen" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Update" django-import-export-3.3.4/import_export/locale/es/000077500000000000000000000000001453511766500224105ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/es/LC_MESSAGES/000077500000000000000000000000001453511766500241755ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/es/LC_MESSAGES/django.mo000066400000000000000000000046431453511766500260030ustar00rootroot00000000000000%PkQh&5<C'Jr/ _GOX\d0!K.BY`h.qC!'@  & 1 18 j -u      %(exc_name)s encountered while trying to read file. Ensure you have chosen the correct format for the file.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.PreviewResourceRowSkippedSome 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: 2023-09-22 11:53-0300 Last-Translator: Santiago Muñoz Language-Team: Spanish Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); Se encontró %(exc_name)s mientras se intentaba leer el archivo. Asegúrese que seleccionó el formato correcto para el archivo.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 importarFormatoInicioImportarProceso de importación finalizado, con {} nuevos y {} actualizadosNúmero de líneaNuevoNo específico del campoPor favor corrija los siguientes errores en la información ingresada donde sea posible, luego vuelva a subir el archivo utilizando el formulario de la parte superior.Vista previaRecursoFilaOmitidoFalló la validación de algunas filasEnviarEste importador importará los siguientes campos:ActualizarDebes seleccionar un formato de exportación.django-import-export-3.3.4/import_export/locale/es/LC_MESSAGES/django.po000066400000000000000000000077751453511766500260170ustar00rootroot00000000000000# 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. # Santiago Muñoz , 2023. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-11-08 18:22+0000\n" "PO-Revision-Date: 2023-09-22 11:53-0300\n" "Last-Translator: Santiago Muñoz \n" "Language-Team: Spanish\n" "Language: es\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Proceso de importación finalizado, con {} nuevos y {} actualizados" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" "Se encontró %(exc_name)s mientras se intentaba leer el archivo. Asegúrese " "que seleccionó el formato correcto para el archivo." #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importar" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportar" #: admin.py:892 msgid "You must select an export format." msgstr "Debes seleccionar un formato de exportación." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionados" #: forms.py:12 msgid "Resource" msgstr "Recurso" #: forms.py:40 msgid "File to import" msgstr "Fichero a importar" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inicio" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Confirmar importación" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Este importador importará los siguientes campos:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Errores" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Número de línea" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Falló la validación de algunas filas" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Por favor corrija los siguientes errores en la información ingresada donde " "sea posible, luego vuelva a subir el archivo utilizando el formulario de la " "parte superior." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "Fila" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "No específico del campo" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Vista previa" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nuevo" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Omitido" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Borrar" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Actualizar" django-import-export-3.3.4/import_export/locale/es_AR/000077500000000000000000000000001453511766500227725ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/es_AR/LC_MESSAGES/000077500000000000000000000000001453511766500245575ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/es_AR/LC_MESSAGES/django.mo000066400000000000000000000046751453511766500263720ustar00rootroot00000000000000%PkQh&5<C'Jr/ _GOX\d0!fI]t{.C*<B[     &% L 1S ,      %(exc_name)s encountered while trying to read file. Ensure you have chosen the correct format for the file.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.PreviewResourceRowSkippedSome 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: 2023-09-22 11:53-0300 Last-Translator: Santiago Muñoz 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 Se encontró %(exc_name)s mientras se intentaba leer el archivo. Asegúrese que seleccionó el formato correcto para el archivo.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 importarFormatoInicioImportarProceso de importación finalizado, con {} nuevos y {} actualizadosNúmero de líneaNuevoNo específico del campoPor favor corrija los siguientes errores en la información ingresada donde sea posible, luego vuelva a subir el archivo utilizando el formulario de la parte superior.Vista previaRecursoFilaOmitidoFalló la validación de algunas filasEnviarEste importador importará los siguientes campos:ActualizarDebe seleccionar un formato de exportación.django-import-export-3.3.4/import_export/locale/es_AR/LC_MESSAGES/django.po000066400000000000000000000100011453511766500263510ustar00rootroot00000000000000# 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. # Santiago Muñoz , 2023. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-11-08 18:22+0000\n" "PO-Revision-Date: 2023-09-22 11:53-0300\n" "Last-Translator: Santiago Muñoz \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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Proceso de importación finalizado, con {} nuevos y {} actualizados" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" "Se encontró %(exc_name)s mientras se intentaba leer el archivo. Asegúrese " "que seleccionó el formato correcto para el archivo." #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importar" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportar" #: admin.py:892 msgid "You must select an export format." msgstr "Debe seleccionar un formato de exportación." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s seleccionados" #: forms.py:12 msgid "Resource" msgstr "Recurso" #: forms.py:40 msgid "File to import" msgstr "Archivo a importar" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Inicio" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Confirmar importación" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Este importador importará los siguientes campos:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Errores" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Número de línea" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Falló la validación de algunas filas" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Por favor corrija los siguientes errores en la información ingresada donde " "sea posible, luego vuelva a subir el archivo utilizando el formulario de la " "parte superior." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "Fila" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "No específico del campo" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Vista previa" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nuevo" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Omitido" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Borrar" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Actualizar" django-import-export-3.3.4/import_export/locale/fa/000077500000000000000000000000001453511766500223675ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fa/LC_MESSAGES/000077500000000000000000000000001453511766500241545ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fa/LC_MESSAGES/django.mo000066400000000000000000000046221453511766500257570ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!Tzv(  6"Ux] 'j:  3 ? @ >S      %s through import_exportBelow 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 یه وسیله ورودی-خروجیپایین یک پیش‌نمایش از دیتا‌هایی است که بارگذاری خواهند شد اگر این موارد درست هستروی 'تایید بارگذاری' گلیگ گنیدتایید بارگذاریحذفخطاهاخروجیخروجی %(verbose_name_plural)s انتخاب شدهقایل برای بارگذاریفرمتخانهبارگذاریبارگذاری تمام شد، با {} مورد جدید و {} مورد به روز شده.شماره خطجدیدفیلد‌های غیر اختصاصیلطفا این خطا را تصحیح کنید و سپس مجدد فایل را بارگذاری کنیدنمایشسظردر شدبرخی از سطر‌ها معتبر نبودندارسالاین بارگذاری شامل این فیلد‌ها هست:بروزرسانیشما باید یک فرمت خروجی انتخاب کنیدdjango-import-export-3.3.4/import_export/locale/fa/LC_MESSAGES/django.po000066400000000000000000000100011453511766500257460ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s یه وسیله ورودی-خروجی" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "بارگذاری تمام شد، با {} مورد جدید و {} مورد به روز شده." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "بارگذاری" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "خروجی" #: admin.py:892 msgid "You must select an export format." msgstr "شما باید یک فرمت خروجی انتخاب کنید" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "خروجی %(verbose_name_plural)s انتخاب شده" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "قایل برای بارگذاری" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "فرمت" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "خانه" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "ارسال" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "تایید بارگذاری" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "این بارگذاری شامل این فیلد‌ها هست:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "خطاها" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "شماره خط" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "برخی از سطر‌ها معتبر نبودند" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "لطفا این خطا را تصحیح کنید و سپس مجدد فایل را بارگذاری کنید" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "سظر" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "فیلد‌های غیر اختصاصی" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "نمایش" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "جدید" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "در شد" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "حذف" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "بروزرسانی" django-import-export-3.3.4/import_export/locale/fi/000077500000000000000000000000001453511766500223775ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fi/LC_MESSAGES/000077500000000000000000000000001453511766500241645ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fi/LC_MESSAGES/django.mo000066400000000000000000000045231453511766500257670ustar00rootroot00000000000000%pkqh_nu|'/   _ 00#!*.L`{XQahp#t9  Z. #%% $ $.      %(exc_name)s encountered while trying to read file. Ensure you have chosen the correct format for the file.%s through import_exportBelow 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.PreviewResourceRowSkippedSome rows failed to validateSubmitThis exporter will export the following fields: This importer will import the following fields: UpdateYou must select an export format.Project-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2023-05-10 15:23+0300 Last-Translator: Lauri Virtanen Language-Team: Language: fi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); Kohdattiin %(exc_name)s tiedostoa lukiessa. Varmista, että olet valinnut oikean tiedostotyypin.%s käyttäen import_exportAlla on esikatselu tuotavista tiedoista. Jos olet tyytyväinen, paina 'Vahvista tuonti'.Vahvista tuontiPoistoVirheetVieVie valitut %(verbose_name_plural)sTuotava tiedostoTiedostotyyppiEtusivuTuoTuonti valmis. Lisätty {} ja päivitetty {} kohteita {}.RivinumeroUusiEi liity mihinkään kenttäänKorjaa nämä virheet tiedoissasi ja lähetä uudelleen käyttäen yllä olevaa lomaketta.EsikatseluResurssiRiviOhitettuJoitakin rivejä ei voitu vahvistaaLähetäTämä vienti vie seuraavat kentät: Tämä tuonti tuo seuraavat kentät: PäivitysSinun täytyy valita tiedostotyyppi.django-import-export-3.3.4/import_export/locale/fi/LC_MESSAGES/django.po000066400000000000000000000073071453511766500257750ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-12-04 16:50+0200\n" "PO-Revision-Date: 2023-05-10 15:23+0300\n" "Last-Translator: Lauri Virtanen \n" "Language-Team: \n" "Language: fi\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:244 #, python-format msgid "%s through import_export" msgstr "%s käyttäen import_export" #: admin.py:252 msgid "Import finished, with {} new and {} updated {}." msgstr "Tuonti valmis. Lisätty {} ja päivitetty {} kohteita {}." #: admin.py:497 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" "Kohdattiin %(exc_name)s tiedostoa lukiessa. Varmista, että olet valinnut " "oikean tiedostotyypin." #: admin.py:648 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Tuo" #: admin.py:833 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Vie" #: admin.py:908 msgid "You must select an export format." msgstr "Sinun täytyy valita tiedostotyyppi." #: admin.py:933 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Vie valitut %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "Resurssi" #: forms.py:40 msgid "File to import" msgstr "Tuotava tiedosto" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Tiedostotyyppi" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Etusivu" #: templates/admin/import_export/export.html:38 #: templates/admin/import_export/import.html:64 msgid "Submit" msgstr "Lähetä" #: templates/admin/import_export/import.html:30 msgid "" "Below is a preview of data to be imported. If you are satisfied with the " "results, click 'Confirm import'" msgstr "" "Alla on esikatselu tuotavista tiedoista. Jos olet tyytyväinen, paina " "'Vahvista tuonti'." #: templates/admin/import_export/import.html:33 msgid "Confirm import" msgstr "Vahvista tuonti" #: templates/admin/import_export/import.html:75 #: templates/admin/import_export/import.html:106 msgid "Errors" msgstr "Virheet" #: templates/admin/import_export/import.html:86 msgid "Line number" msgstr "Rivinumero" #: templates/admin/import_export/import.html:98 msgid "Some rows failed to validate" msgstr "Joitakin rivejä ei voitu vahvistaa" #: templates/admin/import_export/import.html:100 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Korjaa nämä virheet tiedoissasi ja lähetä uudelleen käyttäen yllä olevaa " "lomaketta." #: templates/admin/import_export/import.html:105 msgid "Row" msgstr "Rivi" #: templates/admin/import_export/import.html:132 msgid "Non field specific" msgstr "Ei liity mihinkään kenttään" #: templates/admin/import_export/import.html:155 msgid "Preview" msgstr "Esikatselu" #: templates/admin/import_export/import.html:170 msgid "New" msgstr "Uusi" #: templates/admin/import_export/import.html:172 msgid "Skipped" msgstr "Ohitettu" #: templates/admin/import_export/import.html:174 msgid "Delete" msgstr "Poisto" #: templates/admin/import_export/import.html:176 msgid "Update" msgstr "Päivitys" #: templates/admin/import_export/resource_fields_list.html:5 msgid "This exporter will export the following fields: " msgstr "Tämä vienti vie seuraavat kentät: " #: templates/admin/import_export/resource_fields_list.html:7 msgid "This importer will import the following fields: " msgstr "Tämä tuonti tuo seuraavat kentät: " django-import-export-3.3.4/import_export/locale/fr/000077500000000000000000000000001453511766500224105ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fr/LC_MESSAGES/000077500000000000000000000000001453511766500241755ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000030101453511766500257660ustar00rootroot00000000000000Lh*9@G'Nv 0!Ax[ .6JQYbs{ 11    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); 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-3.3.4/import_export/locale/fr/LC_MESSAGES/django.po000066400000000000000000000067511453511766500260100ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importer" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exporter" #: admin.py:892 msgid "You must select an export format." msgstr "Vous devez sélectionner un format d'exportation." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exporter %(verbose_name_plural)s selectionnés" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Fichier à importer" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Accueil" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Soumettre" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Confirmer l'importation" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Cet importateur va importer les champs suivants: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Erreurs" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Numéro de ligne" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Aperçu" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nouveau" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Ignoré" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Supprimer" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Mettre à jour" django-import-export-3.3.4/import_export/locale/it/000077500000000000000000000000001453511766500224155ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/it/LC_MESSAGES/000077500000000000000000000000001453511766500242025ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/it/LC_MESSAGES/django.mo000066400000000000000000000030011453511766500257730ustar00rootroot00000000000000Lh*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-3.3.4/import_export/locale/it/LC_MESSAGES/django.po000066400000000000000000000067151453511766500260150ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importare" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Esportare" #: admin.py:892 msgid "You must select an export format." msgstr "Devi selezionare un formato di esportazione." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Esporta selezionati %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "File da importare" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Home" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Inviare" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Conferma importazione" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Verranno importati i seguenti campi:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Errori" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Numero linea" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Anteprima" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nuovo" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Salta" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Cancella" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Aggiorna" django-import-export-3.3.4/import_export/locale/ja/000077500000000000000000000000001453511766500223735ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ja/LC_MESSAGES/000077500000000000000000000000001453511766500241605ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ja/LC_MESSAGES/django.mo000066400000000000000000000031621453511766500257610ustar00rootroot00000000000000Lh*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-3.3.4/import_export/locale/ja/LC_MESSAGES/django.po000066400000000000000000000071231453511766500257650ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "インポート" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "エクスポート" #: admin.py:892 msgid "You must select an export format." msgstr "エクスポートフォーマットを選択してください。" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "選択した %(verbose_name_plural)s をエクスポート" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "インポートするファイル" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "フォーマット" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "ホーム" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "確定" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "インポート実行" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "以下の列をインポートします。" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "エラー" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "行番号" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "プレビュー" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "新規" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "スキップ" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "削除" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "更新" django-import-export-3.3.4/import_export/locale/ko/000077500000000000000000000000001453511766500224125ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ko/LC_MESSAGES/000077500000000000000000000000001453511766500241775ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ko/LC_MESSAGES/django.mo000066400000000000000000000037761453511766500260130ustar00rootroot00000000000000 h"'/ $04_G0 !J5=IPW ^.k 7 @ X e2o%'    %s through import_exportBelow 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.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: Yeongkwang Yang 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를 통해 가져왔습니다.다음은 불러올 데이터의 미리보기 입니다.데이터에 문제가 없다면 확인을 눌러 가져오기를 진행하세요.확인삭제에러내보내기선택한 %(verbose_name_plural)s 내보내기파일형식가져오기가져오기 성공, {} 행 추가, {} 행 업데이트행 번호생성지정된 필드 없음에러를 수정한 후 파일을 다시 업로드 해주세요.미리보기넘어감유효성 검증에 실패한 행이 있습니다.제출다음의 필드를 가져옵니다: 갱신내보낼 형식을 선택해주세요.django-import-export-3.3.4/import_export/locale/ko/LC_MESSAGES/django.po000066400000000000000000000073051453511766500260060ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Yeongkwang Yang \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:239 #, python-format msgid "%s through import_export" msgstr "%s은(는) django-import-export를 통해 가져왔습니다." #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "가져오기 성공, {} 행 추가, {} 행 업데이트" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "가져오기" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "내보내기" #: admin.py:892 msgid "You must select an export format." msgstr "내보낼 형식을 선택해주세요." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "선택한 %(verbose_name_plural)s 내보내기" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "파일" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "형식" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "제출" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "확인" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "다음의 필드를 가져옵니다: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "에러" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "행 번호" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "유효성 검증에 실패한 행이 있습니다." #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "에러를 수정한 후 파일을 다시 업로드 해주세요." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "지정된 필드 없음" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "미리보기" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "생성" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "넘어감" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "삭제" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "갱신" django-import-export-3.3.4/import_export/locale/kz/000077500000000000000000000000001453511766500224255ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/kz/LC_MESSAGES/000077500000000000000000000000001453511766500242125ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/kz/LC_MESSAGES/django.mo000066400000000000000000000050451453511766500260150ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!T0vC0B s O$ 1  $ 27 j Vw  G      %s through import_exportBelow 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Төменде импортталатын деректерді алдын ала қарау берілген. Егер сіз нәтижелерге қанағаттансаңыз, 'Импортты растау' түймесін басыңыз.Импортты растауЖоюҚателерЭкспортТаңдалған %(verbose_name_plural)s экспорттаңызИмпорттауға арналған файлФорматБасты бетИмпортИмпорт аяқталды, {} жаңа және {} жаңартылды {}.Жол нөміріЖаңаӨріске қатысты емесМүмкіндігінше деректеріңіздегі қателерді түзетіңіз, содан кейін жоғарыдағы пішінді қолданып қайта жүктеңіз.Алдын-ала қарауҚатарӨткізілдіКейбір жолдар тексерілмедіЖіберуБұл импорттаушы келесі өрістерді импорттайды: ЖаңартуСіз экспорт форматын таңдауыңыз керек.django-import-export-3.3.4/import_export/locale/kz/LC_MESSAGES/django.po000066400000000000000000000102451453511766500260160ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s арқылы import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Импорт аяқталды, {} жаңа және {} жаңартылды {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Импорт" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Экспорт" #: admin.py:892 msgid "You must select an export format." msgstr "Сіз экспорт форматын таңдауыңыз керек." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Таңдалған %(verbose_name_plural)s экспорттаңыз" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Импорттауға арналған файл" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Басты бет" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Жіберу" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Импортты растау" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Бұл импорттаушы келесі өрістерді импорттайды: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Қателер" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Жол нөмірі" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Кейбір жолдар тексерілмеді" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "Мүмкіндігінше деректеріңіздегі қателерді түзетіңіз, содан кейін жоғарыдағы " "пішінді қолданып қайта жүктеңіз." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "Қатар" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Өріске қатысты емес" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Алдын-ала қарау" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Жаңа" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Өткізілді" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Жою" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Жаңарту" django-import-export-3.3.4/import_export/locale/nl/000077500000000000000000000000001453511766500224125ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/nl/LC_MESSAGES/000077500000000000000000000000001453511766500241775ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/nl/LC_MESSAGES/django.mo000066400000000000000000000041671453511766500260060ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!TBvb v / 0 '4:|N $, K!U     %s through import_exportBelow 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_exportHieronder 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-3.3.4/import_export/locale/nl/LC_MESSAGES/django.po000066400000000000000000000074061453511766500260100ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s door import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Import is klaar met {} nieuwe en {} geupdate {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importeren" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exporteren" #: admin.py:892 msgid "You must select an export format." msgstr "U moet een export formaat kiezen." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exporteer geselecteerde %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Bestand om te importeren" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formaat" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Terug" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Indienen" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Accepteer de import" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Deze import zal de volgende velden toevoegen" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Fouten" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Regel nummer" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Sommige regels zijn niet goedgekeurd" #: templates/admin/import_export/import.html:114 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:119 msgid "Row" msgstr "Regel" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Niet veld specifiek" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Voorbeeldweergave" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nieuw" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Overgeslagen" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Verwijderen" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Bijwerken" django-import-export-3.3.4/import_export/locale/pl/000077500000000000000000000000001453511766500224145ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/pl/LC_MESSAGES/000077500000000000000000000000001453511766500242015ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000033141453511766500260010ustar00rootroot00000000000000lhcry'/  !0(Y!`|{)9  DPU ^i+q     %s through import_exportBelow 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_exportPoniż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-3.3.4/import_export/locale/pl/LC_MESSAGES/django.po000066400000000000000000000071121453511766500260040ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s przez import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Import zakończony, z {} nowymi i {} zaktualizowanymi {}." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Import" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Eksport" #: admin.py:892 msgid "You must select an export format." msgstr "Musisz wybrać format eksportu." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Eksportuj wybrane %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Plik do importu" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Format" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Powrót" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Wyślij" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Potwierdź import" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Zostaną zaimportowane następujące pola: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Błędy" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Numer linii" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Podgląd" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nowy" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Pominięty" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Usuń" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Zaktualizowany" django-import-export-3.3.4/import_export/locale/pt_BR/000077500000000000000000000000001453511766500230075ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001453511766500245745ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000041571453511766500264020ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!TGv]t|->):?_Y #1 04:     %s through import_exportBelow 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 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-3.3.4/import_export/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000073161453511766500264050ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s através import_export " #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "A importação foi completada com {} novas e {} atualizadas {}" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importar" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportar" #: admin.py:892 msgid "You must select an export format." msgstr "Você tem que selecionar um formato de exportação." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportar %(verbose_name_plural)s selecionados" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Arquivo a ser importado" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formato" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Início" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Enviar" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Confirmar importação" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Este importador vai importar os seguintes campos:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Erros" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Número da linha" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Algumas linhas não foram validadas" #: templates/admin/import_export/import.html:114 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:119 msgid "Row" msgstr "Linha" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Campo não é específico" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Prévia" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Novo" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Não usados" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Remover" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Atualizar" django-import-export-3.3.4/import_export/locale/ru/000077500000000000000000000000001453511766500224275ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ru/LC_MESSAGES/000077500000000000000000000000001453511766500242145ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000051241453511766500260150ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!Tv# G*r  F>,k* C P Dc  C  A      %s through import_exportBelow 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Ниже показано то, что будет импортировано. Нажмите 'Подтвердить импорт', если Вас устраивает результатПодтвердить импортУдаленоОшибкиЭкспортЭкспортировать выбранные %(verbose_name_plural)sФайл для импортаФорматГлавнаяИмпортИмпорт завершен, {} новых и {} обновлено.Номер строкиДобавленоНе относящиеся к конкретному полюПо возможности исправьте эти ошибки в своих данных, а затем повторно загрузите их, используя форму выше.ПредпросмотрСтрокаПропущеноНекоторые строки не прошли валидациюОтправитьБудут импортированы следующие поля: ОбновленоНеобходимо выбрать формат экспортаdjango-import-export-3.3.4/import_export/locale/ru/LC_MESSAGES/django.po000066400000000000000000000103331453511766500260160ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s через import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "Импорт завершен, {} новых и {} обновлено." #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Импорт" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Экспорт" #: admin.py:892 msgid "You must select an export format." msgstr "Необходимо выбрать формат экспорта" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Экспортировать выбранные %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Файл для импорта" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Формат" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Главная" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Отправить" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Подтвердить импорт" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Будут импортированы следующие поля: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Ошибки" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Номер строки" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Некоторые строки не прошли валидацию" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" "По возможности исправьте эти ошибки в своих данных, а затем повторно " "загрузите их, используя форму выше." #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "Строка" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Не относящиеся к конкретному полю" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Предпросмотр" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Добавлено" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Пропущено" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Удалено" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Обновлено" django-import-export-3.3.4/import_export/locale/sk/000077500000000000000000000000001453511766500224165ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/sk/LC_MESSAGES/000077500000000000000000000000001453511766500242035ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/sk/LC_MESSAGES/django.mo000066400000000000000000000030071453511766500260020ustar00rootroot00000000000000Lh*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-3.3.4/import_export/locale/sk/LC_MESSAGES/django.po000066400000000000000000000067531453511766500260200ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "Importovať" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Exportovať" #: admin.py:892 msgid "You must select an export format." msgstr "Je potrebné vybrať formát exportu." #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Exportovať vybrané %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "Importovať súbor" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Formát" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Domov" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Odoslať" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "Potvrdiť import" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "Budú importované nasledujúce polia: " #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Chyby" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Číslo riadku" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Náhľad" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Nový" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Preskočený" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Vymazaný" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Aktualizovaný" django-import-export-3.3.4/import_export/locale/tr/000077500000000000000000000000001453511766500224265ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/tr/LC_MESSAGES/000077500000000000000000000000001453511766500242135ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/tr/LC_MESSAGES/django.mo000066400000000000000000000041711453511766500260150ustar00rootroot00000000000000%@AhZ'%*/1 amq_0M!TAv[sw 3   4/@EsZ  > OY     %s through import_exportBelow 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_exportAş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-3.3.4/import_export/locale/tr/LC_MESSAGES/django.po000066400000000000000000000073751453511766500260310ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s vasıtasıyla import_export" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "{} yeni ve {} güncellenen {} ile içe aktarma bitti" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "İçe aktar" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "Dışa aktar" #: admin.py:892 msgid "You must select an export format." msgstr "Bir dosya biçimi seçmelisiniz" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "Seçililenleri dışa aktar %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "İçe alınacak dosya" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "Dosya biçimi" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "Ana sayfa" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "Kaydet" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "İçe aktarmayı onayla" #: templates/admin/import_export/import.html:44 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:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "Hatalar" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "Satır numarası" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "Bazı satırlar doğrulanamadı" #: templates/admin/import_export/import.html:114 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:119 msgid "Row" msgstr "Satır" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "Alan olmayana özgü" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "Ön izleme" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "Yeni" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "Atlandı" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "Sil" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "Güncelle" django-import-export-3.3.4/import_export/locale/zh_Hans/000077500000000000000000000000001453511766500233735ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/zh_Hans/LC_MESSAGES/000077500000000000000000000000001453511766500251605ustar00rootroot00000000000000django-import-export-3.3.4/import_export/locale/zh_Hans/LC_MESSAGES/django.mo000066400000000000000000000036711453511766500267660ustar00rootroot00000000000000%01hJ'/ LX\_o08!?=a$a &3:A'H p}7Q9@DKgn$     %s through import_exportBelow 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导入以下是导入数据的预览。如果确认结果没有问题,可以点击 “确认导入”确认导入删除错误导出导出选中的 %(verbose_name_plural)s导入文件格式导入导入成功,新增{}条记录,更新{}条记录。行号新增没有指定的字段请使用上面的表单,纠正这些提示有错误的数据,并重新上传预览行忽略某些行验数据证失败提交此次将导入以下字段:更新您必须选择一个导出格式。django-import-export-3.3.4/import_export/locale/zh_Hans/LC_MESSAGES/django.po000066400000000000000000000071071453511766500267670ustar00rootroot00000000000000# 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: 2023-11-08 18:22+0000\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:239 #, python-format msgid "%s through import_export" msgstr "%s 通过 django-import-export导入" #: admin.py:246 msgid "Import finished, with {} new and {} updated {}." msgstr "导入成功,新增{}条记录,更新{}条记录。" #: admin.py:491 #, python-format msgid "" "%(exc_name)s encountered while trying to read file. Ensure you have chosen " "the correct format for the file." msgstr "" #: admin.py:642 templates/admin/import_export/change_list_import_item.html:5 #: templates/admin/import_export/import.html:19 msgid "Import" msgstr "导入" #: admin.py:827 templates/admin/import_export/change_list_export_item.html:5 #: templates/admin/import_export/export.html:12 msgid "Export" msgstr "导出" #: admin.py:892 msgid "You must select an export format." msgstr "您必须选择一个导出格式。" #: admin.py:917 #, python-format msgid "Export selected %(verbose_name_plural)s" msgstr "导出选中的 %(verbose_name_plural)s" #: forms.py:12 msgid "Resource" msgstr "" #: forms.py:40 msgid "File to import" msgstr "导入文件" #: forms.py:42 forms.py:83 forms.py:116 msgid "Format" msgstr "格式" #: templates/admin/import_export/base.html:11 msgid "Home" msgstr "" #: templates/admin/import_export/export.html:36 #: templates/admin/import_export/import.html:78 msgid "Submit" msgstr "提交" #: templates/admin/import_export/import.html:30 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:33 msgid "Confirm import" msgstr "确认导入" #: templates/admin/import_export/import.html:44 msgid "This importer will import the following fields: " msgstr "此次将导入以下字段:" #: templates/admin/import_export/import.html:89 #: templates/admin/import_export/import.html:120 msgid "Errors" msgstr "错误" #: templates/admin/import_export/import.html:100 msgid "Line number" msgstr "行号" #: templates/admin/import_export/import.html:112 msgid "Some rows failed to validate" msgstr "某些行验数据证失败" #: templates/admin/import_export/import.html:114 msgid "" "Please correct these errors in your data where possible, then reupload it " "using the form above." msgstr "请使用上面的表单,纠正这些提示有错误的数据,并重新上传" #: templates/admin/import_export/import.html:119 msgid "Row" msgstr "行" #: templates/admin/import_export/import.html:146 msgid "Non field specific" msgstr "没有指定的字段" #: templates/admin/import_export/import.html:169 msgid "Preview" msgstr "预览" #: templates/admin/import_export/import.html:184 msgid "New" msgstr "新增" #: templates/admin/import_export/import.html:186 msgid "Skipped" msgstr "忽略" #: templates/admin/import_export/import.html:188 msgid "Delete" msgstr "删除" #: templates/admin/import_export/import.html:190 msgid "Update" msgstr "更新" django-import-export-3.3.4/import_export/mixins.py000066400000000000000000000177171453511766500224400ustar00rootroot00000000000000import logging import warnings from django.conf import settings from 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 logger = logging.getLogger(__name__) class BaseImportExportMixin: resource_class = None resource_classes = [] @property def formats(self): return getattr(settings, "IMPORT_EXPORT_FORMATS", base_formats.DEFAULT_FORMATS) @property def export_formats(self): return getattr(settings, "EXPORT_FORMATS", self.formats) @property def import_formats(self): return getattr(settings, "IMPORT_FORMATS", self.formats) def check_resource_classes(self, resource_classes): if resource_classes and not hasattr(resource_classes, "__getitem__"): raise Exception( "The resource_classes field type must be " "subscriptable (list, tuple, ...)" ) def get_resource_classes(self): """Return subscriptable type (list, tuple, ...) containing resource classes""" if self.resource_classes and self.resource_class: raise Exception( "Only one of 'resource_class' and 'resource_classes' can be set" ) if hasattr(self, "get_resource_class"): warnings.warn( "The 'get_resource_class()' method has been deprecated. " "Please implement the new 'get_resource_classes()' method", DeprecationWarning, ) return [self.get_resource_class()] if self.resource_class: warnings.warn( "The 'resource_class' field has been deprecated. " "Please implement the new 'resource_classes' field", DeprecationWarning, ) if not self.resource_classes and not self.resource_class: return [modelresource_factory(self.model)] if self.resource_classes: return self.resource_classes return [self.resource_class] def get_resource_kwargs(self, request, *args, **kwargs): return {} def get_resource_index(self, form): resource_index = 0 if form and "resource" in form.cleaned_data: try: resource_index = int(form.cleaned_data["resource"]) except ValueError: pass return resource_index class BaseImportMixin(BaseImportExportMixin): def get_import_resource_classes(self): """ Returns ResourceClass subscriptable (list, tuple, ...) to use for import. """ if hasattr(self, "get_import_resource_class"): warnings.warn( "The 'get_import_resource_class()' method has been deprecated. " "Please implement the new 'get_import_resource_classes()' method", DeprecationWarning, ) return [self.get_import_resource_class()] resource_classes = self.get_resource_classes() self.check_resource_classes(resource_classes) return resource_classes def get_import_formats(self): """ Returns available import formats. """ return [f for f in self.import_formats if f().can_import()] def get_import_resource_kwargs(self, request, *args, **kwargs): return self.get_resource_kwargs(request, *args, **kwargs) def choose_import_resource_class(self, form): resource_index = self.get_resource_index(form) return self.get_import_resource_classes()[resource_index] class BaseExportMixin(BaseImportExportMixin): model = None escape_exported_data = False escape_html = False escape_formulae = False @property def should_escape_html(self): if hasattr(settings, "IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT"): warnings.warn( "IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT is deprecated and " "will be removed in a future release.", DeprecationWarning, ) v = getattr(settings, "IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT", self.escape_html) if v is True: logger.debug("IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT is enabled") return v @property def should_escape_formulae(self): v = getattr( settings, "IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT", self.escape_formulae ) if v is True: logger.debug("IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT is enabled") return v def get_export_formats(self): """ Returns available export formats. """ return [f for f in self.export_formats if f().can_export()] def get_export_resource_classes(self): """ Returns ResourceClass subscriptable (list, tuple, ...) to use for export. """ if hasattr(self, "get_export_resource_class"): warnings.warn( "The 'get_export_resource_class()' method has been deprecated. " "Please implement the new 'get_export_resource_classes()' method", DeprecationWarning, ) return [self.get_export_resource_class()] resource_classes = self.get_resource_classes() self.check_resource_classes(resource_classes) return resource_classes def choose_export_resource_class(self, form): resource_index = self.get_resource_index(form) return self.get_export_resource_classes()[resource_index] 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): export_form = kwargs.get("export_form") export_class = self.choose_export_resource_class(export_form) export_resource_kwargs = self.get_export_resource_kwargs( request, *args, **kwargs ) cls = export_class(**export_resource_kwargs) export_data = cls.export(*args, queryset=queryset, **kwargs) return export_data 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-3.3.4/import_export/resources.py000066400000000000000000001423431453511766500231350ustar00rootroot00000000000000import functools import logging import traceback import warnings from collections import OrderedDict from copy import deepcopy import tablib from diff_match_patch import diff_match_patch from django.conf import settings from django.core.exceptions import ( FieldDoesNotExist, 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, set_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 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 def has_natural_foreign_key(model): """ Determine if a model has natural foreign key functions """ return hasattr(model, "natural_key") and hasattr( model.objects, "get_by_natural_key" ) 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 :class:`~import_export.results.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 :class:`~import_export.results.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. """ store_row_values = False """ If True, each row's raw data will be stored in each :class:`~import_export.results.RowResult`. Enabling this parameter will increase the memory usage during import which should be considered when importing large datasets. """ store_instance = False """ If True, the row instance will be stored in each :class:`~import_export.results.RowResult`. Enabling this parameter will increase the memory usage during import which should be considered when importing large datasets. """ use_natural_foreign_keys = False """ If True, use_natural_foreign_keys = True will be passed to all foreign key widget fields whose models support natural foreign keys. That is, the model has a natural_key function and the manager has a get_by_natural_key function. """ 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, **kwargs): """ kwargs: An optional dict of kwargs. Subclasses can use kwargs to pass dynamic values to enhance import / exports. """ # 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 @classmethod 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, result=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: self.handle_import_error(result, e, raise_errors) finally: self.create_instances.clear() def bulk_update( self, using_transactions, dry_run, raise_errors, batch_size=None, result=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: self.handle_import_error(result, e, raise_errors) finally: self.update_instances.clear() def bulk_delete(self, using_transactions, dry_run, raise_errors, result=None): """ 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: self.handle_import_error(result, e, raise_errors) 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, is_create, 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. :param instance: The instance of the object to be persisted. :param is_create: A boolean flag to indicate whether this is a new object to be created, or an existing object to be updated. :param using_transactions: A flag to indicate whether db transactions are used. :param dry_run: A flag to indicate dry-run mode. """ self.before_save_instance(instance, using_transactions, dry_run) if self._meta.use_bulk: if is_create: self.create_instances.append(instance) else: self.update_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, row, import_validation_errors=None): """ 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. By default, rows are not skipped if validation errors have been detected during import. You can change this behavior and choose to ignore validation errors by overriding this method. 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, row, import_validation_errors=None): # Add code here return super().skip_row(instance, original, row, import_validation_errors=import_validation_errors) """ if ( not self._meta.skip_unchanged or self._meta.skip_diff or import_validation_errors ): return False for field in self.get_import_fields(): # For fields that are models.fields.related.ManyRelatedManager # we need to compare the results if isinstance(field.widget, widgets.ManyToManyWidget): # #1437 - handle m2m field not present in import file if field.column_name not in row.keys(): continue # m2m instance values are taken from the 'row' because they # have not been written to the 'instance' at this point instance_values = list(field.clean(row)) original_values = ( list() if original.pk is None else list(field.get_value(original).all()) ) if len(instance_values) != len(original_values): return False if sorted(v.pk for v in instance_values) != sorted( v.pk for v in original_values ): return False else: 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. :param row: A ``dict`` of the import row. :param row_result: A ``RowResult`` instance. References the persisted ``instance`` as an attribute. :param row_number: The row number from the dataset. """ pass def after_import_instance(self, instance, new, row_number=None, **kwargs): """ Override to add additional logic. Does nothing by default. """ pass def handle_import_error(self, result, error, raise_errors=False): logger.debug(error, exc_info=error) if result: tb_info = traceback.format_exc() result.append_base_error(self.get_error_result_class()(error, tb_info)) if raise_errors: raise def import_row( self, row, instance_loader, using_transactions=True, dry_run=False, raise_errors=None, **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. """ if raise_errors is not None: warnings.warn( "raise_errors argument is deprecated and " "will be removed in a future release.", category=DeprecationWarning, ) skip_diff = self._meta.skip_diff row_result = self.get_row_result_class()() if self._meta.store_row_values: row_result.row_values = row 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) if self._meta.store_instance: row_result.instance = 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, import_validation_errors): row_result.import_type = RowResult.IMPORT_TYPE_SKIP else: self.validate_instance(instance, import_validation_errors) self.save_instance(instance, new, using_transactions, dry_run) self.save_m2m(instance, row, using_transactions, dry_run) row_result.add_instance_info(instance) if self._meta.store_instance: row_result.instance = instance if not skip_diff: diff.compare_with(self, instance, dry_run) if not new: row_result.original = original 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)) 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): result = self.import_data_inner( dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, **kwargs ) if using_transactions and ( dry_run or result.has_errors() or (rollback_on_validation_errors and result.has_validation_errors()) ): set_rollback(True, using=db_connection) return result def import_data_inner( self, dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, rollback_on_validation_errors=None, **kwargs ): if rollback_on_validation_errors is not None: warnings.warn( "rollback_on_validation_errors argument is deprecated and will be " "removed in a future release.", category=DeprecationWarning, ) result = self.get_result_class()() result.diff_headers = self.get_diff_headers() result.total_rows = len(dataset) db_connection = self.get_db_connection_name() try: with atomic_if_using_transaction(using_transactions, using=db_connection): self.before_import(dataset, using_transactions, dry_run, **kwargs) except Exception as e: self.handle_import_error(result, e, raise_errors) 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, data_row in enumerate(dataset, 1): row = OrderedDict(zip(dataset.headers, data_row)) with atomic_if_using_transaction( using_transactions and not self._meta.use_bulk, using=db_connection ): row_result = self.import_row( row, instance_loader, using_transactions=using_transactions, dry_run=dry_run, row_number=i, **kwargs ) 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: with atomic_if_using_transaction( using_transactions, using=db_connection ): self.bulk_create( using_transactions, dry_run, raise_errors, batch_size=self._meta.batch_size, result=result, ) if len(self.update_instances) == self._meta.batch_size: with atomic_if_using_transaction( using_transactions, using=db_connection ): self.bulk_update( using_transactions, dry_run, raise_errors, batch_size=self._meta.batch_size, result=result, ) if len(self.delete_instances) == self._meta.batch_size: with atomic_if_using_transaction( using_transactions, using=db_connection ): self.bulk_delete( using_transactions, dry_run, raise_errors, result=result ) 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, result=result ) self.bulk_update( using_transactions, dry_run, raise_errors, result=result ) self.bulk_delete( using_transactions, dry_run, raise_errors, result=result ) 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: self.handle_import_error(result, e, raise_errors) 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 filter_export(self, queryset, *args, **kwargs): """ Override to filter an export queryset. """ return queryset def export_field(self, field, obj): field_name = self.get_field_name(field) dehydrate_method = field.get_dehydrate_method(field_name) method = getattr(self, dehydrate_method, 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, *args, queryset=None, **kwargs): """ Exports a resource. :returns: Dataset object. """ if len(args) == 1 and ( isinstance(args[0], QuerySet) or isinstance(args[0], list) ): # issue 1565: definition of export() was incorrect # if queryset is being passed, it must be as the first arg or named # parameter # this logic is included for backwards compatibility: # if the method is being called without a named parameter, add a warning # this check should be removed in a future release warnings.warn( "'queryset' must be supplied as a named parameter", category=DeprecationWarning, ) queryset = args[0] self.before_export(queryset, *args, **kwargs) if queryset is None: queryset = self.get_queryset() queryset = self.filter_export(queryset, *args, **kwargs) 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 f.name not 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", "CharField": widgets.CharWidget, "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, "JSONField": widgets.JSONWidget, } @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 """ model = get_related_model(field) use_natural_foreign_keys = ( has_natural_foreign_key(model) and cls._meta.use_natural_foreign_keys ) return functools.partial( widgets.ForeignKeyWidget, model=model, use_natural_foreign_keys=use_natural_foreign_keys, ) @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 field. 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 except ImportError: # ImportError: No module named psycopg2.extras class ArrayField: pass if isinstance(f, ArrayField): return widgets.SimpleArrayWidget 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() @classmethod def get_display_name(cls): if hasattr(cls._meta, "name"): return cls._meta.name return cls.__name__ 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-3.3.4/import_export/results.py000066400000000000000000000135611453511766500226230ustar00rootroot00000000000000from 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: """Container for values relating to a row import.""" 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): #: An instance of :class:`~import_export.results.Error` which may have been #: raised during import. self.errors = [] #: Contains any ValidationErrors which may have been raised during import. self.validation_error = None #: A HTML representation of the difference between the 'original' and #: 'updated' model instance. self.diff = None #: A string identifier which identifies what type of import was performed. self.import_type = None #: Can the raw values associated with each imported row. self.row_values = {} #: The instance id (used in Admin UI) self.object_id = None #: The object representation (used in Admin UI) self.object_repr = None #: A reference to the model instance which was created, updated or deleted. self.instance = None #: A reference to the model instance before updates were applied. #: This value is only set for updates. self.original = None #: A boolean flag indicating whether the record is `new` or not. #: Deprecated: use the value of ``import_type`` instead. #: See issue 1586. self.new_record = 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): headers = list() if not headers else 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-3.3.4/import_export/signals.py000066400000000000000000000001561453511766500225560ustar00rootroot00000000000000from django.dispatch import Signal # Args: model post_export = Signal() # Args: model post_import = Signal() django-import-export-3.3.4/import_export/static/000077500000000000000000000000001453511766500220315ustar00rootroot00000000000000django-import-export-3.3.4/import_export/static/import_export/000077500000000000000000000000001453511766500247445ustar00rootroot00000000000000django-import-export-3.3.4/import_export/static/import_export/action_formats.js000066400000000000000000000013751453511766500303200ustar00rootroot00000000000000(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-3.3.4/import_export/static/import_export/guess_format.js000066400000000000000000000013071453511766500300010ustar00rootroot00000000000000(function($) { $().ready(function () { $('input.guess_format[type="file"]').change(function () { var files = this.files; var dropdowns = $(this.form).find('select.guess_format'); if(files.length > 0) { var extension = files[0].name.split('.').pop().trim().toLowerCase(); for(var i = 0; i < dropdowns.length; i++) { var dropdown = dropdowns[i]; dropdown.selectedIndex = 0; for(var j = 0; j < dropdown.options.length; j++) { if(extension === dropdown.options[j].text.trim().toLowerCase()) { dropdown.selectedIndex = j; break; } } } } }); }); })(django.jQuery); django-import-export-3.3.4/import_export/static/import_export/import.css000066400000000000000000000040031453511766500267650ustar00rootroot00000000000000.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; } @media (prefers-color-scheme: dark) { table.import-preview tr.skip { background-color: #2d2d2d; } table.import-preview tr.new { background-color: #42274d; } table.import-preview tr.delete { background-color: #064140; } table.import-preview tr.update { background-color: #020230; } .validation-error-container { background-color: #003e3e; } /* these declarations are necessary to forcibly override the formatting applied by the diff-match-patch python library */ table.import-preview td ins { background-color: #190019 !important; } table.import-preview td del { background-color: #001919 !important; } }django-import-export-3.3.4/import_export/templates/000077500000000000000000000000001453511766500225405ustar00rootroot00000000000000django-import-export-3.3.4/import_export/templates/admin/000077500000000000000000000000001453511766500236305ustar00rootroot00000000000000django-import-export-3.3.4/import_export/templates/admin/import_export/000077500000000000000000000000001453511766500265435ustar00rootroot00000000000000django-import-export-3.3.4/import_export/templates/admin/import_export/base.html000066400000000000000000000014251453511766500303450ustar00rootroot00000000000000{% 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-3.3.4/import_export/templates/admin/import_export/change_list.html000066400000000000000000000006101453511766500317060ustar00rootroot00000000000000{% extends base_change_list_template %} {# 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-3.3.4/import_export/templates/admin/import_export/change_list_export.html000066400000000000000000000002731453511766500333140ustar00rootroot00000000000000{% 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-3.3.4/import_export/templates/admin/import_export/change_list_export_item.html000066400000000000000000000003171453511766500343310ustar00rootroot00000000000000{% load i18n %} {% load admin_urls %} {% if has_export_permission %}
  • {% trans "Export" %}
  • {% endif %} django-import-export-3.3.4/import_export/templates/admin/import_export/change_list_import.html000066400000000000000000000002731453511766500333050ustar00rootroot00000000000000{% 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.html000066400000000000000000000003761453511766500346330ustar00rootroot00000000000000django-import-export-3.3.4/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-3.3.4/import_export/templates/admin/import_export/change_list_import_item.html000066400000000000000000000002701453511766500343200ustar00rootroot00000000000000{% load i18n %} {% load admin_urls %} {% if has_import_permission %}
  • {% trans "Import" %}
  • {% endif %} django-import-export-3.3.4/import_export/templates/admin/import_export/export.html000066400000000000000000000017361453511766500307610ustar00rootroot00000000000000{% extends "admin/import_export/base.html" %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% block extrahead %}{{ block.super }} {{ form.media }} {% endblock %} {% block breadcrumbs_last %} {% trans "Export" %} {% endblock %} {% block content %}
    {% csrf_token %} {% include "admin/import_export/resource_fields_list.html" with import_or_export="export" %}
    {% 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-3.3.4/import_export/templates/admin/import_export/import.html000066400000000000000000000133631453511766500307510ustar00rootroot00000000000000{% extends "admin/import_export/base.html" %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% load static %} {% block extrastyle %}{{ block.super }}{% endblock %} {% block extrahead %}{{ block.super }} {% if confirm_form %} {{ confirm_form.media }} {% else %} {{ form.media }} {% endif %} {% endblock %} {% block breadcrumbs_last %} {% trans "Import" %} {% endblock %} {% block content %} {% if confirm_form %} {% block confirm_import_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'" %}

    {% endblock %} {% else %} {% block import_form %}
    {% csrf_token %} {% include "admin/import_export/resource_fields_list.html" with import_or_export="import" %} {% block form_detail %}
    {% for field in form %}
    {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.field.help_text %}

    {{ field.field.help_text|safe }}

    {% endif %}
    {% endfor %}
    {% endblock %} {% block form_submit_button %}
    {% endblock %}
    {% endblock %} {% endif %} {% if result %} {% if result.has_errors %} {% block 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 %}
    {% endblock %} {% elif result.has_validation_errors %} {% block 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 }}
    {% endblock %} {% else %} {% block preview %}

    {% 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 }}
    {% endblock %} {% endif %} {% endif %} {% endblock %} django-import-export-3.3.4/import_export/templates/admin/import_export/resource_fields_list.html000066400000000000000000000011621453511766500336410ustar00rootroot00000000000000{% load i18n %} {% block fields_help %}

    {% if import_or_export == "export" %} {% trans "This exporter will export the following fields: " %} {% elif import_or_export == "import" %} {% trans "This importer will import the following fields: " %} {% endif %} {% if fields_list|length <= 1 %} {{ fields_list.0.1|join:", " }} {% else %}

    {% for resource, fields in fields_list %}
    {{ resource }}
    {{ fields|join:", " }}
    {% endfor %}
    {% endif %}

    {% endblock %} django-import-export-3.3.4/import_export/templatetags/000077500000000000000000000000001453511766500232345ustar00rootroot00000000000000django-import-export-3.3.4/import_export/templatetags/__init__.py000066400000000000000000000000001453511766500253330ustar00rootroot00000000000000django-import-export-3.3.4/import_export/templatetags/import_export_tags.py000066400000000000000000000005021453511766500275340ustar00rootroot00000000000000from 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-3.3.4/import_export/tmp_storages.py000066400000000000000000000046631453511766500236340ustar00rootroot00000000000000import 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, **kwargs): self.name = kwargs.get("name", None) self.read_mode = kwargs.get("read_mode", "r") self.encoding = kwargs.get("encoding", None) def save(self, data): raise NotImplementedError def read(self): raise NotImplementedError def remove(self): raise NotImplementedError class TempFolderStorage(BaseStorage): def save(self, data): with self._open(mode="w") as file: file.write(data) def read(self): with self._open(mode=self.read_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) def _open(self, mode="r"): if self.name: return open(self.get_full_path(), mode, encoding=self.encoding) else: tmp_file = tempfile.NamedTemporaryFile(delete=False) self.name = tmp_file.name return tmp_file 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): if not self.name: self.name = uuid4().hex cache.set(self.CACHE_PREFIX + self.name, data, self.CACHE_LIFETIME) def read(self): return cache.get(self.CACHE_PREFIX + self.name) def remove(self): cache.delete(self.CACHE_PREFIX + self.name) class MediaStorage(BaseStorage): MEDIA_FOLDER = "django-import-export" def __init__(self, **kwargs): # issue 1589 - Ensure that for MediaStorage, we read in binary mode kwargs.update({"read_mode": "rb"}) super().__init__(**kwargs) def save(self, data): if not self.name: self.name = uuid4().hex default_storage.save(self.get_full_path(), ContentFile(data)) def read(self): with default_storage.open(self.get_full_path(), mode=self.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-3.3.4/import_export/utils.py000066400000000000000000000017461453511766500222640ustar00rootroot00000000000000from 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) def original(method): """ A decorator used to mark some class methods as 'original', making it easy to detect whether they have been overridden by a subclass. Useful for method deprecation. """ method.is_original = True return method django-import-export-3.3.4/import_export/widgets.py000066400000000000000000000377441453511766500226010ustar00rootroot00000000000000import 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 from django.utils.formats import number_format 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, **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): """ Widget for converting numeric fields. :param coerce_to_string: If True, render will return a string representation of the value (None is returned as ""), otherwise the value is returned. """ def __init__(self, coerce_to_string=False): self.coerce_to_string = coerce_to_string 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): if self.coerce_to_string: return "" if value is None else number_format(value) return value class FloatWidget(NumberWidget): """ Widget for converting float fields. """ def clean(self, value, row=None, **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, **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, **kwargs): if self.is_empty(value): return None return Decimal(force_str(value)) class CharWidget(Widget): """ Widget for converting text fields. :param coerce_to_string: If True, the value returned by clean() is cast to a string. :param allow_blank: If True, and if coerce_to_string is True, then clean() will return null values as empty strings, otherwise as null. """ def __init__(self, coerce_to_string=False, allow_blank=False): """ """ self.coerce_to_string = coerce_to_string self.allow_blank = allow_blank def clean(self, value, row=None, **kwargs): val = super().clean(value, row, **kwargs) if self.coerce_to_string is True: if val is None: if self.allow_blank is True: return "" else: return force_str(val) return val 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, **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. If none is set, either ``settings.DATE_INPUT_FORMATS`` or ``"%Y-%m-%d"`` is used. """ 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, **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, **kwargs): dt = None if not value: return None if isinstance(value, datetime): dt = value else: for format_ in self.formats: try: dt = datetime.strptime(value, format_) except (ValueError, TypeError): continue if dt: if settings.USE_TZ and timezone.is_naive(dt): dt = timezone.make_aware(dt) return dt 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. If none is set, either ``settings.DATETIME_INPUT_FORMATS`` or ``"%H:%M:%S"`` is used. """ 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, **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, **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, **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, **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 either the PK or a user specified field that uniquely identifies the instance in both export and import. The lookup field defaults to using the primary key (``pk``) as lookup criterion but can be customized 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. :param use_natural_foreign_keys: Use natural key functions to identify related object, default to False """ def __init__(self, model, field="pk", use_natural_foreign_keys=False, **kwargs): self.model = model self.field = field self.use_natural_foreign_keys = use_natural_foreign_keys super().__init__(**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, *args, **kwargs): 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, **kwargs): val = super().clean(value) if val: if self.use_natural_foreign_keys: # natural keys will always be a tuple, which ends up as a json list. value = json.loads(value) return self.model.objects.get_by_natural_key(*value) else: return self.get_queryset(value, row, **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: if self.use_natural_foreign_keys: # inbound natural keys must be a json list. return json.dumps(value.natural_key()) else: 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, **kwargs): if separator is None: separator = "," if field is None: field = "pk" self.model = model self.separator = separator self.field = field super().__init__(**kwargs) def clean(self, value, row=None, **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-3.3.4/requirements/000077500000000000000000000000001453511766500203525ustar00rootroot00000000000000django-import-export-3.3.4/requirements/base.txt000066400000000000000000000001721453511766500220250ustar00rootroot00000000000000Django>=3.2 # tablib temporarily pinned to 3.5.0 - see issue #1602 tablib[html,ods,xls,xlsx,yaml]==3.5.0 diff-match-patch django-import-export-3.3.4/requirements/deploy.txt000066400000000000000000000000231453511766500224020ustar00rootroot00000000000000wheel zest.releaserdjango-import-export-3.3.4/requirements/docs.txt000066400000000000000000000000441453511766500220410ustar00rootroot00000000000000-r base.txt sphinx sphinx-rtd-theme django-import-export-3.3.4/requirements/test.txt000066400000000000000000000001231453511766500220660ustar00rootroot00000000000000psycopg2-binary mysqlclient chardet pytz memory-profiler django-extensions coveragedjango-import-export-3.3.4/runtests.py000066400000000000000000000012441453511766500200710ustar00rootroot00000000000000""" Helper script to generate coverage data only if running in CI. This script is called from tox (see tox.ini). Coverage files are generated only if a `COVERAGE` environment variable is present. This is necessary to prevent unwanted coverage files when running locally (issue #1424) """ import os def main(): coverage_args = "-m coverage run" if os.environ.get("COVERAGE") else "" retval = os.system( "python -W error::DeprecationWarning -W error::PendingDeprecationWarning " f"{coverage_args} " "./tests/manage.py test core --settings=settings" ) if retval != 0: exit(1) exit(0) if __name__ == "__main__": main() django-import-export-3.3.4/runtests.sh000077500000000000000000000015641453511766500200630ustar00rootroot00000000000000#!/usr/bin/env sh # run tests against all supported databases using tox # postgres / mysql run via docker # sqlite (default) runs against local database file (database.db) # use pyenv or similar to install multiple python instances export DJANGO_SETTINGS_MODULE=settings export IMPORT_EXPORT_POSTGRESQL_USER=pguser export IMPORT_EXPORT_POSTGRESQL_PASSWORD=pguserpass export IMPORT_EXPORT_MYSQL_USER=mysqluser export IMPORT_EXPORT_MYSQL_PASSWORD=mysqluserpass echo "starting local database instances" docker-compose -f tests/docker-compose.yml up -d echo "waiting for db initialization" sleep 45 echo "running tests (sqlite)" tox echo "running tests (mysql)" export IMPORT_EXPORT_TEST_TYPE=mysql-innodb tox echo "running tests (postgres)" export IMPORT_EXPORT_TEST_TYPE=postgres tox echo "removing local database instances" docker-compose -f tests/docker-compose.yml down -v django-import-export-3.3.4/setup.cfg000066400000000000000000000004311453511766500174460ustar00rootroot00000000000000[metadata] license_file = LICENSE [zest.releaser] create-wheel = yes python-file-with-version = import_export/__init__.py [isort] profile = black [flake8] exclude = build,.git,.tox extend-ignore = E203 max-line-length = 88 [coverage:run] parallel = true source = import_export django-import-export-3.3.4/setup.py000066400000000000000000000037201453511766500173430ustar00rootroot00000000000000import os from setuptools import find_packages, setup VERSION = __import__("import_export").__version__ CLASSIFIERS = [ "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development", ] # tablib temporarily pinned to 3.5.0 - see issue #1602 install_requires = [ "diff-match-patch", "Django>=3.2", "tablib[html,ods,xls,xlsx,yaml]==3.5.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="Bojan Mihelač", author_email="djangoimportexport@gmail.com", maintainer="Matthew Hegarty", maintainer_email="djangoimportexport@gmail.com", 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", # noqa: E501 }, packages=find_packages(exclude=["tests"]), include_package_data=True, install_requires=install_requires, python_requires=">=3.8", classifiers=CLASSIFIERS, zip_safe=False, ) django-import-export-3.3.4/tests/000077500000000000000000000000001453511766500167715ustar00rootroot00000000000000django-import-export-3.3.4/tests/books-sample.csv000066400000000000000000000003331453511766500221010ustar00rootroot00000000000000id,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-3.3.4/tests/core/000077500000000000000000000000001453511766500177215ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/__init__.py000066400000000000000000000000001453511766500220200ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/admin.py000066400000000000000000000073551453511766500213750ustar00rootroot00000000000000from django.contrib import admin from import_export.admin import ( ExportActionModelAdmin, ImportExportModelAdmin, ImportMixin, ) from import_export.resources import ModelResource from .forms import CustomConfirmImportForm, CustomExportForm, CustomImportForm from .models import Author, Book, Category, Child, EBook, LegacyBook 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 BookNameResource(ModelResource): class Meta: model = Book fields = ["id", "name"] name = "Export/Import only book names" class BookAdmin(ImportExportModelAdmin): list_display = ("name", "author", "added") list_filter = ["categories", "author"] resource_classes = [BookResource, BookNameResource] change_list_template = "core/admin/change_list.html" class CategoryAdmin(ExportActionModelAdmin): def export_admin_action(self, request, queryset): return super().export_admin_action(request, queryset) class AuthorAdmin(ImportMixin, admin.ModelAdmin): pass class EBookResource(ModelResource): def __init__(self, **kwargs): super().__init__() self.author_id = kwargs.get("author_id") def filter_export(self, queryset, *args, **kwargs): return queryset.filter(author_id=self.author_id) class Meta: model = EBook class CustomBookAdmin(ImportExportModelAdmin): """Example usage of custom import / export forms""" resource_classes = [EBookResource] import_form_class = CustomImportForm confirm_form_class = CustomConfirmImportForm export_form_class = CustomExportForm def get_confirm_form_initial(self, request, import_form): initial = super().get_confirm_form_initial(request, import_form) # Pass on the `author` value from the import form to # the confirm form (if provided) if import_form: initial["author"] = import_form.cleaned_data["author"].id return initial def get_import_resource_kwargs(self, request, *args, **kwargs): # update resource kwargs so that the Resource is passed the authenticated user # This is included as an example of how dynamic values # can be passed to resources kwargs = super().get_resource_kwargs(request, *args, **kwargs) kwargs.update({"user": request.user}) return kwargs def get_export_resource_kwargs(self, request, *args, **kwargs): # this is overridden to demonstrate that custom form fields can be used # to override the export query. # The dict returned here will be passed as kwargs to EBookResource export_form = kwargs["export_form"] if export_form: return dict(author_id=export_form.cleaned_data["author"].id) return {} class LegacyBookAdmin(BookAdmin): """ BookAdmin with deprecated function overrides. This class exists solely to test import works correctly using the deprecated functions. This class can be removed when the deprecated code is removed. """ def get_import_form(self): return super().get_import_form() def get_confirm_import_form(self): return super().get_confirm_import_form() def get_form_kwargs(self, form, *args, **kwargs): return super().get_form_kwargs(form, *args, **kwargs) def get_export_form(self): return super().get_export_form() 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) admin.site.register(LegacyBook, LegacyBookAdmin) django-import-export-3.3.4/tests/core/exports/000077500000000000000000000000001453511766500214255ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/exports/books-ISO-8859-1.csv000066400000000000000000000000641453511766500244600ustar00rootroot00000000000000id,name,author_email 1,Merci toi,test@example.com django-import-export-3.3.4/tests/core/exports/books-dos.csv000066400000000000000000000000641453511766500240420ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-3.3.4/tests/core/exports/books-empty-author-email.xlsx000066400000000000000000000226621453511766500272130ustar00rootroot00000000000000PK!bh^[Content_Types].xml (N0EHC-Jܲ@5*Q>ēƪc[iiBj7{2hnmƻR U^7/%rZY@1__fqR4DAJh>Vƹ Z9NV8ʩji){^-I"{v^P!XS)bRrKs(3`c07M4ZƐk+|\|z(P6h_-[@!Pk2n}?L %ddN"m,ǞDO97*~ɸ8Oc|nEB!$};{[2PK!U0#L _rels/.rels (MO0 HݐBKwAH!T~I$ݿ'TG~xl/_rels/workbook.xml.rels (RMK0 0wvt/"Uɴ)&!3~*]XK/oyv5+zl;obG s>,8(%"D҆4j0u2jsMY˴S쭂 )fCy I< y!+EfMyk K5=|t G)s墙UtB),fPK!,Gxl/workbook.xmlVQo8~?;5@*YtUfۻ'x 8gLC~cfs:Qbx7&ןڲ0^BD,d*!J 2jEb!zc54뽐/k!^ J4g%ĎU n;hV猩eyB=B / OBM*ՃHVPL/+|ivf*@y[2 nt]@-qV׃``lRb'&$y.Ca^ᑕ>;by`e4@>hz{k3-u dVqB4[g'f /`XxOrF6) <=AQbsQ)!_\=(X.4 b]TF#/5R*g{|/Wg_i&/wB*z 2PTH{K0=!a`HLO9I +iJ@_p 72sʀ<}B,'2rL+%fDg>v9!!Ζh;+hxNuzi־پ~W5'^ebE6\=o-2$tXnk Rgdf9m#[lLw8,汛H]6<&{Q@=&Ѹ l=X.t(ޒ9yqnuwmz~J.ufrhZ[M('5,JXd3{7xiG1 ]s\3dBb<j^% bX9N<@[AU{k , Y]d9lv_C$ s^8dFYQb+Лʀ\Ͷ;420KJFJnSuvd%HeNϴd9f8֟l߇3KvS->ǧPU0M8U؏窚'wZW(b8SjKDicYtF{umxgd -mgo5R?*kR >]e!1^q9Њ1,ǟwZ*T~PK!N xl/theme/theme1.xmlY͋7? sw5%l$dQV32%9R(Bo=@ $'#$lJZv G~ztҽzG ’_P=ؘ$Ӗk8(4|OHe n ,K۟~rmDlI9*f8&H#ޘ+R#^bP{}2!# J{O1B (W%òBR!a1;{(~h%/V&DYCn2L`|Xsj Z{_\Zҧh4:na PաWU_]נT E A)>\Çfgנ_[K^PkPDIr.jwd A)Q RSLX"7Z2>R$I O(9%o&`T) JU>#02]`XRxbL+7 /={=_*Kn%SSՏ__7'Ŀ˗:/}}O!c&a?0BĒ@v^[ uXsXa3W"`J+U`ek)r+emgoqx(ߤDJ]8TzM5)0IYgz|]p+~o`_=|j QkekZAj|&O3!ŻBw}ь0Q'j"5,ܔ#-q&?'2ڏ ZCeLTx3&cu+ЭNxNg x)\CJZ=ޭ~TwY(aLfQuQ_B^g^ٙXtXPꗡZFq 0mxEAAfc ΙFz3Pb/3 tSٺqyjuiE-#t00,;͖Yƺ2Obr3kE"'&&S;nj*#4kx#[SvInwaD:\N1{-_- 4m+W>Z@+qt;x2#iQNSp$½:7XX/+r1w`h׼9#:Pvd5O+Oٚ.<O7sig*t; CԲ*nN-rk.yJ}0-2MYNÊQ۴3, O6muF8='?ȝZu@,JܼfwQ6vygcx D(c~\h@M[Z۟pa:ursP| })eY{>>#%0MVTP gq2ÿ 8RY^3L A E2R 4H""IZo-Uj\!G޹Й`w73xZNX3ƶGu6k6zNє8؝T%$BSq+ꬠ )Fb^gyRw6L=ߩD2eE[_Otn0EŚ=MJZ.ZU` 0^S)Q69oq0q*kP9lC`1M`_GT>2%Om M0T)D%8,aUdk'6r2J$I{[\*x(R1%;uq)/TA²N옫vsPfpw̢#d옃HXz0<9Ús`n*˷>-^k*PK!:4docProps/app.xml (Sn0 ?7rYnaΪLBdIY#WAwڍ||xx|w[.JVӾ1n[wV )(TȮOb}HH+;.إIc(q} ^?_=kGA6)_CHVHOeI %؈!XHѣoW8?sHKm@?GCY >oF+ 7ɏlE;P92h5& 4/)KV<)Erɴk;BarΝ\Td$ -ֆ,v"rx0O WKn8?=w PK!{FdocProps/core.xml (RAN0#I *hfMkHlR{VpfglWFOI>HZjJtB\K SO.ӓRX&g,8T=vJֈQZGXr [Q;_-삶\rLHR v( Zi>9` w6̴{-EOvmFc5UJJ)*l*3 pѸH)aC=x{hyXY@v\ g1k% Yzp%MvCu?X74^ܐȊqi>^+&l } o{Q<[9;@[ fpڄK@ЊiPK-!bh^[Content_Types].xmlPK-!U0#L _rels/.relsPK-!>xl/_rels/workbook.xml.relsPK-!,Gxl/workbook.xmlPK-!s2 xl/sharedStrings.xmlPK-!aX)' xl/styles.xmlPK-!!LD#xl/worksheets/_rels/sheet1.xml.relsPK-!N &xl/theme/theme1.xmlPK-! _xl/worksheets/sheet1.xmlPK-!:4:docProps/app.xmlPK-!{F docProps/core.xmlPK "django-import-export-3.3.4/tests/core/exports/books-for-delete.csv000066400000000000000000000000511453511766500252770ustar00rootroot00000000000000id,name,author_email 1,,test@example.com django-import-export-3.3.4/tests/core/exports/books-invalid-date.csv000066400000000000000000000000741453511766500256170ustar00rootroot00000000000000id,name,published 1,book,1996-01-01 2,Some book,1996x-01-01 django-import-export-3.3.4/tests/core/exports/books-mac.csv000066400000000000000000000000621453511766500240130ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-3.3.4/tests/core/exports/books-mac.tsv000066400000000000000000000000621453511766500240340ustar00rootroot00000000000000id name author_email 1 Some book test@example.com django-import-export-3.3.4/tests/core/exports/books-unicode.csv000066400000000000000000000000641453511766500247030ustar00rootroot00000000000000id,name,author_email 1,Some bookš,test@example.com django-import-export-3.3.4/tests/core/exports/books-unicode.tsv000066400000000000000000000000641453511766500247240ustar00rootroot00000000000000id name author_email 1 Some bookš test@example.com django-import-export-3.3.4/tests/core/exports/books.csv000066400000000000000000000000621453511766500232550ustar00rootroot00000000000000id,name,author_email 1,Some book,test@example.com django-import-export-3.3.4/tests/core/exports/books.xls000066400000000000000000000130001453511766500232640ustar00rootroot00000000000000ࡱ;  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-3.3.4/tests/core/exports/books.xlsx000066400000000000000000000114261453511766500234660ustar00rootroot00000000000000PKyS _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-3.3.4/tests/core/exports/child.csv000066400000000000000000000000331453511766500232210ustar00rootroot00000000000000id,parent,name 1,1234,Some django-import-export-3.3.4/tests/core/fixtures/000077500000000000000000000000001453511766500215725ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/fixtures/author.json000066400000000000000000000005241453511766500237700ustar00rootroot00000000000000[ { "model": "core.author", "pk": 11, "fields": { "name": "George R. R. Martin", "birthday": "1948-09-20" } }, { "model": "core.author", "pk": 5, "fields": { "name": "Ian Fleming", "birthday": "1908-05-28" } } ] django-import-export-3.3.4/tests/core/fixtures/book.json000066400000000000000000000021221453511766500234140ustar00rootroot00000000000000[ { "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": "The Man with the Golden Gun", "author": 5, "author_email": "ian@example.com", "imported": false, "published": "1965-04-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-3.3.4/tests/core/fixtures/category.json000066400000000000000000000004011453511766500242750ustar00rootroot00000000000000[ { "model": "core.category", "pk": 1, "fields": { "name": "Category 1" } }, { "model": "core.category", "pk": 2, "fields": { "name": "Category 2" } } ] django-import-export-3.3.4/tests/core/forms.py000066400000000000000000000012031453511766500214150ustar00rootroot00000000000000from django import forms from import_export.forms import ConfirmImportForm, ExportForm, 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 class CustomExportForm(AuthorFormMixin, ExportForm): """Customized ExportForm, with author field required""" pass django-import-export-3.3.4/tests/core/migrations/000077500000000000000000000000001453511766500220755ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/migrations/0001_initial.py000066400000000000000000000130361453511766500245430ustar00rootroot00000000000000import 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-3.3.4/tests/core/migrations/0002_book_published_time.py000066400000000000000000000006131453511766500271170ustar00rootroot00000000000000from 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-3.3.4/tests/core/migrations/0003_withfloatfield.py000066400000000000000000000012151453511766500261150ustar00rootroot00000000000000from 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-3.3.4/tests/core/migrations/0004_bookwithchapters.py000066400000000000000000000030701453511766500264720ustar00rootroot00000000000000from 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-3.3.4/tests/core/migrations/0005_addparentchild.py000066400000000000000000000025151453511766500260640ustar00rootroot00000000000000import 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-3.3.4/tests/core/migrations/0006_auto_20171130_0147.py000066400000000000000000000005271453511766500255210ustar00rootroot00000000000000from 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-3.3.4/tests/core/migrations/0007_auto_20180628_0411.py000066400000000000000000000031261453511766500255260ustar00rootroot00000000000000import 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-3.3.4/tests/core/migrations/0008_auto_20190409_0846.py000066400000000000000000000011461453511766500255410ustar00rootroot00000000000000# 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-3.3.4/tests/core/migrations/0009_auto_20211111_0807.py000066400000000000000000000060731453511766500255230ustar00rootroot00000000000000# 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-3.3.4/tests/core/migrations/0010_uuidbook.py000066400000000000000000000011741453511766500247330ustar00rootroot00000000000000# Generated by Django 2.2.7 on 2021-05-02 07:46 import uuid from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0009_auto_20211111_0807"), ] operations = [ migrations.CreateModel( name="UUIDBook", fields=[ ( "id", models.UUIDField( primary_key=True, default=uuid.uuid4, editable=False ), ), ("name", models.CharField(max_length=100, verbose_name="Book name")), ], ), ] 0011_uuidcategory_legacybook_alter_uuidbook_id_and_more.py000066400000000000000000000025471453511766500353540ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/migrations# Generated by Django 4.0.4 on 2022-05-12 12:39 import uuid from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0010_uuidbook"), ] operations = [ migrations.CreateModel( name="UUIDCategory", fields=[ ( "catid", models.UUIDField( default=uuid.uuid4, editable=False, primary_key=True, serialize=False, ), ), ("name", models.CharField(max_length=32)), ], ), migrations.CreateModel( name="LegacyBook", fields=[], options={ "proxy": True, "indexes": [], "constraints": [], }, bases=("core.book",), ), migrations.AlterField( model_name="uuidbook", name="id", field=models.UUIDField( default=uuid.uuid4, editable=False, primary_key=True, serialize=False ), ), migrations.AddField( model_name="uuidbook", name="categories", field=models.ManyToManyField(blank=True, to="core.uuidcategory"), ), ] django-import-export-3.3.4/tests/core/migrations/__init__.py000066400000000000000000000000001453511766500241740ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/models.py000066400000000000000000000117451453511766500215660ustar00rootroot00000000000000import random import string import uuid from django.core.exceptions import ValidationError from django.db import models class AuthorManager(models.Manager): """ Used to enable the get_by_natural_key method. NOTE: Manager classes are only required to enable using the natural key functionality of ForeignKeyWidget """ def get_by_natural_key(self, name): """ Django pattern function for finding an author by its name """ return self.get(name=name) class Author(models.Model): objects = AuthorManager() name = models.CharField(max_length=100) birthday = models.DateTimeField(auto_now_add=True) def natural_key(self): """ Django pattern function for serializing a model by its natural key Used only by the ForeignKeyWidget using use_natural_foreign_keys. """ return (self.name,) 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 BookManager(models.Manager): """ Added to enable get_by_natural_key method NOTE: Manager classes are only required to enable using the natural key functionality of ForeignKeyWidget """ def get_by_natural_key(self, name, author): """ Django pattern function for returning a book by its natural key """ return self.get(name=name, author=Author.objects.get_by_natural_key(author)) class Book(models.Model): objects = BookManager() 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 natural_key(self): """ Django pattern function for serializing a book by its natural key. Used only by the ForeignKeyWidget using use_natural_foreign_keys. """ return (self.name,) + self.author.natural_key() natural_key.dependencies = ["core.Author"] 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 class LegacyBook(Book): """ Book proxy model to have a separate admin url access and name. This class exists solely to test import works correctly using the deprecated functions. This class can be removed when the deprecated code is removed. """ class Meta: proxy = True class UUIDCategory(models.Model): catid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=32) class UUIDBook(models.Model): """A model which uses a UUID pk (issue 1274)""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField("Book name", max_length=100) categories = models.ManyToManyField(UUIDCategory, blank=True) django-import-export-3.3.4/tests/core/templates/000077500000000000000000000000001453511766500217175ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/templates/core/000077500000000000000000000000001453511766500226475ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/templates/core/admin/000077500000000000000000000000001453511766500237375ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/templates/core/admin/change_list.html000066400000000000000000000004051453511766500271040ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {% comment %} A template used for testing customizations to the change_list view (See #1483). {% endcomment %} {% block object-tools-items %} {{ block.super }} {% endblock %} django-import-export-3.3.4/tests/core/templates/core/category_list.html000066400000000000000000000001011453511766500263750ustar00rootroot00000000000000{{ form }} {% for obj in object_list %} {{ obj }} {% endfor %} django-import-export-3.3.4/tests/core/tests/000077500000000000000000000000001453511766500210635ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/tests/__init__.py000066400000000000000000000000001453511766500231620ustar00rootroot00000000000000django-import-export-3.3.4/tests/core/tests/test_admin_integration.py000066400000000000000000001645631453511766500262060ustar00rootroot00000000000000import os.path import warnings from datetime import datetime from io import BytesIO from unittest import mock from unittest.mock import MagicMock, patch import chardet import django import tablib from core.admin import AuthorAdmin, BookAdmin, 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.http import HttpRequest from django.test.testcases import TestCase, TransactionTestCase from django.test.utils import override_settings from django.utils.translation import gettext_lazy as _ from openpyxl.reader.excel import load_workbook from tablib import Dataset from import_export import formats from import_export.admin import ( ExportActionMixin, ExportActionModelAdmin, ExportMixin, ImportExportActionModelAdmin, ) from import_export.formats import base_formats from import_export.formats.base_formats import DEFAULT_FORMATS from import_export.tmp_storages import TempFolderStorage class AdminTestMixin(object): category_change_url = "/admin/core/category/" book_import_url = "/admin/core/book/import/" book_process_import_url = "/admin/core/book/process_import/" legacybook_import_url = "/admin/core/legacybook/import/" legacybook_process_import_url = "/admin/core/legacybook/process_import/" child_import_url = "/admin/core/child/import/" child_process_import_url = "/admin/core/child/process_import/" def setUp(self): super().setUp() 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 _do_import_post( self, url, filename, input_format=0, encoding=None, resource=None, follow=False ): input_format = input_format filename = os.path.join( os.path.dirname(__file__), os.path.pardir, "exports", filename ) with open(filename, "rb") as f: data = { "input_format": str(input_format), "import_file": f, } if encoding: BookAdmin.from_encoding = encoding if resource: data.update({"resource": resource}) response = self.client.post(url, data, follow=follow) return response def _assert_string_in_response( self, url, filename, input_format, encoding=None, str_in_response=None, follow=False, status_code=200, ): response = self._do_import_post( url, filename, input_format, encoding=encoding, follow=follow ) self.assertEqual(response.status_code, status_code) self.assertIn("result", response.context) self.assertFalse(response.context["result"].has_errors()) if str_in_response is not None: self.assertContains(response, str_in_response) def _get_input_format_index(self, format): for i, f in enumerate(DEFAULT_FORMATS): if f().get_title() == format: xlsx_index = i break else: raise Exception( "Unable to find %s format. DEFAULT_FORMATS: %r" % (format, DEFAULT_FORMATS) ) return xlsx_index class ImportAdminIntegrationTest(AdminTestMixin, TestCase): 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.assertTemplateUsed(response, "admin/change_list.html") self.assertTemplateUsed(response, "core/admin/change_list.html") self.assertContains(response, _("Import")) self.assertContains(response, _("Export")) self.assertContains(response, "Custom change list item") @override_settings(TEMPLATE_STRING_IF_INVALID="INVALID_VARIABLE") def test_import(self): # GET the import form response = self.client.get(self.book_import_url) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "admin/import_export/import.html") self.assertContains(response, 'form action=""') response = self._do_import_post(self.book_import_url, "books.csv") 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(self.book_process_import_url, 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 ), ) @override_settings(DEBUG=True) def test_correct_scripts_declared_when_debug_is_true(self): # GET the import form response = self.client.get(self.book_import_url) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "admin/import_export/import.html") self.assertContains(response, 'form action=""') self.assertContains( response, '") response = self.client.get("/admin/core/book/export/") self.assertEqual(response.status_code, 200) xlsx_index = self._get_input_format_index("xlsx") data = {"file_format": str(xlsx_index)} response = self.client.post("/admin/core/book/export/", data) self.assertEqual(response.status_code, 200) content = response.content # #1698 temporary catch for deprecation warning in openpyxl # this catch block must be removed when openpyxl updated with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) wb = load_workbook(filename=BytesIO(content)) self.assertEqual("", wb.active["B2"].value) self.assertEqual("SUM(1+1)", wb.active["B3"].value) mock_logger.debug.assert_called_once_with( "IMPORT_EXPORT_ESCAPE_FORMULAE_ON_EXPORT is enabled" ) @override_settings(IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT=True) def test_export_escape_html_deprecation_warning(self): response = self.client.get("/admin/core/book/export/") self.assertEqual(response.status_code, 200) xlsx_index = self._get_input_format_index("xlsx") data = {"file_format": str(xlsx_index)} with self.assertWarnsRegex( DeprecationWarning, r"IMPORT_EXPORT_ESCAPE_HTML_ON_EXPORT is deprecated " "and will be removed in a future release.", ): self.client.post("/admin/core/book/export/", data) class FilteredExportAdminIntegrationTest(AdminTestMixin, TestCase): fixtures = ["category", "book", "author"] def test_export_filters_by_form_param(self): # issue 1578 author = Author.objects.get(name="Ian Fleming") data = {"file_format": "0", "author": str(author.id)} date_str = datetime.now().strftime("%Y-%m-%d") response = self.client.post("/admin/core/ebook/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="EBook-{}.csv"'.format(date_str), ) self.assertEqual( b"id,name,author,author_email,imported,published," b"published_time,price,added,categories\r\n" b"5,The Man with the Golden Gun,5,ian@example.com," b"0,1965-04-01,21:00:00,5.00,,2\r\n", response.content, ) class ConfirmImportEncodingTest(AdminTestMixin, TestCase): """Test handling 'confirm import' step using different file encodings and storage types. """ def _is_str_in_response(self, filename, input_format, encoding=None): super()._assert_string_in_response( self.book_import_url, filename, input_format, encoding=encoding, str_in_response="test@example.com", ) @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_binary(self): self._is_str_in_response("books.xls", "1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_binary(self): self._is_str_in_response("books.xls", "1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_binary(self): self._is_str_in_response("books.xls", "1") class CompleteImportEncodingTest(AdminTestMixin, TestCase): """Test handling 'complete import' step using different file encodings and storage types. """ def _is_str_in_response(self, filename, input_format, encoding=None): response = self._do_import_post( self.book_import_url, filename, input_format, encoding=encoding ) confirm_form = response.context["confirm_form"] data = confirm_form.initial response = self.client.post(self.book_process_import_url, data, follow=True) self.assertEqual(response.status_code, 200) self.assertContains( response, "Import finished, with 1 new and 0 updated books." ) @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.TempFolderStorage" ) def test_import_action_handles_TempFolderStorage_read_binary(self): self._is_str_in_response("books.xls", "1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.CacheStorage" ) def test_import_action_handles_CacheStorage_read_binary(self): self._is_str_in_response("books.xls", "1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read(self): self._is_str_in_response("books.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_mac(self): self._is_str_in_response("books-mac.csv", "0") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_iso_8859_1(self): self._is_str_in_response("books-ISO-8859-1.csv", "0", "ISO-8859-1") @override_settings( IMPORT_EXPORT_TMP_STORAGE_CLASS="import_export.tmp_storages.MediaStorage" ) def test_import_action_handles_MediaStorage_read_binary(self): self._is_str_in_response("books.xls", "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 ExportActionAdminIntegrationTest(AdminTestMixin, TestCase): def setUp(self): super().setUp() self.cat1 = Category.objects.create(name="Cat 1") self.cat2 = Category.objects.create(name="Cat 2") 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) def test_export_admin_action_one_formats(self): mock_model = mock.MagicMock() mock_site = mock.MagicMock() class TestCategoryAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) export_formats = [base_formats.CSV] m = TestCategoryAdmin() action_form = m.action_form items = list(action_form.base_fields.items()) file_format = items[len(items) - 1][1] choices = file_format.choices self.assertNotEqual(choices[0][0], "---") self.assertEqual(choices[0][1], "csv") def test_export_admin_action_formats(self): mock_model = mock.MagicMock() mock_site = mock.MagicMock() class TestCategoryAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) class TestFormatsCategoryAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) export_formats = [base_formats.CSV, base_formats.JSON] m = TestCategoryAdmin() action_form = m.action_form items = list(action_form.base_fields.items()) file_format = items[len(items) - 1][1] choices = file_format.choices self.assertEqual(choices[0][1], "---") self.assertEqual(len(choices), 9) m = TestFormatsCategoryAdmin() action_form = m.action_form items = list(action_form.base_fields.items()) file_format = items[len(items) - 1][1] choices = file_format.choices self.assertEqual(choices[0][1], "---") self.assertEqual(len(m.export_formats) + 1, len(choices)) self.assertIn("csv", [c[1] for c in choices]) self.assertIn("json", [c[1] for c in choices]) @override_settings(EXPORT_FORMATS=[base_formats.XLSX, base_formats.CSV]) def test_export_admin_action_uses_export_format_settings(self): """ Test that export action only avails the formats provided by the EXPORT_FORMATS setting """ mock_model = mock.MagicMock() mock_site = mock.MagicMock() class TestCategoryAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) m = TestCategoryAdmin() action_form = m.action_form items = list(action_form.base_fields.items()) file_format = items[len(items) - 1][1] choices = file_format.choices self.assertEqual(len(choices), 3) self.assertEqual(choices[0][1], "---") self.assertEqual(choices[1][1], "xlsx") self.assertEqual(choices[2][1], "csv") @override_settings(IMPORT_EXPORT_FORMATS=[base_formats.XLS, base_formats.CSV]) def test_export_admin_action_uses_import_export_format_settings(self): """ Test that export action only avails the formats provided by the IMPORT_EXPORT_FORMATS setting """ mock_model = mock.MagicMock() mock_site = mock.MagicMock() class TestCategoryAdmin(ExportActionModelAdmin): def __init__(self): super().__init__(mock_model, mock_site) m = TestCategoryAdmin() action_form = m.action_form items = list(action_form.base_fields.items()) file_format = items[len(items) - 1][1] choices = file_format.choices self.assertEqual(len(choices), 3) self.assertEqual(choices[0][1], "---") self.assertEqual(choices[1][1], "xls") self.assertEqual(choices[2][1], "csv") class TestExportEncoding(TestCase): mock_request = MagicMock(spec=HttpRequest) mock_request.POST = {"file_format": 0} class TestMixin(ExportMixin): model = Book 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) class TestImportMixinDeprecationWarnings(TestCase): class TestMixin(ImportMixin): """ TestMixin is a subclass which mimics a class which the user may have created """ def get_import_form(self): return super().get_import_form() def get_confirm_import_form(self): return super().get_confirm_import_form() def get_form_kwargs(self, form_class, **kwargs): return super().get_form_kwargs(form_class, **kwargs) def setUp(self): super().setUp() self.import_mixin = ImportMixin() def test_get_import_form_warning(self): target_msg = ( "ImportMixin.get_import_form() is deprecated and will be removed " "in a future release. " "Please use get_import_form_class() instead." ) with self.assertWarns(DeprecationWarning) as w: self.import_mixin.get_import_form() self.assertEqual(target_msg, str(w.warnings[0].message)) def test_get_confirm_import_form_warning(self): target_msg = ( "ImportMixin.get_confirm_import_form() is deprecated and will be removed " "in a future release. " "Please use get_confirm_form_class() instead." ) with self.assertWarns(DeprecationWarning) as w: self.import_mixin.get_confirm_import_form() self.assertEqual(target_msg, str(w.warnings[0].message)) def test_get_form_kwargs_warning(self): target_msg = ( "ImportMixin.get_form_kwargs() is deprecated and will be removed in a " "future release. " "Please use get_import_form_kwargs() or get_confirm_form_kwargs() instead." ) with self.assertWarns(DeprecationWarning) as w: self.import_mixin.get_form_kwargs(None) self.assertEqual(target_msg, str(w.warnings[0].message)) def test_get_import_form_class_warning(self): self.import_mixin = self.TestMixin() target_msg = ( "ImportMixin.get_import_form() is deprecated and will be removed in a " "future release. " "Please use the new 'import_form_class' attribute to specify a custom form " "class, " "or override the get_import_form_class() method if your requirements are " "more complex." ) with self.assertWarns(DeprecationWarning) as w: self.import_mixin.get_import_form_class(None) self.assertEqual(target_msg, str(w.warnings[0].message)) def test_get_confirm_form_class_warning(self): self.import_mixin = self.TestMixin() target_msg = ( "ImportMixin.get_confirm_import_form() is deprecated and will be removed " "in a future release. " "Please use the new 'confirm_form_class' attribute to specify a custom " "form class, " "or override the get_confirm_form_class() method if your requirements " "are more complex." ) with self.assertWarns(DeprecationWarning) as w: self.import_mixin.get_confirm_form_class(None) self.assertEqual(target_msg, str(w.warnings[0].message)) class TestExportMixinDeprecationWarnings(TestCase): class TestMixin(ExportMixin): """ TestMixin is a subclass which mimics a class which the user may have created """ def get_export_form(self): return super().get_export_form() def setUp(self): super().setUp() self.export_mixin = self.TestMixin() def test_get_export_form_warning(self): target_msg = ( "ExportMixin.get_export_form() is deprecated and will " "be removed in a future release. Please use the new " "'export_form_class' attribute to specify a custom form " "class, or override the get_export_form_class() method if " "your requirements are more complex." ) with self.assertWarns(DeprecationWarning) as w: self.export_mixin.get_export_form() self.assertEqual(target_msg, str(w.warnings[0].message)) @override_settings(IMPORT_EXPORT_SKIP_ADMIN_CONFIRM=True) class TestImportSkipConfirm(AdminTestMixin, TransactionTestCase): def _is_str_in_response( self, filename, input_format, encoding=None, str_in_response=None, follow=False, status_code=200, ): response = self._do_import_post( self.book_import_url, filename, input_format, encoding=encoding, follow=follow, ) self.assertEqual(response.status_code, status_code) if str_in_response is not None: self.assertContains(response, str_in_response) def _is_regex_in_response( self, filename, input_format, encoding=None, regex_in_response=None, follow=False, status_code=200, ): response = self._do_import_post( self.book_import_url, filename, input_format, encoding=encoding, follow=follow, ) self.assertEqual(response.status_code, status_code) if regex_in_response is not None: self.assertRegex(str(response.content), regex_in_response) def test_import_action_create(self): self._is_str_in_response( "books.csv", "0", follow=True, str_in_response="Import finished, with 1 new and 0 updated books.", ) self.assertEqual(1, Book.objects.count()) def test_import_action_invalid_date(self): # test that a row with an invalid date redirects to errors page response = self._do_import_post( self.book_import_url, "books-invalid-date.csv", "0" ) result = response.context["result"] # there should be a single invalid row self.assertEqual(1, len(result.invalid_rows)) self.assertEqual( "Enter a valid date.", result.invalid_rows[0].error.messages[0] ) # no rows should be imported because we rollback on validation errors self.assertEqual(0, Book.objects.count()) def test_import_action_empty_author_email(self): xlsx_index = self._get_input_format_index("xlsx") # sqlite / MySQL / Postgres have different error messages self._is_regex_in_response( "books-empty-author-email.xlsx", xlsx_index, follow=True, regex_in_response=r"(NOT NULL|null value in column|cannot be null)", ) @override_settings(IMPORT_EXPORT_USE_TRANSACTIONS=True) def test_import_transaction_enabled_validation_error(self): # with transactions enabled, a validation error should cause the entire # import to be rolled back self._do_import_post(self.book_import_url, "books-invalid-date.csv") self.assertEqual(0, Book.objects.count()) @override_settings(IMPORT_EXPORT_USE_TRANSACTIONS=False) def test_import_transaction_disabled_validation_error(self): # with transactions disabled, a validation error should not cause the entire # import to fail self._do_import_post(self.book_import_url, "books-invalid-date.csv") self.assertEqual(1, Book.objects.count()) @override_settings(IMPORT_EXPORT_USE_TRANSACTIONS=True) def test_import_transaction_enabled_core_error(self): # with transactions enabled, a core error should cause the entire import to fail xlsx_index = self._get_input_format_index("xlsx") self._do_import_post( self.book_import_url, "books-empty-author-email.xlsx", xlsx_index ) self.assertEqual(0, Book.objects.count()) @override_settings(IMPORT_EXPORT_USE_TRANSACTIONS=False) def test_import_transaction_disabled_core_error(self): # with transactions disabled, a core (db contraint) error should not cause the # entire import to fail xlsx_index = self._get_input_format_index("xlsx") self._do_import_post( self.book_import_url, "books-empty-author-email.xlsx", xlsx_index ) self.assertEqual(1, Book.objects.count()) def test_import_action_mac(self): self._is_str_in_response( "books-mac.csv", "0", follow=True, str_in_response="Import finished, with 1 new and 0 updated books.", ) def test_import_action_iso_8859_1(self): self._is_str_in_response( "books-ISO-8859-1.csv", "0", "ISO-8859-1", follow=True, str_in_response="Import finished, with 1 new and 0 updated books.", ) def test_import_action_decode_error(self): # attempting to read a file with the incorrect encoding should raise an error self._is_regex_in_response( "books-ISO-8859-1.csv", "0", follow=True, encoding="utf-8-sig", regex_in_response=( ".*UnicodeDecodeError.* encountered " "while trying to read file" ), ) def test_import_action_binary(self): self._is_str_in_response( "books.xls", "1", follow=True, str_in_response="Import finished, with 1 new and 0 updated books.", ) django-import-export-3.3.4/tests/core/tests/test_base_formats.py000066400000000000000000000175371453511766500251560ustar00rootroot00000000000000import 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 TablibFormatTest(TestCase): def setUp(self): self.format = base_formats.TablibFormat() def test_get_format_for_undefined_TABLIB_MODULE_raises_AttributeError(self): with self.assertRaises(AttributeError): self.format.get_format() 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()) class HTMLFormatTest(TestCase): def setUp(self): self.format = base_formats.HTML() self.dataset = tablib.Dataset(headers=["id", "username", "name"]) self.dataset.append((1, "good_user", "John Doe")) self.dataset.append( ( "2", "evil_user", '', ) ) def test_export_html_escape(self): res = self.format.export_data(self.dataset, escape_html=True) self.assertIn( ( "1\n" "good_user\n" "John Doe\n" "2\n" "evil_user\n" "<script>alert("I want to steal your credit card data" "")</script>\n" ), res, ) def test_export_data_no_escape(self): res = self.format.export_data(self.dataset) self.assertIn( ( "1\n" "good_user\n" "John Doe\n" "2\n" "evil_user\n" '\n" ), res, ) django-import-export-3.3.4/tests/core/tests/test_exceptions.py000066400000000000000000000006161453511766500246600ustar00rootroot00000000000000import warnings from unittest import TestCase class ExceptionTest(TestCase): # exceptions.py is deprecated but this test ensures # there is code coverage def test_field_error(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) from import_export import exceptions exceptions.FieldError() django-import-export-3.3.4/tests/core/tests/test_fields.py000066400000000000000000000116721453511766500237510ustar00rootroot00000000000000from datetime import date from unittest import mock from django.test import TestCase from import_export import fields from import_export.exceptions import FieldError 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), "") def testget_dehydrate_method_default(self): field = fields.Field(attribute="foo", column_name="bar") # `field_name` is the variable name defined in `Resource` resource_field_name = "field" method_name = field.get_dehydrate_method(resource_field_name) self.assertEqual(f"dehydrate_{resource_field_name}", method_name) def testget_dehydrate_method_with_custom_method_name(self): custom_dehydrate_method = "custom_method_name" field = fields.Field( attribute="foo", column_name="bar", dehydrate_method=custom_dehydrate_method ) resource_field_name = "field" method_name = field.get_dehydrate_method(resource_field_name) self.assertEqual(method_name, custom_dehydrate_method) def testget_dehydrate_method_without_params_raises_attribute_error(self): field = fields.Field(attribute="foo", column_name="bar") self.assertRaises(FieldError, field.get_dehydrate_method) def test_m2m_add_true(self): m2m_related_manager = mock.Mock(spec=["add", "set", "all"]) m2m_related_manager.all.return_value = [] self.obj.aliases = m2m_related_manager field = fields.Field(column_name="aliases", attribute="aliases", m2m_add=True) row = { "aliases": ["Foo", "Bar"], } field.save(self.obj, row, is_m2m=True) self.assertEqual(m2m_related_manager.add.call_count, 1) self.assertEqual(m2m_related_manager.set.call_count, 0) m2m_related_manager.add.assert_called_once_with("Foo", "Bar") row = { "aliases": ["apple"], } field.save(self.obj, row, is_m2m=True) m2m_related_manager.add.assert_called_with("apple") def test_m2m_add_False(self): m2m_related_manager = mock.Mock(spec=["add", "set", "all"]) self.obj.aliases = m2m_related_manager field = fields.Field(column_name="aliases", attribute="aliases") row = { "aliases": ["Foo", "Bar"], } field.save(self.obj, row, is_m2m=True) self.assertEqual(m2m_related_manager.add.call_count, 0) self.assertEqual(m2m_related_manager.set.call_count, 1) m2m_related_manager.set.assert_called_once_with(["Foo", "Bar"]) django-import-export-3.3.4/tests/core/tests/test_forms.py000066400000000000000000000031261453511766500236240ustar00rootroot00000000000000from core import admin from django.test import TestCase from import_export import forms, resources class MyResource(resources.ModelResource): class Meta: name = "My super resource" class FormTest(TestCase): def test_formbase_init_blank_resources(self): resource_list = [] form = forms.ImportExportFormBase(resources=resource_list) self.assertTrue("resource" not in form.fields) def test_formbase_init_one_resources(self): resource_list = [resources.ModelResource] form = forms.ImportExportFormBase(resources=resource_list) self.assertTrue("resource" not in form.fields) def test_formbase_init_two_resources(self): resource_list = [resources.ModelResource, MyResource] form = forms.ImportExportFormBase(resources=resource_list) self.assertEqual( form.fields["resource"].choices, [(0, "ModelResource"), (1, "My super resource")], ) def test_resources_arg_deprecation_warning(self): class TestForm(forms.ImportExportFormBase): def __init__(self, *args, resources=None, **kwargs): self.args_ = args super().__init__(*args, resources=resources, **kwargs) resource_list = [resources.ModelResource, admin.BookResource] with self.assertWarns(DeprecationWarning) as w: f = TestForm(resource_list) self.assertEqual( "'resources' must be supplied as a named parameter", str(w.warnings[0].message), ) self.assertEqual(f.args_, (resource_list,)) django-import-export-3.3.4/tests/core/tests/test_import_export_tags.py000066400000000000000000000005611453511766500264270ustar00rootroot00000000000000from unittest import TestCase from import_export.templatetags import import_export_tags class TagsTest(TestCase): def test_compare_values(self): target = ( 'a' 'b' ) self.assertEqual(target, import_export_tags.compare_values("a", "b")) django-import-export-3.3.4/tests/core/tests/test_instance_loaders.py000066400000000000000000000053101453511766500260100ustar00rootroot00000000000000import 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-3.3.4/tests/core/tests/test_invalidrow.py000066400000000000000000000035731453511766500246620ustar00rootroot00000000000000from 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-3.3.4/tests/core/tests/test_mixins.py000066400000000000000000000326561453511766500240170ustar00rootroot00000000000000from 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 admin, formats, forms, mixins, resources 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) class TestBaseImportMixin(mixins.BaseImportMixin): @property def import_formats(self): return [CanImportFormat, CannotImportFormat] m = TestBaseImportMixin() formats = m.get_import_formats() self.assertEqual(1, len(formats)) self.assertEqual("CanImportFormat", formats[0].__name__) class FooResource(resources.Resource): pass 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_classes(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_classes(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_classes() 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_classes() 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 BaseModelResourceClassTest(mixins.BaseImportMixin, mixins.BaseExportMixin): resource_class = resources.Resource export_call_count = 0 import_call_count = 0 def get_export_resource_class(self): self.export_call_count += 1 def get_import_resource_class(self): self.import_call_count += 1 def test_deprecated_resource_class_raises_warning(self): """Test that the mixin throws error if user didn't migrate to resource_classes""" admin = self.BaseModelResourceClassTest() with self.assertWarnsRegex( DeprecationWarning, r"^The 'get_export_resource_class\(\)' method has been deprecated. " r"Please implement the new 'get_export_resource_classes\(\)' method$", ): admin.get_export_resource_classes() with self.assertWarnsRegex( DeprecationWarning, r"^The 'get_import_resource_class\(\)' method has been deprecated. " r"Please implement the new 'get_import_resource_classes\(\)' method$", ): admin.get_import_resource_classes() with self.assertWarnsRegex( DeprecationWarning, r"^The 'resource_class' field has been deprecated. " r"Please implement the new 'resource_classes' field$", ): self.assertEqual(admin.get_resource_classes(), [resources.Resource]) self.assertEqual(1, admin.export_call_count) self.assertEqual(1, admin.import_call_count) class BaseModelGetExportResourceClassTest(mixins.BaseExportMixin): def get_resource_class(self): pass def test_deprecated_get_resource_class_raises_warning(self): """Test that the mixin throws error if user didn't migrate to resource_classes""" admin = self.BaseModelGetExportResourceClassTest() with self.assertWarnsRegex( DeprecationWarning, r"^The 'get_resource_class\(\)' method has been deprecated. " r"Please implement the new 'get_resource_classes\(\)' method$", ): admin.get_resource_classes() class BaseModelAdminFaultyResourceClassesTest(mixins.BaseExportMixin): resource_classes = resources.Resource def test_faulty_resource_class_raises_exception(self): """Test fallback mechanism to old get_export_resource_class() method""" admin = self.BaseModelAdminFaultyResourceClassesTest() with self.assertRaisesRegex( Exception, r"^The resource_classes field type must be subscriptable" ): admin.get_export_resource_classes() class BaseModelAdminBothResourceTest(mixins.BaseExportMixin): call_count = 0 resource_class = resources.Resource resource_classes = [resources.Resource] def test_both_resource_class_raises_exception(self): """Test fallback mechanism to old get_export_resource_class() method""" admin = self.BaseModelAdminBothResourceTest() with self.assertRaisesRegex( Exception, "Only one of 'resource_class' and 'resource_classes' can be set" ): admin.get_export_resource_classes() class BaseModelExportChooseTest(mixins.BaseExportMixin): resource_classes = [resources.Resource, FooResource] @mock.patch("import_export.admin.ExportForm") def test_choose_export_resource_class(self, form): """Test choose_export_resource_class() method""" admin = self.BaseModelExportChooseTest() self.assertEqual(admin.choose_export_resource_class(form), resources.Resource) form.cleaned_data = {"resource": 1} self.assertEqual(admin.choose_export_resource_class(form), FooResource) class BaseModelImportChooseTest(mixins.BaseImportMixin): resource_classes = [resources.Resource, FooResource] @mock.patch("import_export.admin.ImportForm") def test_choose_import_resource_class(self, form): """Test choose_import_resource_class() method""" admin = self.BaseModelImportChooseTest() self.assertEqual(admin.choose_import_resource_class(form), resources.Resource) form.cleaned_data = {"resource": 1} self.assertEqual(admin.choose_import_resource_class(form), FooResource) class BaseModelResourceClassOldTest(mixins.BaseImportMixin, mixins.BaseExportMixin): def get_resource_class(self): return FooResource def test_get_resource_class_old(self): """ Test that if only the old get_resource_class() method is defined, the get_export_resource_classes() and get_import_resource_classes() still return list of resources. """ admin = self.BaseModelResourceClassOldTest() with self.assertWarnsRegex( DeprecationWarning, r"^The 'get_resource_class\(\)' method has been deprecated. " r"Please implement the new 'get_resource_classes\(\)' method$", ): self.assertEqual(admin.get_export_resource_classes(), [FooResource]) with self.assertWarnsRegex( DeprecationWarning, r"^The 'get_resource_class\(\)' method has been deprecated. " r"Please implement the new 'get_resource_classes\(\)' method$", ): self.assertEqual(admin.get_import_resource_classes(), [FooResource]) 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) class TestBaseExportMixin(mixins.BaseExportMixin): @property def export_formats(self): return [CanExportFormat, CannotExportFormat] m = TestBaseExportMixin() formats = m.get_export_formats() self.assertEqual(1, len(formats)) self.assertEqual("CanExportFormat", formats[0].__name__) class ExportMixinTest(TestCase): class TestExportMixin(admin.ExportMixin): def __init__(self, export_form) -> None: super().__init__() self.export_form = export_form def get_export_form(self): return self.export_form class TestExportForm(forms.ExportForm): pass def test_get_export_form(self): m = admin.ExportMixin() self.assertEqual(forms.ExportForm, m.get_export_form_class()) def test_get_export_form_with_custom_form(self): m = self.TestExportMixin(self.TestExportForm) self.assertEqual(self.TestExportForm, m.get_export_form()) django-import-export-3.3.4/tests/core/tests/test_permissions.py000066400000000000000000000076021453511766500250540ustar00rootroot00000000000000import 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-3.3.4/tests/core/tests/test_resources.py000066400000000000000000003217051453511766500245160ustar00rootroot00000000000000import json import sys from collections import OrderedDict from copy import deepcopy from datetime import date from decimal import Decimal, InvalidOperation from unittest import mock, skipUnless from unittest.mock import patch import tablib from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ( FieldDoesNotExist, ImproperlyConfigured, ValidationError, ) from django.core.paginator import Paginator from django.db import IntegrityError from django.db.models import CharField, 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, UUIDBook, UUIDCategory, WithDefault, WithDynamicDefault, WithFloatField, ) class MyResource(resources.Resource): name = fields.Field() email = fields.Field() extra = fields.Field() def __init__(self, **kwargs): super().__init__(**kwargs) self.kwargs = kwargs 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_kwargs(self): target_kwargs = {"a": 1} my_resource = MyResource(**target_kwargs) self.assertEqual(my_resource.kwargs, target_kwargs) 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"] ) def test_default_after_import(self): self.assertIsNone( self.my_resource.after_import( tablib.Dataset(), results.Result(), False, False ) ) # 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([]) @patch("core.models.Book.full_clean") def test_validate_instance_called_with_import_validation_errors_as_None( self, full_clean_mock ): # validate_instance() import_validation_errors is an optional kwarg # If not provided, it defaults to an empty dict # this tests that scenario by ensuring that an empty dict is passed # to the model instance full_clean() method. book = Book() self.my_resource._meta.clean_model_instances = True self.my_resource.validate_instance(book) target = dict() full_clean_mock.assert_called_once_with( exclude=target.keys(), validate_unique=True ) def test_raise_errors_deprecation_import_row( self, ): target_msg = ( "raise_errors argument is deprecated and " "will be removed in a future release." ) dataset = tablib.Dataset(headers=["name", "email", "extra"]) dataset.append(["Some book", "test@example.com", "10.25"]) class Loader: def __init__(self, *args, **kwargs): pass class A(MyResource): class Meta: instance_loader_class = Loader force_init_instance = True def init_instance(self, row=None): return row or {} def import_row( self, row, instance_loader, using_transactions=True, dry_run=False, raise_errors=False, **kwargs, ): return super().import_row( row, instance_loader, using_transactions, dry_run, raise_errors, **kwargs, ) def save_instance( self, instance, is_create, using_transactions=True, dry_run=False ): pass resource = A() with self.assertWarns(DeprecationWarning) as w: resource.import_data(dataset, raise_errors=True) self.assertEqual(target_msg, str(w.warnings[0].message)) def test_rollback_on_validation_errors_deprecation_import_inner( self, ): target_msg = ( "rollback_on_validation_errors argument is deprecated " "and will be removed in a future release." ) dataset = tablib.Dataset(headers=["name", "email", "extra"]) dataset.append(["Some book", "test@example.com", "10.25"]) class Loader: def __init__(self, *args, **kwargs): pass class A(MyResource): class Meta: instance_loader_class = Loader force_init_instance = True def init_instance(self, row=None): return row or {} def import_data_inner( self, dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, rollback_on_validation_errors=False, **kwargs, ): return super().import_data_inner( dataset, dry_run, raise_errors, using_transactions, collect_failed_rows, rollback_on_validation_errors, **kwargs, ) def save_instance( self, instance, is_create, using_transactions=True, dry_run=False ): pass resource = A() with self.assertWarns(DeprecationWarning) as w: resource.import_data(dataset, raise_errors=True) self.assertEqual(target_msg, str(w.warnings[0].message)) 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 BookResourceWithStoreInstance(resources.ModelResource): class Meta: model = Book store_instance = True 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 ModelResourcePostgresModuleLoadTest(TestCase): pg_module_name = "django.contrib.postgres.fields" class ImportRaiser: def find_spec(self, fullname, path, target=None): if fullname == ModelResourcePostgresModuleLoadTest.pg_module_name: # we get here if the module is not loaded and not in sys.modules raise ImportError() def setUp(self): super().setUp() self.resource = BookResource() if self.pg_module_name in sys.modules: self.pg_modules = sys.modules[self.pg_module_name] del sys.modules[self.pg_module_name] def tearDown(self): super().tearDown() sys.modules[self.pg_module_name] = self.pg_modules def test_widget_from_django_field_cannot_import_postgres(self): # test that default widget is returned if postgres extensions # are not present sys.meta_path.insert(0, self.ImportRaiser()) f = fields.Field() res = self.resource.widget_from_django_field(f) self.assertEqual(widgets.Widget, res) 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_get_display_name(self): display_name = self.resource.get_display_name() self.assertEqual(display_name, "BookResource") class BookResource(resources.ModelResource): class Meta: name = "Foo Name" model = Book import_id_fields = ["name"] resource = BookResource() display_name = resource.get_display_name() self.assertEqual(display_name, "Foo Name") 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_export_handles_args(self): # issue 1565 with self.assertWarns(DeprecationWarning) as w: self.resource.export(Book.objects.none()) self.assertEqual( "'queryset' must be supplied as a named parameter", str(w.warnings[0].message), ) def test_export_handles_named_queryset_parameter(self): class _BookResource(BookResource): def before_export(self, queryset, *args, **kwargs): self.qs = queryset self.args_ = args self.kwargs_ = kwargs self.resource = _BookResource() # when queryset is supplied, it should be passed to before_export() self.resource.export(1, 2, 3, queryset=Book.objects.all(), **{"a": 1}) self.assertEqual(Book.objects.count(), len(self.resource.qs)) self.assertEqual((1, 2, 3), self.resource.args_) self.assertEqual(dict(a=1), self.resource.kwargs_) 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")]) def test_import_data_update(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 ) self.assertEqual(result.rows[0].row_values.get("name"), None) self.assertEqual(result.rows[0].row_values.get("author_email"), None) self.assertIsNone(result.rows[0].instance) self.assertIsNotNone(result.rows[0].original) instance = Book.objects.get(pk=self.book.pk) self.assertEqual(instance.author_email, "test@example.com") self.assertEqual(instance.price, Decimal("10.25")) def test_import_data_new(self): Book.objects.all().delete() self.assertEqual(0, Book.objects.count()) 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_NEW) self.assertEqual(result.rows[0].row_values.get("name"), None) self.assertEqual(result.rows[0].row_values.get("author_email"), None) self.assertIsNone(result.rows[0].instance) self.assertIsNone(result.rows[0].original) self.assertEqual(1, Book.objects.count()) instance = Book.objects.first() self.assertEqual(instance.author_email, "test@example.com") self.assertEqual(instance.price, Decimal("10.25")) def test_import_data_new_store_instance(self): self.resource = BookResourceWithStoreInstance() Book.objects.all().delete() self.assertEqual(0, Book.objects.count()) result = self.resource.import_data(self.dataset, raise_errors=True) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_NEW) self.assertIsNotNone(result.rows[0].instance) self.assertIsNone(result.rows[0].original) self.assertEqual(1, Book.objects.count()) book = Book.objects.first() self.assertEqual(book.pk, result.rows[0].instance.pk) def test_import_data_update_store_instance(self): self.resource = BookResourceWithStoreInstance() result = self.resource.import_data(self.dataset, raise_errors=True) self.assertEqual( result.rows[0].import_type, results.RowResult.IMPORT_TYPE_UPDATE ) self.assertIsNotNone(result.rows[0].instance) self.assertIsNotNone(result.rows[0].original) self.assertEqual(1, Book.objects.count()) book = Book.objects.first() self.assertEqual(book.pk, result.rows[0].instance.pk) @skipUnlessDBFeature("supports_transactions") @mock.patch("import_export.resources.connections") def test_import_data_no_transaction(self, mock_db_connections): class Features: supports_transactions = False class DummyConnection: features = Features() dummy_connection = DummyConnection() mock_db_connections.__getitem__.return_value = dummy_connection result = self.resource.import_data( self.dataset, dry_run=True, use_transactions=False, 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 ) self.assertEqual(result.rows[0].row_values.get("name"), None) self.assertEqual(result.rows[0].row_values.get("author_email"), None) @mock.patch("import_export.resources.connections") def test_ImproperlyConfigured_if_use_transactions_set_when_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() 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_import_data_raises_field_specific_validation_errors_with_skip_unchanged( self, ): resource = AuthorResource() resource._meta.skip_unchanged = True author = Author.objects.create(name="Some author") dataset = tablib.Dataset(headers=["id", "birthday"]) dataset.append([author.id, "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_import_data_empty_dataset_with_collect_failed_rows(self): resource = AuthorResource() result = resource.import_data(tablib.Dataset(), collect_failed_rows=True) self.assertEqual(["Error"], result.failed_dataset.headers) 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)) self.assertIsNone(result.rows[0].instance) self.assertIsNone(result.rows[0].original) def test_import_data_delete_store_instance(self): class B(BookResource): delete = fields.Field(widget=widgets.BooleanWidget()) def for_delete(self, row, instance): return self.fields["delete"].clean(row) class Meta: store_instance = True 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.assertEqual( result.rows[0].import_type, results.RowResult.IMPORT_TYPE_DELETE ) self.assertIsNotNone(result.rows[0].instance) 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, new, using_transactions=True, dry_run=False ): super().save_instance(instance, new, 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, is_create=False, 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_dehydrate_field_using_custom_dehydrate_field_method(self): class B(resources.ModelResource): full_title = fields.Field( column_name="Full title", dehydrate_method="foo_dehydrate_full_title" ) class Meta: model = Book fields = "full_title" def foo_dehydrate_full_title(self, obj): return f"{obj.name} by {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, f"{self.book.name} by {self.book.author.name}") def test_invalid_relation_field_name(self): class B(resources.ModelResource): full_title = fields.Field(column_name="Full title") class Meta: model = Book # author_name is not a valid field or relation, # so should be ignored fields = ("author_name", "full_title") resource = B() self.assertEqual(1, len(resource.fields)) self.assertEqual("full_title", list(resource.fields.keys())[0]) 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_m2m_add(self): cat1 = Category.objects.create(name="Cat 1") cat2 = Category.objects.create(name="Cat 2") cat3 = Category.objects.create(name="Cat 3") cat4 = Category.objects.create(name="Cat 4") 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", m2m_add=True, 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()) self.assertNotIn(cat3, book.categories.all()) self.assertNotIn(cat4, book.categories.all()) row1 = [ book.id, "FooBook", "Cat 1|Cat 2", ] # This should have no effect, since Cat 1 and Cat 2 already exist row2 = [book.id, "FooBook", "Cat 3|Cat 4"] dataset = tablib.Dataset(row1, row2, headers=headers) resource.import_data(dataset, raise_errors=True) book2 = Book.objects.get(name="FooBook") self.assertEqual(book.id, book2.id) self.assertEqual(book.categories.count(), 4) self.assertIn(cat1, book2.categories.all()) self.assertIn(cat2, book2.categories.all()) self.assertIn(cat3, book2.categories.all()) self.assertIn(cat4, book2.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, new, using_transactions, 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) self.assertEqual(result.rows[0].object_id, self.book.pk) # 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) def test_natural_foreign_key_detection(self): """ Test that when the _meta option for use_natural_foreign_keys is set on a resource that foreign key widgets are created with that flag, and when it's off they are not. """ # For future proof testing, we have one resource with natural # foreign keys on, and one off. If the default ever changes # this should still work. class _BookResource_Unfk(resources.ModelResource): class Meta: use_natural_foreign_keys = True model = Book class _BookResource(resources.ModelResource): class Meta: use_natural_foreign_keys = False model = Book resource_with_nfks = _BookResource_Unfk() author_field_widget = resource_with_nfks.fields["author"].widget self.assertTrue(author_field_widget.use_natural_foreign_keys) resource_without_nfks = _BookResource() author_field_widget = resource_without_nfks.fields["author"].widget self.assertFalse(author_field_widget.use_natural_foreign_keys) def test_natural_foreign_key_false_positives(self): """ Ensure that if the field's model does not have natural foreign key functions, it is not set to use natural foreign keys. """ from django.db import models class RelatedModel(models.Model): name = models.CharField() class Meta: app_label = "Test" class TestModel(models.Model): related_field = models.ForeignKey(RelatedModel, on_delete=models.PROTECT) class Meta: app_label = "Test" class TestModelResource(resources.ModelResource): class Meta: model = TestModel fields = ("id", "related_field") use_natural_foreign_keys = True resource = TestModelResource() related_field_widget = resource.fields["related_field"].widget self.assertFalse(related_field_widget.use_natural_foreign_keys) 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) class WidgetFromDjangoFieldTest(TestCase): def test_widget_from_django_field_for_CharField_returns_CharWidget(self): f = CharField() resource = BookResource() w = resource.widget_from_django_field(f) self.assertEqual(widgets.CharWidget, w) @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.") def test_widget_from_django_field_for_ArrayField_returns_SimpleArrayWidget(self): f = ArrayField(CharField) resource = BookResource() res = resource.widget_from_django_field(f) self.assertEqual(widgets.SimpleArrayWidget, res) if "postgresql" in settings.DATABASES["default"]["ENGINE"]: from django.contrib.postgres.fields import ArrayField from django.db import models class BookWithChapters(models.Model): name = models.CharField("Book name", max_length=100) chapters = ArrayField(models.CharField(max_length=100), default=list) data = models.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", "author"] 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 = ( '1' '2' ) 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) class ManyToManyWidgetDiffTest(TestCase): # issue #1270 - ensure ManyToMany fields are correctly checked for # changes when skip_unchanged=True fixtures = ["category", "book", "author"] def setUp(self): pass def test_many_to_many_widget_create(self): # the book is associated with 0 categories # when we import a book with category 1, the book # should be updated, not skipped book = Book.objects.first() book.categories.clear() dataset_headers = ["id", "name", "categories"] dataset_row = [book.id, book.name, "1"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_resource = BookResource() book_resource._meta.skip_unchanged = True self.assertEqual(0, book.categories.count()) result = book_resource.import_data(dataset, dry_run=False) book.refresh_from_db() self.assertEqual(1, book.categories.count()) self.assertEqual( result.rows[0].import_type, results.RowResult.IMPORT_TYPE_UPDATE ) self.assertEqual(Category.objects.first(), book.categories.first()) def test_many_to_many_widget_create_with_m2m_being_compared(self): # issue 1558 - when the object is a new instance and m2m is # evaluated for differences dataset_headers = ["categories"] dataset_row = ["1"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_resource = BookResource() book_resource._meta.skip_unchanged = True result = book_resource.import_data(dataset, dry_run=False) self.assertFalse(result.has_errors()) self.assertEqual(len(result.rows), 1) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_NEW) def test_many_to_many_widget_update(self): # the book is associated with 1 category ('Category 2') # when we import a book with category 1, the book # should be updated, not skipped, so that Category 2 is replaced by Category 1 book = Book.objects.first() dataset_headers = ["id", "name", "categories"] dataset_row = [book.id, book.name, "1"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_resource = BookResource() book_resource._meta.skip_unchanged = True self.assertEqual(1, book.categories.count()) result = book_resource.import_data(dataset, dry_run=False) self.assertEqual( result.rows[0].import_type, results.RowResult.IMPORT_TYPE_UPDATE ) self.assertEqual(1, book.categories.count()) self.assertEqual(Category.objects.first(), book.categories.first()) def test_many_to_many_widget_no_changes(self): # the book is associated with 1 category ('Category 2') # when we import a row with a book with category 1, the book # should be skipped, because there is no change book = Book.objects.first() dataset_headers = ["id", "name", "categories"] dataset_row = [book.id, book.name, book.categories.first().id] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_resource = BookResource() book_resource._meta.skip_unchanged = True self.assertEqual(1, book.categories.count()) result = book_resource.import_data(dataset, dry_run=False) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) self.assertEqual(1, book.categories.count()) def test_many_to_many_widget_handles_ordering(self): # the book is associated with 2 categories ('Category 1', 'Category 2') # when we import a row with a book with both categories (in any order), the book # should be skipped, because there is no change book = Book.objects.first() self.assertEqual(1, book.categories.count()) cat1 = Category.objects.get(name="Category 1") cat2 = Category.objects.get(name="Category 2") book.categories.add(cat1) book.save() self.assertEqual(2, book.categories.count()) dataset_headers = ["id", "name", "categories"] book_resource = BookResource() book_resource._meta.skip_unchanged = True # import with natural order dataset_row = [book.id, book.name, f"{cat1.id}, {cat2.id}"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) result = book_resource.import_data(dataset, dry_run=False) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) # import with reverse order dataset_row = [book.id, book.name, f"{cat2.id}, {cat1.id}"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) result = book_resource.import_data(dataset, dry_run=False) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) self.assertEqual(2, book.categories.count()) def test_many_to_many_widget_handles_uuid(self): # Test for #1435 - skip_row() handles M2M field when UUID pk used class _UUIDBookResource(resources.ModelResource): class Meta: model = UUIDBook uuid_resource = _UUIDBookResource() uuid_resource._meta.skip_unchanged = True cat1 = UUIDCategory.objects.create(name="Category 1") cat2 = UUIDCategory.objects.create(name="Category 2") uuid_book = UUIDBook.objects.create(name="uuid book") uuid_book.categories.add(cat1, cat2) uuid_book.save() dataset_headers = ["id", "name", "categories"] dataset_row = [uuid_book.id, uuid_book.name, f"{cat1.catid}, {cat2.catid}"] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) result = uuid_resource.import_data(dataset, dry_run=False) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) def test_skip_row_no_m2m_data_supplied(self): # issue #1437 # test skip_row() when the model defines a m2m field # but it is not present in the dataset book = Book.objects.first() dataset_headers = ["id", "name"] dataset_row = [book.id, book.name] dataset = tablib.Dataset(headers=dataset_headers) dataset.append(dataset_row) book_resource = BookResource() book_resource._meta.skip_unchanged = True self.assertEqual(1, book.categories.count()) result = book_resource.import_data(dataset, dry_run=False) self.assertEqual(result.rows[0].import_type, results.RowResult.IMPORT_TYPE_SKIP) self.assertEqual(1, book.categories.count()) @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, model=Book): [model.objects.create(name="book_name") for _ in range(10)] self.assertEqual(10, model.objects.count()) rows = model.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.UUIDBook.objects.bulk_create") def test_bulk_create_uuid_model(self, mock_bulk_create): """Test create of a Model which defines uuid not pk (issue #1274)""" class _UUIDBookResource(resources.ModelResource): class Meta: model = UUIDBook use_bulk = True batch_size = 5 fields = ( "id", "name", ) resource = _UUIDBookResource() 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.debug") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e, exc_info=mock.ANY) 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) @mock.patch("core.models.Book.objects.bulk_create") def test_bulk_create_exception_gathered_on_dry_run(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() result = resource.import_data(self.dataset, dry_run=True, raise_errors=False) self.assertTrue(result.has_errors()) 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] ) ) @mock.patch("import_export.resources.atomic_if_using_transaction") def test_no_sub_transaction_on_row_for_bulk(self, mock_atomic_if_using_transaction): class _BookResource(resources.ModelResource): class Meta: model = Book use_bulk = True resource = _BookResource() resource.import_data(self.dataset) self.assertIn( False, [x[0][0] for x in mock_atomic_if_using_transaction.call_args_list] ) 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.debug") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e, exc_info=mock.ANY) 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 BulkUUIDBookUpdateTest(BulkTest): def setUp(self): super().setUp() self.init_update_test_data(model=UUIDBook) @mock.patch("core.models.UUIDBook.objects.bulk_update") def test_bulk_update_uuid_model(self, mock_bulk_update): """Test update of a Model which defines uuid not pk (issue #1274)""" class _UUIDBookResource(resources.ModelResource): class Meta: model = UUIDBook use_bulk = True batch_size = 5 fields = ( "id", "name", ) resource = _UUIDBookResource() 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"]) 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.debug") as mock_exception: resource.import_data(self.dataset) mock_exception.assert_called_with(e, exc_info=mock.ANY) 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) class BulkUUIDBookDeleteTest(BulkTest): class DeleteBookResource(resources.ModelResource): def for_delete(self, row, instance): return True class Meta: model = UUIDBook use_bulk = True batch_size = 5 def setUp(self): super().setUp() self.resource = self.DeleteBookResource() self.init_update_test_data(model=UUIDBook) def test_bulk_delete_batch_size_of_5(self): self.assertEqual(10, UUIDBook.objects.count()) self.resource.import_data(self.dataset) self.assertEqual(0, UUIDBook.objects.count()) class RawValueTest(TestCase): def setUp(self): class _BookResource(resources.ModelResource): class Meta: model = Book store_row_values = True 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_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 ) self.assertEqual(result.rows[0].row_values.get("name"), "Some book") self.assertEqual( result.rows[0].row_values.get("author_email"), "test@example.com" ) self.assertEqual(result.rows[0].row_values.get("price"), "10.25") class ResourcesHelperFunctionsTest(TestCase): """ Test the helper functions in resources. """ def test_has_natural_foreign_key(self): """ Ensure that resources.has_natural_foreign_key detects correctly whether a model has a natural foreign key """ cases = {Book: True, Author: True, Category: False} for model, expected_result in cases.items(): self.assertEqual(resources.has_natural_foreign_key(model), expected_result) class AfterImportComparisonTest(TestCase): class BookResource(resources.ModelResource): is_published = False def after_import_row(self, row, row_result, **kwargs): if ( getattr(row_result.original, "published") is None and getattr(row_result.instance, "published") is not None ): self.is_published = True class Meta: model = Book store_instance = True def setUp(self): super().setUp() self.resource = AfterImportComparisonTest.BookResource() self.book = Book.objects.create(name="Some book") self.dataset = tablib.Dataset(headers=["id", "name", "published"]) row = [self.book.pk, "Some book", "2023-05-09"] self.dataset.append(row) def test_after_import_row_check_for_change(self): # issue 1583 - assert that `original` object is available to after_import_row() self.resource.import_data(self.dataset, raise_errors=True) self.assertTrue(self.resource.is_published) django-import-export-3.3.4/tests/core/tests/test_results.py000066400000000000000000000047561453511766500242110ustar00rootroot00000000000000from 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 = ["some_header", "Error"] self.result.add_dataset_headers(["some_header"]) self.assertEqual(target, self.result.failed_dataset.headers) def test_add_dataset_headers_empty_list(self): target = ["Error"] self.result.add_dataset_headers([]) self.assertEqual(target, self.result.failed_dataset.headers) def test_add_dataset_headers_None(self): target = ["Error"] self.result.add_dataset_headers(None) 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-3.3.4/tests/core/tests/test_tmp_storages.py000066400000000000000000000070031453511766500252030ustar00rootroot00000000000000import os from unittest.mock import mock_open, patch 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 TestTempFolderStorage(TempFolderStorage): def get_full_path(self): return "/tmp/f" class TestMediaStorage(MediaStorage): def get_full_path(self): return "f" 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_temp_folder_storage_read_with_encoding(self): tmp_storage = TestTempFolderStorage(encoding="utf-8") tmp_storage.name = "f" with patch("builtins.open", mock_open(read_data="data")) as mock_file: tmp_storage.read() mock_file.assert_called_with("/tmp/f", "r", encoding="utf-8") 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.assertIsNotNone(cache.get(tmp_storage.CACHE_PREFIX + tmp_storage.name)) tmp_storage.remove() self.assertIsNone(cache.get(tmp_storage.CACHE_PREFIX + tmp_storage.name)) def test_cache_storage_read_with_encoding(self): tmp_storage = CacheStorage() tmp_storage.name = "f" cache.set("django-import-export-f", 101) res = tmp_storage.read() self.assertEqual(101, res) def test_cache_storage_read_with_encoding_unicode_chars(self): tmp_storage = CacheStorage() tmp_storage.name = "f" tmp_storage.save("àèìòùçñ") res = tmp_storage.read() self.assertEqual("àèìòùçñ", res) 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_with_encoding(self): tmp_storage = TestMediaStorage() tmp_storage.name = "f" with patch("import_export.tmp_storages.default_storage.open") as mock_open: tmp_storage.read() mock_open.assert_called_with("f", mode="rb") django-import-export-3.3.4/tests/core/tests/test_widgets.py000066400000000000000000000554131453511766500241520ustar00rootroot00000000000000import json from datetime import date, datetime, time, timedelta from decimal import Decimal from unittest import mock, skipUnless import django import pytz from core.models import Author, Book, Category from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone from import_export import widgets class WidgetTest(TestCase): def setUp(self): self.widget = widgets.Widget() def test_clean(self): self.assertEqual("a", self.widget.clean("a")) def test_render(self): self.assertEqual("1", self.widget.render(1)) class CharWidgetTest(TestCase): def setUp(self): self.widget = widgets.CharWidget() def test_clean(self): self.assertEqual("a", self.widget.clean("a")) def test_render(self): self.assertEqual("1", self.widget.render(1)) def test_clean_coerce_to_string(self): self.widget = widgets.CharWidget(coerce_to_string=True) self.assertEqual("1", self.widget.clean(1)) def test_clean_no_coerce_to_string(self): self.assertEqual(1, self.widget.clean(1)) def test_clean_coerce_to_string_None(self): self.widget = widgets.CharWidget(coerce_to_string=True) self.assertIsNone(self.widget.clean(None)) def test_clean_coerce_to_string_with_allow_blank(self): self.widget = widgets.CharWidget(coerce_to_string=True, allow_blank=True) self.assertEqual("", self.widget.clean(None)) def test_clean_coerce_to_string_is_False_with_allow_blank(self): self.widget = widgets.CharWidget(coerce_to_string=False, allow_blank=True) self.assertIsNone(self.widget.clean(None)) 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 FormatDatetimeTest(TestCase): date = date(10, 8, 2) target_dt = "02.08.0010" format = "%d.%m.%Y" @skipUnless( django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test" ) def test_format_datetime_lt_django4(self): self.assertEqual( self.target_dt, widgets.format_datetime(self.date, self.format) ) @skipUnless( django.VERSION[0] >= 4, f"running django {django.VERSION} version specific test" ) def test_format_datetime_gte_django4(self): self.assertEqual( self.target_dt, widgets.format_datetime(self.date, self.format) ) 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(USE_TZ=True, TIME_ZONE="Europe/Ljubljana") def test_clean_returns_tz_aware_datetime_when_naive_datetime_passed(self): # issue 1165 if django.VERSION >= (5, 0): from zoneinfo import ZoneInfo tz = ZoneInfo("Europe/Ljubljana") else: tz = pytz.timezone("Europe/Ljubljana") target_dt = timezone.make_aware(self.datetime, tz) self.assertEqual(target_dt, self.widget.clean(self.datetime)) @override_settings(USE_TZ=True, TIME_ZONE="Europe/Ljubljana") def test_clean_handles_tz_aware_datetime(self): self.datetime = datetime(2012, 8, 13, 18, 0, 0, tzinfo=pytz.timezone("UTC")) self.assertEqual(self.datetime, self.widget.clean(self.datetime)) @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 NumberWidgetTest(TestCase): def setUp(self): self.value = 11.111 self.widget = widgets.NumberWidget() self.widget_coerce_to_string = widgets.NumberWidget(coerce_to_string=True) def test_is_empty_value_is_none(self): self.assertTrue(self.widget.is_empty(None)) def test_is_empty_value_is_empty_string(self): self.assertTrue(self.widget.is_empty("")) def test_is_empty_value_is_whitespace(self): self.assertTrue(self.widget.is_empty(" ")) def test_is_empty_value_is_zero(self): self.assertFalse(self.widget.is_empty(0)) def test_render(self): self.assertEqual(self.value, self.widget.render(self.value)) def test_render_None_coerce_to_string_False(self): self.assertIsNone(self.widget.render(None)) @skipUnless( django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test" ) @override_settings(LANGUAGE_CODE="fr-fr", USE_L10N=True) def test_locale_render_coerce_to_string_lt4(self): self.assertEqual("11,111", self.widget_coerce_to_string.render(self.value)) @skipUnless( django.VERSION[0] >= 4, f"skipping django {django.VERSION} version specific test", ) @override_settings(LANGUAGE_CODE="fr-fr") def test_locale_render_coerce_to_string_gte4(self): self.assertEqual("11,111", self.widget_coerce_to_string.render(self.value)) def test_coerce_to_string_value_is_None(self): self.assertEqual("", self.widget_coerce_to_string.render(None)) class FloatWidgetTest(TestCase): def setUp(self): self.value = 11.111 self.widget = widgets.FloatWidget() self.widget_coerce_to_string = widgets.FloatWidget(coerce_to_string=True) 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) @skipUnless( django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test" ) @override_settings(LANGUAGE_CODE="fr-fr", USE_L10N=True) def test_locale_render_coerce_to_string_lt4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "11,111") @skipUnless( django.VERSION[0] >= 4, f"skipping django {django.VERSION} version specific test", ) @override_settings(LANGUAGE_CODE="fr-fr") def test_locale_render_coerce_to_string_gte4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "11,111") class DecimalWidgetTest(TestCase): def setUp(self): self.value = Decimal("11.111") self.widget = widgets.DecimalWidget() self.widget_coerce_to_string = widgets.DecimalWidget(coerce_to_string=True) 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) @skipUnless( django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test" ) @override_settings(LANGUAGE_CODE="fr-fr", USE_L10N=True) def test_locale_render_coerce_to_string_lt4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "11,111") @skipUnless( django.VERSION[0] >= 4, f"skipping django {django.VERSION} version specific test", ) @override_settings(LANGUAGE_CODE="fr-fr") def test_locale_render_coerce_to_string_gte4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "11,111") class IntegerWidgetTest(TestCase): def setUp(self): self.value = 0 self.widget = widgets.IntegerWidget() self.bigintvalue = 163371428940853127 self.widget_coerce_to_string = widgets.IntegerWidget(coerce_to_string=True) 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) @skipUnless( django.VERSION[0] < 4, f"skipping django {django.VERSION} version specific test" ) @override_settings(LANGUAGE_CODE="fr-fr", USE_L10N=True) def test_locale_render_lt_django4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "0") @skipUnless( django.VERSION[0] >= 4, f"skipping django {django.VERSION} version specific test", ) @override_settings(LANGUAGE_CODE="fr-fr") def test_locale_render_gte_django4(self): self.assertEqual(self.widget_coerce_to_string.render(self.value), "0") class ForeignKeyWidgetTest(TestCase): def setUp(self): self.widget = widgets.ForeignKeyWidget(Author) self.natural_key_author_widget = widgets.ForeignKeyWidget( Author, use_natural_foreign_keys=True ) self.natural_key_book_widget = widgets.ForeignKeyWidget( Book, use_natural_foreign_keys=True ) self.author = Author.objects.create(name="Foo") self.book = Book.objects.create(name="Bar", author=self.author) 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, *args, **kwargs): 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_dict = {"name": "Foo", "birthday": author2.birthday} self.assertEqual(birthday_widget.clean("Foo", row=row_dict), author2) def test_invalid_get_queryset(self): class BirthdayWidget(widgets.ForeignKeyWidget): def get_queryset(self, value, row): return self.model.objects.filter(birthday=row["birthday"]) birthday_widget = BirthdayWidget(Author, "name") row_dict = {"name": "Foo", "age": 38} with self.assertRaises(TypeError): birthday_widget.clean("Foo", row=row_dict, row_number=1) 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)) def test_author_natural_key_clean(self): """ Ensure that we can import an author by its natural key. Note that this will always need to be an iterable. Generally this will be rendered as a list. """ self.assertEqual( self.natural_key_author_widget.clean(json.dumps(self.author.natural_key())), self.author, ) def test_author_natural_key_render(self): """ Ensure we can render an author by its natural key. Natural keys will always be tuples. """ self.assertEqual( self.natural_key_author_widget.render(self.author), json.dumps(self.author.natural_key()), ) def test_book_natural_key_clean(self): """ Use the book case to validate a composite natural key of book name and author can be cleaned. """ self.assertEqual( self.natural_key_book_widget.clean(json.dumps(self.book.natural_key())), self.book, ) def test_book_natural_key_render(self): """ Use the book case to validate a composite natural key of book name and author can be rendered """ self.assertEqual( self.natural_key_book_widget.render(self.book), json.dumps(self.book.natural_key()), ) 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-3.3.4/tests/core/views.py000066400000000000000000000003051453511766500214260ustar00rootroot00000000000000from 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-3.3.4/tests/docker-compose.yml000066400000000000000000000022561453511766500224330ustar00rootroot00000000000000version: '3.3' services: postgresdb: container_name: importexport_pgdb environment: 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:12 restart: "no" ports: - "5432:5432" volumes: - ./docker/db/init-postgres-db.sh/:/docker-entrypoint-initdb.d/init-postgres-db.sh - postgres-db-data:/var/lib/postgresql/data mysqldb: container_name: importexport_mysqldb image: mysql:8.0 platform: linux/x86_64 restart: "no" environment: MYSQL_DATABASE: import_export MYSQL_USER: ${IMPORT_EXPORT_MYSQL_USER} MYSQL_PASSWORD: ${IMPORT_EXPORT_MYSQL_PASSWORD} MYSQL_ROOT_PASSWORD: ${IMPORT_EXPORT_MYSQL_PASSWORD} ports: - "3306:3306" expose: - '3306' volumes: - ./docker/db/init-mysql-db.sh:/docker-entrypoint-initdb.d/init-mysql-db.sh - mysql-db-data:/var/lib/mysql volumes: postgres-db-data: driver: local mysql-db-data: driver: local django-import-export-3.3.4/tests/docker/000077500000000000000000000000001453511766500202405ustar00rootroot00000000000000django-import-export-3.3.4/tests/docker/db/000077500000000000000000000000001453511766500206255ustar00rootroot00000000000000django-import-export-3.3.4/tests/docker/db/init-mysql-db.sh000077500000000000000000000002221453511766500236510ustar00rootroot00000000000000#!/usr/bin/env bash set -e mysql -u root -p"$MYSQL_ROOT_PASSWORD" <<-EOSQL GRANT ALL PRIVILEGES ON test_import_export.* to '$MYSQL_USER'; EOSQLdjango-import-export-3.3.4/tests/docker/db/init-postgres-db.sh000077500000000000000000000006211453511766500243550ustar00rootroot00000000000000#!/usr/bin/env bash set -e 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-3.3.4/tests/manage.py000077500000000000000000000005001453511766500205710ustar00rootroot00000000000000#!/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-3.3.4/tests/scripts/000077500000000000000000000000001453511766500204605ustar00rootroot00000000000000django-import-export-3.3.4/tests/scripts/__init__.py000066400000000000000000000000001453511766500225570ustar00rootroot00000000000000django-import-export-3.3.4/tests/scripts/bulk_import.py000066400000000000000000000123351453511766500233650ustar00rootroot00000000000000""" Helper module for testing bulk imports. See testing.rst """ 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() 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(): 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 force_init_instance = True print("\ndo_create()") # clearing down existing objects books = Book.objects.all() books._raw_delete(books.db) 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 books._raw_delete(books.db) def do_update(): print("\ndo_update()") # clearing down existing objects books = Book.objects.all() books._raw_delete(books.db) 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() books = Book.objects.all() books._raw_delete(books.db) 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 books = Book.objects.all() books._raw_delete(books.db) 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-3.3.4/tests/settings.py000066400000000000000000000056731453511766500212160ustar00rootroot00000000000000import 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", ), }, }, ] 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"), "PASSWORD": os.environ.get("IMPORT_EXPORT_MYSQL_PASSWORD"), "HOST": "127.0.0.1", "PORT": 3306, "TEST": { "CHARSET": "utf8", "COLLATION": "utf8_general_ci", "NAME": "test_import_export", }, } } 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 if django.VERSION >= (4, 1): FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer" if django.VERSION >= (5, 0): FORM_RENDERER = "django.forms.renderers.DjangoTemplates" django-import-export-3.3.4/tests/urls.py000066400000000000000000000007551453511766500203370ustar00rootroot00000000000000from 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-3.3.4/tox.ini000066400000000000000000000016771453511766500171550ustar00rootroot00000000000000[tox] min_version = 4.0 envlist = {py38,py39,py310}-{django32} {py310,py311,py312}-{django42,django50,djangomain} # tablib dev temporarily removed - see issue #1602 # py311-djangomain-tablibdev [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 [testenv] setenv = PYTHONPATH = {toxinidir}/tests commands = python ./runtests.py deps = tablibdev: -egit+https://github.com/jazzband/tablib.git@master\#egg=tablib django32: Django>=3.2,<4.0 django42: Django>=4.2,<4.3 django50: Django>=5.0,<6.0 djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/test.txt # if postgres / mysql environment variables exist, we can go ahead and run db specific tests passenv = COVERAGE IMPORT_EXPORT_POSTGRESQL_USER IMPORT_EXPORT_POSTGRESQL_PASSWORD IMPORT_EXPORT_MYSQL_USER IMPORT_EXPORT_MYSQL_PASSWORD IMPORT_EXPORT_TEST_TYPE