Commit 3f3a6c4e authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'groups/master'

parents 6383df70 ca331b82
+A only test:test_group1
+B only test:test_group1
+A and +B test:test_group1
No project test:test_group1
Different item test:test_group2 l:1
Another item test:test_group2 l:0
Test 1 due:2016-12-06 test:test_group3
Test 2 due:2016-12-07 test:test_group3
Test 1 t:2016-12-06 test:test_group4 test:test_group5
Test 2 t:2016-12-07 test:test_group4 test:test_group5
Group by non-existing tag test:test_group6
Sort descending +A test:test_group7
Sort descending +B test:test_group7
Inner sort 1 +A @A test:test_group8
Inner sort 2 +A @B test:test_group8
Inner sort 3 +B @A test:test_group8
Inner sort 4 +B @B test:test_group8
Inner sort 1 +A test:test_group9
Inner sort 2 +A test:test_group9
...@@ -552,5 +552,225 @@ class ListCommandDotTest(CommandTest): ...@@ -552,5 +552,225 @@ class ListCommandDotTest(CommandTest):
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
@freeze_time('2016, 12, 6')
class ListCommandGroupTest(CommandTest):
def test_group1(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-g", "project", "test:test_group1"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
Project: A
==========
| 1| +A only test:test_group1
| 3| +A and +B test:test_group1
Project: B
==========
| 3| +A and +B test:test_group1
| 2| +B only test:test_group1
Project: None
=============
| 4| No project test:test_group1
""")
def test_group2(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-g", "l", "test:test_group2"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
l: 0
====
| 6| Another item l:0 test:test_group2
l: 1
====
| 5| Different item l:1 test:test_group2
""")
def test_group3(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-g", "due", "test:test_group3"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
due: today
==========
| 7| Test 1 test:test_group3 due:2016-12-06
due: in a day
=============
| 8| Test 2 test:test_group3 due:2016-12-07
""")
def test_group4(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-g", "t", "test:test_group4"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
t: today
========
| 9| Test 1 test:test_group4 test:test_group5 t:2016-12-06
""")
def test_group5(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g", "t", "test:test_group5"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
t: today
========
| 9| Test 1 test:test_group4 test:test_group5 t:2016-12-06
t: in a day
===========
| 10| Test 2 test:test_group4 test:test_group5 t:2016-12-07
""")
def test_group6(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g", "fake", "test_group6"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
fake: No value
==============
| 11| Group by non-existing tag test:test_group6
""")
def test_group7(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g", "desc:project", "test_group7"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
Project: B
==========
| 13| Sort descending +B test:test_group7
Project: A
==========
| 12| Sort descending +A test:test_group7
""")
def test_group8(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g", "project,desc:context", "test_group8"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
Project: A, Context: B
======================
| 15| Inner sort 2 +A @B test:test_group8
Project: A, Context: A
======================
| 14| Inner sort 1 +A @A test:test_group8
Project: B, Context: B
======================
| 17| Inner sort 4 +B @B test:test_group8
Project: B, Context: A
======================
| 16| Inner sort 3 +B @A test:test_group8
""")
def test_group9(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g", "project", "-s", "desc:text", "test_group9"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
Project: A
==========
| 19| Inner sort 2 +A test:test_group9
| 18| Inner sort 1 +A test:test_group9
""")
def test_group10(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-x", "-g"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "option -g requires argument\n")
def test_group11(self):
config(p_overrides={('sort', 'group_string'): 'project'})
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["test:test_group1"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
Project: A
==========
| 1| +A only test:test_group1
| 3| +A and +B test:test_group1
Project: B
==========
| 3| +A and +B test:test_group1
| 2| +B only test:test_group1
Project: None
=============
| 4| No project test:test_group1
""")
self.assertEqual(self.errors, "")
def test_group12(self):
todolist = load_file_to_todolist("test/data/ListCommandGroupTest.txt")
command = ListCommand(["-g", ",", "test:test_group1"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.dirty)
self.assertEqual(self.output, """\
| 1| +A only test:test_group1
| 2| +B only test:test_group1
| 3| +A and +B test:test_group1
| 4| No project test:test_group1
""")
self.assertEqual(self.errors, "")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -24,7 +24,9 @@ from topydo.lib.Filter import HiddenTagFilter, InstanceFilter ...@@ -24,7 +24,9 @@ from topydo.lib.Filter import HiddenTagFilter, InstanceFilter
from topydo.lib.printers.PrettyPrinter import pretty_printer_factory from topydo.lib.printers.PrettyPrinter import pretty_printer_factory
from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Sorter import Sorter
from topydo.lib.Utils import get_terminal_size from topydo.lib.Utils import get_terminal_size
from topydo.lib.View import View
class ListCommand(ExpressionCommand): class ListCommand(ExpressionCommand):
...@@ -37,6 +39,7 @@ class ListCommand(ExpressionCommand): ...@@ -37,6 +39,7 @@ class ListCommand(ExpressionCommand):
self.printer = None self.printer = None
self.sort_expression = config().sort_string() self.sort_expression = config().sort_string()
self.group_expression = config().group_string()
self.show_all = False self.show_all = False
self.ids = None self.ids = None
self.format = config().list_format() self.format = config().list_format()
...@@ -55,7 +58,7 @@ class ListCommand(ExpressionCommand): ...@@ -55,7 +58,7 @@ class ListCommand(ExpressionCommand):
return True return True
def _process_flags(self): def _process_flags(self):
opts, args = self.getopt('f:F:i:n:Ns:x') opts, args = self.getopt('f:F:g:i:n:Ns:x')
for opt, value in opts: for opt, value in opts:
if opt == '-x': if opt == '-x':
...@@ -81,6 +84,8 @@ class ListCommand(ExpressionCommand): ...@@ -81,6 +84,8 @@ class ListCommand(ExpressionCommand):
self.printer = None self.printer = None
elif opt == '-F': elif opt == '-F':
self.format = value self.format = value
elif opt == '-g':
self.group_expression = value
elif opt == '-N': elif opt == '-N':
# 2 lines are assumed to be taken up by printing the next prompt # 2 lines are assumed to be taken up by printing the next prompt
# display at least one item # display at least one item
...@@ -143,7 +148,16 @@ class ListCommand(ExpressionCommand): ...@@ -143,7 +148,16 @@ class ListCommand(ExpressionCommand):
self.printer = pretty_printer_factory(self.todolist, filters) self.printer = pretty_printer_factory(self.todolist, filters)
self.out(self.printer.print_list(self._view().todos)) if self.group_expression:
self.out(self.printer.print_groups(self._view().groups))
else:
self.out(self.printer.print_list(self._view().todos))
def _view(self):
sorter = Sorter(self.sort_expression, self.group_expression)
filters = self._filters()
return View(sorter, filters, self.todolist)
def _N_lines(self): def _N_lines(self):
''' Determine how many lines to print, such that the number of items ''' Determine how many lines to print, such that the number of items
...@@ -190,9 +204,9 @@ class ListCommand(ExpressionCommand): ...@@ -190,9 +204,9 @@ class ListCommand(ExpressionCommand):
return True return True
def usage(self): def usage(self):
return """Synopsis: ls [-x] [-s <SORT EXPRESSION>] [-f <OUTPUT FORMAT>] return """Synopsis: ls [-x] [-s <SORT EXPRESSION>]
[-F <FORMAT STRING>] [-i <NUMBER 1>[,<NUMBER 2> ...]] [-N | -n <INTEGER>] [-g <GROUP EXPRESSION>] [-f <OUTPUT FORMAT>] [-F <FORMAT STRING>]
[EXPRESSION]""" [-i <NUMBER 1>[,<NUMBER 2> ...]] [-N | -n <INTEGER>] [EXPRESSION]"""
def help(self): def help(self):
return """\ return """\
...@@ -247,11 +261,14 @@ When an EXPRESSION is given, only the todos matching that EXPRESSION are shown. ...@@ -247,11 +261,14 @@ When an EXPRESSION is given, only the todos matching that EXPRESSION are shown.
(empty string) when an item has no priority set. (empty string) when an item has no priority set.
A tab character serves as a marker to start right alignment. A tab character serves as a marker to start right alignment.
-g : Group items according to a GROUP EXPRESSION. A group expression is similar
to a sort expression. Defaults to the group expression in the
configuration.
-i : Comma separated list of todo IDs to print. -i : Comma separated list of todo IDs to print.
-n : Number of items to display. Defaults to the value in the configuration. -n : Number of items to display. Defaults to the value in the configuration.
-N : Limit number of items displayed such that they fit on the terminal. -N : Limit number of items displayed such that they fit on the terminal.
-s : Sort the list according to a SORT EXPRESSION. Defaults to the expression -s : Sort the list according to a SORT EXPRESSION. Defaults to the sort
in the configuration. expression in the configuration.
-x : Show all todos (i.e. do not filter on dependencies, relevance, or hidden -x : Show all todos (i.e. do not filter on dependencies, relevance, or hidden
status).\ status).\
""" """
...@@ -92,6 +92,7 @@ class _Config: ...@@ -92,6 +92,7 @@ class _Config:
'sort': { 'sort': {
'keep_sorted': '0', 'keep_sorted': '0',
'sort_string': 'desc:importance,due,desc:priority', 'sort_string': 'desc:importance,due,desc:priority',
'group_string': '',
'ignore_weekends': '1', 'ignore_weekends': '1',
}, },
...@@ -272,6 +273,9 @@ class _Config: ...@@ -272,6 +273,9 @@ class _Config:
def sort_string(self): def sort_string(self):
return self.cp.get('sort', 'sort_string') return self.cp.get('sort', 'sort_string')
def group_string(self):
return self.cp.get('sort', 'group_string')
def ignore_weekends(self): def ignore_weekends(self):
try: try:
return self.cp.getboolean('sort', 'ignore_weekends') return self.cp.getboolean('sort', 'ignore_weekends')
......
...@@ -16,56 +16,110 @@ ...@@ -16,56 +16,110 @@
""" This module provides functionality to sort lists with todo items. """ """ This module provides functionality to sort lists with todo items. """
from collections import OrderedDict, namedtuple
from itertools import groupby
import re import re
from datetime import date from datetime import date
from topydo.lib.Config import config
from topydo.lib.Importance import average_importance, importance from topydo.lib.Importance import average_importance, importance
from topydo.lib.Utils import humanize_date
def is_priority_field(p_field): Field = namedtuple('Field', ['sort', 'group', 'label'])
""" Returns True when the field name denotes the priority. """
return p_field.startswith('prio')
FIELDS = {
def get_field_function(p_field): 'completed': Field(
""" # when a task has no completion date, push it to the end by assigning it
Given a property (string) of a todo, return a function that attempts to # the maximum possible date.
access that property. If the property could not be located, return the sort=(lambda t: t.completion_date() if t.completion_date() else date.max),
identity function. group=(lambda t: humanize_date(t.completion_date()) if t.completion_date() else 'None'),
""" label='Completed',
result = lambda a: a ),
'context': Field(
if is_priority_field(p_field): sort=lambda t: sorted(c.lower() for c in t.contexts()) or ['zz'],
# assign dummy priority when a todo has no priority group=lambda t: sorted(t.contexts()) or ['None'],
result = lambda a: a.priority() or 'ZZ' label='Context'
elif p_field == 'context' or p_field == 'contexts': ),
result = lambda a: sorted([c.lower() for c in a.contexts()]) 'created': Field(
elif p_field == 'creationdate' or p_field == 'creation':
# when a task has no creation date, push it to the end by assigning it # when a task has no creation date, push it to the end by assigning it
# the maximum possible date. # the maximum possible date.
result = (lambda a: a.creation_date() if a.creation_date() sort=(lambda t: t.creation_date() if t.creation_date() else date.max),
else date.max) group=(lambda t: humanize_date(t.creation_date()) if t.creation_date() else 'None'),
elif p_field == 'done' or p_field == 'completed' or p_field == 'completion': label='Created',
result = (lambda a: a.completion_date() if a.completion_date() ),
else date.max) 'importance': Field(
elif p_field == 'importance': sort=importance,
result = importance group=importance,
elif p_field == 'importance-avg' or p_field == 'importance-average': label='Importance',
result = average_importance ),
elif p_field == 'length': 'importance-avg': Field(
result = lambda a: a.length() sort= average_importance,
elif p_field == 'project' or p_field == 'projects': group=lambda t: round(average_importance(t), 1),
result = lambda a: sorted([c.lower() for c in a.projects()]) label='Importance (avg)',
elif p_field == 'text': ),
result = lambda a: a.text().lower() 'length': Field(
else: sort=lambda t: t.length(),
# try to find the corresponding tag group=lambda t: t.length(),
# when a tag is not present, push it to the end of the list by giving label='Length',
# it an artificially higher value ),
result = (lambda a: "0" + a.tag_value(p_field) if a.has_tag(p_field) 'priority': Field(
else "1") sort=(lambda t: t.priority() or 'ZZ'),
group=(lambda t: t.priority() or 'None'),
return result label='Priority',
),
'project': Field(
sort=lambda t: sorted(p.lower() for p in t.projects()) or ['zz'],
group=lambda t: sorted(t.projects()) or ['None'],
label='Project',
),
'text': Field(
sort=lambda t: t.text().lower(),
group=lambda t: t.text(),
label='Text',
),
}
# map UI properties to properties in the FIELDS hash
FIELD_MAP = {
'completed': 'completed',
'completion': 'completed',
'completion_date': 'completed',
'done': 'completed',
'context': 'context',
'contexts': 'context',
'created': 'created',
'creation': 'created',
'creation_date': 'created',
'importance': 'importance',
'importance-avg': 'importance-avg',
'importance-average': 'importance-avg',
'length': 'length',
'len': 'length',
'prio': 'priority',
'priorities': 'priority',
'priority': 'priority',
'project': 'project',
'projects': 'project',
'text': 'text',
}
def _apply_sort_functions(p_todos, p_functions):
sorted_todos = p_todos
for function, order in reversed(p_functions):
sorted_todos = sorted(sorted_todos, key=function,
reverse=(order == 'desc'))
return sorted_todos
class Sorter(object): class Sorter(object):
...@@ -93,10 +147,10 @@ class Sorter(object): ...@@ -93,10 +147,10 @@ class Sorter(object):
stable. stable.
""" """
def __init__(self, p_sortstring="desc:priority"): def __init__(self, p_sortstring="desc:priority", p_groupstring=""):
self.sortstring = p_sortstring self.groupfunctions = self._parse(p_groupstring, p_group=True) if p_groupstring else []
self.functions = [] self.pregroupfunctions = self._parse(p_groupstring, p_group=False) if p_groupstring else []
self._parse() self.sortfunctions = self._parse(p_sortstring, p_group=False)
def sort(self, p_todos): def sort(self, p_todos):
""" """
...@@ -107,19 +161,85 @@ class Sorter(object): ...@@ -107,19 +161,85 @@ class Sorter(object):
sort operation is done first, relying on the stability of the sorted() sort operation is done first, relying on the stability of the sorted()
function. function.
""" """
sorted_todos = p_todos return _apply_sort_functions(p_todos, self.sortfunctions)
for function, order in reversed(self.functions):
sorted_todos = sorted(sorted_todos, key=function,
reverse=(order == 'desc'))
return sorted_todos def group(self, p_todos):
"""
Groups the todos according to the given group string.
"""
# preorder todos for the group sort
p_todos = _apply_sort_functions(p_todos, self.pregroupfunctions)
# initialize result with a single group
result = OrderedDict([((), p_todos)])
for (function, label), _ in self.groupfunctions:
oldresult = result
result = OrderedDict()
for oldkey, oldgroup in oldresult.items():
for key, _group in groupby(oldgroup, function):
newgroup = list(_group)
if not isinstance(key, list):
key = [key]
for subkey in key:
subkey = "{}: {}".format(label, subkey)
newkey = oldkey + (subkey,)
if newkey in result:
result[newkey] = result[newkey] + newgroup
else:
result[newkey] = newgroup
# sort all groups
for key, _group in result.items():
result[key] = self.sort(_group)
def _parse(self): return result
def _parse(self, p_string, p_group):
""" """
Parses a sort string and returns a list of functions and the Parses a sort/group string and returns a list of functions and the
desired order. desired order.
""" """
fields = self.sortstring.lower().split(',') def get_field_function(p_field, p_group=False):
"""
Turns a field, part of a sort/group string, into a lambda that
takes a todo item and returns the field value.
"""
compose = lambda i: i.sort if not p_group else (i.group, i.label)
def group_value(p_todo):
"""
Returns a value to assign the given todo to a group. Date tags
are grouped according to the relative date (1 day, 1 month,
...)
"""
result = 'No value'
if p_todo.has_tag(p_field):
if p_field == config().tag_due():
result = humanize_date(p_todo.due_date())
elif p_field == config().tag_start():
result = humanize_date(p_todo.start_date())
else:
result = p_todo.tag_value(p_field)
return result
if p_field in FIELD_MAP:
return compose(FIELDS[FIELD_MAP[p_field]])
else:
# treat it as a tag value
return compose(Field(
sort=lambda t: '0' + t.tag_value(p_field) if t.has_tag(p_field) else '1',
group=group_value,
label=p_field,
))
result = []
fields = p_string.lower().split(',')
for field in fields: for field in fields:
parsed_field = re.match( parsed_field = re.match(
...@@ -134,11 +254,14 @@ class Sorter(object): ...@@ -134,11 +254,14 @@ class Sorter(object):
field = parsed_field.group('field') field = parsed_field.group('field')
if field: if field:
function = get_field_function(field) function = get_field_function(field, p_group)
# reverse order for priority: lower characters have higher # reverse order for priority: lower characters have higher
# priority # priority
if is_priority_field(field): if field in FIELD_MAP and FIELD_MAP[field] == 'priority':
order = 'asc' if order == 'desc' else 'desc' order = 'asc' if order == 'desc' else 'desc'
self.functions.append((function, order)) result.append((function, order))
return result
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" A view is a list of todos, sorted and filtered. """ """ A view is a list of todos, sorted, grouped and filtered. """
class View(object): class View(object):
...@@ -29,12 +29,22 @@ class View(object): ...@@ -29,12 +29,22 @@ class View(object):
self._sorter = p_sorter self._sorter = p_sorter
self._filters = p_filters self._filters = p_filters
@property def _apply_filters(self, p_todos):
def todos(self): """ Applies the filters to the list of todo items. """
""" Returns a sorted and filtered list of todos in this view. """ result = p_todos
result = self._sorter.sort(self.todolist.todos())
for _filter in self._filters: for _filter in self._filters:
result = _filter.filter(result) result = _filter.filter(result)
return result return result
@property
def todos(self):
""" Returns a sorted and filtered list of todos in this view. """
result = self._sorter.sort(self.todolist.todos())
return self._apply_filters(result)
@property
def groups(self):
result = self._apply_filters(self.todolist.todos())
return self._sorter.group(result)
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from itertools import chain
from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.TopydoString import TopydoString from topydo.lib.TopydoString import TopydoString
...@@ -30,8 +32,16 @@ class Printer(object): ...@@ -30,8 +32,16 @@ class Printer(object):
raise NotImplementedError raise NotImplementedError
def print_list(self, p_todos): def print_list(self, p_todos):
result = ''
for todo in p_todos: for todo in p_todos:
self.print_todo(todo) result += self.print_todo(todo)
return result
def print_groups(self, p_groups):
todos = list(chain.from_iterable(p_groups.values()))
return self.print_list(todos)
class PrettyPrinter(Printer): class PrettyPrinter(Printer):
...@@ -76,6 +86,29 @@ class PrettyPrinter(Printer): ...@@ -76,6 +86,29 @@ class PrettyPrinter(Printer):
""" """
return [self.print_todo(todo) for todo in p_todos] return [self.print_todo(todo) for todo in p_todos]
def print_groups(self, p_groups):
result = []
first = True
def print_header(p_key):
""" Prints a header for the given key. """
if not first:
result.append('')
key_string = ", ".join(p_key)
result.append(key_string)
result.append("=" * len(key_string))
for key, todos in p_groups.items():
if key != ():
# don't print a header for the case that no valid grouping
# could be made (e.g. an invalid group expression)
print_header(key)
first = False
result += self.print_list(todos)
return [TopydoString(s) for s in result]
def pretty_printer_factory(p_todolist, p_additional_filters=None): def pretty_printer_factory(p_todolist, p_additional_filters=None):
""" Returns a pretty printer suitable for the ls and dep subcommands. """ """ Returns a pretty printer suitable for the ls and dep subcommands. """
......
...@@ -30,6 +30,7 @@ def columns(p_alt_layout_path=None): ...@@ -30,6 +30,7 @@ def columns(p_alt_layout_path=None):
column_dict['title'] = p_cp.get(p_column, 'title') column_dict['title'] = p_cp.get(p_column, 'title')
column_dict['filterexpr'] = p_cp.get(p_column, 'filterexpr') column_dict['filterexpr'] = p_cp.get(p_column, 'filterexpr')
column_dict['sortexpr'] = p_cp.get(p_column, 'sortexpr') column_dict['sortexpr'] = p_cp.get(p_column, 'sortexpr')
column_dict['groupexpr'] = p_cp.get(p_column, 'groupexpr')
column_dict['show_all'] = p_cp.getboolean(p_column, 'show_all') column_dict['show_all'] = p_cp.getboolean(p_column, 'show_all')
return column_dict return column_dict
...@@ -38,6 +39,7 @@ def columns(p_alt_layout_path=None): ...@@ -38,6 +39,7 @@ def columns(p_alt_layout_path=None):
'title': 'Yet another column', 'title': 'Yet another column',
'filterexpr': '', 'filterexpr': '',
'sortexpr': config().sort_string(), 'sortexpr': config().sort_string(),
'groupexpr': config().group_string(),
'show_all': '0', 'show_all': '0',
} }
......
...@@ -415,7 +415,7 @@ class UIApplication(CLIApplicationBase): ...@@ -415,7 +415,7 @@ class UIApplication(CLIApplicationBase):
""" """
Converts a dictionary describing a view to an actual UIView instance. Converts a dictionary describing a view to an actual UIView instance.
""" """
sorter = Sorter(p_data['sortexpr']) sorter = Sorter(p_data['sortexpr'], p_data['groupexpr'])
filters = [] filters = []
if not p_data['show_all']: if not p_data['show_all']:
...@@ -609,6 +609,7 @@ class UIApplication(CLIApplicationBase): ...@@ -609,6 +609,7 @@ class UIApplication(CLIApplicationBase):
dummy = { dummy = {
"title": "All tasks", "title": "All tasks",
"sortexpr": "desc:prio", "sortexpr": "desc:prio",
"groupexpr": "",
"filterexpr": "", "filterexpr": "",
"show_all": True, "show_all": True,
} }
......
...@@ -92,11 +92,17 @@ class TodoListWidget(urwid.LineBox): ...@@ -92,11 +92,17 @@ class TodoListWidget(urwid.LineBox):
del self.todolist[:] del self.todolist[:]
for todo in self.view.todos: for group, todos in self.view.groups.items():
todowidget = TodoWidget.create(todo) if len(self.view.groups) > 1:
todowidget.number = self.view.todolist.number(todo) grouplabel = ", ".join(group)
self.todolist.append(todowidget) self.todolist.append(urwid.Text(grouplabel))
self.todolist.append(urwid.Divider('-')) self.todolist.append(urwid.Divider('-'))
for todo in todos:
todowidget = TodoWidget.create(todo)
todowidget.number = self.view.todolist.number(todo)
self.todolist.append(todowidget)
self.todolist.append(urwid.Divider('-'))
if old_focus_position: if old_focus_position:
try: try:
......
...@@ -24,16 +24,18 @@ class ViewWidget(urwid.LineBox): ...@@ -24,16 +24,18 @@ class ViewWidget(urwid.LineBox):
self.titleedit = urwid.Edit("Title: ", "") self.titleedit = urwid.Edit("Title: ", "")
self.sortedit = urwid.Edit("Sort expression: ", "") self.sortedit = urwid.Edit("Sort expression: ", "")
self.groupedit = urwid.Edit("Group expression: ", "")
self.filteredit = urwid.Edit("Filter expression: ", "") self.filteredit = urwid.Edit("Filter expression: ", "")
group = [] radiogroup = []
self.relevantradio = urwid.RadioButton(group, "Only show relevant todo items", True) self.relevantradio = urwid.RadioButton(radiogroup, "Only show relevant todo items", True)
self.allradio = urwid.RadioButton(group, "Show all todo items") self.allradio = urwid.RadioButton(radiogroup, "Show all todo items")
self.pile = urwid.Pile([ self.pile = urwid.Pile([
self.filteredit, self.filteredit,
self.titleedit, self.titleedit,
self.sortedit, self.sortedit,
self.groupedit,
self.relevantradio, self.relevantradio,
self.allradio, self.allradio,
urwid.Button("Save", lambda _: urwid.emit_signal(self, 'save')), urwid.Button("Save", lambda _: urwid.emit_signal(self, 'save')),
...@@ -51,6 +53,7 @@ class ViewWidget(urwid.LineBox): ...@@ -51,6 +53,7 @@ class ViewWidget(urwid.LineBox):
return { return {
'title': self.titleedit.edit_text or self.filteredit.edit_text, 'title': self.titleedit.edit_text or self.filteredit.edit_text,
'sortexpr': self.sortedit.edit_text or config().sort_string(), 'sortexpr': self.sortedit.edit_text or config().sort_string(),
'groupexpr': self.groupedit.edit_text or config().group_string(),
'filterexpr': self.filteredit.edit_text, 'filterexpr': self.filteredit.edit_text,
'show_all': self.allradio.state, 'show_all': self.allradio.state,
} }
...@@ -59,6 +62,7 @@ class ViewWidget(urwid.LineBox): ...@@ -59,6 +62,7 @@ class ViewWidget(urwid.LineBox):
def data(self, p_data): def data(self, p_data):
self.titleedit.edit_text = p_data['title'] self.titleedit.edit_text = p_data['title']
self.sortedit.edit_text = p_data['sortexpr'] self.sortedit.edit_text = p_data['sortexpr']
self.groupedit.edit_text = p_data['groupexpr']
self.filteredit.edit_text = p_data['filterexpr'] self.filteredit.edit_text = p_data['filterexpr']
self.relevantradio.set_state(not p_data['show_all']) self.relevantradio.set_state(not p_data['show_all'])
self.allradio.set_state(p_data['show_all']) self.allradio.set_state(p_data['show_all'])
......
[all] [all]
title = All tasks title = All tasks
filterexpr = filterexpr =
groupexpr =
[today] [today]
title = Due today title = Due today
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment