Exemple #1
0
def commit(args, modifier=None):
    from mstodo.api import tasks
    from mstodo.sync import background_sync

    task = _task(args)
    prefs = Preferences.current_prefs()

    prefs.last_taskfolder_id = task.list_id

    req = tasks.create_task(task.list_id,
                            task.title,
                            assignee_id=task.assignee_id,
                            recurrence_type=task.recurrence_type,
                            recurrence_count=task.recurrence_count,
                            due_date=task.due_date,
                            reminder_date=task.reminder_date,
                            starred=task.starred,
                            completed=task.completed,
                            note=task.note)

    if req.status_code == 201:
        print('The new task was created')
        # Output must be a UTF-8 encoded string
        print('The task was added to ' + task.list_title).encode('utf-8')
        if modifier == 'alt':
            import webbrowser
            webbrowser.open('ms-to-do://search/%s' % req.json()['title'])
        background_sync()
    elif req.status_code > 400:
        print(str(req.json()['error']['message']))
    else:
        print('Unknown API error. Please try again')
Exemple #2
0
def parsedatetime_constants():
    from parsedatetime import Constants
    from mstodo.models.preferences import Preferences

    loc = Preferences.current_prefs().date_locale or user_locale()

    return Constants(loc)
Exemple #3
0
    def reminder_date_combine(cls, date_component, time_component=None):
        """
        Returns a datetime based on the date portion of the date_component and
        the time portion of the time_component with special handling for an
        unspecified time_component. Based on the user preferences, a None
        time_component will result in either the default reminder time or an
        adjustment based on the current time if the reminder date is today.
        """
        prefs = Preferences.current_prefs()

        if isinstance(date_component, datetime):
            date_component = date_component.date()

        # Set a dynamic reminder date if due today
        if date_component == date.today(
        ) and time_component is None and prefs.reminder_today_offset:
            adjusted_now = datetime.now()
            adjusted_now -= timedelta(seconds=adjusted_now.second,
                                      microseconds=adjusted_now.microsecond)
            time_component = adjusted_now + prefs.reminder_today_offset_timedelta

            # "Round" to nearest 5 minutes, e.g. from :01 to :05, :44 to :45,
            # :50 to :50
            time_component += timedelta(
                minutes=(5 - time_component.minute % 5) % 5)

        # Default an unspecified time component on any day other than today to
        # the default reminder time
        if time_component is None:
            time_component = prefs.reminder_time

        if isinstance(time_component, datetime):
            time_component = time_component.time()

        return datetime.combine(date_component, time_component)
Exemple #4
0
def background_sync_if_necessary(seconds=30):
    last_sync = Preferences.current_prefs().last_sync

    # Avoid syncing on every keystroke, background_sync will also prevent
    # multiple concurrent syncs
    if last_sync is None or (datetime.utcnow() -
                             last_sync).total_seconds() > seconds:
        background_sync()
Exemple #5
0
    def sync_modified_tasks(cls, background=False):
        from mstodo.api import tasks
        from concurrent import futures
        from mstodo.models.preferences import Preferences
        from mstodo.models.hashtag import Hashtag
        start = time.time()
        instances = []
        all_tasks = []

        # Remove 60 seconds to make sure all recent tasks are included
        dt = Preferences.current_prefs().last_sync - timedelta(seconds=60)

        # run a single future for all tasks modified since last run
        with futures.ThreadPoolExecutor() as executor:
            job = executor.submit(lambda p: tasks.tasks(**p), {'dt':dt, 'afterdt':True})
            modified_tasks = job.result()

        # run a separate futures map over all taskfolders @TODO change this to be per taskfolder
        with futures.ThreadPoolExecutor(max_workers=4) as executor:
            jobs = (
                executor.submit(lambda p: tasks.tasks(**p), {'fields': _primary_api_fields, 'completed':True}),
                executor.submit(lambda p: tasks.tasks(**p), {'fields': _primary_api_fields, 'completed':False})
            )
            for job in futures.as_completed(jobs):
                all_tasks += job.result()
                log.debug(job.result())

        # if task in modified_tasks then remove from all taskfolder data
        modified_tasks_ids = [task['id'] for task in modified_tasks]
        for task in all_tasks:
            if task['id'] in modified_tasks_ids:
                all_tasks.remove(task)
        all_tasks.extend(modified_tasks)

        log.info('Retrieved all %d tasks including %d modifications since %s in %s', len(all_tasks), len(modified_tasks), dt, time.time() - start)
        start = time.time()

        try:
            # Pull instances from DB
            instances = cls.select(cls.id, cls.title, cls.changeKey)
        except PeeweeException:
            pass

        log.info('Loaded all %d tasks from the database in %s', len(instances), time.time() - start)
        start = time.time()

        all_tasks = cls.transform_datamodel(all_tasks)
        cls._perform_updates(instances, all_tasks)

        Hashtag.sync(background=background)

        log.info('Completed updates to tasks in %s', time.time() - start)

        return None
Exemple #6
0
def commit(args, modifier=None):
    relaunch_command = None
    prefs = Preferences.current_prefs()
    action = args[1]

    if action == 'duration':
        relaunch_command = 'td-completed '
        prefs.completed_duration = int(args[2])

    if relaunch_command:
        relaunch_alfred(relaunch_command)
Exemple #7
0
def icon_theme():
    global _icon_theme
    if not _icon_theme:
        prefs = Preferences.current_prefs()

        if prefs.icon_theme:
            _icon_theme = prefs.icon_theme
        else:
            _icon_theme = 'light' if alfred_is_dark() else 'dark'

    return _icon_theme
