-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #144 from modoboa/feature/import_command
Added management command to import contacts from CSV
- Loading branch information
Showing
14 changed files
with
310 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import csv | ||
|
||
from modoboa_contacts.importer.backends.outlook import OutlookBackend | ||
|
||
|
||
BACKENDS = [ | ||
OutlookBackend, | ||
] | ||
|
||
|
||
def detect_import_backend(fp, delimiter: str = ";"): | ||
reader = csv.DictReader( | ||
fp, | ||
delimiter=delimiter, | ||
skipinitialspace=True | ||
) | ||
columns = reader.fieldnames | ||
rows = reader | ||
|
||
for backend in BACKENDS: | ||
if backend.detect_from_columns(columns): | ||
return backend, rows | ||
|
||
raise RuntimeError("Failed to detect backend to use") | ||
|
||
|
||
def import_csv_file(addressbook, | ||
csv_filename: str, | ||
delimiter: str, | ||
carddav_password: str = None): | ||
with open(csv_filename) as fp: | ||
backend, rows = detect_import_backend(fp, delimiter) | ||
backend(addressbook).proceed(rows, carddav_password) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
from modoboa_contacts import models, tasks | ||
|
||
|
||
class ImporterBackend: | ||
"""Base class of all importer backends.""" | ||
|
||
name: str = None | ||
|
||
field_names: dict = {} | ||
|
||
def __init__(self, addressbook: models.AddressBook): | ||
self.addressbook = addressbook | ||
|
||
@classmethod | ||
def detect_from_columns(cls, columns: list) -> bool: | ||
raise NotImplementedError | ||
|
||
def get_email(self, values: dict): | ||
return None | ||
|
||
def get_phone_number(self, values: dict): | ||
return None | ||
|
||
def import_contact(self, row) -> models.Contact: | ||
contact = models.Contact(addressbook=self.addressbook) | ||
for local_name, row_name in self.field_names.items(): | ||
method_name = f"get_{local_name}" | ||
if hasattr(self, method_name): | ||
value = getattr(self, method_name)(row) | ||
else: | ||
value = row[row_name] | ||
setattr(contact, local_name, value) | ||
contact.save() | ||
if self.get_email(row): | ||
models.EmailAddress.objects.create( | ||
contact=contact, address=self.get_email(row), type="work" | ||
) | ||
if self.get_phone_number(row): | ||
models.PhoneNumber.objects.create( | ||
contact=contact, number=self.get_phone_number(row), type="work" | ||
) | ||
return contact | ||
|
||
def proceed(self, rows: list, carddav_password: str = None): | ||
for row in rows: | ||
contact = self.import_contact(row) | ||
if carddav_password: | ||
# FIXME: refactor CDAV tasks to allow connection from | ||
# credentials and not only request | ||
clt = tasks.get_cdav_client( | ||
self.addressbook, | ||
self.addressbook.user.email, | ||
carddav_password, | ||
True | ||
) | ||
path, etag = clt.upload_new_card(contact.uid, contact.to_vcard()) | ||
contact.etag = etag | ||
contact.save(update_fields=["etag"]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from . import ImporterBackend | ||
|
||
|
||
OUTLOOK_COLUMNS = [ | ||
"First Name", | ||
"Middle Name", | ||
"Last Name", | ||
"Company", | ||
"E-mail Address", | ||
"Business Phone", | ||
"Business Street", | ||
"Business Street 2", | ||
"Business City", | ||
"Business State", | ||
"Business Postal Code" | ||
] | ||
|
||
|
||
class OutlookBackend(ImporterBackend): | ||
"""Outlook contact importer backend.""" | ||
|
||
name = "outlook" | ||
field_names = { | ||
"first_name": "", | ||
"last_name": "Last Name", | ||
"company": "Company", | ||
"address": "", | ||
"city": "Business City", | ||
"zipcode": "Business Postal Code", | ||
"state": "Business State", | ||
} | ||
|
||
@classmethod | ||
def detect_from_columns(cls, columns): | ||
return columns == OUTLOOK_COLUMNS | ||
|
||
def get_first_name(self, values: dict) -> str: | ||
result = values["First Name"] | ||
if values["Middle Name"]: | ||
result += f" {values['Middle Name']}" | ||
return result | ||
|
||
def get_address(self, values: dict) -> str: | ||
result = values["Business Street"] | ||
if values["Business Street 2"]: | ||
result += f" {values['Business Street 2']}" | ||
return result | ||
|
||
def get_email(self, values: dict) -> str: | ||
return values["E-mail Address"] | ||
|
||
def get_phone_number(self, values: dict) -> str: | ||
return values["Business Phone"] |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
"""Management command to import contacts from a CSV file.""" | ||
|
||
from django.core.management.base import BaseCommand, CommandError | ||
|
||
from modoboa_contacts import models | ||
from modoboa_contacts.importer import import_csv_file | ||
|
||
|
||
class Command(BaseCommand): | ||
"""Management command to import contacts.""" | ||
|
||
help = "Import contacts from a CSV file" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument( | ||
"--delimiter", type=str, default=",", | ||
help="Delimiter used in CSV file" | ||
) | ||
parser.add_argument( | ||
"--carddav-password", type=str, default=None, | ||
help=( | ||
"Password associated to email. If provided, imported " | ||
"contacts will be synced to CardDAV servert too" | ||
) | ||
) | ||
parser.add_argument( | ||
"email", type=str, | ||
help="Email address to import contacts for" | ||
) | ||
parser.add_argument( | ||
"file", type=str, | ||
help="Path of the CSV file to import" | ||
) | ||
|
||
def handle(self, *args, **options): | ||
addressbook = ( | ||
models.AddressBook.objects.filter( | ||
user__email=options["email"]).first() | ||
) | ||
if not addressbook: | ||
raise CommandError( | ||
"Address Book for email '%s' not found" % options["email"] | ||
) | ||
try: | ||
import_csv_file( | ||
addressbook, | ||
options["file"], | ||
options["delimiter"], | ||
options.get("carddav_password") | ||
) | ||
except RuntimeError as err: | ||
raise CommandError(err) | ||
self.stdout.write( | ||
self.style.SUCCESS("File was imported successfuly") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 4.2.5 on 2023-12-06 15:04 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('modoboa_contacts', '0006_alter_phonenumber_type'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='contact', | ||
name='address', | ||
field=models.TextField(blank=True), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
"First Name","Middle Name","Last Name","Company","E-mail Address","Business Phone","Business Street","Business Street 2","Business City","Business State","Business Postal Code" | ||
Toto,Tata,Titi,Company,[email protected],12345678,Street 1,Street 2,City,State,France |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
"First Name","Middle Name","Last Name","Company","E-mail Address","Business Phone","Business Street","Business Street 2","City","Business State","Business Postal Code" | ||
Toto,Tata,Titi,Company,[email protected],12345678,Street 1,Street 2,City,State,France |
Oops, something went wrong.