Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: import contacts from remote CardDAV server #134

Open
wants to merge 12 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 104 additions & 39 deletions apps/personal/contacts/main.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,135 @@
# coding=utf-8
import argparse
import doctest

import os

from address_book import AddressBook, ZPUI_HOME, Contact
from apps import ZeroApp
from helpers import setup_logger
from ui import NumberedMenu, Listbox
from vcard_converter import VCardContactConverter
from ui import (NumberedMenu, Listbox, Menu, LoadingIndicator, DialogBox,
PrettyPrinter as Printer, UniversalInput)
from distutils.spawn import find_executable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import?


logger = setup_logger(__name__, "info")
from libs.address_book import AddressBook, Contact
from libs.webdav import vdirsyncer

logger = setup_logger(__name__, 'info')

class ContactApp(ZeroApp):
def __init__(self, i, o):
super(ContactApp, self).__init__(i, o)
self.menu_name = "Contacts"
self.menu_name = 'Contacts'
self.address_book = AddressBook()
self.menu = None

def on_start(self):
self.address_book.load_from_file()
self.menu = NumberedMenu(self.create_menu_content(), self.i, self.o, prepend_numbers=False)
self.reload()

def reload(self):
self.menu = NumberedMenu(self.build_main_menu_content(), self.i,
self.o, prepend_numbers=False)
self.menu.activate()

def create_menu_content(self):
def build_main_menu_content(self):
all_contacts = self.address_book.contacts
return [[c.short_name(), lambda x=c: self.create_contact_page(x)] for c in all_contacts]

def create_contact_page(self, contact):
# type: (Contact) -> None
contact_attrs = [getattr(contact, a) for a in contact.get_filled_attributes()]
Listbox(i=self.i, o=self.o, contents=contact_attrs).activate()


def find_contact_files(folder):
# type: (str) -> list(str)
home = os.path.expanduser(folder)
if not os.path.exists(home):
os.mkdir(home)
contact_card_files = [os.path.join(home, f) for f in os.listdir(home) if f.lower().endswith("vcf")]
return contact_card_files

menu_entries = [['|| Actions', lambda: self.open_actions_menu()]]
for c in all_contacts:
menu_entries.append([c.short_name(), lambda x=c:
self.open_contact_details_page(x)])

def load_vcf(folder):
# type: (str) -> None
contact_card_files = find_contact_files(folder)
contacts = VCardContactConverter.from_vcards(contact_card_files)

address_book = AddressBook()
for contact in contacts:
address_book.add_contact(contact)
address_book.save_to_file()
logger.info("Saved to {}".format(address_book.get_save_file_path()))
return menu_entries

def open_contact_details_page(self, contact):
# type: (Contact) -> None
contact_attrs = [getattr(contact, a) for a in
contact.get_filled_attributes()]
Listbox(contact_attrs, self.i, self.o).activate()

def open_actions_menu(self):
menu_contents = [
['CardDAV Setup Wizard', lambda: self.open_remote_setup_wizard()],
['CardDAV Sync', lambda: self.synchronize_carddav()],
['Reset address book', lambda: self.reset_addressbook()]
]
Menu(menu_contents, self.i, self.o, name='My menu').activate()

def reset_addressbook(self):
alert = 'This action will delete all of your contacts. Are you sure?'
do_reset = DialogBox('yc', self.i, self.o, message=alert,
name='Address book reset').activate()

if do_reset:
self.address_book.reset()
announce = 'All of your contacts were deleted.'
Printer(announce, self.i, self.o, sleep_time=2, skippable=True)
# Reload the now empty address book
self.reload()

def synchronize_carddav(self):
with LoadingIndicator(self.i, self.o, message='Syncing contacts'):
exit_status = vdirsyncer.sync('contacts')

if (exit_status != 0):
error_msg = "Error in contact synchronization. See ZPUI logs for \
details."
Printer(error_msg, self.i, self.o, sleep_time=3)
self.open_actions_menu()

with LoadingIndicator(self.i, self.o, message='Importing contacts'):
self.address_book.import_vcards_from_directory(
vdirsyncer.get_storage_directory_for('contacts')
)

# Reload the synced address book
self.reload()

def open_remote_setup_wizard(self):
# Define wizard fields
url_field = UniversalInput(self.i, self.o,
message='CardDAV URL:',
name='CardDAV URL field')
username_field = UniversalInput(self.i, self.o,
message='CardDAV Username:',
name='CardDAV username field')
password_field = UniversalInput(self.i, self.o,
message='CardDAV Password:',
name='CardDAV password field')

# Run wizard
url = url_field.activate()
username = username_field.activate()
password = password_field.activate()

# Update ZPUI vdirsyncer config, generate vdirsyncer config file
vdirsyncer.set_carddav_remote(url, username, password)
vdirsyncer.generate_config()