Exemple #8
0
def commit(args, modifier=None):
    action = args[1]
    prefs = Preferences.current_prefs()
    relaunch_command = None

    if action == 'sort' and len(args) > 2:
        command = args[2]

        if command == 'toggle-skipped':
            prefs.hoist_skipped_tasks = not prefs.hoist_skipped_tasks
            relaunch_command = 'td-due sort'
        else:
            try:
                index = int(command)
                order_info = _due_orders[index - 1]
                prefs.due_order = order_info['due_order']
                relaunch_command = 'td-due '
            except IndexError:
                pass
            except ValueError:
                pass

    if relaunch_command:
        relaunch_alfred(relaunch_command)
Exemple #9
0
def filter(args):
    wf = workflow()
    prefs = Preferences.current_prefs()
    command = args[1] if len(args) > 1 else None

    # Show sort options
    if command == 'sort':
        for i, order_info in enumerate(_due_orders):
            wf.add_item(order_info['title'],
                        order_info['subtitle'],
                        arg='-due sort %d' % (i + 1),
                        valid=True,
                        icon=icons.RADIO_SELECTED if order_info['due_order']
                        == prefs.due_order else icons.RADIO)

        wf.add_item(
            'Highlight skipped recurring tasks',
            'Hoists recurring tasks that have been missed multiple times over to the top',
            arg='-due sort toggle-skipped',
            valid=True,
            icon=icons.CHECKBOX_SELECTED
            if prefs.hoist_skipped_tasks else icons.CHECKBOX)

        wf.add_item('Back', autocomplete='-due ', icon=icons.BACK)

        return

    # Force a sync if not done recently or wait on the current sync
    if not prefs.last_sync or \
       datetime.utcnow() - prefs.last_sync > timedelta(seconds=30) or \
       is_running('sync'):
        sync()

    conditions = True

    # Build task title query based on the args
    for arg in args[1:]:
        if len(arg) > 1:
            conditions = conditions & (Task.title.contains(arg)
                                       | TaskFolder.title.contains(arg))

    if conditions is None:
        conditions = True

    tasks = Task.select().join(TaskFolder).where(
        (Task.status != 'completed')
        & (Task.dueDateTime < datetime.now() + timedelta(days=1))
        & Task.list_id.is_null(False) & conditions)

    # Sort the tasks according to user preference
    for key in prefs.due_order:
        order = 'asc'
        field = None
        if key[0] == '-':
            order = 'desc'
            key = key[1:]

        if key == 'due_date':
            field = Task.dueDateTime
        elif key == 'taskfolder.id':
            field = TaskFolder.id
        elif key == 'order':
            field = Task.lastModifiedDateTime

        if field:
            if order == 'asc':
                tasks = tasks.order_by(field.asc())
            else:
                tasks = tasks.order_by(field.desc())

    try:
        if prefs.hoist_skipped_tasks:
            log.debug('hoisting skipped tasks')
            tasks = sorted(tasks, key=lambda t: -t.overdue_times)

        for t in tasks:
            wf.add_item(u'%s – %s' % (t.list_title, t.title),
                        t.subtitle(),
                        autocomplete='-task %s ' % t.id,
                        icon=icons.TASK_COMPLETED
                        if t.status == 'completed' else icons.TASK)
    except OperationalError:
        background_sync()

    wf.add_item(u'Sort order',
                'Change the display order of due tasks',
                autocomplete='-due sort',
                icon=icons.SORT)

    wf.add_item('Main menu', autocomplete='', icon=icons.BACK)

    # Make sure tasks stay up-to-date
    background_sync_if_necessary(seconds=2)
