1
+ import xml .etree .ElementTree as ET
2
+ from functools import wraps
3
+
4
+ from tableaudocumentapi import Field
5
+
6
+ ########
7
+ # This is needed in order to determine if something is a string or not. It is necessary because
8
+ # of differences between python2 (basestring) and python3 (str). If python2 support is every
9
+ # dropped, remove this and change the basestring references below to str
10
+ try :
11
+ basestring
12
+ except NameError : # pragma: no cover
13
+ basestring = str
14
+
15
+
16
+ class AlreadyMemberOfThisFolderException (Exception ):
17
+ pass
18
+
19
+
20
+ class MemberOfMultipleFoldersException (Exception ):
21
+ pass
22
+
23
+
24
+ def argument_is_one_of (* allowed_values ):
25
+ def property_type_decorator (func ):
26
+ @wraps (func )
27
+ def wrapper (self , value ):
28
+ if value not in allowed_values :
29
+ error = "Invalid argument: {0}. {1} must be one of {2}."
30
+ msg = error .format (value , func .__name__ , allowed_values )
31
+ raise ValueError (error )
32
+ return func (self , value )
33
+ return wrapper
34
+ return property_type_decorator
35
+
36
+
37
+ class FolderItem (object ):
38
+ """ FolderItems belong to Folders and describe the Field-Objects
39
+ that belong to a folder
40
+ """
41
+
42
+ def __init__ (self , name , _type ):
43
+ self .name = name
44
+ self .type = _type
45
+
46
+ @classmethod
47
+ def from_xml (cls , xml ):
48
+ return cls (xml .get ('name' , None ), xml .get ('type' , None ))
49
+
50
+ @classmethod
51
+ def from_field (cls , field ):
52
+ return cls (field .id , 'field' )
53
+
54
+
55
+ class Folder (object ):
56
+ """ This class represents a folder in a Datasource.
57
+
58
+ Folders have a name, a role (dimensions or measures) and contain Items
59
+ """
60
+
61
+ def __init__ (self , datasource , xml ):
62
+ self ._datasource = datasource
63
+ self ._xml = xml
64
+ self .name = self ._xml .get ('name' , None )
65
+ self .role = self ._xml .get ('role' , None )
66
+ folder_item_xml = self ._xml .findall ('folder-item' )
67
+ self ._folder_items = [FolderItem .from_xml (xml ) for xml in folder_item_xml ]
68
+
69
+ # Alternative constructors
70
+
71
+ @classmethod
72
+ def all_folders_from_datasource (cls , datasource ):
73
+ folders_xml = datasource ._datasourceTree .findall ('.//folder' )
74
+ return [cls (datasource , xml ) for xml in folders_xml ]
75
+
76
+ @classmethod
77
+ def from_name_and_role (cls , name , role , parent_datasource ):
78
+ """Creates a new folder with a given name and a given role.
79
+ """
80
+ attributes = {
81
+ 'name' : name ,
82
+ 'role' : role
83
+ }
84
+ xml = ET .Element ('folder' , attrib = attributes )
85
+ return cls (parent_datasource , xml )
86
+
87
+ # Properties
88
+
89
+ @property
90
+ def folder_items (self ):
91
+ return self ._folder_items
92
+
93
+ @property
94
+ def name (self ):
95
+ return self ._name
96
+
97
+ @name .setter
98
+ def name (self , name ):
99
+ self ._name = name
100
+ self ._xml .set ('name' , name )
101
+
102
+ @property
103
+ def role (self ):
104
+ return self ._role
105
+
106
+ @property
107
+ def xml (self ):
108
+ return self ._xml
109
+
110
+ @role .setter
111
+ @argument_is_one_of ('dimensions' , 'measures' )
112
+ def role (self , role ):
113
+ self ._role = role
114
+ self ._xml .set ('role' , role )
115
+
116
+ # Functions that deal with folder-items
117
+
118
+ def add_field (self , field ):
119
+ """ Adds a field to this folder
120
+ """
121
+ if not isinstance (field , Field ):
122
+ msg = 'Can only add Fields to Folders, not {}'
123
+ raise ValueError (msg .format (type (field )))
124
+ if self .has_item (field ):
125
+ raise AlreadyMemberOfThisFolderException (field )
126
+ if any (f .has_item (field ) for f in self ._datasource .folders .values ()):
127
+ raise MemberOfMultipleFoldersException (field )
128
+
129
+ self ._add_field (field )
130
+
131
+ def _add_field (self , field ):
132
+ """ Internal function to add a field
133
+ """
134
+ folder_item = FolderItem .from_field (field )
135
+ self ._folder_items .append (folder_item )
136
+ name , _type = folder_item .name , folder_item .type
137
+ ET .SubElement (self ._xml , 'folder-item' , {'name' : name , 'type' : _type })
138
+
139
+ def remove_field (self , field ):
140
+ """ Removes a field from this folder
141
+ """
142
+ if not isinstance (field , Field ):
143
+ msg = 'Can only remove Fields from Folders, not {}'
144
+ raise ValueError (msg .format (type (field )))
145
+ if not self .has_item (field ):
146
+ raise ValueError ('This field is not a member of the folder' )
147
+
148
+ self ._remove_field (field )
149
+
150
+ def _remove_field (self , field ):
151
+ """ Internal function to remove field
152
+ """
153
+ # remove from the data structure
154
+ folder_items = filter (lambda f : f .name == field .id , self .folder_items )
155
+ folder_item = list (folder_items )[0 ]
156
+ self .folder_items .remove (folder_item )
157
+
158
+ # remove from xml
159
+ xml_elem = self .xml .find ("folder-item[@name='{}']" .format (field .id ))
160
+ self .xml .remove (xml_elem )
161
+
162
+ # Utility functions
163
+
164
+ def has_item (self , item ):
165
+ """ Returns True if the given item is a FolderItem of this Folder.
166
+ Item may be String, Field or FolderItem
167
+ """
168
+ if isinstance (item , FolderItem ):
169
+ return item in self .folder_items
170
+ elif isinstance (item , Field ):
171
+ return item .id in map (lambda fi : fi .name , self .folder_items )
172
+ elif isinstance (item , basestring ):
173
+ return item in map (lambda fi : fi .name , self .folder_items )
174
+ else :
175
+ msg = 'Argument must be either String or FolderItem, not {}'
176
+ raise ValueError (msg .format (type (item )))
0 commit comments