Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/import_export/widgets.py: 35%

218 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import json 

2from datetime import date, datetime, time 

3from decimal import Decimal 

4 

5import django 

6from django.conf import settings 

7from django.core.exceptions import ObjectDoesNotExist 

8from django.utils import timezone 

9from django.utils.dateparse import parse_duration 

10from django.utils.encoding import force_str, smart_str 

11from django.utils.formats import number_format 

12 

13 

14def format_datetime(value, datetime_format): 

15 # conditional logic to handle correct formatting of dates 

16 # see https://code.djangoproject.com/ticket/32738 

17 if django.VERSION[0] >= 4: 

18 format = django.utils.formats.sanitize_strftime_format(datetime_format) 

19 return value.strftime(format) 

20 else: 

21 return django.utils.datetime_safe.new_datetime(value).strftime(datetime_format) 

22 

23 

24class Widget: 

25 """ 

26 A Widget takes care of converting between import and export representations. 

27 

28 This is achieved by the two methods, 

29 :meth:`~import_export.widgets.Widget.clean` and 

30 :meth:`~import_export.widgets.Widget.render`. 

31 """ 

32 def clean(self, value, row=None, **kwargs): 

33 """ 

34 Returns an appropriate Python object for an imported value. 

35 

36 For example, if you import a value from a spreadsheet, 

37 :meth:`~import_export.widgets.Widget.clean` handles conversion 

38 of this value into the corresponding Python object. 

39 

40 Numbers or dates can be *cleaned* to their respective data types and 

41 don't have to be imported as Strings. 

42 """ 

43 return value 

44 

45 def render(self, value, obj=None): 

46 """ 

47 Returns an export representation of a Python value. 

48 

49 For example, if you have an object you want to export, 

50 :meth:`~import_export.widgets.Widget.render` takes care of converting 

51 the object's field to a value that can be written to a spreadsheet. 

52 """ 

53 return force_str(value) 

54 

55 

56class NumberWidget(Widget): 

57 """ 

58 Takes optional ``coerce_to_string`` parameter, set to ``True`` the  

59 :meth:`~import_export.widgets.Widget.render` method will return a string 

60 else it will return a value. 

61 """ 

62 

63 def __init__(self, coerce_to_string=False): 

64 self.coerce_to_string = coerce_to_string 

65 

66 def is_empty(self, value): 

67 if isinstance(value, str): 

68 value = value.strip() 

69 # 0 is not empty 

70 return value is None or value == "" 

71 

72 def render(self, value, obj=None): 

73 return number_format(value) if self.coerce_to_string else value 

74 

75 

76class FloatWidget(NumberWidget): 

77 """ 

78 Widget for converting floats fields. 

79 """ 

80 

81 def clean(self, value, row=None, **kwargs): 

82 if self.is_empty(value): 

83 return None 

84 return float(value) 

85 

86 

87class IntegerWidget(NumberWidget): 

88 """ 

89 Widget for converting integer fields. 

90 """ 

91 

92 def clean(self, value, row=None, **kwargs): 

93 if self.is_empty(value): 

94 return None 

95 return int(Decimal(value)) 

96 

97 

98class DecimalWidget(NumberWidget): 

99 """ 

100 Widget for converting decimal fields. 

101 """ 

102 

103 def clean(self, value, row=None, **kwargs): 

104 if self.is_empty(value): 

105 return None 

106 return Decimal(force_str(value)) 

107 

108 

109class CharWidget(Widget): 

110 """ 

111 Widget for converting text fields. 

112 """ 

113 pass 

114 

115 

116class BooleanWidget(Widget): 