Exemple #10
0
def filter(args):
    task = _task(args)
    subtitle = task_subtitle(task)
    wf = workflow()
    matching_hashtags = []

    if not task.title:
        subtitle = 'Begin typing to add a new task'

    # Preload matching hashtags into a list so that we can get the length
    if task.has_hashtag_prompt:
        from mstodo.models.hashtag import Hashtag

        hashtags = Hashtag.select().where(
            Hashtag.id.contains(task.hashtag_prompt.lower())).order_by(
                fn.Lower(Hashtag.tag).asc())

        for hashtag in hashtags:
            matching_hashtags.append(hashtag)

    # Show hashtag prompt if there is more than one matching hashtag or the
    # hashtag being typed does not exactly match the single matching hashtag
    if task.has_hashtag_prompt and len(matching_hashtags) > 0 and (
            len(matching_hashtags) > 1
            or task.hashtag_prompt != matching_hashtags[0].tag):
        for hashtag in matching_hashtags:
            wf.add_item(hashtag.tag[1:],
                        '',
                        autocomplete=' ' +
                        task.phrase_with(hashtag=hashtag.tag) + ' ',
                        icon=icons.HASHTAG)

    elif task.has_list_prompt:
        taskfolders = wf.stored_data('taskfolders')
        if taskfolders:
            for taskfolder in taskfolders:
                # Show some full list names and some concatenated in command
                # suggestions
                sample_command = taskfolder['title']
                if random() > 0.5:
                    sample_command = sample_command[:int(
                        len(sample_command) * .75)]
                icon = icons.INBOX if taskfolder[
                    'isDefaultFolder'] else icons.LIST
                wf.add_item(taskfolder['title'],
                            'Assign task to this folder, e.g. %s: %s' %
                            (sample_command.lower(), task.title),
                            autocomplete=' ' +
                            task.phrase_with(list_title=taskfolder['title']),
                            icon=icon)
            wf.add_item('Remove folder',
                        'Tasks without a folder are added to the Inbox',
                        autocomplete=' ' + task.phrase_with(list_title=False),
                        icon=icons.CANCEL)
        elif is_running('sync'):
            wf.add_item('Your folders are being synchronized',
                        'Please try again in a few moments',
                        autocomplete=' ' + task.phrase_with(list_title=False),
                        icon=icons.BACK)

    # Task has an unfinished recurrence phrase
    elif task.has_recurrence_prompt:
        wf.add_item('Every month',
                    'Same day every month, e.g. every mo',
                    uid="recurrence_1m",
                    autocomplete=' %s ' %
                    task.phrase_with(recurrence='every month'),
                    icon=icons.RECURRENCE)
        wf.add_item('Every week',
                    'Same day every week, e.g. every week, every Tuesday',
                    uid="recurrence_1w",
                    autocomplete=' %s ' %
                    task.phrase_with(recurrence='every week'),
                    icon=icons.RECURRENCE)
        wf.add_item('Every year',
                    'Same date every year, e.g. every 1 y, every April 15',
                    uid="recurrence_1y",
                    autocomplete=' %s ' %
                    task.phrase_with(recurrence='every year'),
                    icon=icons.RECURRENCE)
        wf.add_item('Every 3 months',
                    'Same day every 3 months, e.g. every 3 months',
                    uid="recurrence_3m",
                    autocomplete=' %s ' %
                    task.phrase_with(recurrence='every 3 months'),
                    icon=icons.RECURRENCE)
        wf.add_item('Remove recurrence',
                    autocomplete=' ' + task.phrase_with(recurrence=False),
                    icon=icons.CANCEL)

    # Task has an unfinished due date phrase
    elif task.has_due_date_prompt:
        wf.add_item('Today',
                    'e.g. due today',
                    autocomplete=' %s ' %
                    task.phrase_with(due_date='due today'),
                    icon=icons.TODAY)
        wf.add_item('Tomorrow',
                    'e.g. due tomorrow',
                    autocomplete=' %s ' %
                    task.phrase_with(due_date='due tomorrow'),
                    icon=icons.TOMORROW)
        wf.add_item('Next Week',
                    'e.g. due next week',
                    autocomplete=' %s ' %
                    task.phrase_with(due_date='due next week'),
                    icon=icons.NEXT_WEEK)
        wf.add_item('Next Month',
                    'e.g. due next month',
                    autocomplete=' %s ' %
                    task.phrase_with(due_date='due next month'),
                    icon=icons.CALENDAR)
        wf.add_item('Next Year',
                    'e.g. due next year, due April 15',
                    autocomplete=' %s ' %
                    task.phrase_with(due_date='due next year'),
                    icon=icons.CALENDAR)
        wf.add_item('Remove due date',
                    'Add "not due" to fix accidental dates, or see td-pref',
                    autocomplete=' ' + task.phrase_with(due_date=False),
                    icon=icons.CANCEL)

    # Task has an unfinished reminder phrase
    elif task.has_reminder_prompt:
        prefs = Preferences.current_prefs()
        default_reminder_time = format_time(prefs.reminder_time, 'short')
        due_date_hint = ' on the due date' if task.due_date else ''
        wf.add_item(
            'Reminder at %s%s' % (default_reminder_time, due_date_hint),
            'e.g. r %s' % default_reminder_time,
            autocomplete=' %s ' %
            task.phrase_with(reminder_date='remind me at %s' %
                             format_time(prefs.reminder_time, 'short')),
            icon=icons.REMINDER)
        wf.add_item('At noon%s' % due_date_hint,
                    'e.g. reminder noon',
                    autocomplete=' %s ' %
                    task.phrase_with(reminder_date='remind me at noon'),
                    icon=icons.REMINDER)
        wf.add_item('At 8:00 PM%s' % due_date_hint,
                    'e.g. remind at 8:00 PM',
                    autocomplete=' %s ' %
                    task.phrase_with(reminder_date='remind me at 8:00pm'),
                    icon=icons.REMINDER)
        wf.add_item('At dinner%s' % due_date_hint,
                    'e.g. alarm at dinner',
                    autocomplete=' %s ' %
                    task.phrase_with(reminder_date='remind me at dinner'),
                    icon=icons.REMINDER)
        wf.add_item(
            'Today at 6:00 PM',
            'e.g. remind me today at 6pm',
            autocomplete=' %s ' %
            task.phrase_with(reminder_date='remind me today at 6:00pm'),
            icon=icons.REMINDER)
        wf.add_item('Remove reminder',
                    autocomplete=' ' + task.phrase_with(reminder_date=False),
                    icon=icons.CANCEL)

    # Main menu for tasks
    else:
        wf.add_item(task.list_title + u' – create a new task...',
                    subtitle,
                    modifier_subtitles={
                        'alt':
                        u'…then edit it in the ToDo app    %s' % subtitle
                    },
                    arg='--stored-query',
                    valid=task.title != '',
                    icon=icons.TASK)

        title = 'Change folder' if task.list_title else 'Select a folder'
        wf.add_item(title,
                    'Prefix the task, e.g. Automotive: ' + task.title,
                    autocomplete=' ' + task.phrase_with(list_title=True),
                    icon=icons.LIST)

        title = 'Change the due date' if task.due_date else 'Set a due date'
        wf.add_item(
            title,
            '"due" followed by any date-related phrase, e.g. due next Tuesday; due May 4',
            autocomplete=' ' + task.phrase_with(due_date=True),
            icon=icons.CALENDAR)

        title = 'Change the recurrence' if task.recurrence_type else 'Make it a recurring task'
        wf.add_item(
            title,
            '"every" followed by a unit of time, e.g. every 2 months; every year; every 4w',
            autocomplete=' ' + task.phrase_with(recurrence=True),
            icon=icons.RECURRENCE)

        title = 'Change the reminder' if task.reminder_date else 'Set a reminder'
        wf.add_item(
            title,
            '"remind me" followed by a time and/or date, e.g. remind me at noon; r 10am; alarm 8:45p',
            autocomplete=' ' + task.phrase_with(reminder_date=True),
            icon=icons.REMINDER)

        if task.starred:
            wf.add_item('Remove star',
                        'Remove * from the task',
                        autocomplete=' ' + task.phrase_with(starred=False),
                        icon=icons.STAR_REMOVE)
        else:
            wf.add_item('Star',
                        'End the task with * (asterisk)',
                        autocomplete=' ' + task.phrase_with(starred=True),
                        icon=icons.STAR)

        wf.add_item('Main menu', autocomplete='', icon=icons.BACK)
