0005-csv_import-adapt-user-csv-logic-to-new-phone_number-.patch
src/authentic2/csv_import.py | ||
---|---|---|
19 | 19 |
import io |
20 | 20 | |
21 | 21 |
import attr |
22 |
import phonenumbers |
|
22 | 23 |
from chardet.universaldetector import UniversalDetector |
23 | 24 |
from django import forms |
24 | 25 |
from django.contrib.auth.hashers import identify_hasher |
... | ... | |
27 | 28 |
from django.db import IntegrityError, models |
28 | 29 |
from django.db.transaction import atomic |
29 | 30 |
from django.utils.encoding import force_bytes, force_str |
31 |
from django.utils.timezone import now |
|
30 | 32 |
from django.utils.translation import gettext as _ |
31 | 33 | |
32 | 34 |
from authentic2 import app_settings |
... | ... | |
35 | 37 |
from authentic2.custom_user.models import User |
36 | 38 |
from authentic2.forms.profile import BaseUserForm, modelform_factory |
37 | 39 |
from authentic2.models import Attribute, AttributeValue, PasswordReset, UserExternalId |
38 |
from authentic2.utils.misc import send_password_reset_mail |
|
40 |
from authentic2.utils.misc import parse_phone_number, send_password_reset_mail
|
|
39 | 41 | |
40 | 42 | |
41 | 43 |
# http://www.attrs.org/en/stable/changelog.html : |
... | ... | |
470 | 472 |
header.key = True |
471 | 473 |
elif header.name not in SPECIAL_COLUMNS: |
472 | 474 |
try: |
473 |
if header.name in ['email', 'first_name', 'last_name', 'username']: |
|
475 |
if header.name in ['email', 'phone', 'first_name', 'last_name', 'username']:
|
|
474 | 476 |
User._meta.get_field(header.name) |
475 | 477 |
header.field = True |
476 | 478 |
if header.name == 'email': |
... | ... | |
543 | 545 | |
544 | 546 |
def parse_row(self, form_class, row, line): |
545 | 547 |
data = {} |
546 | ||
548 |
errors = {} |
|
547 | 549 |
for header in self.headers: |
548 | 550 |
try: |
549 | 551 |
data[header.name] = row[header.column - 1] |
550 | 552 |
except IndexError: |
551 | 553 |
pass |
552 | 554 | |
555 |
if 'phone' in data: |
|
556 |
pn = parse_phone_number(data['phone']) |
|
557 |
if pn: |
|
558 |
# fill multi value field |
|
559 |
data['phone_0'] = str(pn.country_code) |
|
560 |
data['phone_1'] = phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.NATIONAL) |
|
561 |
data.pop('phone') |
|
562 |
else: |
|
563 |
# E.164 compliant parsing failed, leave the number untouched, add an error |
|
564 |
errors.update({'phone': [_('Enter a valid phone number.')]}) |
|
565 | ||
553 | 566 |
form = form_class(data=data) |
567 |
form.errors.update(errors) |
|
554 | 568 |
form.is_valid() |
555 | 569 | |
556 | 570 |
def get_form_errors(form, name): |
... | ... | |
716 | 730 |
setattr(user, cell.header.name, cell.value) |
717 | 731 |
if cell.header.name == 'email' and cell.header.verified: |
718 | 732 |
user.set_email_verified(True) |
733 |
if cell.header.name == 'phone' and cell.header.verified: |
|
734 |
user.phone_verified_on = now() |
|
719 | 735 |
cell.action = 'updated' |
720 | 736 |
continue |
721 | 737 |
cell.action = 'nothing' |
tests/test_csv_import.py | ||
---|---|---|
201 | 201 |
CsvHeader(1, 'email', field=True, key=True, verified=True), |
202 | 202 |
CsvHeader(2, 'first_name', field=True), |
203 | 203 |
CsvHeader(3, 'last_name', field=True), |
204 |
CsvHeader(4, 'phone', attribute=True),
|
|
204 |
CsvHeader(4, 'phone', field=True),
|
|
205 | 205 |
] |
206 | 206 |
assert importer.has_errors |
207 | 207 |
assert len(importer.rows) == 3 |
... | ... | |
211 | 211 |
assert all(error == Error('data-error') for error in importer.rows[2].cells[0].errors) |
212 | 212 |
assert not importer.rows[2].cells[1].errors |
213 | 213 |
assert not importer.rows[2].cells[2].errors |
214 |
assert not importer.rows[2].cells[3].errors
|
|
214 |
assert importer.rows[2].cells[3].errors |
|
215 | 215 |
assert all(error == Error('data-error') for error in importer.rows[2].cells[3].errors) |
216 | 216 | |
217 | 217 |
assert importer.updated == 0 |
... | ... | |
225 | 225 |
assert thomas.attributes.first_name == 'Thomas' |
226 | 226 |
assert thomas.last_name == 'Noël' |
227 | 227 |
assert thomas.attributes.last_name == 'Noël' |
228 |
assert thomas.attributes.phone == '1234' |
|
228 |
# phonenumbers' e.164 representation from a settings.DEFAULT_COUNTRY_CODE dial: |
|
229 |
assert thomas.attributes.phone == '+331234' |
|
229 | 230 |
assert thomas.password |
230 | 231 | |
231 | 232 |
fpeters = User.objects.get(email='fpeters@entrouvert.com') |
... | ... | |
235 | 236 |
assert fpeters.attributes.first_name == 'Frédéric' |
236 | 237 |
assert fpeters.last_name == 'Péters' |
237 | 238 |
assert fpeters.attributes.last_name == 'Péters' |
238 |
assert fpeters.attributes.phone == '5678' |
|
239 |
# phonenumbers' e.164 representation from a settings.DEFAULT_COUNTRY_CODE dial: |
|
240 |
assert fpeters.attributes.phone == '+335678' |
|
239 | 241 | |
240 | 242 | |
241 | 243 |
def test_simulate(profile, user_csv_importer_factory): |
... | ... | |
251 | 253 |
CsvHeader(1, 'email', field=True, key=True, verified=True), |
252 | 254 |
CsvHeader(2, 'first_name', field=True), |
253 | 255 |
CsvHeader(3, 'last_name', field=True), |
254 |
CsvHeader(4, 'phone', attribute=True),
|
|
256 |
CsvHeader(4, 'phone', field=True),
|
|
255 | 257 |
] |
256 | 258 |
assert importer.has_errors |
257 | 259 |
assert len(importer.rows) == 3 |
... | ... | |
277 | 279 |
importer = user_csv_importer_factory(content) |
278 | 280 | |
279 | 281 |
user = User.objects.create(ou=get_default_ou()) |
280 |
user.attributes.phone = '1234' |
|
282 |
user.attributes.phone = '+331234'
|
|
281 | 283 | |
282 | 284 |
assert importer.run() |
283 | 285 | |
... | ... | |
298 | 300 |
importer = user_csv_importer_factory(content) |
299 | 301 | |
300 | 302 |
user = User.objects.create() |
301 |
user.attributes.phone = '1234' |
|
303 |
user.attributes.phone = '+331234'
|
|
302 | 304 | |
303 | 305 |
assert importer.run() |
304 | 306 | |
... | ... | |
318 | 320 |
importer = user_csv_importer_factory(content) |
319 | 321 | |
320 | 322 |
user = User.objects.create() |
321 |
user.attributes.phone = '1234' |
|
323 |
user.attributes.phone = '+331234'
|
|
322 | 324 | |
323 | 325 |
assert importer.run() |
324 | 326 | |
... | ... | |
356 | 358 |
importer = user_csv_importer_factory(content) |
357 | 359 | |
358 | 360 |
user = User.objects.create(ou=get_default_ou()) |
359 |
user.attributes.phone = '1234' |
|
361 |
user.attributes.phone = '+331234'
|
|
360 | 362 | |
361 | 363 |
user = User.objects.create(email='tnoel@entrouvert.com', ou=get_default_ou()) |
362 | 364 | |
... | ... | |
378 | 380 |
importer = user_csv_importer_factory(content) |
379 | 381 | |
380 | 382 |
user = User.objects.create() |
381 |
user.attributes.phone = '1234' |
|
383 |
user.attributes.phone = '+331234'
|
|
382 | 384 | |
383 | 385 |
User.objects.create(email='tnoel@entrouvert.com', ou=get_default_ou()) |
384 | 386 | |
... | ... | |
400 | 402 |
importer = user_csv_importer_factory(content) |
401 | 403 | |
402 | 404 |
user = User.objects.create() |
403 |
user.attributes.phone = '1234' |
|
405 |
user.attributes.phone = '+331234'
|
|
404 | 406 | |
405 | 407 |
thomas = User.objects.create(email='tnoel@entrouvert.com', ou=get_default_ou()) |
406 | 408 | |
... | ... | |
418 | 420 |
thomas.refresh_from_db() |
419 | 421 |
assert not thomas.first_name |
420 | 422 |
assert not thomas.last_name |
421 |
assert thomas.attributes.phone == '1234' |
|
423 |
assert thomas.attributes.phone == '+331234'
|
|
422 | 424 | |
423 | 425 | |
424 | 426 |
def test_external_id(profile, user_csv_importer_factory): |
... | ... | |
436 | 438 |
CsvHeader(3, 'email', field=True, verified=True), |
437 | 439 |
CsvHeader(4, 'first_name', field=True), |
438 | 440 |
CsvHeader(5, 'last_name', field=True), |
439 |
CsvHeader(6, 'phone', attribute=True),
|
|
441 |
CsvHeader(6, 'phone', field=True),
|
|
440 | 442 |
] |
441 | 443 |
assert not importer.has_errors |
442 | 444 |
assert len(importer.rows) == 2 |
... | ... | |
449 | 451 |
assert thomas.attributes.first_name == 'Thomas' |
450 | 452 |
assert thomas.last_name == 'Noël' |
451 | 453 |
assert thomas.attributes.last_name == 'Noël' |
452 |
assert thomas.attributes.phone == '1234' |
|
454 |
assert thomas.attributes.phone == '+331234'
|
|
453 | 455 | |
454 | 456 |
importer = user_csv_importer_factory(content) |
455 | 457 |
assert importer.run(), importer.errors |
tests/test_user_manager.py | ||
---|---|---|
467 | 467 |
'users.csv', |
468 | 468 |
'''email key verified,first_name,last_name,phone |
469 | 469 |
tnoel@entrouvert.com,Thomas,Noël,1234 |
470 |
fpeters@entrouvert.com,Frédéric,Péters,5678 |
|
470 |
fpeters@entrouvert.com,Frédéric,Péters,+325678
|
|
471 | 471 |
john.doe@entrouvert.com,John,Doe,9101112 |
472 | 472 |
x,x,x,x'''.encode( |
473 | 473 |
encoding |
... | ... | |
529 | 529 |
assert len(response.pyquery('tr.row-cells-errors')) == 1 |
530 | 530 |
assert sum(bool(response.pyquery(td).text()) for td in response.pyquery('tr.row-cells-errors td li')) == 2 |
531 | 531 |
assert 'Enter a valid email address' in response.pyquery('tr.row-cells-errors td.cell-email li').text() |
532 |
assert 'Phone number can start with' in response.pyquery('tr.row-cells-errors td.cell-phone li').text()
|
|
532 |
assert 'Enter a valid phone number' in response.pyquery('tr.row-cells-errors td.cell-phone li').text()
|
|
533 | 533 | |
534 | 534 |
assert User.objects.count() == user_count |
535 | 535 | |
... | ... | |
548 | 548 |
email='tnoel@entrouvert.com', |
549 | 549 |
first_name='Thomas', |
550 | 550 |
last_name='Noël', |
551 |
attribute_values__content='1234', |
|
551 |
attribute_values__content='+331234',
|
|
552 | 552 |
).count() |
553 | 553 |
== 1 |
554 | 554 |
) |
... | ... | |
557 | 557 |
email='fpeters@entrouvert.com', |
558 | 558 |
first_name='Frédéric', |
559 | 559 |
last_name='Péters', |
560 |
attribute_values__content='5678', |
|
560 |
attribute_values__content='+325678',
|
|
561 | 561 |
).count() |
562 | 562 |
== 1 |
563 | 563 |
) |
... | ... | |
566 | 566 |
email='john.doe@entrouvert.com', |
567 | 567 |
first_name='John', |
568 | 568 |
last_name='Doe', |
569 |
attribute_values__content='9101112', |
|
569 |
attribute_values__content='+339101112',
|
|
570 | 570 |
).count() |
571 | 571 |
== 1 |
572 | 572 |
) |
... | ... | |
696 | 696 |
assert 'Enter a valid date.' in response.text |
697 | 697 |
assert 'birthdate must be in the past and greater or equal than 1900-01-01.' in response.text |
698 | 698 |
assert 'The value must be a valid french postcode' in response.text |
699 |
assert 'Phone number can start with a + and must contain only digits' in response.text |
|
700 | 699 | |
701 | 700 |
assert User.objects.count() == user_count + 1 |
702 | 701 |
elliot = User.objects.filter(email='elliot@universalpictures.com')[0] |
... | ... | |
706 | 705 |
assert elliot.attributes.values['saintsday'].content == '2019-07-20' |
707 | 706 |
assert elliot.attributes.values['birthdate'].content == '1972-05-26' |
708 | 707 |
assert elliot.attributes.values['zip'].content == '75014' |
709 |
assert elliot.attributes.values['phone'].content == '1234' |
|
708 |
assert elliot.attributes.values['phone'].content == '+331234'
|
|
710 | 709 | |
711 | 710 |
csv_lines[2] = "et@universalpictures.com,ET,the Extra-Terrestrial,,,,,,42000,+888 5678" |
712 | 711 |
response = import_csv('\n'.join(csv_lines), app) |
713 |
- |