117 """ 

118 Widget for converting boolean fields. 

119 

120 The widget assumes that ``True``, ``False``, and ``None`` are all valid 

121 values, as to match Django's `BooleanField 

122 <https://docs.djangoproject.com/en/dev/ref/models/fields/#booleanfield>`_. 

123 That said, whether the database/Django will actually accept NULL values 

124 will depend on if you have set ``null=True`` on that Django field. 

125 

126 While the BooleanWidget is set up to accept as input common variations of 

127 "True" and "False" (and "None"), you may need to munge less common values 

128 to ``True``/``False``/``None``. Probably the easiest way to do this is to 

129 override the :func:`~import_export.resources.Resource.before_import_row` 

130 function of your Resource class. A short example:: 

131 

132 from import_export import fields, resources, widgets 

133 

134 class BooleanExample(resources.ModelResource): 

135 warn = fields.Field(widget=widgets.BooleanWidget()) 

136 

137 def before_import_row(self, row, row_number=None, **kwargs): 

138 if "warn" in row.keys(): 

139 # munge "warn" to "True" 

140 if row["warn"] in ["warn", "WARN"]: 

141 row["warn"] = True 

142 

143 return super().before_import_row(row, row_number, **kwargs) 

144 """ 

145 TRUE_VALUES = ["1", 1, True, "true", "TRUE", "True"] 

146 FALSE_VALUES = ["0", 0, False, "false", "FALSE", "False"] 

147 NULL_VALUES = ["", None, "null", "NULL", "none", "NONE", "None"] 

148 

149 def render(self, value, obj=None): 

150 """ 

151 On export, ``True`` is represented as ``1``, ``False`` as ``0``, and 

152 ``None``/NULL as a empty string. 

153 

154 Note that these values are also used on the import confirmation view. 

155 """ 

156 if value in self.NULL_VALUES: 

157 return "" 

158 return self.TRUE_VALUES[0] if value else self.FALSE_VALUES[0] 

159 

160 def clean(self, value, row=None, **kwargs): 

161 if value in self.NULL_VALUES: 

162 return None 

163 return True if value in self.TRUE_VALUES else False 

164 

165 

166class DateWidget(Widget): 

167 """ 

168 Widget for converting date fields. 

169 

170 Takes optional ``format`` parameter. If none is set, either 

171 ``settings.DATE_INPUT_FORMATS`` or ``"%Y-%m-%d"`` is used. 

172 """ 

173 

174 def __init__(self, format=None): 

175 if format is None: 175 ↛ 181line 175 didn't jump to line 181, because the condition on line 175 was never false

176 if not settings.DATE_INPUT_FORMATS: 176 ↛ 177line 176 didn't jump to line 177, because the condition on line 176 was never true

177 formats = ("%Y-%m-%d",) 

178 else: 

179 formats = settings.DATE_INPUT_FORMATS 

180 else: 

181 formats = (format,) 

182 self.formats = formats 

183 

184 def clean(self, value, row=None, **kwargs): 

185 if not value: 

186 return None 

187 if isinstance(value, date): 

188 return value 

189 for format in self.formats: 

190 try: 

191 return datetime.strptime(value, format).date() 

192 except (ValueError, TypeError): 

193 continue 

194 raise ValueError("Enter a valid date.") 

195 

196 def render(self, value, obj=None): 

197 if not value: 

198 return "" 

199 return format_datetime(value, self.formats[0]) 

200 

201 

202class DateTimeWidget(Widget): 

203 """ 

204 Widget for converting date fields. 

205 

206 Takes optional ``format`` parameter. If none is set, either 

207 ``settings.DATETIME_INPUT_FORMATS`` or ``"%Y-%m-%d %H:%M:%S"`` is used. 

208 """ 

209 

210 def __init__(self, format=None): 

211 if format is None: 211 ↛ 217line 211 didn't jump to line 217, because the condition on line 211 was never false

212 if not settings.DATETIME_INPUT_FORMATS: 212 ↛ 213line 212 didn't jump to line 213, because the condition on line 212 was never true

213 formats = ("%Y-%m-%d %H:%M:%S",) 

214 else: 

215 formats = settings.DATETIME_INPUT_FORMATS 

216 else: 

217 formats = (format,) 

218 self.formats = formats 

219 

220 def clean(self, value, row=None, **kwargs): 

221 dt = None 

222 if not value: 

223 return None 

224 if isinstance(value, datetime): 

225 dt = value 

226 else: 

227 for format_ in self.formats: 

228 try: 

229 dt = datetime.strptime(value, format_) 

230 except (ValueError, TypeError): 

231 continue 

232 if dt: 

233 if settings.USE_TZ and timezone.is_naive(dt): 