Exemple #11
0
def filter(args):
    wf = workflow()
    prefs = Preferences.current_prefs()
    command = args[1] if len(args) > 1 else None
    duration_info = _duration_info(prefs.completed_duration)

    if command == 'duration':
        selected_duration = prefs.completed_duration

        # Apply selected duration option
        if len(args) > 2:
            try:
                selected_duration = int(args[2])
            except:
                pass

        duration_info = _duration_info(selected_duration)

        if 'custom' in duration_info:
            wf.add_item(duration_info['label'],
                        duration_info['subtitle'],
                        arg='-completed duration %d' % (duration_info['days']),
                        valid=True,
                        icon=icons.RADIO_SELECTED if duration_info['days']
                        == selected_duration else icons.RADIO)

        for duration_info in _durations:
            wf.add_item(duration_info['label'],
                        duration_info['subtitle'],
                        arg='-completed duration %d' % (duration_info['days']),
                        valid=True,
                        icon=icons.RADIO_SELECTED if duration_info['days']
                        == selected_duration else icons.RADIO)

        wf.add_item('Back', autocomplete='-completed ', icon=icons.BACK)

        return

    # Force a sync if not done recently or join if already running
    if not prefs.last_sync or \
       datetime.utcnow() - prefs.last_sync > timedelta(seconds=30) or \
       is_running('sync'):
        sync()

    wf.add_item(duration_info['label'],
                subtitle='Change the duration for completed tasks',
                autocomplete='-completed duration ',
                icon=icons.YESTERDAY)

    conditions = True

    # Build task title query based on the args
    for arg in args[1:]:
        if len(arg) > 1:
            conditions = conditions & (Task.title.contains(arg)
                                       | TaskFolder.title.contains(arg))

    if conditions is None:
        conditions = True

    tasks = Task.select().join(TaskFolder).where(
        (Task.completedDateTime > date.today() - timedelta(days=duration_info['days'])) &
        Task.list.is_null(False) &
        conditions
    )\
        .order_by(Task.completedDateTime.desc(), Task.reminderDateTime.asc(), Task.changeKey.asc())

    try:
        for t in tasks:
            wf.add_item(u'%s – %s' % (t.list_title, t.title),
                        t.subtitle(),
                        autocomplete='-task %s ' % t.id,
                        icon=icons.TASK_COMPLETED
                        if t.status == 'completed' else icons.TASK)
    except OperationalError:
        background_sync()

    wf.add_item('Main menu', autocomplete='', icon=icons.BACK)

    # Make sure tasks stay up-to-date
    background_sync_if_necessary(seconds=2)
Exemple #12
0
def filter(args):
    prefs = Preferences.current_prefs()

    if 'reminder' in args:
        reminder_time = _parse_time(' '.join(args))

        if reminder_time is not None:
            workflow().add_item('Change default reminder time',
                                u'⏰ %s' % format_time(reminder_time, 'short'),
                                arg=' '.join(args),
                                valid=True,
                                icon=icons.REMINDER)
        else:
            workflow().add_item(
                'Type a new reminder time',
                'Date offsets like the morning before the due date are not supported yet',
                valid=False,
                icon=icons.REMINDER)

        workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK)
    elif 'reminder_today' in args:
        reminder_today_offset = _parse_time(' '.join(args))

        if reminder_today_offset is not None:
            workflow().add_item('Set a custom reminder offset',
                                u'⏰ now + %s' %
                                _format_time_offset(reminder_today_offset),
                                arg=' '.join(args),
                                valid=True,
                                icon=icons.REMINDER)
        else:
            workflow().add_item('Type a custom reminder offset',
                                'Use the formats hh:mm or 2h 5m',
                                valid=False,
                                icon=icons.REMINDER)

        workflow().add_item('30 minutes',
                            arg='-pref reminder_today 30m',
                            valid=True,
                            icon=icons.REMINDER)

        workflow().add_item('1 hour',
                            '(default)',
                            arg='-pref reminder_today 1h',
                            valid=True,
                            icon=icons.REMINDER)

        workflow().add_item('90 minutes',
                            arg='-pref reminder_today 90m',
                            valid=True,
                            icon=icons.REMINDER)

        workflow().add_item(
            'Always use the default reminder time',
            'Avoids adjusting the reminder based on the current date',
            arg='-pref reminder_today disabled',
            valid=True,
            icon=icons.CANCEL)

        workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK)
    elif 'default_folder' in args:
        taskfolders = workflow().stored_data('taskfolders')
        matching_taskfolders = taskfolders

        if len(args) > 2:
            taskfolder_query = ' '.join(args[2:])
            if taskfolder_query:
                matching_taskfolders = workflow().filter(
                    taskfolder_query,
                    taskfolders,
                    lambda f: f['title'],
                    # Ignore MATCH_ALLCHARS which is expensive and inaccurate
                    match_on=MATCH_ALL ^ MATCH_ALLCHARS)

        for i, f in enumerate(matching_taskfolders):
            if i == 1:
                workflow().add_item(
                    'Most recently used folder',
                    'Default to the last folder to which a task was added',
                    arg='-pref default_folder %s' %
                    DEFAULT_TASKFOLDER_MOST_RECENT,
                    valid=True,
                    icon=icons.RECURRENCE)
            icon = icons.INBOX if f['isDefaultFolder'] else icons.LIST
            workflow().add_item(f['title'],
                                arg='-pref default_folder %s' % f['id'],
                                valid=True,
                                icon=icon)

        workflow().add_item('Cancel', autocomplete='-pref', icon=icons.BACK)
    else:
        current_user = None
        taskfolders = workflow().stored_data('taskfolders')
        loc = user_locale()
        default_folder_name = 'Tasks'

        try:
            current_user = User.get()
        except User.DoesNotExist:
            pass
        except OperationalError:
            from mstodo.sync import background_sync
            background_sync()

        if prefs.default_taskfolder_id == DEFAULT_TASKFOLDER_MOST_RECENT:
            default_folder_name = 'Most recent folder'
        else:
            default_taskfolder_id = prefs.default_taskfolder_id
            default_folder_name = next(
                (f['title']
                 for f in taskfolders if f['id'] == default_taskfolder_id),
                'Tasks')

        if current_user and current_user.name:
            workflow().add_item('Sign out',
                                'You are logged in as ' + current_user.name,
                                autocomplete='-logout',
                                icon=icons.CANCEL)

        workflow().add_item('Show completed tasks',
                            'Includes completed tasks in search results',
                            arg='-pref show_completed_tasks',
                            valid=True,
                            icon=icons.TASK_COMPLETED
                            if prefs.show_completed_tasks else icons.TASK)

        workflow().add_item(
            'Default reminder time',
            u'⏰ %s    Reminders without a specific time will be set to this time'
            % format_time(prefs.reminder_time, 'short'),
            autocomplete='-pref reminder ',
            icon=icons.REMINDER)

        workflow().add_item(
            'Default reminder when due today',
            u'⏰ %s    Default reminder time for tasks due today is %s' %
            (_format_time_offset(prefs.reminder_today_offset),
             'relative to the current time' if prefs.reminder_today_offset else
             'always %s' % format_time(prefs.reminder_time, 'short')),
            autocomplete='-pref reminder_today ',
            icon=icons.REMINDER)

        workflow().add_item(
            'Default folder',
            u'%s    Change the default folder when creating new tasks' %
            default_folder_name,
            autocomplete='-pref default_folder ',
            icon=icons.LIST)

        workflow().add_item(
            'Automatically set a reminder on the due date',
            u'Sets a default reminder for tasks with a due date.',
            arg='-pref automatic_reminders',
            valid=True,
            icon=icons.TASK_COMPLETED
            if prefs.automatic_reminders else icons.TASK)

        if loc != 'en_US' or prefs.date_locale:
            workflow().add_item('Force US English for dates',
                                'Rather than the current locale (%s)' % loc,
                                arg='-pref force_en_US',
                                valid=True,
                                icon=icons.TASK_COMPLETED if prefs.date_locale
                                == 'en_US' else icons.TASK)

        workflow().add_item(
            'Require explicit due keyword',
            'Requires the due keyword to avoid accidental due date extraction',
            arg='-pref explicit_keywords',
            valid=True,
            icon=icons.TASK_COMPLETED
            if prefs.explicit_keywords else icons.TASK)

        workflow().add_item(
            'Check for experimental updates to this workflow',
            'The workflow automatically checks for updates; enable this to include pre-releases',
            arg=':pref prerelease_channel',
            valid=True,
            icon=icons.TASK_COMPLETED
            if prefs.prerelease_channel else icons.TASK)

        workflow().add_item(
            'Force sync',
            'The workflow syncs automatically, but feel free to be forcible.',
            arg='-pref sync',
            valid=True,
            icon=icons.SYNC)

        workflow().add_item('Switch theme',
                            'Toggle between light and dark icons',
                            arg='-pref retheme',
                            valid=True,
                            icon=icons.PAINTBRUSH)

        workflow().add_item('Main menu', autocomplete='', icon=icons.BACK)
