1#!/usr/bin/env python
2#coding:utf-8
3# Purpose: table cell objects
4# Created: 03.01.2011
5# Copyright (C) 2011, Manfred Moitzi
6# License: MIT license
7from __future__ import unicode_literals, print_function, division
8__author__ = "mozman <mozman@gmx.at>"
9
10from .xmlns import register_class, CN
11from .base import GenericWrapper
12from .text import Paragraph, Span
13from .propertymixins import StringProperty, BooleanProperty
14from .compatibility import tostr, is_string
15
16VALID_VALUE_TYPES = frozenset( ('float', 'percentage', 'currency', 'date', 'time',
17                                'boolean', 'string') )
18NUMERIC_TYPES = frozenset( ('float', 'percentage', 'currency') )
19
20TYPE_VALUE_MAP = {
21    'string': CN('office:string-value'),
22    'float': CN('office:value'),
23    'percentage': CN('office:value'),
24    'currency': CN('office:value'),
25    'date': CN('office:date-value'),
26    'time': CN('office:time-value'),
27    'boolean': CN('office:boolean-value'),
28}
29
30# These Classes are supported to read their plaintext content from the
31# cell-content.
32SUPPORTED_CELL_CONTENT = ("Paragraph", "Heading")
33
34@register_class
35class Cell(GenericWrapper):
36    CELL_ONLY_ATTRIBS = (CN('table:number-rows-spanned'),
37                         CN('table:number-columns-spanned'),
38                         CN('table:number-matrix-columns-spanned'),
39                         CN('table:number-matrix-rows-spanned'))
40
41    TAG = CN('table:table-cell')
42    style_name = StringProperty(CN('table:style-name'))
43    formula = StringProperty(CN('table:formula'))
44    protected = BooleanProperty(CN('table:protect'))
45    content_validation_name = StringProperty(CN('table:content-validation-name'))
46
47    def __init__(self, value=None, value_type=None, currency=None, style_name=None, xmlnode=None):
48        super(Cell, self).__init__(xmlnode=xmlnode)
49        if xmlnode is None:
50            if style_name is not None:
51                self.style_name = style_name
52            if value is not None:
53                self.set_value(value, value_type, currency)
54            elif value_type is not None:
55                self._set_value_type(value_type)
56
57    @property
58    def value_type(self):
59        return self.get_attr(CN('office:value-type'))
60
61    @property
62    def value(self):
63        def convert(value, value_type):
64            if value is None:
65                pass
66            elif value_type in NUMERIC_TYPES:
67                value = float(value)
68            elif value_type == 'boolean':
69                value = True if value == 'true' else False
70            return value
71
72        t = self.value_type
73        if  t is None:
74            result = None
75        elif t == 'string':
76            result = self.plaintext()
77        else:
78            result = convert(self.xmlnode.get(TYPE_VALUE_MAP[t]), t)
79        return result
80
81    def value_as(self, value_type=None):
82        def convert(value, value_type):
83            if value is None:
84                pass
85            elif value_type in NUMERIC_TYPES:
86                value = float(value)
87            elif value_type == 'boolean':
88                value = True if value == 'true' else False
89            return value
90
91        t = value_type
92        if t is None:
93            result = None
94        elif t == 'string':
95            result = self.plaintext()
96        else:
97            result = convert(self.xmlnode.get(TYPE_VALUE_MAP[t]), t)
98        return result
99
100    def set_value(self, value, value_type=None, currency=None):
101
102        def is_valid_value(value):
103            result = True
104            if value is None:
105                result = False
106            elif isinstance(value, GenericWrapper):
107                if value.kind not in SUPPORTED_CELL_CONTENT:
108                    result = False
109            return result
110
111        def is_valid_type(value_type):
112            return True if value_type in VALID_VALUE_TYPES else False
113
114        def determine_value_type(value):
115            if type(value) == bool:
116                value_type = 'boolean'
117            elif isinstance(value, (float, int)):
118                value_type = 'float'
119            else:
120                value_type = 'string'
121            return value_type
122
123        def convert(value, value_type):
124            if isinstance(value, GenericWrapper):
125                pass
126            elif value_type == 'string':
127                value = Paragraph(tostr(value))
128            elif value_type == 'boolean':
129                value = 'true' if value else 'false'
130            else:
131                value = tostr(value)
132            return value
133
134        if not is_valid_value(value):
135            raise ValueError("invalid value: %s" % tostr(value))
136        if is_string(currency):
137            value_type = 'currency'
138        if value_type is None:
139            value_type = determine_value_type(value)
140        if not is_valid_type(value_type):
141            raise TypeError(value_type)
142
143        value = convert(value, value_type)
144        self._clear_old_value()
145        self._set_new_value(value, value_type, currency)
146
147    def _set_new_value(self, value, value_type, currency):
148        if isinstance(value, GenericWrapper):
149            value_type = 'string'
150            self.append(value)
151        else:
152            self.set_attr(TYPE_VALUE_MAP[value_type], value)
153        self._set_value_type(value_type)
154
155        if currency and (value_type == 'currency'):
156            self.set_attr(CN('office:currency'), currency)
157
158    def _set_value_type(self, value_type):
159        self.set_attr(CN('office:value-type'), value_type)
160
161    def _clear_old_value(self):
162        self._clear_value_attribute(self.value_type)
163        self._clear_content()
164
165    def _clear_content(self):
166        xmlnode = self.xmlnode
167        for _ in range(len(xmlnode)):
168            del xmlnode[0]
169
170    def _clear_value_attribute(self, value_type):
171        try:
172            attribute_name = TYPE_VALUE_MAP[value_type]
173            del self.xmlnode.attrib[attribute_name]
174        except KeyError:
175            pass
176
177    @property
178    def display_form(self):
179        return self.plaintext()
180    @display_form.setter
181    def display_form(self, text):
182        t = self.value_type
183        if t is None or t == 'string':
184            raise TypeError("not supported for value type 'None' and  'string'")
185        display_form = Paragraph(text)
186        first_paragraph = self.find(Paragraph.TAG)
187        if first_paragraph is None:
188            self.append(display_form)
189        else:
190            self.replace(first_paragraph, display_form)
191
192    def plaintext(self):
193        return "\n".join([p.plaintext() for p in iter(self)
194                          if p.kind in SUPPORTED_CELL_CONTENT])
195
196    def append_text(self, text, style_name=None):
197        if self.value_type != 'string':
198            raise TypeError('invalid cell type: %s' % self.value_type)
199        try:
200            last_child = self.get_child(-1)
201            if last_child.kind in ("Paragraph", "Heading"):
202                last_child.append(Span(text, style_name=style_name))
203                return
204        except IndexError:
205            pass
206        self.append(Paragraph(text, style_name=style_name))
207
208    @property
209    def currency(self):
210        return self.xmlnode.get(CN('office:currency'))
211
212    @property
213    def span(self):
214        rows = self.xmlnode.get(CN('table:number-rows-spanned'))
215        cols = self.xmlnode.get(CN('table:number-columns-spanned'))
216        rows = 1 if rows is None else max(1, int(rows))
217        cols = 1 if cols is None else max(1, int(cols))
218        return (rows, cols)
219
220    def _set_span(self, value):
221        rows, cols = value
222        rows = max(1, int(rows))
223        cols = max(1, int(cols))
224        if rows == 1 and cols == 1:
225            self._del_span_attributes()
226        else:
227            self._set_span_attributes(rows, cols)
228
229    def _del_span_attributes(self):
230        del self.xmlnode.attrib[CN('table:number-rows-spanned')]
231        del self.xmlnode.attrib[CN('table:number-columns-spanned')]
232
233    def _set_span_attributes(self, rows, cols):
234        self.xmlnode.set(CN('table:number-rows-spanned'), tostr(rows))
235        self.xmlnode.set(CN('table:number-columns-spanned'), tostr(cols))
236
237    @property
238    def covered(self):
239        return self.xmlnode.tag == CN('table:covered-table-cell')
240
241    def _set_covered(self, value):
242        if value:
243            self.TAG = CN('table:covered-table-cell')
244            self.xmlnode.tag = self.TAG
245            self._remove_exclusive_cell_attributes()
246        else:
247            self.TAG = CN('table:table-cell')
248            self.xmlnode.tag = self.TAG
249
250    def _remove_exclusive_cell_attributes(self):
251        for key in self.CELL_ONLY_ATTRIBS:
252            if key in self.xmlnode.attrib:
253                del self.xmlnode.attrib[key]
254
255@register_class
256class CoveredCell(Cell):
257    TAG = CN('table:covered-table-cell')
258
259    @property
260    def kind(self):
261        return 'Cell'
262