234 dt = timezone.make_aware(dt) 

235 return dt 

236 raise ValueError("Enter a valid date/time.") 

237 

238 def render(self, value, obj=None): 

239 if not value: 

240 return "" 

241 if settings.USE_TZ: 

242 value = timezone.localtime(value) 

243 return format_datetime(value, self.formats[0]) 

244 

245 

246class TimeWidget(Widget): 

247 """ 

248 Widget for converting time fields. 

249 

250 Takes optional ``format`` parameter. If none is set, either 

251 ``settings.DATETIME_INPUT_FORMATS`` or ``"%H:%M:%S"`` is used. 

252 """ 

253 

254 def __init__(self, format=None): 

255 if format is None: 

256 if not settings.TIME_INPUT_FORMATS: 

257 formats = ("%H:%M:%S",) 

258 else: 

259 formats = settings.TIME_INPUT_FORMATS 

260 else: 

261 formats = (format,) 

262 self.formats = formats 

263 

264 def clean(self, value, row=None, **kwargs): 

265 if not value: 

266 return None 

267 if isinstance(value, time): 

268 return value 

269 for format in self.formats: 

270 try: 

271 return datetime.strptime(value, format).time() 

272 except (ValueError, TypeError): 

273 continue 

274 raise ValueError("Enter a valid time.") 

275 

276 def render(self, value, obj=None): 

277 if not value: 

278 return "" 

279 return value.strftime(self.formats[0]) 

280 

281 

282class DurationWidget(Widget): 

283 """ 

284 Widget for converting time duration fields. 

285 """ 

286 

287 def clean(self, value, row=None, **kwargs): 

288 if not value: 

289 return None 

290 

291 try: 

292 return parse_duration(value) 

293 except (ValueError, TypeError): 

294 raise ValueError("Enter a valid duration.") 

295 

296 def render(self, value, obj=None): 

297 if value is None: 

298 return "" 

299 return str(value) 

300 

301 

302class SimpleArrayWidget(Widget): 

303 """ 

304 Widget for an Array field. Can be used for Postgres' Array field. 

305 

306 :param separator: Defaults to ``','`` 

307 """ 

308 

309 def __init__(self, separator=None): 

310 if separator is None: 

311 separator = ',' 

312 self.separator = separator 

313 super().__init__() 

314 

315 def clean(self, value, row=None, **kwargs): 

316 return value.split(self.separator) if value else [] 

317 

318 def render(self, value, obj=None): 

319 return self.separator.join(str(v) for v in value) 

320 

321 

322class JSONWidget(Widget): 

323 """ 

324 Widget for a JSON object (especially required for jsonb fields in PostgreSQL database.) 

325 

326 :param value: Defaults to JSON format. 

327 The widget covers two cases: Proper JSON string with double quotes, else it 

328 tries to use single quotes and then convert it to proper JSON. 

329 """ 

330 

331 def clean(self, value, row=None, **kwargs): 

332 val = super().clean(value) 

333 if val: 

334 try: 

335 return json.loads(val) 

336 except json.decoder.JSONDecodeError: 

337 return json.loads(val.replace("'", "\"")) 

338 

339 def render(self, value, obj=None): 

340 if value: 

341 return json.dumps(value) 

342 

343 

344class ForeignKeyWidget(Widget): 

345 """ 

346 Widget for a ``ForeignKey`` field which looks up a related model using 

347 either the PK or a user specified field that uniquely identifies the 

348 instance in both export and import. 

349 

350 The lookup field defaults to using the primary key (``pk``) as lookup 

351 criterion but can be customized to use any field on the related model. 

352 

353 Unlike specifying a related field in your resource like so… 

354 

355 :: 

356 

357 class Meta: 

358 fields = ('author__name',) 

359 

360 …using a :class:`~import_export.widgets.ForeignKeyWidget` has the 

361 advantage that it can not only be used for exporting, but also importing 

362 data with foreign key relationships. 

363 

364 Here's an example on how to use 

365 :class:`~import_export.widgets.ForeignKeyWidget` to lookup related objects 

366 using ``Author.name`` instead of ``Author.pk``:: 

367 

368 from import_export import fields, resources 

369 from import_export.widgets import ForeignKeyWidget 

370 

371 class BookResource(resources.ModelResource): 

372 author = fields.Field( 

373 column_name='author', 

374 attribute='author', 

375 widget=ForeignKeyWidget(Author, 'name')) 

376 

377 class Meta: 

378 fields = ('author',) 

379 

380 :param model: The Model the ForeignKey refers to (required). 

381 :param field: A field on the related model used for looking up a particular 

382 object. 

383 :param use_natural_foreign_keys: Use natural key functions to identify 

384 related object, default to False 

385 """ 