Exemple #13
0
def commit(args, modifier=None):
    prefs = Preferences.current_prefs()
    relaunch_command = '-pref'

    if '--alfred' in args:
        relaunch_command = ' '.join(args[args.index('--alfred') + 1:])

    if 'sync' in args:
        from mstodo.sync import sync
        sync('background' in args)

        relaunch_command = None
    elif 'show_completed_tasks' in args:
        prefs.show_completed_tasks = not prefs.show_completed_tasks

        if prefs.show_completed_tasks:
            print('Completed tasks are now visible in the workflow')
        else:
            print('Completed tasks will not be visible in the workflow')
    elif 'default_folder' in args:
        default_taskfolder_id = None
        taskfolders = workflow().stored_data('taskfolders')

        if len(args) > 2:
            default_taskfolder_id = args[2]

        prefs.default_taskfolder_id = default_taskfolder_id

        if default_taskfolder_id:
            default_folder_name = next(
                (f['title']
                 for f in taskfolders if f['id'] == default_taskfolder_id),
                'most recent')
            print('Tasks will be added to your %s folder by default' %
                  default_folder_name)
        else:
            print('Tasks will be added to the Tasks folder by default')
    elif 'explicit_keywords' in args:
        prefs.explicit_keywords = not prefs.explicit_keywords

        if prefs.explicit_keywords:
            print('Remember to use the "due" keyword')
        else:
            print('Implicit due dates enabled (e.g. "Recycling tomorrow")')
    elif 'reminder' in args:
        reminder_time = _parse_time(' '.join(args))

        if reminder_time is not None:
            prefs.reminder_time = reminder_time

            print('Reminders will now default to %s' %
                  format_time(reminder_time, 'short'))
    elif 'reminder_today' in args:
        reminder_today_offset = None

        if not 'disabled' in args:
            reminder_today_offset = _parse_time(' '.join(args))

        prefs.reminder_today_offset = reminder_today_offset

        print('The offset for current-day reminders is now %s' %
              _format_time_offset(reminder_today_offset))
    elif 'automatic_reminders' in args:
        prefs.automatic_reminders = not prefs.automatic_reminders

        if prefs.automatic_reminders:
            print('A reminder will automatically be set for due tasks')
        else:
            print('A reminder will not be added automatically')
    elif 'retheme' in args:
        prefs.icon_theme = 'light' if icons.icon_theme() == 'dark' else 'dark'

        print('The workflow is now using the %s icon theme' %
              (prefs.icon_theme))
    elif 'prerelease_channel' in args:

        prefs.prerelease_channel = not prefs.prerelease_channel

        # Update the workflow settings and reverify the update data
        workflow().check_update(True)

        if prefs.prerelease_channel:
            print(
                'The workflow will prompt you to update to experimental pre-releases'
            )
        else:
            print(
                'The workflow will only prompt you to update to final releases'
            )
    elif 'force_en_US' in args:
        if prefs.date_locale:
            prefs.date_locale = None
            print(
                'The workflow will expect your local language and date format')
        else:
            prefs.date_locale = 'en_US'
            print('The workflow will expect dates in US English')

    if relaunch_command:
        relaunch_alfred('td%s' % relaunch_command)
