• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

saritasa-nest / django-import-export-extensions / 6143239757

11 Sep 2023 07:22AM UTC coverage: 77.914% (-0.2%) from 78.1%
6143239757

push

github

web-flow
Merge pull request #14 from saritasa-nest/extend-documentation

13 of 19 new or added lines in 5 files covered. (68.42%)

6 existing lines in 2 files now uncovered.

1083 of 1390 relevant lines covered (77.91%)

9.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.17
/import_export_extensions/fields.py
1
from django.db.models.fields.reverse_related import ManyToManyRel
12✔
2

3
from import_export.fields import Field
12✔
4

5

6
class M2MField(Field):
12✔
7
    """Base M2M field provides faster related instances import."""
8

9
    def __init__(self, *args, export_order=None, **kwargs):
12✔
10
        """Save additional field params.
11

12
        Args:
13
            export_order(str): field name that should be used for ordering
14
                instances during export
15

16
        """
17
        super().__init__(*args, **kwargs)
12✔
18
        self.export_order = export_order
12✔
19

20
    def _format_exception(self, exception):
12✔
21
        """Shortcut for humanizing exception."""
22
        error = str(exception)
×
23
        if hasattr(exception, "messages"):
×
24
            error = " ".join(exception.messages)
×
25

26
        msg = f"Column '{self.column_name}': {error}"
×
27
        raise ValueError(msg) from exception
×
28

29
    def get_value(self, obj):
12✔
30
        """Return the value of the object's attribute.
31

32
        This method should return instances of intermediate model, i.e.
33
        Membership instances
34

35
        """
36
        if self.attribute is None:
12✔
37
            return None
12✔
38

39
        m2m_rel, _, field_name, _ = self.get_relation_field_params(obj)
12✔
40

41
        # retrieve intermediate model
42
        # intermediate_model == Membership
43
        intermediate_model = m2m_rel.through
12✔
44

45
        # filter relations with passed object
46
        qs = intermediate_model.objects.filter(**{field_name: obj})
12✔
47
        if self.export_order:
12✔
48
            qs = qs.order_by(self.export_order)
×
49
        return qs
12✔
50

51
    def get_relation_field_params(self, obj):
12✔
52
        """Shortcut to get relation field params.
53

54
        Gets relation, field itself, its name and its reversed field name
55

56
        """
57
        # retrieve M2M field itself (i.e. Artist.bands)
58
        field = obj._meta.get_field(self.attribute)
12✔
59

60
        # if field is `ManyToManyRel` - it is a reversed relation
61
        if isinstance(field, ManyToManyRel):
12✔
62
            m2m_rel = field
×
63
            m2m_field = m2m_rel.field
×
64
            field_name = m2m_field.m2m_reverse_field_name()
×
65
            reversed_field_name = m2m_field.m2m_field_name()
×
66
            return m2m_rel, m2m_field, field_name, reversed_field_name
×
67

68
        # otherwise it is a forward relation
69
        m2m_rel = field.remote_field
12✔
70
        m2m_field = field
12✔
71
        field_name = m2m_field.m2m_field_name()
12✔
72
        reversed_field_name = m2m_field.m2m_reverse_field_name()
12✔
73
        return m2m_rel, m2m_field, field_name, reversed_field_name
12✔
74

75
    def save(self, obj, data, *args, **kwargs):
12✔
76
        """Delete intermediate models.
77

78
        This implementation deletes intermediate models, which were excluded
79
        and creates intermediate models only for newly added models.
80

81
        Parent `save` method deletes and recreates intermediate models for all
82
        instances which generates a lot of exceed Feed Entries, so overridden.
83

84
        """
85
        (
×
86
            _,
87
            m2m_field,
88
            field_name,
89
            reversed_field_name,
90
        ) = self.get_relation_field_params(obj)
91

92
        # retrieve intermediate model (AttendeeTeamMembership)
93
        intermediate_model = m2m_field.remote_field.through
×
94

95
        # should be returned following list:
96
        # [{'object': Instance01, 'properties': {}},