386 def __init__(self, model, field='pk', use_natural_foreign_keys=False, **kwargs): 

387 self.model = model 

388 self.field = field 

389 self.use_natural_foreign_keys = use_natural_foreign_keys 

390 super().__init__(**kwargs) 

391 

392 def get_queryset(self, value, row, *args, **kwargs): 

393 """ 

394 Returns a queryset of all objects for this Model. 

395 

396 Overwrite this method if you want to limit the pool of objects from 

397 which the related object is retrieved. 

398 

399 :param value: The field's value in the datasource. 

400 :param row: The datasource's current row. 

401 

402 As an example; if you'd like to have ForeignKeyWidget look up a Person 

403 by their pre- **and** lastname column, you could subclass the widget 

404 like so:: 

405 

406 class FullNameForeignKeyWidget(ForeignKeyWidget): 

407 def get_queryset(self, value, row, *args, **kwargs): 

408 return self.model.objects.filter( 

409 first_name__iexact=row["first_name"], 

410 last_name__iexact=row["last_name"] 

411 ) 

412 """ 

413 return self.model.objects.all() 

414 

415 def clean(self, value, row=None, **kwargs): 

416 val = super().clean(value) 

417 if val: 

418 if self.use_natural_foreign_keys: 

419 # natural keys will always be a tuple, which ends up as a json list. 

420 value = json.loads(value) 

421 return self.model.objects.get_by_natural_key(*value) 

422 else: 

423 return self.get_queryset(value, row, **kwargs).get(**{self.field: val}) 

424 else: 

425 return None 

426 

427 def render(self, value, obj=None): 

428 if value is None: 

429 return "" 

430 

431 attrs = self.field.split('__') 

432 for attr in attrs: 

433 try: 

434 if self.use_natural_foreign_keys: 

435 # inbound natural keys must be a json list. 

436 return json.dumps(value.natural_key()) 

437 else: 

438 value = getattr(value, attr, None) 

439 except (ValueError, ObjectDoesNotExist): 

440 # needs to have a primary key value before a many-to-many 

441 # relationship can be used. 

442 return None 

443 if value is None: 

444 return None 

445 

446 return value 

447 

448 

449class ManyToManyWidget(Widget): 

450 """ 

451 Widget that converts between representations of a ManyToMany relationships 

452 as a list and an actual ManyToMany field. 

453 

454 :param model: The model the ManyToMany field refers to (required). 

455 :param separator: Defaults to ``','``. 

456 :param field: A field on the related model. Default is ``pk``. 

457 """ 

458 

459 def __init__(self, model, separator=None, field=None, **kwargs): 

460 if separator is None: 460 ↛ 462line 460 didn't jump to line 462, because the condition on line 460 was never false

461 separator = ',' 

462 if field is None: 462 ↛ 464line 462 didn't jump to line 464, because the condition on line 462 was never false

463 field = 'pk' 

464 self.model = model 

465 self.separator = separator 

466 self.field = field 

467 super().__init__(**kwargs) 

468 

469 def clean(self, value, row=None, **kwargs): 

470 if not value: 

471 return self.model.objects.none() 

472 if isinstance(value, (float, int)): 

473 ids = [int(value)] 

474 else: 

475 ids = value.split(self.separator) 

476 ids = filter(None, [i.strip() for i in ids]) 

477 return self.model.objects.filter(**{ 

478 '%s__in' % self.field: ids 

479 }) 

480 

481 def render(self, value, obj=None): 

482 ids = [smart_str(getattr(obj, self.field)) for obj in value.all()] 

483 return self.separator.join(ids)