Exemple #14
0
def filter(args):
    query = ' '.join(args[1:])
    wf = workflow()
    prefs = Preferences.current_prefs()
    matching_hashtags = []

    if not query:
        wf.add_item('Begin typing to search tasks', '', icon=icons.SEARCH)

    hashtag_match = re.search(_hashtag_prompt_pattern, query)
    if hashtag_match:
        from mstodo.models.hashtag import Hashtag

        hashtag_prompt = hashtag_match.group().lower()
        hashtags = Hashtag.select().where(
            Hashtag.id.contains(hashtag_prompt)).order_by(
                fn.Lower(Hashtag.tag).asc())

        for hashtag in hashtags:
            # If there is an exact match, do not show hashtags
            if hashtag.id == hashtag_prompt:
                matching_hashtags = []
                break

            matching_hashtags.append(hashtag)

    # Show hashtag prompt if there is more than one matching hashtag or the
    # hashtag being typed does not exactly match the single matching hashtag
    if len(matching_hashtags) > 0:
        for hashtag in matching_hashtags:
            wf.add_item(hashtag.tag[1:],
                        '',
                        autocomplete=u'-search %s%s ' %
                        (query[:hashtag_match.start()], hashtag.tag),
                        icon=icons.HASHTAG)

    else:
        conditions = True
        taskfolders = workflow().stored_data('taskfolders')
        matching_taskfolders = None
        query = ' '.join(args[1:]).strip()
        taskfolder_query = None

        # Show all task folders on the main search screen
        if not query:
            matching_taskfolders = taskfolders
        # Filter task folders when colon is used
        if ':' in query:
            matching_taskfolders = taskfolders
            components = re.split(r':\s*', query, 1)
            taskfolder_query = components[0]
            if taskfolder_query:
                matching_taskfolders = workflow().filter(
                    taskfolder_query,
                    taskfolders if taskfolders else [],
                    lambda f: f['title'],
                    # Ignore MATCH_ALLCHARS which is expensive and inaccurate
                    match_on=MATCH_ALL ^ MATCH_ALLCHARS)

                # If no matching task folder search against all tasks
                if matching_taskfolders:
                    query = components[1] if len(components) > 1 else ''

                # If there is a task folder exactly matching the query ignore
                # anything else. This takes care of taskfolders that are substrings
                # of other taskfolders
                if len(matching_taskfolders) > 1:
                    for f in matching_taskfolders:
                        if f['title'].lower() == taskfolder_query.lower():
                            matching_taskfolders = [f]
                            break

        if matching_taskfolders:
            if not taskfolder_query:
                wf.add_item('Browse by hashtag',
                            autocomplete='-search #',
                            icon=icons.HASHTAG)

            if len(matching_taskfolders) > 1:
                for f in matching_taskfolders:
                    icon = icons.INBOX if f['isDefaultFolder'] else icons.LIST
                    wf.add_item(f['title'],
                                autocomplete='-search %s: ' % f['title'],
                                icon=icon)
            else:
                conditions = conditions & (Task.list
                                           == matching_taskfolders[0]['id'])

        if not matching_taskfolders or len(matching_taskfolders) <= 1:
            for arg in query.split(' '):
                if len(arg) > 1:
                    conditions = conditions & (Task.title.contains(arg) |
                                               TaskFolder.title.contains(arg))

            if conditions:
                if not prefs.show_completed_tasks:
                    conditions = (Task.status != 'completed') & conditions

                tasks = Task.select().where(
                    Task.list.is_null(False) & conditions)

                tasks = tasks.join(TaskFolder).order_by(
                    Task.lastModifiedDateTime.desc(),
                    TaskFolder.changeKey.asc())

                # Avoid excessive results
                tasks = tasks.limit(50)

                try:
                    for t in tasks:
                        wf.add_item(u'%s – %s' % (t.list_title, t.title),
                                    t.subtitle(),
                                    autocomplete='-task %s  ' % t.id,
                                    icon=icons.TASK_COMPLETED
                                    if t.status == 'completed' else icons.TASK)
                except OperationalError:
                    background_sync()

            if prefs.show_completed_tasks:
                wf.add_item('Hide completed tasks',
                            arg='-pref show_completed_tasks --alfred %s' %
                            ' '.join(args),
                            valid=True,
                            icon=icons.HIDDEN)
            else:
                wf.add_item('Show completed tasks',
                            arg='-pref show_completed_tasks --alfred %s' %
                            ' '.join(args),
                            valid=True,
                            icon=icons.VISIBLE)

        wf.add_item('New search', autocomplete='-search ', icon=icons.CANCEL)
        wf.add_item('Main menu', autocomplete='', icon=icons.BACK)

        # Make sure tasks are up-to-date while searching
        background_sync()
