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')
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)
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)
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()
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
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)
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
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)
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)
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)
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)
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)
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)
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()
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
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()