Statistics
| Branch: | Tag: | Revision:

root / snf-django-lib / snf_django / management / utils.py @ 8c911970

History | View | Annotate | Download (8.3 kB)

1
# Copyright 2012 - 2014 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import json
35
import csv
36
import operator
37
import locale
38
import unicodedata
39

    
40
from datetime import datetime
41
from django.utils.timesince import timesince, timeuntil
42
from django.db.models.query import QuerySet
43
from django.utils.encoding import smart_unicode, smart_str
44

    
45

    
46
def smart_locale_unicode(s, **kwargs):
47
    """Wrapper around 'smart_unicode' using user's preferred encoding."""
48
    encoding = locale.getpreferredencoding()
49
    return smart_unicode(s, encoding=encoding, **kwargs)
50

    
51

    
52
def smart_locale_str(s, errors='replace', **kwargs):
53
    """Wrapper around 'smart_str' using user's preferred encoding."""
54
    encoding = locale.getpreferredencoding()
55
    return smart_str(s, encoding=encoding, errors=errors, **kwargs)
56

    
57

    
58
def safe_string(s):
59
    """Escape control characters from unicode and string objects."""
60
    if not isinstance(s, basestring):
61
        return s
62
    if isinstance(s, unicode):
63
        return "".join(ch.encode("unicode_escape")
64
                       if unicodedata.category(ch)[0] == "C" else
65
                       ch for ch in s)
66
    return s.encode("string-escape")
67

    
68

    
69
def parse_bool(value, strict=True):
70
    """Convert a string to boolen value.
71

72
    If strict is True, then ValueError will be raised, if the string can not be
73
    converted to boolean. Otherwise the string will be returned as is.
74

75
    """
76
    if value.lower() in ("yes", "true", "t", "1"):
77
        return True
78
    if value.lower() in ("no", "false", "f", "0"):
79
        return False
80

    
81
    if strict:
82
        raise ValueError("Cannot convert '%s' to boolean value" % value)
83
    else:
84
        return value
85

    
86

    
87
def format_bool(b):
88
    """Convert a boolean value to YES or NO."""
89
    return "YES" if b else "NO"
90

    
91

    
92
def format_date(d):
93
    if not d:
94
        return ""
95

    
96
    if d < datetime.now():
97
        return timesince(d) + " ago"
98
    else:
99
        return "in " + timeuntil(d)
100

    
101

    
102
def filter_results(results, filters):
103
    if isinstance(results, QuerySet):
104
        return filter_queryset_results(results, filters)
105
    elif isinstance(results, list):
106
        return filter_object_results(results, filters)
107
    else:
108
        raise ValueError("Invalid type for results argument: %s", results)
109

    
110

    
111
def parse_queryset_filters(filters):
112
    """Parse a string into lookup parameters for QuerySet.filter(**kwargs).
113

114
    This functions converts a string of comma-separated key 'cond' val triples
115
    to two dictionaries, containing lookup parameters to be used for filter
116
    and exclude functions of QuerySet.
117

118
    e.g. filter_by="foo>=2, baz!=4" -> ({"foo__gte": "2"}, {"baz": "4"})
119

120
    """
121
    OP_MAP = [
122
        (">=", "__gte"),
123
        ("=>", "__gte"),
124
        (">",  "__gt"),
125
        ("<=", "__lte"),
126
        ("=<", "__lte"),
127
        ("<", "__lt"),
128
        ("=", ""),
129
        ]
130

    
131
    filter_dict = {}
132
    exclude_dict = {}
133
    for filter_str in filters.split(","):
134
        if "!=" in filter_str:
135
            key, val = filter_str.split("!=")
136
            exclude_dict[key] = parse_bool(val, strict=False)
137
            continue
138
        for op, new_op in OP_MAP:
139
            if op in filter_str:
140
                key, val = filter_str.split(op)
141
                filter_dict[key + new_op] = parse_bool(val, strict=False)