Exemple #15
0
def sync(background=False):
    log.info('running mstodo/sync')
    from mstodo.models import base, task, user, taskfolder, hashtag
    from peewee import OperationalError

    # If a sync is already running, wait for it to finish. Otherwise, store
    # the current pid in alfred-workflow's pid cache file
    if not background:
        if is_running('sync'):
            wait_count = 0
            while is_running('sync'):
                time.sleep(.25)
                wait_count += 1

                if wait_count == 2:
                    notify(
                        'Please wait...',
                        'The workflow is making sure your tasks are up-to-date'
                    )

            return False

        pidfile = workflow().cachefile('sync.pid')
        with open(pidfile, 'wb') as file_obj:
            file_obj.write('{0}'.format(os.getpid()))

    base.BaseModel._meta.database.create_tables(
        [taskfolder.TaskFolder, task.Task, user.User, hashtag.Hashtag],
        safe=True)

    # Perform a query that requires the latest schema; if it fails due to a
    # mismatched scheme, delete the old database and re-sync
    try:
        task.Task.select().where(task.Task.recurrence_count > 0).count()
        hashtag.Hashtag.select().where(hashtag.Hashtag.tag == '').count()
    except OperationalError:
        base.BaseModel._meta.database.close()
        workflow().clear_data(lambda f: 'mstodo.db' in f)

        # Make sure that this sync does not try to wait until its own process
        # finishes
        sync(background=True)
        return

    first_sync = False

    try:
        # get root item from DB. If it doesn't exist then make this the first sync.
        user.User.get()
    except user.User.DoesNotExist:
        first_sync = True
        Preferences.current_prefs().last_sync = datetime.utcnow()
        notify('Please wait...',
               'The workflow is syncing tasks for the first time')

    user.User.sync(background=background)
    taskfolder.TaskFolder.sync(background=background)
    if first_sync:
        task.Task.sync_all_tasks(background=background)
    else:
        task.Task.sync_modified_tasks(background=background)

    if background:
        if first_sync:
            notify('Initial sync has completed',
                   'All of your tasks are now available for browsing')

        # If executed manually, this will pass on to the post notification action
        print('Sync completed successfully')

    log.debug('First sync: ' + str(first_sync))
    log.debug('Last sync time: ' + str(Preferences.current_prefs().last_sync))
    Preferences.current_prefs().last_sync = datetime.utcnow()
    log.debug('This sync time: ' + str(Preferences.current_prefs().last_sync))
    return True