97
        # {'object': Instance02, 'properties': {}}]
98
        data = self.clean(data)
×
99

100
        # IDs of related instances in imported data
101
        imported_ids = {i["object"].id for i in data}
×
102

103
        # IDs of current instances
104
        manager = getattr(obj, self.attribute)
×
105
        current_ids = set(manager.values_list("id", flat=True))
×
106

107
        # Find instances to be excluded after import
108
        excluded_ids = current_ids - imported_ids
×
109
        intermediate_model.objects.filter(
×
110
            **{
111
                field_name: obj,
112
                f"{reversed_field_name}__id__in": excluded_ids,
113
            },
114
        ).delete()
115

116
        # Find instances to add after import
117
        added_ids = imported_ids - current_ids
×
118
        for instance in data:
×
119
            # process only newly added attendees
120
            if instance["object"].id not in added_ids:
×
121
                continue
×
122
            obj_data = instance["properties"].copy()
×
123
            obj_data.update(
×
124
                {
125
                    field_name: obj,
126
                    reversed_field_name: instance["object"],
127
                },
128
            )
129
            intermediate_obj = intermediate_model(**obj_data)
×
130
            try:
×
131
                intermediate_obj.full_clean()
×
132
            except Exception as exception:
×
133
                self._format_exception(exception)
×
134
            intermediate_obj.save()
×
135

136

137
class IntermediateManyToManyField(M2MField):
12✔
138
    """Resource field for M2M with custom ``through`` model.
139

140
    By default, ``django-import-export`` set up object attributes using
141
    ``setattr(obj, attribute_name, value)``, where ``value`` is ``QuerySet``
142
    of related model objects. But django forbid this when `ManyToManyField``
143
    used with custom ``through`` model.
144

145
    This field expects be used with custom widget that return not simple value,
146
    but dict with intermediate model attributes.
147

148
    For easy comments following models will be used:
149

150
        Artist:
151
            name
152
            bands ->
153

154
        Membership:
155
            artist
156
            band
157
            date_joined
158

159
        Band:
160
            title
161
            <- artists
162

163
    So this field should be used for exporting Artists with `bands` field.
164

165
    Save workflow is following:
166
        1. clean data (extract dicts)
167
        2. Remove current M2M instances of object
168
        3. Create new M2M instances based on current object
169

170
    """
171

172
    def save(self, obj, data, *args, **kwargs):
12✔
173
        """Add M2M relations for obj from data.
174

175
        Args:
176
            obj(model instance): object being imported
177
            data(OrderedDict): all extracted data for object
178

179
        Example:
180
            obj - Artist instance
181

182
        """
183
        if self.readonly:
12✔
184
            return
12✔
185

186
        (
12✔
187
            m2m_rel,
188
            m2m_field,
189
            field_name,
190
            reversed_field_name,
191
        ) = self.get_relation_field_params(obj)
192

193
        # retrieve intermediate model
194
        # IntermediateModel == Membership
195
        IntermediateModel = m2m_rel.through
12✔
196

197
        # should be returned following list:
198
        # [{'band': <Band obj>, 'date_joined': '2016-08-18'}]
199
        instances_data = self.clean(data)
12✔
200

201
        # remove current related objects,
202
        # i.e. clear artists's band
203
        IntermediateModel.objects.filter(**{field_name: obj}).delete()
12✔
204

205
        for rel_obj_data in instances_data:
12✔
206
            # add current and remote object to intermediate instance data
207
            # i.e. {'artist': <Artist obj>, 'band': rel_obj_data['properties']}
208
            obj_data = rel_obj_data["properties"].copy()
12✔
209
            obj_data.update(
12✔
210
                {
211
                    field_name: obj,
212
                    reversed_field_name: rel_obj_data["object"],
213
                },
214
            )
215
            intermediate_obj = IntermediateModel(**obj_data)
12✔
216
            try:
12✔
217
                intermediate_obj.full_clean()
12✔
218
            except Exception as e:
×
219
                self._format_exception(e)
×
220
            intermediate_obj.save()
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc