Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 167 additions & 1 deletion tableaudocumentapi/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from tableaudocumentapi import Field
from tableaudocumentapi.multilookup_dict import MultiLookupDict
from tableaudocumentapi.xfile import xml_open
from tableaudocumentapi import metadata_structure

########
# This is needed in order to determine if something is a string or not. It is necessary because
Expand Down Expand Up @@ -240,9 +241,12 @@ def clear_repository_location(self):
@property
def fields(self):
if not self._fields:
self._fields = self._get_all_fields()
self._refresh_fields()
return self._fields

def _refresh_fields(self):
self._fields = self._get_all_fields()

def _get_all_fields(self):
column_field_objects = self._get_column_objects()
existing_column_fields = [x.id for x in column_field_objects]
Expand All @@ -258,3 +262,165 @@ def _get_metadata_objects(self):
def _get_column_objects(self):
return [_column_object_from_column_xml(self._datasourceTree, xml)
for xml in self._datasourceTree.findall('.//column')]

def add_field(self, name, datatype, role, type, caption):
""" Adds a base field object with the given values.

Args:
name: Name of the new Field. String.
datatype: Datatype of the new field. String.
role: Role of the new field. String.
type: Type of the new field. String.
caption: Caption of the new field. String.

Returns:
The new field that was created. Field.
"""
# TODO: A better approach would be to create an empty column and then
# use the input validation from its "Field"-object-representation to set values.
# However, creating an empty column causes errors :(

# If no caption is specified, create one with the same format Tableau does
if not caption:
caption = name.replace('[', '').replace(']', '').title()

# Create the elements
column = ET.Element('column')
column.set('caption', caption)
column.set('datatype', datatype)
column.set('role', role)
column.set('type', type)
column.set('name', name)

self._datasourceTree.getroot().append(column)

# Refresh fields to reflect changes and return the Field object
self._refresh_fields()
return self.fields[name]

def remove_field(self, field):
""" Remove a given field

Args:
field: The field to remove. ET.Element

Returns:
None
"""
if not field or not isinstance(field, Field):
raise ValueError("Need to supply a field to remove element")

self._datasourceTree.getroot().remove(field.xml)
self._refresh_fields()

def add_metadata_record(self, name, parent, datatype):
""" This function creates and appends a full metadata-record column.

This function depends on the datatype-to-values information from metadata_structure.py.

TODO: This function is huge and compicated. Rework this ASAP!
Maybe create a own file for metadata-fields?
"""
# get first part of values needed for a meta-record
db_class = self.connections[0].dbclass
defaults = getattr(metadata_structure, db_class, None)

# TODO Are those mappings valid for other databases?
# If not: Create default mappings for other databases
if not defaults:
msg = "No default mappings are available for {}-databases.".format(db_class)
raise NotImplementedError(msg)

# get second part of values needed for a meta-record
passed_values = {
'remote-name': name,
'local-name': '[{}]'.format(name),
'parent-name': '[{}]'.format(parent),
'remote-alias': name,
'local-type': datatype,
}

# merge them
record_attributes = dict(itertools.chain(defaults[datatype].items(), passed_values.items()))

# determine the value of "ordinal" for new record
ordinals = self._datasourceTree.findall('.//*/metadata-record/ordinal')
max_nr = max([int(x.text) for x in ordinals] + [0])
record_attributes['ordinal'] = str(max_nr + 1)

# create base record
record = ET.Element('metadata-record')
record.set('class', 'column')

# add all sub-elements of the metadata-record field
for key, value in record_attributes.items():
if key == '__extra__':
# Those need special treatment
continue
elem = ET.Element(key)
elem.text = value
record.append(elem)

# add the 'collation' sub-elements if the metadata_structure provides them
collation_info = record_attributes.get('__extra__', {}).get('collation', {})
if collation_info:
collation = ET.Element('collation')
collation.set('flag', collation_info['flag'])
collation.set('name', collation_info['name'])
record.append(collation)

# add the 'attribute' sub-elements if the metadata_structure provides them
attribute_info = record_attributes.get('__extra__', {}).get('attributes', {})
if attribute_info:

attrs = ET.Element('attributes')

attr1 = ET.Element('attribute')
attr1.set('datatype', 'string')
attr1.set('name', 'DebugRemoteType')
attr1.text = attribute_info['DebugRemoteType']
attrs.append(attr1)

attr2 = ET.Element('attribute')
attr2.set('datatype', 'string')
attr2.set('name', 'DebugWireType')
attr2.text = attribute_info['DebugWireType']
attrs.append(attr2)

record.append(attrs)

# append to the base xml
base = self._datasourceTree.find('.//metadata-records')
base.append(record)
self._refresh_fields()

###########
# Calculations
###########
@property
def calculations(self):
""" Returns all calculated fields.
"""
# TODO: There is a default [Number of Records] calculation.
# Should this be excluded so users can't meddle with it?
return {k: v for k, v in self.fields.items() if v.calculation is not None}

def add_calculation(self, caption, formula, datatype, role, type):
""" Adds a calculated field with the given values.

Args:
caption: Caption of the new calculation. String.
formula: Formula of the new calculation. String.
datatype: Datatype of the new calculation. String.
role: Role of the new calculation. String.
type: Type of the new calculation. String.

Returns:
The new calculated field that was created. Field.
"""
# Dynamically create the name of the field
name = '[Calculation_{}]'.format(str(uuid4().int)[:18])
field = self.add_field(name, datatype, role, type, caption)
field.calculation = formula

return field
Loading