Exemple #16
0
    def _parse(self):
        cls = type(self)
        phrase = self.phrase
        cal = parsedatetime_calendar()
        wf = workflow()
        taskfolders = wf.stored_data('taskfolders')
        prefs = Preferences.current_prefs()
        ignore_due_date = False

        match = re.search(HASHTAG_PROMPT_PATTERN, phrase)
        if match:
            self.hashtag_prompt = match.group(1)
            self.has_hashtag_prompt = True

        match = re.search(SLASHES_PATTERN, phrase)
        if match:
            self._note_phrase = match.group(1) + match.group(2)
            self.note = re.sub(WHITESPACE_CLEANUP_PATTERN, ' ',
                               match.group(2)).strip()
            phrase = phrase[:match.start()] + phrase[match.end():]

        match = re.search(STAR_PATTERN, phrase)
        if match:
            self.starred = True
            self._starred_phrase = match.group()
            phrase = phrase[:match.start()] + phrase[match.end():]

        match = re.search(NOT_DUE_PATTERN, phrase)
        if match:
            ignore_due_date = True
            phrase = phrase[:match.start()] + phrase[match.end():]

        match = re.search(LIST_TITLE_PATTERN, phrase)
        if taskfolders and match:
            if match.group(1):
                matching_taskfolders = wf.filter(
                    match.group(1),
                    taskfolders,
                    lambda l: l['title'],
                    # Ignore MATCH_ALLCHARS which is expensive and inaccurate
                    match_on=MATCH_ALL ^ MATCH_ALLCHARS)

                # Take the first match as the desired list
                if matching_taskfolders:
                    self.list_id = matching_taskfolders[0]['id']
                    self.list_title = matching_taskfolders[0]['title']
            # The list name was empty
            else:
                self.has_list_prompt = True

            if self.list_title or self.has_list_prompt:
                self._list_phrase = match.group()
                phrase = phrase[:match.start()] + phrase[match.end():]

        # Parse and remove the recurrence phrase first so that any dates do
        # not interfere with the due date
        match = re.search(RECURRENCE_PATTERN, phrase)
        if match:
            type_phrase = match.group(2) if match.group(2) else match.group(3)
            if type_phrase:
                # Look up the recurrence type based on the first letter of the
                # work or abbreviation used in the phrase
                self.recurrence_type = RECURRENCE_TYPES[type_phrase[0].lower()]
                self.recurrence_count = int(match.group(1) or 1)
            else:
                match = re.search(RECURRENCE_BY_DATE_PATTERN, phrase)
                if match:
                    recurrence_phrase = match.group()
                    dates = cal.nlp(match.group(1), version=2)

                    if dates:
                        # Only remove the first date following `every`
                        datetime_info = dates[0]
                        # Set due_date if a datetime was found and it is not time only
                        if datetime_info[1].hasDate:
                            self.due_date = datetime_info[0].date()
                            date_expression = datetime_info[4]

                            # FIXME: This logic could be improved to better
                            # differentiate between week and year expressions

                            # If the date expression is only one word and the next
                            # due date is less than one week from now, set a
                            # weekly recurrence, e.g. every Tuesday
                            if len(date_expression.split(
                                    ' ')) == 1 and self.due_date < date.today(
                                    ) + timedelta(days=8):
                                self.recurrence_count = 1
                                self.recurrence_type = 'week'
                            # Otherwise expect a multi-word value like a date,
                            # e.g. every May 17
                            else:
                                self.recurrence_count = 1
                                self.recurrence_type = 'year'

                            self.has_recurrence_prompt = False

                            # Pull in any words between the `due` keyword and the
                            # actual date text
                            date_pattern = re.escape(date_expression)
                            date_pattern = r'.*?' + date_pattern

                            # Prepare to set the recurrence phrase below
                            match = re.search(date_pattern, recurrence_phrase)

            # This is just the "every" keyword with no date following
            if not self.recurrence_type:
                self.has_recurrence_prompt = True

            self._recurrence_phrase = match.group()
            phrase = phrase.replace(self._recurrence_phrase, '', 1)

        reminder_info = None
        match = re.search(REMINDER_PATTERN, phrase)
        if match:
            datetimes = cal.nlp(match.group(2), version=2)

            # If there is at least one date immediately following the reminder
            # phrase use it as the reminder date
            if datetimes and datetimes[0][2] == 0:
                # Only remove the first date following the keyword
                reminder_info = datetimes[0]

                self._reminder_phrase = match.group(1) + reminder_info[4]
                phrase = phrase.replace(self._reminder_phrase, '', 1)
            # Otherwise if there is just a reminder phrase, set the reminder
            # to the default time on the date due
            else:
                # There is no text following the reminder phrase, prompt for a reminder
                if not match.group(2):
                    self.has_reminder_prompt = True
                self._reminder_phrase = match.group(1)

                # Careful, this might just be the letter "r" so rather than
                # replacing it is better to strip out by index
                phrase = phrase[:match.start(1)] + phrase[match.end(1):]

        due_keyword = None
        potential_date_phrase = None
        if not ignore_due_date:
            match = re.search(DUE_PATTERN, phrase)
            # Search for the due date only following the `due` keyword
            if match:
                due_keyword = match.group(1)

                if match.group(2):
                    potential_date_phrase = match.group(2)
            # Otherwise find a due date anywhere in the phrase
            elif not prefs.explicit_keywords:
                potential_date_phrase = phrase

        if potential_date_phrase:
            dates = cal.nlp(potential_date_phrase, version=2)

            if dates:
                # Only remove the first date following `due`
                datetime_info = dates[0]
                # Set due_date if a datetime was found and it is not time only
                if datetime_info[1].hasDate:
                    self.due_date = datetime_info[0].date()
                elif datetime_info[1].hasTime and not self.due_date:
                    self.due_date = date.today()

                if self.due_date:
                    # Pull in any words between the `due` keyword and the
                    # actual date text
                    date_pattern = re.escape(datetime_info[4])

                    if due_keyword:
                        date_pattern = re.escape(
                            due_keyword) + r'.*?' + date_pattern

                    due_date_phrase_match = re.search(date_pattern, phrase)

                    if due_date_phrase_match:
                        self._due_date_phrase = due_date_phrase_match.group()
                        phrase = phrase.replace(self._due_date_phrase, '', 1)

                    # If the due date specifies a time, set it as the reminder
                    if datetime_info[1].hasTime:
                        if datetime_info[1].hasDate:
                            self.reminder_date = datetime_info[0]
                        elif self.due_date:
                            self.reminder_date = datetime.combine(
                                self.due_date, datetime_info[0].time())
                # Just a time component
                else:
                    due_keyword = None
            # No dates in the phrase
            else:
                due_keyword = None

        # The word due was not followed by a date
        if due_keyword and not self._due_date_phrase:
            self.has_due_date_prompt = True
            self._due_date_phrase = match.group(1)

            # Avoid accidentally replacing "due" inside words elsewhere in the
            # string
            phrase = phrase[:match.start(1)] + phrase[match.end(1):]

        if self.recurrence_type and not self.due_date:
            self.due_date = date.today()

        if self._reminder_phrase:
            # If a due date is set, a time-only reminder is relative to that
            # date; otherwise if there is no due date it is relative to today
            reference_date = self.due_date if self.due_date else date.today()

            if reminder_info:
                (dt, datetime_context, _, _, _) = reminder_info

                # Date and time; use as-is
                if datetime_context.hasTime and datetime_context.hasDate:
                    self.reminder_date = dt
                # Time only; set the reminder on the due day
                elif datetime_context.hasTime:
                    self.reminder_date = cls.reminder_date_combine(
                        reference_date, dt)
                # Date only; set the default reminder time on that day
                elif datetime_context.hasDate:
                    self.reminder_date = cls.reminder_date_combine(dt)

            else:
                self.reminder_date = cls.reminder_date_combine(reference_date)

        # Look for a list title at the end of the remaining phrase, like
        # "in list Office"
        if not self.list_title:
            matches = re.finditer(INFIX_LIST_KEYWORD_PATTERN, phrase)
            for match in matches:
                subphrase = phrase[match.end():]

                # Just a couple characters are too likely to result in a false
                # positive, but allow it if the letters are capitalized
                if len(subphrase) > 2 or subphrase == subphrase.upper():
                    matching_taskfolders = wf.filter(
                        subphrase,
                        taskfolders,
                        lambda f: f['title'],
                        # Ignore MATCH_ALLCHARS which is expensive and inaccurate
                        match_on=MATCH_ALL ^ MATCH_ALLCHARS)

                    # Take the first match as the desired list
                    if matching_taskfolders:
                        self.list_id = matching_taskfolders[0]['id']
                        self.list_title = matching_taskfolders[0]['title']
                        self._list_phrase = match.group() + subphrase
                        phrase = phrase[:match.start()]
                        break

        # No list parsed, assign to Tasks
        if not self.list_title:
            if prefs.default_taskfolder_id and taskfolders:
                if prefs.default_taskfolder_id == DEFAULT_TASKFOLDER_MOST_RECENT:
                    self.list_id = prefs.last_taskfolder_id
                else:
                    self.list_id = prefs.default_taskfolder_id
                default_taskfolder = next(
                    (f for f in taskfolders if f['id'] == self.list_id), None)
                if default_taskfolder:
                    self.list_title = default_taskfolder['title']

            if not self.list_title:
                if taskfolders:
                    inbox = taskfolders[0]
                    self.list_id = inbox['id']
                    self.list_title = inbox['title']
                else:
                    self.list_id = 0
                    self.list_title = 'Tasks'

        # Set an automatic reminder when there is a due date without a
        # specified reminder
        if self.due_date and not self.reminder_date and prefs.automatic_reminders:
            self.reminder_date = cls.reminder_date_combine(self.due_date)

        # Condense extra whitespace remaining in the task title after parsing
        self.title = re.sub(WHITESPACE_CLEANUP_PATTERN, ' ', phrase).strip()