diff --git a/README.rst b/README.rst index e8c98dbcd..48d782806 100644 --- a/README.rst +++ b/README.rst @@ -4,10 +4,19 @@ django-import-export .. image:: https://travis-ci.org/django-import-export/django-import-export.svg?branch=master :target: https://travis-ci.org/django-import-export/django-import-export + :alt: Build status on Travis-CI + .. image:: https://img.shields.io/pypi/v/django-import-export.svg :target: https://pypi.python.org/pypi/django-import-export + :alt: Current version on PyPi + .. image:: https://img.shields.io/pypi/dm/django-import-export.svg - :target: https://pypi.python.org/pypi/django-import-export + :target: https://pypi.python.org/pypi/django-import-export + :alt: Downloads per month on PyPi + +.. image:: http://readthedocs.org/projects/django-import-export/badge/?version=latest + :target: http://django-import-export.rtfd.org + :alt: Docmentation django-import-export is a Django application and library for importing and exporting data with included admin integration. @@ -56,6 +65,10 @@ If you'd like to contribute, simply fork `the repository`_, commit your changes to the **develop** branch (or branch off of it), and send a pull request. Make sure you add yourself to AUTHORS_. +As most projects, we try to follow PEP8_ as closely as possible. Please bear +in mind that most pull requests will be rejected without proper unit testing. + +.. _`PEP8`: https://www.python.org/dev/peps/pep-0008/ .. _`tablib`: https://github.com/kennethreitz/tablib .. _`the repository`: https://github.com/django-import-export/django-import-export/ .. _AUTHORS: https://github.com/django-import-export/django-import-export/blob/master/AUTHORS diff --git a/docs/api_admin.rst b/docs/api_admin.rst index d0b6514dc..67b8ca0fd 100644 --- a/docs/api_admin.rst +++ b/docs/api_admin.rst @@ -2,5 +2,7 @@ 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: diff --git a/docs/api_tmp_storages.rst b/docs/api_tmp_storages.rst index 2dbabed6d..fd60c8e5f 100644 --- a/docs/api_tmp_storages.rst +++ b/docs/api_tmp_storages.rst @@ -1,6 +1,6 @@ -============ -Tmp storages -============ +================== +Temporary storages +================== .. currentmodule:: import_export.tmp_storages diff --git a/docs/api_widgets.rst b/docs/api_widgets.rst index d020ec331..cb567bbb5 100644 --- a/docs/api_widgets.rst +++ b/docs/api_widgets.rst @@ -23,6 +23,9 @@ Widgets .. autoclass:: import_export.widgets.TimeWidget :members: +.. autoclass:: import_export.widgets.DateTimeWidget + :members: + .. autoclass:: import_export.widgets.ForeignKeyWidget :members: diff --git a/docs/changelog.rst b/docs/changelog.rst index b44858ba5..b9873241e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,5 @@ -Changelog for django-import-export -================================== +Changelog +========= 0.4.3 (unreleased) ------------------ @@ -12,6 +12,7 @@ Changelog for django-import-export - Add support for django.db.models.TimeField (#381) + 0.4.2 (2015-12-18) ------------------ @@ -89,7 +90,7 @@ Changelog for django-import-export - added use of get_diff_headers method into import.html template (#158) -- Try to use OrderedDict instead of SortedDict, which is deprecated in +- Try to use OrderedDict instead of SortedDict, which is deprecated in Django 1.7 (#157) - fixed #105 unicode import @@ -112,9 +113,9 @@ Changelog for django-import-export - Fixed XLS import on python 3. Optimized loop -- Fixed properly skipping row marked as skipped when importing data from +- 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 diff --git a/docs/conf.py b/docs/conf.py index 1d9283d0f..07e6bebe4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,7 +35,7 @@ # General information about the project. project = u'django-import-export' -copyright = u'2012, Bojan Mihelac' +copyright = u'2012–2016, 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 @@ -90,6 +90,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/configuration.rst b/docs/configuration.rst deleted file mode 100644 index e5bccb2e6..000000000 --- a/docs/configuration.rst +++ /dev/null @@ -1,16 +0,0 @@ -Configuration -============= - -You only need to perform this configuration step if you use django-import-export in the admin. - -Add ``import_export`` to your ``INSTALLED_APPS``: - - INSTALLED_APPS = [ - # ... - - 'import_export', - ] - -Deploy static files: - - $ python manage.py collectstatic diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index 2ebd1c91f..000000000 --- a/docs/contributing.rst +++ /dev/null @@ -1,10 +0,0 @@ -============= -Contributing -============= - -Code guidelines ---------------- - -* As most projects, we try to follow PEP8 as closely as possible - -* Most pull requests will be rejected without proper unit testing diff --git a/docs/example_app.rst b/docs/example_app.rst deleted file mode 100644 index de39b8328..000000000 --- a/docs/example_app.rst +++ /dev/null @@ -1,9 +0,0 @@ -=========== -Example app -=========== - -:: - - cd tests && ./manage.py runserver - -Username and password for admin are 'admin', 'password'. diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 92d829908..42961081d 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -3,7 +3,9 @@ Getting started =============== For example purposes, we'll use a simplified book app. Here is our -``core.models.py``:: +``models.py``:: + + # app/models.py class Author(models.Model): name = models.CharField(max_length=100) @@ -25,8 +27,7 @@ For example purposes, we'll use a simplified book app. Here is our 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) + price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) categories = models.ManyToManyField(Category, blank=True) def __unicode__(self): @@ -36,27 +37,27 @@ For example purposes, we'll use a simplified book app. Here is our .. _base-modelresource: Creating import-export resource -------------------------------- +=============================== -To integrate `django-import-export` with ``Book`` model, we will create -a resource class in ``admin.py`` that will describe how this resource can be imported or -exported. +To integrate `django-import-export` with our ``Book`` model, we will create a +:class:`~import_export.resources.ModelResource` class in ``admin.py`` that will +describe how this resource can be imported or exported:: -:: + # app/admin.py from import_export import resources from core.models import Book - class BookResource(resources.ModelResource): class Meta: model = Book Exporting data --------------- +============== -Now that we have defined a resource class, we can export books:: +Now that we have defined a :class:`~import_export.resources.ModelResource` class, +we can export books:: >>> dataset = BookResource().export() >>> print dataset.csv @@ -64,11 +65,11 @@ Now that we have defined a resource class, we can export books:: 2,Some book,1,,0,2012-12-05,8.85,1 Customize resource options --------------------------- +========================== -By default ``ModelResource`` introspects model fields and creates -``import_export.fields.Field`` attributes with an appropriate widget -for each field. +By default :class:`~import_export.resources.ModelResource` introspects model +fields and creates :class:`~import_export.fields.Field`-attributes with an +appropriate :class:`~import_export.widgets.Widget` for each field. To affect which model fields will be included in an import-export resource, use the ``fields`` option to whitelist fields:: @@ -105,7 +106,7 @@ The default field for object identification is ``id``, you can optionally set wh import_id_fields = ('isbn',) fields = ('isbn', 'name', 'author', 'price',) -When defining ``ModelResource`` fields it is possible to follow +When defining :class:`~import_export.resources.ModelResource` fields it is possible to follow model relationships:: class BookResource(resources.ModelResource): @@ -135,10 +136,10 @@ whether skipped records will show in the import preview page:: .. seealso:: :doc:`/api_resources` - + Declaring fields ----------------- +================ It is possible to override a resource field to change some of its options:: @@ -147,14 +148,14 @@ options:: class BookResource(resources.ModelResource): published = fields.Field(column_name='published_date') - + class Meta: model = Book Other fields that don't exist in the target model may be added:: from import_export import fields - + class BookResource(resources.ModelResource): myfield = fields.Field(column_name='myfield') @@ -168,7 +169,7 @@ Other fields that don't exist in the target model may be added:: Advanced data manipulation --------------------------- +========================== Not all data can be easily extracted from an object/model attribute. In order to turn complicated data model into a (generally simpler) processed @@ -178,7 +179,7 @@ data structure, ``dehydrate_`` method should be defined:: class BookResource(resources.ModelResource): full_title = fields.Field() - + class Meta: model = Book @@ -187,11 +188,11 @@ data structure, ``dehydrate_`` method should be defined:: Customize widgets ------------------ +================= -``ModelResource`` creates a field with a default widget for a given field -type. If the widget should be initialized with different arguments, set the -``widgets`` dict. +A :class:`~import_export.resources.ModelResource` creates a field with a +default widget for a given field type. If the widget should be initialized +with different arguments, set the ``widgets`` dict. In this example widget, the ``published`` field is overriden to use a different date format. This format will be used both for importing @@ -200,7 +201,7 @@ and exporting resource. :: class BookResource(resources.ModelResource): - + class Meta: model = Book widgets = { @@ -213,66 +214,78 @@ and exporting resource. available widget types and options. Importing data --------------- +============== Let's import data:: >>> 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']) + >>> book_resource = resources.modelresource_factory(model=Book)() # Line 4 + >>> dataset = tablib.Dataset( # Line 5 + ... ['', '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 4th line we use ``modelresource_factory`` to create a default -``ModelResource``. ModelResource class created this way is equal -as in :ref:`base-modelresource`. +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 5th line a ``Dataset`` with subset of ``Book`` fields is created. +In fifth line a :class:`~tablib.Dataset` with columns ``id`` and ``name``, and one book entry, are created. A field for a primary key field (in this case, ``id``) always needs to be present. -In rest of code we first pretend to import data with ``dry_run`` set, then -check for any errors and import data. +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 detailed import workflow descripton and customization options. + for a detailed description of the import workflow and its customization options. + Deleting data -^^^^^^^^^^^^^ +------------- -To delete objects during import, implement ``for_delete`` method on resource -class. +To delete objects during import, implement the +:meth:`~import_export.resources.Resource.for_delete` method on +your :class:`~import_export.resources.Resource` class. -Example resource with ``delete`` field:: +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 -Import of this resource will delete model instances for rows -that have column ``delete`` set to ``1``. + +.. _admin-integration: Admin integration ------------------ +================= -Admin integration is achieved by subclassing (in ``admin.py``) -``ImportExportModelAdmin`` or one of the available mixins (``ImportMixin``, -``ExportMixin``, or ``ImportExportMixin``):: +Exporting via list filters +-------------------------- - from import_export.admin import ImportExportModelAdmin +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 import_export.admin import ImportExportModelAdmin class BookAdmin(ImportExportModelAdmin): - resource_class = BookResource pass .. figure:: _static/images/django-import-export-change.png @@ -287,18 +300,19 @@ Admin integration is achieved by subclassing (in ``admin.py``) A screenshot of the confirm import view. -| + +Exporting via admin action +-------------------------- Another approach to exporting data is by subclassing -``ImportExportActionModelAdmin`` which implements export as an admin action. -As a result it's possible to export a list of objects selected on the change -list page:: +:class:`~import_export.admin.ImportExportActionModelAdmin` which implements +export as an admin action. As a result it's possible to export a list of +objects selected on the change list page:: + # app/admin.py from import_export.admin import ImportExportActionModelAdmin - class BookAdmin(ImportExportActionModelAdmin): - resource_class = BookResource pass @@ -306,7 +320,6 @@ list page:: A screenshot of the change view with Import and Export as an admin action. -| .. seealso:: diff --git a/docs/import_workflow.rst b/docs/import_workflow.rst index 40f3b9fee..7df39e18c 100644 --- a/docs/import_workflow.rst +++ b/docs/import_workflow.rst @@ -2,117 +2,112 @@ Import data workflow ==================== -This document describes import data workflow, with hooks that enable -customization of import process. +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. -``import_data`` method arguments --------------------------------- +.. function:: import_data(dataset, dry_run=False, raise_errors=False) -``import_data`` method of :class:`import_export.resources.Resource` class is -responsible for import data from given `dataset`. + The :meth:`~import_export.resources.Resource.import_data` method of + :class:`~import_export.resources.Resource` is responsible for importing data + from a given dataset. -``import_data`` expect following arguments: + ``dataset`` is required and expected to be a :class:`tablib.Dataset` with + a header row. -:attr:`dataset` - REQUIRED. - should be Tablib `Dataset`_ object with 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``. -:attr:`dry_run` - If ``True``, import should not change database. Default is ``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. -:attr:`raise_errors` - If ``True``, import should raise errors. Default is ``False``, which - means that eventual errors and traceback will be saved in ``Result`` - instance. -``import_data`` method workflow -------------------------------- +This is what happens when the method is invoked: -#. ``import_data`` intialize new :class:`import_export.results.Result` - instance. ``Result`` instance holds errors and other information - gathered during import. +#. First, a new :class:`~import_export.results.Result` instance, which holds + errors and other information gathered during the import, is initialized. -#. ``InstanceLoader`` responsible for loading existing instances - is intitalized. + Then, an :class:`~import_export.instance_loaders.InstanceLoader` responsible for loading existing instances + is intitalized. 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. - Different ``InstanceLoader`` class - can be specified with ``instance_loader_class`` - option of :class:`import_export.resources.ResourceOptions`. +#. The :meth:`~import_export.resources.Resource.before_import` hook is called. + By implementing this method in your resource, you can customize the import process. - :class:`import_export.instance_loaders.CachedInstanceLoader` can be used to - reduce number of database queries. +#. Each row of the to-be-imported dataset is processed according to the following steps: + + #. :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 customized + 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. - See :mod:`import_export.instance_loaders` for available implementations. - -#. ``import_data`` calls the ``before_import`` hook method which by default does - not do anything but can be overriden to customize the import process. The - method receives the ``dataset`` and ``dry_run`` arguments as well as any - additional keyword arguments passed to ``import_data`` in a ``kwargs`` dict. - -#. Process each `row` in ``dataset`` - - #. ``get_or_init_instance`` method is called with current ``InstanceLoader`` - and current `row` returning object `instance` and `Boolean` variable - that indicates if object instance is new. - - ``get_or_init_instance`` tries to load instance for current `row` or - calls ``init_instance`` to init object if object does not exists yet. - - Default ``ModelResource.init_instance`` initialize Django Model without - arguments. You can override ``init_instance`` method to manipulate how - new objects are initialized (ie: to set default values). - - #. ``for_delete`` method is called to determine if current `instance` - should be deleted: - - #. current `instance` is deleted - - OR - - #. ``import_obj`` method is called with the current object ``instance`` and - current ``row`` and ``dry run`` arguments. - - ``import_obj`` loop through all `Resource` `fields`, skipping - many to many fields and calls ``import_field`` for each. (Many to many - fields require that instance have a primary key, this is why assigning - them is postponed, after object is saved). - - ``import_field`` calls ``field.save`` method, if ``field`` has - both `attribute` and field `column_name` exists in given row. - - #. ``skip_row`` method is called with current object ``instance`` and - original object ``original`` to determine if the row should be skipped - - #. ``row_result.import_type`` is set to ``IMPORT_TYPE_SKIP`` - - OR - - #. ``save_instance`` method is called. - - ``save_instance`` receives ``dry_run`` argument and actually saves - instance only when ``dry_run`` is False. - - ``save_instance`` calls two hooks methods that by default does not - do anything but can be overriden to customize import process: - - * ``before_save_instance`` - - * ``after_save_instance`` - - Both methods receive ``instance`` and ``dry_run`` arguments. - - #. ``save_m2m`` method is called to save many to many fields. - - #. ``RowResult`` is assigned with diff between original and imported - object fields as well as import type(new, updated, skipped). - - If exception is raised inside row processing, and ``raise_errors`` is - ``False`` (default), traceback is appended to ``RowResult``. - - If the row was not skipped or the `Resource` is configured to report - skipped rows the ``RowResult`` is appended to the ``result`` - -#. ``result`` is returned. + 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 :class:`~import_export.results.Result` is returned. Transaction support ------------------- diff --git a/docs/index.rst b/docs/index.rst index dd11ae0eb..d98e05abf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,52 +5,44 @@ Django import / export django-import-export is a Django application and library for importing and exporting data with included admin integration. -Features: +**Features:** -* support multiple formats (Excel, CSV, JSON, ... - and everything else that `tablib`_ supports) + * support multiple formats (Excel, CSV, JSON, ... + and everything else that `tablib`_ supports) -* admin integration for importing + * admin integration for importing -* preview import changes + * preview import changes -* admin integration for exporting + * admin integration for exporting -* export data respecting admin filters + * export data respecting admin filters -.. figure:: _static/images/django-import-export-change.png + .. figure:: _static/images/django-import-export-change.png - A screenshot of the change view with Import and Export buttons. + A screenshot of the change view with Import and Export buttons. -User Guide ----------- .. toctree:: :maxdepth: 2 + :caption: User Guide installation - configuration getting_started import_workflow - example_app - settings - todo - contributing changelog -API documentation ------------------ - .. toctree:: :maxdepth: 2 + :caption: API documentation + api_admin api_resources - api_fields api_widgets + api_fields api_instance_loaders - api_admin - api_results api_tmp_storages + api_results .. _`tablib`: https://github.com/kennethreitz/tablib diff --git a/docs/installation.rst b/docs/installation.rst index 8c1698939..dec387425 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,10 +1,69 @@ -============ -Installation -============ +============================== +Installation and configuration +============================== -django-import-export is on the Python Package Index (PyPI), -so it can be installed with standard Python tools like pip or easy_install: - -:: +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 + +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 prequisites are set up! See :doc:`getting_started` to learn how to use django-import-export in your project. + + + +Settings +======== + +You can use the following directives in your settings file: + +``IMPORT_EXPORT_USE_TRANSACTIONS`` + Global setting controls if resource importing should use database + transactions. Default is ``False``. + +``IMPORT_EXPORT_SKIP_ADMIN_LOG`` + Global setting controls if creating log entries for + the admin changelist should be skipped when importing resource. + The `skip_admin_log` attribute of `ImportMixin` is checked first, + which defaults to ``None``. If not found, this global option is used. + This will speed up importing large datasets, but will lose + changing logs in the admin changelist view. Default is ``False``. + +``IMPORT_EXPORT_TMP_STORAGE_CLASS`` + Global setting for the class to use to handle temporary storage + of the uploaded file when importing from the admin using an + `ImportMixin`. The `tmp_storage_class` attribute of `ImportMixin` + is checked first, which defaults to ``None``. If not found, this + global option is used. Default is ``TempFolderStorage``. + + + +Example app +=========== + +There's an example application that showcases what django-import-export can do. You can run it via:: + + cd tests + ./manage.py runserver + +Username and password for admin are ``admin`` and ``password``. diff --git a/docs/settings.rst b/docs/settings.rst deleted file mode 100644 index 20276f016..000000000 --- a/docs/settings.rst +++ /dev/null @@ -1,22 +0,0 @@ -======== -Settings -======== - -``IMPORT_EXPORT_USE_TRANSACTIONS`` - Global setting controls if resource importing should use database - transactions. Default is ``False``. - -``IMPORT_EXPORT_SKIP_ADMIN_LOG`` - Global setting controls if creating log entries for - the admin changelist should be skipped when importing resource. - The `skip_admin_log` attribute of `ImportMixin` is checked first, - which defaults to ``None``. If not found, this global option is used. - This will speed up importing large datasets, but will lose - changing logs in the admin changelist view. Default is ``False``. - -``IMPORT_EXPORT_TMP_STORAGE_CLASS`` - Global setting for the class to use to handle temporary storage - of the uploaded file when importing from the admin using an - `ImportMixin`. The `tmp_storage_class` attribute of `ImportMixin` - is checked first, which defaults to ``None``. If not found, this - global option is used. Default is ``TempFolderStorage``. diff --git a/docs/todo.rst b/docs/todo.rst deleted file mode 100644 index 00fc9341b..000000000 --- a/docs/todo.rst +++ /dev/null @@ -1,4 +0,0 @@ -==== -TODO -==== - diff --git a/import_export/admin.py b/import_export/admin.py index 0a21fee84..687e398e6 100644 --- a/import_export/admin.py +++ b/import_export/admin.py @@ -48,7 +48,8 @@ msg = "Could not import '%s' for import_export setting 'IMPORT_EXPORT_TMP_STORAGE_CLASS'" % TMP_STORAGE_CLASS raise ImportError(msg) -#: import / export formats +#: 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 = ( base_formats.CSV, base_formats.XLS, diff --git a/import_export/resources.py b/import_export/resources.py index 88ab431d2..0d0583ad2 100644 --- a/import_export/resources.py +++ b/import_export/resources.py @@ -64,47 +64,65 @@ class ResourceOptions(object): """ The inner Meta class allows for class-level configuration of how the Resource should behave. The following options are available: + """ - * ``fields`` - Controls what introspected fields the Resource - should include. A whitelist of fields. - - * ``exclude`` - Controls what introspected fields the Resource should - NOT include. A blacklist of fields. - - * ``model`` - Django Model class. It is used to introspect available - fields. - - * ``instance_loader_class`` - Controls which class instance will take - care of loading existing objects. - - * ``import_id_fields`` - Controls which object fields will be used to - identify existing instances. - - * ``export_order`` - Controls export order for columns. - - * ``widgets`` - dictionary defines widget kwargs for fields. - - * ``use_transactions`` - Controls if import should use database - transactions. Default value is ``None`` meaning - ``settings.IMPORT_EXPORT_USE_TRANSACTIONS`` will be evaluated. - - * ``skip_unchanged`` - Controls if the import should skip unchanged - records. Default value is False - - * ``report_skipped`` - Controls if the result reports skipped rows - Default value is True + model = None + """ + Django Model class. It is used to introspect available + fields. """ fields = None - model = 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 + """ class DeclarativeMetaclass(type): @@ -161,14 +179,15 @@ def get_use_transactions(self): def get_fields(self): """ - Returns fields in ``export_order`` order. + Returns fields sorted according to + :attr:`~import_export.resources.ResourceOptions.export_order`. """ return [self.fields[f] for f in self.get_export_order()] @classmethod def get_field_name(cls, field): """ - Returns field name for given field. + Returns the field name for a given field. """ for field_name, f in cls.fields.items(): if f == field: @@ -180,9 +199,15 @@ def init_instance(self, row=None): raise NotImplementedError() def get_instance(self, instance_loader, row): + """ + Calls the :doc:`InstanceLoader `. + """ 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. + """ instance = self.get_instance(instance_loader, row) if instance: return (instance, False) @@ -190,6 +215,12 @@ def get_or_init_instance(self, instance_loader, row): return (self.init_instance(row), True) def save_instance(self, instance, dry_run=False): + """ + Takes care of saving the object to the database. + + Keep in mind that this is done by calling ``instance.save()``, so + objects are not created in bulk! + """ self.before_save_instance(instance, dry_run) if not dry_run: instance.save() @@ -197,17 +228,20 @@ def save_instance(self, instance, dry_run=False): def before_save_instance(self, instance, dry_run): """ - Override to add additional logic. + Override to add additional logic. Does nothing by default. """ pass def after_save_instance(self, instance, dry_run): """ - Override to add additional logic. + Override to add additional logic. Does nothing by default. """ pass def delete_instance(self, instance, dry_run=False): + """ + Calls :meth:`instance.delete` as long as ``dry_run`` is not set. + """ self.before_delete_instance(instance, dry_run) if not dry_run: instance.delete() @@ -215,22 +249,28 @@ def delete_instance(self, instance, dry_run=False): def before_delete_instance(self, instance, dry_run): """ - Override to add additional logic. + Override to add additional logic. Does nothing by default. """ pass def after_delete_instance(self, instance, dry_run): """ - Override to add additional logic. + Override to add additional logic. Does nothing by default. """ pass def import_field(self, field, obj, data): + """ + Calls :meth:`import_export.fields.Field.save` if ``Field.attribute`` + and ``Field.column_name`` are found in ``data``. + """ if field.attribute and field.column_name in data: field.save(obj, data) def import_obj(self, obj, data, dry_run): """ + Traverses every field in this Resource and calls + :meth:`~import_export.resources.Resource.import_field`. """ for field in self.get_fields(): if isinstance(field.widget, widgets.ManyToManyWidget): @@ -308,7 +348,12 @@ def get_diff_headers(self): def before_import(self, dataset, dry_run, **kwargs): """ - Override to add additional logic. + Override to add additional logic. Does nothing by default. + + This method receives the ``dataset`` that's going to be imported, the + ``dry_run`` parameter which determines whether changes are saved to + the database, and any additional keyword arguments passed to + ``import_data`` in a ``kwargs`` dict. """ pass @@ -316,12 +361,19 @@ def before_import(self, dataset, dry_run, **kwargs): def import_data(self, dataset, dry_run=False, raise_errors=False, use_transactions=None, **kwargs): """ - Imports data from ``dataset``. + 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. - ``use_transactions`` - If ``True`` import process will be processed inside transaction. - If ``dry_run`` is set, or error occurs, transaction will be rolled - back. + :param use_transactions: If ``True`` import process will be processed + inside transaction. + + :param dry_run: If ``dry_run`` is set, or error occurs, transaction + will be rolled back. """ result = Result() result.diff_headers = self.get_diff_headers() @@ -600,12 +652,21 @@ def field_from_django_field(self, field_name, django_field, readonly): return field def get_import_id_fields(self): + """ + """ return self._meta.import_id_fields 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() diff --git a/import_export/widgets.py b/import_export/widgets.py index af971cfc2..6a7cd3513 100644 --- a/import_export/widgets.py +++ b/import_export/widgets.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from decimal import Decimal @@ -14,29 +15,40 @@ class Widget(object): """ - Widget takes care of converting between import and export representations. + A Widget takes care of converting between import and export representations. - Widget objects have two functions: - - * converts object field value to export representation - - * converts import value and converts it to appropriate python - representation + This is achieved by the two methods, + :meth:`~import_export.widgets.Widget.clean` and + :meth:`~import_export.widgets.Widget.render`. """ + def clean(self, value): """ - Returns appropriate python objects for import value. + 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): """ - Returns export representation of python value. + 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_text(value) class NumberWidget(Widget): + """ + """ def is_empty(self, value): # 0 is not empty @@ -135,7 +147,8 @@ class DateTimeWidget(Widget): """ Widget for converting date fields. - Takes optional ``format`` parameter. + 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): @@ -205,25 +218,38 @@ def render(self, value): class ForeignKeyWidget(Widget): """ - Widget for ``ForeignKey`` which looks up a related model. + Widget for a ``ForeignKey`` field which looks up a related model using + "natural keys" in both export an import. + + The lookup field defaults to using the primary key (``pk``) as lookup + criterion but can be customised to use any field on the related model. + + Unlike specifying a related field in your resource like so… + + :: - The lookup field defaults to using the primary key (``pk``), but - can be customised to use any field on the related model. + class Meta: + fields = ('author__name',) - e.g. To use a lookup field other than ``pk``, rather than specifying a - field in your Resource as ``class Meta: fields = ('author__name', ...)``, - you would specify it in your Resource like so: + …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``:: class BookResource(resources.ModelResource): - author = fields.Field(column_name='author', attribute='author', \ + author = fields.Field( + column_name='author', + attribute='author', widget=ForeignKeyWidget(Author, 'name')) - class Meta: fields = ('author', ...) - This will allow you to use "natural keys" for both import and export. + class Meta: + fields = ('author',) - Parameters: - ``model`` should be the Model instance for this ForeignKey (required). - ``field`` should be the lookup field on the related model. + :param model: The Model the ForeignKey refers to (required). + :param field: A field on the related model used for looking up a particular object. """ def __init__(self, model, field='pk', *args, **kwargs): self.model = model @@ -242,16 +268,12 @@ def render(self, value): class ManyToManyWidget(Widget): """ - Widget for ``ManyToManyField`` model field that represent m2m field - as values that identify many-to-many relationship. - - Requires a positional argument: the class to which the field is related. - - Optional keyword arguments are: - - separator - default "," + Widget that converts between representations of a ManyToMany relationships + as a list and an actual ManyToMany field. - field - field of related model, default ``pk`` + :param model: The model the ManyToMany field refers to (required). + :param separator: Defaults to ``','``. + :param field: A field on the related model. Default is ``pk``. """ def __init__(self, model, separator=None, field=None, *args, **kwargs):