# Initialize vdirsyncer remote
with LoadingIndicator(self.i, self.o, message='Initializing remote'):
exit_status = vdirsyncer.discover('contacts')

if (exit_status != 0):
error_msg = "Error in remote initialization. Check vdirsyncer \
configuration"
Printer(error_msg, self.i, self.o, sleep_time=3)
return

# Synchronize contacts if the user request it
sync_now = DialogBox('yn', self.i, self.o,
message='Remote saved. Sync now?',
name='Sync synced contacts').activate()
if sync_now: self.synchronize_carddav()

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--src-folder', dest='src_folder', action='store', metavar="DIR",
logger.info('Generating vdirsyncer configuration')
parser.add_argument('-i', '--src-folder', dest='src_folder', action='store', metavar='DIR',
help='Folder to read vcard from', default=ZPUI_HOME)
parser.add_argument('-t', '--run-tests', dest='test', action='store_true', default=False)
arguments = parser.parse_args()

if arguments.test:
logger.info("Running tests...")
logger.info('Running tests...')
doctest.testmod()

load_vcf(arguments.src_folder)
address_book = AddressBook()
address_book.import_vcards_from_directory(arguments.src_folder)
address_book.save_to_file()
9 changes: 6 additions & 3 deletions helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from config_parse import read_config, write_config, read_or_create_config, save_config_gen, save_config_method_gen
from general import local_path_gen, flatten, Singleton
from config_parse import (read_config, write_config, read_or_create_config,
save_config_gen, save_config_method_gen)
from logger import setup_logger
from general import flatten, Singleton
from paths import (XDG_CACHE_HOME, XDG_CONFIG_HOME, XDG_DATA_HOME,
ZP_CACHE_DIR, ZP_CONFIG_DIR, ZP_DATA_DIR, local_path_gen)
from runners import BooleanEvent, Oneshot, BackgroundRunner
from usability import ExitHelper, remove_left_failsafe
from logger import setup_logger
25 changes: 0 additions & 25 deletions helpers/general.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,3 @@
import os
import sys


def local_path_gen(_name_):
"""This function generates a ``local_path`` function you can use
in your scripts to get an absolute path to a file in your app's
directory. You need to pass ``__name__`` to ``local_path_gen``. Example usage:

.. code-block:: python

from helpers import local_path_gen
local_path = local_path_gen(__name__)
...
config_path = local_path("config.json")

The resulting local_path function supports multiple arguments,
passing all of them to ``os.path.join`` internally."""
app_path = os.path.dirname(sys.modules[_name_].__file__)

def local_path(*path):
return os.path.join(app_path, *path)
return local_path


def flatten(foo):
for x in foo:
if hasattr(x, '__iter__'):
Expand Down
61 changes: 61 additions & 0 deletions helpers/paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Paths helpers

This module is used across ZPUI to obtain consistant path for configurations,
data and cache files.
"""

# Parts of this file are inspired from Scott Stevenson's xdg python package,
# licensed under the ISC licenses.
# See https://github.com/srstevenson/xdg/blob/3.0.2/xdg.py ;

import os
import sys

def _getenv(variable, default):
return os.environ.get(variable) or default

def _ensure_directory_exists(path):
if not os.path.exists(path):
os.makedirs(path, 0760)

if not os.path.isdir(path):
raise os.error('Expected a directory but found file instead.')

def _zp_dir(path):
path = os.path.join(path, 'zp')
_ensure_directory_exists(path)
return path

XDG_CACHE_HOME = _getenv(
"XDG_CACHE_HOME", os.path.expandvars(os.path.join("$HOME", ".cache"))
)
XDG_CONFIG_HOME = _getenv(
"XDG_CONFIG_HOME", os.path.expandvars(os.path.join("$HOME", ".config"))
)
XDG_DATA_HOME = _getenv(
"XDG_DATA_HOME",
os.path.expandvars(os.path.join("$HOME", ".local", "share")),
)
ZP_CACHE_DIR = _zp_dir(XDG_CACHE_HOME)
ZP_CONFIG_DIR = _zp_dir(XDG_CONFIG_HOME)
ZP_DATA_DIR = _zp_dir(XDG_DATA_HOME)

def local_path_gen(_name_):
"""This function generates a ``local_path`` function you can use
in your scripts to get an absolute path to a file in your app's
directory. You need to pass ``__name__`` to ``local_path_gen``. Example usage:

.. code-block:: python

from helpers import local_path_gen
local_path = local_path_gen(__name__)
...
config_path = local_path("config.json")

The resulting local_path function supports multiple arguments,
passing all of them to ``os.path.join`` internally."""
app_path = os.path.dirname(sys.modules[_name_].__file__)

def local_path(*path):
return os.path.join(app_path, *path)
return local_path
2 changes: 2 additions & 0 deletions libs/address_book/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from address_book import AddressBook
from contact import Contact
Loading