Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
T
topydo
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
topydo
Commits
3f3a6c4e
Commit
3f3a6c4e
authored
Dec 15, 2016
by
Bram Schoenmakers
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'groups/master'
parents
6383df70
ca331b82
Changes
12
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
518 additions
and
78 deletions
+518
-78
test/data/ListCommandGroupTest.txt
test/data/ListCommandGroupTest.txt
+19
-0
test/test_list_command.py
test/test_list_command.py
+220
-0
topydo/commands/ListCommand.py
topydo/commands/ListCommand.py
+24
-7
topydo/lib/Config.py
topydo/lib/Config.py
+4
-0
topydo/lib/Sorter.py
topydo/lib/Sorter.py
+179
-56
topydo/lib/View.py
topydo/lib/View.py
+15
-5
topydo/lib/printers/PrettyPrinter.py
topydo/lib/printers/PrettyPrinter.py
+34
-1
topydo/ui/columns/ColumnLayout.py
topydo/ui/columns/ColumnLayout.py
+2
-0
topydo/ui/columns/Main.py
topydo/ui/columns/Main.py
+2
-1
topydo/ui/columns/TodoListWidget.py
topydo/ui/columns/TodoListWidget.py
+11
-5
topydo/ui/columns/ViewWidget.py
topydo/ui/columns/ViewWidget.py
+7
-3
topydo_columns.conf
topydo_columns.conf
+1
-0
No files found.
test/data/ListCommandGroupTest.txt
0 → 100644
View file @
3f3a6c4e
+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
test/test_list_command.py
View file @
3f3a6c4e
...
@@ -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
()
topydo/commands/ListCommand.py
View file @
3f3a6c4e
...
@@ -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).
\
"""
"""
topydo/lib/Config.py
View file @
3f3a6c4e
...
@@ -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'
)
...
...
topydo/lib/Sorter.py
View file @
3f3a6c4e
...
@@ -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
topydo/lib/View.py
View file @
3f3a6c4e
...
@@ -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
)
topydo/lib/printers/PrettyPrinter.py
View file @
3f3a6c4e
...
@@ -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. """
...
...
topydo/ui/columns/ColumnLayout.py
View file @
3f3a6c4e
...
@@ -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'
,
}
}
...
...
topydo/ui/columns/Main.py
View file @
3f3a6c4e
...
@@ -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
,
}
}
...
...
topydo/ui/columns/TodoListWidget.py
View file @
3f3a6c4e
...
@@ -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
:
...
...
topydo/ui/columns/ViewWidget.py
View file @
3f3a6c4e
...
@@ -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
=
[]
radio
group
=
[]
self
.
relevantradio
=
urwid
.
RadioButton
(
group
,
"Only show relevant todo items"
,
True
)
self
.
relevantradio
=
urwid
.
RadioButton
(
radio
group
,
"Only show relevant todo items"
,
True
)
self
.
allradio
=
urwid
.
RadioButton
(
group
,
"Show all todo items"
)
self
.
allradio
=
urwid
.
RadioButton
(
radio
group
,
"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'
])
...
...
topydo_columns.conf
View file @
3f3a6c4e
[
all
]
[
all
]
title
=
All
tasks
title
=
All
tasks
filterexpr
=
filterexpr
=
groupexpr
=
[
today
]
[
today
]
title
=
Due
today
title
=
Due
today
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment