Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.4 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 operator
36
import locale
37
import unicodedata
38

    
39
from datetime import datetime
40
from django.utils.timesince import timesince, timeuntil
41
from django.db.models.query import QuerySet
42
from django.utils.encoding import smart_unicode, smart_str
43
from snf_django.management.unicodecsv import UnicodeWriter
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_unicode, 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
        enc = locale.getpreferredencoding()
215
        cw = UnicodeWriter(out, encoding=enc)
216
        if headers:
217
            table.insert(0, headers)
218
        cw.writerows(table)
219
    elif output_format == "pretty":
220
        if vertical:
221
            assert(len(table) == 1)
222
            row = table[0]
223
            max_key = max(map(len, headers))
224
            for row in table:
225
                for (k, v) in zip(headers, row):
226
                    k = k.ljust(max_key)
227
                    out.write("%s: %s\n" % (k, v))
228
        else:
229
            # Find out the max width of each column
230
            columns = [headers] + table if headers else table
231
            widths = [max(map(len, col)) for col in zip(*(columns))]
232

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

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