142
                break
143
        else:
144
            raise ValueError("Unknown filter expression: %s" % filter_str)
145

    
146
    return (filter_dict, exclude_dict)
147

    
148

    
149
def filter_queryset_results(results, filters):
150
    filter_dict, exclude_dict = parse_queryset_filters(filters)
151
    return results.exclude(**exclude_dict).filter(**filter_dict)
152

    
153

    
154
def parse_object_filters(filters):
155
    OP_MAP = [
156
        (">=", operator.ge),
157
        ("=>", operator.ge),
158
        (">",  operator.gt),
159
        ("<=", operator.le),
160
        ("=<", operator.le),
161
        ("<", operator.lt),
162
        ("!=", operator.ne),
163
        ("=", operator.eq),
164
    ]
165
    filters = []
166
    for filter_str in filters.split(","):
167
        for op, op_func in OP_MAP:
168
            if op in filter_str:
169
                key, val = filter_str.split(op)
170
                filters.append((key.strip(), op_func, val.strip()))
171
                break
172
        else:
173
            raise ValueError("Unknown filter expression: %s" % filter_str)
174
    return filters
175

    
176

    
177
def filter_object_results(results, filters):
178
    results = list(results)
179
    if results is []:
180
        return results
181
    zero_result = results[0]
182
    for key, op_func, val in parse_object_filters(filters):
183
        val_type = type(getattr(zero_result, key))
184
        results = filter(lambda x: op_func(getattr(x, key), val_type(val)),
185
                         results)
186
    return results
187

    
188

    
189
def pprint_table(out, table, headers=None, output_format='pretty',
190
                 separator=None, vertical=False, title=None):
191
    """Print a pretty, aligned string representation of table.
192

193
    Works by finding out the max width of each column and padding to data
194
    to this value.
195
    """
196

    
197
    assert(isinstance(table, (list, tuple))), "Invalid table type"
198
    if headers:
199
        assert(isinstance(headers, (list, tuple))), "Invalid headers type"
200

    
201
    sep = separator if separator else "  "
202

    
203
    if headers:
204
        headers = map(smart_unicode, headers)
205
    table = [map(safe_string, row) for row in table]
206
    table = [map(smart_locale_str, row) for row in table]
207

    
208
    if output_format == "json":
209
        assert(headers is not None), "json output format requires headers"
210
        table = [dict(zip(headers, row)) for row in table]
211
        out.write(json.dumps(table, indent=4))
212
        out.write("\n")
213
    elif output_format == "csv":
214
        cw = csv.writer(out)
215
        if headers:
216
            table.insert(0, headers)
217
        cw.writerows(table)
218
    elif output_format == "pretty":
219
        if vertical:
220
            assert(len(table) == 1)
221
            row = table[0]
222
            max_key = max(map(len, headers))
223
            for row in table:
224
                for (k, v) in zip(headers, row):
225
                    k = k.ljust(max_key)
226
                    out.write("%s: %s\n" % (k, v))
227
        else:
228
            # Find out the max width of each column
229
            columns = [headers] + table if headers else table
230
            widths = [max(map(len, col)) for col in zip(*(columns))]
231

    
232
            t_length = sum(widths) + len(sep) * (len(widths) - 1)
233
            if title is not None:
234
                t_length = max(t_length, len(title))
235
                out.write("-" * t_length + "\n")
236
                out.write(title.center(t_length) + "\n")
237
                out.write("-" * t_length + "\n")
238
            if headers:
239
                # pretty print the headers
240
                line = sep.join(v.rjust(w)
241
                                for v, w in zip(headers, widths))
242
                out.write(line + "\n")
243
                out.write("-" * t_length + "\n")
244

    
245
            # print the rest table
246
            for row in table:
247
                line = sep.join(v.rjust(w) for v, w in zip(row, widths))
248
                out.write(line + "\n")
249
    else:
250
        raise ValueError("Unknown output format '%s'" % output_format)