Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
XNAT-Group
XNAT_Query_Client_XSA
Commits
07f67d22
Commit
07f67d22
authored
Mar 10, 2015
by
Franziska Koehn
Browse files
Merge branch 'feature/cSubAssessment-Extension' into develop
parents
364aa838
47f71165
Changes
12
Hide whitespace changes
Inline
Side-by-side
datatypes/ext_cSubAssessment.JSON
0 → 100644
View file @
07f67d22
{
"root-type"
:
"ext:cSubAssessment"
,
"comparison_data"
:{
"identifier"
:[
"expt_id"
,
"project"
],
"extra-source"
:
"/projects/{project}/experiments/{expt_id}"
},
"fields"
:[
{
"label"
:
"Subject-ID"
,
"field"
:
"ext:cSubAssessment/SUBJECT_ID"
,
"required"
:
false
,
"key"
:
"subject_id"
},
{
"label"
:
"Experiment-ID"
,
"field"
:
"ext:cSubAssessment/EXPT_ID"
,
"required"
:
true
,
"key"
:
"expt_id"
},
{
"label"
:
"Date"
,
"field"
:
"ext:cSubAssessment/DATE"
,
"required"
:
false
,
"key"
:
"date"
},
{
"label"
:
"Age"
,
"field"
:
"ext:cSubAssessment/AGE"
,
"required"
:
false
,
"key"
:
"age"
},
{
"label"
:
"Projects"
,
"field"
:
"ext:cSubAssessment/PROJECTS"
,
"required"
:
false
,
"key"
:
"projects"
},
{
"label"
:
"Project"
,
"field"
:
"ext:cSubAssessment/PROJECT"
,
"required"
:
true
,
"key"
:
"project"
},
{
"label"
:
"Label"
,
"field"
:
"ext:cSubAssessment/LABEL"
,
"required"
:
false
,
"key"
:
"label"
},
{
"label"
:
"Insert Date"
,
"field"
:
"ext:cSubAssessment/INSERT_DATE"
,
"required"
:
false
,
"key"
:
"insert_date"
},
{
"label"
:
"Insert User"
,
"field"
:
"ext:cSubAssessment/INSERT_USER"
,
"required"
:
false
,
"key"
:
"insert_user"
},
{
"label"
:
"Datatype"
,
"field"
:
"ext:cSubAssessment/DATATYPE"
,
"required"
:
false
,
"key"
:
"datatype"
},
{
"label"
:
"Datatype Category"
,
"field"
:
"ext:cSubAssessment/DATATYPE_CATEGORY"
,
"required"
:
false
,
"key"
:
"datatype_category"
},
{
"label"
:
"Import Date"
,
"field"
:
"ext:cSubAssessment/IMPORT_DATE"
,
"required"
:
false
,
"key"
:
"import_date"
},
{
"label"
:
"Project Protocol"
,
"field"
:
"ext:cSubAssessment/PROJECT_PROTOCOL"
,
"required"
:
false
,
"key"
:
"project_protocol"
},
{
"label"
:
"Validation Method"
,
"field"
:
"ext:cSubAssessment/VALIDATION_METHOD"
,
"required"
:
false
,
"key"
:
"validation_method"
},
{
"label"
:
"Validation Status"
,
"field"
:
"ext:cSubAssessment/VALIDATION_STATUS"
,
"required"
:
false
,
"key"
:
"validation_status"
}
]
}
datatypes/xnat_mrScanData.JSON
View file @
07f67d22
{
"root-type"
:
"xnat:mrScanData"
,
"comparison_data"
:{
"identifier"
:[
"id"
,
"xnat_mrsessiondata_session_id"
]},
"REST-API"
:
"/data/archive/projects/{xnat_mrsessiondata_project}/subjects/{xnat_mrsessiondata_subject_id}/experiments/{xnat_mrsessiondata_session_id}/scans/{id}/resources/DICOM/files?format=zip"
,
"fields"
:[
{
"label"
:
"Type"
,
"field"
:
"xnat:mrScanData/TYPE"
,
"required"
:
true
,
"key"
:
"type"
},
...
...
datatypes/xnat_mrSessionData.JSON
View file @
07f67d22
{
"root-type"
:
"xnat:mrSessionData"
,
"comparison_data"
:{
"identifier"
:[
"session_id"
]},
"fields"
:[
{
"label"
:
"Session-ID"
,
"field"
:
"xnat:mrSessionData/SESSION_ID"
,
"required"
:
false
,
"key"
:
"session_id"
},
{
"label"
:
"Date"
,
"field"
:
"xnat:mrSessionData/DATE"
,
"required"
:
false
,
"key"
:
"date"
},
...
...
datatypes/xnat_projectData.JSON
View file @
07f67d22
{
"root-type"
:
"xnat:projectData"
,
{
"root-type"
:
"xnat:projectData"
,
"comparison_data"
:{
"identifier"
:[
"id"
]},
"fields"
:[
{
"label"
:
"Project-ID"
,
"field"
:
"xnat:projectData/ID"
,
"required"
:
true
,
"key"
:
"id"
},
{
"label"
:
"NAME"
,
"field"
:
"xnat:projectData/NAME"
,
"required"
:
false
,
"key"
:
"name"
},
...
...
datatypes/xnat_subjectData.JSON
View file @
07f67d22
{
"root-type"
:
"xnat:subjectData"
,
{
"root-type"
:
"xnat:subjectData"
,
"comparison_data"
:{
"identifier"
:[
"subject_id"
]},
"fields"
:[
{
"label"
:
"Subject-ID"
,
"field"
:
"xnat:subjectData/SUBJECT_ID"
,
"required"
:
true
,
"key"
:
"subject_id"
},
{
"label"
:
"Subject-Label"
,
"field"
:
"xnat:subjectData/SUBJECT_LABEL"
,
"required"
:
false
,
"key"
:
"subject_label"
},
...
...
xsa/datatypereader.py
View file @
07f67d22
...
...
@@ -39,6 +39,26 @@ def get_all(force=False):
return
get_all
()
get_all
.
cache
=
None
def
get_comparison_extra_source
(
root_type
):
for
data
in
get_all
():
if
data
[
'root-type'
]
==
root_type
:
if
not
'comparison_data'
in
data
or
not
data
[
'comparison_data'
]:
raise
NoComparisonDataError
(
"No comparison-data was defined for this datatype"
)
elif
not
'extra-source'
in
data
[
'comparison_data'
]
or
not
data
[
'comparison_data'
][
'extra-source'
]:
raise
NoComparisonDataError
(
"No Source of additional Data was defined for this datatype"
)
else
:
return
data
[
'comparison_data'
][
'extra-source'
]
def
get_comparison_identifier
(
root_type
):
for
data
in
get_all
():
if
data
[
'root-type'
]
==
root_type
:
if
not
'comparison_data'
in
data
or
not
data
[
'comparison_data'
]:
raise
NoComparisonDataError
(
"No comparison-data was defined for this datatype"
)
elif
not
'identifier'
in
data
[
'comparison_data'
]
or
not
data
[
'comparison_data'
][
'identifier'
]:
raise
NoComparisonDataError
(
"No Identifier for this datatype was defined"
)
else
:
return
data
[
'comparison_data'
][
'identifier'
]
def
get_rest
(
root_type
):
"""Returns REST-API-Interface of a given Root-Type, defined in its json-file.
...
...
@@ -83,6 +103,18 @@ def get_labels(root_type):
return
[]
def
get_keys
(
root_type
):
"""Returns a list of field-keys of a given root_type.
**Parameters**
:root_type: str, Root-Type of returned field-labels
"""
fields
=
get_fields
(
root_type
)
if
fields
is
not
None
:
return
list
(
f
[
'key'
]
for
f
in
fields
)
return
[]
def
get_field_label_by_key
(
root_type
,
field_key
):
"""Returns the label of the given field-key.
...
...
xsa/errors.py
View file @
07f67d22
...
...
@@ -38,6 +38,10 @@ class NoRestApiError(Error):
"""Should be raised when no REST-API was defined in the json-file of a root-type"""
pass
class
NoComparisonDataError
(
Error
):
"""Should be raised when no comparision was defined in the json-file of a root-type"""
pass
class
CorruptedQueryError
(
Error
):
"""Should be raised when the query-definition can not be loaded from file."""
pass
xsa/queries.py
View file @
07f67d22
...
...
@@ -164,6 +164,28 @@ def disconnect_xnat_server():
get_xnat_server_connection
.
cache
=
None
except
:
return
def
query_for_additional_data
(
rest
,
result
,
host
,
creds
):
import
re
def
subfunc
(
f
):
key
=
f
.
group
(
0
).
strip
(
"{}"
)
return
result
[
key
]
ret
=
re
.
sub
(
'\{\w+\}'
,
subfunc
,
rest
)
user
,
passw
=
creds
central
=
get_xnat_server_connection
(
host
=
host
,
user
=
user
,
passw
=
passw
)
exp
=
central
.
select
(
ret
)
new_fields
=
exp
.
xpath
(
"//xnat:field"
)
new_result
=
result
.
copy
()
for
f
in
new_fields
:
fieldname
=
f
.
get
(
"name"
)
value
=
exp
.
xpath
(
"//xnat:field[@name='"
+
fieldname
+
"']/child::text()"
)
new_result
[
fieldname
]
=
''
.
join
(
value
).
replace
(
"
\n
"
,
""
)
return
new_result
def
search_for
(
host
,
root_element
,
constraints
,
search_fields
,
user
,
passw
):
"""
Does a search for given values. raises xsa -Exceptions
...
...
@@ -182,6 +204,7 @@ def search_for(host, root_element, constraints, search_fields, user, passw):
:xsa.errors.ResponseNotReady:
:xsa.errors.QueryError:
"""
import
xsa.errors
...
...
xsagtk/main_controller.py
View file @
07f67d22
...
...
@@ -10,9 +10,14 @@ This module contains the controller that provides the communication between view
import
gtk
import
gobject
import
xsa.datatypereader
class
QueryController
(
gobject
.
GObject
):
"""Provides communication between different views."""
results
=
[]
_host
=
""
_user
=
""
_passw
=
""
...
...
@@ -83,7 +88,6 @@ class QueryController(gobject.GObject):
def
on_search_query
(
host
,
credentials
,
root_type
,
query
,
fields
):
user
,
passw
=
credentials
import
xsa.queries
as
queries
import
xsa.errors
if
passw
==
''
or
user
==
''
or
host
==
''
:
...
...
@@ -104,7 +108,7 @@ class QueryController(gobject.GObject):
return
try
:
query_results
=
queries
.
search_for
(
host
,
query_results
=
xsa
.
queries
.
search_for
(
host
,
root_type
,
query
,
fields
,
...
...
@@ -119,11 +123,12 @@ class QueryController(gobject.GObject):
md
.
run
()
md
.
destroy
()
self
.
results
=
[]
except
Exception
:
except
Exception
as
e
:
md
=
gtk
.
MessageDialog
(
type
=
gtk
.
MESSAGE_WARNING
,
buttons
=
gtk
.
BUTTONS_OK
,
message_format
=
"An unexpected Error occured
.
"
message_format
=
"An unexpected Error occured
:
"
)
md
.
format_secondary_text
(
e
)
md
.
run
()
md
.
destroy
()
self
.
results
=
[]
...
...
@@ -134,16 +139,28 @@ class QueryController(gobject.GObject):
self
.
statusbar
.
push
(
0
,
"%s Results"
%
len
(
self
.
results
))
self
.
chartview
.
update_chart_view
(
self
.
results
,
root_type
)
if
self
.
results
==
[]:
self
.
menuview
.
disable_download_button
(
False
,
"No Results for downloading"
)
self
.
menuview
.
disable_export_button
(
False
,
"No Results for exporting"
)
self
.
menuview
.
disable_adddata_button
(
False
,
"No Results for getting additional data"
)
return
else
:
try
:
xsa
.
datatypereader
.
get_rest
(
root_type
)
self
.
menuview
.
disable_download_button
(
True
,
"Download selected items from result-table"
)
except
xsa
.
errors
.
NoRestApiError
as
e
:
self
.
menuview
.
disable_download_button
(
False
,
str
(
e
))
self
.
menuview
.
disable_adddata_button
(
True
,
"Get additional data"
)
self
.
menuview
.
disable_export_button
(
True
,
"Export Result-Table as csv-file"
)
self
.
root
=
self
.
queryview
.
get_root_type
()
self
.
query
=
self
.
queryview
.
get_query
()
self
.
labels
=
self
.
queryview
.
get_fields
()
import
xsa.datatypereader
as
type_reader
fields
=
type_reader
.
get_fields_from_labels
(
self
.
labels
,
self
.
root
)
on_search_query
(
self
.
host
,
self
.
credentials_tuple
,
self
.
root
,
self
.
query
,
fields
)
fields
=
xsa
.
datatypereader
.
get_fields_from_labels
(
self
.
labels
,
self
.
root
)
on_search_query
(
self
.
host
,
self
.
credentials_tuple
,
self
.
root
,
self
.
query
,
fields
)
def
__init__
(
self
,
main
,
queryview
,
menuview
,
chartview
,
resultsview
,
statusbar
):
...
...
@@ -167,6 +184,48 @@ class QueryController(gobject.GObject):
self
.
resultsview
=
resultsview
self
.
statusbar
=
statusbar
def
callback_get_additional_data
(
*
_
):
rest
=
xsa
.
datatypereader
.
get_comparison_extra_source
(
self
.
root
)
new_data_sets
=
[]
for
r
in
self
.
results
:
new_data_sets
.
append
(
xsa
.
queries
.
query_for_additional_data
(
rest
,
r
,
self
.
host
,
self
.
credentials_tuple
))
keys
=
[]
for
data_set
in
new_data_sets
:
for
key
in
data_set
.
keys
():
keys
.
append
(
key
)
new_result
=
[]
for
data_set
in
new_data_sets
:
dic
=
{}
for
key
in
keys
:
try
:
dic
[
key
]
=
data_set
[
key
]
except
:
dic
[
key
]
=
"N/A"
new_result
.
append
(
dic
)
from
pyxnat.core.jsonutil
import
JsonTable
self
.
results
=
JsonTable
(
new_result
)
self
.
resultsview
.
show_data
(
self
.
results
,
self
.
root
)
self
.
chartview
.
update_chart_view
(
self
.
results
,
self
.
root
)
def
callback_export_csv
(
*
_
):
dialog
=
gtk
.
FileChooserDialog
(
"Save as..."
,
None
,
gtk
.
FILE_CHOOSER_ACTION_SAVE
,
(
gtk
.
STOCK_CANCEL
,
gtk
.
RESPONSE_CANCEL
,
gtk
.
STOCK_OPEN
,
gtk
.
RESPONSE_OK
))
dialog
.
set_default_response
(
gtk
.
RESPONSE_CANCEL
)
response
=
dialog
.
run
()
if
response
==
gtk
.
RESPONSE_OK
:
self
.
results
.
dump_csv
(
dialog
.
get_filename
())
dialog
.
destroy
()
def
callback_edit_Server_Settings
(
*
_
):
dialog
=
gtk
.
Dialog
(
"Server Settings"
,
...
...
@@ -178,7 +237,7 @@ class QueryController(gobject.GObject):
dialog
.
set_size_request
(
330
,
180
)
hbox
=
gtk
.
HBox
(
False
,
8
)
hbox
.
set_border_width
(
8
)
dialog
.
vbox
.
pack_start
(
hbox
,
False
,
Fals
e
,
0
)
dialog
.
vbox
.
pack_start
(
hbox
,
True
,
Tru
e
,
0
)
stock
=
gtk
.
image_new_from_stock
(
gtk
.
STOCK_DIALOG_AUTHENTICATION
,
...
...
@@ -221,6 +280,16 @@ class QueryController(gobject.GObject):
if
response
==
gtk
.
RESPONSE_OK
:
self
.
credentials_tuple
=
(
entry_user
.
get_text
(),
entry_passw
.
get_text
())
self
.
host
=
entry_host
.
get_text
()
import
xsa.queries
try
:
xsa
.
queries
.
get_xnat_server_connection
(
force
=
True
,
host
=
self
.
host
,
user
=
self
.
credentials_tuple
[
0
],
passw
=
self
.
credentials_tuple
[
1
])
except
Exception
as
e
:
md
=
gtk
.
MessageDialog
(
type
=
gtk
.
MESSAGE_WARNING
,
buttons
=
gtk
.
BUTTONS_OK
,
message_format
=
str
(
e
)
)
md
.
run
()
md
.
destroy
()
dialog
.
destroy
()
...
...
@@ -232,9 +301,6 @@ class QueryController(gobject.GObject):
if
self
.
resultsview
.
get_selected_items
()
==
[]:
return
import
xsa.queries
as
queries
import
xsa.datatypereader
as
type_reader
dialog
=
gtk
.
FileChooserDialog
(
"Open.."
,
None
,
gtk
.
FILE_CHOOSER_ACTION_SELECT_FOLDER
,
...
...
@@ -244,7 +310,7 @@ class QueryController(gobject.GObject):
response
=
dialog
.
run
()
if
response
==
gtk
.
RESPONSE_OK
:
rest
=
type
_
reader
.
get_rest
(
self
.
root
)
rest
=
xsa
.
data
typereader
.
get_rest
(
self
.
root
)
enabled
=
(
d
for
d
in
self
.
resultsview
.
get_store
()
if
d
[
0
])
for
d
in
enabled
:
def
stop_spinner
(
_row
):
...
...
@@ -252,7 +318,7 @@ class QueryController(gobject.GObject):
_row
[
0
]
=
False
# Checkbox to false
d
[
3
]
=
0
d
[
2
]
=
True
# Show Spinner
queries
.
download_async
(
d
[
1
],
xsa
.
queries
.
download_async
(
d
[
1
],
self
.
host
,
self
.
credentials_tuple
,
rest
,
...
...
@@ -262,9 +328,6 @@ class QueryController(gobject.GObject):
)
dialog
.
destroy
()
def
disable_download_button
(
_
,
is_enable
,
text
):
self
.
menuview
.
disable_download_button
(
is_enable
,
text
)
root_key
=
'root'
query_key
=
'query'
labels_key
=
'labels'
...
...
@@ -314,15 +377,13 @@ class QueryController(gobject.GObject):
query
=
f
[
query_key
],
field_labels
=
f
[
labels_key
]
)
dialog
.
destroy
()
self
.
menuview
.
connect
(
"spawn-connection-dialog"
,
callback_edit_Server_Settings
)
self
.
menuview
.
connect
(
"send-query"
,
self
.
send_query
)
self
.
menuview
.
connect
(
"toggle-selection"
,
event_clicked_toggle_selection
)
self
.
menuview
.
connect
(
"download-selection"
,
event_clicked_download
)
self
.
menuview
.
connect
(
"save-query"
,
save_query
)
self
.
menuview
.
connect
(
"load-query"
,
load_query
)
self
.
query
view
.
connect
(
"
rest-api"
,
disable_download_button
)
self
.
menuview
.
connect
(
"get-additional-data"
,
callback_get_additional_data
)
self
.
menu
view
.
connect
(
"
export-csv"
,
callback_export_csv
)
xsagtk/menu_view.py
View file @
07f67d22
...
...
@@ -13,6 +13,16 @@ import gobject
class
MenuView
(
gtk
.
Toolbar
):
"""A ToolBar containing Buttons"""
def
disable_export_button
(
self
,
is_enable
,
text
):
"""Disables export-button and sets given text as tooltip.
**Parameters**
:is_enable: bool: true for enabling and false for disabling export-button
:text: str, that will be set as tooltip
"""
self
.
tb_export
.
set_tooltip_text
(
text
)
self
.
tb_export
.
set_sensitive
(
is_enable
)
def
disable_download_button
(
self
,
is_enable
,
text
):
"""Disables download-button and sets given text as tooltip.
...
...
@@ -23,6 +33,16 @@ class MenuView(gtk.Toolbar):
self
.
tb_downl
.
set_tooltip_text
(
text
)
self
.
tb_downl
.
set_sensitive
(
is_enable
)
def
disable_adddata_button
(
self
,
is_enable
,
text
):
"""Disables get-additional-data-button and sets given text as tooltip.
**Parameters**
:is_enable: bool: true for enabling and false for disabling button
:text: str, that will be set as tooltip
"""
self
.
tb_adddata
.
set_tooltip_text
(
text
)
self
.
tb_adddata
.
set_sensitive
(
is_enable
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
"""Creates buttons for this Toolbar and defines signals when this buttons will be pressed.
...
...
@@ -41,6 +61,11 @@ class MenuView(gtk.Toolbar):
gobject
.
SIGNAL_RUN_FIRST
,
gobject
.
TYPE_NONE
,
())
gobject
.
signal_new
(
"get-additional-data"
,
MenuView
,
gobject
.
SIGNAL_RUN_FIRST
,
gobject
.
TYPE_NONE
,
())
gobject
.
signal_new
(
"send-query"
,
MenuView
,
gobject
.
SIGNAL_RUN_FIRST
,
...
...
@@ -66,6 +91,11 @@ class MenuView(gtk.Toolbar):
gobject
.
SIGNAL_RUN_FIRST
,
gobject
.
TYPE_NONE
,
())
gobject
.
signal_new
(
"export-csv"
,
MenuView
,
gobject
.
SIGNAL_RUN_FIRST
,
gobject
.
TYPE_NONE
,
())
tb_savequery
=
gtk
.
ToolButton
()
tb_savequery
.
set_label
(
"Save Query"
)
...
...
@@ -109,3 +139,23 @@ class MenuView(gtk.Toolbar):
self
.
tb_downl
.
set_stock_id
(
gtk
.
STOCK_SAVE
)
self
.
tb_downl
.
connect
(
"clicked"
,
lambda
*
_
:
self
.
emit
(
"download-selection"
))
self
.
insert
(
self
.
tb_downl
,
6
)
self
.
tb_export
=
gtk
.
ToolButton
()
self
.
tb_export
.
set_label
(
"Export As CSV"
)
self
.
tb_export
.
set_stock_id
(
gtk
.
STOCK_CONVERT
)
self
.
tb_export
.
connect
(
"clicked"
,
lambda
*
_
:
self
.
emit
(
"export-csv"
))
self
.
insert
(
self
.
tb_export
,
7
)
self
.
insert
(
gtk
.
SeparatorToolItem
(),
8
)
self
.
tb_adddata
=
gtk
.
ToolButton
()
self
.
tb_adddata
.
set_label
(
"Get Additional Data"
)
self
.
tb_adddata
.
set_stock_id
(
gtk
.
STOCK_INDEX
)
self
.
tb_adddata
.
connect
(
"clicked"
,
lambda
*
_
:
self
.
emit
(
"get-additional-data"
))
self
.
insert
(
self
.
tb_adddata
,
9
)
self
.
disable_download_button
(
False
,
"No Results"
)
self
.
disable_adddata_button
(
False
,
"No Results"
)
self
.
disable_export_button
(
False
,
"No Results"
)
xsagtk/query_view.py
View file @
07f67d22
...
...
@@ -13,7 +13,7 @@ class QueryView(gtk.VBox):
"""Contains all widgets for creating the query"""
def
__init__
(
self
,
*
args
,
**
kwargs
):
"""Creates
signal "rest-api". Creates
all query-view-widgets.
"""Creates all query-view-widgets.
**Parameters**
:\*args: same as gtk.VBox
...
...
@@ -22,13 +22,6 @@ class QueryView(gtk.VBox):
super
(
QueryView
,
self
).
__init__
(
*
args
,
**
kwargs
)
import
gobject
gobject
.
signal_new
(
"rest-api"
,
QueryView
,
gobject
.
SIGNAL_RUN_FIRST
,
gobject
.
TYPE_NONE
,
(
bool
,
str
))
# root-type
hBox_root_type
=
gtk
.
HBox
()
...
...
@@ -42,13 +35,6 @@ class QueryView(gtk.VBox):
import
xsa.datatypereader
as
type_reader
import
xsa.errors
selected
=
combobox
.
get_active_text
()
try
:
type_reader
.
get_rest
(
selected
)
except
xsa
.
errors
.
NoRestApiError
as
e
:
self
.
emit
(
"rest-api"
,
False
,
e
)
else
:
self
.
emit
(
"rest-api"
,
True
,
"Download selected items from result-table"
)
self
.
treeView_search
.
set_root_type
(
selected
)
self
.
treeView_fields
.
reset_fields
(
selected
)
...
...
xsagtk/xsa_app_main.py
View file @
07f67d22
...
...
@@ -75,7 +75,6 @@ class XsaApp(gtk.Window):
vpaned
.
add1
(
self
.
resultsview
)
# chart
from
xsagtk.chart_view
import
ChartView
self
.
chartview
=
ChartView
()
vpaned
.
add2
(
self
.
chartview
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment