From 324ae0e6b8acfff52b174b3b730e9a9b1f3b74ca Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Tue, 20 Sep 2016 17:12:34 -0600 Subject: [PATCH 01/19] #293 Updating the series_service and cv_servic. Also fixed some warnings --- odmtools/controller/olvDataTable.py | 3 +- odmtools/gui/plotProbability.py | 5 +- odmtools/odmdata/__init__.py | 2 +- odmtools/odmdata/session_factory.py | 114 +++++++++---------- odmtools/odmservices/cv_service.py | 140 ++++++++++++++++------- odmtools/odmservices/series_service.py | 143 ++++-------------------- odmtools/odmservices/service_manager.py | 9 +- 7 files changed, 189 insertions(+), 227 deletions(-) diff --git a/odmtools/controller/olvDataTable.py b/odmtools/controller/olvDataTable.py index 58110d1..9c9ef48 100644 --- a/odmtools/controller/olvDataTable.py +++ b/odmtools/controller/olvDataTable.py @@ -31,7 +31,8 @@ def init(self, memDB): self.SetColumns(columns) self.dataframe = self.memDB.getDataValuesDF() sort_by_index = list(self.dataframe.columns).index("LocalDateTime") - self.dataframe.sort(self.dataframe.columns[sort_by_index], inplace=True) + # self.dataframe.sort(self.dataframe.columns[sort_by_index], inplace=True) # sort() is depecrated + self.dataframe.sort_values(self.dataframe.columns[sort_by_index], inplace=True) self.dataObjects = self.dataframe.values.tolist() self.SetObjectGetter(self.ObjectGetter) diff --git a/odmtools/gui/plotProbability.py b/odmtools/gui/plotProbability.py index b875a49..1cc1f38 100644 --- a/odmtools/gui/plotProbability.py +++ b/odmtools/gui/plotProbability.py @@ -136,8 +136,9 @@ def updatePlot(self): #self.prob.append( #prop = oneSeries.Probability.plot(column="DataValue", ax=self.plots) - xValues = oneSeries.Probability.xAxis.order().values - yValues = oneSeries.Probability.yAxis.order().values + # xValues = oneSeries.Probability.xAxis.order().values + xValues = oneSeries.Probability.xAxis.sort_values().values + yValues = oneSeries.Probability.yAxis.sort_values().values ax = self.plots.plot(xValues, yValues, 'bs', color=oneSeries.color, label=oneSeries.plotTitle) diff --git a/odmtools/odmdata/__init__.py b/odmtools/odmdata/__init__.py index dfb89a2..dd2a352 100644 --- a/odmtools/odmdata/__init__.py +++ b/odmtools/odmdata/__init__.py @@ -14,7 +14,7 @@ from sample_medium_cv import SampleMediumCV from sample_type_cv import SampleTypeCV from series import Series -from session_factory import SessionFactory +# from session_factory import SessionFactory from site import Site from site_type_cv import SiteTypeCV from source import Source diff --git a/odmtools/odmdata/session_factory.py b/odmtools/odmdata/session_factory.py index 0d2d488..8afda48 100644 --- a/odmtools/odmdata/session_factory.py +++ b/odmtools/odmdata/session_factory.py @@ -1,57 +1,57 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - - -class SessionFactory(): - def __init__(self, connection_string, echo): - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_size=20) - self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'connect_timeout': 1}) - self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'timeout': 1}) - self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'connect_timeout': 1}) - - ''' - # Removing pool_timeout and max_overflow allowed the tests to pass - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, pool_size=20, max_overflow=0) - self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) - self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) - self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) - ''' - - ''' - # Old code - class SessionFactory(): - def __init__(self, connection_string, echo): - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, - #pool_size=20, - pool_recycle=3600) - - # Create session maker - self.Session = sessionmaker(bind=self.engine) - - def get_session(self): - return self.Session() - - def __repr__(self): - return "" % (self.engine) - ''' - - # Create session maker - self.Session = sessionmaker(bind=self.engine) - self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) - self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) - self.my_test_Session = sessionmaker(bind=self.my_test_engine) - - def get_session(self): - return self.Session() - - def __repr__(self): - return "" % (self.engine) - +# from sqlalchemy import create_engine +# from sqlalchemy.orm import sessionmaker +# +# +# class SessionFactory(): +# def __init__(self, connection_string, echo): +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_size=20) +# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'connect_timeout': 1}) +# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'timeout': 1}) +# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'connect_timeout': 1}) +# +# ''' +# # Removing pool_timeout and max_overflow allowed the tests to pass +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, pool_size=20, max_overflow=0) +# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) +# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) +# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) +# ''' +# +# ''' +# # Old code +# class SessionFactory(): +# def __init__(self, connection_string, echo): +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, +# #pool_size=20, +# pool_recycle=3600) +# +# # Create session maker +# self.Session = sessionmaker(bind=self.engine) +# +# def get_session(self): +# return self.Session() +# +# def __repr__(self): +# return "" % (self.engine) +# ''' +# +# # Create session maker +# self.Session = sessionmaker(bind=self.engine) +# self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) +# self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) +# self.my_test_Session = sessionmaker(bind=self.my_test_engine) +# +# def get_session(self): +# return self.Session() +# +# def __repr__(self): +# return "" % (self.engine) +# diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index 24b5e81..ce42cb9 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -1,5 +1,5 @@ # CV imports -from odmtools.odmdata import SessionFactory +# from odmtools.odmdata import SessionFactory from odmtools.odmdata import VerticalDatumCV from odmtools.odmdata import SiteTypeCV from odmtools.odmdata import VariableNameCV @@ -16,69 +16,79 @@ from odmtools.odmdata import Qualifier from odmtools.odmdata import Unit from sqlalchemy import not_ +from odm2api.ODMconnection import SessionFactory +from odm2api.ODM2.services.readService import ReadODM2 -class CVService(): +class CVService(): # change to readSerivice() # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): - self._session_factory = SessionFactory(connection_string, debug) - self._edit_session = self._session_factory.get_session() + self._session_factory = SessionFactory(connection_string=connection_string, echo=debug) + self._edit_session = self._session_factory.getSession() self._debug = debug + self.read_service = ReadODM2(session_factory=self._session_factory, debug=self._debug) # Controlled Vocabulary get methods + # Returns a list of all terms in the Controller Vocabulary (CV) - #return a list of all terms in the cv - def get_vertical_datum_cvs(self): - result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() - return result + # From ODM1 -> ODM2 Qualifier was changed to Annotations + def get_annotations(self, type): + return self.read_service.getAnnotations(type=type) - def get_samples(self): - result = self._edit_session.query(Sample).order_by(Sample.lab_sample_code).all() + def get_censor_code_cvs(self): + result = self._edit_session.query(CensorCodeCV).order_by(CensorCodeCV.term).all() return result - def get_site_type_cvs(self): - result = self._edit_session.query(SiteTypeCV).order_by(SiteTypeCV.term).all() + def get_data_type_cvs(self): + result = self._edit_session.query(DataTypeCV).order_by(DataTypeCV.term).all() return result - def get_variable_name_cvs(self): - result = self._edit_session.query(VariableNameCV).order_by(VariableNameCV.term).all() + def get_general_category_cvs(self): + result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() return result def get_offset_type_cvs(self): - result = self._edit_session.query(OffsetType).order_by(OffsetType.id).all() - return result + # result = self._edit_session.query(OffsetType).order_by(OffsetType.id).all() + # return result + return self.read_service.getCVs(type="Spatial Offset Type") - def get_speciation_cvs(self): - result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() - return result + # From ODM1 -> ODM2 Quality Controlled Level was changed to Processing Level + def get_all_processing_levels(self): + self.read_service.getProcessingLevels() - def get_sample_medium_cvs(self): - result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() - return result + def get_processing_level_by_id(self, id): + self.read_service.getProcessingLevels(ids=id) - def get_value_type_cvs(self): - result = self._edit_session.query(ValueTypeCV).order_by(ValueTypeCV.term).all() - return result + def get_processing_level_by_code(self, code): + self.read_service.getProcessingLevels(codes=code) - def get_data_type_cvs(self): - result = self._edit_session.query(DataTypeCV).order_by(DataTypeCV.term).all() + def get_samples(self): + result = self._edit_session.query(Sample).order_by(Sample.lab_sample_code).all() return result - def get_general_category_cvs(self): - result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() - return result + def get_sample_medium_cvs(self): + # result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() + # return result + return self.read_service.getCVs(type="Medium") - def get_censor_code_cvs(self): - result = self._edit_session.query(CensorCodeCV).order_by(CensorCodeCV.term).all() - return result + def get_site_type_cvs(self): + # result = self._edit_session.query(SiteTypeCV).order_by(SiteTypeCV.term).all() + # return result + return self.read_service.getCVs(type="Site Type") + + def get_speciation_cvs(self): + # result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() + # return result + return self.read_service.getCVs(type="Speciation") def get_sample_type_cvs(self): result = self._edit_session.query(SampleTypeCV).order_by(SampleTypeCV.term).all() return result def get_units(self): - result = self._edit_session.query(Unit).all() - return result + # result = self._edit_session.query(Unit).all() + # return result + self.read_service.getUnits(ids=None, name=None, type=None) def get_units_not_uni(self): result = self._edit_session.query(Unit).filter(not_(Unit.name.contains('angstrom'))).all() @@ -90,9 +100,63 @@ def get_units_names(self): # return a single cv def get_unit_by_name(self, unit_name): - result = self._edit_session.query(Unit).filter_by(name=unit_name).first() - return result + # result = self._edit_session.query(Unit).filter_by(name=unit_name).first() + # return result + return self.read_service.getUnits(name=unit_name) def get_unit_by_id(self, unit_id): - result = self._edit_session.query(Unit).filter_by(id=unit_id).first() + # result = self._edit_session.query(Unit).filter_by(id=unit_id).first() + # return result + return self.read_service.getUnits(ids=unit_id) + + def get_value_type_cvs(self): + result = self._edit_session.query(ValueTypeCV).order_by(ValueTypeCV.term).all() return result + + def get_variable_name_cvs(self): + # result = self._edit_session.query(VariableNameCV).order_by(VariableNameCV.term).all() + # return result + return self.read_service.getCVs(type="Variable Name") + + def get_vertical_datum_cvs(self): + # result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() + # return result + return self.read_service.getCVs("Elevation Datum") + + def get_all_variables(self): + return self.read_service.getVariables() + + def get_variable_by_id(self, id): + return self.read_service.getVariables(ids=id) + + def get_variable_by_code(self, code): + return self.read_service.getVariables(codes=code) + + # def get_all_qualifiers(self): + # """ + # + # :return: List[Qualifiers] + # """ + # result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() + # return result + # + # def get_qualifier_by_code(self, code): + # """ + # + # :return: Qualifiers + # """ + # result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() + # return result + # + # def get_qualifiers_by_series_id(self, series_id): + # """ + # + # :param series_id: + # :return: + # """ + # subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( + # Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() + # return self._edit_session.query(Qualifier).join(subquery).distinct().all() + + + diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 078e6fe..0a05800 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -1,10 +1,6 @@ import logging - - from sqlalchemy import distinct, func - - -from odmtools.odmdata import SessionFactory +from odm2api.ODMconnection import SessionFactory # from odmtools.odmdata import SessionFactory from odmtools.odmdata import Site from odmtools.odmdata import Variable from odmtools.odmdata import Unit @@ -18,20 +14,22 @@ from odmtools.odmdata import ODMVersion from odmtools.common.logger import LoggerTool import pandas as pd +from odm2api.ODM2.services.createService import CreateODM2 # tool = LoggerTool() # logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) -logger =logging.getLogger('main') +logger = logging.getLogger('main') -class SeriesService(): +class SeriesService(): # Change to createService # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): - self._session_factory = SessionFactory(connection_string, debug) - self._edit_session = self._session_factory.get_session() + self._session_factory = SessionFactory(connection_string=connection_string, echo=debug) + self._edit_session = self._session_factory.getSession() self._debug = debug + self.create_service = CreateODM2(session_factory=self._session_factory, debug=self._debug) def reset_session(self): - self._edit_session = self._session_factory.get_session() # Reset the session in order to prevent memory leaks + self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks def get_db_version(self): return self._edit_session.query(ODMVersion).first().version_number @@ -59,7 +57,7 @@ def get_used_sites(self): try: site_ids = [x[0] for x in self._edit_session.query(distinct(Series.site_id)).all()] except: - site_ids = None + return None if not site_ids: return None @@ -105,35 +103,6 @@ def get_used_variables(self): return Variables - def get_all_variables(self): - """ - - :return: List[Variables] - """ - return self._edit_session.query(Variable).all() - - def get_variable_by_id(self, variable_id): - """ - - :param variable_id: int - :return: Variables - """ - try: - return self._edit_session.query(Variable).filter_by(id=variable_id).first() - except: - return None - - def get_variable_by_code(self, variable_code): - """ - - :param variable_code: str - :return: Variables - """ - try: - return self._edit_session.query(Variable).filter_by(code=variable_code).first() - except: - return None - def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeUnits """ Finds all of variables at a site @@ -152,79 +121,6 @@ def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeU return variables - # Unit methods - def get_all_units(self): - """ - - :return: List[Units] - """ - return self._edit_session.query(Unit).all() - - def get_unit_by_name(self, unit_name): - """ - - :param unit_name: str - :return: Units - """ - try: - return self._edit_session.query(Unit).filter_by(name=unit_name).first() - except: - return None - - def get_unit_by_id(self, unit_id): - """ - - :param unit_id: int - :return: Units - """ - try: - return self._edit_session.query(Unit).filter_by(id=unit_id).first() - except: - return None - - - def get_all_qualifiers(self): - """ - - :return: List[Qualifiers] - """ - result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() - return result - - def get_qualifier_by_code(self, code): - """ - - :return: Qualifiers - """ - result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() - return result - - def get_qualifiers_by_series_id(self, series_id): - """ - - :param series_id: - :return: - """ - subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( - Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() - return self._edit_session.query(Qualifier).join(subquery).distinct().all() - - #QCL methods - def get_all_qcls(self): - return self._edit_session.query(QualityControlLevel).all() - - def get_qcl_by_id(self, qcl_id): - try: - return self._edit_session.query(QualityControlLevel).filter_by(id=qcl_id).first() - except: - return None - - def get_qcl_by_code(self, qcl_code): - try: - return self._edit_session.query(QualityControlLevel).filter_by(code=qcl_code).first() - except: - return None - # Method methods def get_all_methods(self): return self._edit_session.query(Method).all() @@ -567,8 +463,8 @@ def create_method(self, description, link): if link is not None: meth.link = link - self._edit_session.add(meth) - self._edit_session.commit() + self.create_service.createMethod(method=meth) + return meth def create_variable_by_var(self, var): @@ -577,12 +473,14 @@ def create_variable_by_var(self, var): :param var: Variable Object :return: """ - try: - self._edit_session.add(var) - self._edit_session.commit() - return var - except: - return None + # try: + # self._edit_session.add(var) + # self._edit_session.commit() + # return var + # except: + # return None + self.create_service.createVariable(var) + return var def create_variable( self, code, name, speciation, variable_unit_id, sample_medium, @@ -618,8 +516,7 @@ def create_variable( var.general_category = general_category var.no_data_value = no_data_value - self._edit_session.add(var) - self._edit_session.commit() + self.create_variable_by_var(var) return var def create_qcl(self, code, definition, explanation): diff --git a/odmtools/odmservices/service_manager.py b/odmtools/odmservices/service_manager.py index 1ad5ff1..c106394 100755 --- a/odmtools/odmservices/service_manager.py +++ b/odmtools/odmservices/service_manager.py @@ -13,7 +13,7 @@ from odmtools.controller import EditTools from export_service import ExportService from odmtools.lib.Appdirs.appdirs import user_config_dir -from odmtools.odmdata.session_factory import SessionFactory +from odm2api.ODMconnection import SessionFactory #from odmtools.odmdata.session_factory import SessionFactory # tool = LoggerTool() @@ -100,12 +100,11 @@ def add_connection(self, conn_dict): def testEngine(self, connection_string): s = SessionFactory(connection_string, echo=False) if 'mssql' in connection_string: - s.ms_test_Session().execute("Select top 1 VariableCode From Variables") + s.test_Session().execute("Select top 1 VariableCode From Variables") elif 'mysql' in connection_string: - s.my_test_Session().execute('Select "VariableCode" From Variables Limit 1') + s.test_Session().execute('Select "VariableCode" From Variables Limit 1') elif 'postgresql' in connection_string: - #s.psql_test_Session().execute('Select "VariableCode" From "ODM2"."Variables" Limit 1') - s.psql_test_Session().execute('Select "VariableCode" From "Variables" Limit 1') + s.test_Session().execute('Select "VariableCode" From "Variables" Limit 1') return True def test_connection(self, conn_dict): From cc7709d14ec2a6d47c4821756f1a4c0de790e520 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Tue, 20 Sep 2016 18:35:41 -0600 Subject: [PATCH 02/19] #293 Finishing up the get and create functions in series_service --- odmtools/odmservices/cv_service.py | 64 +++++---------- odmtools/odmservices/series_service.py | 107 ++++++++----------------- 2 files changed, 53 insertions(+), 118 deletions(-) diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index ce42cb9..b1a01b2 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -29,7 +29,6 @@ def __init__(self, connection_string="", debug=False): self.read_service = ReadODM2(session_factory=self._session_factory, debug=self._debug) # Controlled Vocabulary get methods - # Returns a list of all terms in the Controller Vocabulary (CV) # From ODM1 -> ODM2 Qualifier was changed to Annotations def get_annotations(self, type): @@ -48,14 +47,21 @@ def get_general_category_cvs(self): return result def get_offset_type_cvs(self): - # result = self._edit_session.query(OffsetType).order_by(OffsetType.id).all() - # return result return self.read_service.getCVs(type="Spatial Offset Type") # From ODM1 -> ODM2 Quality Controlled Level was changed to Processing Level def get_all_processing_levels(self): self.read_service.getProcessingLevels() + def get_all_method(self): # Rename to get_method_all + return self.read_service.getMethods(ids=None, codes=None, type=None) + + def get_method_by_id(self, method_id): + return self.read_service.getMethods(ids=method_id) + + def get_method_by_description(self, code): + return self.read_service.getMethods(codes=code) + def get_processing_level_by_id(self, id): self.read_service.getProcessingLevels(ids=id) @@ -67,27 +73,29 @@ def get_samples(self): return result def get_sample_medium_cvs(self): - # result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() - # return result return self.read_service.getCVs(type="Medium") def get_site_type_cvs(self): - # result = self._edit_session.query(SiteTypeCV).order_by(SiteTypeCV.term).all() - # return result return self.read_service.getCVs(type="Site Type") def get_speciation_cvs(self): - # result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() - # return result return self.read_service.getCVs(type="Speciation") def get_sample_type_cvs(self): result = self._edit_session.query(SampleTypeCV).order_by(SampleTypeCV.term).all() return result + # From ODM1 -> ODM2 Site was changed to Sampling Feature + def get_all_sites(self): + return self.read_service.getSamplingFeatures(ids=None, codes=None, uuids=None, type=None, wkt=None) + + def get_site_by_id(self, site_id): + return self.read_service.getSamplingFeatures(ids=site_id) + + def get_timeseries_result_values(self, type): + return self.read_service.getAnnotations(type=type) + def get_units(self): - # result = self._edit_session.query(Unit).all() - # return result self.read_service.getUnits(ids=None, name=None, type=None) def get_units_not_uni(self): @@ -100,13 +108,9 @@ def get_units_names(self): # return a single cv def get_unit_by_name(self, unit_name): - # result = self._edit_session.query(Unit).filter_by(name=unit_name).first() - # return result return self.read_service.getUnits(name=unit_name) def get_unit_by_id(self, unit_id): - # result = self._edit_session.query(Unit).filter_by(id=unit_id).first() - # return result return self.read_service.getUnits(ids=unit_id) def get_value_type_cvs(self): @@ -114,13 +118,9 @@ def get_value_type_cvs(self): return result def get_variable_name_cvs(self): - # result = self._edit_session.query(VariableNameCV).order_by(VariableNameCV.term).all() - # return result return self.read_service.getCVs(type="Variable Name") def get_vertical_datum_cvs(self): - # result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() - # return result return self.read_service.getCVs("Elevation Datum") def get_all_variables(self): @@ -132,31 +132,7 @@ def get_variable_by_id(self, id): def get_variable_by_code(self, code): return self.read_service.getVariables(codes=code) - # def get_all_qualifiers(self): - # """ - # - # :return: List[Qualifiers] - # """ - # result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() - # return result - # - # def get_qualifier_by_code(self, code): - # """ - # - # :return: Qualifiers - # """ - # result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() - # return result - # - # def get_qualifiers_by_series_id(self, series_id): - # """ - # - # :param series_id: - # :return: - # """ - # subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( - # Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() - # return self._edit_session.query(Qualifier).join(subquery).distinct().all() + diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 0a05800..7f6b136 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -29,7 +29,8 @@ def __init__(self, connection_string="", debug=False): self.create_service = CreateODM2(session_factory=self._session_factory, debug=self._debug) def reset_session(self): - self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks + # self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks + self._edit_session = self.create_service.getSession() def get_db_version(self): return self._edit_session.query(ODMVersion).first().version_number @@ -40,15 +41,6 @@ def get_db_version(self): # ##################### - # Site methods - def get_all_sites(self): - """ - - :return: List[Sites] - """ - return self._edit_session.query(Site).order_by(Site.code).all() - - def get_used_sites(self): """ Return a list of all sites that are being referenced in the Series Catalog Table @@ -68,18 +60,6 @@ def get_used_sites(self): return Sites - - def get_site_by_id(self, site_id): - """ - return a Site object that has an id=site_id - :param site_id: integer- the identification number of the site - :return: Sites - """ - try: - return self._edit_session.query(Site).filter_by(id=site_id).first() - except: - return None - # Variables methods def get_used_variables(self): """ @@ -103,43 +83,6 @@ def get_used_variables(self): return Variables - def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeUnits - """ - Finds all of variables at a site - :param site_code: str - :return: List[Variables] - """ - try: - var_ids = [x[0] for x in self._edit_session.query(distinct(Series.variable_id)).filter_by( - site_code=site_code).all()] - except: - var_ids = None - - variables = [] - for var_id in var_ids: - variables.append(self._edit_session.query(Variable).filter_by(id=var_id).first()) - - return variables - - # Method methods - def get_all_methods(self): - return self._edit_session.query(Method).all() - - def get_method_by_id(self, method_id): - try: - result = self._edit_session.query(Method).filter_by(id=method_id).first() - except: - result = None - return result - - def get_method_by_description(self, method_code): - try: - result = self._edit_session.query(Method).filter_by(description=method_code).first() - except: - result = None - logger.error("method not found") - return result - def get_offset_types_by_series_id(self, series_id): """ @@ -215,6 +158,31 @@ def get_series_from_filter(self): # Pass in probably a Series object, match it against the database pass + def get_all_qualifiers(self): + """ + + :return: List[Qualifiers] + """ + result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() + return result + + def get_qualifier_by_code(self, code): + """ + + :return: Qualifiers + """ + result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() + return result + + def get_qualifiers_by_series_id(self, series_id): + """ + + :param series_id: + :return: + """ + subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( + Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() + return self._edit_session.query(Qualifier).join(subquery).distinct().all() #Data Value Methods def get_values_by_series(self, series_id): @@ -430,7 +398,6 @@ def save_values(self, values): def create_new_series(self, data_values, site_id, variable_id, method_id, source_id, qcl_id): """ - :param data_values: :param site_id: :param variable_id: @@ -451,9 +418,8 @@ def create_new_series(self, data_values, site_id, variable_id, method_id, source self._edit_session.commit() return series - def create_method(self, description, link): + def create_method(self, description, link): # DONE """ - :param description: :param link: :return: @@ -467,25 +433,19 @@ def create_method(self, description, link): return meth - def create_variable_by_var(self, var): + def create_variable_by_var(self, var): # DONE """ - :param var: Variable Object :return: """ - # try: - # self._edit_session.add(var) - # self._edit_session.commit() - # return var - # except: - # return None + self.create_service.createVariable(var) return var def create_variable( self, code, name, speciation, variable_unit_id, sample_medium, value_type, is_regular, time_support, time_unit_id, data_type, - general_category, no_data_value): + general_category, no_data_value): # DONE """ :param code: @@ -519,7 +479,7 @@ def create_variable( self.create_variable_by_var(var) return var - def create_qcl(self, code, definition, explanation): + def create_qcl(self, code, definition, explanation): # DONE """ :param code: @@ -532,8 +492,7 @@ def create_qcl(self, code, definition, explanation): qcl.definition = definition qcl.explanation = explanation - self._edit_session.add(qcl) - self._edit_session.commit() + self.create_service.createProcessingLevel(qcl) return qcl From 9039513f78a458702a8e2944509173e9fc8de4a9 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Thu, 22 Sep 2016 17:00:22 -0600 Subject: [PATCH 03/19] Undoing the last two commits I made changes I was not supposed to. I understand the project better and will restart the process --- odmtools/controller/frmAddPoints.py | 0 odmtools/controller/frmBulkInsert.py | 0 odmtools/controller/frmDBConfig.py | 0 odmtools/controller/frmSeriesSelector.py | 1244 ++-- odmtools/controller/logicCellEdit.py | 0 odmtools/controller/olvAddPoint.py | 0 odmtools/controller/olvDataTable.py | 3 +- odmtools/gui/frmFlagValues.py | 0 odmtools/gui/frmODMTools.py | 0 odmtools/gui/mnuRibbon.py | 0 odmtools/gui/plotProbability.py | 5 +- odmtools/gui/plotTimeSeries.py | 0 odmtools/lib/ObjectListView/ObjectListView.py | 0 odmtools/lib/oldOlv/CellEditor.py | 1096 ++-- odmtools/lib/oldOlv/Filter.py | 276 +- odmtools/lib/oldOlv/ListCtrlPrinter.py | 5594 ++++++++--------- odmtools/lib/oldOlv/OLVEvent.py | 558 +- odmtools/lib/oldOlv/ObjectListView.py | 0 odmtools/lib/oldOlv/WordWrapRenderer.py | 464 +- odmtools/lib/oldOlv/t.py | 456 +- odmtools/odmdata/__init__.py | 2 +- odmtools/odmdata/session_factory.py | 114 +- odmtools/odmservices/cv_service.py | 116 +- odmtools/odmservices/series_service.py | 236 +- odmtools/odmservices/service_manager.py | 9 +- odmtools/view/clsAddPoints.py | 0 odmtools/view/clsSeriesSelector.py | 566 +- 27 files changed, 5421 insertions(+), 5318 deletions(-) mode change 100755 => 100644 odmtools/controller/frmAddPoints.py mode change 100755 => 100644 odmtools/controller/frmBulkInsert.py mode change 100755 => 100644 odmtools/controller/frmDBConfig.py mode change 100755 => 100644 odmtools/controller/frmSeriesSelector.py mode change 100755 => 100644 odmtools/controller/logicCellEdit.py mode change 100755 => 100644 odmtools/controller/olvAddPoint.py mode change 100755 => 100644 odmtools/gui/frmFlagValues.py mode change 100755 => 100644 odmtools/gui/frmODMTools.py mode change 100755 => 100644 odmtools/gui/mnuRibbon.py mode change 100755 => 100644 odmtools/gui/plotTimeSeries.py mode change 100755 => 100644 odmtools/lib/ObjectListView/ObjectListView.py mode change 100755 => 100644 odmtools/lib/oldOlv/ObjectListView.py mode change 100755 => 100644 odmtools/odmservices/service_manager.py mode change 100755 => 100644 odmtools/view/clsAddPoints.py mode change 100755 => 100644 odmtools/view/clsSeriesSelector.py diff --git a/odmtools/controller/frmAddPoints.py b/odmtools/controller/frmAddPoints.py old mode 100755 new mode 100644 diff --git a/odmtools/controller/frmBulkInsert.py b/odmtools/controller/frmBulkInsert.py old mode 100755 new mode 100644 diff --git a/odmtools/controller/frmDBConfig.py b/odmtools/controller/frmDBConfig.py old mode 100755 new mode 100644 diff --git a/odmtools/controller/frmSeriesSelector.py b/odmtools/controller/frmSeriesSelector.py old mode 100755 new mode 100644 index 4563190..c06324e --- a/odmtools/controller/frmSeriesSelector.py +++ b/odmtools/controller/frmSeriesSelector.py @@ -1,622 +1,622 @@ -import os -import wx -import logging - -from wx.lib.pubsub import pub as Publisher -from odmtools.lib.ObjectListView.Filter import TextSearch, Chain -import odmtools.gui.frmQueryBuilder as frmQueryBuilder - -from odmtools.common.logger import LoggerTool -from odmtools.odmdata import MemoryDatabase -from odmtools.view import clsSeriesSelector - -# tool = LoggerTool() -# logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) -logger =logging.getLogger('main') - -__author__ = 'Jacob' - -class FrmSeriesSelector(clsSeriesSelector.ClsSeriesSelector): - """ - - """ - def __init__(self, *args, **kwargs): - """ - - """ - - self.taskserver = kwargs.pop("taskserver") - self.memDB = kwargs.pop("memdb") - self.pnlPlot = kwargs.pop("plot") - - clsSeriesSelector.ClsSeriesSelector.__init__(self, *args, **kwargs) - - def initPubSub(self): - #Publisher.subscribe(self.onEditButton, ("selectEdit")) - Publisher.subscribe(self.refreshSeries, "refreshSeries") - - def resetDB(self, series_service): - """ - - :param series_service: - :return: - """ - - if not self.rbAll.GetValue(): - #self.GetEventHandler().ProcessEvent(wx.PyCommandEvent(wx.EVT_RADIOBUTTON.typeId, self.rbAll.Id)) - wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(wx.EVT_RADIOBUTTON.typeId, self.rbAll.Id)) - self.rbAll.SetValue(True) - - #####INIT DB Connection - self.series_service = series_service - #self.refreshSeries() - self.cbVariables.Clear() - self.cbSites.Clear() - - self.siteList = None - self.varList = None - - self.initTableSeries() - self.initSVBoxes() - self.Layout() - - def initTableSeries(self): - """Set up columns and objects to be used in the objectlistview to be visible in the series selector - - :return: - """ - try: - self.memDB.set_series_service(self.series_service) - - object = self.series_service.get_all_series() - - if object: - self.tblSeries.SetObjects(object) - else: - self.tblSeries.SetObjects(None) - - except AttributeError as e: - logger.error(e) - #self.tblSeries.SaveObject(object) - - def refreshTableSeries(self, db): - """ Refreshes the objectlistview to include newly saved database series and preserve which series was 'checked' - for plotting/editing - - :return: - """ - chcklist= self.tblSeries.GetCheckedObjects() - - - self.memDB.set_series_service(db) - object = self.series_service.get_all_series() - #checkedObjs = self.tblSeries.GetCheckedObjects() - idList = [x.id for x in self.tblSeries.modelObjects] - - for x in object: - if x.id not in idList: - self.tblSeries.AddObject(x) - - - for c in chcklist: - self.tblSeries.SetCheckState(c, True) - - - #for x in checkedObjs: - # super(FastObjectListView, self.tblSeries).SetCheckState(x, True) - - def refreshSeries(self): - """ - - :return: - """ - chcklist= self.tblSeries.GetCheckedObjects() - self.series_service = None - - self.series_service = self.parent.Parent.createService() - #self.refreshTableSeries(self.dbservice) - self.resetDB(self.series_service) - - for c in chcklist: - for val in self.tblSeries.GetObjects(): - if c == val: - self.tblSeries.SetCheckState(val, True) - break - - logger.debug("Repopulate Series Selector") - - - - def initSVBoxes(self): - """ - - :return: - """ - - self.site_code = None - self.variable_code = None - - #####INIT drop down boxes for Simple Filter - try: - self.siteList = self.series_service.get_used_sites() - for site in self.siteList: - self.cbSites.Append(site.code + '-' + site.name) - self.cbSites.SetSelection(0) - self.site_code = self.siteList[0].code - - self.varList = self.series_service.get_used_variables() - for var in self.varList: - self.cbVariables.Append(var.code + '-' + var.name) - self.cbVariables.SetSelection(0) - except AttributeError as e: - logger.error(e) - - def OnTableRightDown(self, event): - """Right click down menu - - :param event: - :return: - """ - - # build pop-up menu for right-click display - self.selectedIndex = event.m_itemIndex - #self.selectedID = self.tableSeries.getColumnText(event.m_itemIndex, 1) - self.selectedID = self.tblSeries.GetSelectedObject().id - - # print self.selectedID - popup_edit_series = wx.NewId() - popup_plot_series = wx.NewId() - popup_export_data = wx.NewId() - popup_series_refresh = wx.NewId() - popup_clear_selected = wx.NewId() - - popup_export_metadata = wx.NewId() - popup_select_all = wx.NewId() - popup_select_none = wx.NewId() - - popup_menu = wx.Menu() - plotItem = popup_menu.Append(popup_plot_series, 'Plot') - editItem = popup_menu.Append(popup_edit_series, 'Edit') - - self.Bind(wx.EVT_MENU, self.onRightPlot, plotItem) - # TODO @jmeline needs to fix edit, it doesn't unedit when a plot is being edited - self.Bind(wx.EVT_MENU, self.onRightEdit, editItem) - # TODO @jmeline will refresh and clear selected as an enhancement - #self.Bind(wx.EVT_MENU, self.onRightRefresh, popup_menu.Append(popup_series_refresh, 'Refresh')) - #self.Bind(wx.EVT_MENU, self.onRightClearSelected, popup_menu.Append(popup_series_refresh, 'Clear Selected')) - - popup_menu.AppendSeparator() - self.Bind(wx.EVT_MENU, self.onRightExData, popup_menu.Append(popup_export_data, 'Export Data')) - self.Bind(wx.EVT_MENU, self.onRightExMeta, popup_menu.Append(popup_export_metadata, 'Export MetaData')) - - if self.isEditing: - popup_menu.Enable(popup_edit_series, False) - - self.tblSeries.PopupMenu(popup_menu) - event.Skip() - - def onPaneChanged(self, event=None): - #if event: - # print 'wx.EVT_COLLAPSIBLEPANE_CHANGED: %s\n' % event.Collapsed - self.Layout() - - def onRbAdvancedRadiobutton(self, event): - """ - - :param event: - :return: - """ - - self.cpnlSimple.Collapse(True) - self.Layout() - series_filter = frmQueryBuilder.frmQueryBuilder(self) - self.filterlist = series_filter.ShowModal() - event.Skip() - - def onRbAllRadiobutton(self, event): - """ - - :param event: - :return: - """ - - if self.checkSite.GetValue() or self.checkVariable.GetValue(): - self.checkSite.SetValue(False) - self.checkVariable.SetValue(False) - - #logger.debug("onRbAllRadioButton called! ") - self.cpnlSimple.Collapse(True) - self.Layout() - self.setFilter() - event.Skip() - - def onRbSimpleRadiobutton(self, event): - """ - - :param event: - :return: - """ - - self.cpnlSimple.Expand() - self.Layout() - - - ## - if not self.checkSite.GetValue() and not self.checkVariable.GetValue(): - self.setFilter() - return - - self.setFilter(self.site_code, self.variable_code) - - event.Skip() - - def onRightPlot(self, event): - """ - - :param event: - :return: - """ - object = self.tblSeries.GetSelectedObject() - self.tblSeries.ToggleCheck(object) - self.onReadyToPlot(event) - event.Skip() - - def onRightEdit(self, event): - """ - - :param event: - :return: - """ - Publisher.sendMessage(("selectEdit"), event=event) - if self.isEditing: - Publisher.sendMessage("toggleEdit", checked=True) - event.Skip() - - # allows user to right-click refresh the Series Selector - def onRightRefresh(self, event): - """ - - :param event: - :return: - """ - self.refreshSeries() - event.Skip() - - def onRightClearSelected(self, event): - """ - - :param event: - :return: - """ - event.Skip() - - - - def onRightExData(self, event): - """ - - :param event: - :return: - """ - dlg = wx.FileDialog(self, "Choose a save location", '', "", "*.csv", wx.SAVE | wx.OVERWRITE_PROMPT) - if dlg.ShowModal() == wx.ID_OK: - full_path = os.path.join(dlg.GetDirectory(), dlg.GetFilename()) - - #series_id = self.tableSeries.getColumnText(self.selectedIndex, 1) - series_id = self.tblSeries.GetSelectedObject().id - self.export_service.export_series_data(series_id, full_path, True, True, True, True, True, True, True) - self.Close() - - dlg.Destroy() - - event.Skip() - - def onRightExMeta(self, event): - """ - - :param event: - :return: - """ - dlg = wx.FileDialog(self, "Choose a save location", '', "", "*.xml", wx.SAVE | wx.OVERWRITE_PROMPT) - if dlg.ShowModal() == wx.ID_OK: - full_path = os.path.join(dlg.GetDirectory(), dlg.GetFilename()) - - self.selectedIndex = self.tblSeries.GetSelectedObject().id - #series_id = self.tableSeries.getColumnText(self.selectedIndex, 1) - #print "series_id", series_id - - self.export_service.export_series_metadata(self.selectedIndex, full_path) - self.Close() - - dlg.Destroy() - - event.Skip() - - def onCbSitesCombobox(self, event): - """ - - :param event: - :return: - """ - - if self.checkSite.GetValue(): - self.site_code = self.siteList[event.GetSelection()].code - self.varList = self.series_service.get_variables_by_site_code(self.site_code) - - self.cbVariables.Clear() - for var in self.varList: - self.cbVariables.Append(var.code + '-' + var.name) - self.cbVariables.SetSelection(0) - - if (self.checkSite.GetValue() and not self.checkVariable.GetValue()): - self.variable_code = None - - self.setFilter(site_code=self.site_code, var_code=self.variable_code) - event.Skip() - - def onCbVariablesCombobox(self, event): - """ - - :param event: - :return: - """ - - if self.checkVariable.GetValue(): - self.variable_code = self.varList[event.GetSelection()].code - if (not self.checkSite.GetValue() and self.checkVariable.GetValue()): - self.site_code = None - self.setFilter(site_code=self.site_code, var_code=self.variable_code) - event.Skip() - - def siteAndVariables(self): - """ - - :return: - """ - self.site_code = self.siteList[self.cbSites.Selection].code - - self.cbVariables.Clear() - self.varList = self.series_service.get_variables_by_site_code(self.site_code) - for var in self.varList: - self.cbVariables.Append(var.code + '-' + var.name) - self.cbVariables.SetSelection(0) - - try: - self.variable_code = self.varList[self.cbVariables.Selection].code - self.setFilter(site_code=self.site_code, var_code=self.variable_code) - self.cbVariables.Enabled = True - self.cbSites.Enabled = True - except IndexError as i: - logger.error(i) - pass - - def siteOnly(self): - """ - - :return: - """ - self.cbVariables.Enabled = False - self.cbSites.Enabled = True - self.variable_code = None - - self.site_code = self.siteList[self.cbSites.Selection].code - self.setFilter(site_code=self.site_code) - - def variableOnly(self): - """ - - :return: - """ - self.site_code = None - self.cbVariables.Clear() - self.varList = self.series_service.get_used_variables() - for var in self.varList: - self.cbVariables.Append(var.code + '-' + var.name) - self.cbVariables.SetSelection(0) - self.cbSites.Enabled = False - self.cbVariables.Enabled = True - - self.variable_code = self.varList[0].code - - self.setFilter(var_code=self.variable_code) - - def onCheck(self, event): - """ - - :param event: - :return: - """ - # self.tableSeries.DeleteAllItems() - if self.rbAll.GetValue(): - #logger.debug("Called!") - self.rbAll.SetValue(False) - self.rbSimple.SetValue(True) - - if not self.checkSite.GetValue() and not self.checkVariable.GetValue(): - self.setFilter() - self.cbSites.Enabled = False - self.cbVariables.Enabled = False - - elif self.checkSite.GetValue(): - if self.checkVariable.GetValue(): - self.siteAndVariables() - else: - self.siteOnly() - else: - if self.checkVariable.GetValue(): - self.variableOnly() - else: - self.cbSites.Enabled = False - self.cbVariables.Enabled = False - event.Skip() - - def setFilter(self, site_code='', var_code='', advfilter=''): - """ - - :param site_code: - :param var_code: - :param advfilter: - :return: - """ - if site_code and var_code: - self.siteFilter = TextSearch(self.tblSeries, columns=self.tblSeries.columns[3:4],text=site_code) - self.variableFilter = TextSearch(self.tblSeries, columns=self.tblSeries.columns[6:7],text=var_code) - self.tblSeries.SetFilter(Chain(self.siteFilter, self.variableFilter)) - elif site_code: - self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[3:4], text=site_code)) - elif var_code: - self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[6:7], text=var_code)) - elif advfilter: - self.tblSeries.SetFilter(advfilter) - else: - self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[0:1])) - self.tblSeries.RepopulateList() - - - def onReadyToPlot(self, event): - """Plots a series selected from the series selector - - :param event: EVT_OVL_CHECK_EVENT type - """ - logger.debug("Starting to Plot") - - checkedCount = len(self.tblSeries.GetCheckedObjects()) - - Publisher.sendMessage("EnablePlotButtons", plot=0, isActive=(checkedCount > 0)) - - logger.debug("Obtain object") - try: - object = event.object - except Exception as e : - object = self.tblSeries.GetSelectedObject() - - if not self.tblSeries.IsChecked(object): - Publisher.sendMessage("removePlot", seriesID=object.id) - Publisher.sendMessage("updateCursor", deselectedObject=object) - - else: - logger.debug("Obtained object, entering addplot") - self.pnlPlot.addPlot(self.memDB, object.id) - Publisher.sendMessage("updateCursor", selectedObject=object) - - logger.debug("refreshing...") - self.Refresh() - - logger.debug("Finish Plotting") - - - #from meliae import scanner - #scanner.dump_all_objects("plot_plotting.dat") - - def getSelectedObject(self, event): - """Capture the currently selected Object to be used for editing - - :param event: wx.EVT_LIST_ITEM_FOCUSED type - """ - - logger.debug("Selecting object from Series Catalog") - - object = event.GetEventObject() - editingObject = object.innerList[object.FocusedItem] - - self.tblSeries.currentlySelectedObject = editingObject - - ## update Cursor - if self.parent.Parent.pnlPlot._seriesPlotInfo: - if self.parent.Parent.pnlPlot._seriesPlotInfo.isPlotted(editingObject.id): - #print "Updating Cursor", editingObject.id - Publisher.sendMessage("updateCursor", selectedObject=editingObject) - - - def onReadyToEdit(self): - """Choose a series to edit from the series selector - - :return: - """ - - ovl = self.tblSeries - - object = ovl.currentlySelectedObject - if object is None: - # # Select the first one - if len(ovl.modelObjects) == 0: - logger.fatal("There are no model objects available to edit") - raise Exception() - object = ovl.modelObjects[0] - - if len(ovl.GetCheckedObjects()) <= ovl.allowedLimit: - - if object not in ovl.GetCheckedObjects(): - ovl.ToggleCheck(object) - - - - # logger.debug("Initializing DataTable") - # - # tasks = [("dataTable", self.memDB.conn)] - # self.taskserver.setTasks(tasks) - # self.taskserver.processTasks() - - self.isEditing = True - ovl.editingObject = object - ovl.RefreshObject(ovl.editingObject) - - - return True, object.id#, self.memDB - else: - isSelected = False - logger.debug("series was not checked") - val_2 = wx.MessageBox("Visualization is limited to 6 series.", "Can't add plot", - wx.OK | wx.ICON_INFORMATION) - - - self.isEditing = False - ovl.editingObject = None - return False, object.id#, self.memDB - - def stopEdit(self): - """When edit button is untoggled, the editing feature closes - :return: - """ - - self.isEditing = False - chcklist= self.tblSeries.GetCheckedObjects() - - self.tblSeries.RefreshObject(self.tblSeries.editingObject) - for c in chcklist: - self.tblSeries.SetCheckState(c, True) - - self.memDB.stopEdit() - - - def isEditing(self): - """ - - :return: - """ - return self.isEditing - - - def _rowFormatter(self, listItem, object): - """Handles the formatting of rows for object list view - :param: wx.ListCtrl listitem - :param: ModelObject object - - :rtype: None - """ - ''' - if self.tblSeries.editingObject and \ - object.id == self.tblSeries.editingObject.id: - #listItem.SetTextColour(wx.Colour(255, 25, 112)) - # font type: wx.DEFAULT, wx.DECORATIVE, wx.ROMAN, wx.SCRIPT, wx.SWISS, wx.MODERN - # slant: wx.NORMAL, wx.SLANT or wx.ITALIC - # weight: wx.NORMAL, wx.LIGHT or wx.BOLD - #font1 = wx.Font(10, wx.SWISS, wx.ITALIC, wx.NORMAL) - # use additional fonts this way ... - #font1 = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False, u'Comic Sans MS') - listItem.SetFont( - wx.Font(9, family=wx.DEFAULT, weight=wx.BOLD, style=wx.ITALIC)) - else: - listItem.SetTextColour(wx.Colour()) - listItem.SetFont(wx.Font(9, wx.SWISS, wx.NORMAL, wx.NORMAL, False)) - ''' +import os +import wx +import logging + +from wx.lib.pubsub import pub as Publisher +from odmtools.lib.ObjectListView.Filter import TextSearch, Chain +import odmtools.gui.frmQueryBuilder as frmQueryBuilder + +from odmtools.common.logger import LoggerTool +from odmtools.odmdata import MemoryDatabase +from odmtools.view import clsSeriesSelector + +# tool = LoggerTool() +# logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) +logger =logging.getLogger('main') + +__author__ = 'Jacob' + +class FrmSeriesSelector(clsSeriesSelector.ClsSeriesSelector): + """ + + """ + def __init__(self, *args, **kwargs): + """ + + """ + + self.taskserver = kwargs.pop("taskserver") + self.memDB = kwargs.pop("memdb") + self.pnlPlot = kwargs.pop("plot") + + clsSeriesSelector.ClsSeriesSelector.__init__(self, *args, **kwargs) + + def initPubSub(self): + #Publisher.subscribe(self.onEditButton, ("selectEdit")) + Publisher.subscribe(self.refreshSeries, "refreshSeries") + + def resetDB(self, series_service): + """ + + :param series_service: + :return: + """ + + if not self.rbAll.GetValue(): + #self.GetEventHandler().ProcessEvent(wx.PyCommandEvent(wx.EVT_RADIOBUTTON.typeId, self.rbAll.Id)) + wx.PostEvent(self.GetEventHandler(), wx.PyCommandEvent(wx.EVT_RADIOBUTTON.typeId, self.rbAll.Id)) + self.rbAll.SetValue(True) + + #####INIT DB Connection + self.series_service = series_service + #self.refreshSeries() + self.cbVariables.Clear() + self.cbSites.Clear() + + self.siteList = None + self.varList = None + + self.initTableSeries() + self.initSVBoxes() + self.Layout() + + def initTableSeries(self): + """Set up columns and objects to be used in the objectlistview to be visible in the series selector + + :return: + """ + try: + self.memDB.set_series_service(self.series_service) + + object = self.series_service.get_all_series() + + if object: + self.tblSeries.SetObjects(object) + else: + self.tblSeries.SetObjects(None) + + except AttributeError as e: + logger.error(e) + #self.tblSeries.SaveObject(object) + + def refreshTableSeries(self, db): + """ Refreshes the objectlistview to include newly saved database series and preserve which series was 'checked' + for plotting/editing + + :return: + """ + chcklist= self.tblSeries.GetCheckedObjects() + + + self.memDB.set_series_service(db) + object = self.series_service.get_all_series() + #checkedObjs = self.tblSeries.GetCheckedObjects() + idList = [x.id for x in self.tblSeries.modelObjects] + + for x in object: + if x.id not in idList: + self.tblSeries.AddObject(x) + + + for c in chcklist: + self.tblSeries.SetCheckState(c, True) + + + #for x in checkedObjs: + # super(FastObjectListView, self.tblSeries).SetCheckState(x, True) + + def refreshSeries(self): + """ + + :return: + """ + chcklist= self.tblSeries.GetCheckedObjects() + self.series_service = None + + self.series_service = self.parent.Parent.createService() + #self.refreshTableSeries(self.dbservice) + self.resetDB(self.series_service) + + for c in chcklist: + for val in self.tblSeries.GetObjects(): + if c == val: + self.tblSeries.SetCheckState(val, True) + break + + logger.debug("Repopulate Series Selector") + + + + def initSVBoxes(self): + """ + + :return: + """ + + self.site_code = None + self.variable_code = None + + #####INIT drop down boxes for Simple Filter + try: + self.siteList = self.series_service.get_used_sites() + for site in self.siteList: + self.cbSites.Append(site.code + '-' + site.name) + self.cbSites.SetSelection(0) + self.site_code = self.siteList[0].code + + self.varList = self.series_service.get_used_variables() + for var in self.varList: + self.cbVariables.Append(var.code + '-' + var.name) + self.cbVariables.SetSelection(0) + except AttributeError as e: + logger.error(e) + + def OnTableRightDown(self, event): + """Right click down menu + + :param event: + :return: + """ + + # build pop-up menu for right-click display + self.selectedIndex = event.m_itemIndex + #self.selectedID = self.tableSeries.getColumnText(event.m_itemIndex, 1) + self.selectedID = self.tblSeries.GetSelectedObject().id + + # print self.selectedID + popup_edit_series = wx.NewId() + popup_plot_series = wx.NewId() + popup_export_data = wx.NewId() + popup_series_refresh = wx.NewId() + popup_clear_selected = wx.NewId() + + popup_export_metadata = wx.NewId() + popup_select_all = wx.NewId() + popup_select_none = wx.NewId() + + popup_menu = wx.Menu() + plotItem = popup_menu.Append(popup_plot_series, 'Plot') + editItem = popup_menu.Append(popup_edit_series, 'Edit') + + self.Bind(wx.EVT_MENU, self.onRightPlot, plotItem) + # TODO @jmeline needs to fix edit, it doesn't unedit when a plot is being edited + self.Bind(wx.EVT_MENU, self.onRightEdit, editItem) + # TODO @jmeline will refresh and clear selected as an enhancement + #self.Bind(wx.EVT_MENU, self.onRightRefresh, popup_menu.Append(popup_series_refresh, 'Refresh')) + #self.Bind(wx.EVT_MENU, self.onRightClearSelected, popup_menu.Append(popup_series_refresh, 'Clear Selected')) + + popup_menu.AppendSeparator() + self.Bind(wx.EVT_MENU, self.onRightExData, popup_menu.Append(popup_export_data, 'Export Data')) + self.Bind(wx.EVT_MENU, self.onRightExMeta, popup_menu.Append(popup_export_metadata, 'Export MetaData')) + + if self.isEditing: + popup_menu.Enable(popup_edit_series, False) + + self.tblSeries.PopupMenu(popup_menu) + event.Skip() + + def onPaneChanged(self, event=None): + #if event: + # print 'wx.EVT_COLLAPSIBLEPANE_CHANGED: %s\n' % event.Collapsed + self.Layout() + + def onRbAdvancedRadiobutton(self, event): + """ + + :param event: + :return: + """ + + self.cpnlSimple.Collapse(True) + self.Layout() + series_filter = frmQueryBuilder.frmQueryBuilder(self) + self.filterlist = series_filter.ShowModal() + event.Skip() + + def onRbAllRadiobutton(self, event): + """ + + :param event: + :return: + """ + + if self.checkSite.GetValue() or self.checkVariable.GetValue(): + self.checkSite.SetValue(False) + self.checkVariable.SetValue(False) + + #logger.debug("onRbAllRadioButton called! ") + self.cpnlSimple.Collapse(True) + self.Layout() + self.setFilter() + event.Skip() + + def onRbSimpleRadiobutton(self, event): + """ + + :param event: + :return: + """ + + self.cpnlSimple.Expand() + self.Layout() + + + ## + if not self.checkSite.GetValue() and not self.checkVariable.GetValue(): + self.setFilter() + return + + self.setFilter(self.site_code, self.variable_code) + + event.Skip() + + def onRightPlot(self, event): + """ + + :param event: + :return: + """ + object = self.tblSeries.GetSelectedObject() + self.tblSeries.ToggleCheck(object) + self.onReadyToPlot(event) + event.Skip() + + def onRightEdit(self, event): + """ + + :param event: + :return: + """ + Publisher.sendMessage(("selectEdit"), event=event) + if self.isEditing: + Publisher.sendMessage("toggleEdit", checked=True) + event.Skip() + + # allows user to right-click refresh the Series Selector + def onRightRefresh(self, event): + """ + + :param event: + :return: + """ + self.refreshSeries() + event.Skip() + + def onRightClearSelected(self, event): + """ + + :param event: + :return: + """ + event.Skip() + + + + def onRightExData(self, event): + """ + + :param event: + :return: + """ + dlg = wx.FileDialog(self, "Choose a save location", '', "", "*.csv", wx.SAVE | wx.OVERWRITE_PROMPT) + if dlg.ShowModal() == wx.ID_OK: + full_path = os.path.join(dlg.GetDirectory(), dlg.GetFilename()) + + #series_id = self.tableSeries.getColumnText(self.selectedIndex, 1) + series_id = self.tblSeries.GetSelectedObject().id + self.export_service.export_series_data(series_id, full_path, True, True, True, True, True, True, True) + self.Close() + + dlg.Destroy() + + event.Skip() + + def onRightExMeta(self, event): + """ + + :param event: + :return: + """ + dlg = wx.FileDialog(self, "Choose a save location", '', "", "*.xml", wx.SAVE | wx.OVERWRITE_PROMPT) + if dlg.ShowModal() == wx.ID_OK: + full_path = os.path.join(dlg.GetDirectory(), dlg.GetFilename()) + + self.selectedIndex = self.tblSeries.GetSelectedObject().id + #series_id = self.tableSeries.getColumnText(self.selectedIndex, 1) + #print "series_id", series_id + + self.export_service.export_series_metadata(self.selectedIndex, full_path) + self.Close() + + dlg.Destroy() + + event.Skip() + + def onCbSitesCombobox(self, event): + """ + + :param event: + :return: + """ + + if self.checkSite.GetValue(): + self.site_code = self.siteList[event.GetSelection()].code + self.varList = self.series_service.get_variables_by_site_code(self.site_code) + + self.cbVariables.Clear() + for var in self.varList: + self.cbVariables.Append(var.code + '-' + var.name) + self.cbVariables.SetSelection(0) + + if (self.checkSite.GetValue() and not self.checkVariable.GetValue()): + self.variable_code = None + + self.setFilter(site_code=self.site_code, var_code=self.variable_code) + event.Skip() + + def onCbVariablesCombobox(self, event): + """ + + :param event: + :return: + """ + + if self.checkVariable.GetValue(): + self.variable_code = self.varList[event.GetSelection()].code + if (not self.checkSite.GetValue() and self.checkVariable.GetValue()): + self.site_code = None + self.setFilter(site_code=self.site_code, var_code=self.variable_code) + event.Skip() + + def siteAndVariables(self): + """ + + :return: + """ + self.site_code = self.siteList[self.cbSites.Selection].code + + self.cbVariables.Clear() + self.varList = self.series_service.get_variables_by_site_code(self.site_code) + for var in self.varList: + self.cbVariables.Append(var.code + '-' + var.name) + self.cbVariables.SetSelection(0) + + try: + self.variable_code = self.varList[self.cbVariables.Selection].code + self.setFilter(site_code=self.site_code, var_code=self.variable_code) + self.cbVariables.Enabled = True + self.cbSites.Enabled = True + except IndexError as i: + logger.error(i) + pass + + def siteOnly(self): + """ + + :return: + """ + self.cbVariables.Enabled = False + self.cbSites.Enabled = True + self.variable_code = None + + self.site_code = self.siteList[self.cbSites.Selection].code + self.setFilter(site_code=self.site_code) + + def variableOnly(self): + """ + + :return: + """ + self.site_code = None + self.cbVariables.Clear() + self.varList = self.series_service.get_used_variables() + for var in self.varList: + self.cbVariables.Append(var.code + '-' + var.name) + self.cbVariables.SetSelection(0) + self.cbSites.Enabled = False + self.cbVariables.Enabled = True + + self.variable_code = self.varList[0].code + + self.setFilter(var_code=self.variable_code) + + def onCheck(self, event): + """ + + :param event: + :return: + """ + # self.tableSeries.DeleteAllItems() + if self.rbAll.GetValue(): + #logger.debug("Called!") + self.rbAll.SetValue(False) + self.rbSimple.SetValue(True) + + if not self.checkSite.GetValue() and not self.checkVariable.GetValue(): + self.setFilter() + self.cbSites.Enabled = False + self.cbVariables.Enabled = False + + elif self.checkSite.GetValue(): + if self.checkVariable.GetValue(): + self.siteAndVariables() + else: + self.siteOnly() + else: + if self.checkVariable.GetValue(): + self.variableOnly() + else: + self.cbSites.Enabled = False + self.cbVariables.Enabled = False + event.Skip() + + def setFilter(self, site_code='', var_code='', advfilter=''): + """ + + :param site_code: + :param var_code: + :param advfilter: + :return: + """ + if site_code and var_code: + self.siteFilter = TextSearch(self.tblSeries, columns=self.tblSeries.columns[3:4],text=site_code) + self.variableFilter = TextSearch(self.tblSeries, columns=self.tblSeries.columns[6:7],text=var_code) + self.tblSeries.SetFilter(Chain(self.siteFilter, self.variableFilter)) + elif site_code: + self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[3:4], text=site_code)) + elif var_code: + self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[6:7], text=var_code)) + elif advfilter: + self.tblSeries.SetFilter(advfilter) + else: + self.tblSeries.SetFilter(TextSearch(self.tblSeries, columns=self.tblSeries.columns[0:1])) + self.tblSeries.RepopulateList() + + + def onReadyToPlot(self, event): + """Plots a series selected from the series selector + + :param event: EVT_OVL_CHECK_EVENT type + """ + logger.debug("Starting to Plot") + + checkedCount = len(self.tblSeries.GetCheckedObjects()) + + Publisher.sendMessage("EnablePlotButtons", plot=0, isActive=(checkedCount > 0)) + + logger.debug("Obtain object") + try: + object = event.object + except Exception as e : + object = self.tblSeries.GetSelectedObject() + + if not self.tblSeries.IsChecked(object): + Publisher.sendMessage("removePlot", seriesID=object.id) + Publisher.sendMessage("updateCursor", deselectedObject=object) + + else: + logger.debug("Obtained object, entering addplot") + self.pnlPlot.addPlot(self.memDB, object.id) + Publisher.sendMessage("updateCursor", selectedObject=object) + + logger.debug("refreshing...") + self.Refresh() + + logger.debug("Finish Plotting") + + + #from meliae import scanner + #scanner.dump_all_objects("plot_plotting.dat") + + def getSelectedObject(self, event): + """Capture the currently selected Object to be used for editing + + :param event: wx.EVT_LIST_ITEM_FOCUSED type + """ + + logger.debug("Selecting object from Series Catalog") + + object = event.GetEventObject() + editingObject = object.innerList[object.FocusedItem] + + self.tblSeries.currentlySelectedObject = editingObject + + ## update Cursor + if self.parent.Parent.pnlPlot._seriesPlotInfo: + if self.parent.Parent.pnlPlot._seriesPlotInfo.isPlotted(editingObject.id): + #print "Updating Cursor", editingObject.id + Publisher.sendMessage("updateCursor", selectedObject=editingObject) + + + def onReadyToEdit(self): + """Choose a series to edit from the series selector + + :return: + """ + + ovl = self.tblSeries + + object = ovl.currentlySelectedObject + if object is None: + # # Select the first one + if len(ovl.modelObjects) == 0: + logger.fatal("There are no model objects available to edit") + raise Exception() + object = ovl.modelObjects[0] + + if len(ovl.GetCheckedObjects()) <= ovl.allowedLimit: + + if object not in ovl.GetCheckedObjects(): + ovl.ToggleCheck(object) + + + + # logger.debug("Initializing DataTable") + # + # tasks = [("dataTable", self.memDB.conn)] + # self.taskserver.setTasks(tasks) + # self.taskserver.processTasks() + + self.isEditing = True + ovl.editingObject = object + ovl.RefreshObject(ovl.editingObject) + + + return True, object.id#, self.memDB + else: + isSelected = False + logger.debug("series was not checked") + val_2 = wx.MessageBox("Visualization is limited to 6 series.", "Can't add plot", + wx.OK | wx.ICON_INFORMATION) + + + self.isEditing = False + ovl.editingObject = None + return False, object.id#, self.memDB + + def stopEdit(self): + """When edit button is untoggled, the editing feature closes + :return: + """ + + self.isEditing = False + chcklist= self.tblSeries.GetCheckedObjects() + + self.tblSeries.RefreshObject(self.tblSeries.editingObject) + for c in chcklist: + self.tblSeries.SetCheckState(c, True) + + self.memDB.stopEdit() + + + def isEditing(self): + """ + + :return: + """ + return self.isEditing + + + def _rowFormatter(self, listItem, object): + """Handles the formatting of rows for object list view + :param: wx.ListCtrl listitem + :param: ModelObject object + + :rtype: None + """ + ''' + if self.tblSeries.editingObject and \ + object.id == self.tblSeries.editingObject.id: + #listItem.SetTextColour(wx.Colour(255, 25, 112)) + # font type: wx.DEFAULT, wx.DECORATIVE, wx.ROMAN, wx.SCRIPT, wx.SWISS, wx.MODERN + # slant: wx.NORMAL, wx.SLANT or wx.ITALIC + # weight: wx.NORMAL, wx.LIGHT or wx.BOLD + #font1 = wx.Font(10, wx.SWISS, wx.ITALIC, wx.NORMAL) + # use additional fonts this way ... + #font1 = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, False, u'Comic Sans MS') + listItem.SetFont( + wx.Font(9, family=wx.DEFAULT, weight=wx.BOLD, style=wx.ITALIC)) + else: + listItem.SetTextColour(wx.Colour()) + listItem.SetFont(wx.Font(9, wx.SWISS, wx.NORMAL, wx.NORMAL, False)) + ''' diff --git a/odmtools/controller/logicCellEdit.py b/odmtools/controller/logicCellEdit.py old mode 100755 new mode 100644 diff --git a/odmtools/controller/olvAddPoint.py b/odmtools/controller/olvAddPoint.py old mode 100755 new mode 100644 diff --git a/odmtools/controller/olvDataTable.py b/odmtools/controller/olvDataTable.py index 9c9ef48..58110d1 100644 --- a/odmtools/controller/olvDataTable.py +++ b/odmtools/controller/olvDataTable.py @@ -31,8 +31,7 @@ def init(self, memDB): self.SetColumns(columns) self.dataframe = self.memDB.getDataValuesDF() sort_by_index = list(self.dataframe.columns).index("LocalDateTime") - # self.dataframe.sort(self.dataframe.columns[sort_by_index], inplace=True) # sort() is depecrated - self.dataframe.sort_values(self.dataframe.columns[sort_by_index], inplace=True) + self.dataframe.sort(self.dataframe.columns[sort_by_index], inplace=True) self.dataObjects = self.dataframe.values.tolist() self.SetObjectGetter(self.ObjectGetter) diff --git a/odmtools/gui/frmFlagValues.py b/odmtools/gui/frmFlagValues.py old mode 100755 new mode 100644 diff --git a/odmtools/gui/frmODMTools.py b/odmtools/gui/frmODMTools.py old mode 100755 new mode 100644 diff --git a/odmtools/gui/mnuRibbon.py b/odmtools/gui/mnuRibbon.py old mode 100755 new mode 100644 diff --git a/odmtools/gui/plotProbability.py b/odmtools/gui/plotProbability.py index 1cc1f38..b875a49 100644 --- a/odmtools/gui/plotProbability.py +++ b/odmtools/gui/plotProbability.py @@ -136,9 +136,8 @@ def updatePlot(self): #self.prob.append( #prop = oneSeries.Probability.plot(column="DataValue", ax=self.plots) - # xValues = oneSeries.Probability.xAxis.order().values - xValues = oneSeries.Probability.xAxis.sort_values().values - yValues = oneSeries.Probability.yAxis.sort_values().values + xValues = oneSeries.Probability.xAxis.order().values + yValues = oneSeries.Probability.yAxis.order().values ax = self.plots.plot(xValues, yValues, 'bs', color=oneSeries.color, label=oneSeries.plotTitle) diff --git a/odmtools/gui/plotTimeSeries.py b/odmtools/gui/plotTimeSeries.py old mode 100755 new mode 100644 diff --git a/odmtools/lib/ObjectListView/ObjectListView.py b/odmtools/lib/ObjectListView/ObjectListView.py old mode 100755 new mode 100644 diff --git a/odmtools/lib/oldOlv/CellEditor.py b/odmtools/lib/oldOlv/CellEditor.py index 1244941..aa71423 100644 --- a/odmtools/lib/oldOlv/CellEditor.py +++ b/odmtools/lib/oldOlv/CellEditor.py @@ -1,548 +1,548 @@ -# -*- coding: utf-8 -*- -#---------------------------------------------------------------------------- -# Name: CellEditor.py -# Author: Phillip Piper -# Created: 3 April 2008 -# SVN-ID: $Id$ -# Copyright: (c) 2008 by Phillip Piper, 2008 -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/05/26 JPP Fixed pyLint annoyances -# 2008/04/04 JPP Initial version complete -#---------------------------------------------------------------------------- -# To do: -# - there has to be a better DateTimeEditor somewhere!! - -""" -The *CellEditor* module provides some editors for standard types that can be installed -in an *ObjectListView*. It also provides a *Registry* that maps between standard types -and functions that will create editors for that type. - -Cell Editors - - A cell editor can be any subclass of wx.Window provided that it supports the - following protocol: - - SetValue(self, value) - The editor should show the given value for editing - - GetValue(self) - The editor should return the value that it holds. Return None to indicate - an invalid value. The returned value should be of the correct type, i.e. - don't return a string if the editor was registered for the bool type. - - The editor should invoke FinishCellEdit() on its parent ObjectListView when it - loses focus or when the user commits the change by pressing Return or Enter. - - The editor should invoke CancelCellEdit() on its parent ObjectListView when - the user presses Escape. - -Editor Registry - - The editor registry remembers a function that will be called to create - an editor for a given type. -""" - -__author__ = "Phillip Piper" -__date__ = "3 May 2008" -__version__ = "1.0" - -import datetime -import wx - -#====================================================================== -# Editor Registry - -# Module level variable -_cellEditorRegistrySingleton = None - -def CellEditorRegistry(): - """ - Return the registry that is managing type to creator functions - """ - global _cellEditorRegistrySingleton - - if _cellEditorRegistrySingleton is None: - _cellEditorRegistrySingleton = EditorRegistry() - - return _cellEditorRegistrySingleton - - -class EditorRegistry: - """ - An *EditorRegistry* manages a mapping of types onto creator functions. - - When called, creator functions will create the appropriate kind of cell editor - """ - - def __init__(self): - self.typeToFunctionMap = {} - - # Standard types and their creator functions - self.typeToFunctionMap[str] = self._MakeStringEditor - self.typeToFunctionMap[unicode] = self._MakeStringEditor - self.typeToFunctionMap[bool] = self._MakeBoolEditor - self.typeToFunctionMap[int] = self._MakeIntegerEditor - self.typeToFunctionMap[long] = self._MakeLongEditor - self.typeToFunctionMap[float] = self._MakeFloatEditor - self.typeToFunctionMap[datetime.datetime] = self._MakeDateTimeEditor - self.typeToFunctionMap[datetime.date] = self._MakeDateEditor - self.typeToFunctionMap[datetime.time] = self._MakeTimeEditor - - # TODO: Install editors for mxDateTime if installed - - def GetCreatorFunction(self, aValue): - """ - Return the creator function that is register for the type of the given value. - Return None if there is no registered function for the type. - """ - return self.typeToFunctionMap.get(type(aValue), None) - - def RegisterCreatorFunction(self, aType, aFunction): - """ - Register the given function to be called when we need an editor for the given type. - - The function must accept three parameter: an ObjectListView, row index, and subitem index. - It should return a wxWindow that is parented on the listview, and that responds to: - - - SetValue(newValue) - - - GetValue() to return the value shown in the editor - - """ - self.typeToFunctionMap[aType] = aFunction - - #---------------------------------------------------------------------------- - # Creator functions for standard types - - @staticmethod - def _MakeStringEditor(olv, rowIndex, subItemIndex): - return BaseCellTextEditor(olv, subItemIndex) - - @staticmethod - def _MakeBoolEditor(olv, rowIndex, subItemIndex): - return BooleanEditor(olv) - - @staticmethod - def _MakeIntegerEditor(olv, rowIndex, subItemIndex): - return IntEditor(olv, subItemIndex, validator=NumericValidator()) - - @staticmethod - def _MakeLongEditor(olv, rowIndex, subItemIndex): - return LongEditor(olv, subItemIndex) - - @staticmethod - def _MakeFloatEditor(olv, rowIndex, subItemIndex): - return FloatEditor(olv, subItemIndex, validator=NumericValidator("0123456789-+eE.")) - - @staticmethod - def _MakeDateTimeEditor(olv, rowIndex, subItemIndex): - dte = DateTimeEditor(olv, subItemIndex) - - column = olv.columns[subItemIndex] - if isinstance(column.stringConverter, basestring): - dte.formatString = column.stringConverter - - return dte - - @staticmethod - def _MakeDateEditor(olv, rowIndex, subItemIndex): - dte = DateEditor(olv, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY | wx.WANTS_CHARS) - #dte.SetValidator(MyValidator(olv)) - return dte - - @staticmethod - def _MakeTimeEditor(olv, rowIndex, subItemIndex): - editor = TimeEditor(olv, subItemIndex) - - column = olv.columns[subItemIndex] - if isinstance(column.stringConverter, basestring): - editor.formatString = column.stringConverter - - return editor - -#====================================================================== -# Cell editors - - -class BooleanEditor(wx.Choice): - """This is a simple editor to edit a boolean value that can be used in an - ObjectListView""" - - def __init__(self, *args, **kwargs): - kwargs["choices"] = ["True", "False"] - wx.Choice.__init__(self, *args, **kwargs) - - def GetValue(self): - "Get the value from the editor" - return self.GetSelection() == 0 - - def SetValue(self, value): - "Put a new value into the editor" - if value: - self.Select(0) - else: - self.Select(1) - -#---------------------------------------------------------------------------- - -class BaseCellTextEditor(wx.TextCtrl): - """This is a base text editor for text-like editors used in an ObjectListView""" - - def __init__(self, olv, subItemIndex, **kwargs): - style = wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB - # Allow for odd case where parent isn't an ObjectListView - if hasattr(olv, "columns"): - if olv.HasFlag(wx.LC_ICON): - style |= (wx.TE_CENTRE | wx.TE_MULTILINE) - else: - style |= olv.columns[subItemIndex].GetAlignmentForText() - wx.TextCtrl.__init__(self, olv, style=style, **kwargs) - - # With the MULTILINE flag, the text control always has a vertical - # scrollbar, which looks stupid. I don't know how to get rid of it. - # This doesn't do it: - # self.ToggleWindowStyle(wx.VSCROLL) - -#---------------------------------------------------------------------------- - -class IntEditor(BaseCellTextEditor): - """This is a text editor for integers for use in an ObjectListView""" - - def GetValue(self): - "Get the value from the editor" - s = wx.TextCtrl.GetValue(self).strip() - try: - return int(s) - except ValueError: - return None - - def SetValue(self, value): - "Put a new value into the editor" - if isinstance(value, int): - value = repr(value) - wx.TextCtrl.SetValue(self, value) - -#---------------------------------------------------------------------------- - -class LongEditor(BaseCellTextEditor): - """This is a text editor for long values for use in an ObjectListView""" - - def GetValue(self): - "Get the value from the editor" - s = wx.TextCtrl.GetValue(self).strip() - try: - return long(s) - except ValueError: - return None - - def SetValue(self, value): - "Put a new value into the editor" - if isinstance(value, long): - value = repr(value) - wx.TextCtrl.SetValue(self, value) - -#---------------------------------------------------------------------------- - -class FloatEditor(BaseCellTextEditor): - """This is a text editor for floats for use in an ObjectListView. - - Because of the trouble of precisely converting floats to strings, - this editor sometimes behaves a little strangely.""" - - def GetValue(self): - "Get the value from the editor" - s = wx.TextCtrl.GetValue(self).strip() - try: - return float(s) - except ValueError: - return None - - def SetValue(self, value): - "Put a new value into the editor" - if isinstance(value, float): - value = repr(value) - wx.TextCtrl.SetValue(self, value) - -#---------------------------------------------------------------------------- - -class DateTimeEditor(BaseCellTextEditor): - """ - A DateTimeEditor allows the user to enter a date/time combination, where the time is optional - and many formats of date and time are allowed. - - The control accepts these date formats (in all cases, the year can be only 2 digits): - - '31/12/2008' - - '2008/12/31' - - '12/31/2008' - - '31 December 2008' - - '31 Dec 2008' - - 'Dec 31 2008' - - 'December 31 2008' - - Slash character can also be '-' or ' '. Consecutive whitespace are collapsed. - - The control accepts these time formats: - - '23:59:59' - - '11:59:59pm' - - '23:59' - - '11:59pm' - - '11pm' - - The colons are required. The am/pm is case insensitive. - - The implementation uses a brute force approach to parsing the data. - """ - # Acceptable formats: - # '31/12/2008', '2008/12/31', '12/31/2008', '31 December 2008', '31 Dec 2008', 'Dec 31 2007' - # second line is the same but with two-digit year. - # slash character can also be '-' or ' '. Consecutive whitespace are collapsed. - STD_DATE_FORMATS = ['%d %m %Y', '%Y %m %d', '%m %d %Y', '%d %B %Y', '%d %b %Y', '%b %d %Y', '%B %d %Y', - '%d %m %y', '%y %m %d', '%m %d %y', '%d %B %y', '%d %b %y', '%b %d %y', '%B %d %y'] - - STD_DATE_WITHOUT_YEAR_FORMATS = ['%d %m', '%m %d', '%d %B', '%d %b', '%B %d', '%b %d'] - - # Acceptable formats: '23:59:59', '11:59:59pm', '23:59', '11:59pm', '11pm' - STD_TIME_FORMATS = ['%H:%M:%S', '%I:%M:%S %p', '%H:%M', '%I:%M %p', '%I %p'] - - # These separators are treated as whitespace - STD_SEPARATORS = "/-," - - def __init__(self, *args, **kwargs): - BaseCellTextEditor.__init__(self, *args, **kwargs) - self.formatString = "%X %x" - - self.allDateTimeFormats = [] - for dtFmt in self.STD_DATE_FORMATS: - self.allDateTimeFormats.append(dtFmt) - for timeFmt in self.STD_TIME_FORMATS: - self.allDateTimeFormats.append("%s %s" % (dtFmt, timeFmt)) - - self.allDateTimeWithoutYearFormats = [] - for dtFmt in self.STD_DATE_WITHOUT_YEAR_FORMATS: - self.allDateTimeWithoutYearFormats.append(dtFmt) - for timeFmt in self.STD_TIME_FORMATS: - self.allDateTimeWithoutYearFormats.append("%s %s" % (dtFmt, timeFmt)) - - - def SetValue(self, value): - "Put a new value into the editor" - if isinstance(value, datetime.datetime): - value = value.strftime(self.formatString) - wx.TextCtrl.SetValue(self, value) - - - def GetValue(self): - "Get the value from the editor" - s = wx.TextCtrl.GetValue(self).strip() - return self._ParseDateTime(s) - - - def _ParseDateTime(self, s): - # Try the installed format string first - try: - return datetime.datetime.strptime(s, self.formatString) - except ValueError: - pass - - for x in self.STD_SEPARATORS: - s = s.replace(x, " ") - - # Because of the logic of strptime, we have to check shorter patterns first. - # For example: - # "31 12" matches "%d %m %y" => datetime(2012, 1, 3, 0, 0) ?? - # but we want: - # "31 12" to match "%d %m" => datetime(1900, 12, 31, 0, 0) - # JPP 4/4/2008 Python 2.5.1 - for fmt in self.allDateTimeWithoutYearFormats: - try: - dt = datetime.datetime.strptime(s, fmt) - return dt.replace(year=datetime.datetime.today().year) - except ValueError: - pass - - for fmt in self.allDateTimeFormats: - try: - return datetime.datetime.strptime(s, fmt) - except ValueError: - pass - - return None - -#---------------------------------------------------------------------------- - -class NumericValidator(wx.PyValidator): - """This validator only accepts numeric keys""" - - def __init__(self, acceptableChars="0123456789+-"): - wx.PyValidator.__init__(self) - self.Bind(wx.EVT_CHAR, self._OnChar) - self.acceptableChars = acceptableChars - self.acceptableCodes = [ord(x) for x in self.acceptableChars] - stdEditKeys = [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE, wx.WXK_CANCEL, - wx.WXK_TAB, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_HOME, wx.WXK_END, - wx.WXK_LEFT, wx.WXK_RIGHT] - self.acceptableCodes.extend(stdEditKeys) - - def Clone(self): - "Make a new copy of this validator" - return NumericValidator(self.acceptableChars) - - def _OnChar(self, event): - "Handle the OnChar event by rejecting non-numerics" - if event.GetModifiers() != 0 and event.GetModifiers() != wx.MOD_SHIFT: - event.Skip() - return - - if event.GetKeyCode() in self.acceptableCodes: - event.Skip() - return - - wx.Bell() - -#---------------------------------------------------------------------------- - -class DateEditor(wx.DatePickerCtrl): - """ - This control uses standard datetime. - wx.DatePickerCtrl works only with wx.DateTime, but they are strange beasts. - wx.DataTime use 0 indexed months, i.e. January==0 and December==11. - """ - - def __init__(self, *args, **kwargs): - wx.DatePickerCtrl.__init__(self, *args, **kwargs) - self.SetValue(None) - - def SetValue(self, value): - if value: - dt = wx.DateTime() - dt.Set(value.day, value.month-1, value.year) - else: - dt = wx.DateTime.Today() - wx.DatePickerCtrl.SetValue(self, dt) - - def GetValue(self): - "Get the value from the editor" - dt = wx.DatePickerCtrl.GetValue(self) - if dt.IsOk(): - return datetime.date(dt.Year, dt.Month+1, dt.Day) - else: - return None - -#---------------------------------------------------------------------------- - -class TimeEditor(BaseCellTextEditor): - """A text editor that expects and return time values""" - - # Acceptable formats: '23:59', '11:59pm', '11pm' - STD_TIME_FORMATS = ['%X', '%H:%M', '%I:%M %p', '%I %p'] - - def __init__(self, *args, **kwargs): - BaseCellTextEditor.__init__(self, *args, **kwargs) - self.formatString = "%X" - - def SetValue(self, value): - "Put a new value into the editor" - value = value or "" - if isinstance(value, datetime.time): - value = value.strftime(self.formatString) - wx.TextCtrl.SetValue(self, value) - - def GetValue(self): - "Get the value from the editor" - s = wx.TextCtrl.GetValue(self).strip() - fmts = self.STD_TIME_FORMATS[:] - if self.formatString not in fmts: - fmts.insert(0, self.formatString) - for fmt in fmts: - try: - dt = datetime.datetime.strptime(s, fmt) - return dt.time() - except ValueError: - pass - - return None - -#====================================================================== -# Auto complete controls - -def MakeAutoCompleteTextBox(olv, columnIndex, maxObjectsToConsider=10000): - """ - Return a TextCtrl that lets the user choose from all existing values in this column. - Do not call for large lists - """ - col = olv.columns[columnIndex] - #THINK: We could make this time based, i.e. it escapes after 1 second. - maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) - options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) - tb = BaseCellTextEditor(olv, columnIndex) - AutoCompleteHelper(tb, list(options)) - return tb - -def MakeAutoCompleteComboBox(olv, columnIndex, maxObjectsToConsider=10000): - """ - Return a ComboBox that lets the user choose from all existing values in this column. - Do not call for large lists - """ - col = olv.columns[columnIndex] - maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) - options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) - cb = wx.ComboBox(olv, choices=list(options), - style=wx.CB_DROPDOWN|wx.CB_SORT|wx.TE_PROCESS_ENTER) - AutoCompleteHelper(cb) - return cb - - -#------------------------------------------------------------------------- - -class AutoCompleteHelper(object): - """ - This class operates on a text control or combobox, and automatically completes the - text typed by the user from a list of entries in a given list. - - """ - - def __init__(self, control, possibleValues=None): - self.control = control - self.lastUserEnteredString = self.control.GetValue() - self.control.Bind(wx.EVT_TEXT, self._OnTextEvent) - if isinstance(self.control, wx.ComboBox): - self.possibleValues = self.control.GetStrings() - else: - self.possibleValues = possibleValues or [] - self.lowerCasePossibleValues = [x.lower() for x in self.possibleValues] - - - def _OnTextEvent(self, evt): - evt.Skip() - # After the SetValue() we want to ignore this event. If we get this event - # and the value hasn't been modified, we know it was a SetValue() call. - if hasattr(self.control, "IsModified") and not self.control.IsModified(): - return - - # If the text has changed more than the user just typing one letter, - # then don't try to autocomplete it. - if len(evt.GetString()) != len(self.lastUserEnteredString)+1: - self.lastUserEnteredString = evt.GetString() - return - - self.lastUserEnteredString = evt.GetString() - s = evt.GetString().lower() - for i, x in enumerate(self.lowerCasePossibleValues): - if x.startswith(s): - self._AutocompleteWith(self.possibleValues[i]) - break - - - def _AutocompleteWith(self, newValue): - """Suggest the given value by autocompleting it.""" - # GetInsertionPoint() doesn't seem reliable under linux - insertIndex = len(self.control.GetValue()) - self.control.SetValue(newValue) - if isinstance(self.control, wx.ComboBox): - self.control.SetMark(insertIndex, len(newValue)) - else: - # Seems that under linux, selecting only seems to work here if we do it - # outside of the text event - wx.CallAfter(self.control.SetSelection, insertIndex, len(newValue)) +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: CellEditor.py +# Author: Phillip Piper +# Created: 3 April 2008 +# SVN-ID: $Id$ +# Copyright: (c) 2008 by Phillip Piper, 2008 +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/05/26 JPP Fixed pyLint annoyances +# 2008/04/04 JPP Initial version complete +#---------------------------------------------------------------------------- +# To do: +# - there has to be a better DateTimeEditor somewhere!! + +""" +The *CellEditor* module provides some editors for standard types that can be installed +in an *ObjectListView*. It also provides a *Registry* that maps between standard types +and functions that will create editors for that type. + +Cell Editors + + A cell editor can be any subclass of wx.Window provided that it supports the + following protocol: + + SetValue(self, value) + The editor should show the given value for editing + + GetValue(self) + The editor should return the value that it holds. Return None to indicate + an invalid value. The returned value should be of the correct type, i.e. + don't return a string if the editor was registered for the bool type. + + The editor should invoke FinishCellEdit() on its parent ObjectListView when it + loses focus or when the user commits the change by pressing Return or Enter. + + The editor should invoke CancelCellEdit() on its parent ObjectListView when + the user presses Escape. + +Editor Registry + + The editor registry remembers a function that will be called to create + an editor for a given type. +""" + +__author__ = "Phillip Piper" +__date__ = "3 May 2008" +__version__ = "1.0" + +import datetime +import wx + +#====================================================================== +# Editor Registry + +# Module level variable +_cellEditorRegistrySingleton = None + +def CellEditorRegistry(): + """ + Return the registry that is managing type to creator functions + """ + global _cellEditorRegistrySingleton + + if _cellEditorRegistrySingleton is None: + _cellEditorRegistrySingleton = EditorRegistry() + + return _cellEditorRegistrySingleton + + +class EditorRegistry: + """ + An *EditorRegistry* manages a mapping of types onto creator functions. + + When called, creator functions will create the appropriate kind of cell editor + """ + + def __init__(self): + self.typeToFunctionMap = {} + + # Standard types and their creator functions + self.typeToFunctionMap[str] = self._MakeStringEditor + self.typeToFunctionMap[unicode] = self._MakeStringEditor + self.typeToFunctionMap[bool] = self._MakeBoolEditor + self.typeToFunctionMap[int] = self._MakeIntegerEditor + self.typeToFunctionMap[long] = self._MakeLongEditor + self.typeToFunctionMap[float] = self._MakeFloatEditor + self.typeToFunctionMap[datetime.datetime] = self._MakeDateTimeEditor + self.typeToFunctionMap[datetime.date] = self._MakeDateEditor + self.typeToFunctionMap[datetime.time] = self._MakeTimeEditor + + # TODO: Install editors for mxDateTime if installed + + def GetCreatorFunction(self, aValue): + """ + Return the creator function that is register for the type of the given value. + Return None if there is no registered function for the type. + """ + return self.typeToFunctionMap.get(type(aValue), None) + + def RegisterCreatorFunction(self, aType, aFunction): + """ + Register the given function to be called when we need an editor for the given type. + + The function must accept three parameter: an ObjectListView, row index, and subitem index. + It should return a wxWindow that is parented on the listview, and that responds to: + + - SetValue(newValue) + + - GetValue() to return the value shown in the editor + + """ + self.typeToFunctionMap[aType] = aFunction + + #---------------------------------------------------------------------------- + # Creator functions for standard types + + @staticmethod + def _MakeStringEditor(olv, rowIndex, subItemIndex): + return BaseCellTextEditor(olv, subItemIndex) + + @staticmethod + def _MakeBoolEditor(olv, rowIndex, subItemIndex): + return BooleanEditor(olv) + + @staticmethod + def _MakeIntegerEditor(olv, rowIndex, subItemIndex): + return IntEditor(olv, subItemIndex, validator=NumericValidator()) + + @staticmethod + def _MakeLongEditor(olv, rowIndex, subItemIndex): + return LongEditor(olv, subItemIndex) + + @staticmethod + def _MakeFloatEditor(olv, rowIndex, subItemIndex): + return FloatEditor(olv, subItemIndex, validator=NumericValidator("0123456789-+eE.")) + + @staticmethod + def _MakeDateTimeEditor(olv, rowIndex, subItemIndex): + dte = DateTimeEditor(olv, subItemIndex) + + column = olv.columns[subItemIndex] + if isinstance(column.stringConverter, basestring): + dte.formatString = column.stringConverter + + return dte + + @staticmethod + def _MakeDateEditor(olv, rowIndex, subItemIndex): + dte = DateEditor(olv, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY | wx.WANTS_CHARS) + #dte.SetValidator(MyValidator(olv)) + return dte + + @staticmethod + def _MakeTimeEditor(olv, rowIndex, subItemIndex): + editor = TimeEditor(olv, subItemIndex) + + column = olv.columns[subItemIndex] + if isinstance(column.stringConverter, basestring): + editor.formatString = column.stringConverter + + return editor + +#====================================================================== +# Cell editors + + +class BooleanEditor(wx.Choice): + """This is a simple editor to edit a boolean value that can be used in an + ObjectListView""" + + def __init__(self, *args, **kwargs): + kwargs["choices"] = ["True", "False"] + wx.Choice.__init__(self, *args, **kwargs) + + def GetValue(self): + "Get the value from the editor" + return self.GetSelection() == 0 + + def SetValue(self, value): + "Put a new value into the editor" + if value: + self.Select(0) + else: + self.Select(1) + +#---------------------------------------------------------------------------- + +class BaseCellTextEditor(wx.TextCtrl): + """This is a base text editor for text-like editors used in an ObjectListView""" + + def __init__(self, olv, subItemIndex, **kwargs): + style = wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB + # Allow for odd case where parent isn't an ObjectListView + if hasattr(olv, "columns"): + if olv.HasFlag(wx.LC_ICON): + style |= (wx.TE_CENTRE | wx.TE_MULTILINE) + else: + style |= olv.columns[subItemIndex].GetAlignmentForText() + wx.TextCtrl.__init__(self, olv, style=style, **kwargs) + + # With the MULTILINE flag, the text control always has a vertical + # scrollbar, which looks stupid. I don't know how to get rid of it. + # This doesn't do it: + # self.ToggleWindowStyle(wx.VSCROLL) + +#---------------------------------------------------------------------------- + +class IntEditor(BaseCellTextEditor): + """This is a text editor for integers for use in an ObjectListView""" + + def GetValue(self): + "Get the value from the editor" + s = wx.TextCtrl.GetValue(self).strip() + try: + return int(s) + except ValueError: + return None + + def SetValue(self, value): + "Put a new value into the editor" + if isinstance(value, int): + value = repr(value) + wx.TextCtrl.SetValue(self, value) + +#---------------------------------------------------------------------------- + +class LongEditor(BaseCellTextEditor): + """This is a text editor for long values for use in an ObjectListView""" + + def GetValue(self): + "Get the value from the editor" + s = wx.TextCtrl.GetValue(self).strip() + try: + return long(s) + except ValueError: + return None + + def SetValue(self, value): + "Put a new value into the editor" + if isinstance(value, long): + value = repr(value) + wx.TextCtrl.SetValue(self, value) + +#---------------------------------------------------------------------------- + +class FloatEditor(BaseCellTextEditor): + """This is a text editor for floats for use in an ObjectListView. + + Because of the trouble of precisely converting floats to strings, + this editor sometimes behaves a little strangely.""" + + def GetValue(self): + "Get the value from the editor" + s = wx.TextCtrl.GetValue(self).strip() + try: + return float(s) + except ValueError: + return None + + def SetValue(self, value): + "Put a new value into the editor" + if isinstance(value, float): + value = repr(value) + wx.TextCtrl.SetValue(self, value) + +#---------------------------------------------------------------------------- + +class DateTimeEditor(BaseCellTextEditor): + """ + A DateTimeEditor allows the user to enter a date/time combination, where the time is optional + and many formats of date and time are allowed. + + The control accepts these date formats (in all cases, the year can be only 2 digits): + - '31/12/2008' + - '2008/12/31' + - '12/31/2008' + - '31 December 2008' + - '31 Dec 2008' + - 'Dec 31 2008' + - 'December 31 2008' + + Slash character can also be '-' or ' '. Consecutive whitespace are collapsed. + + The control accepts these time formats: + - '23:59:59' + - '11:59:59pm' + - '23:59' + - '11:59pm' + - '11pm' + + The colons are required. The am/pm is case insensitive. + + The implementation uses a brute force approach to parsing the data. + """ + # Acceptable formats: + # '31/12/2008', '2008/12/31', '12/31/2008', '31 December 2008', '31 Dec 2008', 'Dec 31 2007' + # second line is the same but with two-digit year. + # slash character can also be '-' or ' '. Consecutive whitespace are collapsed. + STD_DATE_FORMATS = ['%d %m %Y', '%Y %m %d', '%m %d %Y', '%d %B %Y', '%d %b %Y', '%b %d %Y', '%B %d %Y', + '%d %m %y', '%y %m %d', '%m %d %y', '%d %B %y', '%d %b %y', '%b %d %y', '%B %d %y'] + + STD_DATE_WITHOUT_YEAR_FORMATS = ['%d %m', '%m %d', '%d %B', '%d %b', '%B %d', '%b %d'] + + # Acceptable formats: '23:59:59', '11:59:59pm', '23:59', '11:59pm', '11pm' + STD_TIME_FORMATS = ['%H:%M:%S', '%I:%M:%S %p', '%H:%M', '%I:%M %p', '%I %p'] + + # These separators are treated as whitespace + STD_SEPARATORS = "/-," + + def __init__(self, *args, **kwargs): + BaseCellTextEditor.__init__(self, *args, **kwargs) + self.formatString = "%X %x" + + self.allDateTimeFormats = [] + for dtFmt in self.STD_DATE_FORMATS: + self.allDateTimeFormats.append(dtFmt) + for timeFmt in self.STD_TIME_FORMATS: + self.allDateTimeFormats.append("%s %s" % (dtFmt, timeFmt)) + + self.allDateTimeWithoutYearFormats = [] + for dtFmt in self.STD_DATE_WITHOUT_YEAR_FORMATS: + self.allDateTimeWithoutYearFormats.append(dtFmt) + for timeFmt in self.STD_TIME_FORMATS: + self.allDateTimeWithoutYearFormats.append("%s %s" % (dtFmt, timeFmt)) + + + def SetValue(self, value): + "Put a new value into the editor" + if isinstance(value, datetime.datetime): + value = value.strftime(self.formatString) + wx.TextCtrl.SetValue(self, value) + + + def GetValue(self): + "Get the value from the editor" + s = wx.TextCtrl.GetValue(self).strip() + return self._ParseDateTime(s) + + + def _ParseDateTime(self, s): + # Try the installed format string first + try: + return datetime.datetime.strptime(s, self.formatString) + except ValueError: + pass + + for x in self.STD_SEPARATORS: + s = s.replace(x, " ") + + # Because of the logic of strptime, we have to check shorter patterns first. + # For example: + # "31 12" matches "%d %m %y" => datetime(2012, 1, 3, 0, 0) ?? + # but we want: + # "31 12" to match "%d %m" => datetime(1900, 12, 31, 0, 0) + # JPP 4/4/2008 Python 2.5.1 + for fmt in self.allDateTimeWithoutYearFormats: + try: + dt = datetime.datetime.strptime(s, fmt) + return dt.replace(year=datetime.datetime.today().year) + except ValueError: + pass + + for fmt in self.allDateTimeFormats: + try: + return datetime.datetime.strptime(s, fmt) + except ValueError: + pass + + return None + +#---------------------------------------------------------------------------- + +class NumericValidator(wx.PyValidator): + """This validator only accepts numeric keys""" + + def __init__(self, acceptableChars="0123456789+-"): + wx.PyValidator.__init__(self) + self.Bind(wx.EVT_CHAR, self._OnChar) + self.acceptableChars = acceptableChars + self.acceptableCodes = [ord(x) for x in self.acceptableChars] + stdEditKeys = [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE, wx.WXK_CANCEL, + wx.WXK_TAB, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_HOME, wx.WXK_END, + wx.WXK_LEFT, wx.WXK_RIGHT] + self.acceptableCodes.extend(stdEditKeys) + + def Clone(self): + "Make a new copy of this validator" + return NumericValidator(self.acceptableChars) + + def _OnChar(self, event): + "Handle the OnChar event by rejecting non-numerics" + if event.GetModifiers() != 0 and event.GetModifiers() != wx.MOD_SHIFT: + event.Skip() + return + + if event.GetKeyCode() in self.acceptableCodes: + event.Skip() + return + + wx.Bell() + +#---------------------------------------------------------------------------- + +class DateEditor(wx.DatePickerCtrl): + """ + This control uses standard datetime. + wx.DatePickerCtrl works only with wx.DateTime, but they are strange beasts. + wx.DataTime use 0 indexed months, i.e. January==0 and December==11. + """ + + def __init__(self, *args, **kwargs): + wx.DatePickerCtrl.__init__(self, *args, **kwargs) + self.SetValue(None) + + def SetValue(self, value): + if value: + dt = wx.DateTime() + dt.Set(value.day, value.month-1, value.year) + else: + dt = wx.DateTime.Today() + wx.DatePickerCtrl.SetValue(self, dt) + + def GetValue(self): + "Get the value from the editor" + dt = wx.DatePickerCtrl.GetValue(self) + if dt.IsOk(): + return datetime.date(dt.Year, dt.Month+1, dt.Day) + else: + return None + +#---------------------------------------------------------------------------- + +class TimeEditor(BaseCellTextEditor): + """A text editor that expects and return time values""" + + # Acceptable formats: '23:59', '11:59pm', '11pm' + STD_TIME_FORMATS = ['%X', '%H:%M', '%I:%M %p', '%I %p'] + + def __init__(self, *args, **kwargs): + BaseCellTextEditor.__init__(self, *args, **kwargs) + self.formatString = "%X" + + def SetValue(self, value): + "Put a new value into the editor" + value = value or "" + if isinstance(value, datetime.time): + value = value.strftime(self.formatString) + wx.TextCtrl.SetValue(self, value) + + def GetValue(self): + "Get the value from the editor" + s = wx.TextCtrl.GetValue(self).strip() + fmts = self.STD_TIME_FORMATS[:] + if self.formatString not in fmts: + fmts.insert(0, self.formatString) + for fmt in fmts: + try: + dt = datetime.datetime.strptime(s, fmt) + return dt.time() + except ValueError: + pass + + return None + +#====================================================================== +# Auto complete controls + +def MakeAutoCompleteTextBox(olv, columnIndex, maxObjectsToConsider=10000): + """ + Return a TextCtrl that lets the user choose from all existing values in this column. + Do not call for large lists + """ + col = olv.columns[columnIndex] + #THINK: We could make this time based, i.e. it escapes after 1 second. + maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) + options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) + tb = BaseCellTextEditor(olv, columnIndex) + AutoCompleteHelper(tb, list(options)) + return tb + +def MakeAutoCompleteComboBox(olv, columnIndex, maxObjectsToConsider=10000): + """ + Return a ComboBox that lets the user choose from all existing values in this column. + Do not call for large lists + """ + col = olv.columns[columnIndex] + maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) + options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) + cb = wx.ComboBox(olv, choices=list(options), + style=wx.CB_DROPDOWN|wx.CB_SORT|wx.TE_PROCESS_ENTER) + AutoCompleteHelper(cb) + return cb + + +#------------------------------------------------------------------------- + +class AutoCompleteHelper(object): + """ + This class operates on a text control or combobox, and automatically completes the + text typed by the user from a list of entries in a given list. + + """ + + def __init__(self, control, possibleValues=None): + self.control = control + self.lastUserEnteredString = self.control.GetValue() + self.control.Bind(wx.EVT_TEXT, self._OnTextEvent) + if isinstance(self.control, wx.ComboBox): + self.possibleValues = self.control.GetStrings() + else: + self.possibleValues = possibleValues or [] + self.lowerCasePossibleValues = [x.lower() for x in self.possibleValues] + + + def _OnTextEvent(self, evt): + evt.Skip() + # After the SetValue() we want to ignore this event. If we get this event + # and the value hasn't been modified, we know it was a SetValue() call. + if hasattr(self.control, "IsModified") and not self.control.IsModified(): + return + + # If the text has changed more than the user just typing one letter, + # then don't try to autocomplete it. + if len(evt.GetString()) != len(self.lastUserEnteredString)+1: + self.lastUserEnteredString = evt.GetString() + return + + self.lastUserEnteredString = evt.GetString() + s = evt.GetString().lower() + for i, x in enumerate(self.lowerCasePossibleValues): + if x.startswith(s): + self._AutocompleteWith(self.possibleValues[i]) + break + + + def _AutocompleteWith(self, newValue): + """Suggest the given value by autocompleting it.""" + # GetInsertionPoint() doesn't seem reliable under linux + insertIndex = len(self.control.GetValue()) + self.control.SetValue(newValue) + if isinstance(self.control, wx.ComboBox): + self.control.SetMark(insertIndex, len(newValue)) + else: + # Seems that under linux, selecting only seems to work here if we do it + # outside of the text event + wx.CallAfter(self.control.SetSelection, insertIndex, len(newValue)) diff --git a/odmtools/lib/oldOlv/Filter.py b/odmtools/lib/oldOlv/Filter.py index 97dcf5d..4e2a653 100644 --- a/odmtools/lib/oldOlv/Filter.py +++ b/odmtools/lib/oldOlv/Filter.py @@ -1,138 +1,138 @@ -# -*- coding: utf-8 -*- -#---------------------------------------------------------------------------- -# Name: Filter.py -# Author: Phillip Piper -# Created: 26 August 2008 -# Copyright: (c) 2008 Phillip Piper -# SVN-ID: $Id$ -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/08/26 JPP First version -#---------------------------------------------------------------------------- -# To do: -# - -""" -Filters provide a structured mechanism to display only some of the model objects -given to an ObjectListView. Only those model objects which are 'chosen' by -an installed filter will be presented to the user. - -Filters are simple callable objects which accept a single parameter, which -is the list of model objects to be filtered, and returns a collection of -those objects which will be presented to the user. - -This module provides some standard filters. - -Filters almost always impose a performance penalty on the ObjectListView. -The penalty is normally O(n) since the filter normally examines each model -object to see if it should be included. Head() and Tail() are exceptions -to this observation. -""" - -def Predicate(predicate): - """ - Display only those objects that match the given predicate - - Example:: - self.olv.SetFilter(Filter.Predicate(lambda x: x.IsOverdue())) - """ - return lambda modelObjects: [x for x in modelObjects if predicate(x)] - - -def Head(num): - """ - Display at most the first N of the model objects - - Example:: - self.olv.SetFilter(Filter.Head(1000)) - """ - return lambda modelObjects: modelObjects[:num] - - -def Tail(num): - """ - Display at most the last N of the model objects - - Example:: - self.olv.SetFilter(Filter.Tail(1000)) - """ - return lambda modelObjects: modelObjects[-num:] - - -class TextSearch(object): - """ - Return only model objects that match a given string. If columns is not empty, - only those columns will be considered when searching for the string. Otherwise, - all columns will be searched. - - Example:: - self.olv.SetFilter(Filter.TextSearch(self.olv, text="findthis")) - self.olv.RepopulateList() - """ - - def __init__(self, objectListView, columns=(), text=""): - """ - Create a filter that includes on modelObject that have 'self.text' somewhere in the given columns. - """ - self.objectListView = objectListView - self.columns = columns - self.text = text - - def __call__(self, modelObjects): - """ - Return the model objects that contain our text in one of the columns to consider - """ - if not self.text: - return modelObjects - - # In non-report views, we can only search the primary column - if self.objectListView.InReportView(): - cols = self.columns or self.objectListView.columns - else: - cols = [self.objectListView.columns[0]] - - textToFind = self.text.lower() - - def _containsText(modelObject): - for col in cols: - if textToFind in col.GetStringValue(modelObject).lower(): - return True - return False - - return [x for x in modelObjects if _containsText(x)] - - def SetText(self, text): - """ - Set the text that this filter will match. Set this to None or "" to disable the filter. - """ - self.text = text - - -class Chain(object): - """ - Return only model objects that match all of the given filters. - - Example:: - # Show at most 100 people whose salary is over 50,000 - salaryFilter = Filter.Predicate(lambda person: person.GetSalary() > 50000) - self.olv.SetFilter(Filter.Chain(salaryFilter, Filter.Tail(100))) - self.olv.RepopulateList() - """ - - def __init__(self, *filters): - """ - Create a filter that performs all the given filters. - - The order of the filters is important. - """ - self.filters = filters - - - def __call__(self, modelObjects): - """ - Return the model objects that match all of our filters - """ - for filter in self.filters: - modelObjects = filter(modelObjects) - return modelObjects +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: Filter.py +# Author: Phillip Piper +# Created: 26 August 2008 +# Copyright: (c) 2008 Phillip Piper +# SVN-ID: $Id$ +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/08/26 JPP First version +#---------------------------------------------------------------------------- +# To do: +# + +""" +Filters provide a structured mechanism to display only some of the model objects +given to an ObjectListView. Only those model objects which are 'chosen' by +an installed filter will be presented to the user. + +Filters are simple callable objects which accept a single parameter, which +is the list of model objects to be filtered, and returns a collection of +those objects which will be presented to the user. + +This module provides some standard filters. + +Filters almost always impose a performance penalty on the ObjectListView. +The penalty is normally O(n) since the filter normally examines each model +object to see if it should be included. Head() and Tail() are exceptions +to this observation. +""" + +def Predicate(predicate): + """ + Display only those objects that match the given predicate + + Example:: + self.olv.SetFilter(Filter.Predicate(lambda x: x.IsOverdue())) + """ + return lambda modelObjects: [x for x in modelObjects if predicate(x)] + + +def Head(num): + """ + Display at most the first N of the model objects + + Example:: + self.olv.SetFilter(Filter.Head(1000)) + """ + return lambda modelObjects: modelObjects[:num] + + +def Tail(num): + """ + Display at most the last N of the model objects + + Example:: + self.olv.SetFilter(Filter.Tail(1000)) + """ + return lambda modelObjects: modelObjects[-num:] + + +class TextSearch(object): + """ + Return only model objects that match a given string. If columns is not empty, + only those columns will be considered when searching for the string. Otherwise, + all columns will be searched. + + Example:: + self.olv.SetFilter(Filter.TextSearch(self.olv, text="findthis")) + self.olv.RepopulateList() + """ + + def __init__(self, objectListView, columns=(), text=""): + """ + Create a filter that includes on modelObject that have 'self.text' somewhere in the given columns. + """ + self.objectListView = objectListView + self.columns = columns + self.text = text + + def __call__(self, modelObjects): + """ + Return the model objects that contain our text in one of the columns to consider + """ + if not self.text: + return modelObjects + + # In non-report views, we can only search the primary column + if self.objectListView.InReportView(): + cols = self.columns or self.objectListView.columns + else: + cols = [self.objectListView.columns[0]] + + textToFind = self.text.lower() + + def _containsText(modelObject): + for col in cols: + if textToFind in col.GetStringValue(modelObject).lower(): + return True + return False + + return [x for x in modelObjects if _containsText(x)] + + def SetText(self, text): + """ + Set the text that this filter will match. Set this to None or "" to disable the filter. + """ + self.text = text + + +class Chain(object): + """ + Return only model objects that match all of the given filters. + + Example:: + # Show at most 100 people whose salary is over 50,000 + salaryFilter = Filter.Predicate(lambda person: person.GetSalary() > 50000) + self.olv.SetFilter(Filter.Chain(salaryFilter, Filter.Tail(100))) + self.olv.RepopulateList() + """ + + def __init__(self, *filters): + """ + Create a filter that performs all the given filters. + + The order of the filters is important. + """ + self.filters = filters + + + def __call__(self, modelObjects): + """ + Return the model objects that match all of our filters + """ + for filter in self.filters: + modelObjects = filter(modelObjects) + return modelObjects diff --git a/odmtools/lib/oldOlv/ListCtrlPrinter.py b/odmtools/lib/oldOlv/ListCtrlPrinter.py index 550ec6b..e1b3fe5 100644 --- a/odmtools/lib/oldOlv/ListCtrlPrinter.py +++ b/odmtools/lib/oldOlv/ListCtrlPrinter.py @@ -1,2797 +1,2797 @@ -# -*- coding: utf-8 -*- -#!/usr/bin/env python -#---------------------------------------------------------------------------- -# Name: ListCtrlPrinter.py -# Author: Phillip Piper -# Created: 17 July 2008 -# SVN-ID: $Id$ -# Copyright: (c) 2008 by Phillip Piper, 2008 -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/07/17 JPP Inspired beginning -#---------------------------------------------------------------------------- -# To do: -# - persistence of ReportFormat -# - allow cell contents to be vertically aligned -# - in light of several of the "to dos", consider having CellBlockFormat object -# - write a data abstraction layer between the printer and the ListCtrl. -# This layer could understand ObjectListViews and be more efficient. -# - consider if this could be made to work with a wx.Grid (needs data abstraction layer) - -# Known issues: -# - the 'space' setting on decorations is not intuitive - - -""" -An ``ListCtrlPrinter`` takes an ``ObjectListView`` or ``wx.ListCtrl`` and turns it into a -pretty report. - -As always, the goal is for this to be as easy to use as possible. A typical -usage should be as simple as:: - - printer = ListCtrlPrinter(self.myOlv, "My Report Title") - printer.PrintPreview() - -This will produce a report with reasonable formatting. The formatting of a report is -controlled completely by the ReportFormat object of the ListCtrlPrinter. To change the appearance -of the report, you change the settings in this object. - -A report consists of various sections (called "blocks") and each of these blocks has a -matching BlockFormat object in the ReportFormat. So, to modify the format of the -page header, you change the ReportFormat.PageHeader object. - -A ReportFormat object has the following properties which control the appearance of the matching -sections of the report: - -* PageHeader -* ListHeader -* ColumnHeader -* GroupTitle -* Row -* ListFooter -* PageFooter -* Page - -These properties return BlockFormat objects, which have the following properties: - -* AlwaysCenter -* CanWrap -* Font -* Padding -* TextAlignment -* TextColor - -* CellPadding -* GridPen - -Implementation -============== - -A ``ListCtrlPrinter`` is a *Facade* over two classes: - - ``ListCtrlPrintout``, which handles the interface to the wx printing subsystem - - ``ReportEngine``, which does the actual work of creating the report - -The ``ListCtrlPrintout`` handles all the details of the wx printing subsystem. In -particular, it configures the printing DC so that its origin and scale are correct. This -enables the ``ReportEngine`` to simply render the report without knowing the -characteristics of the underlying printer DPI, unprintable region, or the scale of a print -preview. When The ``ListCtrlPrintout`` encounters some action that is cannot perform (like -actually rendering a page) it calls back into ``ListCtrlPrinter`` (which simply forwards -to the ``ReportEngine``). - -The ``ReportEngine`` uses a block structure approach to reports. Each element of -a report is a "block", which stacks vertically with other blocks. - -When a block is printed, it can either render itself into a given DC or it can replace -itself in the report structure with one or more other blocks. A ``TextBlock`` renders -itself by drawing into the DC. In contrast, the block that prints a ``ListCtrl`` replaces -itself with a ``ListHeaderBlock``, a ``ColumnHeaderBlock``, ``ListRowsBlock`` and finally a -``ListFooterBlock``. - -The blocks describe the structure of the report. The formatting of a report is -controlled by ``BlockFormat`` objects. The ``ReportFormat`` contains a ``BlockFormat`` -object for each formattable section of the report. - -Each section must be formatted in same fashion, i.e. all column headers will look the same, -all page footer will look the same. There is no way to make the first footer look one way, -and the second look a different way. -""" - -import datetime -import math - -import wx - -from WordWrapRenderer import WordWrapRenderer - -#---------------------------------------------------------------------------- - -class ListCtrlPrinter(object): - """ - An ListCtrlPrinter creates a pretty report from an ObjectListView/ListCtrl. - - """ - - def __init__(self, listCtrl=None, title="ListCtrl Printing"): - """ - """ - self.printout = ListCtrlPrintout(self) - self.engine = ReportEngine() - if listCtrl is not None: - self.AddListCtrl(listCtrl, title) - - #---------------------------------------------------------------------------- - # Accessing - - def GetPageFooter(self): - """ - Return a 3-tuple of the texts that will be shown in left, center, and right cells - of the page footer - """ - return self.engine.pageFooter - - def SetPageFooter(self, leftText="", centerText="", rightText=""): - """ - Set the texts that will be shown in various cells of the page footer. - - leftText can be a string or a 3-tuple of strings. - """ - if isinstance(leftText, (tuple, list)): - self.engine.pageFooter = leftText - else: - self.engine.pageFooter = (leftText, centerText, rightText) - - def GetPageHeader(self): - """ - Return a 3-tuple of the texts that will be shown in left, center, and right cells - of the page header - """ - return self.engine.pageHeader - - def SetPageHeader(self, leftText="", centerText="", rightText=""): - """ - Set the texts that will be shown in various cells of the page header - """ - if isinstance(leftText, (tuple, list)): - self.engine.pageHeader = leftText - else: - self.engine.pageHeader = (leftText, centerText, rightText) - - def GetPrintData(self): - """ - Return the wx.PrintData that controls the printing of this report - """ - return self.printout.printData - - def GetReportFormat(self): - """ - Return the ReportFormat object that controls the appearance of this printout - """ - return self.engine.reportFormat - - def SetReportFormat(self, fmt): - """ - Set the ReportFormat object that controls the appearance of this printout - """ - self.engine.reportFormat = fmt - - def GetWatermark(self, txt): - """ - Get the text that will be printed as a watermark on the report - """ - return self.engine.watermark - - def SetWatermark(self, txt): - """ - Set the text that will be printed as a watermark on the report - """ - self.engine.watermark = txt - - ReportFormat = property(GetReportFormat, SetReportFormat) - PageFooter = property(GetPageFooter, SetPageFooter) - PageHeader = property(GetPageHeader, SetPageHeader) - PrintData = property(GetPrintData) - Watermark = property(GetWatermark, SetWatermark) - - #---------------------------------------------------------------------------- - # Setup - - def AddListCtrl(self, listCtrl, title=None): - """ - Add the given list to those that will be printed by this report. - """ - self.engine.AddListCtrl(listCtrl, title) - - - def Clear(self): - """ - Remove all ListCtrls from this printer - """ - self.engine.ClearListCtrls() - - #---------------------------------------------------------------------------- - # Printing Commands - - def PageSetup(self, parent=None): - """ - Show a Page Setup dialog that will change the configuration of this printout - """ - self.printout.PageSetup(parent) - - - def PrintPreview(self, parent=None, title="ListCtrl Print Preview", bounds=(20, 50, 800, 800)): - """ - Show a Print Preview of this report - """ - self.printout.PrintPreview(parent, title, bounds) - - - def Print(self, parent=None): - """ - Print this report to the selected printer - """ - self.printout.DoPrint(parent) - - #---------------------------------------------------------------------------- - # Callbacks - # These methods are invoked by the ListCtrlPrintout when required - - def CalculateTotalPages(self, dc, bounds): - """ - Do the work of calculating how many pages this report will occupy? - - This is expensive because it basically prints the whole report. - """ - return self.engine.CalculateTotalPages(dc, bounds) - - - def StartPrinting(self): - """ - A new print job is about to begin. - """ - self.engine.StartPrinting() - - - def PrintPage(self, dc, pageNumber, bounds): - """ - Print the given page on the given device context. - """ - self.engine.PrintPage(dc, pageNumber, bounds) - -#---------------------------------------------------------------------------- - -class ReportEngine(object): - """ - A ReportEngine handles all the work of actually producing a report. - - Public instance variables (all others should be treated as private): - - * dateFormat - When the current date/time is substituted into report text, how should - the datetime be formatted? This must be a valid format string for the - strftime() method. - Default: "%x %X" - - """ - - def __init__(self): - """ - """ - self.currentPage = -1 - self.totalPages = -1 - self.blocks = list() - self.blockInsertionIndex = 0 - self.listCtrls = list() - self.dateFormat = "%x %X" - - self.reportFormat = ReportFormat.Normal() - - self.watermark = "" - self.pageHeader = list() - self.pageFooter = list() - #self.isPrintSelectionOnly = False # not currently implemented - - # If this is False, no drawing should be done. The engine is either counting - # pages, or skipping to a specific page - self.shouldDrawBlocks = True - - #---------------------------------------------------------------------------- - # Accessing - - def GetNamedFormat(self, name): - """ - Return the given format - """ - return self.reportFormat.GetNamedFormat(name) - - - def GetTotalPages(self): - """ - Return the total number of pages that this report will produce. - - CalculateTotalPages() must be called before this is accurate. - """ - return self.totalPages - - - def GetSubstitutionInfo(self): - """ - Return a dictionary that can be used for substituting values into strings - """ - dateString = datetime.datetime.now().strftime(self.dateFormat) - info = { - "currentPage": self.currentPage, - "date": dateString, - "totalPages": self.totalPages, - } - return info - - #---------------------------------------------------------------------------- - # Calculating - - def CalculateTotalPages(self, dc, bounds): - """ - Do the work of calculating how many pages this report will occupy? - - This is expensive because it basically prints the whole report. - """ - self.StartPrinting() - self.totalPages = 1 - self.shouldDrawBlocks = False - while self.PrintOnePage(dc, self.totalPages, bounds): - self.totalPages += 1 - self.shouldDrawBlocks = True - return self.totalPages - - #---------------------------------------------------------------------------- - # Commands - - def AddBlock(self, block): - """ - Add the given block at the current insertion point - """ - self.blocks.insert(self.blockInsertionIndex, block) - self.blockInsertionIndex += 1 - block.engine = self - - - def AddListCtrl(self, listCtrl, title=None): - """ - Add the given list to those that will be printed by this report. - """ - if listCtrl.InReportView(): - self.listCtrls.append([listCtrl, title]) - - - def ClearListCtrls(self): - """ - Remove all ListCtrls from this report. - """ - self.listCtrls = list() - - - def DropCurrentBlock(self): - """ - Remove the current block from our list of blocks - """ - self.blocks.pop(0) - self.blockInsertionIndex = 1 - - #---------------------------------------------------------------------------- - # Printing - - def StartPrinting(self): - """ - Initial a print job on this engine - """ - self.currentPage = 0 - self.blockInsertionIndex = 0 - self.blocks = list() - self.AddBlock(ReportBlock()) - self.runningBlocks = list() - self.AddRunningBlock(PageHeaderBlock()) - self.AddRunningBlock(PageFooterBlock()) - - if self.watermark: - self._CreateReplaceWatermarkDecoration() - - - def AddRunningBlock(self, block): - """ - A running block is printed on every page until it is removed - """ - self.runningBlocks.append(block) - block.engine = self - - - def RemoveRunningBlock(self, block): - """ - A running block is printed on every page until it is removed - """ - self.runningBlocks.remove(block) - - - def PrintPage(self, dc, pageNumber, bounds): - """ - Print the given page on the given device context. - """ - # If the request page isn't next in order, we have to restart - # the printing process and advance until we reach the desired page. - if pageNumber != self.currentPage + 1: - self.StartPrinting() - self.shouldDrawBlocks = False - for i in range(1, pageNumber): - self.PrintOnePage(dc, i, bounds) - self.shouldDrawBlocks = True - - return self.PrintOnePage(dc, pageNumber, bounds) - - - def PrintOnePage(self, dc, pageNumber, bounds): - """ - Print the current page on the given device context. - - Return true if there is still more to print. - """ - # Initialize state - self.currentPage = pageNumber - self.pageBounds = list(bounds) - self.workBounds = list(self.pageBounds) - self.SubtractDecorations(dc) - - # Print page adornments, including under-text decorations - self.DrawPageDecorations(dc, False) - for x in self.runningBlocks: - x.Print(dc) - - # Print blocks until they won't fit or we run out of blocks - while len(self.blocks) and self.blocks[0].Print(dc): - self.DropCurrentBlock() - - # Finally, print over-the-text decorations - self.DrawPageDecorations(dc, True) - - return len(self.blocks) > 0 - - - def SubtractDecorations(self, dc): - """ - # Subtract the area used from the work area - """ - fmt = self.GetNamedFormat("Page") - self.workBounds = fmt.SubtractDecorations(dc, self.workBounds) - - - def DrawPageDecorations(self, dc, over): - """ - Draw the page decorations - """ - if not self.shouldDrawBlocks: - return - - fmt = self.GetNamedFormat("Page") - bounds = list(self.pageBounds) - fmt.DrawDecorations(dc, bounds, self, over) - - - def _CreateReplaceWatermarkDecoration(self): - """ - Create a watermark decoration, replacing any existing watermark - """ - pageFmt = self.GetNamedFormat("Page") - pageFmt.decorations = [x for x in pageFmt.decorations if not isinstance(x, WatermarkDecoration)] - - watermarkFmt = self.GetNamedFormat("Watermark") - pageFmt.Add(WatermarkDecoration(self.watermark, font=watermarkFmt.Font, - color=watermarkFmt.TextColor, angle=watermarkFmt.Angle, - over=watermarkFmt.Over)) - -#---------------------------------------------------------------------------- - -class ListCtrlPrintout(wx.Printout): - """ - An ListCtrlPrintout is the interface between the wx printing system - and ListCtrlPrinter. - """ - - def __init__(self, olvPrinter, margins=None): - """ - """ - wx.Printout.__init__(self) - self.olvPrinter = olvPrinter - self.margins = margins or (wx.Point(15, 15), wx.Point(15, 15)) - self.totalPages = -1 - - self.printData = wx.PrintData() - self.printData.SetPrinterName("") # Use default printer - self.printData.SetPaperId(wx.PAPER_A4) - self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) - - - #---------------------------------------------------------------------------- - # Accessing - - def HasPage(self, page): - """ - Return true if this printout has the given page number - """ - return page <= self.totalPages - - - def GetPageInfo(self): - """ - Return a 4-tuple indicating the ... - """ - return (1, self.totalPages, 1, 1) - - - def GetPrintPreview(self): - """ - Get a wxPrintPreview of this report - """ - data = wx.PrintDialogData(self.printData) - forViewing = ListCtrlPrintout(self.olvPrinter, self.margins) - forPrinter = ListCtrlPrintout(self.olvPrinter, self.margins) - preview = wx.PrintPreview(forViewing, forPrinter, data) - return preview - - #---------------------------------------------------------------------------- - # Commands - - def PageSetup(self, parent): - """ - Show a Page Setup dialog that will change the configuration of this printout - """ - data = wx.PageSetupDialogData() - data.SetPrintData(self.printData) - data.SetDefaultMinMargins(True) - data.SetMarginTopLeft(self.margins[0]) - data.SetMarginBottomRight(self.margins[1]) - dlg = wx.PageSetupDialog(parent, data) - if dlg.ShowModal() == wx.ID_OK: - data = dlg.GetPageSetupData() - self.printData = wx.PrintData(data.GetPrintData()) - self.printData.SetPaperId(data.GetPaperId()) - self.margins = (data.GetMarginTopLeft(), data.GetMarginBottomRight()) - dlg.Destroy() - - - - def PrintPreview(self, parent, title, bounds): - """ - Show a Print Preview of this report - """ - self.preview = self.GetPrintPreview() - - if not self.preview.Ok(): - return False - - pfrm = wx.PreviewFrame(self.preview, parent, title) - - pfrm.Initialize() - pfrm.SetPosition(bounds[0:2]) - pfrm.SetSize(bounds[2:4]) - pfrm.Show(True) - - return True - - - def DoPrint(self, parent): - """ - Send the report to the configured printer - """ - try: - pdd = wx.PrintDialogData(self.printData) - printer = wx.Printer(pdd) - - if printer.Print(parent, self, True): - self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) - else: - wx.MessageBox("There was a problem printing.\nPerhaps your current printer is not set correctly?", "Printing", wx.OK) - finally: - pdd.Destroy() - - - #---------------------------------------------------------------------------- - # Event handlers - - def OnPreparePrinting(self): - """ - Prepare for printing. This event is sent before any of the others - """ - dc = self.GetDC() - self.SetScaleAndBounds(dc) - self.totalPages = self.olvPrinter.CalculateTotalPages(dc, self.bounds) - self.olvPrinter.StartPrinting() - - def OnBeginDocument(self, start, end): - """ - Begin printing one copy of the document. Return False to cancel the job - """ - return super(ListCtrlPrintout, self).OnBeginDocument(start, end) - - def OnEndDocument(self): - super(ListCtrlPrintout, self).OnEndDocument() - - def OnBeginPrinting(self): - super(ListCtrlPrintout, self).OnBeginPrinting() - - def OnEndPrinting(self): - super(ListCtrlPrintout, self).OnEndPrinting() - - def OnPrintPage(self, page): - """ - Do the work of printing the given page number. - """ - # We bounce this back to the printer facade - dc = self.GetDC() - self.SetScaleAndBounds(dc) - return self.olvPrinter.PrintPage(dc, page, self.bounds) - - def SetScaleAndBounds(self, dc): - """ - Calculate the scale required for our printout to match what appears on screen, - and the bounds that will be effective at that scale and margins - """ - # This code comes from Robin Dunn's "wxPython In Action." - # Without that, I would never have figured this out. - ppiPrinterX, ppiPrinterY = self.GetPPIPrinter() - ppiScreenX, ppiScreenY = self.GetPPIScreen() - logicalScale = float(ppiPrinterX) / float(ppiScreenX) - pw, ph = self.GetPageSizePixels() - dw, dh = dc.GetSize() - scale = logicalScale * float(dw)/float(pw) - dc.SetUserScale(scale, scale) - - # Now calculate our boundries - logicalUnitsMM = float(ppiPrinterX) / (logicalScale*25.4) - topLeft, bottomRight = self.margins - left = round(topLeft.x * logicalUnitsMM) - top = round(topLeft.y * logicalUnitsMM) - right = round(dc.DeviceToLogicalYRel(dw) - bottomRight.x * logicalUnitsMM) - bottom = round(dc.DeviceToLogicalYRel(dh) - bottomRight.y * logicalUnitsMM) - self.bounds = (left, top, right-left, bottom-top) - - -#---------------------------------------------------------------------------- - -class ReportFormat(object): - """ - A ReportFormat defines completely how a report is formatted. - - It holds a collection of BlockFormat objects which control the - formatting of individual blocks of the report - - Public instance variables: - - * IncludeImages - Should images from the ListCtrl be printed in the report? - Default: *True* - - * IsColumnHeadingsOnEachPage - Will the column headers be printed at the top of each page? - Default: *True* - - * IsShrinkToFit - Will the columns be shrunk so they all fit into the width of one page? - Default: *False* - - * UseListCtrlTextFormat - If this is True, the text format (i.e. font and text color) of each row will be taken from the ListCtrl, - rather than from the *Cell* format. - Default: *False* - - """ - - def __init__(self): - """ - """ - # Initialize the formats that control the various portions of the report - self.Page = BlockFormat() - self.PageHeader = BlockFormat() - self.ListHeader = BlockFormat() - self.GroupTitle = BlockFormat() - self.ColumnHeader = BlockFormat() - self.Row = BlockFormat() - self.ListFooter = BlockFormat() - self.PageFooter = BlockFormat() - self.Watermark = BlockFormat() - - self.IncludeImages = True - self.IsColumnHeadingsOnEachPage = False - self.IsShrinkToFit = False - self.UseListCtrlTextFormat = True - - # Initialize the watermark format to default values - self.WatermarkFormat() - - #---------------------------------------------------------------------------- - # Accessing - - def GetNamedFormat(self, name): - """ - Return the format used in to format a block with the given name. - """ - return getattr(self, name) - - #---------------------------------------------------------------------------- - # Commands - - def WatermarkFormat(self, font=None, color=None, angle=30, over=False): - """ - Change the format of the watermark printed on this report. - - The actual text of the water mark is set by `ListCtrlPrinter.Watermark` property. - """ - defaultFaceName = "Stencil" - self.Watermark.Font = font or wx.FFont(96, wx.FONTFAMILY_DEFAULT, 0, defaultFaceName) - self.Watermark.TextColor = color or wx.Colour(204, 204, 204) - self.Watermark.Angle = angle - self.Watermark.Over = over - - #---------------------------------------------------------------------------- - # Standard formats - # These are meant to be illustrative rather than definitive - - @staticmethod - def Minimal(headerFontName="Arial", rowFontName="Times New Roman"): - """ - Return a minimal format for a report - """ - fmt = ReportFormat() - fmt.IsShrinkToFit = False - - fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.PageHeader.Line(wx.BOTTOM, wx.BLACK, 1, space=5) - fmt.PageHeader.Padding = (0, 0, 0, 12) - - fmt.ListHeader.Font = wx.FFont(18, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.ListHeader.Padding = (0, 12, 0, 12) - fmt.ListHeader.Line(wx.BOTTOM, wx.BLACK, 1, space=5) - - fmt.GroupTitle.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.GroupTitle.Padding = (0, 12, 0, 12) - fmt.GroupTitle.Line(wx.BOTTOM, wx.BLACK, 1, space=5) - - fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.PageFooter.Line(wx.TOP, wx.BLACK, 1, space=3) - fmt.PageFooter.Padding = (0, 16, 0, 0) - - fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.ColumnHeader.Padding = (0, 12, 0, 12) - fmt.ColumnHeader.CellPadding = 5 - fmt.ColumnHeader.Line(wx.BOTTOM, wx.Colour(192, 192, 192), 1, space=3) - fmt.ColumnHeader.AlwaysCenter = True - - fmt.Row.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=rowFontName) - fmt.Row.CellPadding = 5 - fmt.Row.Line(wx.BOTTOM, wx.Colour(192, 192, 192), 1, space=3) - fmt.Row.CanWrap = True - - return fmt - - @staticmethod - def Normal(headerFontName="Gill Sans", rowFontName="Times New Roman"): - """ - Return a reasonable default format for a report - """ - fmt = ReportFormat() - fmt.IsShrinkToFit = True - - fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.PageHeader.Line(wx.BOTTOM, wx.BLUE, 2, space=5) - fmt.PageHeader.Padding = (0, 0, 0, 12) - - fmt.ListHeader.Font = wx.FFont(26, wx.FONTFAMILY_SWISS, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.ListHeader.TextColor = wx.WHITE - fmt.ListHeader.Padding = (0, 12, 0, 12) - fmt.ListHeader.TextAlignment = wx.ALIGN_LEFT - fmt.ListHeader.Background(wx.BLUE, wx.WHITE, space=(16, 4, 0, 4)) - - fmt.GroupTitle.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.GroupTitle.Line(wx.BOTTOM, wx.BLUE, 4, toColor=wx.WHITE, space=5) - fmt.GroupTitle.Padding = (0, 12, 0, 12) - - fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=headerFontName) - fmt.PageFooter.Background(wx.WHITE, wx.BLUE, space=(0, 4, 0, 4)) - - fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.ColumnHeader.CellPadding = 2 - fmt.ColumnHeader.Background(wx.Colour(192, 192, 192)) - fmt.ColumnHeader.GridPen = wx.Pen(wx.WHITE, 1) - fmt.ColumnHeader.Padding = (0, 0, 0, 12) - fmt.ColumnHeader.AlwaysCenter = True - - fmt.Row.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=rowFontName) - fmt.Row.Line(wx.BOTTOM, pen=wx.Pen(wx.BLUE, 1, wx.DOT), space=3) - fmt.Row.CellPadding = 2 - fmt.Row.CanWrap = True - - return fmt - - @staticmethod - def TooMuch(headerFontName="Chiller", rowFontName="Gill Sans"): - """ - Return a reasonable default format for a report - """ - fmt = ReportFormat() - fmt.IsShrinkToFit = False - - fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DECORATIVE, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.PageHeader.TextColor = wx.WHITE - fmt.PageHeader.Background(wx.GREEN, wx.RED, space=(16, 4, 0, 4)) - fmt.PageHeader.Padding = (0, 0, 0, 12) - - fmt.ListHeader.Font = wx.FFont(24, wx.FONTFAMILY_DECORATIVE, face=headerFontName) - fmt.ListHeader.TextColor = wx.WHITE - fmt.ListHeader.Padding = (0, 12, 0, 12) - fmt.ListHeader.TextAlignment = wx.ALIGN_CENTER - fmt.ListHeader.Background(wx.RED, wx.GREEN, space=(16, 4, 0, 4)) - - fmt.GroupTitle.Font = wx.FFont(14, wx.FONTFAMILY_DECORATIVE, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.GroupTitle.TextColor = wx.BLUE - fmt.GroupTitle.Padding = (0, 12, 0, 12) - fmt.GroupTitle.Line(wx.BOTTOM, wx.GREEN, 4, toColor=wx.WHITE, space=5) - - fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DECORATIVE, face=headerFontName) - fmt.PageFooter.Line(wx.TOP, wx.GREEN, 2, toColor=wx.RED, space=3) - fmt.PageFooter.Padding = (0, 16, 0, 0) - - fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_SWISS, wx.FONTFLAG_BOLD, face=headerFontName) - fmt.ColumnHeader.Background(wx.Colour(255, 215, 0)) - fmt.ColumnHeader.CellPadding = 5 - fmt.ColumnHeader.GridPen = wx.Pen(wx.Colour(192, 192, 192), 1) - - fmt.Row.Font = wx.FFont(12, wx.FONTFAMILY_SWISS, face=rowFontName) - fmt.Row.CellPadding = 5 - fmt.Row.GridPen = wx.Pen(wx.BLUE, 1, wx.DOT) - fmt.Row.CanWrap = True - - fmt.Watermark.TextColor = wx.Colour(233, 150, 122) - - return fmt - -#---------------------------------------------------------------------------- - -class BlockFormat(object): - """ - A block format defines how a Block is formatted. - - These properties control the formatting of the matching Block: - - * CanWrap - If the text for this block cannot fit horizontally, should be wrap to a new line (True) - or should it be truncated (False)? - * Font - What font should be used to draw the text of this block - * Padding - How much padding should be applied to the block before the text or other decorations - are drawn? This can be a numeric (which will be applied to all sides) or it can be - a collection of the paddings to be applied to the various sides: (left, top, right, bottom). - * TextAlignment - How should text be aligned within this block? Can be wx.ALIGN_LEFT, wx.ALIGN_CENTER, or - wx.ALIGN_RIGHT. - * TextColor - In what color should be text be drawn? - - The blocks that are based on cells (PageHeader, ColumnHeader, Row, PageFooter) can also - have the following properties set: - - * AlwaysCenter - Will the text in the cells be center aligned, regardless of other settings? - * CellPadding - How much padding should be applied to this cell before the text or other decorations - are drawn? This can be a numeric (which will be applied to all sides) or it can be a - collection of the paddings to be applied to the various sides: (left, top, right, - bottom). - * GridPen - What Pen will be used to draw the grid lines of the cells? - - In addition to these properties, there are some methods which add various decorations to - the blocks: - - * Background(color=wx.BLUE, toColor=None, space=0) - - This gives the block a solid color background (or a gradient background if *toColor* - is not None). If *space* is not 0, *space* pixels will be subtracted from all sides - from the space available to the block. - - * Frame(pen=None, space=0) - - Draw a rectangle around the block in the given pen - - * Line(side=wx.BOTTOM, color=wx.BLACK, width=1, toColor=None, space=0, pen=None) - - Draw a line on a given side of the block. If a pen is given, that is used to draw the - line (and the other parameters are ignored), otherwise a solid line (or a gradient - line is *toColor* is not None) of *width* pixels is drawn. - - """ - - def __init__(self): - """ - """ - self.padding = None - self.decorations = list() - self.font = wx.FFont(11, wx.FONTFAMILY_SWISS, face="Gill Sans") - self.textColor = None - self.textAlignment = wx.ALIGN_LEFT - self.alwaysCenter = False - self.canWrap = False - - #THINK: These attributes are only for grids. Should we have a GridBlockFormat object? - self.cellPadding = None - self.gridPen = None - - #---------------------------------------------------------------------------- - # Accessing - - def GetFont(self): - """ - Return the font used by this format - """ - return self.font - - def SetFont(self, font): - """ - Set the font used by this format - """ - self.font = font - - def GetTextAlignment(self): - """ - Return the alignment of text in this format - """ - return self.textAlignment - - def SetTextAlignment(self, alignment): - """ - Set the alignment of text in this format - """ - self.textAlignment = alignment - - def GetTextColor(self): - """ - Return the color of text in this format - """ - return self.textColor - - def SetTextColor(self, color): - """ - Set the color of text in this format - """ - self.textColor = color - - def GetPadding(self): - """ - Get the padding around this format - """ - return self.padding - - def SetPadding(self, padding): - """ - Set the padding around this format - - Padding is either a single numeric (indicating the values on all sides) - or a collection of paddings [left, top, right, bottom] - """ - self.padding = self._MakePadding(padding) - - def GetCellPadding(self): - """ - Get the padding around cells in this format - """ - return self.cellPadding - - def SetCellPadding(self, padding): - """ - Set the padding around cells in this format - - Padding is either a single numeric (indicating the values on all sides) - or a collection of paddings [left, top, right, bottom] - """ - self.cellPadding = self._MakePadding(padding) - - def GetGridPen(self): - """ - Return the pen used to draw a grid in this format - """ - return self.gridPen - - def SetGridPen(self, pen): - """ - Set the pen used to draw a grid in this format - """ - self.gridPen = pen - if self.gridPen: - # Other styles don't produce nice joins - self.gridPen.SetCap(wx.CAP_BUTT) - self.gridPen.SetJoin(wx.JOIN_MITER) - - def _MakePadding(self, padding): - try: - if len(padding) < 4: - return (tuple(padding) + (0, 0, 0, 0))[:4] - else: - return padding - except TypeError: - return (padding,) * 4 - - def GetAlwaysCenter(self): - """ - Return if the text controlled by this format should always be centered? - """ - return self.alwaysCenter - - def SetAlwaysCenter(self, value): - """ - Remember if the text controlled by this format should always be centered? - """ - self.alwaysCenter = value - - def GetCanWrap(self): - """ - Return if the text controlled by this format can wrap to cover more than one line? - """ - return self.canWrap - - def SetCanWrap(self, value): - """ - Remember if the text controlled by this format can wrap to cover more than one line? - """ - self.canWrap = value - - Font = property(GetFont, SetFont) - Padding = property(GetPadding, SetPadding) - TextAlignment = property(GetTextAlignment, SetTextAlignment) - TextColor = property(GetTextColor, SetTextColor) - CellPadding = property(GetCellPadding, SetCellPadding) - GridPen = property(GetGridPen, SetGridPen) - AlwaysCenter = property(GetAlwaysCenter, SetAlwaysCenter) - CanWrap = property(GetCanWrap, SetCanWrap) - - # Misspellers of the world Untie! - # Ok, ok... there're not really misspellings - just alternatives :) - TextAlign = property(GetTextAlignment, SetTextAlignment) - TextColour = property(GetTextColor, SetTextColor) - - #---------------------------------------------------------------------------- - # Calculations - - def CalculateCellPadding(self): - """ - Return a four-tuple that indicates pixel padding (left, top, right, bottom) - around cells in this format - """ - if self.CellPadding: - cellPadding = list(self.CellPadding) - else: - cellPadding = 0, 0, 0, 0 - - if self.GridPen: - penFactor = int((self.GridPen.GetWidth()+1)/2) - cellPadding = [x + penFactor for x in cellPadding] - - return cellPadding - - #---------------------------------------------------------------------------- - # Decorations - - def Add(self, decoration): - """ - Add the given decoration to those adorning blocks with this format - """ - self.decorations.append(decoration) - - - def Line(self, side=wx.BOTTOM, color=wx.BLACK, width=1, toColor=None, space=0, pen=None): - """ - Add a line to our decorations. - If a pen is given, we use a straight Line decoration, otherwise we use a - coloured rectangle - """ - if pen: - self.Add(LineDecoration(side, pen, space)) - else: - self.Add(RectangleDecoration(side, None, color, toColor, width, space)) - - - def Background(self, color=wx.BLUE, toColor=None, space=0): - """ - Add a coloured background to the block - """ - self.Add(RectangleDecoration(color=color, toColor=toColor, space=space)) - - - def Frame(self, pen=None, space=0): - """ - Add a rectangle around this block - """ - self.Add(RectangleDecoration(pen=pen, space=space)) - - #---------------------------------------------------------------------------- - # Commands - - def SubtractPadding(self, bounds): - """ - Subtract any padding used by this format from the given bounds - """ - if self.padding is None: - return bounds - else: - return RectUtils.InsetRect(bounds, self.padding) - - - def SubtractDecorations(self, dc, bounds): - """ - Subtract any space used by our decorations from the given bounds - """ - for x in self.decorations: - bounds = x.SubtractFrom(dc, bounds) - return bounds - - - def DrawDecorations(self, dc, bounds, block, over): - """ - Draw our decorations on the given block - """ - for x in self.decorations: - if x.IsDrawOver() == over: - x.DrawDecoration(dc, bounds, block) - - -#---------------------------------------------------------------------------- - -class Block(object): - """ - A Block is a portion of a report that will stack vertically with other - Block. A report consists of several Blocks. - """ - - def __init__(self, engine=None): - self.engine = engine # This is also set when the block is added to a print engine - - #---------------------------------------------------------------------------- - # Accessing - - def GetFont(self): - """ - Return Font that should be used to draw text in this block - """ - return self.GetFormat().GetFont() - - - def GetTextColor(self): - """ - Return Colour that should be used to draw text in this block - """ - return self.GetFormat().GetTextColor() - - - def GetFormat(self): - """ - Return the BlockFormat object that controls the formatting of this block - """ - return self.engine.GetNamedFormat(self.__class__.__name__[:-5]) - - - def GetReducedBlockBounds(self, dc, bounds=None): - """ - Return the bounds of this block once padding and decoration have taken their toll. - """ - bounds = bounds or list(self.GetWorkBounds()) - fmt = self.GetFormat() - bounds = fmt.SubtractPadding(bounds) - bounds = fmt.SubtractDecorations(dc, bounds) - return bounds - - - def GetWorkBounds(self): - """ - Return the boundaries of the work area for this block - """ - return self.engine.workBounds - - - def IsColumnHeadingsOnEachPage(self): - """ - Should the column headers be report at the top of each new page? - """ - return self.engine.reportFormat.IsColumnHeadingsOnEachPage - - - def IncludeImages(self): - """ - Should the images from the ListCtrl be printed in the report? - """ - return self.engine.reportFormat.IncludeImages - - - def IsShrinkToFit(self): - """ - Should row blocks be shrunk to fit within the width of a page? - """ - return self.engine.reportFormat.IsShrinkToFit - - - def IsUseSubstitution(self): - """ - Should the text values printed by this block have substitutions performed before being printed? - - This allows, for example, footers to have '%(currentPage)d of %(totalPages)d' - """ - return True - - - #---------------------------------------------------------------------------- - # Calculating - - def CalculateExtrasHeight(self, dc): - """ - Return the height of the padding and decorations themselves - """ - return 0 - RectUtils.Height(self.GetReducedBlockBounds(dc, [0, 0, 0, 0])) - - - def CalculateExtrasWidth(self, dc): - """ - Return the width of the padding and decorations themselves - """ - return 0 - RectUtils.Width(self.GetReducedBlockBounds(dc, [0, 0, 0, 0])) - - - def CalculateHeight(self, dc): - """ - Return the heights of this block in pixels - """ - return -1 - - - def CalculateTextHeight(self, dc, txt, bounds=None, font=None): - """ - Return the height of the given txt in pixels - """ - bounds = bounds or self.GetReducedBlockBounds(dc) - font = font or self.GetFont() - dc.SetFont(font) - if self.GetFormat().CanWrap: - return WordWrapRenderer.CalculateHeight(dc, txt, RectUtils.Width(bounds)) - else: - # Calculate the height of one line. The 1000 pixel width - # ensures that 'Wy' doesn't wrap, which might happen if bounds is narrow - return WordWrapRenderer.CalculateHeight(dc, "Wy", 1000) - - - def CanFit(self, height): - """ - Can this block fit into the remaining work area on the page? - """ - return height <= RectUtils.Height(self.GetWorkBounds()) - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - if not self.ShouldPrint(): - return True - - bounds = self.CalculateBounds(dc) - if not self.CanFit(RectUtils.Height(bounds)): - return False - - if self.engine.shouldDrawBlocks: - self.PreDraw(dc, bounds) - self.Draw(dc, bounds) - self.PostDraw(dc, bounds) - - self.ChangeWorkBoundsBy(RectUtils.Height(bounds)) - return True - - - def ShouldPrint(self): - """ - Should this block be printed? - """ - # If this block does not have a format, it is simply skipped - return self.GetFormat() is not None - - - def CalculateBounds(self, dc): - """ - Calculate the bounds of this block - """ - height = self.CalculateHeight(dc) - bounds = list(self.GetWorkBounds()) - bounds = RectUtils.SetHeight(bounds, height) - return bounds - - - def ChangeWorkBoundsBy(self, amt): - """ - Move the top of our work bounds down by the given amount - """ - RectUtils.MoveTopBy(self.engine.workBounds, amt) - - - def Draw(self, dc, bounds): - """ - Draw this block and its decorations allowing for any padding - """ - fmt = self.GetFormat() - decorationBounds = fmt.SubtractPadding(bounds) - fmt.DrawDecorations(dc, decorationBounds, self, False) - textBounds = fmt.SubtractDecorations(dc, list(decorationBounds)) - self.DrawSelf(dc, textBounds) - fmt.DrawDecorations(dc, decorationBounds, self, True) - - - def PreDraw(self, dc, bounds): - """ - Executed before any drawing is done - """ - pass - - - def DrawSelf(self, dc, bounds): - """ - Do the actual work of rendering this block. - """ - pass - - - def PostDraw(self, dc, bounds): - """ - Executed after drawing has completed - """ - pass - - - def DrawText(self, dc, txt, bounds, font=None, alignment=wx.ALIGN_LEFT, valignment=wx.ALIGN_CENTRE, - image=None, color=None, canWrap=True, imageIndex=-1, listCtrl=None): - """ - Draw the given text in the given DC according to the given characteristics. - - This is the workhorse text drawing method for our reporting engine. - - The *font*, *alignment*, and *color* attributes are applied to the drawn text. - - If *image* is not None, it will be drawn to the left of the text. All text is indented - by the width of the image, even if the text is multi-line. - - If *imageIndex* is 0 or more and *listCtrl* is not None, the corresponding image from - the ListCtrl's small image list will be drawn to the left of the text. - - If *canWrap* is True, the text will be wrapped to fit within the given bounds. If it is False, - then the first line of *txt* will be truncated at the edge of the given *bounds*. - """ - GAP_BETWEEN_IMAGE_AND_TEXT = 4 - - def _CalcBitmapPosition(r, height): - if valignment == wx.ALIGN_TOP: - return RectUtils.Top(r) - elif valignment == wx.ALIGN_CENTER: - return RectUtils.CenterY(r) - height / 2 - elif valignment == wx.ALIGN_BOTTOM: - return RectUtils.Bottom(r) - height - else: - return RectUtils.Top(r) - - # Draw any image - if image: - y = _CalcBitmapPosition(bounds, image.Height) - dc.DrawBitmap(image, RectUtils.Left(bounds), y) - RectUtils.MoveLeftBy(bounds, image.GetWidth()+GAP_BETWEEN_IMAGE_AND_TEXT) - elif listCtrl and imageIndex >=0: - imageList = listCtrl.GetImageList(wx.IMAGE_LIST_SMALL) - y = _CalcBitmapPosition(bounds, imageList.GetSize(0)[1]) - imageList.Draw(imageIndex, dc, RectUtils.Left(bounds), y, wx.IMAGELIST_DRAW_TRANSPARENT) - RectUtils.MoveLeftBy(bounds, imageList.GetSize(0)[0]+GAP_BETWEEN_IMAGE_AND_TEXT) - - # Draw the text - dc.SetFont(font or self.GetFont()) - dc.SetTextForeground(color or self.GetTextColor() or wx.BLACK) - if canWrap: - WordWrapRenderer.DrawString(dc, txt, bounds, alignment, valignment) - else: - WordWrapRenderer.DrawTruncatedString(dc, txt, bounds, alignment, valignment) - - - def PerformSubstitutions(self, strings): - """ - Substituted % markers in the given collection of strings. - """ - info = self.engine.GetSubstitutionInfo() - try: - # 'strings' could be a single string or a list of strings - try: - return strings % info - except TypeError: - return [x % info for x in strings] - except ValueError: - # We don't die if we get a substitution error - we just ignore it - return strings - -#---------------------------------------------------------------------------- - -class TextBlock(Block): - """ - A TextBlock prints a string objects. - """ - - def ShouldPrint(self): - """ - Should this block be printed? - """ - # If the block has no text, it should not be printed - if self.GetText(): - return Block.ShouldPrint(self) - else: - return False - - - def GetText(self): - return "Missing GetText() in class %s" % self.__class__.__name__ - - - def GetSubstitutedText(self): - """ - Return the text for this block after all markers have been substituted - """ - if self.IsUseSubstitution(): - return self.PerformSubstitutions(self.GetText()) - else: - return self.GetText() - - - def CalculateHeight(self, dc): - """ - Return the heights of this block in pixels - """ - textHeight = self.CalculateTextHeight(dc, self.GetSubstitutedText()) - extras = self.CalculateExtrasHeight(dc) - return textHeight + extras - - - def DrawSelf(self, dc, bounds): - """ - Do the actual work of rendering this block. - """ - fmt = self.GetFormat() - self.DrawText(dc, self.GetSubstitutedText(), bounds, canWrap=fmt.CanWrap, alignment=fmt.TextAlignment) - - -#---------------------------------------------------------------------------- - -class CellBlock(Block): - """ - A CellBlock is a Block whose data is presented in a cell format. - """ - - def __init__(self): - self.scale = 1 - self.oldScale = 1 - - #---------------------------------------------------------------------------- - # Accessing - Subclasses should override - - def GetCellWidths(self): - """ - Return a list of the widths of the cells in this block. - """ - return list() - - def GetTexts(self): - """ - Return a list of the texts that should be drawn with the cells - """ - return list() - - def GetAlignments(self): - """ - Return a list indicating how the text within each cell is aligned. - """ - return list() - - def GetImages(self): - """ - Return a list of the images that should be drawn in each cell - """ - return list() - - - #---------------------------------------------------------------------------- - # Accessing - - - def GetCombinedLists(self): - """ - Return a collection of Buckets that hold all the values of the - subclass-overridable collections above - """ - buckets = [Bucket(cellWidth=x, text="", align=None, image=None) for x in self.GetCellWidths()] - for (i, x) in enumerate(self.GetSubstitutedTexts()): - buckets[i].text = x - for (i, x) in enumerate(self.GetAlignments()): - buckets[i].align = x - - if self.IncludeImages(): - for (i, x) in enumerate(self.GetImages()): - buckets[i].image = x - - # Calculate where the cell contents should be drawn - cellPadding = self.GetFormat().CalculateCellPadding() - for x in buckets: - x.innerCellWidth = max(0, x.cellWidth - (cellPadding[0] + cellPadding[2])) - - return buckets - - - def GetListCtrl(self): - """ - Return the ListCtrl that is behind this cell block. - """ - return None - - - def GetSubstitutedTexts(self): - """ - Return a list of the texts that should be drawn with the cells - """ - if self.IsUseSubstitution(): - return self.PerformSubstitutions(self.GetTexts()) - else: - return self.GetTexts() - - - #---------------------------------------------------------------------------- - # Calculating - - def CalculateHeight(self, dc): - """ - Return the heights of this block in pixels - """ - GAP_BETWEEN_IMAGE_AND_TEXT = 4 - - # If cells can wrap, figure out the tallest, otherwise we just figure out the height of one line - if self.GetFormat().CanWrap: - font = self.GetFont() - height = 0 - for x in self.GetCombinedLists(): - textWidth = x.innerCellWidth - if self.GetListCtrl() and x.image != -1: - imageList = self.GetListCtrl().GetImageList(wx.IMAGE_LIST_SMALL) - textWidth -= imageList.GetSize(0)[0]+GAP_BETWEEN_IMAGE_AND_TEXT - bounds = [0, 0, textWidth, 99999] - height = max(height, self.CalculateTextHeight(dc, x.text, bounds, font)) - else: - height = self.CalculateTextHeight(dc, "Wy") - - # We also have to allow for cell padding, on top of the normal padding and decorations - cellPadding = self.GetFormat().CalculateCellPadding() - return height + cellPadding[1] + cellPadding[3] + self.CalculateExtrasHeight(dc) - - - def CalculateWidth(self, dc): - """ - Calculate the total width of this block (cells plus padding) - """ - return sum(x.cellWidth for x in self.GetCombinedLists()) + self.CalculateExtrasWidth(dc) - - #---------------------------------------------------------------------------- - # Commands - - def CalculateBounds(self, dc): - """ - Calculate the bounds of this block - """ - height = self.CalculateHeight(dc) - bounds = list(self.GetWorkBounds()) - bounds = RectUtils.SetHeight(bounds, height) - bounds = RectUtils.SetWidth(bounds, self.CalculateWidth(dc)) - bounds = RectUtils.MultiplyOrigin(bounds, 1 / self.scale) - return bounds - - #def CanFit(self, height): - # height *= self.scale - # return Block.CanFit(self, height) - - def ChangeWorkBoundsBy(self, height): - """ - Change our workbounds by our scaled height - """ - Block.ChangeWorkBoundsBy(self, height * self.scale) - - - def PreDraw(self, dc, bounds): - """ - Apply our scale before performing any drawing - """ - self.oldScale = dc.GetUserScale() - dc.SetUserScale(self.scale * self.oldScale[0], self.scale * self.oldScale[1]) - - - def PostDraw(self, dc, bounds): - """ - Restore the scaling to what it used to be - """ - dc.SetUserScale(*self.oldScale) - - - def DrawSelf(self, dc, bounds): - """ - Do the actual work of rendering this block. - """ - cellFmt = self.GetFormat() - cellPadding = cellFmt.CalculateCellPadding() - combined = self.GetCombinedLists() - - # Calculate cell boundaries - cell = list(bounds) - RectUtils.SetWidth(cell, 0) - for x in combined: - RectUtils.SetLeft(cell, RectUtils.Right(cell)) - RectUtils.SetWidth(cell, x.cellWidth) - x.cell = list(cell) - - # Draw each cell - font = self.GetFont() - for x in combined: - cellBounds = RectUtils.InsetRect(x.cell, cellPadding) - self.DrawText(dc, x.text, cellBounds, font, x.align, imageIndex=x.image, - canWrap=cellFmt.CanWrap, listCtrl=self.GetListCtrl()) - - if cellFmt.GridPen and combined: - dc.SetPen(cellFmt.GridPen) - dc.SetBrush(wx.TRANSPARENT_BRUSH) - - top = RectUtils.Top(bounds) - bottom = RectUtils.Bottom(bounds) - - # Draw the interior dividers - for x in combined[:-1]: - right = RectUtils.Right(x.cell) - dc.DrawLine(right, top, right, bottom) - - # Draw the surrounding frame - left = RectUtils.Left(combined[0].cell) - right = RectUtils.Right(combined[-1].cell) - dc.DrawRectangle(left, top, right-left, bottom-top) - - -#---------------------------------------------------------------------------- - -class ThreeCellBlock(CellBlock): - """ - A ThreeCellBlock divides its space evenly into three cells. - """ - - def GetCellWidths(self): - """ - Return a list of the widths of the cells in this block - """ - # We divide the available space between the non-empty cells - numNonEmptyTexts = sum(1 for x in self.GetTexts() if x) - if not numNonEmptyTexts: - return (0, 0, 0) - - widths = list() - width = round(RectUtils.Width(self.GetWorkBounds()) / numNonEmptyTexts) - for x in self.GetTexts(): - if x: - widths.append(width) - else: - widths.append(0) - return widths - - - def GetAlignments(self): - """ - Return a list indicating how the text within each cell is aligned. - """ - return (wx.ALIGN_LEFT, wx.ALIGN_CENTER_HORIZONTAL, wx.ALIGN_RIGHT) - -#---------------------------------------------------------------------------- - -class ReportBlock(Block): - """ - A ReportBlock is boot strap Block that represents an entire report. - """ - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - if not self.engine.listCtrls: - return True - - # Print each ListView. Each list but the first starts on a separate page - self.engine.AddBlock(ListBlock(*self.engine.listCtrls[0])) - for (lv, title) in self.engine.listCtrls[1:]: - self.engine.AddBlock(PageBreakBlock()) - self.engine.AddBlock(ListBlock(lv, title)) - - return True - - -#---------------------------------------------------------------------------- - -class PageHeaderBlock(ThreeCellBlock): - """ - A PageHeaderBlock appears at the top of every page. - """ - - def GetTexts(self): - """ - Return the array of texts to be printed in the cells - """ - return self.engine.pageHeader - - - -#---------------------------------------------------------------------------- - -class PageFooterBlock(ThreeCellBlock): - """ - A PageFooterBlock appears at the bottom of every page. - """ - - def GetTexts(self): - """ - Return the array of texts to be printed in the cells - """ - return self.engine.pageFooter - - - #---------------------------------------------------------------------------- - # Printing - - - def CalculateBounds(self, dc): - """ - Calculate the bounds of this block - """ - # Draw the footer at the bottom of the page - height = self.CalculateHeight(dc) - bounds = list(self.GetWorkBounds()) - return [RectUtils.Left(bounds), RectUtils.Bottom(bounds) - height, - RectUtils.Width(bounds), height] - - - def ChangeWorkBoundsBy(self, height): - """ - The footer changes the bottom of the work bounds - """ - RectUtils.MoveBottomBy(self.engine.workBounds, -height) - - -#---------------------------------------------------------------------------- - -class PageBreakBlock(Block): - """ - A PageBreakBlock acts a page break. - """ - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - - # Completely fill the remaining area on the page, forcing a page break - bounds = self.GetWorkBounds() - self.ChangeWorkBoundsBy(RectUtils.Height(bounds)) - - return True - -#---------------------------------------------------------------------------- - -class RunningBlockPusher(Block): - """ - A RunningBlockPusher pushes (or pops) a running block onto the stack when it is executed. - """ - - def __init__(self, block, push=True): - """ - """ - self.block = block - self.push = push - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - if self.push: - self.engine.AddRunningBlock(self.block) - else: - self.engine.RemoveRunningBlock(self.block) - - return True - -#---------------------------------------------------------------------------- - -class ListBlock(Block): - """ - A ListBlock handles the printing of an entire ListCtrl. - """ - - def __init__(self, lv, title): - self.lv = lv - self.title = title - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - - cellWidths = self.CalculateCellWidths() - boundsWidth = RectUtils.Width(self.GetWorkBounds()) - - # Break the list into vertical slices. Each one but the first will be placed on a - # new page. - first = True - for (left, right) in self.CalculateSlices(boundsWidth, cellWidths): - if not first: - self.engine.AddBlock(PageBreakBlock()) - self.engine.AddBlock(ListHeaderBlock(self.lv, self.title)) - self.engine.AddBlock(ListSliceBlock(self.lv, left, right, cellWidths)) - self.engine.AddBlock(ListFooterBlock(self.lv, "")) - first = False - - return True - - - def CalculateCellWidths(self): - """ - Return a list of the widths of the cells in this lists - """ - columnHeaderFmt = self.engine.GetNamedFormat("ColumnHeader") - cellPadding = columnHeaderFmt.CalculateCellPadding() - padding = cellPadding[0] + cellPadding[2] - return [self.lv.GetColumnWidth(i) + padding for i in range(self.lv.GetColumnCount())] - - - def CalculateSlices(self, maxWidth, columnWidths): - """ - Return a list of integer pairs, where each pair represents - the left and right columns that can fit into the width of one page - """ - firstColumn = 0 - - # If a GroupListView has a column just for the expand/collapse, don't include it - if hasattr(self.lv, "useExpansionColumn") and self.lv.useExpansionColumn: - firstColumn = 1 - - # If we are shrinking to fit or all the columns fit, just return all columns - if self.IsShrinkToFit() or (sum(columnWidths)) <= maxWidth: - return [ [firstColumn, len(columnWidths)-1] ] - - pairs = list() - left = firstColumn - right = firstColumn - while right < len(columnWidths): - if (sum(columnWidths[left:right+1])) > maxWidth: - if left == right: - pairs.append([left, right]) - left += 1 - right += 1 - else: - pairs.append([left, right-1]) - left = right - else: - right += 1 - - if left < len(columnWidths): - pairs.append([left, right-1]) - - return pairs - - -#---------------------------------------------------------------------------- - -class ListHeaderBlock(TextBlock): - """ - A ListHeaderBlock is the title that comes before an ListCtrl. - """ - - def __init__(self, lv, title): - self.lv = lv - self.title = title - - def GetText(self): - """ - Return the text that will be printed in this block - """ - return self.title - -#---------------------------------------------------------------------------- - -class ListFooterBlock(TextBlock): - """ - A ListFooterBlock is the text that comes after an ListCtrl. - """ - - def __init__(self, lv, text): - self.lv = lv - self.text = text - - def GetText(self): - """ - Return the text that will be printed in this block - """ - return self.text - - -#---------------------------------------------------------------------------- - -class GroupTitleBlock(TextBlock): - """ - A GroupTitleBlock is the title that comes before a list group. - """ - - def __init__(self, lv, group): - self.lv = lv - self.group = group - - def GetText(self): - """ - Return the text that will be printed in this block - """ - return self.group.title - -#---------------------------------------------------------------------------- - -class ListSliceBlock(Block): - """ - A ListSliceBlock prints a vertical slice of an ListCtrl. - """ - - def __init__(self, lv, left, right, allCellWidths): - self.lv = lv - self.left = left - self.right = right - self.allCellWidths = allCellWidths - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - # If we are shrinking our cells, calculate the shrink factor - if self.IsShrinkToFit(): - scale = self.CalculateShrinkToFit(dc) - else: - scale = 1 - - headerBlock = ColumnHeaderBlock(self.lv, self.left, self.right, scale, self.allCellWidths) - self.engine.AddBlock(headerBlock) - - if self.IsColumnHeadingsOnEachPage(): - self.engine.AddBlock(RunningBlockPusher(headerBlock)) - - # Are we printing a GroupListView? - # We can't use isinstance() since ObjectListView may not be installed - if hasattr(self.lv, "GetShowGroups") and self.lv.GetShowGroups(): - self.engine.AddBlock(GroupListRowsBlock(self.lv, self.left, self.right, scale, self.allCellWidths)) - else: - self.engine.AddBlock(ListRowsBlock(self.lv, self.left, self.right, scale, self.allCellWidths)) - - if self.IsColumnHeadingsOnEachPage(): - self.engine.AddBlock(RunningBlockPusher(headerBlock, False)) - - return True - - def CalculateShrinkToFit(self, dc): - """ - How much do we have to shrink our rows by to fit onto the page? - """ - block = ColumnHeaderBlock(self.lv, self.left, self.right, 1, self.allCellWidths) - block.engine = self.engine - rowWidth = block.CalculateWidth(dc) - boundsWidth = RectUtils.Width(self.GetWorkBounds()) - - if rowWidth > boundsWidth: - return float(boundsWidth) / float(rowWidth) - else: - return 1 - -#---------------------------------------------------------------------------- - -class ColumnBasedBlock(CellBlock): - """ - A ColumnBasedBlock is a cell block that takes its width from the - columns of a ListCtrl. - - This is an abstract class - """ - - def __init__(self, lv, left, right, scale, allCellWidths): - self.lv = lv - self.left = left - self.right = right - self.scale = scale - self.allCellWidths = allCellWidths - - #---------------------------------------------------------------------------- - # Accessing - Subclasses should override - - def GetListCtrl(self): - """ - Return the ListCtrl that is behind this cell block. - """ - return self.lv - - - def GetCellWidths(self): - """ - Return the widths of the cells used in this block - """ - #return [self.allCellWidths[i] for i in range(self.left, self.right+1)] - return self.allCellWidths[self.left:self.right+1] - - #---------------------------------------------------------------------------- - # Utiltities - - def GetColumnAlignments(self, lv, left, right): - """ - Return the alignments of the given slice of columns - """ - listAlignments = [lv.GetColumn(i).GetAlign() for i in range(left, right+1)] - mapping = { - wx.LIST_FORMAT_LEFT: wx.ALIGN_LEFT, - wx.LIST_FORMAT_RIGHT: wx.ALIGN_RIGHT, - wx.LIST_FORMAT_CENTRE: wx.ALIGN_CENTRE, - } - return [mapping[x] for x in listAlignments] - - - -#---------------------------------------------------------------------------- - -class ColumnHeaderBlock(ColumnBasedBlock): - """ - A ColumnHeaderBlock prints a portion of the columns header in a ListCtrl. - """ - - #---------------------------------------------------------------------------- - # Accessing - - def GetTexts(self): - """ - Return a list of the texts that should be drawn with the cells - """ - return [self.lv.GetColumn(i).GetText() for i in range(self.left, self.right+1)] - - def GetAlignments(self): - """ - Return a list indicating how the text within each cell is aligned. - """ - if self.GetFormat().AlwaysCenter: - return [wx.ALIGN_CENTRE for i in range(self.left, self.right+1)] - else: - return self.GetColumnAlignments(self.lv, self.left, self.right) - - def GetImages(self): - """ - Return a list of the images that should be drawn in each cell - """ - # For some reason, columns return 0 when they have no image, rather than -1 like they should - images = list() - for i in range(self.left, self.right+1): - imageIndex = self.lv.GetColumn(i).GetImage() - if imageIndex == 0: - imageIndex = -1 - images.append(imageIndex) - return images - - - def IsUseSubstitution(self): - """ - Should the text values printed by this block have substitutions performed before being printed? - - Normally, we don't want to substitute within values that comes from the ListCtrl. - """ - return False - - -#---------------------------------------------------------------------------- - -class ListRowsBlock(Block): - """ - A ListRowsBlock prints rows of an ListCtrl. - """ - - def __init__(self, lv, left, right, scale, allCellWidths): - """ - """ - self.lv = lv - self.left = left - self.right = right - self.scale = scale - self.allCellWidths = allCellWidths - self.currentIndex = 0 - self.totalRows = self.lv.GetItemCount() - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - # This block works by printing a single row and then rescheduling itself - # to print the remaining rows after the current row has finished. - - if self.currentIndex < self.totalRows: - self.engine.AddBlock(RowBlock(self.lv, self.currentIndex, self.left, self.right, self.scale, self.allCellWidths)) - self.currentIndex += 1 - self.engine.AddBlock(self) - - return True - -#---------------------------------------------------------------------------- - -class GroupListRowsBlock(Block): - """ - A GroupListRowsBlock prints rows of an GroupListView. - """ - - def __init__(self, lv, left, right, scale, allCellWidths): - """ - """ - self.lv = lv # Must be a GroupListView - self.left = left - self.right = right - self.scale = scale - self.allCellWidths = allCellWidths - - self.currentIndex = 0 - self.totalRows = self.lv.GetItemCount() - - #---------------------------------------------------------------------------- - # Commands - - def Print(self, dc): - """ - Print this Block. - - Return True if the Block has finished printing - """ - # This block works by printing a single row and then rescheduling itself - # to print the remaining rows after the current row has finished. - - if self.currentIndex >= self.totalRows: - return True - - # If GetObjectAt() return an object, then it's a normal row. - # Otherwise, if the innerList object isn't None, it must be a ListGroup - # We can't use isinstance(x, GroupListView) because ObjectListView may not be installed - if self.lv.GetObjectAt(self.currentIndex): - self.engine.AddBlock(RowBlock(self.lv, self.currentIndex, self.left, self.right, self.scale, self.allCellWidths)) - elif self.lv.innerList[self.currentIndex]: - self.engine.AddBlock(GroupTitleBlock(self.lv, self.lv.innerList[self.currentIndex])) - - # Schedule the printing of the remaining rows - self.currentIndex += 1 - self.engine.AddBlock(self) - - return True - - -#---------------------------------------------------------------------------- - -class RowBlock(ColumnBasedBlock): - """ - A RowBlock prints a vertical slice of a single row of an ListCtrl. - """ - - def __init__(self, lv, rowIndex, left, right, scale, allCellWidths): - self.lv = lv - self.rowIndex = rowIndex - self.left = left - self.right = right - self.scale = scale - self.allCellWidths = allCellWidths - - #---------------------------------------------------------------------------- - # Accessing - - - def GetFont(self): - """ - Return Font that should be used to draw text in this block - """ - font = None - if self.engine.reportFormat.UseListCtrlTextFormat and self.GetListCtrl(): - # Figure out what font is being used for this row in the list. - # Unfortunately, there is no one way to find this information: virtual mode lists - # have to be treated in a different manner from non-virtual lists. - listCtrl = self.GetListCtrl() - if listCtrl.IsVirtual(): - attr = listCtrl.OnGetItemAttr(self.rowIndex) - if attr and attr.HasFont(): - font = attr.GetFont() - else: - font = listCtrl.GetItemFont(self.rowIndex) - - if font and font.IsOk(): - return font - else: - return self.GetFormat().GetFont() - - - def GetTextColor(self): - """ - Return Colour that should be used to draw text in this block - """ - color = None - if self.engine.reportFormat.UseListCtrlTextFormat and self.GetListCtrl(): - # Figure out what text colour is being used for this row in the list. - # Unfortunately, there is no one way to find this information: virtual mode lists - # have to be treated in a different manner from non-virtual lists. - listCtrl = self.GetListCtrl() - if listCtrl.IsVirtual(): - attr = listCtrl.OnGetItemAttr(self.rowIndex) - if attr and attr.HasTextColour(): - color = attr.GetTextColour() - else: - color = listCtrl.GetItemTextColour(self.rowIndex) - - if color and color.IsOk(): - return color - else: - return self.GetFormat().GetTextColor() - - - def IsUseSubstitution(self): - """ - Should the text values printed by this block have substitutions performed before being printed? - - Normally, we don't want to substitute within values that comes from the ListCtrl. - """ - return False - - #---------------------------------------------------------------------------- - # Overrides for CellBlock - - def GetTexts(self): - """ - Return a list of the texts that should be drawn with the cells - """ - return [self.lv.GetItem(self.rowIndex, i).GetText() for i in range(self.left, self.right+1)] - - def GetAlignments(self): - """ - Return a list indicating how the text within each cell is aligned. - """ - return self.GetColumnAlignments(self.lv, self.left, self.right) - - def GetImages(self): - """ - Return a list of the images that should be drawn in each cell - """ - return [self.lv.GetItem(self.rowIndex, i).GetImage() for i in range(self.left, self.right+1)] - - #def GetTexts(self): - # """ - # Return a list of the texts that should be drawn with the cells - # """ - # modelObjects = self.lv.GetObjectAt(self.rowIndex) - # return [self.lv.GetStringValueAt(modelObjects, i) for i in range(self.left, self.right+1)] - # - #def GetAlignments(self): - # """ - # Return a list indicating how the text within each cell is aligned. - # """ - # return [self.lv.columns[i].GetAlignment() for i in range(self.left, self.right+1)] - # - #def GetImages(self): - # """ - # Return a list of the images that should be drawn in each cell - # """ - # modelObjects = self.lv.GetObjectAt(self.rowIndex) - # return [self.lv.GetImageAt(modelObjects, i) for i in range(self.left, self.right+1)] - - -#---------------------------------------------------------------------------- - -class Decoration(object): - """ - A Decoration add some visual effect to a Block (e.g. borders, - background image, watermark). They can also reserve a chunk of their Blocks - space for their own use. - - Decorations are added to a BlockFormat which is then applied to a ReportBlock. - - All the decorations for a block are drawn into the same area. If two decorations are - added, they will draw over the top of each other. This is normally what is expected, - but may sometimes be surprising. For example, if you add two Lines to the left of the - same block, they will draw over the top of each other. - - """ - - #---------------------------------------------------------------------------- - # Accessing - - def IsDrawOver(self): - """ - Return True if this decoration should be drawn over it's block. If not, - it will be drawn underneath - """ - return False - - #---------------------------------------------------------------------------- - # Commands - - def SubtractFrom(self, dc, bounds): - """ - Subtract the space used by this decoration from the given bounds - """ - return bounds - - def DrawDecoration(self, dc, bounds, block): - """ - Draw this decoration - """ - pass - -#---------------------------------------------------------------------------- - -class RectangleDecoration(Decoration): - """ - A RectangleDecoration draw a rectangle around or on the side of a block. - - The rectangle can be hollow, solid filled or gradient-filled. It can have - a frame drawn as well. - - """ - - def __init__(self, side=None, pen=None, color=None, toColor=None, width=0, space=0): - """ - If color is None, the rectangle will be hollow. - If toColor is None, the rectangle will be filled with "color" otherwise it will be - gradient-filled. - If pen is not None, the rectangle will be framed with that pen. - If no side is given, the rectangle will be drawn around the block. In that case, - space can be a list giving the space on each side. - """ - self.side = side - self.pen = pen - self.color = color - self.toColor = toColor - self.width = width - self.space = space - - #---------------------------------------------------------------------------- - # Commands - - def SubtractFrom(self, dc, bounds): - """ - Subtract the space used by this decoration from the given bounds - """ - if self.side is None: - return RectUtils.InsetBy(RectUtils.InsetBy(bounds, self.space), self.width) - - inset = self.space + self.width - if self.side == wx.LEFT: return RectUtils.MoveLeftBy(bounds, inset) - if self.side == wx.RIGHT: return RectUtils.MoveRightBy(bounds, -inset) - if self.side == wx.TOP: return RectUtils.MoveTopBy(bounds, inset) - if self.side == wx.BOTTOM: return RectUtils.MoveBottomBy(bounds, -inset) - return bounds - - - def DrawDecoration(self, dc, bounds, block): - """ - Draw this decoration - """ - rect = self._CalculateRect(bounds) - - if self.color: - if self.toColor is None: - dc.SetPen(wx.TRANSPARENT_PEN) - dc.SetBrush(wx.Brush(self.color)) - dc.DrawRectangle(*rect) - else: - dc.GradientFillLinear(wx.Rect(*rect), self.color, self.toColor) - - if self.pen: - dc.SetPen(self.pen) - dc.SetBrush(wx.TRANSPARENT_BRUSH) - dc.DrawRectangle(*rect) - - - def _CalculateRect(self, bounds): - """ - Calculate the rectangle that this decoration is going to paint - """ - if self.side is None: - return bounds - if self.side == wx.LEFT: - return (RectUtils.Left(bounds), RectUtils.Top(bounds), self.width, RectUtils.Height(bounds)) - if self.side == wx.RIGHT: - return (RectUtils.Right(bounds) - self.width, RectUtils.Top(bounds), self.width, RectUtils.Height(bounds)) - if self.side == wx.TOP: - return (RectUtils.Left(bounds), RectUtils.Top(bounds), RectUtils.Width(bounds), self.width) - if self.side == wx.BOTTOM: - return (RectUtils.Left(bounds), RectUtils.Bottom(bounds) - self.width, RectUtils.Width(bounds), self.width) - - return bounds - -#---------------------------------------------------------------------------- - -class LineDecoration(Decoration): - """ - A LineDecoration draws a line on the side of a decoration. - """ - - def __init__(self, side=wx.BOTTOM, pen=None, space=0): - self.side = side - self.pen = pen - self.space = space - - #---------------------------------------------------------------------------- - # Commands - - def SubtractFrom(self, dc, bounds): - """ - Subtract the space used by this decoration from the given bounds - """ - inset = self.space - if self.pen is not None: - inset += self.pen.GetWidth() - - if self.side == wx.LEFT: return RectUtils.MoveLeftBy(bounds, inset) - if self.side == wx.RIGHT: return RectUtils.MoveRightBy(bounds, -inset) - if self.side == wx.TOP: return RectUtils.MoveTopBy(bounds, inset) - if self.side == wx.BOTTOM: return RectUtils.MoveBottomBy(bounds, -inset) - return bounds - - - def DrawDecoration(self, dc, bounds, block): - """ - Draw this decoration - """ - if self.pen == None: - return - - if self.side == wx.LEFT: - pt1 = RectUtils.TopLeft(bounds) - pt2 = RectUtils.BottomLeft(bounds) - elif self.side == wx.RIGHT: - pt1 = RectUtils.TopRight(bounds) - pt2 = RectUtils.BottomRight(bounds) - elif self.side == wx.TOP: - pt1 = RectUtils.TopLeft(bounds) - pt2 = RectUtils.TopRight(bounds) - elif self.side == wx.BOTTOM: - pt1 = RectUtils.BottomLeft(bounds) - pt2 = RectUtils.BottomRight(bounds) - - dc.SetPen(self.pen) - dc.DrawLine(pt1[0], pt1[1], pt2[0], pt2[1]) - -#---------------------------------------------------------------------------- - -class WatermarkDecoration(Decoration): - """ - A WatermarkDecoration draws an angled line of text over each page. - - The watermark is rotated around the center of the page. - - If *over* is True, the watermark will be printed on top of the page. - Otherwise, it will be printed under the page. - - """ - - def __init__(self, text, font=None, color=None, angle=30, over=True): - """ - """ - self.text = text - self.color = color or wx.Color(128, 128, 128, 128) - self.font = font or wx.FFont(128, wx.SWISS, 0) - self.angle = angle - self.over = over - - def IsDrawOver(self): - """ - Should this decoration be drawn over the rest of page? - """ - return self.over - - def DrawDecoration(self, dc, bounds, block): - """ - Draw the decoration - """ - dc.SetFont(self.font) - dc.SetTextForeground(self.color) - - # Rotate the text around the center of the page - cx, cy = RectUtils.Center(bounds) - w, h = dc.GetTextExtent(self.text) - - x = cx - w/2 - y = cy - h/2 + (w/2 * math.sin(math.radians(self.angle))) - - dc.DrawRotatedText(self.text, x, y, self.angle) - -#---------------------------------------------------------------------------- - -class ImageDecoration(Decoration): - """ - A ImageDecoration draws an image over (or under) the given block. - - NB: Printer contexts do not honor alpha channels. - """ - - def __init__(self, image=None, horizontalAlign=wx.CENTER, verticalAlign=wx.CENTER, over=True): - """ - image must be either an wx.Image or a wx.Bitmap - """ - self.horizontalAlign = horizontalAlign - self.verticalAlign = verticalAlign - self.over = True - - self.bitmap = image - if isinstance(image, wx.Image): - self.bitmap = wx.BitmapFromImage(image) - - def DrawDecoration(self, dc, bounds, block): - """ - Draw the decoration - """ - if not self.bitmap: - return - - if self.horizontalAlign == wx.LEFT: - x = RectUtils.Left(bounds) - elif self.horizontalAlign == wx.RIGHT: - x = RectUtils.Right(bounds) - self.bitmap.Width - else: - x = RectUtils.CenterX(bounds) - self.bitmap.Width/2 - - if self.verticalAlign == wx.TOP: - y = RectUtils.Top(bounds) - elif self.verticalAlign == wx.BOTTOM: - y = RectUtils.Bottom(bounds) - self.bitmap.Height - else: - y = RectUtils.CenterY(bounds) - self.bitmap.Height/2 - - dc.DrawBitmap(self.bitmap, x, y, True) - - -#---------------------------------------------------------------------------- - -class Bucket(object): - """ - General purpose, hold-all data object - """ - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __repr__(self): - strs = ["%s=%r" % kv for kv in self.__dict__.items()] - return "Bucket(" + ", ".join(strs) + ")" - -#---------------------------------------------------------------------------- - -class RectUtils: - """ - Static rectangle utilities. - - Rectangles are a list or tuple of 4-elements: [left, top, width, height] - """ - - #---------------------------------------------------------------------------- - # Accessing - - @staticmethod - def Left(r): return r[0] - - @staticmethod - def Top(r): return r[1] - - @staticmethod - def Width(r): return r[2] - - @staticmethod - def Height(r): return r[3] - - @staticmethod - def Right(r): return r[0] + r[2] - - @staticmethod - def Bottom(r): return r[1] + r[3] - - @staticmethod - def TopLeft(r): return [r[0], r[1]] - - @staticmethod - def TopRight(r): return [r[0] + r[2], r[1]] - - @staticmethod - def BottomLeft(r): return [r[0], r[1] + r[3]] - - @staticmethod - def BottomRight(r): return [r[0] + r[2], r[1] + r[3]] - - @staticmethod - def CenterX(r): return r[0] + r[2]/2 - - @staticmethod - def CenterY(r): return r[1] + r[3]/2 - - @staticmethod - def Center(r): return [r[0] + r[2]/2, r[1] + r[3]/2] - - #---------------------------------------------------------------------------- - # Modifying - - @staticmethod - def SetLeft(r, l): - r[0] = l - return r - - @staticmethod - def SetTop(r, t): - r[1] = t - return r - - @staticmethod - def SetWidth(r, w): - r[2] = w - return r - - @staticmethod - def SetHeight(r, h): - r[3] = h - return r - - @staticmethod - def MoveLeftBy(r, delta): - r[0] += delta - r[2] -= delta - return r - - @staticmethod - def MoveTopBy(r, delta): - r[1] += delta - r[3] -= delta - return r - - @staticmethod - def MoveRightBy(r, delta): - r[2] += delta - return r - - @staticmethod - def MoveBottomBy(r, delta): - r[3] += delta - return r - - #---------------------------------------------------------------------------- - # Calculations - - @staticmethod - def InsetBy(r, delta): - if delta is None: - return r - try: - delta[0] # is it indexable? - return RectUtils.InsetRect(r, delta) - except: - return RectUtils.InsetRect(r, (delta, delta, delta, delta)) - - @staticmethod - def InsetRect(r, r2): - if r2 is None: - return r - else: - return [r[0] + r2[0], r[1] + r2[1], r[2] - (r2[0] + r2[2]), r[3] - (r2[1] + r2[3])] - - @staticmethod - def MultiplyOrigin(r, factor): - return [r[0]*factor, r[1]*factor, r[2], r[3]] - -#---------------------------------------------------------------------------- -# TESTING ONLY -#---------------------------------------------------------------------------- - -if __name__ == '__main__': - import wx - from ObjectListView import ObjectListView, FastObjectListView, GroupListView, ColumnDefn - - # Where can we find the Example module? - import sys - sys.path.append("../Examples") - - import ExampleModel - import ExampleImages - - class MyFrame(wx.Frame): - def __init__(self, *args, **kwds): - kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Frame.__init__(self, *args, **kwds) - - self.panel = wx.Panel(self, -1) - #self.lv = ObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) - self.lv = GroupListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) - #self.lv = FastObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) - - sizer_2 = wx.BoxSizer(wx.VERTICAL) - sizer_2.Add(self.lv, 1, wx.ALL|wx.EXPAND, 4) - self.panel.SetSizer(sizer_2) - self.panel.Layout() - - sizer_1 = wx.BoxSizer(wx.VERTICAL) - sizer_1.Add(self.panel, 1, wx.EXPAND) - self.SetSizer(sizer_1) - self.Layout() - - musicImage = self.lv.AddImages(ExampleImages.getMusic16Bitmap(), ExampleImages.getMusic32Bitmap()) - artistImage = self.lv.AddImages(ExampleImages.getUser16Bitmap(), ExampleImages.getUser32Bitmap()) - - self.lv.SetColumns([ - ColumnDefn("Title", "left", 200, "title", imageGetter=musicImage), - ColumnDefn("Artist", "left", 150, "artist", imageGetter=artistImage), - ColumnDefn("Last Played", "left", 100, "lastPlayed"), - ColumnDefn("Size", "center", 100, "sizeInBytes"), - ColumnDefn("Rating", "center", 100, "rating"), - ]) - - #self.lv.CreateCheckStateColumn() - self.lv.SetSortColumn(self.lv.columns[2]) - self.lv.SetObjects(ExampleModel.GetTracks()) - - wx.CallLater(50, self.run) - - def run(self): - printer = ListCtrlPrinter(self.lv, "Playing with ListCtrl Printing") - printer.ReportFormat = ReportFormat.Normal() - printer.ReportFormat.WatermarkFormat(over=True) - printer.ReportFormat.IsColumnHeadingsOnEachPage = True - - #printer.ReportFormat.Page.Add(ImageDecoration(ExampleImages.getGroup32Bitmap(), wx.RIGHT, wx.BOTTOM)) - - #printer.PageHeader("%(listTitle)s") # nice idea but not possible at the moment - printer.PageHeader = "Playing with ListCtrl Printing" - printer.PageFooter = ("Bright Ideas Software", "%(date)s", "%(currentPage)d of %(totalPages)d") - printer.Watermark = "Sloth!" - - #printer.PageSetup() - printer.PrintPreview(self) - - - app = wx.PySimpleApp(0) - wx.InitAllImageHandlers() - frame_1 = MyFrame(None, -1, "") - app.SetTopWindow(frame_1) - frame_1.Show() - app.MainLoop() +# -*- coding: utf-8 -*- +#!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: ListCtrlPrinter.py +# Author: Phillip Piper +# Created: 17 July 2008 +# SVN-ID: $Id$ +# Copyright: (c) 2008 by Phillip Piper, 2008 +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/07/17 JPP Inspired beginning +#---------------------------------------------------------------------------- +# To do: +# - persistence of ReportFormat +# - allow cell contents to be vertically aligned +# - in light of several of the "to dos", consider having CellBlockFormat object +# - write a data abstraction layer between the printer and the ListCtrl. +# This layer could understand ObjectListViews and be more efficient. +# - consider if this could be made to work with a wx.Grid (needs data abstraction layer) + +# Known issues: +# - the 'space' setting on decorations is not intuitive + + +""" +An ``ListCtrlPrinter`` takes an ``ObjectListView`` or ``wx.ListCtrl`` and turns it into a +pretty report. + +As always, the goal is for this to be as easy to use as possible. A typical +usage should be as simple as:: + + printer = ListCtrlPrinter(self.myOlv, "My Report Title") + printer.PrintPreview() + +This will produce a report with reasonable formatting. The formatting of a report is +controlled completely by the ReportFormat object of the ListCtrlPrinter. To change the appearance +of the report, you change the settings in this object. + +A report consists of various sections (called "blocks") and each of these blocks has a +matching BlockFormat object in the ReportFormat. So, to modify the format of the +page header, you change the ReportFormat.PageHeader object. + +A ReportFormat object has the following properties which control the appearance of the matching +sections of the report: + +* PageHeader +* ListHeader +* ColumnHeader +* GroupTitle +* Row +* ListFooter +* PageFooter +* Page + +These properties return BlockFormat objects, which have the following properties: + +* AlwaysCenter +* CanWrap +* Font +* Padding +* TextAlignment +* TextColor + +* CellPadding +* GridPen + +Implementation +============== + +A ``ListCtrlPrinter`` is a *Facade* over two classes: + - ``ListCtrlPrintout``, which handles the interface to the wx printing subsystem + - ``ReportEngine``, which does the actual work of creating the report + +The ``ListCtrlPrintout`` handles all the details of the wx printing subsystem. In +particular, it configures the printing DC so that its origin and scale are correct. This +enables the ``ReportEngine`` to simply render the report without knowing the +characteristics of the underlying printer DPI, unprintable region, or the scale of a print +preview. When The ``ListCtrlPrintout`` encounters some action that is cannot perform (like +actually rendering a page) it calls back into ``ListCtrlPrinter`` (which simply forwards +to the ``ReportEngine``). + +The ``ReportEngine`` uses a block structure approach to reports. Each element of +a report is a "block", which stacks vertically with other blocks. + +When a block is printed, it can either render itself into a given DC or it can replace +itself in the report structure with one or more other blocks. A ``TextBlock`` renders +itself by drawing into the DC. In contrast, the block that prints a ``ListCtrl`` replaces +itself with a ``ListHeaderBlock``, a ``ColumnHeaderBlock``, ``ListRowsBlock`` and finally a +``ListFooterBlock``. + +The blocks describe the structure of the report. The formatting of a report is +controlled by ``BlockFormat`` objects. The ``ReportFormat`` contains a ``BlockFormat`` +object for each formattable section of the report. + +Each section must be formatted in same fashion, i.e. all column headers will look the same, +all page footer will look the same. There is no way to make the first footer look one way, +and the second look a different way. +""" + +import datetime +import math + +import wx + +from WordWrapRenderer import WordWrapRenderer + +#---------------------------------------------------------------------------- + +class ListCtrlPrinter(object): + """ + An ListCtrlPrinter creates a pretty report from an ObjectListView/ListCtrl. + + """ + + def __init__(self, listCtrl=None, title="ListCtrl Printing"): + """ + """ + self.printout = ListCtrlPrintout(self) + self.engine = ReportEngine() + if listCtrl is not None: + self.AddListCtrl(listCtrl, title) + + #---------------------------------------------------------------------------- + # Accessing + + def GetPageFooter(self): + """ + Return a 3-tuple of the texts that will be shown in left, center, and right cells + of the page footer + """ + return self.engine.pageFooter + + def SetPageFooter(self, leftText="", centerText="", rightText=""): + """ + Set the texts that will be shown in various cells of the page footer. + + leftText can be a string or a 3-tuple of strings. + """ + if isinstance(leftText, (tuple, list)): + self.engine.pageFooter = leftText + else: + self.engine.pageFooter = (leftText, centerText, rightText) + + def GetPageHeader(self): + """ + Return a 3-tuple of the texts that will be shown in left, center, and right cells + of the page header + """ + return self.engine.pageHeader + + def SetPageHeader(self, leftText="", centerText="", rightText=""): + """ + Set the texts that will be shown in various cells of the page header + """ + if isinstance(leftText, (tuple, list)): + self.engine.pageHeader = leftText + else: + self.engine.pageHeader = (leftText, centerText, rightText) + + def GetPrintData(self): + """ + Return the wx.PrintData that controls the printing of this report + """ + return self.printout.printData + + def GetReportFormat(self): + """ + Return the ReportFormat object that controls the appearance of this printout + """ + return self.engine.reportFormat + + def SetReportFormat(self, fmt): + """ + Set the ReportFormat object that controls the appearance of this printout + """ + self.engine.reportFormat = fmt + + def GetWatermark(self, txt): + """ + Get the text that will be printed as a watermark on the report + """ + return self.engine.watermark + + def SetWatermark(self, txt): + """ + Set the text that will be printed as a watermark on the report + """ + self.engine.watermark = txt + + ReportFormat = property(GetReportFormat, SetReportFormat) + PageFooter = property(GetPageFooter, SetPageFooter) + PageHeader = property(GetPageHeader, SetPageHeader) + PrintData = property(GetPrintData) + Watermark = property(GetWatermark, SetWatermark) + + #---------------------------------------------------------------------------- + # Setup + + def AddListCtrl(self, listCtrl, title=None): + """ + Add the given list to those that will be printed by this report. + """ + self.engine.AddListCtrl(listCtrl, title) + + + def Clear(self): + """ + Remove all ListCtrls from this printer + """ + self.engine.ClearListCtrls() + + #---------------------------------------------------------------------------- + # Printing Commands + + def PageSetup(self, parent=None): + """ + Show a Page Setup dialog that will change the configuration of this printout + """ + self.printout.PageSetup(parent) + + + def PrintPreview(self, parent=None, title="ListCtrl Print Preview", bounds=(20, 50, 800, 800)): + """ + Show a Print Preview of this report + """ + self.printout.PrintPreview(parent, title, bounds) + + + def Print(self, parent=None): + """ + Print this report to the selected printer + """ + self.printout.DoPrint(parent) + + #---------------------------------------------------------------------------- + # Callbacks + # These methods are invoked by the ListCtrlPrintout when required + + def CalculateTotalPages(self, dc, bounds): + """ + Do the work of calculating how many pages this report will occupy? + + This is expensive because it basically prints the whole report. + """ + return self.engine.CalculateTotalPages(dc, bounds) + + + def StartPrinting(self): + """ + A new print job is about to begin. + """ + self.engine.StartPrinting() + + + def PrintPage(self, dc, pageNumber, bounds): + """ + Print the given page on the given device context. + """ + self.engine.PrintPage(dc, pageNumber, bounds) + +#---------------------------------------------------------------------------- + +class ReportEngine(object): + """ + A ReportEngine handles all the work of actually producing a report. + + Public instance variables (all others should be treated as private): + + * dateFormat + When the current date/time is substituted into report text, how should + the datetime be formatted? This must be a valid format string for the + strftime() method. + Default: "%x %X" + + """ + + def __init__(self): + """ + """ + self.currentPage = -1 + self.totalPages = -1 + self.blocks = list() + self.blockInsertionIndex = 0 + self.listCtrls = list() + self.dateFormat = "%x %X" + + self.reportFormat = ReportFormat.Normal() + + self.watermark = "" + self.pageHeader = list() + self.pageFooter = list() + #self.isPrintSelectionOnly = False # not currently implemented + + # If this is False, no drawing should be done. The engine is either counting + # pages, or skipping to a specific page + self.shouldDrawBlocks = True + + #---------------------------------------------------------------------------- + # Accessing + + def GetNamedFormat(self, name): + """ + Return the given format + """ + return self.reportFormat.GetNamedFormat(name) + + + def GetTotalPages(self): + """ + Return the total number of pages that this report will produce. + + CalculateTotalPages() must be called before this is accurate. + """ + return self.totalPages + + + def GetSubstitutionInfo(self): + """ + Return a dictionary that can be used for substituting values into strings + """ + dateString = datetime.datetime.now().strftime(self.dateFormat) + info = { + "currentPage": self.currentPage, + "date": dateString, + "totalPages": self.totalPages, + } + return info + + #---------------------------------------------------------------------------- + # Calculating + + def CalculateTotalPages(self, dc, bounds): + """ + Do the work of calculating how many pages this report will occupy? + + This is expensive because it basically prints the whole report. + """ + self.StartPrinting() + self.totalPages = 1 + self.shouldDrawBlocks = False + while self.PrintOnePage(dc, self.totalPages, bounds): + self.totalPages += 1 + self.shouldDrawBlocks = True + return self.totalPages + + #---------------------------------------------------------------------------- + # Commands + + def AddBlock(self, block): + """ + Add the given block at the current insertion point + """ + self.blocks.insert(self.blockInsertionIndex, block) + self.blockInsertionIndex += 1 + block.engine = self + + + def AddListCtrl(self, listCtrl, title=None): + """ + Add the given list to those that will be printed by this report. + """ + if listCtrl.InReportView(): + self.listCtrls.append([listCtrl, title]) + + + def ClearListCtrls(self): + """ + Remove all ListCtrls from this report. + """ + self.listCtrls = list() + + + def DropCurrentBlock(self): + """ + Remove the current block from our list of blocks + """ + self.blocks.pop(0) + self.blockInsertionIndex = 1 + + #---------------------------------------------------------------------------- + # Printing + + def StartPrinting(self): + """ + Initial a print job on this engine + """ + self.currentPage = 0 + self.blockInsertionIndex = 0 + self.blocks = list() + self.AddBlock(ReportBlock()) + self.runningBlocks = list() + self.AddRunningBlock(PageHeaderBlock()) + self.AddRunningBlock(PageFooterBlock()) + + if self.watermark: + self._CreateReplaceWatermarkDecoration() + + + def AddRunningBlock(self, block): + """ + A running block is printed on every page until it is removed + """ + self.runningBlocks.append(block) + block.engine = self + + + def RemoveRunningBlock(self, block): + """ + A running block is printed on every page until it is removed + """ + self.runningBlocks.remove(block) + + + def PrintPage(self, dc, pageNumber, bounds): + """ + Print the given page on the given device context. + """ + # If the request page isn't next in order, we have to restart + # the printing process and advance until we reach the desired page. + if pageNumber != self.currentPage + 1: + self.StartPrinting() + self.shouldDrawBlocks = False + for i in range(1, pageNumber): + self.PrintOnePage(dc, i, bounds) + self.shouldDrawBlocks = True + + return self.PrintOnePage(dc, pageNumber, bounds) + + + def PrintOnePage(self, dc, pageNumber, bounds): + """ + Print the current page on the given device context. + + Return true if there is still more to print. + """ + # Initialize state + self.currentPage = pageNumber + self.pageBounds = list(bounds) + self.workBounds = list(self.pageBounds) + self.SubtractDecorations(dc) + + # Print page adornments, including under-text decorations + self.DrawPageDecorations(dc, False) + for x in self.runningBlocks: + x.Print(dc) + + # Print blocks until they won't fit or we run out of blocks + while len(self.blocks) and self.blocks[0].Print(dc): + self.DropCurrentBlock() + + # Finally, print over-the-text decorations + self.DrawPageDecorations(dc, True) + + return len(self.blocks) > 0 + + + def SubtractDecorations(self, dc): + """ + # Subtract the area used from the work area + """ + fmt = self.GetNamedFormat("Page") + self.workBounds = fmt.SubtractDecorations(dc, self.workBounds) + + + def DrawPageDecorations(self, dc, over): + """ + Draw the page decorations + """ + if not self.shouldDrawBlocks: + return + + fmt = self.GetNamedFormat("Page") + bounds = list(self.pageBounds) + fmt.DrawDecorations(dc, bounds, self, over) + + + def _CreateReplaceWatermarkDecoration(self): + """ + Create a watermark decoration, replacing any existing watermark + """ + pageFmt = self.GetNamedFormat("Page") + pageFmt.decorations = [x for x in pageFmt.decorations if not isinstance(x, WatermarkDecoration)] + + watermarkFmt = self.GetNamedFormat("Watermark") + pageFmt.Add(WatermarkDecoration(self.watermark, font=watermarkFmt.Font, + color=watermarkFmt.TextColor, angle=watermarkFmt.Angle, + over=watermarkFmt.Over)) + +#---------------------------------------------------------------------------- + +class ListCtrlPrintout(wx.Printout): + """ + An ListCtrlPrintout is the interface between the wx printing system + and ListCtrlPrinter. + """ + + def __init__(self, olvPrinter, margins=None): + """ + """ + wx.Printout.__init__(self) + self.olvPrinter = olvPrinter + self.margins = margins or (wx.Point(15, 15), wx.Point(15, 15)) + self.totalPages = -1 + + self.printData = wx.PrintData() + self.printData.SetPrinterName("") # Use default printer + self.printData.SetPaperId(wx.PAPER_A4) + self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) + + + #---------------------------------------------------------------------------- + # Accessing + + def HasPage(self, page): + """ + Return true if this printout has the given page number + """ + return page <= self.totalPages + + + def GetPageInfo(self): + """ + Return a 4-tuple indicating the ... + """ + return (1, self.totalPages, 1, 1) + + + def GetPrintPreview(self): + """ + Get a wxPrintPreview of this report + """ + data = wx.PrintDialogData(self.printData) + forViewing = ListCtrlPrintout(self.olvPrinter, self.margins) + forPrinter = ListCtrlPrintout(self.olvPrinter, self.margins) + preview = wx.PrintPreview(forViewing, forPrinter, data) + return preview + + #---------------------------------------------------------------------------- + # Commands + + def PageSetup(self, parent): + """ + Show a Page Setup dialog that will change the configuration of this printout + """ + data = wx.PageSetupDialogData() + data.SetPrintData(self.printData) + data.SetDefaultMinMargins(True) + data.SetMarginTopLeft(self.margins[0]) + data.SetMarginBottomRight(self.margins[1]) + dlg = wx.PageSetupDialog(parent, data) + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetPageSetupData() + self.printData = wx.PrintData(data.GetPrintData()) + self.printData.SetPaperId(data.GetPaperId()) + self.margins = (data.GetMarginTopLeft(), data.GetMarginBottomRight()) + dlg.Destroy() + + + + def PrintPreview(self, parent, title, bounds): + """ + Show a Print Preview of this report + """ + self.preview = self.GetPrintPreview() + + if not self.preview.Ok(): + return False + + pfrm = wx.PreviewFrame(self.preview, parent, title) + + pfrm.Initialize() + pfrm.SetPosition(bounds[0:2]) + pfrm.SetSize(bounds[2:4]) + pfrm.Show(True) + + return True + + + def DoPrint(self, parent): + """ + Send the report to the configured printer + """ + try: + pdd = wx.PrintDialogData(self.printData) + printer = wx.Printer(pdd) + + if printer.Print(parent, self, True): + self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) + else: + wx.MessageBox("There was a problem printing.\nPerhaps your current printer is not set correctly?", "Printing", wx.OK) + finally: + pdd.Destroy() + + + #---------------------------------------------------------------------------- + # Event handlers + + def OnPreparePrinting(self): + """ + Prepare for printing. This event is sent before any of the others + """ + dc = self.GetDC() + self.SetScaleAndBounds(dc) + self.totalPages = self.olvPrinter.CalculateTotalPages(dc, self.bounds) + self.olvPrinter.StartPrinting() + + def OnBeginDocument(self, start, end): + """ + Begin printing one copy of the document. Return False to cancel the job + """ + return super(ListCtrlPrintout, self).OnBeginDocument(start, end) + + def OnEndDocument(self): + super(ListCtrlPrintout, self).OnEndDocument() + + def OnBeginPrinting(self): + super(ListCtrlPrintout, self).OnBeginPrinting() + + def OnEndPrinting(self): + super(ListCtrlPrintout, self).OnEndPrinting() + + def OnPrintPage(self, page): + """ + Do the work of printing the given page number. + """ + # We bounce this back to the printer facade + dc = self.GetDC() + self.SetScaleAndBounds(dc) + return self.olvPrinter.PrintPage(dc, page, self.bounds) + + def SetScaleAndBounds(self, dc): + """ + Calculate the scale required for our printout to match what appears on screen, + and the bounds that will be effective at that scale and margins + """ + # This code comes from Robin Dunn's "wxPython In Action." + # Without that, I would never have figured this out. + ppiPrinterX, ppiPrinterY = self.GetPPIPrinter() + ppiScreenX, ppiScreenY = self.GetPPIScreen() + logicalScale = float(ppiPrinterX) / float(ppiScreenX) + pw, ph = self.GetPageSizePixels() + dw, dh = dc.GetSize() + scale = logicalScale * float(dw)/float(pw) + dc.SetUserScale(scale, scale) + + # Now calculate our boundries + logicalUnitsMM = float(ppiPrinterX) / (logicalScale*25.4) + topLeft, bottomRight = self.margins + left = round(topLeft.x * logicalUnitsMM) + top = round(topLeft.y * logicalUnitsMM) + right = round(dc.DeviceToLogicalYRel(dw) - bottomRight.x * logicalUnitsMM) + bottom = round(dc.DeviceToLogicalYRel(dh) - bottomRight.y * logicalUnitsMM) + self.bounds = (left, top, right-left, bottom-top) + + +#---------------------------------------------------------------------------- + +class ReportFormat(object): + """ + A ReportFormat defines completely how a report is formatted. + + It holds a collection of BlockFormat objects which control the + formatting of individual blocks of the report + + Public instance variables: + + * IncludeImages + Should images from the ListCtrl be printed in the report? + Default: *True* + + * IsColumnHeadingsOnEachPage + Will the column headers be printed at the top of each page? + Default: *True* + + * IsShrinkToFit + Will the columns be shrunk so they all fit into the width of one page? + Default: *False* + + * UseListCtrlTextFormat + If this is True, the text format (i.e. font and text color) of each row will be taken from the ListCtrl, + rather than from the *Cell* format. + Default: *False* + + """ + + def __init__(self): + """ + """ + # Initialize the formats that control the various portions of the report + self.Page = BlockFormat() + self.PageHeader = BlockFormat() + self.ListHeader = BlockFormat() + self.GroupTitle = BlockFormat() + self.ColumnHeader = BlockFormat() + self.Row = BlockFormat() + self.ListFooter = BlockFormat() + self.PageFooter = BlockFormat() + self.Watermark = BlockFormat() + + self.IncludeImages = True + self.IsColumnHeadingsOnEachPage = False + self.IsShrinkToFit = False + self.UseListCtrlTextFormat = True + + # Initialize the watermark format to default values + self.WatermarkFormat() + + #---------------------------------------------------------------------------- + # Accessing + + def GetNamedFormat(self, name): + """ + Return the format used in to format a block with the given name. + """ + return getattr(self, name) + + #---------------------------------------------------------------------------- + # Commands + + def WatermarkFormat(self, font=None, color=None, angle=30, over=False): + """ + Change the format of the watermark printed on this report. + + The actual text of the water mark is set by `ListCtrlPrinter.Watermark` property. + """ + defaultFaceName = "Stencil" + self.Watermark.Font = font or wx.FFont(96, wx.FONTFAMILY_DEFAULT, 0, defaultFaceName) + self.Watermark.TextColor = color or wx.Colour(204, 204, 204) + self.Watermark.Angle = angle + self.Watermark.Over = over + + #---------------------------------------------------------------------------- + # Standard formats + # These are meant to be illustrative rather than definitive + + @staticmethod + def Minimal(headerFontName="Arial", rowFontName="Times New Roman"): + """ + Return a minimal format for a report + """ + fmt = ReportFormat() + fmt.IsShrinkToFit = False + + fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.PageHeader.Line(wx.BOTTOM, wx.BLACK, 1, space=5) + fmt.PageHeader.Padding = (0, 0, 0, 12) + + fmt.ListHeader.Font = wx.FFont(18, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.ListHeader.Padding = (0, 12, 0, 12) + fmt.ListHeader.Line(wx.BOTTOM, wx.BLACK, 1, space=5) + + fmt.GroupTitle.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.GroupTitle.Padding = (0, 12, 0, 12) + fmt.GroupTitle.Line(wx.BOTTOM, wx.BLACK, 1, space=5) + + fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.PageFooter.Line(wx.TOP, wx.BLACK, 1, space=3) + fmt.PageFooter.Padding = (0, 16, 0, 0) + + fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.ColumnHeader.Padding = (0, 12, 0, 12) + fmt.ColumnHeader.CellPadding = 5 + fmt.ColumnHeader.Line(wx.BOTTOM, wx.Colour(192, 192, 192), 1, space=3) + fmt.ColumnHeader.AlwaysCenter = True + + fmt.Row.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=rowFontName) + fmt.Row.CellPadding = 5 + fmt.Row.Line(wx.BOTTOM, wx.Colour(192, 192, 192), 1, space=3) + fmt.Row.CanWrap = True + + return fmt + + @staticmethod + def Normal(headerFontName="Gill Sans", rowFontName="Times New Roman"): + """ + Return a reasonable default format for a report + """ + fmt = ReportFormat() + fmt.IsShrinkToFit = True + + fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.PageHeader.Line(wx.BOTTOM, wx.BLUE, 2, space=5) + fmt.PageHeader.Padding = (0, 0, 0, 12) + + fmt.ListHeader.Font = wx.FFont(26, wx.FONTFAMILY_SWISS, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.ListHeader.TextColor = wx.WHITE + fmt.ListHeader.Padding = (0, 12, 0, 12) + fmt.ListHeader.TextAlignment = wx.ALIGN_LEFT + fmt.ListHeader.Background(wx.BLUE, wx.WHITE, space=(16, 4, 0, 4)) + + fmt.GroupTitle.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.GroupTitle.Line(wx.BOTTOM, wx.BLUE, 4, toColor=wx.WHITE, space=5) + fmt.GroupTitle.Padding = (0, 12, 0, 12) + + fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DEFAULT, face=headerFontName) + fmt.PageFooter.Background(wx.WHITE, wx.BLUE, space=(0, 4, 0, 4)) + + fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_DEFAULT, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.ColumnHeader.CellPadding = 2 + fmt.ColumnHeader.Background(wx.Colour(192, 192, 192)) + fmt.ColumnHeader.GridPen = wx.Pen(wx.WHITE, 1) + fmt.ColumnHeader.Padding = (0, 0, 0, 12) + fmt.ColumnHeader.AlwaysCenter = True + + fmt.Row.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=rowFontName) + fmt.Row.Line(wx.BOTTOM, pen=wx.Pen(wx.BLUE, 1, wx.DOT), space=3) + fmt.Row.CellPadding = 2 + fmt.Row.CanWrap = True + + return fmt + + @staticmethod + def TooMuch(headerFontName="Chiller", rowFontName="Gill Sans"): + """ + Return a reasonable default format for a report + """ + fmt = ReportFormat() + fmt.IsShrinkToFit = False + + fmt.PageHeader.Font = wx.FFont(12, wx.FONTFAMILY_DECORATIVE, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.PageHeader.TextColor = wx.WHITE + fmt.PageHeader.Background(wx.GREEN, wx.RED, space=(16, 4, 0, 4)) + fmt.PageHeader.Padding = (0, 0, 0, 12) + + fmt.ListHeader.Font = wx.FFont(24, wx.FONTFAMILY_DECORATIVE, face=headerFontName) + fmt.ListHeader.TextColor = wx.WHITE + fmt.ListHeader.Padding = (0, 12, 0, 12) + fmt.ListHeader.TextAlignment = wx.ALIGN_CENTER + fmt.ListHeader.Background(wx.RED, wx.GREEN, space=(16, 4, 0, 4)) + + fmt.GroupTitle.Font = wx.FFont(14, wx.FONTFAMILY_DECORATIVE, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.GroupTitle.TextColor = wx.BLUE + fmt.GroupTitle.Padding = (0, 12, 0, 12) + fmt.GroupTitle.Line(wx.BOTTOM, wx.GREEN, 4, toColor=wx.WHITE, space=5) + + fmt.PageFooter.Font = wx.FFont(10, wx.FONTFAMILY_DECORATIVE, face=headerFontName) + fmt.PageFooter.Line(wx.TOP, wx.GREEN, 2, toColor=wx.RED, space=3) + fmt.PageFooter.Padding = (0, 16, 0, 0) + + fmt.ColumnHeader.Font = wx.FFont(14, wx.FONTFAMILY_SWISS, wx.FONTFLAG_BOLD, face=headerFontName) + fmt.ColumnHeader.Background(wx.Colour(255, 215, 0)) + fmt.ColumnHeader.CellPadding = 5 + fmt.ColumnHeader.GridPen = wx.Pen(wx.Colour(192, 192, 192), 1) + + fmt.Row.Font = wx.FFont(12, wx.FONTFAMILY_SWISS, face=rowFontName) + fmt.Row.CellPadding = 5 + fmt.Row.GridPen = wx.Pen(wx.BLUE, 1, wx.DOT) + fmt.Row.CanWrap = True + + fmt.Watermark.TextColor = wx.Colour(233, 150, 122) + + return fmt + +#---------------------------------------------------------------------------- + +class BlockFormat(object): + """ + A block format defines how a Block is formatted. + + These properties control the formatting of the matching Block: + + * CanWrap + If the text for this block cannot fit horizontally, should be wrap to a new line (True) + or should it be truncated (False)? + * Font + What font should be used to draw the text of this block + * Padding + How much padding should be applied to the block before the text or other decorations + are drawn? This can be a numeric (which will be applied to all sides) or it can be + a collection of the paddings to be applied to the various sides: (left, top, right, bottom). + * TextAlignment + How should text be aligned within this block? Can be wx.ALIGN_LEFT, wx.ALIGN_CENTER, or + wx.ALIGN_RIGHT. + * TextColor + In what color should be text be drawn? + + The blocks that are based on cells (PageHeader, ColumnHeader, Row, PageFooter) can also + have the following properties set: + + * AlwaysCenter + Will the text in the cells be center aligned, regardless of other settings? + * CellPadding + How much padding should be applied to this cell before the text or other decorations + are drawn? This can be a numeric (which will be applied to all sides) or it can be a + collection of the paddings to be applied to the various sides: (left, top, right, + bottom). + * GridPen + What Pen will be used to draw the grid lines of the cells? + + In addition to these properties, there are some methods which add various decorations to + the blocks: + + * Background(color=wx.BLUE, toColor=None, space=0) + + This gives the block a solid color background (or a gradient background if *toColor* + is not None). If *space* is not 0, *space* pixels will be subtracted from all sides + from the space available to the block. + + * Frame(pen=None, space=0) + + Draw a rectangle around the block in the given pen + + * Line(side=wx.BOTTOM, color=wx.BLACK, width=1, toColor=None, space=0, pen=None) + + Draw a line on a given side of the block. If a pen is given, that is used to draw the + line (and the other parameters are ignored), otherwise a solid line (or a gradient + line is *toColor* is not None) of *width* pixels is drawn. + + """ + + def __init__(self): + """ + """ + self.padding = None + self.decorations = list() + self.font = wx.FFont(11, wx.FONTFAMILY_SWISS, face="Gill Sans") + self.textColor = None + self.textAlignment = wx.ALIGN_LEFT + self.alwaysCenter = False + self.canWrap = False + + #THINK: These attributes are only for grids. Should we have a GridBlockFormat object? + self.cellPadding = None + self.gridPen = None + + #---------------------------------------------------------------------------- + # Accessing + + def GetFont(self): + """ + Return the font used by this format + """ + return self.font + + def SetFont(self, font): + """ + Set the font used by this format + """ + self.font = font + + def GetTextAlignment(self): + """ + Return the alignment of text in this format + """ + return self.textAlignment + + def SetTextAlignment(self, alignment): + """ + Set the alignment of text in this format + """ + self.textAlignment = alignment + + def GetTextColor(self): + """ + Return the color of text in this format + """ + return self.textColor + + def SetTextColor(self, color): + """ + Set the color of text in this format + """ + self.textColor = color + + def GetPadding(self): + """ + Get the padding around this format + """ + return self.padding + + def SetPadding(self, padding): + """ + Set the padding around this format + + Padding is either a single numeric (indicating the values on all sides) + or a collection of paddings [left, top, right, bottom] + """ + self.padding = self._MakePadding(padding) + + def GetCellPadding(self): + """ + Get the padding around cells in this format + """ + return self.cellPadding + + def SetCellPadding(self, padding): + """ + Set the padding around cells in this format + + Padding is either a single numeric (indicating the values on all sides) + or a collection of paddings [left, top, right, bottom] + """ + self.cellPadding = self._MakePadding(padding) + + def GetGridPen(self): + """ + Return the pen used to draw a grid in this format + """ + return self.gridPen + + def SetGridPen(self, pen): + """ + Set the pen used to draw a grid in this format + """ + self.gridPen = pen + if self.gridPen: + # Other styles don't produce nice joins + self.gridPen.SetCap(wx.CAP_BUTT) + self.gridPen.SetJoin(wx.JOIN_MITER) + + def _MakePadding(self, padding): + try: + if len(padding) < 4: + return (tuple(padding) + (0, 0, 0, 0))[:4] + else: + return padding + except TypeError: + return (padding,) * 4 + + def GetAlwaysCenter(self): + """ + Return if the text controlled by this format should always be centered? + """ + return self.alwaysCenter + + def SetAlwaysCenter(self, value): + """ + Remember if the text controlled by this format should always be centered? + """ + self.alwaysCenter = value + + def GetCanWrap(self): + """ + Return if the text controlled by this format can wrap to cover more than one line? + """ + return self.canWrap + + def SetCanWrap(self, value): + """ + Remember if the text controlled by this format can wrap to cover more than one line? + """ + self.canWrap = value + + Font = property(GetFont, SetFont) + Padding = property(GetPadding, SetPadding) + TextAlignment = property(GetTextAlignment, SetTextAlignment) + TextColor = property(GetTextColor, SetTextColor) + CellPadding = property(GetCellPadding, SetCellPadding) + GridPen = property(GetGridPen, SetGridPen) + AlwaysCenter = property(GetAlwaysCenter, SetAlwaysCenter) + CanWrap = property(GetCanWrap, SetCanWrap) + + # Misspellers of the world Untie! + # Ok, ok... there're not really misspellings - just alternatives :) + TextAlign = property(GetTextAlignment, SetTextAlignment) + TextColour = property(GetTextColor, SetTextColor) + + #---------------------------------------------------------------------------- + # Calculations + + def CalculateCellPadding(self): + """ + Return a four-tuple that indicates pixel padding (left, top, right, bottom) + around cells in this format + """ + if self.CellPadding: + cellPadding = list(self.CellPadding) + else: + cellPadding = 0, 0, 0, 0 + + if self.GridPen: + penFactor = int((self.GridPen.GetWidth()+1)/2) + cellPadding = [x + penFactor for x in cellPadding] + + return cellPadding + + #---------------------------------------------------------------------------- + # Decorations + + def Add(self, decoration): + """ + Add the given decoration to those adorning blocks with this format + """ + self.decorations.append(decoration) + + + def Line(self, side=wx.BOTTOM, color=wx.BLACK, width=1, toColor=None, space=0, pen=None): + """ + Add a line to our decorations. + If a pen is given, we use a straight Line decoration, otherwise we use a + coloured rectangle + """ + if pen: + self.Add(LineDecoration(side, pen, space)) + else: + self.Add(RectangleDecoration(side, None, color, toColor, width, space)) + + + def Background(self, color=wx.BLUE, toColor=None, space=0): + """ + Add a coloured background to the block + """ + self.Add(RectangleDecoration(color=color, toColor=toColor, space=space)) + + + def Frame(self, pen=None, space=0): + """ + Add a rectangle around this block + """ + self.Add(RectangleDecoration(pen=pen, space=space)) + + #---------------------------------------------------------------------------- + # Commands + + def SubtractPadding(self, bounds): + """ + Subtract any padding used by this format from the given bounds + """ + if self.padding is None: + return bounds + else: + return RectUtils.InsetRect(bounds, self.padding) + + + def SubtractDecorations(self, dc, bounds): + """ + Subtract any space used by our decorations from the given bounds + """ + for x in self.decorations: + bounds = x.SubtractFrom(dc, bounds) + return bounds + + + def DrawDecorations(self, dc, bounds, block, over): + """ + Draw our decorations on the given block + """ + for x in self.decorations: + if x.IsDrawOver() == over: + x.DrawDecoration(dc, bounds, block) + + +#---------------------------------------------------------------------------- + +class Block(object): + """ + A Block is a portion of a report that will stack vertically with other + Block. A report consists of several Blocks. + """ + + def __init__(self, engine=None): + self.engine = engine # This is also set when the block is added to a print engine + + #---------------------------------------------------------------------------- + # Accessing + + def GetFont(self): + """ + Return Font that should be used to draw text in this block + """ + return self.GetFormat().GetFont() + + + def GetTextColor(self): + """ + Return Colour that should be used to draw text in this block + """ + return self.GetFormat().GetTextColor() + + + def GetFormat(self): + """ + Return the BlockFormat object that controls the formatting of this block + """ + return self.engine.GetNamedFormat(self.__class__.__name__[:-5]) + + + def GetReducedBlockBounds(self, dc, bounds=None): + """ + Return the bounds of this block once padding and decoration have taken their toll. + """ + bounds = bounds or list(self.GetWorkBounds()) + fmt = self.GetFormat() + bounds = fmt.SubtractPadding(bounds) + bounds = fmt.SubtractDecorations(dc, bounds) + return bounds + + + def GetWorkBounds(self): + """ + Return the boundaries of the work area for this block + """ + return self.engine.workBounds + + + def IsColumnHeadingsOnEachPage(self): + """ + Should the column headers be report at the top of each new page? + """ + return self.engine.reportFormat.IsColumnHeadingsOnEachPage + + + def IncludeImages(self): + """ + Should the images from the ListCtrl be printed in the report? + """ + return self.engine.reportFormat.IncludeImages + + + def IsShrinkToFit(self): + """ + Should row blocks be shrunk to fit within the width of a page? + """ + return self.engine.reportFormat.IsShrinkToFit + + + def IsUseSubstitution(self): + """ + Should the text values printed by this block have substitutions performed before being printed? + + This allows, for example, footers to have '%(currentPage)d of %(totalPages)d' + """ + return True + + + #---------------------------------------------------------------------------- + # Calculating + + def CalculateExtrasHeight(self, dc): + """ + Return the height of the padding and decorations themselves + """ + return 0 - RectUtils.Height(self.GetReducedBlockBounds(dc, [0, 0, 0, 0])) + + + def CalculateExtrasWidth(self, dc): + """ + Return the width of the padding and decorations themselves + """ + return 0 - RectUtils.Width(self.GetReducedBlockBounds(dc, [0, 0, 0, 0])) + + + def CalculateHeight(self, dc): + """ + Return the heights of this block in pixels + """ + return -1 + + + def CalculateTextHeight(self, dc, txt, bounds=None, font=None): + """ + Return the height of the given txt in pixels + """ + bounds = bounds or self.GetReducedBlockBounds(dc) + font = font or self.GetFont() + dc.SetFont(font) + if self.GetFormat().CanWrap: + return WordWrapRenderer.CalculateHeight(dc, txt, RectUtils.Width(bounds)) + else: + # Calculate the height of one line. The 1000 pixel width + # ensures that 'Wy' doesn't wrap, which might happen if bounds is narrow + return WordWrapRenderer.CalculateHeight(dc, "Wy", 1000) + + + def CanFit(self, height): + """ + Can this block fit into the remaining work area on the page? + """ + return height <= RectUtils.Height(self.GetWorkBounds()) + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + if not self.ShouldPrint(): + return True + + bounds = self.CalculateBounds(dc) + if not self.CanFit(RectUtils.Height(bounds)): + return False + + if self.engine.shouldDrawBlocks: + self.PreDraw(dc, bounds) + self.Draw(dc, bounds) + self.PostDraw(dc, bounds) + + self.ChangeWorkBoundsBy(RectUtils.Height(bounds)) + return True + + + def ShouldPrint(self): + """ + Should this block be printed? + """ + # If this block does not have a format, it is simply skipped + return self.GetFormat() is not None + + + def CalculateBounds(self, dc): + """ + Calculate the bounds of this block + """ + height = self.CalculateHeight(dc) + bounds = list(self.GetWorkBounds()) + bounds = RectUtils.SetHeight(bounds, height) + return bounds + + + def ChangeWorkBoundsBy(self, amt): + """ + Move the top of our work bounds down by the given amount + """ + RectUtils.MoveTopBy(self.engine.workBounds, amt) + + + def Draw(self, dc, bounds): + """ + Draw this block and its decorations allowing for any padding + """ + fmt = self.GetFormat() + decorationBounds = fmt.SubtractPadding(bounds) + fmt.DrawDecorations(dc, decorationBounds, self, False) + textBounds = fmt.SubtractDecorations(dc, list(decorationBounds)) + self.DrawSelf(dc, textBounds) + fmt.DrawDecorations(dc, decorationBounds, self, True) + + + def PreDraw(self, dc, bounds): + """ + Executed before any drawing is done + """ + pass + + + def DrawSelf(self, dc, bounds): + """ + Do the actual work of rendering this block. + """ + pass + + + def PostDraw(self, dc, bounds): + """ + Executed after drawing has completed + """ + pass + + + def DrawText(self, dc, txt, bounds, font=None, alignment=wx.ALIGN_LEFT, valignment=wx.ALIGN_CENTRE, + image=None, color=None, canWrap=True, imageIndex=-1, listCtrl=None): + """ + Draw the given text in the given DC according to the given characteristics. + + This is the workhorse text drawing method for our reporting engine. + + The *font*, *alignment*, and *color* attributes are applied to the drawn text. + + If *image* is not None, it will be drawn to the left of the text. All text is indented + by the width of the image, even if the text is multi-line. + + If *imageIndex* is 0 or more and *listCtrl* is not None, the corresponding image from + the ListCtrl's small image list will be drawn to the left of the text. + + If *canWrap* is True, the text will be wrapped to fit within the given bounds. If it is False, + then the first line of *txt* will be truncated at the edge of the given *bounds*. + """ + GAP_BETWEEN_IMAGE_AND_TEXT = 4 + + def _CalcBitmapPosition(r, height): + if valignment == wx.ALIGN_TOP: + return RectUtils.Top(r) + elif valignment == wx.ALIGN_CENTER: + return RectUtils.CenterY(r) - height / 2 + elif valignment == wx.ALIGN_BOTTOM: + return RectUtils.Bottom(r) - height + else: + return RectUtils.Top(r) + + # Draw any image + if image: + y = _CalcBitmapPosition(bounds, image.Height) + dc.DrawBitmap(image, RectUtils.Left(bounds), y) + RectUtils.MoveLeftBy(bounds, image.GetWidth()+GAP_BETWEEN_IMAGE_AND_TEXT) + elif listCtrl and imageIndex >=0: + imageList = listCtrl.GetImageList(wx.IMAGE_LIST_SMALL) + y = _CalcBitmapPosition(bounds, imageList.GetSize(0)[1]) + imageList.Draw(imageIndex, dc, RectUtils.Left(bounds), y, wx.IMAGELIST_DRAW_TRANSPARENT) + RectUtils.MoveLeftBy(bounds, imageList.GetSize(0)[0]+GAP_BETWEEN_IMAGE_AND_TEXT) + + # Draw the text + dc.SetFont(font or self.GetFont()) + dc.SetTextForeground(color or self.GetTextColor() or wx.BLACK) + if canWrap: + WordWrapRenderer.DrawString(dc, txt, bounds, alignment, valignment) + else: + WordWrapRenderer.DrawTruncatedString(dc, txt, bounds, alignment, valignment) + + + def PerformSubstitutions(self, strings): + """ + Substituted % markers in the given collection of strings. + """ + info = self.engine.GetSubstitutionInfo() + try: + # 'strings' could be a single string or a list of strings + try: + return strings % info + except TypeError: + return [x % info for x in strings] + except ValueError: + # We don't die if we get a substitution error - we just ignore it + return strings + +#---------------------------------------------------------------------------- + +class TextBlock(Block): + """ + A TextBlock prints a string objects. + """ + + def ShouldPrint(self): + """ + Should this block be printed? + """ + # If the block has no text, it should not be printed + if self.GetText(): + return Block.ShouldPrint(self) + else: + return False + + + def GetText(self): + return "Missing GetText() in class %s" % self.__class__.__name__ + + + def GetSubstitutedText(self): + """ + Return the text for this block after all markers have been substituted + """ + if self.IsUseSubstitution(): + return self.PerformSubstitutions(self.GetText()) + else: + return self.GetText() + + + def CalculateHeight(self, dc): + """ + Return the heights of this block in pixels + """ + textHeight = self.CalculateTextHeight(dc, self.GetSubstitutedText()) + extras = self.CalculateExtrasHeight(dc) + return textHeight + extras + + + def DrawSelf(self, dc, bounds): + """ + Do the actual work of rendering this block. + """ + fmt = self.GetFormat() + self.DrawText(dc, self.GetSubstitutedText(), bounds, canWrap=fmt.CanWrap, alignment=fmt.TextAlignment) + + +#---------------------------------------------------------------------------- + +class CellBlock(Block): + """ + A CellBlock is a Block whose data is presented in a cell format. + """ + + def __init__(self): + self.scale = 1 + self.oldScale = 1 + + #---------------------------------------------------------------------------- + # Accessing - Subclasses should override + + def GetCellWidths(self): + """ + Return a list of the widths of the cells in this block. + """ + return list() + + def GetTexts(self): + """ + Return a list of the texts that should be drawn with the cells + """ + return list() + + def GetAlignments(self): + """ + Return a list indicating how the text within each cell is aligned. + """ + return list() + + def GetImages(self): + """ + Return a list of the images that should be drawn in each cell + """ + return list() + + + #---------------------------------------------------------------------------- + # Accessing + + + def GetCombinedLists(self): + """ + Return a collection of Buckets that hold all the values of the + subclass-overridable collections above + """ + buckets = [Bucket(cellWidth=x, text="", align=None, image=None) for x in self.GetCellWidths()] + for (i, x) in enumerate(self.GetSubstitutedTexts()): + buckets[i].text = x + for (i, x) in enumerate(self.GetAlignments()): + buckets[i].align = x + + if self.IncludeImages(): + for (i, x) in enumerate(self.GetImages()): + buckets[i].image = x + + # Calculate where the cell contents should be drawn + cellPadding = self.GetFormat().CalculateCellPadding() + for x in buckets: + x.innerCellWidth = max(0, x.cellWidth - (cellPadding[0] + cellPadding[2])) + + return buckets + + + def GetListCtrl(self): + """ + Return the ListCtrl that is behind this cell block. + """ + return None + + + def GetSubstitutedTexts(self): + """ + Return a list of the texts that should be drawn with the cells + """ + if self.IsUseSubstitution(): + return self.PerformSubstitutions(self.GetTexts()) + else: + return self.GetTexts() + + + #---------------------------------------------------------------------------- + # Calculating + + def CalculateHeight(self, dc): + """ + Return the heights of this block in pixels + """ + GAP_BETWEEN_IMAGE_AND_TEXT = 4 + + # If cells can wrap, figure out the tallest, otherwise we just figure out the height of one line + if self.GetFormat().CanWrap: + font = self.GetFont() + height = 0 + for x in self.GetCombinedLists(): + textWidth = x.innerCellWidth + if self.GetListCtrl() and x.image != -1: + imageList = self.GetListCtrl().GetImageList(wx.IMAGE_LIST_SMALL) + textWidth -= imageList.GetSize(0)[0]+GAP_BETWEEN_IMAGE_AND_TEXT + bounds = [0, 0, textWidth, 99999] + height = max(height, self.CalculateTextHeight(dc, x.text, bounds, font)) + else: + height = self.CalculateTextHeight(dc, "Wy") + + # We also have to allow for cell padding, on top of the normal padding and decorations + cellPadding = self.GetFormat().CalculateCellPadding() + return height + cellPadding[1] + cellPadding[3] + self.CalculateExtrasHeight(dc) + + + def CalculateWidth(self, dc): + """ + Calculate the total width of this block (cells plus padding) + """ + return sum(x.cellWidth for x in self.GetCombinedLists()) + self.CalculateExtrasWidth(dc) + + #---------------------------------------------------------------------------- + # Commands + + def CalculateBounds(self, dc): + """ + Calculate the bounds of this block + """ + height = self.CalculateHeight(dc) + bounds = list(self.GetWorkBounds()) + bounds = RectUtils.SetHeight(bounds, height) + bounds = RectUtils.SetWidth(bounds, self.CalculateWidth(dc)) + bounds = RectUtils.MultiplyOrigin(bounds, 1 / self.scale) + return bounds + + #def CanFit(self, height): + # height *= self.scale + # return Block.CanFit(self, height) + + def ChangeWorkBoundsBy(self, height): + """ + Change our workbounds by our scaled height + """ + Block.ChangeWorkBoundsBy(self, height * self.scale) + + + def PreDraw(self, dc, bounds): + """ + Apply our scale before performing any drawing + """ + self.oldScale = dc.GetUserScale() + dc.SetUserScale(self.scale * self.oldScale[0], self.scale * self.oldScale[1]) + + + def PostDraw(self, dc, bounds): + """ + Restore the scaling to what it used to be + """ + dc.SetUserScale(*self.oldScale) + + + def DrawSelf(self, dc, bounds): + """ + Do the actual work of rendering this block. + """ + cellFmt = self.GetFormat() + cellPadding = cellFmt.CalculateCellPadding() + combined = self.GetCombinedLists() + + # Calculate cell boundaries + cell = list(bounds) + RectUtils.SetWidth(cell, 0) + for x in combined: + RectUtils.SetLeft(cell, RectUtils.Right(cell)) + RectUtils.SetWidth(cell, x.cellWidth) + x.cell = list(cell) + + # Draw each cell + font = self.GetFont() + for x in combined: + cellBounds = RectUtils.InsetRect(x.cell, cellPadding) + self.DrawText(dc, x.text, cellBounds, font, x.align, imageIndex=x.image, + canWrap=cellFmt.CanWrap, listCtrl=self.GetListCtrl()) + + if cellFmt.GridPen and combined: + dc.SetPen(cellFmt.GridPen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + + top = RectUtils.Top(bounds) + bottom = RectUtils.Bottom(bounds) + + # Draw the interior dividers + for x in combined[:-1]: + right = RectUtils.Right(x.cell) + dc.DrawLine(right, top, right, bottom) + + # Draw the surrounding frame + left = RectUtils.Left(combined[0].cell) + right = RectUtils.Right(combined[-1].cell) + dc.DrawRectangle(left, top, right-left, bottom-top) + + +#---------------------------------------------------------------------------- + +class ThreeCellBlock(CellBlock): + """ + A ThreeCellBlock divides its space evenly into three cells. + """ + + def GetCellWidths(self): + """ + Return a list of the widths of the cells in this block + """ + # We divide the available space between the non-empty cells + numNonEmptyTexts = sum(1 for x in self.GetTexts() if x) + if not numNonEmptyTexts: + return (0, 0, 0) + + widths = list() + width = round(RectUtils.Width(self.GetWorkBounds()) / numNonEmptyTexts) + for x in self.GetTexts(): + if x: + widths.append(width) + else: + widths.append(0) + return widths + + + def GetAlignments(self): + """ + Return a list indicating how the text within each cell is aligned. + """ + return (wx.ALIGN_LEFT, wx.ALIGN_CENTER_HORIZONTAL, wx.ALIGN_RIGHT) + +#---------------------------------------------------------------------------- + +class ReportBlock(Block): + """ + A ReportBlock is boot strap Block that represents an entire report. + """ + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + if not self.engine.listCtrls: + return True + + # Print each ListView. Each list but the first starts on a separate page + self.engine.AddBlock(ListBlock(*self.engine.listCtrls[0])) + for (lv, title) in self.engine.listCtrls[1:]: + self.engine.AddBlock(PageBreakBlock()) + self.engine.AddBlock(ListBlock(lv, title)) + + return True + + +#---------------------------------------------------------------------------- + +class PageHeaderBlock(ThreeCellBlock): + """ + A PageHeaderBlock appears at the top of every page. + """ + + def GetTexts(self): + """ + Return the array of texts to be printed in the cells + """ + return self.engine.pageHeader + + + +#---------------------------------------------------------------------------- + +class PageFooterBlock(ThreeCellBlock): + """ + A PageFooterBlock appears at the bottom of every page. + """ + + def GetTexts(self): + """ + Return the array of texts to be printed in the cells + """ + return self.engine.pageFooter + + + #---------------------------------------------------------------------------- + # Printing + + + def CalculateBounds(self, dc): + """ + Calculate the bounds of this block + """ + # Draw the footer at the bottom of the page + height = self.CalculateHeight(dc) + bounds = list(self.GetWorkBounds()) + return [RectUtils.Left(bounds), RectUtils.Bottom(bounds) - height, + RectUtils.Width(bounds), height] + + + def ChangeWorkBoundsBy(self, height): + """ + The footer changes the bottom of the work bounds + """ + RectUtils.MoveBottomBy(self.engine.workBounds, -height) + + +#---------------------------------------------------------------------------- + +class PageBreakBlock(Block): + """ + A PageBreakBlock acts a page break. + """ + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + + # Completely fill the remaining area on the page, forcing a page break + bounds = self.GetWorkBounds() + self.ChangeWorkBoundsBy(RectUtils.Height(bounds)) + + return True + +#---------------------------------------------------------------------------- + +class RunningBlockPusher(Block): + """ + A RunningBlockPusher pushes (or pops) a running block onto the stack when it is executed. + """ + + def __init__(self, block, push=True): + """ + """ + self.block = block + self.push = push + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + if self.push: + self.engine.AddRunningBlock(self.block) + else: + self.engine.RemoveRunningBlock(self.block) + + return True + +#---------------------------------------------------------------------------- + +class ListBlock(Block): + """ + A ListBlock handles the printing of an entire ListCtrl. + """ + + def __init__(self, lv, title): + self.lv = lv + self.title = title + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + + cellWidths = self.CalculateCellWidths() + boundsWidth = RectUtils.Width(self.GetWorkBounds()) + + # Break the list into vertical slices. Each one but the first will be placed on a + # new page. + first = True + for (left, right) in self.CalculateSlices(boundsWidth, cellWidths): + if not first: + self.engine.AddBlock(PageBreakBlock()) + self.engine.AddBlock(ListHeaderBlock(self.lv, self.title)) + self.engine.AddBlock(ListSliceBlock(self.lv, left, right, cellWidths)) + self.engine.AddBlock(ListFooterBlock(self.lv, "")) + first = False + + return True + + + def CalculateCellWidths(self): + """ + Return a list of the widths of the cells in this lists + """ + columnHeaderFmt = self.engine.GetNamedFormat("ColumnHeader") + cellPadding = columnHeaderFmt.CalculateCellPadding() + padding = cellPadding[0] + cellPadding[2] + return [self.lv.GetColumnWidth(i) + padding for i in range(self.lv.GetColumnCount())] + + + def CalculateSlices(self, maxWidth, columnWidths): + """ + Return a list of integer pairs, where each pair represents + the left and right columns that can fit into the width of one page + """ + firstColumn = 0 + + # If a GroupListView has a column just for the expand/collapse, don't include it + if hasattr(self.lv, "useExpansionColumn") and self.lv.useExpansionColumn: + firstColumn = 1 + + # If we are shrinking to fit or all the columns fit, just return all columns + if self.IsShrinkToFit() or (sum(columnWidths)) <= maxWidth: + return [ [firstColumn, len(columnWidths)-1] ] + + pairs = list() + left = firstColumn + right = firstColumn + while right < len(columnWidths): + if (sum(columnWidths[left:right+1])) > maxWidth: + if left == right: + pairs.append([left, right]) + left += 1 + right += 1 + else: + pairs.append([left, right-1]) + left = right + else: + right += 1 + + if left < len(columnWidths): + pairs.append([left, right-1]) + + return pairs + + +#---------------------------------------------------------------------------- + +class ListHeaderBlock(TextBlock): + """ + A ListHeaderBlock is the title that comes before an ListCtrl. + """ + + def __init__(self, lv, title): + self.lv = lv + self.title = title + + def GetText(self): + """ + Return the text that will be printed in this block + """ + return self.title + +#---------------------------------------------------------------------------- + +class ListFooterBlock(TextBlock): + """ + A ListFooterBlock is the text that comes after an ListCtrl. + """ + + def __init__(self, lv, text): + self.lv = lv + self.text = text + + def GetText(self): + """ + Return the text that will be printed in this block + """ + return self.text + + +#---------------------------------------------------------------------------- + +class GroupTitleBlock(TextBlock): + """ + A GroupTitleBlock is the title that comes before a list group. + """ + + def __init__(self, lv, group): + self.lv = lv + self.group = group + + def GetText(self): + """ + Return the text that will be printed in this block + """ + return self.group.title + +#---------------------------------------------------------------------------- + +class ListSliceBlock(Block): + """ + A ListSliceBlock prints a vertical slice of an ListCtrl. + """ + + def __init__(self, lv, left, right, allCellWidths): + self.lv = lv + self.left = left + self.right = right + self.allCellWidths = allCellWidths + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + # If we are shrinking our cells, calculate the shrink factor + if self.IsShrinkToFit(): + scale = self.CalculateShrinkToFit(dc) + else: + scale = 1 + + headerBlock = ColumnHeaderBlock(self.lv, self.left, self.right, scale, self.allCellWidths) + self.engine.AddBlock(headerBlock) + + if self.IsColumnHeadingsOnEachPage(): + self.engine.AddBlock(RunningBlockPusher(headerBlock)) + + # Are we printing a GroupListView? + # We can't use isinstance() since ObjectListView may not be installed + if hasattr(self.lv, "GetShowGroups") and self.lv.GetShowGroups(): + self.engine.AddBlock(GroupListRowsBlock(self.lv, self.left, self.right, scale, self.allCellWidths)) + else: + self.engine.AddBlock(ListRowsBlock(self.lv, self.left, self.right, scale, self.allCellWidths)) + + if self.IsColumnHeadingsOnEachPage(): + self.engine.AddBlock(RunningBlockPusher(headerBlock, False)) + + return True + + def CalculateShrinkToFit(self, dc): + """ + How much do we have to shrink our rows by to fit onto the page? + """ + block = ColumnHeaderBlock(self.lv, self.left, self.right, 1, self.allCellWidths) + block.engine = self.engine + rowWidth = block.CalculateWidth(dc) + boundsWidth = RectUtils.Width(self.GetWorkBounds()) + + if rowWidth > boundsWidth: + return float(boundsWidth) / float(rowWidth) + else: + return 1 + +#---------------------------------------------------------------------------- + +class ColumnBasedBlock(CellBlock): + """ + A ColumnBasedBlock is a cell block that takes its width from the + columns of a ListCtrl. + + This is an abstract class + """ + + def __init__(self, lv, left, right, scale, allCellWidths): + self.lv = lv + self.left = left + self.right = right + self.scale = scale + self.allCellWidths = allCellWidths + + #---------------------------------------------------------------------------- + # Accessing - Subclasses should override + + def GetListCtrl(self): + """ + Return the ListCtrl that is behind this cell block. + """ + return self.lv + + + def GetCellWidths(self): + """ + Return the widths of the cells used in this block + """ + #return [self.allCellWidths[i] for i in range(self.left, self.right+1)] + return self.allCellWidths[self.left:self.right+1] + + #---------------------------------------------------------------------------- + # Utiltities + + def GetColumnAlignments(self, lv, left, right): + """ + Return the alignments of the given slice of columns + """ + listAlignments = [lv.GetColumn(i).GetAlign() for i in range(left, right+1)] + mapping = { + wx.LIST_FORMAT_LEFT: wx.ALIGN_LEFT, + wx.LIST_FORMAT_RIGHT: wx.ALIGN_RIGHT, + wx.LIST_FORMAT_CENTRE: wx.ALIGN_CENTRE, + } + return [mapping[x] for x in listAlignments] + + + +#---------------------------------------------------------------------------- + +class ColumnHeaderBlock(ColumnBasedBlock): + """ + A ColumnHeaderBlock prints a portion of the columns header in a ListCtrl. + """ + + #---------------------------------------------------------------------------- + # Accessing + + def GetTexts(self): + """ + Return a list of the texts that should be drawn with the cells + """ + return [self.lv.GetColumn(i).GetText() for i in range(self.left, self.right+1)] + + def GetAlignments(self): + """ + Return a list indicating how the text within each cell is aligned. + """ + if self.GetFormat().AlwaysCenter: + return [wx.ALIGN_CENTRE for i in range(self.left, self.right+1)] + else: + return self.GetColumnAlignments(self.lv, self.left, self.right) + + def GetImages(self): + """ + Return a list of the images that should be drawn in each cell + """ + # For some reason, columns return 0 when they have no image, rather than -1 like they should + images = list() + for i in range(self.left, self.right+1): + imageIndex = self.lv.GetColumn(i).GetImage() + if imageIndex == 0: + imageIndex = -1 + images.append(imageIndex) + return images + + + def IsUseSubstitution(self): + """ + Should the text values printed by this block have substitutions performed before being printed? + + Normally, we don't want to substitute within values that comes from the ListCtrl. + """ + return False + + +#---------------------------------------------------------------------------- + +class ListRowsBlock(Block): + """ + A ListRowsBlock prints rows of an ListCtrl. + """ + + def __init__(self, lv, left, right, scale, allCellWidths): + """ + """ + self.lv = lv + self.left = left + self.right = right + self.scale = scale + self.allCellWidths = allCellWidths + self.currentIndex = 0 + self.totalRows = self.lv.GetItemCount() + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + # This block works by printing a single row and then rescheduling itself + # to print the remaining rows after the current row has finished. + + if self.currentIndex < self.totalRows: + self.engine.AddBlock(RowBlock(self.lv, self.currentIndex, self.left, self.right, self.scale, self.allCellWidths)) + self.currentIndex += 1 + self.engine.AddBlock(self) + + return True + +#---------------------------------------------------------------------------- + +class GroupListRowsBlock(Block): + """ + A GroupListRowsBlock prints rows of an GroupListView. + """ + + def __init__(self, lv, left, right, scale, allCellWidths): + """ + """ + self.lv = lv # Must be a GroupListView + self.left = left + self.right = right + self.scale = scale + self.allCellWidths = allCellWidths + + self.currentIndex = 0 + self.totalRows = self.lv.GetItemCount() + + #---------------------------------------------------------------------------- + # Commands + + def Print(self, dc): + """ + Print this Block. + + Return True if the Block has finished printing + """ + # This block works by printing a single row and then rescheduling itself + # to print the remaining rows after the current row has finished. + + if self.currentIndex >= self.totalRows: + return True + + # If GetObjectAt() return an object, then it's a normal row. + # Otherwise, if the innerList object isn't None, it must be a ListGroup + # We can't use isinstance(x, GroupListView) because ObjectListView may not be installed + if self.lv.GetObjectAt(self.currentIndex): + self.engine.AddBlock(RowBlock(self.lv, self.currentIndex, self.left, self.right, self.scale, self.allCellWidths)) + elif self.lv.innerList[self.currentIndex]: + self.engine.AddBlock(GroupTitleBlock(self.lv, self.lv.innerList[self.currentIndex])) + + # Schedule the printing of the remaining rows + self.currentIndex += 1 + self.engine.AddBlock(self) + + return True + + +#---------------------------------------------------------------------------- + +class RowBlock(ColumnBasedBlock): + """ + A RowBlock prints a vertical slice of a single row of an ListCtrl. + """ + + def __init__(self, lv, rowIndex, left, right, scale, allCellWidths): + self.lv = lv + self.rowIndex = rowIndex + self.left = left + self.right = right + self.scale = scale + self.allCellWidths = allCellWidths + + #---------------------------------------------------------------------------- + # Accessing + + + def GetFont(self): + """ + Return Font that should be used to draw text in this block + """ + font = None + if self.engine.reportFormat.UseListCtrlTextFormat and self.GetListCtrl(): + # Figure out what font is being used for this row in the list. + # Unfortunately, there is no one way to find this information: virtual mode lists + # have to be treated in a different manner from non-virtual lists. + listCtrl = self.GetListCtrl() + if listCtrl.IsVirtual(): + attr = listCtrl.OnGetItemAttr(self.rowIndex) + if attr and attr.HasFont(): + font = attr.GetFont() + else: + font = listCtrl.GetItemFont(self.rowIndex) + + if font and font.IsOk(): + return font + else: + return self.GetFormat().GetFont() + + + def GetTextColor(self): + """ + Return Colour that should be used to draw text in this block + """ + color = None + if self.engine.reportFormat.UseListCtrlTextFormat and self.GetListCtrl(): + # Figure out what text colour is being used for this row in the list. + # Unfortunately, there is no one way to find this information: virtual mode lists + # have to be treated in a different manner from non-virtual lists. + listCtrl = self.GetListCtrl() + if listCtrl.IsVirtual(): + attr = listCtrl.OnGetItemAttr(self.rowIndex) + if attr and attr.HasTextColour(): + color = attr.GetTextColour() + else: + color = listCtrl.GetItemTextColour(self.rowIndex) + + if color and color.IsOk(): + return color + else: + return self.GetFormat().GetTextColor() + + + def IsUseSubstitution(self): + """ + Should the text values printed by this block have substitutions performed before being printed? + + Normally, we don't want to substitute within values that comes from the ListCtrl. + """ + return False + + #---------------------------------------------------------------------------- + # Overrides for CellBlock + + def GetTexts(self): + """ + Return a list of the texts that should be drawn with the cells + """ + return [self.lv.GetItem(self.rowIndex, i).GetText() for i in range(self.left, self.right+1)] + + def GetAlignments(self): + """ + Return a list indicating how the text within each cell is aligned. + """ + return self.GetColumnAlignments(self.lv, self.left, self.right) + + def GetImages(self): + """ + Return a list of the images that should be drawn in each cell + """ + return [self.lv.GetItem(self.rowIndex, i).GetImage() for i in range(self.left, self.right+1)] + + #def GetTexts(self): + # """ + # Return a list of the texts that should be drawn with the cells + # """ + # modelObjects = self.lv.GetObjectAt(self.rowIndex) + # return [self.lv.GetStringValueAt(modelObjects, i) for i in range(self.left, self.right+1)] + # + #def GetAlignments(self): + # """ + # Return a list indicating how the text within each cell is aligned. + # """ + # return [self.lv.columns[i].GetAlignment() for i in range(self.left, self.right+1)] + # + #def GetImages(self): + # """ + # Return a list of the images that should be drawn in each cell + # """ + # modelObjects = self.lv.GetObjectAt(self.rowIndex) + # return [self.lv.GetImageAt(modelObjects, i) for i in range(self.left, self.right+1)] + + +#---------------------------------------------------------------------------- + +class Decoration(object): + """ + A Decoration add some visual effect to a Block (e.g. borders, + background image, watermark). They can also reserve a chunk of their Blocks + space for their own use. + + Decorations are added to a BlockFormat which is then applied to a ReportBlock. + + All the decorations for a block are drawn into the same area. If two decorations are + added, they will draw over the top of each other. This is normally what is expected, + but may sometimes be surprising. For example, if you add two Lines to the left of the + same block, they will draw over the top of each other. + + """ + + #---------------------------------------------------------------------------- + # Accessing + + def IsDrawOver(self): + """ + Return True if this decoration should be drawn over it's block. If not, + it will be drawn underneath + """ + return False + + #---------------------------------------------------------------------------- + # Commands + + def SubtractFrom(self, dc, bounds): + """ + Subtract the space used by this decoration from the given bounds + """ + return bounds + + def DrawDecoration(self, dc, bounds, block): + """ + Draw this decoration + """ + pass + +#---------------------------------------------------------------------------- + +class RectangleDecoration(Decoration): + """ + A RectangleDecoration draw a rectangle around or on the side of a block. + + The rectangle can be hollow, solid filled or gradient-filled. It can have + a frame drawn as well. + + """ + + def __init__(self, side=None, pen=None, color=None, toColor=None, width=0, space=0): + """ + If color is None, the rectangle will be hollow. + If toColor is None, the rectangle will be filled with "color" otherwise it will be + gradient-filled. + If pen is not None, the rectangle will be framed with that pen. + If no side is given, the rectangle will be drawn around the block. In that case, + space can be a list giving the space on each side. + """ + self.side = side + self.pen = pen + self.color = color + self.toColor = toColor + self.width = width + self.space = space + + #---------------------------------------------------------------------------- + # Commands + + def SubtractFrom(self, dc, bounds): + """ + Subtract the space used by this decoration from the given bounds + """ + if self.side is None: + return RectUtils.InsetBy(RectUtils.InsetBy(bounds, self.space), self.width) + + inset = self.space + self.width + if self.side == wx.LEFT: return RectUtils.MoveLeftBy(bounds, inset) + if self.side == wx.RIGHT: return RectUtils.MoveRightBy(bounds, -inset) + if self.side == wx.TOP: return RectUtils.MoveTopBy(bounds, inset) + if self.side == wx.BOTTOM: return RectUtils.MoveBottomBy(bounds, -inset) + return bounds + + + def DrawDecoration(self, dc, bounds, block): + """ + Draw this decoration + """ + rect = self._CalculateRect(bounds) + + if self.color: + if self.toColor is None: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(wx.Brush(self.color)) + dc.DrawRectangle(*rect) + else: + dc.GradientFillLinear(wx.Rect(*rect), self.color, self.toColor) + + if self.pen: + dc.SetPen(self.pen) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + dc.DrawRectangle(*rect) + + + def _CalculateRect(self, bounds): + """ + Calculate the rectangle that this decoration is going to paint + """ + if self.side is None: + return bounds + if self.side == wx.LEFT: + return (RectUtils.Left(bounds), RectUtils.Top(bounds), self.width, RectUtils.Height(bounds)) + if self.side == wx.RIGHT: + return (RectUtils.Right(bounds) - self.width, RectUtils.Top(bounds), self.width, RectUtils.Height(bounds)) + if self.side == wx.TOP: + return (RectUtils.Left(bounds), RectUtils.Top(bounds), RectUtils.Width(bounds), self.width) + if self.side == wx.BOTTOM: + return (RectUtils.Left(bounds), RectUtils.Bottom(bounds) - self.width, RectUtils.Width(bounds), self.width) + + return bounds + +#---------------------------------------------------------------------------- + +class LineDecoration(Decoration): + """ + A LineDecoration draws a line on the side of a decoration. + """ + + def __init__(self, side=wx.BOTTOM, pen=None, space=0): + self.side = side + self.pen = pen + self.space = space + + #---------------------------------------------------------------------------- + # Commands + + def SubtractFrom(self, dc, bounds): + """ + Subtract the space used by this decoration from the given bounds + """ + inset = self.space + if self.pen is not None: + inset += self.pen.GetWidth() + + if self.side == wx.LEFT: return RectUtils.MoveLeftBy(bounds, inset) + if self.side == wx.RIGHT: return RectUtils.MoveRightBy(bounds, -inset) + if self.side == wx.TOP: return RectUtils.MoveTopBy(bounds, inset) + if self.side == wx.BOTTOM: return RectUtils.MoveBottomBy(bounds, -inset) + return bounds + + + def DrawDecoration(self, dc, bounds, block): + """ + Draw this decoration + """ + if self.pen == None: + return + + if self.side == wx.LEFT: + pt1 = RectUtils.TopLeft(bounds) + pt2 = RectUtils.BottomLeft(bounds) + elif self.side == wx.RIGHT: + pt1 = RectUtils.TopRight(bounds) + pt2 = RectUtils.BottomRight(bounds) + elif self.side == wx.TOP: + pt1 = RectUtils.TopLeft(bounds) + pt2 = RectUtils.TopRight(bounds) + elif self.side == wx.BOTTOM: + pt1 = RectUtils.BottomLeft(bounds) + pt2 = RectUtils.BottomRight(bounds) + + dc.SetPen(self.pen) + dc.DrawLine(pt1[0], pt1[1], pt2[0], pt2[1]) + +#---------------------------------------------------------------------------- + +class WatermarkDecoration(Decoration): + """ + A WatermarkDecoration draws an angled line of text over each page. + + The watermark is rotated around the center of the page. + + If *over* is True, the watermark will be printed on top of the page. + Otherwise, it will be printed under the page. + + """ + + def __init__(self, text, font=None, color=None, angle=30, over=True): + """ + """ + self.text = text + self.color = color or wx.Color(128, 128, 128, 128) + self.font = font or wx.FFont(128, wx.SWISS, 0) + self.angle = angle + self.over = over + + def IsDrawOver(self): + """ + Should this decoration be drawn over the rest of page? + """ + return self.over + + def DrawDecoration(self, dc, bounds, block): + """ + Draw the decoration + """ + dc.SetFont(self.font) + dc.SetTextForeground(self.color) + + # Rotate the text around the center of the page + cx, cy = RectUtils.Center(bounds) + w, h = dc.GetTextExtent(self.text) + + x = cx - w/2 + y = cy - h/2 + (w/2 * math.sin(math.radians(self.angle))) + + dc.DrawRotatedText(self.text, x, y, self.angle) + +#---------------------------------------------------------------------------- + +class ImageDecoration(Decoration): + """ + A ImageDecoration draws an image over (or under) the given block. + + NB: Printer contexts do not honor alpha channels. + """ + + def __init__(self, image=None, horizontalAlign=wx.CENTER, verticalAlign=wx.CENTER, over=True): + """ + image must be either an wx.Image or a wx.Bitmap + """ + self.horizontalAlign = horizontalAlign + self.verticalAlign = verticalAlign + self.over = True + + self.bitmap = image + if isinstance(image, wx.Image): + self.bitmap = wx.BitmapFromImage(image) + + def DrawDecoration(self, dc, bounds, block): + """ + Draw the decoration + """ + if not self.bitmap: + return + + if self.horizontalAlign == wx.LEFT: + x = RectUtils.Left(bounds) + elif self.horizontalAlign == wx.RIGHT: + x = RectUtils.Right(bounds) - self.bitmap.Width + else: + x = RectUtils.CenterX(bounds) - self.bitmap.Width/2 + + if self.verticalAlign == wx.TOP: + y = RectUtils.Top(bounds) + elif self.verticalAlign == wx.BOTTOM: + y = RectUtils.Bottom(bounds) - self.bitmap.Height + else: + y = RectUtils.CenterY(bounds) - self.bitmap.Height/2 + + dc.DrawBitmap(self.bitmap, x, y, True) + + +#---------------------------------------------------------------------------- + +class Bucket(object): + """ + General purpose, hold-all data object + """ + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + strs = ["%s=%r" % kv for kv in self.__dict__.items()] + return "Bucket(" + ", ".join(strs) + ")" + +#---------------------------------------------------------------------------- + +class RectUtils: + """ + Static rectangle utilities. + + Rectangles are a list or tuple of 4-elements: [left, top, width, height] + """ + + #---------------------------------------------------------------------------- + # Accessing + + @staticmethod + def Left(r): return r[0] + + @staticmethod + def Top(r): return r[1] + + @staticmethod + def Width(r): return r[2] + + @staticmethod + def Height(r): return r[3] + + @staticmethod + def Right(r): return r[0] + r[2] + + @staticmethod + def Bottom(r): return r[1] + r[3] + + @staticmethod + def TopLeft(r): return [r[0], r[1]] + + @staticmethod + def TopRight(r): return [r[0] + r[2], r[1]] + + @staticmethod + def BottomLeft(r): return [r[0], r[1] + r[3]] + + @staticmethod + def BottomRight(r): return [r[0] + r[2], r[1] + r[3]] + + @staticmethod + def CenterX(r): return r[0] + r[2]/2 + + @staticmethod + def CenterY(r): return r[1] + r[3]/2 + + @staticmethod + def Center(r): return [r[0] + r[2]/2, r[1] + r[3]/2] + + #---------------------------------------------------------------------------- + # Modifying + + @staticmethod + def SetLeft(r, l): + r[0] = l + return r + + @staticmethod + def SetTop(r, t): + r[1] = t + return r + + @staticmethod + def SetWidth(r, w): + r[2] = w + return r + + @staticmethod + def SetHeight(r, h): + r[3] = h + return r + + @staticmethod + def MoveLeftBy(r, delta): + r[0] += delta + r[2] -= delta + return r + + @staticmethod + def MoveTopBy(r, delta): + r[1] += delta + r[3] -= delta + return r + + @staticmethod + def MoveRightBy(r, delta): + r[2] += delta + return r + + @staticmethod + def MoveBottomBy(r, delta): + r[3] += delta + return r + + #---------------------------------------------------------------------------- + # Calculations + + @staticmethod + def InsetBy(r, delta): + if delta is None: + return r + try: + delta[0] # is it indexable? + return RectUtils.InsetRect(r, delta) + except: + return RectUtils.InsetRect(r, (delta, delta, delta, delta)) + + @staticmethod + def InsetRect(r, r2): + if r2 is None: + return r + else: + return [r[0] + r2[0], r[1] + r2[1], r[2] - (r2[0] + r2[2]), r[3] - (r2[1] + r2[3])] + + @staticmethod + def MultiplyOrigin(r, factor): + return [r[0]*factor, r[1]*factor, r[2], r[3]] + +#---------------------------------------------------------------------------- +# TESTING ONLY +#---------------------------------------------------------------------------- + +if __name__ == '__main__': + import wx + from ObjectListView import ObjectListView, FastObjectListView, GroupListView, ColumnDefn + + # Where can we find the Example module? + import sys + sys.path.append("../Examples") + + import ExampleModel + import ExampleImages + + class MyFrame(wx.Frame): + def __init__(self, *args, **kwds): + kwds["style"] = wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + + self.panel = wx.Panel(self, -1) + #self.lv = ObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) + self.lv = GroupListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) + #self.lv = FastObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) + + sizer_2 = wx.BoxSizer(wx.VERTICAL) + sizer_2.Add(self.lv, 1, wx.ALL|wx.EXPAND, 4) + self.panel.SetSizer(sizer_2) + self.panel.Layout() + + sizer_1 = wx.BoxSizer(wx.VERTICAL) + sizer_1.Add(self.panel, 1, wx.EXPAND) + self.SetSizer(sizer_1) + self.Layout() + + musicImage = self.lv.AddImages(ExampleImages.getMusic16Bitmap(), ExampleImages.getMusic32Bitmap()) + artistImage = self.lv.AddImages(ExampleImages.getUser16Bitmap(), ExampleImages.getUser32Bitmap()) + + self.lv.SetColumns([ + ColumnDefn("Title", "left", 200, "title", imageGetter=musicImage), + ColumnDefn("Artist", "left", 150, "artist", imageGetter=artistImage), + ColumnDefn("Last Played", "left", 100, "lastPlayed"), + ColumnDefn("Size", "center", 100, "sizeInBytes"), + ColumnDefn("Rating", "center", 100, "rating"), + ]) + + #self.lv.CreateCheckStateColumn() + self.lv.SetSortColumn(self.lv.columns[2]) + self.lv.SetObjects(ExampleModel.GetTracks()) + + wx.CallLater(50, self.run) + + def run(self): + printer = ListCtrlPrinter(self.lv, "Playing with ListCtrl Printing") + printer.ReportFormat = ReportFormat.Normal() + printer.ReportFormat.WatermarkFormat(over=True) + printer.ReportFormat.IsColumnHeadingsOnEachPage = True + + #printer.ReportFormat.Page.Add(ImageDecoration(ExampleImages.getGroup32Bitmap(), wx.RIGHT, wx.BOTTOM)) + + #printer.PageHeader("%(listTitle)s") # nice idea but not possible at the moment + printer.PageHeader = "Playing with ListCtrl Printing" + printer.PageFooter = ("Bright Ideas Software", "%(date)s", "%(currentPage)d of %(totalPages)d") + printer.Watermark = "Sloth!" + + #printer.PageSetup() + printer.PrintPreview(self) + + + app = wx.PySimpleApp(0) + wx.InitAllImageHandlers() + frame_1 = MyFrame(None, -1, "") + app.SetTopWindow(frame_1) + frame_1.Show() + app.MainLoop() diff --git a/odmtools/lib/oldOlv/OLVEvent.py b/odmtools/lib/oldOlv/OLVEvent.py index 1c4c36f..a447cb4 100644 --- a/odmtools/lib/oldOlv/OLVEvent.py +++ b/odmtools/lib/oldOlv/OLVEvent.py @@ -1,279 +1,279 @@ -# -*- coding: utf-8 -*- -#---------------------------------------------------------------------------- -# Name: OLVEvent.py -# Author: Phillip Piper -# Created: 3 April 2008 -# SVN-ID: $Id$ -# Copyright: (c) 2008 by Phillip Piper, 2008 -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/08/18 JPP Added CELL_EDIT_STARTED and CELL_EDIT_FINISHED events -# 2008/07/16 JPP Added group-related events -# 2008/06/19 JPP Added EVT_SORT -# 2008/05/26 JPP Fixed pyLint annoyances -# 2008/04/04 JPP Initial version complete -#---------------------------------------------------------------------------- -# To do: - -""" -The OLVEvent module holds all the events used by the ObjectListView module. -""" - -__author__ = "Phillip Piper" -__date__ = "3 August 2008" -__version__ = "1.1" - -import wx - -#====================================================================== -# Event ids and types - -def _EventMaker(): - evt = wx.NewEventType() - return (evt, wx.PyEventBinder(evt)) - -(olv_EVT_CELL_EDIT_STARTING, EVT_CELL_EDIT_STARTING) = _EventMaker() -(olv_EVT_CELL_EDIT_STARTED, EVT_CELL_EDIT_STARTED) = _EventMaker() -(olv_EVT_CELL_EDIT_FINISHING, EVT_CELL_EDIT_FINISHING) = _EventMaker() -(olv_EVT_CELL_EDIT_FINISHED, EVT_CELL_EDIT_FINISHED) = _EventMaker() -(olv_EVT_SORT, EVT_SORT) = _EventMaker() -(olv_EVT_GROUP_CREATING, EVT_GROUP_CREATING) = _EventMaker() -(olv_EVT_GROUP_SORT, EVT_GROUP_SORT) = _EventMaker() -(olv_EVT_EXPANDING, EVT_EXPANDING) = _EventMaker() -(olv_EVT_EXPANDED, EVT_EXPANDED) = _EventMaker() -(olv_EVT_COLLAPSING, EVT_COLLAPSING) = _EventMaker() -(olv_EVT_COLLAPSED, EVT_COLLAPSED) = _EventMaker() - -#====================================================================== -# Event parameter blocks - -class VetoableEvent(wx.PyCommandEvent): - """ - Base class for all cancellable actions - """ - - def __init__(self, evtType): - wx.PyCommandEvent.__init__(self, evtType, -1) - self.veto = False - - def Veto(self, isVetoed=True): - """ - Veto (or un-veto) this event - """ - self.veto = isVetoed - - def IsVetoed(self): - """ - Has this event been vetod? - """ - return self.veto - -#---------------------------------------------------------------------------- - -class CellEditEvent(VetoableEvent): - """ - Base class for all cell editing events - """ - - def SetParameters(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor): - self.objectListView = objectListView - self.rowIndex = rowIndex - self.subItemIndex = subItemIndex - self.rowModel = rowModel - self.cellValue = cellValue - self.editor = editor - -#---------------------------------------------------------------------------- - -class CellEditStartedEvent(CellEditEvent): - """ - A cell has started to be edited. - - All attributes are public and should be considered read-only. - """ - - def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): - CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTED) - self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) - self.cellBounds = cellBounds - -#---------------------------------------------------------------------------- - -class CellEditStartingEvent(CellEditEvent): - """ - A cell is about to be edited. - - All attributes are public and should be considered read-only. Methods are provided for - information that can be changed. - """ - - def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): - CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTING) - self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) - self.cellBounds = cellBounds - self.newEditor = None - self.shouldConfigureEditor = True - - def SetCellBounds(self, rect): - """ - Change where the editor will be placed. - rect is a list: [left, top, width, height] - """ - self.cellBounds = rect - - def SetNewEditor(self, control): - """ - Use the given control instead of the editor. - """ - self.newEditor = control - - def DontConfigureEditor(self): - """ - The editor will not be automatically configured. - - If this is called, the event handler must handle all configuration. In - particular, it must configure its own event handlers to that - ObjectListView.CancelCellEdit() is called when the user presses Escape, - and ObjectListView.CommitCellEdit() is called when the user presses - Enter/Return or when the editor loses focus. """ - self.shouldConfigureEditor = False - -#---------------------------------------------------------------------------- - -class CellEditFinishedEvent(CellEditEvent): - """ - The user has finished editing a cell. - """ - def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, userCancelled): - CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHED) - self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, None, None) - self.userCancelled = userCancelled - -#---------------------------------------------------------------------------- - -class CellEditFinishingEvent(CellEditEvent): - """ - The user is finishing editing a cell. - - If this event is vetoed, the edit will be cancelled silently. This is useful if the - event handler completely handles the model updating. - """ - def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor, userCancelled): - CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHING) - self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) - self.userCancelled = userCancelled - - def SetCellValue(self, value): - """ - If the event handler sets the cell value here, this value will be used to update the model - object, rather than the value that was actually in the cell editor - """ - self.cellValue = value - -#---------------------------------------------------------------------------- - -class SortEvent(VetoableEvent): - """ - The user wants to sort the ObjectListView. - - When sortModelObjects is True, the event handler should sort the model objects used by - the given ObjectListView. If the "modelObjects" instance variable is not None, that - collection of objects should be sorted, otherwise the "modelObjects" collection of the - ObjectListView should be sorted. For a VirtualObjectListView, "modelObjects" will - always be None and the programmer must sort the object in whatever backing store is - being used. - - When sortModelObjects is False, the event handler must sort the actual ListItems in - the OLV. It does this by calling SortListItemsBy(), passing a callable that accepts - two model objects as parameters. sortModelObjects must be True for a - VirtualObjectListView (or a FastObjectListView) since virtual lists cannot sort items. - - If the handler calls Veto(), no further default processing will be done. - If the handler calls Handled(), default processing concerned with UI will be done. This - includes updating sort indicators. - If the handler calls neither of these, all default processing will be done. - """ - def __init__(self, objectListView, sortColumnIndex, sortAscending, sortModelObjects, modelObjects=None): - VetoableEvent.__init__(self, olv_EVT_SORT) - self.objectListView = objectListView - self.sortColumnIndex = sortColumnIndex - self.sortAscending = sortAscending - self.sortModelObjects = sortModelObjects - self.modelObjects = modelObjects - self.wasHandled = False - - def Handled(self, wasHandled=True): - """ - Indicate that the event handler has sorted the ObjectListView. - The OLV will handle other tasks like updating sort indicators - """ - self.wasHandled = wasHandled - -#---------------------------------------------------------------------------- - -class GroupCreationEvent(wx.PyCommandEvent): - """ - The user is about to create one or more groups. - - The handler can mess with the list of groups before they are created: change their - names, give them icons, remove them from the list to stop them being created - (that last behaviour could be very confusing for the users). - """ - def __init__(self, objectListView, groups): - wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_CREATING, -1) - self.objectListView = objectListView - self.groups = groups - -#---------------------------------------------------------------------------- - -class ExpandCollapseEvent(VetoableEvent): - """ - The user wants to expand or collapse one or more groups, or has just done so. - - If the handler calls Veto() for a Expanding or Collapsing event, - the expand/collapse action will be cancelled. - - Calling Veto() has no effect on a Expanded or Collapsed event - """ - def __init__(self, eventType, objectListView, groups, isExpand): - VetoableEvent.__init__(self, eventType) - self.objectListView = objectListView - self.groups = groups - self.isExpand = isExpand - -def ExpandingCollapsingEvent(objectListView, groups, isExpand): - if isExpand: - return ExpandCollapseEvent(olv_EVT_EXPANDING, objectListView, groups, True) - else: - return ExpandCollapseEvent(olv_EVT_COLLAPSING, objectListView, groups, False) - -def ExpandedCollapsedEvent(objectListView, groups, isExpand): - if isExpand: - return ExpandCollapseEvent(olv_EVT_EXPANDED, objectListView, groups, True) - else: - return ExpandCollapseEvent(olv_EVT_COLLAPSED, objectListView, groups, False) - -#---------------------------------------------------------------------------- - -class SortGroupsEvent(wx.PyCommandEvent): - """ - The given list of groups needs to be sorted. - - Both the groups themselves and the model objects within the group should be sorted. - - The handler should rearrange the list of groups in the order desired. - """ - def __init__(self, objectListView, groups, sortColumn, sortAscending): - wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_SORT, -1) - self.objectListView = objectListView - self.groups = groups - self.sortColumn = sortColumn - self.sortAscending = sortAscending - self.wasHandled = False - - def Handled(self, wasHandled=True): - """ - Indicate that the event handler has sorted the groups. - """ - self.wasHandled = wasHandled +# -*- coding: utf-8 -*- +#---------------------------------------------------------------------------- +# Name: OLVEvent.py +# Author: Phillip Piper +# Created: 3 April 2008 +# SVN-ID: $Id$ +# Copyright: (c) 2008 by Phillip Piper, 2008 +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/08/18 JPP Added CELL_EDIT_STARTED and CELL_EDIT_FINISHED events +# 2008/07/16 JPP Added group-related events +# 2008/06/19 JPP Added EVT_SORT +# 2008/05/26 JPP Fixed pyLint annoyances +# 2008/04/04 JPP Initial version complete +#---------------------------------------------------------------------------- +# To do: + +""" +The OLVEvent module holds all the events used by the ObjectListView module. +""" + +__author__ = "Phillip Piper" +__date__ = "3 August 2008" +__version__ = "1.1" + +import wx + +#====================================================================== +# Event ids and types + +def _EventMaker(): + evt = wx.NewEventType() + return (evt, wx.PyEventBinder(evt)) + +(olv_EVT_CELL_EDIT_STARTING, EVT_CELL_EDIT_STARTING) = _EventMaker() +(olv_EVT_CELL_EDIT_STARTED, EVT_CELL_EDIT_STARTED) = _EventMaker() +(olv_EVT_CELL_EDIT_FINISHING, EVT_CELL_EDIT_FINISHING) = _EventMaker() +(olv_EVT_CELL_EDIT_FINISHED, EVT_CELL_EDIT_FINISHED) = _EventMaker() +(olv_EVT_SORT, EVT_SORT) = _EventMaker() +(olv_EVT_GROUP_CREATING, EVT_GROUP_CREATING) = _EventMaker() +(olv_EVT_GROUP_SORT, EVT_GROUP_SORT) = _EventMaker() +(olv_EVT_EXPANDING, EVT_EXPANDING) = _EventMaker() +(olv_EVT_EXPANDED, EVT_EXPANDED) = _EventMaker() +(olv_EVT_COLLAPSING, EVT_COLLAPSING) = _EventMaker() +(olv_EVT_COLLAPSED, EVT_COLLAPSED) = _EventMaker() + +#====================================================================== +# Event parameter blocks + +class VetoableEvent(wx.PyCommandEvent): + """ + Base class for all cancellable actions + """ + + def __init__(self, evtType): + wx.PyCommandEvent.__init__(self, evtType, -1) + self.veto = False + + def Veto(self, isVetoed=True): + """ + Veto (or un-veto) this event + """ + self.veto = isVetoed + + def IsVetoed(self): + """ + Has this event been vetod? + """ + return self.veto + +#---------------------------------------------------------------------------- + +class CellEditEvent(VetoableEvent): + """ + Base class for all cell editing events + """ + + def SetParameters(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor): + self.objectListView = objectListView + self.rowIndex = rowIndex + self.subItemIndex = subItemIndex + self.rowModel = rowModel + self.cellValue = cellValue + self.editor = editor + +#---------------------------------------------------------------------------- + +class CellEditStartedEvent(CellEditEvent): + """ + A cell has started to be edited. + + All attributes are public and should be considered read-only. + """ + + def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): + CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTED) + self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) + self.cellBounds = cellBounds + +#---------------------------------------------------------------------------- + +class CellEditStartingEvent(CellEditEvent): + """ + A cell is about to be edited. + + All attributes are public and should be considered read-only. Methods are provided for + information that can be changed. + """ + + def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): + CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTING) + self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) + self.cellBounds = cellBounds + self.newEditor = None + self.shouldConfigureEditor = True + + def SetCellBounds(self, rect): + """ + Change where the editor will be placed. + rect is a list: [left, top, width, height] + """ + self.cellBounds = rect + + def SetNewEditor(self, control): + """ + Use the given control instead of the editor. + """ + self.newEditor = control + + def DontConfigureEditor(self): + """ + The editor will not be automatically configured. + + If this is called, the event handler must handle all configuration. In + particular, it must configure its own event handlers to that + ObjectListView.CancelCellEdit() is called when the user presses Escape, + and ObjectListView.CommitCellEdit() is called when the user presses + Enter/Return or when the editor loses focus. """ + self.shouldConfigureEditor = False + +#---------------------------------------------------------------------------- + +class CellEditFinishedEvent(CellEditEvent): + """ + The user has finished editing a cell. + """ + def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, userCancelled): + CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHED) + self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, None, None) + self.userCancelled = userCancelled + +#---------------------------------------------------------------------------- + +class CellEditFinishingEvent(CellEditEvent): + """ + The user is finishing editing a cell. + + If this event is vetoed, the edit will be cancelled silently. This is useful if the + event handler completely handles the model updating. + """ + def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor, userCancelled): + CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHING) + self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) + self.userCancelled = userCancelled + + def SetCellValue(self, value): + """ + If the event handler sets the cell value here, this value will be used to update the model + object, rather than the value that was actually in the cell editor + """ + self.cellValue = value + +#---------------------------------------------------------------------------- + +class SortEvent(VetoableEvent): + """ + The user wants to sort the ObjectListView. + + When sortModelObjects is True, the event handler should sort the model objects used by + the given ObjectListView. If the "modelObjects" instance variable is not None, that + collection of objects should be sorted, otherwise the "modelObjects" collection of the + ObjectListView should be sorted. For a VirtualObjectListView, "modelObjects" will + always be None and the programmer must sort the object in whatever backing store is + being used. + + When sortModelObjects is False, the event handler must sort the actual ListItems in + the OLV. It does this by calling SortListItemsBy(), passing a callable that accepts + two model objects as parameters. sortModelObjects must be True for a + VirtualObjectListView (or a FastObjectListView) since virtual lists cannot sort items. + + If the handler calls Veto(), no further default processing will be done. + If the handler calls Handled(), default processing concerned with UI will be done. This + includes updating sort indicators. + If the handler calls neither of these, all default processing will be done. + """ + def __init__(self, objectListView, sortColumnIndex, sortAscending, sortModelObjects, modelObjects=None): + VetoableEvent.__init__(self, olv_EVT_SORT) + self.objectListView = objectListView + self.sortColumnIndex = sortColumnIndex + self.sortAscending = sortAscending + self.sortModelObjects = sortModelObjects + self.modelObjects = modelObjects + self.wasHandled = False + + def Handled(self, wasHandled=True): + """ + Indicate that the event handler has sorted the ObjectListView. + The OLV will handle other tasks like updating sort indicators + """ + self.wasHandled = wasHandled + +#---------------------------------------------------------------------------- + +class GroupCreationEvent(wx.PyCommandEvent): + """ + The user is about to create one or more groups. + + The handler can mess with the list of groups before they are created: change their + names, give them icons, remove them from the list to stop them being created + (that last behaviour could be very confusing for the users). + """ + def __init__(self, objectListView, groups): + wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_CREATING, -1) + self.objectListView = objectListView + self.groups = groups + +#---------------------------------------------------------------------------- + +class ExpandCollapseEvent(VetoableEvent): + """ + The user wants to expand or collapse one or more groups, or has just done so. + + If the handler calls Veto() for a Expanding or Collapsing event, + the expand/collapse action will be cancelled. + + Calling Veto() has no effect on a Expanded or Collapsed event + """ + def __init__(self, eventType, objectListView, groups, isExpand): + VetoableEvent.__init__(self, eventType) + self.objectListView = objectListView + self.groups = groups + self.isExpand = isExpand + +def ExpandingCollapsingEvent(objectListView, groups, isExpand): + if isExpand: + return ExpandCollapseEvent(olv_EVT_EXPANDING, objectListView, groups, True) + else: + return ExpandCollapseEvent(olv_EVT_COLLAPSING, objectListView, groups, False) + +def ExpandedCollapsedEvent(objectListView, groups, isExpand): + if isExpand: + return ExpandCollapseEvent(olv_EVT_EXPANDED, objectListView, groups, True) + else: + return ExpandCollapseEvent(olv_EVT_COLLAPSED, objectListView, groups, False) + +#---------------------------------------------------------------------------- + +class SortGroupsEvent(wx.PyCommandEvent): + """ + The given list of groups needs to be sorted. + + Both the groups themselves and the model objects within the group should be sorted. + + The handler should rearrange the list of groups in the order desired. + """ + def __init__(self, objectListView, groups, sortColumn, sortAscending): + wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_SORT, -1) + self.objectListView = objectListView + self.groups = groups + self.sortColumn = sortColumn + self.sortAscending = sortAscending + self.wasHandled = False + + def Handled(self, wasHandled=True): + """ + Indicate that the event handler has sorted the groups. + """ + self.wasHandled = wasHandled diff --git a/odmtools/lib/oldOlv/ObjectListView.py b/odmtools/lib/oldOlv/ObjectListView.py old mode 100755 new mode 100644 diff --git a/odmtools/lib/oldOlv/WordWrapRenderer.py b/odmtools/lib/oldOlv/WordWrapRenderer.py index 4920b3a..c4dd1c9 100644 --- a/odmtools/lib/oldOlv/WordWrapRenderer.py +++ b/odmtools/lib/oldOlv/WordWrapRenderer.py @@ -1,232 +1,232 @@ -# -*- coding: utf-8 -*- -#!/usr/bin/env python -#---------------------------------------------------------------------------- -# Name: WordWrapRenderer.py -# Author: Phillip Piper -# Created: 25 July 2008 -# SVN-ID: $Id$ -# Copyright: (c) 2008 by Phillip Piper, 2008 -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/07/25 JPP Initial version -#---------------------------------------------------------------------------- -# To do: - -""" -A WordWrapRenderer encapsulates the ability to draw and measure word wrapped -strings directly to a device context. - -It is meant to be good enough for general use. It is not suitable for typographic layout --- it does not handle kerning or ligatures. - -The DC passed to these methods cannot be a GraphicContext DC. These methods use -GetPartialTextExtents() which does not work with GCDC's (as of wx 2.8). - -""" - -import wx -import bisect -from wx.lib.wordwrap import wordwrap - -class WordWrapRenderer: - """ - This renderer encapsulates the logic need to draw and measure a word-wrapped - string within a given rectangle. - """ - - #---------------------------------------------------------------------------- - # Calculating - - @staticmethod - def CalculateHeight(dc, text, width): - """ - Calculate the height of the given text when fitted within the given width. - - Remember to set the font on the dc before calling this method. - """ - # There is a bug in the wordwrap routine where a string that needs truncated and - # that ends with a single space causes the method to throw an error (wx 2.8). - # Our simple, but not always accurate, is to remove trailing spaces. - # This won't catch single trailing space imbedded in a multiline string. - text = text.rstrip(' ') - - lines = wordwrap(text, width, dc, True) - (width, height, descent, externalLeading) = dc.GetFullTextExtent("Wy") - return (lines.count("\n")+1) * (height + externalLeading) - - - #---------------------------------------------------------------------------- - # Rendering - - @staticmethod - def DrawString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, allowClipping=False): - """ - Draw the given text word-wrapped within the given bounds. - - bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). - - If allowClipping is True, this method changes the clipping region so that no - text is drawn outside of the given bounds. - """ - if not text: - return - - if align == wx.ALIGN_CENTER: - align = wx.ALIGN_CENTER_HORIZONTAL - - if valign == wx.ALIGN_CENTER: - valign = wx.ALIGN_CENTER_VERTICAL - - # DrawLabel only accepts a wx.Rect - try: - bounds = wx.Rect(*bounds) - except: - pass - - if allowClipping: - clipper = wx.DCClipper(dc, bounds) - - # There is a bug in the wordwrap routine where a string that needs truncated and - # that ends with a single space causes the method to throw an error (wx 2.8). - # Our simple, but not always accurate, is to remove trailing spaces. - # This won't catch single trailing space imbedded in a multiline string. - text = text.rstrip(' ') - - lines = wordwrap(text, bounds[2], dc, True) - dc.DrawLabel(lines, bounds, align|valign) - - - @staticmethod - def DrawTruncatedString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, ellipse=wx.RIGHT, ellipseChars="..."): - """ - Draw the given text truncated to the given bounds. - - bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). - - If allowClipping is True, this method changes the clipping region so that no - text is drawn outside of the given bounds. - """ - if not text: - return - - if align == wx.ALIGN_CENTER: - align = wx.ALIGN_CENTER_HORIZONTAL - - if valign == wx.ALIGN_CENTER: - valign = wx.ALIGN_CENTER_VERTICAL - - try: - bounds = wx.Rect(*bounds) - except: - pass - lines = WordWrapRenderer._Truncate(dc, text, bounds[2], ellipse, ellipseChars) - dc.DrawLabel(lines, bounds, align|valign) - - - @staticmethod - def _Truncate(dc, text, maxWidth, ellipse, ellipseChars): - """ - Return a string that will fit within the given width. - """ - line = text.split("\n")[0] # only consider the first line - if not line: - return "" - - pte = dc.GetPartialTextExtents(line) - - # Does the whole thing fit within our given width? - stringWidth = pte[-1] - if stringWidth <= maxWidth: - return line - - # We (probably) have to ellipse the text so allow for ellipse - maxWidthMinusEllipse = maxWidth - dc.GetTextExtent(ellipseChars)[0] - - if ellipse == wx.LEFT: - i = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse) - return ellipseChars + line[i+1:] - - if ellipse == wx.CENTER: - i = bisect.bisect(pte, maxWidthMinusEllipse / 2) - j = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse / 2) - return line[:i] + ellipseChars + line[j+1:] - - if ellipse == wx.RIGHT: - i = bisect.bisect(pte, maxWidthMinusEllipse) - return line[:i] + ellipseChars - - # No ellipsing, just truncating is the default - i = bisect.bisect(pte, maxWidth) - return line[:i] - -#====================================================================== -# TESTING ONLY -#====================================================================== - -if __name__ == '__main__': - - class TestPanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, -1, style=wx.FULL_REPAINT_ON_RESIZE) - self.Bind(wx.EVT_PAINT, self.OnPaint) - - self.text = """This is Thisisareallylongwordtoseewhathappens the text to be drawn. It needs to be long to see if wrapping works. to long words. -This is on new line by itself. - -This should have a blank line in front of it but still wrap when we reach the edge. - -The bottom of the red rectangle should be immediately below this.""" - self.font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName="Gill Sans") - - def OnPaint(self, evt): - dc = wx.PaintDC(self) - inset = (20, 20, 20, 20) - rect = [inset[0], inset[1], self.GetSize().width-(inset[0]+inset[2]), self.GetSize().height-(inset[1]+inset[3])] - - # Calculate exactly how high the wrapped is going to be and put a frame around it. - dc.SetFont(self.font) - dc.SetPen(wx.RED_PEN) - rect[3] = WordWrapRenderer.CalculateHeight(dc, self.text, rect[2]) - dc.DrawRectangle(*rect) - WordWrapRenderer.DrawString(dc, self.text, rect, wx.ALIGN_LEFT) - #WordWrapRenderer.DrawTruncatedString(dc, self.text, rect, wx.ALIGN_CENTER_HORIZONTAL,s ellipse=wx.CENTER) - - #bmp = wx.EmptyBitmap(rect[0]+rect[2], rect[1]+rect[3]) - #mdc = wx.MemoryDC(bmp) - #mdc.SetBackground(wx.Brush("white")) - #mdc.Clear() - #mdc.SetFont(self.font) - #mdc.SetPen(wx.RED_PEN) - #rect[3] = WordWrapRenderer.CalculateHeight(mdc, self.text, rect[2]) - #mdc.DrawRectangle(*rect) - #WordWrapRenderer.DrawString(mdc, self.text, rect, wx.ALIGN_LEFT) - #del mdc - #dc = wx.ScreenDC() - #dc.DrawBitmap(bmp, 20, 20) - - class MyFrame(wx.Frame): - def __init__(self, *args, **kwds): - kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Frame.__init__(self, *args, **kwds) - - self.panel = wx.Panel(self, -1) - self.testPanel = TestPanel(self.panel) - - sizer_2 = wx.BoxSizer(wx.VERTICAL) - sizer_2.Add(self.testPanel, 1, wx.ALL|wx.EXPAND, 4) - self.panel.SetSizer(sizer_2) - self.panel.Layout() - - sizer_1 = wx.BoxSizer(wx.VERTICAL) - sizer_1.Add(self.panel, 1, wx.EXPAND) - self.SetSizer(sizer_1) - self.Layout() - - - app = wx.PySimpleApp(0) - wx.InitAllImageHandlers() - frame_1 = MyFrame(None, -1, "") - app.SetTopWindow(frame_1) - frame_1.Show() - app.MainLoop() +# -*- coding: utf-8 -*- +#!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: WordWrapRenderer.py +# Author: Phillip Piper +# Created: 25 July 2008 +# SVN-ID: $Id$ +# Copyright: (c) 2008 by Phillip Piper, 2008 +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/07/25 JPP Initial version +#---------------------------------------------------------------------------- +# To do: + +""" +A WordWrapRenderer encapsulates the ability to draw and measure word wrapped +strings directly to a device context. + +It is meant to be good enough for general use. It is not suitable for typographic layout +-- it does not handle kerning or ligatures. + +The DC passed to these methods cannot be a GraphicContext DC. These methods use +GetPartialTextExtents() which does not work with GCDC's (as of wx 2.8). + +""" + +import wx +import bisect +from wx.lib.wordwrap import wordwrap + +class WordWrapRenderer: + """ + This renderer encapsulates the logic need to draw and measure a word-wrapped + string within a given rectangle. + """ + + #---------------------------------------------------------------------------- + # Calculating + + @staticmethod + def CalculateHeight(dc, text, width): + """ + Calculate the height of the given text when fitted within the given width. + + Remember to set the font on the dc before calling this method. + """ + # There is a bug in the wordwrap routine where a string that needs truncated and + # that ends with a single space causes the method to throw an error (wx 2.8). + # Our simple, but not always accurate, is to remove trailing spaces. + # This won't catch single trailing space imbedded in a multiline string. + text = text.rstrip(' ') + + lines = wordwrap(text, width, dc, True) + (width, height, descent, externalLeading) = dc.GetFullTextExtent("Wy") + return (lines.count("\n")+1) * (height + externalLeading) + + + #---------------------------------------------------------------------------- + # Rendering + + @staticmethod + def DrawString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, allowClipping=False): + """ + Draw the given text word-wrapped within the given bounds. + + bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). + + If allowClipping is True, this method changes the clipping region so that no + text is drawn outside of the given bounds. + """ + if not text: + return + + if align == wx.ALIGN_CENTER: + align = wx.ALIGN_CENTER_HORIZONTAL + + if valign == wx.ALIGN_CENTER: + valign = wx.ALIGN_CENTER_VERTICAL + + # DrawLabel only accepts a wx.Rect + try: + bounds = wx.Rect(*bounds) + except: + pass + + if allowClipping: + clipper = wx.DCClipper(dc, bounds) + + # There is a bug in the wordwrap routine where a string that needs truncated and + # that ends with a single space causes the method to throw an error (wx 2.8). + # Our simple, but not always accurate, is to remove trailing spaces. + # This won't catch single trailing space imbedded in a multiline string. + text = text.rstrip(' ') + + lines = wordwrap(text, bounds[2], dc, True) + dc.DrawLabel(lines, bounds, align|valign) + + + @staticmethod + def DrawTruncatedString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, ellipse=wx.RIGHT, ellipseChars="..."): + """ + Draw the given text truncated to the given bounds. + + bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). + + If allowClipping is True, this method changes the clipping region so that no + text is drawn outside of the given bounds. + """ + if not text: + return + + if align == wx.ALIGN_CENTER: + align = wx.ALIGN_CENTER_HORIZONTAL + + if valign == wx.ALIGN_CENTER: + valign = wx.ALIGN_CENTER_VERTICAL + + try: + bounds = wx.Rect(*bounds) + except: + pass + lines = WordWrapRenderer._Truncate(dc, text, bounds[2], ellipse, ellipseChars) + dc.DrawLabel(lines, bounds, align|valign) + + + @staticmethod + def _Truncate(dc, text, maxWidth, ellipse, ellipseChars): + """ + Return a string that will fit within the given width. + """ + line = text.split("\n")[0] # only consider the first line + if not line: + return "" + + pte = dc.GetPartialTextExtents(line) + + # Does the whole thing fit within our given width? + stringWidth = pte[-1] + if stringWidth <= maxWidth: + return line + + # We (probably) have to ellipse the text so allow for ellipse + maxWidthMinusEllipse = maxWidth - dc.GetTextExtent(ellipseChars)[0] + + if ellipse == wx.LEFT: + i = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse) + return ellipseChars + line[i+1:] + + if ellipse == wx.CENTER: + i = bisect.bisect(pte, maxWidthMinusEllipse / 2) + j = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse / 2) + return line[:i] + ellipseChars + line[j+1:] + + if ellipse == wx.RIGHT: + i = bisect.bisect(pte, maxWidthMinusEllipse) + return line[:i] + ellipseChars + + # No ellipsing, just truncating is the default + i = bisect.bisect(pte, maxWidth) + return line[:i] + +#====================================================================== +# TESTING ONLY +#====================================================================== + +if __name__ == '__main__': + + class TestPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent, -1, style=wx.FULL_REPAINT_ON_RESIZE) + self.Bind(wx.EVT_PAINT, self.OnPaint) + + self.text = """This is Thisisareallylongwordtoseewhathappens the text to be drawn. It needs to be long to see if wrapping works. to long words. +This is on new line by itself. + +This should have a blank line in front of it but still wrap when we reach the edge. + +The bottom of the red rectangle should be immediately below this.""" + self.font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName="Gill Sans") + + def OnPaint(self, evt): + dc = wx.PaintDC(self) + inset = (20, 20, 20, 20) + rect = [inset[0], inset[1], self.GetSize().width-(inset[0]+inset[2]), self.GetSize().height-(inset[1]+inset[3])] + + # Calculate exactly how high the wrapped is going to be and put a frame around it. + dc.SetFont(self.font) + dc.SetPen(wx.RED_PEN) + rect[3] = WordWrapRenderer.CalculateHeight(dc, self.text, rect[2]) + dc.DrawRectangle(*rect) + WordWrapRenderer.DrawString(dc, self.text, rect, wx.ALIGN_LEFT) + #WordWrapRenderer.DrawTruncatedString(dc, self.text, rect, wx.ALIGN_CENTER_HORIZONTAL,s ellipse=wx.CENTER) + + #bmp = wx.EmptyBitmap(rect[0]+rect[2], rect[1]+rect[3]) + #mdc = wx.MemoryDC(bmp) + #mdc.SetBackground(wx.Brush("white")) + #mdc.Clear() + #mdc.SetFont(self.font) + #mdc.SetPen(wx.RED_PEN) + #rect[3] = WordWrapRenderer.CalculateHeight(mdc, self.text, rect[2]) + #mdc.DrawRectangle(*rect) + #WordWrapRenderer.DrawString(mdc, self.text, rect, wx.ALIGN_LEFT) + #del mdc + #dc = wx.ScreenDC() + #dc.DrawBitmap(bmp, 20, 20) + + class MyFrame(wx.Frame): + def __init__(self, *args, **kwds): + kwds["style"] = wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + + self.panel = wx.Panel(self, -1) + self.testPanel = TestPanel(self.panel) + + sizer_2 = wx.BoxSizer(wx.VERTICAL) + sizer_2.Add(self.testPanel, 1, wx.ALL|wx.EXPAND, 4) + self.panel.SetSizer(sizer_2) + self.panel.Layout() + + sizer_1 = wx.BoxSizer(wx.VERTICAL) + sizer_1.Add(self.panel, 1, wx.EXPAND) + self.SetSizer(sizer_1) + self.Layout() + + + app = wx.PySimpleApp(0) + wx.InitAllImageHandlers() + frame_1 = MyFrame(None, -1, "") + app.SetTopWindow(frame_1) + frame_1.Show() + app.MainLoop() diff --git a/odmtools/lib/oldOlv/t.py b/odmtools/lib/oldOlv/t.py index a54cdd1..f3a546c 100644 --- a/odmtools/lib/oldOlv/t.py +++ b/odmtools/lib/oldOlv/t.py @@ -1,228 +1,228 @@ -# -*- coding: utf-8 -*- -#!/usr/bin/env python -#---------------------------------------------------------------------------- -# Name: ListCtrlPrinter.py -# Author: Phillip Piper -# Created: 17 July 2008 -# SVN-ID: $Id$ -# Copyright: (c) 2008 by Phillip Piper, 2008 -# License: wxWindows license -#---------------------------------------------------------------------------- -# Change log: -# 2008/07/17 JPP Initial version -#---------------------------------------------------------------------------- -# To do: -# - scaling -# - gradients -# - images -# - attributes from ListCtrl -# - persistence of ReportFormat -# - use wx.wordwrap and DrawLabel -# - investigate DrawImageLabel - -""" -An ListCtrlPrinter takes an ObjectListView and turns it into a pretty report. - -As always, the goal is for this to be as easy to use as possible. A typical -usage should be as simple as:: - - printer = ListCtrlPrinter(self.myOlv, "My Report Title") - printer.PrintPreview() - -""" - -import wx - -#====================================================================== - -class TestPrinter(wx.Printout): - - def __init__(self, margins=None): - """ - """ - wx.Printout.__init__(self, "The title") - - self.printData = wx.PrintData() - self.printData.SetPaperId(wx.PAPER_A4) - self.printData.SetOrientation(wx.PORTRAIT) - self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) - self.printData.SetNoCopies(1) - self.margins = margins or (wx.Point(0, 0), wx.Point(0, 0)) - - - #---------------------------------------------------------------------------- - # Accessing - - def HasPage(self, page): - return page <= 3 - - def GetPageInfo(self): - return (1, 3, 1, 1) - - #---------------------------------------------------------------------------- - # Commands - - - def PageSetup(self): - data = wx.PageSetupDialogData() - data.SetPrintData(self.printData) - data.SetDefaultMinMargins(True) - data.SetMarginTopLeft(self.margins[0]) - data.SetMarginBottomRight(self.margins[1]) - dlg = wx.PageSetupDialog(None, data) - if dlg.ShowModal() == wx.ID_OK: - data = dlg.GetPageSetupData() - self.printData = wx.PrintData(data.GetPrintData()) - self.printData.SetPaperId(data.GetPaperId()) - self.margins = (data.GetMarginTopLeft(), - data.GetMarginBottomRight()) - dlg.Destroy() - - def PrintPreview(self, parent=None, title="ObjectListView Print Preview", bounds=(20, 50, 800, 800)): - """ - Show a Print Preview of this report - """ - data = wx.PrintDialogData(self.printData) - t = TestPrinter(self.margins) - t2 = TestPrinter(self.margins) - self.preview = wx.PrintPreview(t, t2, data) - - if not self.preview.Ok(): - return False - - pfrm = wx.PreviewFrame(self.preview, parent, title) - - pfrm.Initialize() - pfrm.SetPosition(bounds[0:2]) - pfrm.SetSize(bounds[2:4]) - pfrm.Show(True) - - return True - - - def DoPrint(self, parent=None): - """ - Send the report to the configured printer - """ - pdd = wx.PrintDialogData(self.printData) - printer = wx.Printer(pdd) - - if printer.Print(parent, self, True): - self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) - else: - wx.MessageBox("There was a problem printing.\nPerhaps your current printer is not set correctly?", "Printing", wx.OK) - - printout.Destroy() - - - #---------------------------------------------------------------------------- - # Event handlers - - def OnPreparePrinting(self): - """ - Prepare for printing. This event is sent before any of the others - """ - print "OnPreparePrinting" - print "self.GetDC() = %s" % self.GetDC() - - def OnBeginDocument(self, start, end): - """ - Begin printing one copy of the document. Return False to cancel the job - """ - print "OnBeginDocument(%d, %d)" % (start, end) - if not super(TestPrinter, self).OnBeginDocument(start, end): - return False - - return True - - def OnEndDocument(self): - print "OnEndDocument" - super(TestPrinter, self).OnEndDocument() - - def OnBeginPrinting(self): - print "OnBeginPrinting" - super(TestPrinter, self).OnBeginPrinting() - - def OnEndPrinting(self): - print "OnEndPrinting" - super(TestPrinter, self).OnEndPrinting() - - def OnPrintPage(self, page): - print "OnPrintPage(%d)" % page - dc = self.GetDC() - self.CalculateScale(dc) - self.CalculateLayout(dc) - dc.SetPen(wx.BLACK_PEN) - dc.SetBrush(wx.TRANSPARENT_BRUSH) - bounds = (self.x1, self.y1, self.x2-self.x1, self.y2-self.y1) - print bounds - print self.pageHeight - dc.DrawRectangle(*bounds) - font = wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL) - dc.SetFont(font) - dc.DrawText("this is a string", bounds[0], bounds[1]) - - def CalculateScale(self, dc): - # Scaling the DC to screen size - ppiPrinterX, ppiPrinterY = self.GetPPIPrinter() - ppiScreenX, ppiScreenY = self.GetPPIScreen() - logScale = float(ppiPrinterX)/float(ppiScreenX) - pw, ph = self.GetPageSizePixels() # Adjusting scale - dw, dh = dc.GetSize() - scale = logScale * float(dw)/float(pw) - dc.SetUserScale(scale, scale) - self.logUnitsMM = float(ppiPrinterX)/(logScale*25.4) - - def CalculateLayout(self, dc): - topLeft, bottomRight = self.margins - dw, dh = dc.GetSize() - self.x1 = topLeft.x * self.logUnitsMM - self.y1 = topLeft.y * self.logUnitsMM - self.x2 = dc.DeviceToLogicalYRel(dw) - bottomRight.x * self.logUnitsMM - self.y2 = dc.DeviceToLogicalYRel(dh) - bottomRight.y * self.logUnitsMM - self.pageHeight = self.y2 - self.y1 - 2*self.logUnitsMM - - - -#====================================================================== -# TESTING ONLY -#====================================================================== - -if __name__ == '__main__': - import wx - - # Where can we find the Example module? - import sys - - class MyFrame(wx.Frame): - def __init__(self, *args, **kwds): - kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Frame.__init__(self, *args, **kwds) - - self.panel = wx.Panel(self, -1) - self.olv = wx.ListCtrl(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) - - sizer_2 = wx.BoxSizer(wx.VERTICAL) - sizer_2.Add(self.olv, 1, wx.ALL|wx.EXPAND, 4) - self.panel.SetSizer(sizer_2) - self.panel.Layout() - - sizer_1 = wx.BoxSizer(wx.VERTICAL) - sizer_1.Add(self.panel, 1, wx.EXPAND) - self.SetSizer(sizer_1) - self.Layout() - - wx.CallLater(50, self.run) - - def run(self): - printer = TestPrinter() - printer.PageSetup() - printer.PrintPreview(self) - - - app = wx.PySimpleApp(0) - wx.InitAllImageHandlers() - frame_1 = MyFrame(None, -1, "") - app.SetTopWindow(frame_1) - frame_1.Show() - app.MainLoop() +# -*- coding: utf-8 -*- +#!/usr/bin/env python +#---------------------------------------------------------------------------- +# Name: ListCtrlPrinter.py +# Author: Phillip Piper +# Created: 17 July 2008 +# SVN-ID: $Id$ +# Copyright: (c) 2008 by Phillip Piper, 2008 +# License: wxWindows license +#---------------------------------------------------------------------------- +# Change log: +# 2008/07/17 JPP Initial version +#---------------------------------------------------------------------------- +# To do: +# - scaling +# - gradients +# - images +# - attributes from ListCtrl +# - persistence of ReportFormat +# - use wx.wordwrap and DrawLabel +# - investigate DrawImageLabel + +""" +An ListCtrlPrinter takes an ObjectListView and turns it into a pretty report. + +As always, the goal is for this to be as easy to use as possible. A typical +usage should be as simple as:: + + printer = ListCtrlPrinter(self.myOlv, "My Report Title") + printer.PrintPreview() + +""" + +import wx + +#====================================================================== + +class TestPrinter(wx.Printout): + + def __init__(self, margins=None): + """ + """ + wx.Printout.__init__(self, "The title") + + self.printData = wx.PrintData() + self.printData.SetPaperId(wx.PAPER_A4) + self.printData.SetOrientation(wx.PORTRAIT) + self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) + self.printData.SetNoCopies(1) + self.margins = margins or (wx.Point(0, 0), wx.Point(0, 0)) + + + #---------------------------------------------------------------------------- + # Accessing + + def HasPage(self, page): + return page <= 3 + + def GetPageInfo(self): + return (1, 3, 1, 1) + + #---------------------------------------------------------------------------- + # Commands + + + def PageSetup(self): + data = wx.PageSetupDialogData() + data.SetPrintData(self.printData) + data.SetDefaultMinMargins(True) + data.SetMarginTopLeft(self.margins[0]) + data.SetMarginBottomRight(self.margins[1]) + dlg = wx.PageSetupDialog(None, data) + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetPageSetupData() + self.printData = wx.PrintData(data.GetPrintData()) + self.printData.SetPaperId(data.GetPaperId()) + self.margins = (data.GetMarginTopLeft(), + data.GetMarginBottomRight()) + dlg.Destroy() + + def PrintPreview(self, parent=None, title="ObjectListView Print Preview", bounds=(20, 50, 800, 800)): + """ + Show a Print Preview of this report + """ + data = wx.PrintDialogData(self.printData) + t = TestPrinter(self.margins) + t2 = TestPrinter(self.margins) + self.preview = wx.PrintPreview(t, t2, data) + + if not self.preview.Ok(): + return False + + pfrm = wx.PreviewFrame(self.preview, parent, title) + + pfrm.Initialize() + pfrm.SetPosition(bounds[0:2]) + pfrm.SetSize(bounds[2:4]) + pfrm.Show(True) + + return True + + + def DoPrint(self, parent=None): + """ + Send the report to the configured printer + """ + pdd = wx.PrintDialogData(self.printData) + printer = wx.Printer(pdd) + + if printer.Print(parent, self, True): + self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) + else: + wx.MessageBox("There was a problem printing.\nPerhaps your current printer is not set correctly?", "Printing", wx.OK) + + printout.Destroy() + + + #---------------------------------------------------------------------------- + # Event handlers + + def OnPreparePrinting(self): + """ + Prepare for printing. This event is sent before any of the others + """ + print "OnPreparePrinting" + print "self.GetDC() = %s" % self.GetDC() + + def OnBeginDocument(self, start, end): + """ + Begin printing one copy of the document. Return False to cancel the job + """ + print "OnBeginDocument(%d, %d)" % (start, end) + if not super(TestPrinter, self).OnBeginDocument(start, end): + return False + + return True + + def OnEndDocument(self): + print "OnEndDocument" + super(TestPrinter, self).OnEndDocument() + + def OnBeginPrinting(self): + print "OnBeginPrinting" + super(TestPrinter, self).OnBeginPrinting() + + def OnEndPrinting(self): + print "OnEndPrinting" + super(TestPrinter, self).OnEndPrinting() + + def OnPrintPage(self, page): + print "OnPrintPage(%d)" % page + dc = self.GetDC() + self.CalculateScale(dc) + self.CalculateLayout(dc) + dc.SetPen(wx.BLACK_PEN) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + bounds = (self.x1, self.y1, self.x2-self.x1, self.y2-self.y1) + print bounds + print self.pageHeight + dc.DrawRectangle(*bounds) + font = wx.Font(10, wx.TELETYPE, wx.NORMAL, wx.NORMAL) + dc.SetFont(font) + dc.DrawText("this is a string", bounds[0], bounds[1]) + + def CalculateScale(self, dc): + # Scaling the DC to screen size + ppiPrinterX, ppiPrinterY = self.GetPPIPrinter() + ppiScreenX, ppiScreenY = self.GetPPIScreen() + logScale = float(ppiPrinterX)/float(ppiScreenX) + pw, ph = self.GetPageSizePixels() # Adjusting scale + dw, dh = dc.GetSize() + scale = logScale * float(dw)/float(pw) + dc.SetUserScale(scale, scale) + self.logUnitsMM = float(ppiPrinterX)/(logScale*25.4) + + def CalculateLayout(self, dc): + topLeft, bottomRight = self.margins + dw, dh = dc.GetSize() + self.x1 = topLeft.x * self.logUnitsMM + self.y1 = topLeft.y * self.logUnitsMM + self.x2 = dc.DeviceToLogicalYRel(dw) - bottomRight.x * self.logUnitsMM + self.y2 = dc.DeviceToLogicalYRel(dh) - bottomRight.y * self.logUnitsMM + self.pageHeight = self.y2 - self.y1 - 2*self.logUnitsMM + + + +#====================================================================== +# TESTING ONLY +#====================================================================== + +if __name__ == '__main__': + import wx + + # Where can we find the Example module? + import sys + + class MyFrame(wx.Frame): + def __init__(self, *args, **kwds): + kwds["style"] = wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + + self.panel = wx.Panel(self, -1) + self.olv = wx.ListCtrl(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) + + sizer_2 = wx.BoxSizer(wx.VERTICAL) + sizer_2.Add(self.olv, 1, wx.ALL|wx.EXPAND, 4) + self.panel.SetSizer(sizer_2) + self.panel.Layout() + + sizer_1 = wx.BoxSizer(wx.VERTICAL) + sizer_1.Add(self.panel, 1, wx.EXPAND) + self.SetSizer(sizer_1) + self.Layout() + + wx.CallLater(50, self.run) + + def run(self): + printer = TestPrinter() + printer.PageSetup() + printer.PrintPreview(self) + + + app = wx.PySimpleApp(0) + wx.InitAllImageHandlers() + frame_1 = MyFrame(None, -1, "") + app.SetTopWindow(frame_1) + frame_1.Show() + app.MainLoop() diff --git a/odmtools/odmdata/__init__.py b/odmtools/odmdata/__init__.py index dd2a352..dfb89a2 100644 --- a/odmtools/odmdata/__init__.py +++ b/odmtools/odmdata/__init__.py @@ -14,7 +14,7 @@ from sample_medium_cv import SampleMediumCV from sample_type_cv import SampleTypeCV from series import Series -# from session_factory import SessionFactory +from session_factory import SessionFactory from site import Site from site_type_cv import SiteTypeCV from source import Source diff --git a/odmtools/odmdata/session_factory.py b/odmtools/odmdata/session_factory.py index 8afda48..0d2d488 100644 --- a/odmtools/odmdata/session_factory.py +++ b/odmtools/odmdata/session_factory.py @@ -1,57 +1,57 @@ -# from sqlalchemy import create_engine -# from sqlalchemy.orm import sessionmaker -# -# -# class SessionFactory(): -# def __init__(self, connection_string, echo): -# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# pool_size=20) -# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# connect_args={'connect_timeout': 1}) -# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# connect_args={'timeout': 1}) -# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# connect_args={'connect_timeout': 1}) -# -# ''' -# # Removing pool_timeout and max_overflow allowed the tests to pass -# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# pool_timeout=5, pool_size=20, max_overflow=0) -# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) -# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) -# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, -# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) -# ''' -# -# ''' -# # Old code -# class SessionFactory(): -# def __init__(self, connection_string, echo): -# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, -# #pool_size=20, -# pool_recycle=3600) -# -# # Create session maker -# self.Session = sessionmaker(bind=self.engine) -# -# def get_session(self): -# return self.Session() -# -# def __repr__(self): -# return "" % (self.engine) -# ''' -# -# # Create session maker -# self.Session = sessionmaker(bind=self.engine) -# self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) -# self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) -# self.my_test_Session = sessionmaker(bind=self.my_test_engine) -# -# def get_session(self): -# return self.Session() -# -# def __repr__(self): -# return "" % (self.engine) -# +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + + +class SessionFactory(): + def __init__(self, connection_string, echo): + self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + pool_size=20) + self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + connect_args={'connect_timeout': 1}) + self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + connect_args={'timeout': 1}) + self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + connect_args={'connect_timeout': 1}) + + ''' + # Removing pool_timeout and max_overflow allowed the tests to pass + self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + pool_timeout=5, pool_size=20, max_overflow=0) + self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) + self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) + self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, + pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) + ''' + + ''' + # Old code + class SessionFactory(): + def __init__(self, connection_string, echo): + self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, + #pool_size=20, + pool_recycle=3600) + + # Create session maker + self.Session = sessionmaker(bind=self.engine) + + def get_session(self): + return self.Session() + + def __repr__(self): + return "" % (self.engine) + ''' + + # Create session maker + self.Session = sessionmaker(bind=self.engine) + self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) + self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) + self.my_test_Session = sessionmaker(bind=self.my_test_engine) + + def get_session(self): + return self.Session() + + def __repr__(self): + return "" % (self.engine) + diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index b1a01b2..24b5e81 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -1,5 +1,5 @@ # CV imports -# from odmtools.odmdata import SessionFactory +from odmtools.odmdata import SessionFactory from odmtools.odmdata import VerticalDatumCV from odmtools.odmdata import SiteTypeCV from odmtools.odmdata import VariableNameCV @@ -16,87 +16,69 @@ from odmtools.odmdata import Qualifier from odmtools.odmdata import Unit from sqlalchemy import not_ -from odm2api.ODMconnection import SessionFactory -from odm2api.ODM2.services.readService import ReadODM2 -class CVService(): # change to readSerivice() +class CVService(): # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): - self._session_factory = SessionFactory(connection_string=connection_string, echo=debug) - self._edit_session = self._session_factory.getSession() + self._session_factory = SessionFactory(connection_string, debug) + self._edit_session = self._session_factory.get_session() self._debug = debug - self.read_service = ReadODM2(session_factory=self._session_factory, debug=self._debug) # Controlled Vocabulary get methods - # From ODM1 -> ODM2 Qualifier was changed to Annotations - def get_annotations(self, type): - return self.read_service.getAnnotations(type=type) + #return a list of all terms in the cv + def get_vertical_datum_cvs(self): + result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() + return result - def get_censor_code_cvs(self): - result = self._edit_session.query(CensorCodeCV).order_by(CensorCodeCV.term).all() + def get_samples(self): + result = self._edit_session.query(Sample).order_by(Sample.lab_sample_code).all() return result - def get_data_type_cvs(self): - result = self._edit_session.query(DataTypeCV).order_by(DataTypeCV.term).all() + def get_site_type_cvs(self): + result = self._edit_session.query(SiteTypeCV).order_by(SiteTypeCV.term).all() return result - def get_general_category_cvs(self): - result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() + def get_variable_name_cvs(self): + result = self._edit_session.query(VariableNameCV).order_by(VariableNameCV.term).all() return result def get_offset_type_cvs(self): - return self.read_service.getCVs(type="Spatial Offset Type") - - # From ODM1 -> ODM2 Quality Controlled Level was changed to Processing Level - def get_all_processing_levels(self): - self.read_service.getProcessingLevels() - - def get_all_method(self): # Rename to get_method_all - return self.read_service.getMethods(ids=None, codes=None, type=None) - - def get_method_by_id(self, method_id): - return self.read_service.getMethods(ids=method_id) - - def get_method_by_description(self, code): - return self.read_service.getMethods(codes=code) + result = self._edit_session.query(OffsetType).order_by(OffsetType.id).all() + return result - def get_processing_level_by_id(self, id): - self.read_service.getProcessingLevels(ids=id) + def get_speciation_cvs(self): + result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() + return result - def get_processing_level_by_code(self, code): - self.read_service.getProcessingLevels(codes=code) + def get_sample_medium_cvs(self): + result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() + return result - def get_samples(self): - result = self._edit_session.query(Sample).order_by(Sample.lab_sample_code).all() + def get_value_type_cvs(self): + result = self._edit_session.query(ValueTypeCV).order_by(ValueTypeCV.term).all() return result - def get_sample_medium_cvs(self): - return self.read_service.getCVs(type="Medium") + def get_data_type_cvs(self): + result = self._edit_session.query(DataTypeCV).order_by(DataTypeCV.term).all() + return result - def get_site_type_cvs(self): - return self.read_service.getCVs(type="Site Type") + def get_general_category_cvs(self): + result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() + return result - def get_speciation_cvs(self): - return self.read_service.getCVs(type="Speciation") + def get_censor_code_cvs(self): + result = self._edit_session.query(CensorCodeCV).order_by(CensorCodeCV.term).all() + return result def get_sample_type_cvs(self): result = self._edit_session.query(SampleTypeCV).order_by(SampleTypeCV.term).all() return result - # From ODM1 -> ODM2 Site was changed to Sampling Feature - def get_all_sites(self): - return self.read_service.getSamplingFeatures(ids=None, codes=None, uuids=None, type=None, wkt=None) - - def get_site_by_id(self, site_id): - return self.read_service.getSamplingFeatures(ids=site_id) - - def get_timeseries_result_values(self, type): - return self.read_service.getAnnotations(type=type) - def get_units(self): - self.read_service.getUnits(ids=None, name=None, type=None) + result = self._edit_session.query(Unit).all() + return result def get_units_not_uni(self): result = self._edit_session.query(Unit).filter(not_(Unit.name.contains('angstrom'))).all() @@ -108,31 +90,9 @@ def get_units_names(self): # return a single cv def get_unit_by_name(self, unit_name): - return self.read_service.getUnits(name=unit_name) + result = self._edit_session.query(Unit).filter_by(name=unit_name).first() + return result def get_unit_by_id(self, unit_id): - return self.read_service.getUnits(ids=unit_id) - - def get_value_type_cvs(self): - result = self._edit_session.query(ValueTypeCV).order_by(ValueTypeCV.term).all() + result = self._edit_session.query(Unit).filter_by(id=unit_id).first() return result - - def get_variable_name_cvs(self): - return self.read_service.getCVs(type="Variable Name") - - def get_vertical_datum_cvs(self): - return self.read_service.getCVs("Elevation Datum") - - def get_all_variables(self): - return self.read_service.getVariables() - - def get_variable_by_id(self, id): - return self.read_service.getVariables(ids=id) - - def get_variable_by_code(self, code): - return self.read_service.getVariables(codes=code) - - - - - diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 7f6b136..078e6fe 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -1,6 +1,10 @@ import logging + + from sqlalchemy import distinct, func -from odm2api.ODMconnection import SessionFactory # from odmtools.odmdata import SessionFactory + + +from odmtools.odmdata import SessionFactory from odmtools.odmdata import Site from odmtools.odmdata import Variable from odmtools.odmdata import Unit @@ -14,23 +18,20 @@ from odmtools.odmdata import ODMVersion from odmtools.common.logger import LoggerTool import pandas as pd -from odm2api.ODM2.services.createService import CreateODM2 # tool = LoggerTool() # logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) -logger = logging.getLogger('main') +logger =logging.getLogger('main') -class SeriesService(): # Change to createService +class SeriesService(): # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): - self._session_factory = SessionFactory(connection_string=connection_string, echo=debug) - self._edit_session = self._session_factory.getSession() + self._session_factory = SessionFactory(connection_string, debug) + self._edit_session = self._session_factory.get_session() self._debug = debug - self.create_service = CreateODM2(session_factory=self._session_factory, debug=self._debug) def reset_session(self): - # self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks - self._edit_session = self.create_service.getSession() + self._edit_session = self._session_factory.get_session() # Reset the session in order to prevent memory leaks def get_db_version(self): return self._edit_session.query(ODMVersion).first().version_number @@ -41,6 +42,15 @@ def get_db_version(self): # ##################### + # Site methods + def get_all_sites(self): + """ + + :return: List[Sites] + """ + return self._edit_session.query(Site).order_by(Site.code).all() + + def get_used_sites(self): """ Return a list of all sites that are being referenced in the Series Catalog Table @@ -49,7 +59,7 @@ def get_used_sites(self): try: site_ids = [x[0] for x in self._edit_session.query(distinct(Series.site_id)).all()] except: - return None + site_ids = None if not site_ids: return None @@ -60,6 +70,18 @@ def get_used_sites(self): return Sites + + def get_site_by_id(self, site_id): + """ + return a Site object that has an id=site_id + :param site_id: integer- the identification number of the site + :return: Sites + """ + try: + return self._edit_session.query(Site).filter_by(id=site_id).first() + except: + return None + # Variables methods def get_used_variables(self): """ @@ -83,6 +105,145 @@ def get_used_variables(self): return Variables + def get_all_variables(self): + """ + + :return: List[Variables] + """ + return self._edit_session.query(Variable).all() + + def get_variable_by_id(self, variable_id): + """ + + :param variable_id: int + :return: Variables + """ + try: + return self._edit_session.query(Variable).filter_by(id=variable_id).first() + except: + return None + + def get_variable_by_code(self, variable_code): + """ + + :param variable_code: str + :return: Variables + """ + try: + return self._edit_session.query(Variable).filter_by(code=variable_code).first() + except: + return None + + def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeUnits + """ + Finds all of variables at a site + :param site_code: str + :return: List[Variables] + """ + try: + var_ids = [x[0] for x in self._edit_session.query(distinct(Series.variable_id)).filter_by( + site_code=site_code).all()] + except: + var_ids = None + + variables = [] + for var_id in var_ids: + variables.append(self._edit_session.query(Variable).filter_by(id=var_id).first()) + + return variables + + # Unit methods + def get_all_units(self): + """ + + :return: List[Units] + """ + return self._edit_session.query(Unit).all() + + def get_unit_by_name(self, unit_name): + """ + + :param unit_name: str + :return: Units + """ + try: + return self._edit_session.query(Unit).filter_by(name=unit_name).first() + except: + return None + + def get_unit_by_id(self, unit_id): + """ + + :param unit_id: int + :return: Units + """ + try: + return self._edit_session.query(Unit).filter_by(id=unit_id).first() + except: + return None + + + def get_all_qualifiers(self): + """ + + :return: List[Qualifiers] + """ + result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() + return result + + def get_qualifier_by_code(self, code): + """ + + :return: Qualifiers + """ + result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() + return result + + def get_qualifiers_by_series_id(self, series_id): + """ + + :param series_id: + :return: + """ + subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( + Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() + return self._edit_session.query(Qualifier).join(subquery).distinct().all() + + #QCL methods + def get_all_qcls(self): + return self._edit_session.query(QualityControlLevel).all() + + def get_qcl_by_id(self, qcl_id): + try: + return self._edit_session.query(QualityControlLevel).filter_by(id=qcl_id).first() + except: + return None + + def get_qcl_by_code(self, qcl_code): + try: + return self._edit_session.query(QualityControlLevel).filter_by(code=qcl_code).first() + except: + return None + + # Method methods + def get_all_methods(self): + return self._edit_session.query(Method).all() + + def get_method_by_id(self, method_id): + try: + result = self._edit_session.query(Method).filter_by(id=method_id).first() + except: + result = None + return result + + def get_method_by_description(self, method_code): + try: + result = self._edit_session.query(Method).filter_by(description=method_code).first() + except: + result = None + logger.error("method not found") + return result + def get_offset_types_by_series_id(self, series_id): """ @@ -158,31 +319,6 @@ def get_series_from_filter(self): # Pass in probably a Series object, match it against the database pass - def get_all_qualifiers(self): - """ - - :return: List[Qualifiers] - """ - result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() - return result - - def get_qualifier_by_code(self, code): - """ - - :return: Qualifiers - """ - result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() - return result - - def get_qualifiers_by_series_id(self, series_id): - """ - - :param series_id: - :return: - """ - subquery = self._edit_session.query(DataValue.qualifier_id).outerjoin( - Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() - return self._edit_session.query(Qualifier).join(subquery).distinct().all() #Data Value Methods def get_values_by_series(self, series_id): @@ -398,6 +534,7 @@ def save_values(self, values): def create_new_series(self, data_values, site_id, variable_id, method_id, source_id, qcl_id): """ + :param data_values: :param site_id: :param variable_id: @@ -418,8 +555,9 @@ def create_new_series(self, data_values, site_id, variable_id, method_id, source self._edit_session.commit() return series - def create_method(self, description, link): # DONE + def create_method(self, description, link): """ + :param description: :param link: :return: @@ -429,23 +567,27 @@ def create_method(self, description, link): # DONE if link is not None: meth.link = link - self.create_service.createMethod(method=meth) - + self._edit_session.add(meth) + self._edit_session.commit() return meth - def create_variable_by_var(self, var): # DONE + def create_variable_by_var(self, var): """ + :param var: Variable Object :return: """ - - self.create_service.createVariable(var) - return var + try: + self._edit_session.add(var) + self._edit_session.commit() + return var + except: + return None def create_variable( self, code, name, speciation, variable_unit_id, sample_medium, value_type, is_regular, time_support, time_unit_id, data_type, - general_category, no_data_value): # DONE + general_category, no_data_value): """ :param code: @@ -476,10 +618,11 @@ def create_variable( var.general_category = general_category var.no_data_value = no_data_value - self.create_variable_by_var(var) + self._edit_session.add(var) + self._edit_session.commit() return var - def create_qcl(self, code, definition, explanation): # DONE + def create_qcl(self, code, definition, explanation): """ :param code: @@ -492,7 +635,8 @@ def create_qcl(self, code, definition, explanation): # DONE qcl.definition = definition qcl.explanation = explanation - self.create_service.createProcessingLevel(qcl) + self._edit_session.add(qcl) + self._edit_session.commit() return qcl diff --git a/odmtools/odmservices/service_manager.py b/odmtools/odmservices/service_manager.py old mode 100755 new mode 100644 index c106394..1ad5ff1 --- a/odmtools/odmservices/service_manager.py +++ b/odmtools/odmservices/service_manager.py @@ -13,7 +13,7 @@ from odmtools.controller import EditTools from export_service import ExportService from odmtools.lib.Appdirs.appdirs import user_config_dir -from odm2api.ODMconnection import SessionFactory #from odmtools.odmdata.session_factory import SessionFactory +from odmtools.odmdata.session_factory import SessionFactory # tool = LoggerTool() @@ -100,11 +100,12 @@ def add_connection(self, conn_dict): def testEngine(self, connection_string): s = SessionFactory(connection_string, echo=False) if 'mssql' in connection_string: - s.test_Session().execute("Select top 1 VariableCode From Variables") + s.ms_test_Session().execute("Select top 1 VariableCode From Variables") elif 'mysql' in connection_string: - s.test_Session().execute('Select "VariableCode" From Variables Limit 1') + s.my_test_Session().execute('Select "VariableCode" From Variables Limit 1') elif 'postgresql' in connection_string: - s.test_Session().execute('Select "VariableCode" From "Variables" Limit 1') + #s.psql_test_Session().execute('Select "VariableCode" From "ODM2"."Variables" Limit 1') + s.psql_test_Session().execute('Select "VariableCode" From "Variables" Limit 1') return True def test_connection(self, conn_dict): diff --git a/odmtools/view/clsAddPoints.py b/odmtools/view/clsAddPoints.py old mode 100755 new mode 100644 diff --git a/odmtools/view/clsSeriesSelector.py b/odmtools/view/clsSeriesSelector.py old mode 100755 new mode 100644 index fa4a00f..6cd9cdc --- a/odmtools/view/clsSeriesSelector.py +++ b/odmtools/view/clsSeriesSelector.py @@ -1,284 +1,284 @@ -__author__ = 'Jacob' - -import wx -from odmtools.controller.olvSeriesSelector import EVT_OVL_CHECK_EVENT -from odmtools.controller import olvSeriesSelector -from odmtools.odmservices import ServiceManager - -[wxID_PNLSERIESSELECTOR, wxID_PNLSERIESSELECTORCBSITES, wxID_PNLSERIESSELECTORCBVARIABLES, - wxID_PNLSERIESSELECTORCHECKSITE, wxID_PNLSERIESSELECTORCHECKVARIABLE, wxID_PNLSERIESSELECTORLBLSITE, - wxID_PNLSERIESSELECTORLBLVARIABLE, wxID_PNLSERIESSELECTORtableSeries, wxID_PNLSERIESSELECTORPANEL1, - wxID_PNLSERIESSELECTORPANEL2, wxID_PNLSIMPLE, wxID_PNLRADIO, wxID_FRAME1RBADVANCED, wxID_FRAME1RBALL, - wxID_FRAME1RBSIMPLE, wxID_FRAME1SPLITTER, wxID_PNLSPLITTER, wxID_PNLSERIESSELECTORtableSeriesTest, ] = [ - wx.NewId() for _init_ctrls in range(18)] - -class ClsSeriesSelector(wx.Panel): - - def __init__(self, parent, dbservice): - - self.parent = parent - wx.Panel.__init__(self, name=u'pnlSeriesSelector', parent=parent, - size=wx.Size(935, 270), style=wx.TAB_TRAVERSAL) - self._init_ctrls() - self.series_service = dbservice - self.initTableSeries() - self.initSVBoxes() - # Subscribe functions - self.initPubSub() - self.service_manager = ServiceManager() - self.export_service = self.service_manager.get_export_service() - self.selectedIndex = 0 - self.isEditing = False - ## Radio Sizer - ## def _init_coll_boxSizer5_Items(self, parent): - ## # generated method, don't edit - ## - ## parent.AddWindow(self.rbAll, 0, border=1, flag=wx.ALL) - ## parent.AddWindow(self.rbSimple, 0, border=1, flag=wx.ALL) - ## parent.AddWindow(self.rbAdvanced, 0, border=1, flag=wx.ALL) - - - ## Splitter Sizer - def _init_coll_boxSizer3_Items(self, parent): - # generated method, don't edit - parent.AddWindow(self.cpnlSimple, 0, flag=wx.RIGHT | wx.LEFT | wx.EXPAND) - parent.AddWindow(self.tblSeries, 100, flag=wx.EXPAND) - - ## Panel Sizer - def _init_coll_boxSizer1_Items(self, parent): - # generated method, don't edit - parent.AddSizer(self.pnlRadio, 0, border=7, flag=wx.LEFT | wx.RIGHT | wx.TOP) - parent.AddWindow(self.pnlData, 100, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) - - ## Site Sizer - def _init_coll_boxSizer4_Items(self, parent): - # generated method, don't edit - parent.AddWindow(self.checkSite, 0, border=3, flag=wx.LEFT | wx.RIGHT) - parent.AddWindow(self.lblSite, 0, border=3, flag=wx.LEFT | wx.RIGHT) - parent.AddWindow(self.cbSites, 90, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) - - ## Variable Sizer - def _init_coll_boxSizer2_Items(self, parent): - # generated method, don't edit - parent.AddWindow(self.checkVariable, 0, border=3, flag=wx.LEFT | wx.RIGHT) - parent.AddWindow(self.lblVariable, 0, border=3, flag=wx.LEFT | wx.RIGHT) - parent.AddWindow(self.cbVariables, 90, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) - - ## Simple Filter Sizer - def _init_coll_boxSizer6_Items(self, parent): - parent.AddWindow(self.pnlSite, 50, flag=wx.EXPAND) - parent.AddWindow(self.pnlVar, 50, flag=wx.EXPAND) - # parent.AddSizer(self.boxSizer4, 0, border=5, flag=wx.EXPAND) - # parent.AddSizer(self.boxSizer2, 0, border=5, flag=wx.EXPAND) - - - def _init_sizers(self): - # generated method, don't edit - self.boxSizer1 = wx.BoxSizer(orient=wx.VERTICAL) - self.boxSizer2 = wx.BoxSizer(orient=wx.HORIZONTAL) - self.boxSizer3 = wx.BoxSizer(orient=wx.VERTICAL) - self.boxSizer4 = wx.BoxSizer(orient=wx.HORIZONTAL) - ## self.boxSizer5 = wx.BoxSizer(orient=wx.HORIZONTAL) - self.boxSizer6 = wx.BoxSizer(orient=wx.VERTICAL) - - self._init_coll_boxSizer1_Items(self.boxSizer1) - self._init_coll_boxSizer2_Items(self.boxSizer2) - self._init_coll_boxSizer3_Items(self.boxSizer3) - self._init_coll_boxSizer4_Items(self.boxSizer4) - ## self._init_coll_boxSizer5_Items(self.boxSizer5) - self._init_coll_boxSizer6_Items(self.boxSizer6) - - self.SetSizer(self.boxSizer1) - ## self.pnlRadio.SetSizer(self.boxSizer5) - ## self.pnlSite.SetSizer(self.boxSizer4) - ## self.pnlVar.SetSizer(self.boxSizer2) - self.cpnlSimple.SetSizer(self.boxSizer6) - self.pnlData.SetSizer(self.boxSizer3) - # self.pnlRadio.SetSizer(self.boxSizer5) - - def _init_ctrls(self): - # generated method, don't edit - - - self.SetClientSize(wx.Size(919, 232)) - self.Enable(True) - - ## Radio panel - self.pnlRadio = wx.Panel(id=wxID_PNLRADIO, name='pnlRadio', parent=self, pos=wx.Point(3, 3), - size=wx.Size(919, 20), style=wx.TAB_TRAVERSAL) - - self.rbAll = wx.RadioButton(id=wxID_FRAME1RBALL, label=u'All', name=u'rbAll', parent=self.pnlRadio, - pos=wx.Point(0, 0), size=wx.Size(81, 20), style=0) - - self.rbSimple = wx.RadioButton(id=wxID_FRAME1RBSIMPLE, label=u'Simple Filter', name=u'rbSimple', - parent=self.pnlRadio, pos=wx.Point(81, 0), size=wx.Size(112, 20), style=0) - - self.rbAdvanced = wx.RadioButton(id=wxID_FRAME1RBADVANCED, label=u'Advanced Filter', name=u'rbAdvanced', - parent=self.pnlRadio, pos=wx.Point(193, 0), size=wx.Size(104, 20), style=0) - - self.rbAll.SetValue(True) - - self.rbAdvanced.Enable(False) - - ## Splitter panel - self.pnlData = wx.Panel(id=wxID_PNLSPLITTER, name='pnlData', parent=self, pos=wx.Point(0, -10), - size=wx.Size(900, 349), style=wx.TAB_TRAVERSAL) - - self.cpnlSimple = wx.CollapsiblePane(self.pnlData, label="", style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE) - - ## Site Panel - self.pnlSite = wx.Panel(id=wxID_PNLSERIESSELECTORPANEL1, name='pnlSite', parent=self.cpnlSimple.GetPane(), - pos=wx.Point(3, 0), size=wx.Size(800, 25), style=wx.TAB_TRAVERSAL) - - self.cbSites = wx.ComboBox(choices=[], id=wxID_PNLSERIESSELECTORCBSITES, name=u'cbSites', parent=self.pnlSite, - pos=wx.Point(100, 0), size=wx.Size(700, 23), style=wx.CB_READONLY, value=u'') - - self.checkSite = wx.CheckBox(id=wxID_PNLSERIESSELECTORCHECKSITE, label=u'', name=u'checkSite', - parent=self.pnlSite, pos=wx.Point(3, 0), size=wx.Size(21, 21), style=0) - - self.lblSite = wx.StaticText(id=wxID_PNLSERIESSELECTORLBLSITE, label=u'Site', name=u'lblSite', - parent=self.pnlSite, pos=wx.Point(30, 0), size=wx.Size(60, 21), style=0) - self.lblSite.SetToolTipString(u'staticText1') - - self.cbSites.SetLabel(u'') - #self.checkSite.SetValue(False) - - ### Variable Panel - self.pnlVar = wx.Panel(id=wxID_PNLSERIESSELECTORPANEL2, name='pnlVar', parent=self.cpnlSimple.GetPane(), - pos=wx.Point(3, 26), size=wx.Size(800, 25), style=wx.TAB_TRAVERSAL) - - self.lblVariable = wx.StaticText(id=wxID_PNLSERIESSELECTORLBLVARIABLE, label=u'Variable', name=u'lblVariable', - parent=self.pnlVar, pos=wx.Point(30, 0), size=wx.Size(60, 21), style=0) - - self.checkVariable = wx.CheckBox(id=wxID_PNLSERIESSELECTORCHECKVARIABLE, label=u'', name=u'checkVariable', - parent=self.pnlVar, pos=wx.Point(3, 0), size=wx.Size(21, 21), style=0) - - self.cbVariables = wx.ComboBox(choices=[], id=wxID_PNLSERIESSELECTORCBVARIABLES, name=u'cbVariables', - parent=self.pnlVar, pos=wx.Point(100, 0), size=wx.Size(700, 25), style=wx.CB_READONLY, - value='comboBox4') - self.cbVariables.SetLabel(u'') - self.cbVariables.Enable(False) - - #wx.EVT_RADIOBUTTON(self, self.rbAll.Id, self.onRbAllRadiobutton) - self.rbAll.Bind(wx.EVT_RADIOBUTTON, self.onRbAllRadiobutton, id=wxID_FRAME1RBALL) - self.rbSimple.Bind(wx.EVT_RADIOBUTTON, self.onRbSimpleRadiobutton, id=wxID_FRAME1RBSIMPLE) - self.rbAdvanced.Bind(wx.EVT_RADIOBUTTON, self.onRbAdvancedRadiobutton, id=wxID_FRAME1RBADVANCED) - - self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.onPaneChanged, self.cpnlSimple) - self.checkSite.Bind(wx.EVT_CHECKBOX, self.onCheck, id=wxID_PNLSERIESSELECTORCHECKSITE) - self.checkVariable.Bind(wx.EVT_CHECKBOX, self.onCheck, id=wxID_PNLSERIESSELECTORCHECKVARIABLE) - self.cbSites.Bind(wx.EVT_COMBOBOX, self.onCbSitesCombobox, id=wxID_PNLSERIESSELECTORCBSITES) - self.cbVariables.Bind(wx.EVT_COMBOBOX, self.onCbVariablesCombobox, id=wxID_PNLSERIESSELECTORCBVARIABLES) - - - ### New Stuff ################################################################################################## - - self.tblSeries = olvSeriesSelector.clsSeriesTable(id=wxID_PNLSERIESSELECTORtableSeries, parent=self.pnlData, - name=u'tblSeries', size=wx.Size(950, 108), pos=wx.Point(5, 5), - style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VIRTUAL) - - self.tblSeries.SetEmptyListMsg("No Database Loaded") - - #self.tblSeries.rowFormatter = self._rowFormatter - self.tblSeries.Bind(EVT_OVL_CHECK_EVENT, self.onReadyToPlot) - self.tblSeries.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.getSelectedObject) - self.tblSeries.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnTableRightDown, - id=wxID_PNLSERIESSELECTORtableSeries) - self.tblSeries.handleStandardKeys = True - self.tblSeries.useAlternateBackColors = True - #self.tblSeries.oddRowsBackColor = wx.Colour(143, 188, 188) - self.tblSeries.oddRowsBackColor = wx.Colour(191, 217, 217) - self.cpnlSimple.Collapse(True) - self._init_sizers() - - ## Virtual Event Handlers - def onReadyToPlot(self, event): - event.Skip() - - def onReadyToEdit(self, event): - event.Skip() - - def stopEdit(self): - pass - - def getSelectedObject(self, event): - event.Skip() - - def resetDB(self, dbservice): - pass - - def initTableSeries(self): - pass - - def refreshTableSeries(self, db): - pass - - def refreshSeries(self): - pass - - def initSVBoxes(self): - pass - - def initPubSub(self): - pass - - def OnTableRightDown(self, event): - event.Skip() - - def onPaneChanged(self, event=None): - pass - - def onRbAdvancedRadiobutton(self, event): - event.Skip() - - def onRbAllRadiobutton(self, event): - event.Skip() - - def onRbSimpleRadiobutton(self, event): - event.Skip() - - def onRightPlot(self, event): - event.Skip() - - def onRightEdit(self, event): - event.Skip() - - def onRightRefresh(self, event): - event.Skip() - - def onRightClearSelected(self, event): - event.Skip() - - def onRightExData(self, event): - event.Skip() - - def onRightExMeta(self, event): - event.Skip() - - def onCbSitesCombobox(self, event): - event.Skip() - - def onCbVariablesCombobox(self, event): - event.Skip() - - def siteAndVariables(self): - pass - - def siteOnly(self): - pass - - def variableOnly(self): - pass - - def onCheck(self, event): - event.Skip() - - def setFilter(self, site_code='', var_code='', advfilter=''): - pass - - def isEditing(self): - return self.isEditing - - - def _rowFormatter(self, listItem, object): +__author__ = 'Jacob' + +import wx +from odmtools.controller.olvSeriesSelector import EVT_OVL_CHECK_EVENT +from odmtools.controller import olvSeriesSelector +from odmtools.odmservices import ServiceManager + +[wxID_PNLSERIESSELECTOR, wxID_PNLSERIESSELECTORCBSITES, wxID_PNLSERIESSELECTORCBVARIABLES, + wxID_PNLSERIESSELECTORCHECKSITE, wxID_PNLSERIESSELECTORCHECKVARIABLE, wxID_PNLSERIESSELECTORLBLSITE, + wxID_PNLSERIESSELECTORLBLVARIABLE, wxID_PNLSERIESSELECTORtableSeries, wxID_PNLSERIESSELECTORPANEL1, + wxID_PNLSERIESSELECTORPANEL2, wxID_PNLSIMPLE, wxID_PNLRADIO, wxID_FRAME1RBADVANCED, wxID_FRAME1RBALL, + wxID_FRAME1RBSIMPLE, wxID_FRAME1SPLITTER, wxID_PNLSPLITTER, wxID_PNLSERIESSELECTORtableSeriesTest, ] = [ + wx.NewId() for _init_ctrls in range(18)] + +class ClsSeriesSelector(wx.Panel): + + def __init__(self, parent, dbservice): + + self.parent = parent + wx.Panel.__init__(self, name=u'pnlSeriesSelector', parent=parent, + size=wx.Size(935, 270), style=wx.TAB_TRAVERSAL) + self._init_ctrls() + self.series_service = dbservice + self.initTableSeries() + self.initSVBoxes() + # Subscribe functions + self.initPubSub() + self.service_manager = ServiceManager() + self.export_service = self.service_manager.get_export_service() + self.selectedIndex = 0 + self.isEditing = False + ## Radio Sizer + ## def _init_coll_boxSizer5_Items(self, parent): + ## # generated method, don't edit + ## + ## parent.AddWindow(self.rbAll, 0, border=1, flag=wx.ALL) + ## parent.AddWindow(self.rbSimple, 0, border=1, flag=wx.ALL) + ## parent.AddWindow(self.rbAdvanced, 0, border=1, flag=wx.ALL) + + + ## Splitter Sizer + def _init_coll_boxSizer3_Items(self, parent): + # generated method, don't edit + parent.AddWindow(self.cpnlSimple, 0, flag=wx.RIGHT | wx.LEFT | wx.EXPAND) + parent.AddWindow(self.tblSeries, 100, flag=wx.EXPAND) + + ## Panel Sizer + def _init_coll_boxSizer1_Items(self, parent): + # generated method, don't edit + parent.AddSizer(self.pnlRadio, 0, border=7, flag=wx.LEFT | wx.RIGHT | wx.TOP) + parent.AddWindow(self.pnlData, 100, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) + + ## Site Sizer + def _init_coll_boxSizer4_Items(self, parent): + # generated method, don't edit + parent.AddWindow(self.checkSite, 0, border=3, flag=wx.LEFT | wx.RIGHT) + parent.AddWindow(self.lblSite, 0, border=3, flag=wx.LEFT | wx.RIGHT) + parent.AddWindow(self.cbSites, 90, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) + + ## Variable Sizer + def _init_coll_boxSizer2_Items(self, parent): + # generated method, don't edit + parent.AddWindow(self.checkVariable, 0, border=3, flag=wx.LEFT | wx.RIGHT) + parent.AddWindow(self.lblVariable, 0, border=3, flag=wx.LEFT | wx.RIGHT) + parent.AddWindow(self.cbVariables, 90, border=3, flag=wx.LEFT | wx.RIGHT | wx.EXPAND) + + ## Simple Filter Sizer + def _init_coll_boxSizer6_Items(self, parent): + parent.AddWindow(self.pnlSite, 50, flag=wx.EXPAND) + parent.AddWindow(self.pnlVar, 50, flag=wx.EXPAND) + # parent.AddSizer(self.boxSizer4, 0, border=5, flag=wx.EXPAND) + # parent.AddSizer(self.boxSizer2, 0, border=5, flag=wx.EXPAND) + + + def _init_sizers(self): + # generated method, don't edit + self.boxSizer1 = wx.BoxSizer(orient=wx.VERTICAL) + self.boxSizer2 = wx.BoxSizer(orient=wx.HORIZONTAL) + self.boxSizer3 = wx.BoxSizer(orient=wx.VERTICAL) + self.boxSizer4 = wx.BoxSizer(orient=wx.HORIZONTAL) + ## self.boxSizer5 = wx.BoxSizer(orient=wx.HORIZONTAL) + self.boxSizer6 = wx.BoxSizer(orient=wx.VERTICAL) + + self._init_coll_boxSizer1_Items(self.boxSizer1) + self._init_coll_boxSizer2_Items(self.boxSizer2) + self._init_coll_boxSizer3_Items(self.boxSizer3) + self._init_coll_boxSizer4_Items(self.boxSizer4) + ## self._init_coll_boxSizer5_Items(self.boxSizer5) + self._init_coll_boxSizer6_Items(self.boxSizer6) + + self.SetSizer(self.boxSizer1) + ## self.pnlRadio.SetSizer(self.boxSizer5) + ## self.pnlSite.SetSizer(self.boxSizer4) + ## self.pnlVar.SetSizer(self.boxSizer2) + self.cpnlSimple.SetSizer(self.boxSizer6) + self.pnlData.SetSizer(self.boxSizer3) + # self.pnlRadio.SetSizer(self.boxSizer5) + + def _init_ctrls(self): + # generated method, don't edit + + + self.SetClientSize(wx.Size(919, 232)) + self.Enable(True) + + ## Radio panel + self.pnlRadio = wx.Panel(id=wxID_PNLRADIO, name='pnlRadio', parent=self, pos=wx.Point(3, 3), + size=wx.Size(919, 20), style=wx.TAB_TRAVERSAL) + + self.rbAll = wx.RadioButton(id=wxID_FRAME1RBALL, label=u'All', name=u'rbAll', parent=self.pnlRadio, + pos=wx.Point(0, 0), size=wx.Size(81, 20), style=0) + + self.rbSimple = wx.RadioButton(id=wxID_FRAME1RBSIMPLE, label=u'Simple Filter', name=u'rbSimple', + parent=self.pnlRadio, pos=wx.Point(81, 0), size=wx.Size(112, 20), style=0) + + self.rbAdvanced = wx.RadioButton(id=wxID_FRAME1RBADVANCED, label=u'Advanced Filter', name=u'rbAdvanced', + parent=self.pnlRadio, pos=wx.Point(193, 0), size=wx.Size(104, 20), style=0) + + self.rbAll.SetValue(True) + + self.rbAdvanced.Enable(False) + + ## Splitter panel + self.pnlData = wx.Panel(id=wxID_PNLSPLITTER, name='pnlData', parent=self, pos=wx.Point(0, -10), + size=wx.Size(900, 349), style=wx.TAB_TRAVERSAL) + + self.cpnlSimple = wx.CollapsiblePane(self.pnlData, label="", style=wx.CP_DEFAULT_STYLE | wx.CP_NO_TLW_RESIZE) + + ## Site Panel + self.pnlSite = wx.Panel(id=wxID_PNLSERIESSELECTORPANEL1, name='pnlSite', parent=self.cpnlSimple.GetPane(), + pos=wx.Point(3, 0), size=wx.Size(800, 25), style=wx.TAB_TRAVERSAL) + + self.cbSites = wx.ComboBox(choices=[], id=wxID_PNLSERIESSELECTORCBSITES, name=u'cbSites', parent=self.pnlSite, + pos=wx.Point(100, 0), size=wx.Size(700, 23), style=wx.CB_READONLY, value=u'') + + self.checkSite = wx.CheckBox(id=wxID_PNLSERIESSELECTORCHECKSITE, label=u'', name=u'checkSite', + parent=self.pnlSite, pos=wx.Point(3, 0), size=wx.Size(21, 21), style=0) + + self.lblSite = wx.StaticText(id=wxID_PNLSERIESSELECTORLBLSITE, label=u'Site', name=u'lblSite', + parent=self.pnlSite, pos=wx.Point(30, 0), size=wx.Size(60, 21), style=0) + self.lblSite.SetToolTipString(u'staticText1') + + self.cbSites.SetLabel(u'') + #self.checkSite.SetValue(False) + + ### Variable Panel + self.pnlVar = wx.Panel(id=wxID_PNLSERIESSELECTORPANEL2, name='pnlVar', parent=self.cpnlSimple.GetPane(), + pos=wx.Point(3, 26), size=wx.Size(800, 25), style=wx.TAB_TRAVERSAL) + + self.lblVariable = wx.StaticText(id=wxID_PNLSERIESSELECTORLBLVARIABLE, label=u'Variable', name=u'lblVariable', + parent=self.pnlVar, pos=wx.Point(30, 0), size=wx.Size(60, 21), style=0) + + self.checkVariable = wx.CheckBox(id=wxID_PNLSERIESSELECTORCHECKVARIABLE, label=u'', name=u'checkVariable', + parent=self.pnlVar, pos=wx.Point(3, 0), size=wx.Size(21, 21), style=0) + + self.cbVariables = wx.ComboBox(choices=[], id=wxID_PNLSERIESSELECTORCBVARIABLES, name=u'cbVariables', + parent=self.pnlVar, pos=wx.Point(100, 0), size=wx.Size(700, 25), style=wx.CB_READONLY, + value='comboBox4') + self.cbVariables.SetLabel(u'') + self.cbVariables.Enable(False) + + #wx.EVT_RADIOBUTTON(self, self.rbAll.Id, self.onRbAllRadiobutton) + self.rbAll.Bind(wx.EVT_RADIOBUTTON, self.onRbAllRadiobutton, id=wxID_FRAME1RBALL) + self.rbSimple.Bind(wx.EVT_RADIOBUTTON, self.onRbSimpleRadiobutton, id=wxID_FRAME1RBSIMPLE) + self.rbAdvanced.Bind(wx.EVT_RADIOBUTTON, self.onRbAdvancedRadiobutton, id=wxID_FRAME1RBADVANCED) + + self.Bind(wx.EVT_COLLAPSIBLEPANE_CHANGED, self.onPaneChanged, self.cpnlSimple) + self.checkSite.Bind(wx.EVT_CHECKBOX, self.onCheck, id=wxID_PNLSERIESSELECTORCHECKSITE) + self.checkVariable.Bind(wx.EVT_CHECKBOX, self.onCheck, id=wxID_PNLSERIESSELECTORCHECKVARIABLE) + self.cbSites.Bind(wx.EVT_COMBOBOX, self.onCbSitesCombobox, id=wxID_PNLSERIESSELECTORCBSITES) + self.cbVariables.Bind(wx.EVT_COMBOBOX, self.onCbVariablesCombobox, id=wxID_PNLSERIESSELECTORCBVARIABLES) + + + ### New Stuff ################################################################################################## + + self.tblSeries = olvSeriesSelector.clsSeriesTable(id=wxID_PNLSERIESSELECTORtableSeries, parent=self.pnlData, + name=u'tblSeries', size=wx.Size(950, 108), pos=wx.Point(5, 5), + style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VIRTUAL) + + self.tblSeries.SetEmptyListMsg("No Database Loaded") + + #self.tblSeries.rowFormatter = self._rowFormatter + self.tblSeries.Bind(EVT_OVL_CHECK_EVENT, self.onReadyToPlot) + self.tblSeries.Bind(wx.EVT_LIST_ITEM_FOCUSED, self.getSelectedObject) + self.tblSeries.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnTableRightDown, + id=wxID_PNLSERIESSELECTORtableSeries) + self.tblSeries.handleStandardKeys = True + self.tblSeries.useAlternateBackColors = True + #self.tblSeries.oddRowsBackColor = wx.Colour(143, 188, 188) + self.tblSeries.oddRowsBackColor = wx.Colour(191, 217, 217) + self.cpnlSimple.Collapse(True) + self._init_sizers() + + ## Virtual Event Handlers + def onReadyToPlot(self, event): + event.Skip() + + def onReadyToEdit(self, event): + event.Skip() + + def stopEdit(self): + pass + + def getSelectedObject(self, event): + event.Skip() + + def resetDB(self, dbservice): + pass + + def initTableSeries(self): + pass + + def refreshTableSeries(self, db): + pass + + def refreshSeries(self): + pass + + def initSVBoxes(self): + pass + + def initPubSub(self): + pass + + def OnTableRightDown(self, event): + event.Skip() + + def onPaneChanged(self, event=None): + pass + + def onRbAdvancedRadiobutton(self, event): + event.Skip() + + def onRbAllRadiobutton(self, event): + event.Skip() + + def onRbSimpleRadiobutton(self, event): + event.Skip() + + def onRightPlot(self, event): + event.Skip() + + def onRightEdit(self, event): + event.Skip() + + def onRightRefresh(self, event): + event.Skip() + + def onRightClearSelected(self, event): + event.Skip() + + def onRightExData(self, event): + event.Skip() + + def onRightExMeta(self, event): + event.Skip() + + def onCbSitesCombobox(self, event): + event.Skip() + + def onCbVariablesCombobox(self, event): + event.Skip() + + def siteAndVariables(self): + pass + + def siteOnly(self): + pass + + def variableOnly(self): + pass + + def onCheck(self, event): + event.Skip() + + def setFilter(self, site_code='', var_code='', advfilter=''): + pass + + def isEditing(self): + return self.isEditing + + + def _rowFormatter(self, listItem, object): pass \ No newline at end of file From 45f6233acbbebaff3cde23dfbd15b929c2466604 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Thu, 22 Sep 2016 17:56:40 -0600 Subject: [PATCH 04/19] #293 Updated the create methods to use the ODM2 create methods Unit test pass. Only method that has not been updated is the create_qualifier()' --- odmtools/odmdata/__init__.py | 2 +- odmtools/odmdata/session_factory.py | 114 +++++++++--------- odmtools/odmservices/cv_service.py | 7 +- odmtools/odmservices/series_service.py | 37 +++--- odmtools/odmservices/service_manager.py | 9 +- tests/test_odmservices/test_cv_service.py | 6 +- tests/test_odmservices/test_edit_service.py | 2 +- tests/test_odmservices/test_export_service.py | 2 +- tests/test_odmservices/test_series_service.py | 2 +- 9 files changed, 86 insertions(+), 95 deletions(-) diff --git a/odmtools/odmdata/__init__.py b/odmtools/odmdata/__init__.py index dfb89a2..4e1285a 100644 --- a/odmtools/odmdata/__init__.py +++ b/odmtools/odmdata/__init__.py @@ -14,7 +14,7 @@ from sample_medium_cv import SampleMediumCV from sample_type_cv import SampleTypeCV from series import Series -from session_factory import SessionFactory +from odm2api.ODMconnection import SessionFactory from site import Site from site_type_cv import SiteTypeCV from source import Source diff --git a/odmtools/odmdata/session_factory.py b/odmtools/odmdata/session_factory.py index 0d2d488..8afda48 100644 --- a/odmtools/odmdata/session_factory.py +++ b/odmtools/odmdata/session_factory.py @@ -1,57 +1,57 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker - - -class SessionFactory(): - def __init__(self, connection_string, echo): - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_size=20) - self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'connect_timeout': 1}) - self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'timeout': 1}) - self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - connect_args={'connect_timeout': 1}) - - ''' - # Removing pool_timeout and max_overflow allowed the tests to pass - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, pool_size=20, max_overflow=0) - self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) - self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) - self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, - pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) - ''' - - ''' - # Old code - class SessionFactory(): - def __init__(self, connection_string, echo): - self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, - #pool_size=20, - pool_recycle=3600) - - # Create session maker - self.Session = sessionmaker(bind=self.engine) - - def get_session(self): - return self.Session() - - def __repr__(self): - return "" % (self.engine) - ''' - - # Create session maker - self.Session = sessionmaker(bind=self.engine) - self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) - self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) - self.my_test_Session = sessionmaker(bind=self.my_test_engine) - - def get_session(self): - return self.Session() - - def __repr__(self): - return "" % (self.engine) - +# from sqlalchemy import create_engine +# from sqlalchemy.orm import sessionmaker +# +# +# class SessionFactory(): +# def __init__(self, connection_string, echo): +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_size=20) +# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'connect_timeout': 1}) +# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'timeout': 1}) +# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# connect_args={'connect_timeout': 1}) +# +# ''' +# # Removing pool_timeout and max_overflow allowed the tests to pass +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, pool_size=20, max_overflow=0) +# self.psql_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) +# self.ms_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'timeout': 1}) +# self.my_test_engine = create_engine(connection_string, encoding='utf-8', echo=echo, pool_recycle=3600, +# pool_timeout=5, max_overflow=0, connect_args={'connect_timeout': 1}) +# ''' +# +# ''' +# # Old code +# class SessionFactory(): +# def __init__(self, connection_string, echo): +# self.engine = create_engine(connection_string, encoding='utf-8', echo=echo, +# #pool_size=20, +# pool_recycle=3600) +# +# # Create session maker +# self.Session = sessionmaker(bind=self.engine) +# +# def get_session(self): +# return self.Session() +# +# def __repr__(self): +# return "" % (self.engine) +# ''' +# +# # Create session maker +# self.Session = sessionmaker(bind=self.engine) +# self.psql_test_Session = sessionmaker(bind=self.psql_test_engine) +# self.ms_test_Session = sessionmaker(bind=self.ms_test_engine) +# self.my_test_Session = sessionmaker(bind=self.my_test_engine) +# +# def get_session(self): +# return self.Session() +# +# def __repr__(self): +# return "" % (self.engine) +# diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index 24b5e81..c6c9a42 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -16,14 +16,17 @@ from odmtools.odmdata import Qualifier from odmtools.odmdata import Unit from sqlalchemy import not_ +from odm2api.ODM2.services.readService import ReadODM2 -class CVService(): +class CVService(): # Rename to ReadService # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): self._session_factory = SessionFactory(connection_string, debug) - self._edit_session = self._session_factory.get_session() + self._edit_session = self._session_factory.getSession() self._debug = debug + self.read_service = ReadODM2(self._session_factory, debug=self._debug) + # Controlled Vocabulary get methods diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 078e6fe..e3d5716 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -18,20 +18,23 @@ from odmtools.odmdata import ODMVersion from odmtools.common.logger import LoggerTool import pandas as pd +from odm2api.ODM2.services.createService import CreateODM2 + # tool = LoggerTool() # logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) logger =logging.getLogger('main') -class SeriesService(): +class SeriesService(): # Rename to CreateService # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): self._session_factory = SessionFactory(connection_string, debug) - self._edit_session = self._session_factory.get_session() + self._edit_session = self._session_factory.getSession() self._debug = debug + self.create_service = CreateODM2(session_factory=self._session_factory, debug=self._debug) def reset_session(self): - self._edit_session = self._session_factory.get_session() # Reset the session in order to prevent memory leaks + self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks def get_db_version(self): return self._edit_session.query(ODMVersion).first().version_number @@ -534,7 +537,7 @@ def save_values(self, values): def create_new_series(self, data_values, site_id, variable_id, method_id, source_id, qcl_id): """ - + series -> Result in ODM2 :param data_values: :param site_id: :param variable_id: @@ -551,13 +554,10 @@ def create_new_series(self, data_values, site_id, variable_id, method_id, source series.source_id = source_id series.quality_control_level_id = qcl_id - self._edit_session.add(series) - self._edit_session.commit() - return series + return self.create_service.createResult(series) def create_method(self, description, link): """ - :param description: :param link: :return: @@ -567,29 +567,22 @@ def create_method(self, description, link): if link is not None: meth.link = link - self._edit_session.add(meth) - self._edit_session.commit() + self.create_service.createMethod(method=meth) return meth def create_variable_by_var(self, var): """ - :param var: Variable Object :return: """ - try: - self._edit_session.add(var) - self._edit_session.commit() - return var - except: - return None + self.create_service.createVariable(var=var) + return var def create_variable( self, code, name, speciation, variable_unit_id, sample_medium, value_type, is_regular, time_support, time_unit_id, data_type, general_category, no_data_value): """ - :param code: :param name: :param speciation: @@ -618,13 +611,12 @@ def create_variable( var.general_category = general_category var.no_data_value = no_data_value - self._edit_session.add(var) - self._edit_session.commit() + self.create_service.createVariable(var=var) return var def create_qcl(self, code, definition, explanation): """ - + qcl -> Processing Level in ODM2 :param code: :param definition: :param explanation: @@ -634,9 +626,8 @@ def create_qcl(self, code, definition, explanation): qcl.code = code qcl.definition = definition qcl.explanation = explanation + self.create_service.createProcessingLevel(proclevel=qcl) - self._edit_session.add(qcl) - self._edit_session.commit() return qcl diff --git a/odmtools/odmservices/service_manager.py b/odmtools/odmservices/service_manager.py index 1ad5ff1..142a5c0 100644 --- a/odmtools/odmservices/service_manager.py +++ b/odmtools/odmservices/service_manager.py @@ -13,7 +13,8 @@ from odmtools.controller import EditTools from export_service import ExportService from odmtools.lib.Appdirs.appdirs import user_config_dir -from odmtools.odmdata.session_factory import SessionFactory +# from odmtools.odmdata.session_factory import SessionFactory +from odm2api.ODMconnection import SessionFactory # tool = LoggerTool() @@ -100,12 +101,12 @@ def add_connection(self, conn_dict): def testEngine(self, connection_string): s = SessionFactory(connection_string, echo=False) if 'mssql' in connection_string: - s.ms_test_Session().execute("Select top 1 VariableCode From Variables") + s.test_Session().execute("Select top 1 VariableCode From Variables") elif 'mysql' in connection_string: - s.my_test_Session().execute('Select "VariableCode" From Variables Limit 1') + s.test_Session().execute('Select "VariableCode" From Variables Limit 1') elif 'postgresql' in connection_string: #s.psql_test_Session().execute('Select "VariableCode" From "ODM2"."Variables" Limit 1') - s.psql_test_Session().execute('Select "VariableCode" From "Variables" Limit 1') + s.test_Session().execute('Select "VariableCode" From "Variables" Limit 1') return True def test_connection(self, conn_dict): diff --git a/tests/test_odmservices/test_cv_service.py b/tests/test_odmservices/test_cv_service.py index 5bfd72a..61c8ef5 100644 --- a/tests/test_odmservices/test_cv_service.py +++ b/tests/test_odmservices/test_cv_service.py @@ -1,8 +1,4 @@ -import pytest -import sqlalchemy.orm.exc -from odmtools.odmdata import Qualifier from odmtools.odmservices import CVService - from tests import test_util @@ -13,7 +9,7 @@ class TestCVService: def setup(self): self.connection_string = "sqlite:///:memory:" self.cv_service = CVService(self.connection_string, debug=False) - self.session = self.cv_service._session_factory.get_session() + self.session = self.cv_service._session_factory.getSession() engine = self.cv_service._session_factory.engine test_util.build_db(engine) diff --git a/tests/test_odmservices/test_edit_service.py b/tests/test_odmservices/test_edit_service.py index 879113d..f8fc913 100644 --- a/tests/test_odmservices/test_edit_service.py +++ b/tests/test_odmservices/test_edit_service.py @@ -15,7 +15,7 @@ def setup(self): self.memory_database = MemoryDatabase() self.memory_database.set_series_service(self.series_service) - self.session = self.memory_database.series_service._session_factory.get_session() + self.session = self.memory_database.series_service._session_factory.getSession() self.series = test_util.add_series_bulk_data(self.session) #assert len(self.series.data_values) == 100 diff --git a/tests/test_odmservices/test_export_service.py b/tests/test_odmservices/test_export_service.py index acc146f..89e7d58 100644 --- a/tests/test_odmservices/test_export_service.py +++ b/tests/test_odmservices/test_export_service.py @@ -8,7 +8,7 @@ class TestExportService: def setup(self): self.connection_string = "sqlite:///:memory:" self.series_service = SeriesService(self.connection_string, debug=False) - self.session = self.series_service._session_factory.get_session() + self.session = self.series_service._session_factory.getSession() engine = self.series_service._session_factory.engine test_util.build_db(engine) self.series = test_util.add_series(self.session) diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index 5cf0e81..96c1afb 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -8,7 +8,7 @@ class TestSeriesService: def setup(self): self.connection_string = "sqlite:///:memory:" self.series_service = SeriesService(self.connection_string, debug=False) - self.session = self.series_service._session_factory.get_session() + self.session = self.series_service._session_factory.getSession() engine = self.series_service._session_factory.engine test_util.build_db(engine) """ From 55e5721b5d7c0291d05665a251e5ad8154291cc8 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Wed, 28 Sep 2016 11:41:53 -0600 Subject: [PATCH 05/19] Finished the create services, and began working on the get --- odmtools/controller/logicEditTools.py | 2 +- odmtools/gui/frmFlagValues.py | 2 +- odmtools/odmservices/cv_service.py | 8 ++++++ odmtools/odmservices/edit_service.py | 2 +- odmtools/odmservices/series_service.py | 27 ++++++++++--------- tests/test_odmservices/test_series_service.py | 18 ++++++------- 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/odmtools/controller/logicEditTools.py b/odmtools/controller/logicEditTools.py index dd0f813..ef81058 100644 --- a/odmtools/controller/logicEditTools.py +++ b/odmtools/controller/logicEditTools.py @@ -380,7 +380,7 @@ def create_qcl(self, code, definition, explanation): return qcl def create_qualifer(self, code, description): - qual = self._edit_service.create_qualifier(code, description) + qual = self._edit_service.create_annotation(code, description) if self._record: self._script('new_qual = series_service.get_qualifier_by_code(%s)\n' % (qual.code)) Publisher.sendMessage("scroll") diff --git a/odmtools/gui/frmFlagValues.py b/odmtools/gui/frmFlagValues.py index 32e4286..73f7ced 100644 --- a/odmtools/gui/frmFlagValues.py +++ b/odmtools/gui/frmFlagValues.py @@ -151,7 +151,7 @@ def OnBtnOKButton(self, event): q.description = desc ''' - q=self.series_service.create_qualifier(code, desc) + q=self.series_service.create_annotation(code, desc) self.qid = q.id self.selectedValue = q.code + '-' + q.description diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index c6c9a42..3466d0c 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -99,3 +99,11 @@ def get_unit_by_name(self, unit_name): def get_unit_by_id(self, unit_id): result = self._edit_session.query(Unit).filter_by(id=unit_id).first() return result + + def get_annotation_by_code(self, code): + self.read_service.getAnnotations(type=code) + return + + def get_all_annotations(self): + return self.read_service.getAnnotations(type=None) + diff --git a/odmtools/odmservices/edit_service.py b/odmtools/odmservices/edit_service.py index 403f07d..8431f54 100644 --- a/odmtools/odmservices/edit_service.py +++ b/odmtools/odmservices/edit_service.py @@ -589,7 +589,7 @@ def create_method(self, description, link): return self.memDB.series_service.create_method(description, link) def create_qualifier(self, code, definition): - return self.memDB.series_service.create_qualifier(code, definition) + return self.memDB.series_service.create_annotation(code, definition) def create_variable(self, code, name, speciation, variable_unit_id, sample_medium, value_type, is_regular, time_support, time_unit_id, data_type, general_category, no_data_value): diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index e3d5716..c27158b 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -19,6 +19,8 @@ from odmtools.common.logger import LoggerTool import pandas as pd from odm2api.ODM2.services.createService import CreateODM2 +from odm2api.ODM2.models import Annotations +from odm2api.ODM2.services.readService import ReadODM2 # tool = LoggerTool() @@ -32,6 +34,7 @@ def __init__(self, connection_string="", debug=False): self._edit_session = self._session_factory.getSession() self._debug = debug self.create_service = CreateODM2(session_factory=self._session_factory, debug=self._debug) + self.read_service = ReadODM2(self._session_factory, debug=self._debug) def reset_session(self): self._edit_session = self._session_factory.getSession() # Reset the session in order to prevent memory leaks @@ -191,8 +194,9 @@ def get_all_qualifiers(self): :return: List[Qualifiers] """ - result = self._edit_session.query(Qualifier).order_by(Qualifier.code).all() - return result + # result = self._edit_session.query(Annotations).order_by(Annotations.AnnotationCode).all() + # return result + return self.read_service.getAnnotations(None) def get_qualifier_by_code(self, code): """ @@ -630,24 +634,23 @@ def create_qcl(self, code, definition, explanation): return qcl + def create_annotation_by_anno(self, annotation): + self.create_service.create(annotation) + return annotation - def create_qualifier_by_qual(self, qualifier): - self._edit_session.add(qualifier) - self._edit_session.commit() - return qualifier - - def create_qualifier(self, code, description): + def create_annotation(self, code, description): """ :param code: :param description: :return: """ - qual = Qualifier() - qual.code = code - qual.description = description + annotation = Annotations() + annotation.AnnotationCode = code + annotation.AnnotationText = description + annotation.AnnotationTypeCV = "timeSeriesResultValueAnnotation" - return self.create_qualifier_by_qual(qual) + return self.create_annotation_by_anno(annotation) ##################### # diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index 96c1afb..1b4eafa 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -1,5 +1,6 @@ from odmtools.odmdata import * from odmtools.odmservices import SeriesService +from odm2api.ODM2.models import Annotations from tests import test_util @@ -62,32 +63,31 @@ def test_get_all_sites_empty(self): #assert len(sites) == 0 assert sites is None - def test_create_qualifier(self): + def test_create_annotation(self): qual = Qualifier() qual.code = "ABC123" qual.description = "This is a test" - self.series_service.create_qualifier_by_qual(qual) + self.series_service.create_annotation_by_anno(qual) assert qual.id is not None - def test_get_qualifier_by_code(self): - assert self.series_service.get_all_qualifiers() == [] + def test_get_annotation_by_code(self): + assert self.series_service.get_all_qualifiers() == None - qual= self.series_service.create_qualifier("ABC123","This is a test") + qual = self.series_service.create_annotation("ABC123", "This is a test") db_qual = self.series_service.get_qualifier_by_code("ABC123") assert qual.id == db_qual.id - def test_get_qualifiers(self): - assert self.series_service.get_all_qualifiers() == [] + def test_get_annotation(self): + assert self.series_service.get_all_qualifiers() == None - qual= self.series_service.create_qualifier("ABC123","This is a test") + qual= self.series_service.create_annotation("ABC123", "This is a test") db_qual = self.series_service.get_all_qualifiers()[0] assert qual.id == db_qual.id - def test_get_all_sites(self): assert self.series_service.get_used_sites() is None From 5803a170cb92d0db5c88e01a38a4d809ef29da2d Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Sat, 1 Oct 2016 12:22:05 -0600 Subject: [PATCH 06/19] Updated the view on the wizard method page Going to continue to update all the views than the controllers --- odmtools/gui/pageMethod.py | 300 ++++++++++++++++++++++--------------- odmtools/gui/wizSave.py | 79 +++++----- 2 files changed, 228 insertions(+), 151 deletions(-) diff --git a/odmtools/gui/pageMethod.py b/odmtools/gui/pageMethod.py index df72837..1665c33 100644 --- a/odmtools/gui/pageMethod.py +++ b/odmtools/gui/pageMethod.py @@ -16,120 +16,186 @@ # logger = tool.setupLogger(__name__, __name__ + '.log', 'w', logging.DEBUG) logger =logging.getLogger('main') - -class pnlMethod(wx.Panel): - def _init_ctrls(self, prnt): - # generated method, don't edit - wx.Panel.__init__(self, id=wxID_PNLMETHOD, name=u'pnlMethod', - parent=prnt, pos=wx.Point(135, 307), size=wx.Size(439, 357), - style=wx.TAB_TRAVERSAL) - self.SetClientSize(wx.Size(423, 319)) - - self.rbGenerate = wx.RadioButton(id=wxID_PNLMETHODSRBGENERATE, - label=u'Automatically generate a Method ', name=u'rbGenerate', - parent=self, pos=wx.Point(16, 8), size=wx.Size(392, 16), style=0) - self.rbGenerate.SetValue(True) - self.rbGenerate.Bind(wx.EVT_RADIOBUTTON, self.OnRbGenerateRadiobutton, - id=wxID_PNLMETHODSRBGENERATE) - - self.rbSelect = wx.RadioButton(id=wxID_PNLMETHODSRBSELECT, - label=u'Select an existing Method', name=u'rbSelect', parent=self, - pos=wx.Point(16, 32), size=wx.Size(392, 13), style=0) - self.rbSelect.SetValue(False) - self.rbSelect.Bind(wx.EVT_RADIOBUTTON, self.OnRbSelectRadiobutton, - id=wxID_PNLMETHODSRBSELECT) - - self.rbCreateNew = wx.RadioButton(id=wxID_PNLMETHODSRBCREATENEW, - label=u'Create a new Method ', name=u'rbCreateNew', parent=self, - pos=wx.Point(16, 208), size=wx.Size(392, 13), style=0) - self.rbCreateNew.SetValue(False) - self.rbCreateNew.Bind(wx.EVT_RADIOBUTTON, self.OnRbCreateNewRadiobutton, - id=wxID_PNLMETHODSRBCREATENEW) - - self.txtMethodDescrip = wx.richtext.RichTextCtrl(id=wxID_PNLMETHODSRICHTEXTCTRL1, - parent=self, pos=wx.Point(16, 224), size=wx.Size(392, 84), - style=wx.richtext.RE_MULTILINE, value=u'Method Description') - self.txtMethodDescrip.Enable(False) - self.txtMethodDescrip.Bind(wx.EVT_SET_FOCUS, self.OnTxtMethodDescripSetFocus) - self.txtMethodDescrip.Bind(wx.EVT_KILL_FOCUS, self.OnTxtMethodDescripKillFocus) - - self.lstMethods = wx.ListCtrl(id=wxID_PNLMETHODSLISTCTRL1, - name='lstMethods', parent=self, pos=wx.Point(16, 48), - size=wx.Size(392, 152), style=wx.LC_REPORT|wx.LC_SINGLE_SEL) - self.lstMethods.Bind(wx.EVT_SET_FOCUS, self.OnLstMethodSetFocus) - - - self.lstMethods.InsertColumn(0, 'Description') - self.lstMethods.InsertColumn(1, 'Link') - self.lstMethods.InsertColumn(2, 'id') - self.lstMethods.SetColumnWidth(0, 200) - self.lstMethods.SetColumnWidth(1, 153) - self.lstMethods.SetColumnWidth(2,0) - # self.lstMethods.Enable(False) - - - - - def __init__(self, parent, id, pos, size, style, name, ss, method): - self.series_service = ss - self.prev_val = method - self._init_ctrls(parent) - - def OnLstMethodSetFocus(self, event): - self.rbSelect.SetValue(True) - - def OnRbGenerateRadiobutton(self, event): - # self.lstMethods.Enable(False) - self.txtMethodDescrip.Enable(False) - - event.Skip() - - def OnRbSelectRadiobutton(self, event): - self.lstMethods.Enable(True) - self.txtMethodDescrip.Enable(False) - - event.Skip() - - def OnRbCreateNewRadiobutton(self, event): - # self.lstMethods.Enable(False) - self.txtMethodDescrip.Enable(True) - - event.Skip() - - def OnTxtMethodDescripSetFocus(self, event): - if self.txtMethodDescrip.GetValue() =="Method Description": - self.txtMethodDescrip.SetValue("") - - event.Skip() - - def OnTxtMethodDescripKillFocus(self, event): - if self.txtMethodDescrip.GetValue() =="": - self.txtMethodDescrip.SetValue("Method Description") - - event.Skip() - - - def getMethod(self): - - m = Method() - if self.rbGenerate.Value: - genmethod = "Values derived from ODM Tools Python" - m= self.series_service.get_method_by_description(genmethod) - if m is None: - logger.debug("assigning new method description") - m = Method() - m.description = genmethod - - elif self.rbSelect.Value: - index = self.lstMethods.GetFirstSelected() - desc= self.lstMethods.GetItem(index, 0).GetText() - - logger.debug(desc) - m= self.series_service.get_method_by_description(desc) - - - - elif self.rbCreateNew.Value: - logger.debug("assigning new method description") - m.description = self.txtMethodDescrip.GetValue() - return m \ No newline at end of file +class pnlMethod(wx.Panel): # Rename this to page method view + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + # Create components + header_text = wx.StaticText(self, label="Method") + static_line = wx.StaticLine(self, size=(-1, 12)) + + required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) + method_code_text = wx.StaticText(self, label="Method Code") + self.method_code_text_ctrl = wx.TextCtrl(self) + method_name_text = wx.StaticText(self, label="Method Name") + self.method_name_text_ctrl = wx.TextCtrl(self) + method_type_text = wx.StaticText(self, label="Method Type") + self.method_type_combo = wx.ComboBox(self) + + optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) + organization_text = wx.StaticText(self, label="Organization") + self.organization_text_ctrl = wx.TextCtrl(self) + method_link_text = wx.StaticText(self, label="Method Link") + self.method_link_text_ctrl = wx.TextCtrl(self) + description_text = wx.StaticText(self, label="Description") + self.description_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_code_text, 0, wx.EXPAND) + row_sizer.Add(self.method_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 24) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_name_text, 0, wx.EXPAND) + row_sizer.Add(self.method_name_text_ctrl, 1, wx.EXPAND | wx.LEFT, 20) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_type_text, 0, wx.EXPAND) + row_sizer.Add(self.method_type_combo, 1, wx.EXPAND | wx.LEFT, 26) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(organization_text, 0, wx.EXPAND) + row_sizer.Add(self.organization_text_ctrl, 1, wx.EXPAND | wx.LEFT, 27) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_link_text, 0, wx.EXPAND) + row_sizer.Add(self.method_link_text_ctrl, 1, wx.EXPAND | wx.LEFT, 27) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(description_text, 0, wx.EXPAND) + row_sizer.Add(self.description_text_ctrl, 1, wx.EXPAND | wx.LEFT, 34) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + # Style Components + font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) + header_text.SetFont(font) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + + # Add components to sizer + main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) + main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + self.SetSizer(main_sizer) + +# class pnlMethod(wx.Panel): +# def _init_ctrls(self, prnt): +# # generated method, don't edit +# wx.Panel.__init__(self, id=wxID_PNLMETHOD, name=u'pnlMethod', +# parent=prnt, pos=wx.Point(135, 307), size=wx.Size(439, 357), +# style=wx.TAB_TRAVERSAL) +# self.SetClientSize(wx.Size(423, 319)) +# +# self.rbGenerate = wx.RadioButton(id=wxID_PNLMETHODSRBGENERATE, +# label=u'Automatically generate a Method ', name=u'rbGenerate', +# parent=self, pos=wx.Point(16, 8), size=wx.Size(392, 16), style=0) +# self.rbGenerate.SetValue(True) +# self.rbGenerate.Bind(wx.EVT_RADIOBUTTON, self.OnRbGenerateRadiobutton, +# id=wxID_PNLMETHODSRBGENERATE) +# +# self.rbSelect = wx.RadioButton(id=wxID_PNLMETHODSRBSELECT, +# label=u'Select an existing Method', name=u'rbSelect', parent=self, +# pos=wx.Point(16, 32), size=wx.Size(392, 13), style=0) +# self.rbSelect.SetValue(False) +# self.rbSelect.Bind(wx.EVT_RADIOBUTTON, self.OnRbSelectRadiobutton, +# id=wxID_PNLMETHODSRBSELECT) +# +# self.rbCreateNew = wx.RadioButton(id=wxID_PNLMETHODSRBCREATENEW, +# label=u'Create a new Method ', name=u'rbCreateNew', parent=self, +# pos=wx.Point(16, 208), size=wx.Size(392, 13), style=0) +# self.rbCreateNew.SetValue(False) +# self.rbCreateNew.Bind(wx.EVT_RADIOBUTTON, self.OnRbCreateNewRadiobutton, +# id=wxID_PNLMETHODSRBCREATENEW) +# +# self.txtMethodDescrip = wx.richtext.RichTextCtrl(id=wxID_PNLMETHODSRICHTEXTCTRL1, +# parent=self, pos=wx.Point(16, 224), size=wx.Size(392, 84), +# style=wx.richtext.RE_MULTILINE, value=u'Method Description') +# self.txtMethodDescrip.Enable(False) +# self.txtMethodDescrip.Bind(wx.EVT_SET_FOCUS, self.OnTxtMethodDescripSetFocus) +# self.txtMethodDescrip.Bind(wx.EVT_KILL_FOCUS, self.OnTxtMethodDescripKillFocus) +# +# self.lstMethods = wx.ListCtrl(id=wxID_PNLMETHODSLISTCTRL1, +# name='lstMethods', parent=self, pos=wx.Point(16, 48), +# size=wx.Size(392, 152), style=wx.LC_REPORT|wx.LC_SINGLE_SEL) +# self.lstMethods.Bind(wx.EVT_SET_FOCUS, self.OnLstMethodSetFocus) +# +# +# self.lstMethods.InsertColumn(0, 'Description') +# self.lstMethods.InsertColumn(1, 'Link') +# self.lstMethods.InsertColumn(2, 'id') +# self.lstMethods.SetColumnWidth(0, 200) +# self.lstMethods.SetColumnWidth(1, 153) +# self.lstMethods.SetColumnWidth(2,0) +# # self.lstMethods.Enable(False) +# +# +# +# +# def __init__(self, parent, id, pos, size, style, name, ss, method): +# self.series_service = ss +# self.prev_val = method +# self._init_ctrls(parent) +# +# def OnLstMethodSetFocus(self, event): +# self.rbSelect.SetValue(True) +# +# def OnRbGenerateRadiobutton(self, event): +# # self.lstMethods.Enable(False) +# self.txtMethodDescrip.Enable(False) +# +# event.Skip() +# +# def OnRbSelectRadiobutton(self, event): +# self.lstMethods.Enable(True) +# self.txtMethodDescrip.Enable(False) +# +# event.Skip() +# +# def OnRbCreateNewRadiobutton(self, event): +# # self.lstMethods.Enable(False) +# self.txtMethodDescrip.Enable(True) +# +# event.Skip() +# +# def OnTxtMethodDescripSetFocus(self, event): +# if self.txtMethodDescrip.GetValue() =="Method Description": +# self.txtMethodDescrip.SetValue("") +# +# event.Skip() +# +# def OnTxtMethodDescripKillFocus(self, event): +# if self.txtMethodDescrip.GetValue() =="": +# self.txtMethodDescrip.SetValue("Method Description") +# +# event.Skip() +# +# +# def getMethod(self): +# +# m = Method() +# if self.rbGenerate.Value: +# genmethod = "Values derived from ODM Tools Python" +# m= self.series_service.get_method_by_description(genmethod) +# if m is None: +# logger.debug("assigning new method description") +# m = Method() +# m.description = genmethod +# +# elif self.rbSelect.Value: +# index = self.lstMethods.GetFirstSelected() +# desc= self.lstMethods.GetItem(index, 0).GetText() +# +# logger.debug(desc) +# m= self.series_service.get_method_by_description(desc) +# +# +# +# elif self.rbCreateNew.Value: +# logger.debug("assigning new method description") +# m.description = self.txtMethodDescrip.GetValue() +# return m \ No newline at end of file diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index 85e6b0a..c066a5e 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -108,41 +108,51 @@ def _init_data(self, series_service): ######################################################################## -class MethodPage(wiz.WizardPageSimple): - def __init__(self, parent, title, series_service, method): - """Constructor""" +class MethodPage(wiz.WizardPageSimple): # Raname this page to page method controller + def __init__(self, parent): + # pageMethod.pnlMethod.__init__(self, parent) wiz.WizardPageSimple.__init__(self, parent) - sizer = wx.BoxSizer(wx.VERTICAL) - self.sizer = sizer - self.SetSizer(sizer) - self.method = method - - title = wx.StaticText(self, -1, title) - title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) - sizer.Add(title, 10, wx.ALIGN_CENTRE | wx.ALL, 5) - sizer.Add(wx.StaticLine(self, -1), 5, wx.EXPAND | wx.ALL, 5) - self.panel = pageMethod.pnlMethod(self, id=wxID_PNLMETHOD, name=u'pnlMethod', - pos=wx.Point(536, 285), size=wx.Size(439, 357), - style=wx.TAB_TRAVERSAL, ss=series_service, method=method) - self.sizer.Add(self.panel, 85, wx.ALL, 5) - - self._init_data(self.panel.series_service) - - def _init_data(self, series): - meth = series.get_all_methods() - index = 0 - for m, i in zip(meth, range(len(meth))): - num_items = self.panel.lstMethods.GetItemCount() - self.panel.lstMethods.InsertStringItem(num_items, str(m.description)) - self.panel.lstMethods.SetStringItem(num_items, 1, str(m.link)) - self.panel.lstMethods.SetStringItem(num_items, 2, str(m.id)) - - if m.description == self.method.description: - index = i - - self.panel.lstMethods.Focus(index) - self.panel.lstMethods.Select(index) + main_sizer = wx.BoxSizer(wx.VERTICAL) + self.page_method_view = pageMethod.pnlMethod(self) + main_sizer.Add(self.page_method_view, 1, wx.EXPAND | wx.ALL, 0) + self.SetSizer(main_sizer) + +# class MethodPage(wiz.WizardPageSimple): +# def __init__(self, parent, title, series_service, method): +# """Constructor""" +# wiz.WizardPageSimple.__init__(self, parent) +# +# sizer = wx.BoxSizer(wx.VERTICAL) +# self.sizer = sizer +# self.SetSizer(sizer) +# self.method = method +# +# title = wx.StaticText(self, -1, title) +# title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) +# sizer.Add(title, 10, wx.ALIGN_CENTRE | wx.ALL, 5) +# sizer.Add(wx.StaticLine(self, -1), 5, wx.EXPAND | wx.ALL, 5) +# self.panel = pageMethod.pnlMethod(self, id=wxID_PNLMETHOD, name=u'pnlMethod', +# pos=wx.Point(536, 285), size=wx.Size(439, 357), +# style=wx.TAB_TRAVERSAL, ss=series_service, method=method) +# self.sizer.Add(self.panel, 1, wx.EXPAND, 5) +# +# self._init_data(self.panel.series_service) +# +# def _init_data(self, series): +# meth = series.get_all_methods() +# index = 0 +# for m, i in zip(meth, range(len(meth))): +# num_items = self.panel.lstMethods.GetItemCount() +# self.panel.lstMethods.InsertStringItem(num_items, str(m.description)) +# self.panel.lstMethods.SetStringItem(num_items, 1, str(m.link)) +# self.panel.lstMethods.SetStringItem(num_items, 2, str(m.id)) +# +# if m.description == self.method.description: +# index = i +# +# self.panel.lstMethods.Focus(index) +# self.panel.lstMethods.Select(index) ######################################################################## @@ -246,7 +256,8 @@ def __init__(self, parent, service_manager, record_service): self.currSeries = record_service.get_series() self.pgIntro = pageIntro.pageIntro(self, "Intro") - self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) + # self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) + self.pgMethod = MethodPage(self) self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) From 0e2dba8127969e2bba61d44d980416161a88c3b6 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Sat, 1 Oct 2016 12:56:34 -0600 Subject: [PATCH 07/19] Updated the qcl view on the wizard Updated the name to Processing Level. Parse the code into view/contrller and into their own files --- odmtools/controller/WizardMethodController.py | 13 ++++ .../WizardProcessLevelController.py | 13 ++++ odmtools/gui/wizSave.py | 9 ++- odmtools/view/WizardMethodView.py | 68 +++++++++++++++++++ odmtools/view/WizardProcessLevelView.py | 45 ++++++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 odmtools/controller/WizardMethodController.py create mode 100644 odmtools/controller/WizardProcessLevelController.py create mode 100644 odmtools/view/WizardMethodView.py create mode 100644 odmtools/view/WizardProcessLevelView.py diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py new file mode 100644 index 0000000..60dee6f --- /dev/null +++ b/odmtools/controller/WizardMethodController.py @@ -0,0 +1,13 @@ +import wx +from wx.wizard import WizardPageSimple +from odmtools.view.WizardMethodView import WizardMethodView + + +class WizardMethodController(WizardPageSimple): + def __init__(self, parent): + WizardPageSimple.__init__(self, parent) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + self.method_view = WizardMethodView(self) + main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.ALL, 0) + self.SetSizer(main_sizer) \ No newline at end of file diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py new file mode 100644 index 0000000..e33d9b4 --- /dev/null +++ b/odmtools/controller/WizardProcessLevelController.py @@ -0,0 +1,13 @@ +import wx +from odmtools.view.WizardProcessLevelView import WizardProcessLevelView +from wx.wizard import WizardPageSimple + + +class WizardProcessLevelController(WizardPageSimple): + def __init__(self, parent): + WizardPageSimple.__init__(self, parent) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + self.method_view = WizardProcessLevelView(self) + main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.ALL, 0) + self.SetSizer(main_sizer) \ No newline at end of file diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index c066a5e..b950d18 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -209,6 +209,9 @@ def fill_summary(self): ######################################################################## +from odmtools.controller.WizardMethodController import WizardMethodController +from odmtools.controller.WizardProcessLevelController import WizardProcessLevelController + class wizSave(wx.wizard.Wizard): def _init_ctrls(self, prnt): # generated method, don't edit @@ -257,8 +260,10 @@ def __init__(self, parent, service_manager, record_service): self.pgIntro = pageIntro.pageIntro(self, "Intro") # self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) - self.pgMethod = MethodPage(self) - self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) + # self.pgMethod = MethodPage(self) + self.pgMethod = WizardMethodController(self) + # self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) + self.pgQCL = WizardProcessLevelController(self) self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) self.pgSummary = SummaryPage(self, "Summary", self.series_service) diff --git a/odmtools/view/WizardMethodView.py b/odmtools/view/WizardMethodView.py new file mode 100644 index 0000000..1891c66 --- /dev/null +++ b/odmtools/view/WizardMethodView.py @@ -0,0 +1,68 @@ +import wx + + +class WizardMethodView(wx.Panel): # Rename this to page method view + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + # Create components + header_text = wx.StaticText(self, label="Method") + static_line = wx.StaticLine(self, size=(-1, 12)) + + required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) + method_code_text = wx.StaticText(self, label="Method Code") + self.method_code_text_ctrl = wx.TextCtrl(self) + method_name_text = wx.StaticText(self, label="Method Name") + self.method_name_text_ctrl = wx.TextCtrl(self) + method_type_text = wx.StaticText(self, label="Method Type") + self.method_type_combo = wx.ComboBox(self) + + optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) + organization_text = wx.StaticText(self, label="Organization") + self.organization_text_ctrl = wx.TextCtrl(self) + method_link_text = wx.StaticText(self, label="Method Link") + self.method_link_text_ctrl = wx.TextCtrl(self) + description_text = wx.StaticText(self, label="Description") + self.description_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) + + # Style Components + font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) + header_text.SetFont(font) + + # Add components to sizer + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_code_text, 0, wx.EXPAND) + row_sizer.Add(self.method_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 24) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_name_text, 0, wx.EXPAND) + row_sizer.Add(self.method_name_text_ctrl, 1, wx.EXPAND | wx.LEFT, 20) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_type_text, 0, wx.EXPAND) + row_sizer.Add(self.method_type_combo, 1, wx.EXPAND | wx.LEFT, 26) + required_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(organization_text, 0, wx.EXPAND) + row_sizer.Add(self.organization_text_ctrl, 1, wx.EXPAND | wx.LEFT, 27) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(method_link_text, 0, wx.EXPAND) + row_sizer.Add(self.method_link_text_ctrl, 1, wx.EXPAND | wx.LEFT, 27) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(description_text, 0, wx.EXPAND) + row_sizer.Add(self.description_text_ctrl, 1, wx.EXPAND | wx.LEFT, 34) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) + main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + self.SetSizer(main_sizer) \ No newline at end of file diff --git a/odmtools/view/WizardProcessLevelView.py b/odmtools/view/WizardProcessLevelView.py new file mode 100644 index 0000000..475d75a --- /dev/null +++ b/odmtools/view/WizardProcessLevelView.py @@ -0,0 +1,45 @@ +import wx + + +class WizardProcessLevelView(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + # Create components + header_text = wx.StaticText(self, label="Processing Level") + static_line = wx.StaticLine(self, size=(-1, 12)) + required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) + level_code = wx.StaticText(self, label="Processing Level Code") + self.level_code_text_ctrl = wx.TextCtrl(self) + optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) + definition_text = wx.StaticText(self, label="Definition") + self.definition_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) + explanation_text = wx.StaticText(self, label="Explanation") + self.explanation_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) + + # Style components + font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) + header_text.SetFont(font) + + # Add components to sizer + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(level_code, 0, wx.EXPAND) + row_sizer.Add(self.level_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 30) + required_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(definition_text, 0, wx.EXPAND) + row_sizer.Add(self.definition_text_ctrl, 1, wx.EXPAND | wx.LEFT, 95) + optional_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(explanation_text, 0, wx.EXPAND) + row_sizer.Add(self.explanation_text_ctrl, 1, wx.EXPAND | wx.LEFT, 85) + optional_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) + main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + self.SetSizer(main_sizer) From e7acd05237d092a875327112e7e14833d340e69b Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Sat, 1 Oct 2016 13:14:48 -0600 Subject: [PATCH 08/19] Updated the variable page in the wizard --- .../WizardProcessLevelController.py | 4 +- .../controller/WizardVariableController.py | 13 ++++ odmtools/gui/wizSave.py | 14 +++- odmtools/view/WizardVariableView.py | 68 +++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 odmtools/controller/WizardVariableController.py create mode 100644 odmtools/view/WizardVariableView.py diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py index e33d9b4..6555e10 100644 --- a/odmtools/controller/WizardProcessLevelController.py +++ b/odmtools/controller/WizardProcessLevelController.py @@ -8,6 +8,6 @@ def __init__(self, parent): WizardPageSimple.__init__(self, parent) main_sizer = wx.BoxSizer(wx.VERTICAL) - self.method_view = WizardProcessLevelView(self) - main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.ALL, 0) + self.processing_level_view = WizardProcessLevelView(self) + main_sizer.Add(self.processing_level_view, 1, wx.EXPAND | wx.ALL, 0) self.SetSizer(main_sizer) \ No newline at end of file diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py new file mode 100644 index 0000000..8b0579d --- /dev/null +++ b/odmtools/controller/WizardVariableController.py @@ -0,0 +1,13 @@ +import wx +from odmtools.view.WizardVariableView import WizardVariableView +from wx.wizard import WizardPageSimple + + +class WizardVariableController(WizardPageSimple): + def __init__(self, parent): + WizardPageSimple.__init__(self, parent) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + self.variable_view = WizardVariableView(self) + main_sizer.Add(self.variable_view, 1, wx.EXPAND | wx.ALL, 0) + self.SetSizer(main_sizer) \ No newline at end of file diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index b950d18..957e5ef 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -25,6 +25,8 @@ ######################################################################## class QCLPage(wiz.WizardPageSimple): + # CLASS IS DEPECRATED + # REPLACED WITH WizardProcessLevelController.py def __init__(self, parent, title, series_service, qcl): """Constructor""" wiz.WizardPageSimple.__init__(self, parent) @@ -62,6 +64,8 @@ def _init_data(self, series): ######################################################################## class VariablePage(wiz.WizardPageSimple): + # CLASS IS DEPECRATED + # REPLACED WITH WizardVariableController.py def __init__(self, parent, title, service_manager, var): """Constructor""" wiz.WizardPageSimple.__init__(self, parent) @@ -108,7 +112,9 @@ def _init_data(self, series_service): ######################################################################## -class MethodPage(wiz.WizardPageSimple): # Raname this page to page method controller +class MethodPage(wiz.WizardPageSimple): + # THIS CLASS IS DEPECRATED + # REPLACED WITH WizardMethodController.py def __init__(self, parent): # pageMethod.pnlMethod.__init__(self, parent) wiz.WizardPageSimple.__init__(self, parent) @@ -211,6 +217,8 @@ def fill_summary(self): ######################################################################## from odmtools.controller.WizardMethodController import WizardMethodController from odmtools.controller.WizardProcessLevelController import WizardProcessLevelController +from odmtools.controller.WizardVariableController import WizardVariableController + class wizSave(wx.wizard.Wizard): def _init_ctrls(self, prnt): @@ -260,11 +268,11 @@ def __init__(self, parent, service_manager, record_service): self.pgIntro = pageIntro.pageIntro(self, "Intro") # self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) - # self.pgMethod = MethodPage(self) self.pgMethod = WizardMethodController(self) # self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) self.pgQCL = WizardProcessLevelController(self) - self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) + # self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) + self.pgVariable = WizardVariableController(self) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) self.pgSummary = SummaryPage(self, "Summary", self.series_service) diff --git a/odmtools/view/WizardVariableView.py b/odmtools/view/WizardVariableView.py new file mode 100644 index 0000000..31a1d15 --- /dev/null +++ b/odmtools/view/WizardVariableView.py @@ -0,0 +1,68 @@ +import wx + + +class WizardVariableView(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + # Create components + header_text = wx.StaticText(self, label="Variable") + static_line = wx.StaticLine(self, size=(-1, 12)) + + required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) + variable_code_text = wx.StaticText(self, label="Variable Code") + self.variable_code_text_ctrl = wx.TextCtrl(self) + variable_name_text = wx.StaticText(self, label="Variable Name") + self.variable_name_combo = wx.ComboBox(self) + variable_type_text = wx.StaticText(self, label="Variable Type") + self.variable_type_combo = wx.ComboBox(self) + no_data_value_text = wx.StaticText(self, label="No Data Value") + self.no_data_value_text_ctrl = wx.TextCtrl(self, value="-9999") + + optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) + speciation_text = wx.StaticText(self, label="Speciation") + self.speciation_combo = wx.ComboBox(self) + definition_text = wx.StaticText(self, label="Definition") + self.definition_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) + + # Style Components + font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) + header_text.SetFont(font) + + # Add components to sizer + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(variable_code_text, 0, wx.EXPAND) + row_sizer.Add(self.variable_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 30) + required_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(variable_name_text, 0, wx.EXPAND) + row_sizer.Add(self.variable_name_combo, 1, wx.EXPAND | wx.LEFT, 27) + required_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(variable_type_text, 0, wx.EXPAND) + row_sizer.Add(self.variable_type_combo, 1, wx.EXPAND | wx.LEFT, 33) + required_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(no_data_value_text, 0, wx.EXPAND) + row_sizer.Add(self.no_data_value_text_ctrl, 1, wx.EXPAND | wx.LEFT, 29) + required_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(speciation_text, 0, wx.EXPAND) + row_sizer.Add(self.speciation_combo, 1, wx.EXPAND | wx.LEFT, 48) + optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) + + row_sizer = wx.BoxSizer(wx.HORIZONTAL) + row_sizer.Add(definition_text, 0, wx.EXPAND) + row_sizer.Add(self.definition_text_ctrl, 1, wx.EXPAND | wx.LEFT, 52) + optional_static_box_sizer.Add(row_sizer, 1, wx.EXPAND | wx.ALL, 5) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) + main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) + self.SetSizer(main_sizer) From b49bbe67c876b11650c6f9c25373b573ab81e78d Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Tue, 4 Oct 2016 17:32:35 -0600 Subject: [PATCH 09/19] Variable page now has content to select from Removed some warnings, and various improvements and bug fixes --- odmtools/controller/WizardMethodController.py | 8 ++++-- .../controller/WizardVariableController.py | 16 ++++++++++-- odmtools/controller/frmDBConfig.py | 26 ++++++++++++++++++- odmtools/controller/olvDataTable.py | 2 +- odmtools/gui/frmODMTools.py | 5 ---- odmtools/gui/plotProbability.py | 7 ++--- odmtools/gui/wizSave.py | 4 +-- odmtools/view/WizardMethodView.py | 14 +++++++--- odmtools/view/WizardVariableView.py | 9 ++++--- 9 files changed, 66 insertions(+), 25 deletions(-) diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py index 60dee6f..f357a25 100644 --- a/odmtools/controller/WizardMethodController.py +++ b/odmtools/controller/WizardMethodController.py @@ -4,10 +4,14 @@ class WizardMethodController(WizardPageSimple): - def __init__(self, parent): + def __init__(self, parent, series): WizardPageSimple.__init__(self, parent) main_sizer = wx.BoxSizer(wx.VERTICAL) self.method_view = WizardMethodView(self) main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.ALL, 0) - self.SetSizer(main_sizer) \ No newline at end of file + self.SetSizer(main_sizer) + + self.series = series + + self.method_view.method_type_combo.AppendItems(["ABC"]) diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py index 8b0579d..1076d4f 100644 --- a/odmtools/controller/WizardVariableController.py +++ b/odmtools/controller/WizardVariableController.py @@ -4,10 +4,22 @@ class WizardVariableController(WizardPageSimple): - def __init__(self, parent): + def __init__(self, parent, service_manager): WizardPageSimple.__init__(self, parent) + self.service_manager = service_manager main_sizer = wx.BoxSizer(wx.VERTICAL) self.variable_view = WizardVariableView(self) main_sizer.Add(self.variable_view, 1, wx.EXPAND | wx.ALL, 0) - self.SetSizer(main_sizer) \ No newline at end of file + self.SetSizer(main_sizer) + self.__fetch_data() + + def __fetch_data(self): + cv_service = self.service_manager.get_cv_service() + name_list = [x.term for x in cv_service.get_variable_name_cvs()] + var_unit = [x.name for x in cv_service.get_units_names()] + spec_list = [x.term for x in cv_service.get_speciation_cvs()] + + self.variable_view.variable_name_combo.AppendItems(name_list) + self.variable_view.speciation_combo.AppendItems(spec_list) + self.variable_view.variable_type_combo.AppendItems(var_unit) diff --git a/odmtools/controller/frmDBConfig.py b/odmtools/controller/frmDBConfig.py index 6ef3005..be7e1eb 100644 --- a/odmtools/controller/frmDBConfig.py +++ b/odmtools/controller/frmDBConfig.py @@ -113,8 +113,10 @@ def validateInput(self, conn_dict): # Returns a dictionary of the database values entered in the form def getFieldValues(self): - conn_dict = {} + if self.isFormEmpty(): + return {} + conn_dict = {} conn_dict['engine'] = self.choices[self.cbDatabaseType.GetValue()] conn_dict['user'] = self.txtUser.GetValue() conn_dict['password'] = self.txtPass.GetValue() @@ -123,6 +125,28 @@ def getFieldValues(self): return conn_dict + def isFormEmpty(self): + """ + If any of the entries are empty return true + :return: + """ + if not self.cbDatabaseType.GetValue().strip(): + return True + + if not self.txtUser.GetValue().strip(): + return True + + if not self.txtPass.GetValue().strip(): + return True + + if not self.txtServer.GetValue().strip(): + return True + + if not self.txtDBName.GetValue().strip(): + return True + + return False + def set_field_values(self): conn = self.service_manager.is_valid_connection() if conn is not None: diff --git a/odmtools/controller/olvDataTable.py b/odmtools/controller/olvDataTable.py index 58110d1..975f26c 100644 --- a/odmtools/controller/olvDataTable.py +++ b/odmtools/controller/olvDataTable.py @@ -31,7 +31,7 @@ def init(self, memDB): self.SetColumns(columns) self.dataframe = self.memDB.getDataValuesDF() sort_by_index = list(self.dataframe.columns).index("LocalDateTime") - self.dataframe.sort(self.dataframe.columns[sort_by_index], inplace=True) + self.dataframe.sort_values(self.dataframe.columns[sort_by_index], inplace=True) self.dataObjects = self.dataframe.values.tolist() self.SetObjectGetter(self.ObjectGetter) diff --git a/odmtools/gui/frmODMTools.py b/odmtools/gui/frmODMTools.py index d48f642..a064f2c 100644 --- a/odmtools/gui/frmODMTools.py +++ b/odmtools/gui/frmODMTools.py @@ -122,14 +122,9 @@ def _init_s_Items(self, parent): def _init_database(self, quit_if_cancel=True): logger.info("Loading Database...") - - - while True: ## Database connection is valid, therefore proceed through the rest of the program if self.service_manager.is_valid_connection(): - conn_dict = None - series_service = self.createService() conn_dict = self.service_manager.get_current_conn_dict() diff --git a/odmtools/gui/plotProbability.py b/odmtools/gui/plotProbability.py index 6e23761..0cb230d 100644 --- a/odmtools/gui/plotProbability.py +++ b/odmtools/gui/plotProbability.py @@ -133,11 +133,8 @@ def updatePlot(self): self.plots.set_title("\n".join(textwrap.wrap(oneSeries.siteName, 55))) if len(oneSeries.dataTable) > 0: - #self.prob.append( - #prop = oneSeries.Probability.plot(column="DataValue", ax=self.plots) - #todo FutureWarning: order is deprecated, use sort_values(...) - xValues = oneSeries.Probability.xAxis.order().values - yValues = oneSeries.Probability.yAxis.order().values + xValues = oneSeries.Probability.xAxis.sort_values().values + yValues = oneSeries.Probability.yAxis.sort_values().values ax = self.plots.plot(xValues, yValues, 'bs', color=oneSeries.color, label=oneSeries.plotTitle) diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index 957e5ef..fed04da 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -268,11 +268,11 @@ def __init__(self, parent, service_manager, record_service): self.pgIntro = pageIntro.pageIntro(self, "Intro") # self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) - self.pgMethod = WizardMethodController(self) + self.pgMethod = WizardMethodController(self, self.series_service) # self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) self.pgQCL = WizardProcessLevelController(self) # self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) - self.pgVariable = WizardVariableController(self) + self.pgVariable = WizardVariableController(self, service_manager=service_manager) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) self.pgSummary = SummaryPage(self, "Summary", self.series_service) diff --git a/odmtools/view/WizardMethodView.py b/odmtools/view/WizardMethodView.py index 1891c66..3799290 100644 --- a/odmtools/view/WizardMethodView.py +++ b/odmtools/view/WizardMethodView.py @@ -1,10 +1,14 @@ import wx +import wx.lib.scrolledpanel -class WizardMethodView(wx.Panel): # Rename this to page method view +class WizardMethodView(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) + # content_panel = wx.lib.scrolledpanel.ScrolledPanel(self) + # content_panel.SetupScrolling() + # Create components header_text = wx.StaticText(self, label="Method") static_line = wx.StaticLine(self, size=(-1, 12)) @@ -15,11 +19,11 @@ def __init__(self, parent): method_name_text = wx.StaticText(self, label="Method Name") self.method_name_text_ctrl = wx.TextCtrl(self) method_type_text = wx.StaticText(self, label="Method Type") - self.method_type_combo = wx.ComboBox(self) + self.method_type_combo = wx.ComboBox(self, choices=["---"], style=wx.CB_READONLY | wx.CB_SORT) optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) organization_text = wx.StaticText(self, label="Organization") - self.organization_text_ctrl = wx.TextCtrl(self) + self.organization_combo = wx.ComboBox(self, choices=["---"], style=wx.CB_READONLY | wx.CB_SORT) method_link_text = wx.StaticText(self, label="Method Link") self.method_link_text_ctrl = wx.TextCtrl(self) description_text = wx.StaticText(self, label="Description") @@ -28,6 +32,8 @@ def __init__(self, parent): # Style Components font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) header_text.SetFont(font) + self.method_type_combo.SetSelection(0) + self.organization_combo.SetSelection(0) # Add components to sizer row_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -47,7 +53,7 @@ def __init__(self, parent): row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(organization_text, 0, wx.EXPAND) - row_sizer.Add(self.organization_text_ctrl, 1, wx.EXPAND | wx.LEFT, 27) + row_sizer.Add(self.organization_combo, 1, wx.EXPAND | wx.LEFT, 27) optional_static_box_sizer.Add(row_sizer, 0, wx.EXPAND | wx.ALL, 5) row_sizer = wx.BoxSizer(wx.HORIZONTAL) diff --git a/odmtools/view/WizardVariableView.py b/odmtools/view/WizardVariableView.py index 31a1d15..1040a20 100644 --- a/odmtools/view/WizardVariableView.py +++ b/odmtools/view/WizardVariableView.py @@ -13,21 +13,24 @@ def __init__(self, parent): variable_code_text = wx.StaticText(self, label="Variable Code") self.variable_code_text_ctrl = wx.TextCtrl(self) variable_name_text = wx.StaticText(self, label="Variable Name") - self.variable_name_combo = wx.ComboBox(self) + self.variable_name_combo = wx.ComboBox(self, choices=["---"], style=wx.CB_READONLY | wx.CB_SORT) variable_type_text = wx.StaticText(self, label="Variable Type") - self.variable_type_combo = wx.ComboBox(self) + self.variable_type_combo = wx.ComboBox(self, choices=["---"], style=wx.CB_READONLY | wx.CB_SORT) no_data_value_text = wx.StaticText(self, label="No Data Value") self.no_data_value_text_ctrl = wx.TextCtrl(self, value="-9999") optional_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Optional Fields"), orient=wx.VERTICAL) speciation_text = wx.StaticText(self, label="Speciation") - self.speciation_combo = wx.ComboBox(self) + self.speciation_combo = wx.ComboBox(self, choices=["---"], style=wx.CB_READONLY | wx.CB_SORT) definition_text = wx.StaticText(self, label="Definition") self.definition_text_ctrl = wx.TextCtrl(self, size=(-1, 75)) # Style Components font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) header_text.SetFont(font) + self.variable_name_combo.SetSelection(0) + self.speciation_combo.SetSelection(0) + self.variable_type_combo.SetSelection(0) # Add components to sizer row_sizer = wx.BoxSizer(wx.HORIZONTAL) From d53fc8bb0118ec8e403da792edde82e33337bad1 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Tue, 4 Oct 2016 19:08:38 -0600 Subject: [PATCH 10/19] Redesigned the wizard pages to include the old the form plus the new --- odmtools/controller/WizardMethodController.py | 5 +- .../WizardProcessLevelController.py | 7 +- .../controller/WizardVariableController.py | 8 +- odmtools/view/CustomListCtrl.py | 176 ++++++++++++++++++ odmtools/view/WizardMethodView.py | 19 +- odmtools/view/WizardProcessLevelView.py | 13 +- odmtools/view/WizardVariableView.py | 18 +- 7 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 odmtools/view/CustomListCtrl.py diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py index f357a25..e135448 100644 --- a/odmtools/controller/WizardMethodController.py +++ b/odmtools/controller/WizardMethodController.py @@ -9,9 +9,10 @@ def __init__(self, parent, series): main_sizer = wx.BoxSizer(wx.VERTICAL) self.method_view = WizardMethodView(self) - main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.ALL, 0) + main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.RIGHT, -16) # Sufficient to hide the scroll bar self.SetSizer(main_sizer) self.series = series - + table_columns = ["Descriptions", "Link", "ID"] + self.method_view.existing_method_table.set_columns(table_columns) self.method_view.method_type_combo.AppendItems(["ABC"]) diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py index 6555e10..5d7b296 100644 --- a/odmtools/controller/WizardProcessLevelController.py +++ b/odmtools/controller/WizardProcessLevelController.py @@ -9,5 +9,8 @@ def __init__(self, parent): main_sizer = wx.BoxSizer(wx.VERTICAL) self.processing_level_view = WizardProcessLevelView(self) - main_sizer.Add(self.processing_level_view, 1, wx.EXPAND | wx.ALL, 0) - self.SetSizer(main_sizer) \ No newline at end of file + main_sizer.Add(self.processing_level_view, 1, wx.EXPAND | wx.RIGHT, -16) + self.SetSizer(main_sizer) + + table_columns = ["Code", "Definition", "Explanation"] + self.processing_level_view.existing_process_table.set_columns(table_columns) \ No newline at end of file diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py index 1076d4f..ae6d834 100644 --- a/odmtools/controller/WizardVariableController.py +++ b/odmtools/controller/WizardVariableController.py @@ -10,8 +10,14 @@ def __init__(self, parent, service_manager): self.service_manager = service_manager main_sizer = wx.BoxSizer(wx.VERTICAL) self.variable_view = WizardVariableView(self) - main_sizer.Add(self.variable_view, 1, wx.EXPAND | wx.ALL, 0) + main_sizer.Add(self.variable_view, 1, wx.EXPAND | wx.RIGHT, -16) self.SetSizer(main_sizer) + + table_columns = ["Code", "Name", "Speciation", "Units", + "Sample Medium", "Value Type", "IsRegular", "Time Support", + "Time Units", "DataType", "Genaral Category", "NoDataValue"] + self.variable_view.variable_table.set_columns(table_columns) + self.__fetch_data() def __fetch_data(self): diff --git a/odmtools/view/CustomListCtrl.py b/odmtools/view/CustomListCtrl.py new file mode 100644 index 0000000..61fd4dc --- /dev/null +++ b/odmtools/view/CustomListCtrl.py @@ -0,0 +1,176 @@ +import wx +import sys + + +# Place the CustomListCtrl on a panel so it does not grow horizontally +class CustomListCtrl(wx.ListCtrl): + def __init__(self, panel): + wx.ListCtrl.__init__(self, panel, style=wx.LC_REPORT) + self._auto_width_style = wx.LIST_AUTOSIZE + if sys.platform == "win32": + self._auto_width_style = wx.LIST_AUTOSIZE_USEHEADER + + # Message to show in the ListCtrl when it is empty + self.empty_list_message = wx.StaticText(parent=self, label="This list is empty", + style=wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE | wx.FULL_REPAINT_ON_RESIZE) + self.empty_list_message.Hide() + self.empty_list_message.SetForegroundColour(wx.LIGHT_GREY) + self.empty_list_message.SetBackgroundColour(self.GetBackgroundColour()) + self.empty_list_message.SetFont(wx.Font(24, wx.DEFAULT, wx.NORMAL, wx.NORMAL, 0, "")) + self.Bind(wx.EVT_LIST_COL_CLICK, self.on_list_column_clicked) + + def on_list_column_clicked(self, event): + self.__sort_table_by_column(event.GetColumn()) + + def alternate_row_color(self, color="#DCEBEE"): + for i in range(self.GetItemCount()): + if i % 2 == 0: + self.SetItemBackgroundColour(i, color) + + def auto_size_table(self): + for i in range(self.GetColumnCount()): + self.SetColumnWidth(col=i, width=self._auto_width_style) + + def clear_table(self): + """ + Clears everything in the table including the header names + :return: + """ + self.ClearAll() + + def clear_content(self): + """ + Clears everything in the table except the header names + :return: + """ + self.DeleteAllItems() + + def get_all_selected_rows(self): + """ + Gets all the selected rows + :return: 2D list, list(rows) + """ + index = self.GetFirstSelected() + if index == -1: + return [] # No selected row + + data = [self.get_row_at_index(index)] + for i in range(self.GetSelectedItemCount() - 1): + index = self.GetNextSelected(index) + data.append(self.get_row_at_index(index)) + + return data + + def get_row_at_index(self, index): + """ + Given a row number, return all the data related to the row + :param index: int + :return: list + """ + if index > self.GetItemCount(): + return [] # There are not that many rows in the table + + data = [] + for i in range(self.GetColumnCount()): + data.append(self.GetItem(index, i).GetText()) + return data + + def get_all_data_in_table(self): + data = [] + for i in range(self.GetItemCount()): + data.append(self.get_row_at_index(i)) + return data + + def __sort_table_by_column(self, column_number): + """ + Flip flop between sorting by ascending and descending order. + :param column_number: type(int) + :return: + """ + if column_number < 0: + return + + data = self.get_all_data_in_table() + + column_data = [] + for row in data: + column_data.append(row[column_number]) + + if sorted(column_data) == column_data: + column_data.reverse() + else: + column_data.sort() + + new_data = [] + for i in range(len(column_data)): + for j in range(len(data)): + if column_data[i] in data[j]: + if data[j] not in new_data: + new_data.append(data[j]) + break + + # write the data back + self.clear_content() + self.set_table_content(new_data) + + def get_selected_row(self): + """ + Gets the first selected row + :return: data: type(list). Return None if no row is selected + """ + row_number = self.GetFirstSelected() + if row_number == -1: + return None + + data = [] + for i in range(self.GetColumnCount()): + data.append(self.GetItem(row_number, i).GetText()) + return data + + def remove_selected_row(self): + """ + Only removes the top selected row + :return: + """ + row_number = self.GetFirstSelected() + if row_number == -1: + return # No row is selected + + self.DeleteItem(row_number) + + def set_columns(self, columns): + """ + Sets the name of the columns + :param columns: a list of strings + :return: + """ + self.clear_table() + for i in range(len(columns)): + self.InsertColumn(i, columns[i], width=wx.LIST_AUTOSIZE_USEHEADER) + + def set_empty_message_text(self, text): + self.empty_list_message.SetLabel(text) + + def set_table_content(self, data): + """ + data must be a 2D list [[row1 column1, row1 column2, ...], [row2, column1, row2 column2, ...]] + :param data: 2D list + :return: + """ + + number_of_columns = self.GetColumnCount() + if number_of_columns == 0: + print "No column headers have been created" + return + + # loop through all of the site metadata + for i in range(len(data)): + index = self.InsertStringItem(999999, "") + if number_of_columns < len(data[i]): + raise Exception("The length of the row must match the number of columns") + + for j in range(len(data[i])): + self.SetStringItem(index, j, str(data[i][j])) + + self.auto_size_table() + self.alternate_row_color() \ No newline at end of file diff --git a/odmtools/view/WizardMethodView.py b/odmtools/view/WizardMethodView.py index 3799290..ee6b25f 100644 --- a/odmtools/view/WizardMethodView.py +++ b/odmtools/view/WizardMethodView.py @@ -1,17 +1,19 @@ import wx import wx.lib.scrolledpanel +from odmtools.view.CustomListCtrl import CustomListCtrl -class WizardMethodView(wx.Panel): +class WizardMethodView(wx.lib.scrolledpanel.ScrolledPanel): def __init__(self, parent): - wx.Panel.__init__(self, parent) - - # content_panel = wx.lib.scrolledpanel.ScrolledPanel(self) - # content_panel.SetupScrolling() + wx.lib.scrolledpanel.ScrolledPanel.__init__(self, parent) # Create components header_text = wx.StaticText(self, label="Method") static_line = wx.StaticLine(self, size=(-1, 12)) + self.auto_method_radio = wx.RadioButton(self, label="Automatically generate a method") + self.existing_method_radio = wx.RadioButton(self, label="Select an existing method") + self.existing_method_table = CustomListCtrl(self) + self.create_method_radio = wx.RadioButton(self, label="Create a new method") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) method_code_text = wx.StaticText(self, label="Method Code") @@ -34,6 +36,7 @@ def __init__(self, parent): header_text.SetFont(font) self.method_type_combo.SetSelection(0) self.organization_combo.SetSelection(0) + self.SetupScrolling() # Add components to sizer row_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -69,6 +72,10 @@ def __init__(self, parent): main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.auto_method_radio, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(self.existing_method_radio, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.existing_method_table, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.create_method_radio, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) - self.SetSizer(main_sizer) \ No newline at end of file + self.SetSizer(main_sizer) diff --git a/odmtools/view/WizardProcessLevelView.py b/odmtools/view/WizardProcessLevelView.py index 475d75a..d69a066 100644 --- a/odmtools/view/WizardProcessLevelView.py +++ b/odmtools/view/WizardProcessLevelView.py @@ -1,13 +1,18 @@ import wx +import wx.lib.scrolledpanel +from odmtools.view.CustomListCtrl import CustomListCtrl -class WizardProcessLevelView(wx.Panel): +class WizardProcessLevelView(wx.lib.scrolledpanel.ScrolledPanel): def __init__(self, parent): - wx.Panel.__init__(self, parent) + wx.lib.scrolledpanel.ScrolledPanel.__init__(self, parent) # Create components header_text = wx.StaticText(self, label="Processing Level") static_line = wx.StaticLine(self, size=(-1, 12)) + self.existing_process_radio = wx.RadioButton(self, label="Select an existing Processing Level") + self.existing_process_table = CustomListCtrl(self) + self.create_process_level_radio = wx.RadioButton(self, label="Create Processing Level") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) level_code = wx.StaticText(self, label="Processing Level Code") self.level_code_text_ctrl = wx.TextCtrl(self) @@ -20,6 +25,7 @@ def __init__(self, parent): # Style components font = wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD) header_text.SetFont(font) + self.SetupScrolling() # Add components to sizer row_sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -40,6 +46,9 @@ def __init__(self, parent): main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.existing_process_radio, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(self.existing_process_table, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(self.create_process_level_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) self.SetSizer(main_sizer) diff --git a/odmtools/view/WizardVariableView.py b/odmtools/view/WizardVariableView.py index 1040a20..3b6ec1d 100644 --- a/odmtools/view/WizardVariableView.py +++ b/odmtools/view/WizardVariableView.py @@ -1,13 +1,19 @@ import wx +import wx.lib.scrolledpanel +from odmtools.view.CustomListCtrl import CustomListCtrl -class WizardVariableView(wx.Panel): +class WizardVariableView(wx.lib.scrolledpanel.ScrolledPanel): def __init__(self, parent): - wx.Panel.__init__(self, parent) + wx.lib.scrolledpanel.ScrolledPanel.__init__(self, parent) # Create components header_text = wx.StaticText(self, label="Variable") static_line = wx.StaticLine(self, size=(-1, 12)) + table_panel = wx.Panel(self) + self.current_variable_radio = wx.RadioButton(self, label="Use current variable") + self.variable_table = CustomListCtrl(table_panel) + self.existing_variable_radio = wx.RadioButton(self, label="Select an existing variable") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) variable_code_text = wx.StaticText(self, label="Variable Code") @@ -31,8 +37,13 @@ def __init__(self, parent): self.variable_name_combo.SetSelection(0) self.speciation_combo.SetSelection(0) self.variable_type_combo.SetSelection(0) + self.SetupScrolling() # Add components to sizer + table_sizer = wx.BoxSizer() + table_sizer.Add(self.variable_table, 0, wx.EXPAND | wx.ALL, 0) + table_panel.SetSizerAndFit(table_sizer) + row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(variable_code_text, 0, wx.EXPAND) row_sizer.Add(self.variable_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 30) @@ -66,6 +77,9 @@ def __init__(self, parent): main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.current_variable_radio, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(self.existing_variable_radio, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(table_panel, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) self.SetSizer(main_sizer) From b220daa512162c81e209f9b83cab62f09ace3429 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Thu, 6 Oct 2016 12:46:17 -0600 Subject: [PATCH 11/19] Creating new series in wizard retains the previous data All that is left is to setup the functionality of creating a new series --- ODMTools.py | 5 ---- doc/wxFormBuilder/clsIntro.py | 2 +- odmtools/common/taskServer.py | 2 +- odmtools/controller/WizardMethodController.py | 17 +++++++++-- .../WizardProcessLevelController.py | 23 ++++++++++++-- .../controller/WizardVariableController.py | 30 ++++++++++++++++++- odmtools/controller/frmSeriesSelector.py | 12 ++++---- odmtools/controller/logicEditTools.py | 2 +- odmtools/controller/logicPlotOptions.py | 4 +-- odmtools/controller/olvSeriesSelector.py | 4 +-- odmtools/controller/pageExisting.py | 6 ++-- odmtools/gui/mnuRibbon.py | 8 ++--- odmtools/gui/wizSave.py | 8 ++--- odmtools/odmdata/memory_database.py | 4 +-- odmtools/odmdata/series.py | 2 +- odmtools/odmservices/cv_service.py | 1 + odmtools/odmservices/edit_service.py | 28 ++++++++--------- odmtools/odmservices/series_service.py | 10 +++---- odmtools/view/WizardMethodView.py | 9 ++++-- odmtools/view/WizardProcessLevelView.py | 9 ++++-- odmtools/view/clsIntro.py | 4 +-- tests/test_gui/test_plotProbability.py | 2 +- tests/test_gui/test_wizSave.py | 2 +- tests/test_odmdata/test_pandas_memory_db.py | 2 +- tests/test_odmservices/test_edit_service.py | 4 +-- tests/test_odmservices/test_series_service.py | 18 +++++------ tests/test_util.py | 2 +- 27 files changed, 142 insertions(+), 78 deletions(-) diff --git a/ODMTools.py b/ODMTools.py index ecbea16..7552dbe 100755 --- a/ODMTools.py +++ b/ODMTools.py @@ -60,11 +60,6 @@ def OnInit(self): app= self.frame return True -def runODM(): - app = wx.App(False) - #frame = create(None) - #frame.Show() - app.MainLoop() if __name__ == '__main__': logger.info("Welcome to ODMTools Python. Please wait as system loads") diff --git a/doc/wxFormBuilder/clsIntro.py b/doc/wxFormBuilder/clsIntro.py index dd3d888..094fe78 100644 --- a/doc/wxFormBuilder/clsIntro.py +++ b/doc/wxFormBuilder/clsIntro.py @@ -29,7 +29,7 @@ def __init__( self, parent ): bSizer2 = wx.BoxSizer( wx.VERTICAL ) - self.lblHow = wx.StaticText( self.m_panel2, wx.ID_ANY, u"How would you like to save the series?", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.lblHow = wx.StaticText( self.m_panel2, wx.ID_ANY, u"How would you like to save the series_service?", wx.DefaultPosition, wx.DefaultSize, 0 ) self.lblHow.Wrap( -1 ) bSizer2.Add( self.lblHow, 0, wx.ALL, 15 ) diff --git a/odmtools/common/taskServer.py b/odmtools/common/taskServer.py index 0620112..85b949e 100644 --- a/odmtools/common/taskServer.py +++ b/odmtools/common/taskServer.py @@ -131,7 +131,7 @@ def worker(cls, dispatcher): if task_type == "InitEditValues": connection = SeriesService("sqlite:///:memory:") df = task[1] - logger.debug("Load series from db") + logger.debug("Load series_service from db") df.to_sql(name="DataValues", con=connection._session_factory.engine, flavor='sqlite', index = False, chunksize = 10000) logger.debug("done loading database") result = connection diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py index e135448..1d22a49 100644 --- a/odmtools/controller/WizardMethodController.py +++ b/odmtools/controller/WizardMethodController.py @@ -4,7 +4,7 @@ class WizardMethodController(WizardPageSimple): - def __init__(self, parent, series): + def __init__(self, parent, series_service): WizardPageSimple.__init__(self, parent) main_sizer = wx.BoxSizer(wx.VERTICAL) @@ -12,7 +12,20 @@ def __init__(self, parent, series): main_sizer.Add(self.method_view, 1, wx.EXPAND | wx.RIGHT, -16) # Sufficient to hide the scroll bar self.SetSizer(main_sizer) - self.series = series + self.series_service = series_service table_columns = ["Descriptions", "Link", "ID"] self.method_view.existing_method_table.set_columns(table_columns) self.method_view.method_type_combo.AppendItems(["ABC"]) + self.__fetch_data() + + def __fetch_data(self): + methods = self.series_service.get_all_methods() + data = [] + for meth in methods: + data.append([ + meth.description, + meth.link, + meth.id + ]) + + self.method_view.existing_method_table.set_table_content(data=data) diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py index 5d7b296..87dd78b 100644 --- a/odmtools/controller/WizardProcessLevelController.py +++ b/odmtools/controller/WizardProcessLevelController.py @@ -4,13 +4,30 @@ class WizardProcessLevelController(WizardPageSimple): - def __init__(self, parent): + def __init__(self, parent, service_manager): WizardPageSimple.__init__(self, parent) + self.service_manager = service_manager main_sizer = wx.BoxSizer(wx.VERTICAL) self.processing_level_view = WizardProcessLevelView(self) main_sizer.Add(self.processing_level_view, 1, wx.EXPAND | wx.RIGHT, -16) self.SetSizer(main_sizer) - table_columns = ["Code", "Definition", "Explanation"] - self.processing_level_view.existing_process_table.set_columns(table_columns) \ No newline at end of file + table_columns = ["Code", "Definition", "Explanation", "ID"] + self.processing_level_view.existing_process_table.set_columns(table_columns) + self.__fetch_data() + + def __fetch_data(self): + series_service = self.service_manager.get_series_service() + processes = series_service.get_all_qcls() + + data = [] + for proc in processes: + data.append([ + proc.code, + proc.definition, + proc.explanation, + proc.id + ]) + + self.processing_level_view.existing_process_table.set_table_content(data=data) diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py index ae6d834..eadc178 100644 --- a/odmtools/controller/WizardVariableController.py +++ b/odmtools/controller/WizardVariableController.py @@ -15,12 +15,14 @@ def __init__(self, parent, service_manager): table_columns = ["Code", "Name", "Speciation", "Units", "Sample Medium", "Value Type", "IsRegular", "Time Support", - "Time Units", "DataType", "Genaral Category", "NoDataValue"] + "Time Units", "DataType", "Genaral Category", "NoDataValue", "ID"] self.variable_view.variable_table.set_columns(table_columns) self.__fetch_data() def __fetch_data(self): + self.__populate_variable_table() + cv_service = self.service_manager.get_cv_service() name_list = [x.term for x in cv_service.get_variable_name_cvs()] var_unit = [x.name for x in cv_service.get_units_names()] @@ -29,3 +31,29 @@ def __fetch_data(self): self.variable_view.variable_name_combo.AppendItems(name_list) self.variable_view.speciation_combo.AppendItems(spec_list) self.variable_view.variable_type_combo.AppendItems(var_unit) + + def __populate_variable_table(self): + series_serivce = self.service_manager.get_series_service() + variables = series_serivce.get_all_variables() + data = [] + for var in variables: + data.append([var.code, + var.name, + var.speciation, + var.variable_unit.name, + var.sample_medium, + var.value_type, + var.is_regular, + var.time_support, + var.time_unit.name, + var.data_type, + var.general_category, + var.no_data_value, + var.id]) + + self.variable_view.variable_table.set_table_content(data=data) + + + + + diff --git a/odmtools/controller/frmSeriesSelector.py b/odmtools/controller/frmSeriesSelector.py index c06324e..b7d7327 100644 --- a/odmtools/controller/frmSeriesSelector.py +++ b/odmtools/controller/frmSeriesSelector.py @@ -61,7 +61,7 @@ def resetDB(self, series_service): self.Layout() def initTableSeries(self): - """Set up columns and objects to be used in the objectlistview to be visible in the series selector + """Set up columns and objects to be used in the objectlistview to be visible in the series_service selector :return: """ @@ -80,7 +80,7 @@ def initTableSeries(self): #self.tblSeries.SaveObject(object) def refreshTableSeries(self, db): - """ Refreshes the objectlistview to include newly saved database series and preserve which series was 'checked' + """ Refreshes the objectlistview to include newly saved database series_service and preserve which series_service was 'checked' for plotting/editing :return: @@ -474,7 +474,7 @@ def setFilter(self, site_code='', var_code='', advfilter=''): def onReadyToPlot(self, event): - """Plots a series selected from the series selector + """Plots a series_service selected from the series_service selector :param event: EVT_OVL_CHECK_EVENT type """ @@ -529,7 +529,7 @@ def getSelectedObject(self, event): def onReadyToEdit(self): - """Choose a series to edit from the series selector + """Choose a series_service to edit from the series_service selector :return: """ @@ -565,8 +565,8 @@ def onReadyToEdit(self): return True, object.id#, self.memDB else: isSelected = False - logger.debug("series was not checked") - val_2 = wx.MessageBox("Visualization is limited to 6 series.", "Can't add plot", + logger.debug("series_service was not checked") + val_2 = wx.MessageBox("Visualization is limited to 6 series_service.", "Can't add plot", wx.OK | wx.ICON_INFORMATION) diff --git a/odmtools/controller/logicEditTools.py b/odmtools/controller/logicEditTools.py index b88e35c..2504bd8 100644 --- a/odmtools/controller/logicEditTools.py +++ b/odmtools/controller/logicEditTools.py @@ -19,7 +19,7 @@ def __init__(self, parent, script, edit_service, connection_string, record=Fals self._record = record self._serv_man = parent - self._edit_error = "no series selected for editing" + self._edit_error = "no series_service selected for editing" self._add_point_req_error = "A required field was left empty" self._add_point_format_error = "A date is not formatted correctly" diff --git a/odmtools/controller/logicPlotOptions.py b/odmtools/controller/logicPlotOptions.py index 25f69b6..e074eb1 100644 --- a/odmtools/controller/logicPlotOptions.py +++ b/odmtools/controller/logicPlotOptions.py @@ -293,7 +293,7 @@ def getSeriesInfo(self, seriesID): series = self.getSeriesById(seriesID) if not series: - message = "Please check your database connection. Unable to retrieve series %d from the database" % seriesID + message = "Please check your database connection. Unable to retrieve series_service %d from the database" % seriesID wx.MessageBox(message, 'ODMTool Python', wx.OK | wx.ICON_EXCLAMATION) return @@ -339,7 +339,7 @@ def updateDateRange(self, startDate=None, endDate=None): self.currentStart = startDate self.currentEnd = endDate else: - #this returns the series to its full daterange + #this returns the series_service to its full daterange data = self.memDB.getDataValuesforGraph(key, seriesInfo.noDataValue, seriesInfo.startDate, seriesInfo.endDate) self.isSubsetted = False diff --git a/odmtools/controller/olvSeriesSelector.py b/odmtools/controller/olvSeriesSelector.py index f49fdb0..b3f510d 100644 --- a/odmtools/controller/olvSeriesSelector.py +++ b/odmtools/controller/olvSeriesSelector.py @@ -52,7 +52,7 @@ def _buildColumns(self): for key, value in series.returnDict().iteritems()] self.SetColumns(seriesColumns) - """User can select series using the mouse to click on check boxes """ + """User can select series_service using the mouse to click on check boxes """ def _HandleLeftDownOnImage(self, rowIndex, subItemIndex): """ @@ -80,7 +80,7 @@ def _HandleLeftDownOnImage(self, rowIndex, subItemIndex): self.RefreshIndex(rowIndex, modelObject) else: - wx.MessageBox("Visualization is limited to {0} series.".format(self.allowedLimit), "Can't add plot", + wx.MessageBox("Visualization is limited to {0} series_service.".format(self.allowedLimit), "Can't add plot", wx.OK | wx.ICON_INFORMATION) else: if checkedlen > 0: diff --git a/odmtools/controller/pageExisting.py b/odmtools/controller/pageExisting.py index 0544cfe..4d92291 100644 --- a/odmtools/controller/pageExisting.py +++ b/odmtools/controller/pageExisting.py @@ -20,7 +20,7 @@ def __init__(self, parent, title, series_service , site): sizer = wx.BoxSizer(wx.VERTICAL) self.sizer = sizer self.SetSizer(sizer) - #self.series = series + #self.series_service = series_service title = wx.StaticText(self, -1, title) title.SetFont(wx.Font(18, wx.SWISS, wx.NORMAL, wx.BOLD)) @@ -28,7 +28,7 @@ def __init__(self, parent, title, series_service , site): sizer.Add(wx.StaticLine(self, -1), 5, wx.EXPAND | wx.ALL, 5) self.pnlExisting = clsExisting.pnlExisting(self) #, id=wxID_PNLEXISTING, name=u'pnlExisting', #pos=wx.Point(536, 285), size=wx.Size(439, 357), - #style=wx.TAB_TRAVERSAL)#, sm = service_man, series = series) + #style=wx.TAB_TRAVERSAL)#, sm = service_man, series_service = series_service) self.sizer.Add(self.pnlExisting, 85, wx.ALL, 5) self._init_data(series_service, site.id) @@ -70,7 +70,7 @@ def getSeries(self): return selectedObject.method, selectedObject.quality_control_level, selectedObject.variable def initTable(self, dbservice, site_id): - """Set up columns and objects to be used in the objectlistview to be visible in the series selector""" + """Set up columns and objects to be used in the objectlistview to be visible in the series_service selector""" seriesColumns = [clsExisting.ColumnDefn(key, align="left", minimumWidth=-1, valueGetter=value, diff --git a/odmtools/gui/mnuRibbon.py b/odmtools/gui/mnuRibbon.py index 360f6cc..7c4f2f9 100644 --- a/odmtools/gui/mnuRibbon.py +++ b/odmtools/gui/mnuRibbon.py @@ -514,7 +514,7 @@ def onEditSeries(self, event=None): Publisher.sendMessage(("stopEdit"), event=event) #self.parent.stopEdit() - # # Start editing right when a series is being edited + # # Start editing right when a series_service is being edited #self.parent.getRecordService().toggle_record() #record_service.toggle_record() @@ -608,7 +608,7 @@ def onPlotSelection(self, event): value = 3 elif event.Id == wxID_RIBBONPLOTSUMMARY: value = 4 - # TODO fix case where the plot enables the buttons without checking if series is actually selected + # TODO fix case where the plot enables the buttons without checking if series_service is actually selected self.enableButtons(value, True) Publisher.sendMessage(("select.Plot"), value=value) event.Skip() @@ -650,7 +650,7 @@ def enableButtons(self, plot, isActive): self.spnBins.Enabled = False self.enableDateSelection(False) - ##tims series or probability + ##tims series_service or probability elif plot == 0 or plot == 1: self.PlotsOptions_bar.EnableButton(wxID_RIBBONPLOTTSTYPE, True) self.PlotsOptions_bar.EnableButton(wxID_RIBBONPLOTTSLEGEND, True) @@ -686,7 +686,7 @@ def enableButtons(self, plot, isActive): # TODO change states when points are selected rather than all at once def toggleEditButtons(self, state): - # edit when series is selected for editing + # edit when series_service is selected for editing self.main_bar.EnableButton(wxID_RIBBONEDITRESTORE, state) self.main_bar.EnableButton(wxID_RIBBONEDITSAVE, state) self.edit_bar.EnableButton(wxID_RIBBONEDITFILTER, state) diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index fed04da..c0dc75d 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -145,8 +145,8 @@ def __init__(self, parent): # # self._init_data(self.panel.series_service) # -# def _init_data(self, series): -# meth = series.get_all_methods() +# def _init_data(self, series_service): +# meth = series_service.get_all_methods() # index = 0 # for m, i in zip(meth, range(len(meth))): # num_items = self.panel.lstMethods.GetItemCount() @@ -270,7 +270,7 @@ def __init__(self, parent, service_manager, record_service): # self.pgMethod = MethodPage(self, "Method", self.series_service, self.currSeries.method) self.pgMethod = WizardMethodController(self, self.series_service) # self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) - self.pgQCL = WizardProcessLevelController(self) + self.pgQCL = WizardProcessLevelController(self, service_manager=service_manager) # self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) self.pgVariable = WizardVariableController(self, service_manager=service_manager) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) @@ -363,7 +363,7 @@ def on_wizard_finished(self, event): if self.pgExisting.pnlExisting.rbAppend.GetValue(): keyword = "append to" - message = "You are about to " + keyword + " an existing series,\nthis action cannot be undone.\nWould you like to continue?\n" + message = "You are about to " + keyword + " an existing series_service,\nthis action cannot be undone.\nWould you like to continue?\n" cont = wx.MessageBox(message, 'Are you sure?', wx.YES_NO | wx.ICON_QUESTION) if cont == 2: closeSuccessful = True diff --git a/odmtools/odmdata/memory_database.py b/odmtools/odmdata/memory_database.py index 2ae1763..a638527 100644 --- a/odmtools/odmdata/memory_database.py +++ b/odmtools/odmdata/memory_database.py @@ -200,7 +200,7 @@ def initEditValues(self, seriesID): :return: nothing """ if not self.editLoaded: - logger.debug("Load series from db") + logger.debug("Load series_service from db") self.df = self.series_service.get_values_by_series(seriesID) self.editLoaded = True @@ -218,7 +218,7 @@ def initEditValues(self, seriesID): index=False)#,flavor='sqlite', chunksize=10000) logger.debug("done loading database") else: - logger.debug("no data in series") + logger.debug("no data in series_service") def changeSeriesIDs(self, var=None, qcl=None, method=None): """ diff --git a/odmtools/odmdata/series.py b/odmtools/odmdata/series.py index 7688555..a287d85 100644 --- a/odmtools/odmdata/series.py +++ b/odmtools/odmdata/series.py @@ -85,7 +85,7 @@ class Series(Base): "DataValue.quality_control_level_id == Series.quality_control_level_id)", foreign_keys="[DataValue.site_id, DataValue.variable_id, DataValue.method_id, DataValue.source_id, DataValue.quality_control_level_id]", order_by="DataValue.local_date_time", - backref="series") + backref="series_service") site = relationship(Site) variable = relationship(Variable) diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/cv_service.py index 3466d0c..4b448d2 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/cv_service.py @@ -54,6 +54,7 @@ def get_offset_type_cvs(self): def get_speciation_cvs(self): result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() return result + # return self.read_service.getCVs(type="Speciation") # Returns Error running Query, def get_sample_medium_cvs(self): result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() diff --git a/odmtools/odmservices/edit_service.py b/odmtools/odmservices/edit_service.py index 315e927..567b0dc 100644 --- a/odmtools/odmservices/edit_service.py +++ b/odmtools/odmservices/edit_service.py @@ -253,7 +253,7 @@ def get_series(self): return self.memDB.series_service.get_series_by_id(self._series_id) def get_series_points(self): - # all point in the series + # all point in the series_service return self._series_points def get_series_points_df(self): @@ -280,7 +280,7 @@ def get_filtered_dates(self): return self.filtered_dataframe def get_filter_list(self): - # true or false list the length of the entire series. true indicate the point is selected + # true or false list the length of the entire series_service. true indicate the point is selected return self._filter_list def get_qcl(self, qcl_id): @@ -442,7 +442,7 @@ def updateSeries(self, var=None, method=None, qcl=None, is_new_series=False, ove - #if is new series remove valueids + #if is new series_service remove valueids #if is_new_series: dvs["ValueID"] = None ''' @@ -451,7 +451,7 @@ def updateSeries(self, var=None, method=None, qcl=None, is_new_series=False, ove ''' series = self.memDB.series_service.get_series_by_id(self._series_id) - logger.debug("original editing series id: %s" % str(series.id)) + logger.debug("original editing series_service id: %s" % str(series.id)) if (var or method or qcl ): tseries = self.memDB.series_service.get_series_by_id_quint(site_id=int(series.site_id), @@ -462,7 +462,7 @@ def updateSeries(self, var=None, method=None, qcl=None, is_new_series=False, ove qcl_id=qcl_id if qcl else int( series.quality_control_level_id)) if tseries: - logger.debug("Save existing series ID: %s" % str(tseries.id)) + logger.debug("Save existing series_service ID: %s" % str(tseries.id)) series = tseries else: print "Series doesn't exist (if you are not, you should be running SaveAs)" @@ -512,11 +512,11 @@ def updateSeries(self, var=None, method=None, qcl=None, is_new_series=False, ove #pass self.memDB.series_service.delete_values_by_series(series) elif append: - #if series end date is after dvs startdate + #if series_service end date is after dvs startdate dbend = series.end_date_time dfstart = datetime.datetime.strptime(str(np.min(dvs["LocalDateTime"])), form) overlap = dbend>= dfstart - #leave series start dates to those previously set + #leave series_service start dates to those previously set series.end_date_time = datetime.datetime.strptime(str(np.max(dvs["LocalDateTime"])), form) series.end_date_time_utc = datetime.datetime.strptime(str(np.max(dvs["DateTimeUTC"])), form) #TODO figure out how to calculate the new value count @@ -532,7 +532,7 @@ def updateSeries(self, var=None, method=None, qcl=None, is_new_series=False, ove - #logger.debug("series.data_values: %s" % ([x for x in series.data_values])) + #logger.debug("series_service.data_values: %s" % ([x for x in series_service.data_values])) dvs.drop('ValueID', axis=1, inplace=True) return series, dvs @@ -546,7 +546,7 @@ def save(self): series, dvs = self.updateSeries(is_new_series=False) if self.memDB.series_service.save_series(series, dvs): - logger.debug("series saved!") + logger.debug("series_service saved!") return True else: logger.debug("The Save was unsuccessful") @@ -562,7 +562,7 @@ def save_as(self, var=None, method=None, qcl=None): series, dvs = self.updateSeries(var, method, qcl, is_new_series=True) if self.memDB.series_service.save_new_series(series, dvs): - logger.debug("series saved!") + logger.debug("series_service saved!") return True else: logger.debug("The Save As Function was Unsuccessful") @@ -572,7 +572,7 @@ def save_appending(self, var= None, method = None, qcl=None, overwrite=False): series, dvs = self.updateSeries(var, method, qcl, is_new_series=False, append= True, overwrite=overwrite) if self.memDB.series_service.save_series(series, dvs): - logger.debug("series saved!") + logger.debug("series_service saved!") return True else: logger.debug("The Append Existing Function was Unsuccessful") @@ -587,7 +587,7 @@ def save_existing(self, var=None, method=None, qcl=None): """ series, dvs = self.updateSeries(var, method, qcl, is_new_series=False) if self.memDB.series_service.save_series(series, dvs): - logger.debug("series saved!") + logger.debug("series_service saved!") return True else: logger.debug("The Save As Existing Function was Unsuccessful") @@ -610,8 +610,8 @@ def create_variable(self, code, name, speciation, variable_unit_id, sample_mediu general_category, no_data_value) def reconcile_dates(self, parent_series_id): - # FUTURE FEATURE: pull in new field data from another series and add to this series - # (i.e one series contains new field data of an edited series at a higher qcl) + # FUTURE FEATURE: pull in new field data from another series_service and add to this series_service + # (i.e one series_service contains new field data of an edited series_service at a higher qcl) pass diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index c27158b..834014d 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -274,7 +274,7 @@ def get_samples_by_series_id(self, series_id): # Series Catalog methods def get_all_series(self): """ - Returns all series as a modelObject + Returns all series_service as a modelObject :return: List[Series] """ @@ -528,7 +528,7 @@ def save_new_series(self, series, dvs): self._edit_session.rollback() raise e - logger.info("A new series was added to the database, series id: "+str(series.id)) + logger.info("A new series_service was added to the database, series_service id: "+str(series.id)) return True def save_values(self, values): @@ -541,7 +541,7 @@ def save_values(self, values): def create_new_series(self, data_values, site_id, variable_id, method_id, source_id, qcl_id): """ - series -> Result in ODM2 + series_service -> Result in ODM2 :param data_values: :param site_id: :param variable_id: @@ -671,7 +671,7 @@ def delete_series(self, series): self._edit_session.delete(delete_series) self._edit_session.commit() except Exception as e: - message = "series was not successfully deleted: %s" % e + message = "series_service was not successfully deleted: %s" % e print message logger.error(message) raise e @@ -690,7 +690,7 @@ def delete_values_by_series(self, series, startdate = None): source_id = series.source_id, quality_control_level_id = series.quality_control_level_id) if startdate is not None: - #start date indicates what day you should start deleting values. the values will delete to the end of the series + #start date indicates what day you should start deleting values. the values will delete to the end of the series_service return q.filter(DataValue.local_date_time >= startdate).delete() else: return q.delete() diff --git a/odmtools/view/WizardMethodView.py b/odmtools/view/WizardMethodView.py index ee6b25f..532aac1 100644 --- a/odmtools/view/WizardMethodView.py +++ b/odmtools/view/WizardMethodView.py @@ -12,7 +12,8 @@ def __init__(self, parent): static_line = wx.StaticLine(self, size=(-1, 12)) self.auto_method_radio = wx.RadioButton(self, label="Automatically generate a method") self.existing_method_radio = wx.RadioButton(self, label="Select an existing method") - self.existing_method_table = CustomListCtrl(self) + table_panel = wx.Panel(self) + self.existing_method_table = CustomListCtrl(table_panel) self.create_method_radio = wx.RadioButton(self, label="Create a new method") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) @@ -39,6 +40,10 @@ def __init__(self, parent): self.SetupScrolling() # Add components to sizer + table_sizer = wx.BoxSizer() + table_sizer.Add(self.existing_method_table, 0, wx.EXPAND | wx.ALL, 0) + table_panel.SetSizerAndFit(table_sizer) + row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(method_code_text, 0, wx.EXPAND) row_sizer.Add(self.method_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 24) @@ -74,7 +79,7 @@ def __init__(self, parent): main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(self.auto_method_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(self.existing_method_radio, 0, wx.EXPAND | wx.TOP, 5) - main_sizer.Add(self.existing_method_table, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(table_panel, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(self.create_method_radio, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) diff --git a/odmtools/view/WizardProcessLevelView.py b/odmtools/view/WizardProcessLevelView.py index d69a066..b88dec6 100644 --- a/odmtools/view/WizardProcessLevelView.py +++ b/odmtools/view/WizardProcessLevelView.py @@ -11,7 +11,8 @@ def __init__(self, parent): header_text = wx.StaticText(self, label="Processing Level") static_line = wx.StaticLine(self, size=(-1, 12)) self.existing_process_radio = wx.RadioButton(self, label="Select an existing Processing Level") - self.existing_process_table = CustomListCtrl(self) + table_panel = wx.Panel(self) + self.existing_process_table = CustomListCtrl(table_panel) self.create_process_level_radio = wx.RadioButton(self, label="Create Processing Level") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) level_code = wx.StaticText(self, label="Processing Level Code") @@ -28,6 +29,10 @@ def __init__(self, parent): self.SetupScrolling() # Add components to sizer + table_sizer = wx.BoxSizer(wx.VERTICAL) + table_sizer.Add(self.existing_process_table, 0, wx.EXPAND | wx.ALL, 0) + table_panel.SetSizerAndFit(table_sizer) + row_sizer = wx.BoxSizer(wx.HORIZONTAL) row_sizer.Add(level_code, 0, wx.EXPAND) row_sizer.Add(self.level_code_text_ctrl, 1, wx.EXPAND | wx.LEFT, 30) @@ -47,7 +52,7 @@ def __init__(self, parent): main_sizer.Add(header_text, 0, wx.ALIGN_CENTER | wx.ALL, 5) main_sizer.Add(static_line, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(self.existing_process_radio, 0, wx.EXPAND | wx.TOP, 10) - main_sizer.Add(self.existing_process_table, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(table_panel, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(self.create_process_level_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) diff --git a/odmtools/view/clsIntro.py b/odmtools/view/clsIntro.py index 8d9b338..2239e3b 100644 --- a/odmtools/view/clsIntro.py +++ b/odmtools/view/clsIntro.py @@ -34,7 +34,7 @@ # # bSizer2 = wx.BoxSizer(wx.VERTICAL) # -# self.lblHow = wx.StaticText(self.m_panel2, wx.ID_ANY, u"How would you like to save the series?", +# self.lblHow = wx.StaticText(self.m_panel2, wx.ID_ANY, u"How would you like to save the series_service?", # wx.DefaultPosition, wx.DefaultSize, 0) # self.lblHow.Wrap(-1) # bSizer2.Add(self.lblHow, 0, wx.ALL, 15) @@ -139,7 +139,7 @@ def __init__(self, parent): bSizer2 = wx.BoxSizer(wx.VERTICAL) - self.lblHow = wx.StaticText(self.m_panel2, wx.ID_ANY, u"How would you like to save the series?", + self.lblHow = wx.StaticText(self.m_panel2, wx.ID_ANY, u"How would you like to save the series_service?", wx.DefaultPosition, wx.DefaultSize, 0) self.lblHow.Wrap(-1) bSizer2.Add(self.lblHow, 0, wx.ALL, 15) diff --git a/tests/test_gui/test_plotProbability.py b/tests/test_gui/test_plotProbability.py index ffac10b..3fc9eac 100644 --- a/tests/test_gui/test_plotProbability.py +++ b/tests/test_gui/test_plotProbability.py @@ -33,7 +33,7 @@ def setup(self): self.series = add_series(self.session) print "Series: ", self.series self.memory_db.initEditValues(self.series.id) - # add_bulk_data_values(self.session, self.series) + # add_bulk_data_values(self.session, self.series_service) def test_onPlotType(self): diff --git a/tests/test_gui/test_wizSave.py b/tests/test_gui/test_wizSave.py index 038faee..0685d6b 100644 --- a/tests/test_gui/test_wizSave.py +++ b/tests/test_gui/test_wizSave.py @@ -32,7 +32,7 @@ def setup(self): self.memory_database = MemoryDatabase() self.memory_database.set_series_service(self.series_service) - # self.memory_database.initEditValues(self.series.id) + # self.memory_database.initEditValues(self.series_service.id) self.app = wx.App() self.frame = wx.Frame(None) diff --git a/tests/test_odmdata/test_pandas_memory_db.py b/tests/test_odmdata/test_pandas_memory_db.py index 94ef7ee..d4c58cc 100644 --- a/tests/test_odmdata/test_pandas_memory_db.py +++ b/tests/test_odmdata/test_pandas_memory_db.py @@ -10,7 +10,7 @@ class TestPandasMemoryDB: """ - Test to Load up a series from a dataframe and load it into an in memory database + Test to Load up a series_service from a dataframe and load it into an in memory database """ def setup(self): self.connection_string = "sqlite:///:memory:" diff --git a/tests/test_odmservices/test_edit_service.py b/tests/test_odmservices/test_edit_service.py index ebe93f7..d2627e8 100644 --- a/tests/test_odmservices/test_edit_service.py +++ b/tests/test_odmservices/test_edit_service.py @@ -18,7 +18,7 @@ def setup(self): self.session = self.memory_database.series_service._session_factory.getSession() self.series = test_util.add_series_bulk_data(self.session) - #assert len(self.series.data_values) == 100 + #assert len(self.series_service.data_values) == 100 self.edit_service =EditService(1, connection= self.memory_database) @@ -90,7 +90,7 @@ def test_save_append_keep(self): #TODO add custon test len1= len(self.series.data_values) - # keep data from original series if overlap: + # keep data from original series_service if overlap: svalue = self.series.data_values[0] diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index 1b4eafa..c73dfc0 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -209,18 +209,18 @@ def test_series_exists(self): ''' def test_save_series(self): - series = Series() + series_service = Series() site = test_util.add_site(self.session) variable = test_util.add_variable(self.session) method = test_util.add_method(self.session) source = test_util.add_source(self.session) qcl = test_util.add_qcl(self.session) - series.site_id = site.id - series.variable_id = variable.id - series.method_id = method.id - series.source_id = source.id - series.quality_control_level_id = qcl.id + series_service.site_id = site.id + series_service.variable_id = variable.id + series_service.method_id = method.id + series_service.source_id = source.id + series_service.quality_control_level_id = qcl.id dvs = [] for val in range(10): @@ -233,10 +233,10 @@ def test_save_series(self): dv.quality_control_level_id = qcl.id dvs.append(dv) - print series.variable_code - assert self.series_service.save_series(series) + print series_service.variable_code + assert self.series_service.save_series(series_service) assert self.series_service.series_exists(site.id, variable.id, method.id, source.id, qcl.id) - assert not self.series_service.save_series(series) + assert not self.series_service.save_series(series_service) ''' def test_get_data_value_by_id(self): diff --git a/tests/test_util.py b/tests/test_util.py index 43789d3..444fb7a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -13,7 +13,7 @@ def build_db(engine): def add_bulk_data_values(session, series, dvs_size): """ - Load up exampleData.csv into a series' datavalues field + Load up exampleData.csv into a series_service' datavalues field """ assert 10000 >= dvs_size > 0 path = os.path.dirname(os.path.realpath(__file__)) From 190433eb5fabfa567a0e5239da9893e1b8a448dd Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Mon, 10 Oct 2016 14:14:25 -0600 Subject: [PATCH 12/19] Fixed some bugs when creating a new series Chooinsg an existing and new method/variable/pcl will active its respective panels, also added some methods to finish creating a new series such as the getMethods, getVariales, etc --- odmtools/controller/WizardMethodController.py | 49 +++++++++++++++++++ .../WizardProcessLevelController.py | 34 +++++++++++++ .../controller/WizardVariableController.py | 46 ++++++++++++++++- odmtools/gui/wizSave.py | 10 ++-- odmtools/view/CustomListCtrl.py | 1 + odmtools/view/WizardMethodView.py | 3 +- odmtools/view/WizardVariableView.py | 4 +- 7 files changed, 139 insertions(+), 8 deletions(-) diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py index 1d22a49..e0c245d 100644 --- a/odmtools/controller/WizardMethodController.py +++ b/odmtools/controller/WizardMethodController.py @@ -1,6 +1,7 @@ import wx from wx.wizard import WizardPageSimple from odmtools.view.WizardMethodView import WizardMethodView +from odmtools.odmdata import Method class WizardMethodController(WizardPageSimple): @@ -16,8 +17,37 @@ def __init__(self, parent, series_service): table_columns = ["Descriptions", "Link", "ID"] self.method_view.existing_method_table.set_columns(table_columns) self.method_view.method_type_combo.AppendItems(["ABC"]) + self.on_auto_radio(None) + + self.method_view.auto_method_radio.Bind(wx.EVT_RADIOBUTTON, self.on_auto_radio) + self.method_view.existing_method_radio.Bind(wx.EVT_RADIOBUTTON, self.on_existing_method_radio) + self.method_view.create_method_radio.Bind(wx.EVT_RADIOBUTTON, self.on_create_method_radio) + self.__fetch_data() + def on_auto_radio(self, event): + self.method_view.existing_method_table.Enable(False) + self.__set_create_method_section_(False) + + def on_existing_method_radio(self, event): + self.method_view.existing_method_table.Enable() + self.__set_create_method_section_(False) + + def __set_create_method_section_(self, active): + if not isinstance(active, bool): + raise Exception("active must be type bool") + + self.method_view.method_code_text_ctrl.Enable(active) + self.method_view.method_name_text_ctrl.Enable(active) + self.method_view.method_type_combo.Enable(active) + self.method_view.organization_combo.Enable(active) + self.method_view.method_link_text_ctrl.Enable(active) + self.method_view.description_text_ctrl.Enable(active) + + def on_create_method_radio(self, event): + self.method_view.existing_method_table.Disable() + self.__set_create_method_section_(True) + def __fetch_data(self): methods = self.series_service.get_all_methods() data = [] @@ -29,3 +59,22 @@ def __fetch_data(self): ]) self.method_view.existing_method_table.set_table_content(data=data) + + def getMethod(self): + m = Method() + if self.method_view.auto_method_radio.GetValue(): + description = "Values derived from ODM Tools Python" + m = self.series_service.get_method_by_description(description) + if m is None: + m = Method() + m.description = description + elif self.method_view.existing_method_radio.GetValue(): + index = self.method_view.existing_method_table.GetFirstSelected() + desc = self.method_view.existing_method_table.GetItem(index, 0).GetText() + + m = self.series_service.get_method_by_description(desc) + elif self.method_view.create_method_radio.GetValue(): + m.description = self.method_view.description_text_ctrl.GetValue() + + + return m diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py index 87dd78b..76f3aed 100644 --- a/odmtools/controller/WizardProcessLevelController.py +++ b/odmtools/controller/WizardProcessLevelController.py @@ -1,6 +1,7 @@ import wx from odmtools.view.WizardProcessLevelView import WizardProcessLevelView from wx.wizard import WizardPageSimple +from odmtools.odmdata import QualityControlLevel class WizardProcessLevelController(WizardPageSimple): @@ -17,6 +18,25 @@ def __init__(self, parent, service_manager): self.processing_level_view.existing_process_table.set_columns(table_columns) self.__fetch_data() + self.processing_level_view.create_process_level_radio.Bind(wx.EVT_RADIOBUTTON, self.on_create_radio) + self.processing_level_view.existing_process_radio.Bind(wx.EVT_RADIOBUTTON, self.on_existing_radio) + + def on_create_radio(self, event): + self.processing_level_view.existing_process_table.Enable(False) + self.__set_create_proces_section(True) + + def on_existing_radio(self, event): + self.processing_level_view.existing_process_table.Enable(True) + self.__set_create_proces_section(False) + + def __set_create_proces_section(self, active): + if not isinstance(active, bool): + raise Exception("activet must be type bool") + + self.processing_level_view.level_code_text_ctrl.Enable(active) + self.processing_level_view.definition_text_ctrl.Enable(active) + self.processing_level_view.explanation_text_ctrl.Enable(active) + def __fetch_data(self): series_service = self.service_manager.get_series_service() processes = series_service.get_all_qcls() @@ -31,3 +51,17 @@ def __fetch_data(self): ]) self.processing_level_view.existing_process_table.set_table_content(data=data) + + def getQCL(self): + q = QualityControlLevel() + if self.processing_level_view.create_process_level_radio.GetValue(): + q.code = self.processing_level_view.level_code_text_ctrl.GetValue() + q.definition = self.processing_level_view.definition_text_ctrl.GetValue() + q.explanation = self.processing_level_view.explanation_text_ctrl.GetValue() + + elif self.processing_level_view.existing_process_radio.GetValue(): + selected_row = self.processing_level_view.existing_process_table.get_selected_row() + code = selected_row[0] + q = self.service_manager.get_series_service().get_qcl_by_code(qcl_code=code) + + return q diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py index eadc178..f15f345 100644 --- a/odmtools/controller/WizardVariableController.py +++ b/odmtools/controller/WizardVariableController.py @@ -1,13 +1,15 @@ import wx from odmtools.view.WizardVariableView import WizardVariableView from wx.wizard import WizardPageSimple +from odmtools.odmdata import Variable class WizardVariableController(WizardPageSimple): - def __init__(self, parent, service_manager): + def __init__(self, parent, service_manager, current_variable): WizardPageSimple.__init__(self, parent) self.service_manager = service_manager + self.current_variable = current_variable main_sizer = wx.BoxSizer(wx.VERTICAL) self.variable_view = WizardVariableView(self) main_sizer.Add(self.variable_view, 1, wx.EXPAND | wx.RIGHT, -16) @@ -17,8 +19,35 @@ def __init__(self, parent, service_manager): "Sample Medium", "Value Type", "IsRegular", "Time Support", "Time Units", "DataType", "Genaral Category", "NoDataValue", "ID"] self.variable_view.variable_table.set_columns(table_columns) + self.on_current_radio(None) self.__fetch_data() + self.variable_view.current_variable_radio.Bind(wx.EVT_RADIOBUTTON, self.on_current_radio) + self.variable_view.existing_variable_radio.Bind(wx.EVT_RADIOBUTTON, self.on_existing_radio) + self.variable_view.create_variable_radio.Bind(wx.EVT_RADIOBUTTON, self.on_create_radio) + + def on_current_radio(self, event): + self.variable_view.variable_table.Enable(False) + self.__set_create_variable_section(False) + + def on_create_radio(self, event): + self.variable_view.variable_table.Enable(False) + self.__set_create_variable_section(True) + + def on_existing_radio(self, event): + self.variable_view.variable_table.Enable(True) + self.__set_create_variable_section(False) + + def __set_create_variable_section(self, active): + if not isinstance(active, bool): + raise Exception("active must be type bool") + + self.variable_view.variable_code_text_ctrl.Enable(active) + self.variable_view.variable_name_combo.Enable(active) + self.variable_view.variable_type_combo.Enable(active) + self.variable_view.no_data_value_text_ctrl.Enable(active) + self.variable_view.speciation_combo.Enable(active) + self.variable_view.definition_text_ctrl.Enable(active) def __fetch_data(self): self.__populate_variable_table() @@ -53,6 +82,21 @@ def __populate_variable_table(self): self.variable_view.variable_table.set_table_content(data=data) + def get_variable(self): + v = Variable() + if self.variable_view.current_variable_radio.GetValue(): + v = self.current_variable + elif self.variable_view.existing_variable_radio.GetValue(): + row = self.variable_view.variable_table.get_selected_row() + code = row[0] + v = self.service_manager.get_series_service().get_variable_by_code(code) + + elif self.variable_view.create_variable_radio.GetValue(): + # v = self.createdVar + pass + + return v + diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index c0dc75d..a8837d9 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -238,9 +238,9 @@ def get_metadata(self): if self.pgIntro.pnlIntroduction.rbSaveAs.GetValue(): logger.debug("SaveAs") - method = self.pgMethod.panel.getMethod() - qcl = self.pgQCL.panel.getQCL() - variable = self.pgVariable.panel.getVariable() + method = self.pgMethod.getMethod() + qcl = self.pgQCL.getQCL() + variable = self.pgVariable.get_variable() elif self.pgIntro.pnlIntroduction.rbSave.GetValue(): logger.debug("Save") method = self.currSeries.method @@ -272,7 +272,8 @@ def __init__(self, parent, service_manager, record_service): # self.pgQCL = QCLPage(self, "Quality Control Level", self.series_service, self.currSeries.quality_control_level) self.pgQCL = WizardProcessLevelController(self, service_manager=service_manager) # self.pgVariable = VariablePage(self, "Variable", service_manager, self.currSeries.variable) - self.pgVariable = WizardVariableController(self, service_manager=service_manager) + self.pgVariable = WizardVariableController(self, service_manager=service_manager, + current_variable=self.currSeries.variable) self.pgExisting = pageExisting.pageExisting(self, "Existing Series", self.series_service, self.currSeries.site) self.pgSummary = SummaryPage(self, "Summary", self.series_service) @@ -302,7 +303,6 @@ def __init__(self, parent, service_manager, record_service): self.Destroy() def on_page_changed(self, event): - #if isinstance(event.Page, pageSummary.pnlSummary): if event.Page == self.pgSummary: self.pgSummary.fill_summary() diff --git a/odmtools/view/CustomListCtrl.py b/odmtools/view/CustomListCtrl.py index 61fd4dc..4dc2e9b 100644 --- a/odmtools/view/CustomListCtrl.py +++ b/odmtools/view/CustomListCtrl.py @@ -172,5 +172,6 @@ def set_table_content(self, data): for j in range(len(data[i])): self.SetStringItem(index, j, str(data[i][j])) + self.Select(0) self.auto_size_table() self.alternate_row_color() \ No newline at end of file diff --git a/odmtools/view/WizardMethodView.py b/odmtools/view/WizardMethodView.py index 532aac1..612e232 100644 --- a/odmtools/view/WizardMethodView.py +++ b/odmtools/view/WizardMethodView.py @@ -38,6 +38,7 @@ def __init__(self, parent): self.method_type_combo.SetSelection(0) self.organization_combo.SetSelection(0) self.SetupScrolling() + self.auto_method_radio.SetValue(True) # Add components to sizer table_sizer = wx.BoxSizer() @@ -80,7 +81,7 @@ def __init__(self, parent): main_sizer.Add(self.auto_method_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(self.existing_method_radio, 0, wx.EXPAND | wx.TOP, 5) main_sizer.Add(table_panel, 0, wx.EXPAND | wx.TOP, 5) - main_sizer.Add(self.create_method_radio, 0, wx.EXPAND | wx.TOP, 5) + main_sizer.Add(self.create_method_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) self.SetSizer(main_sizer) diff --git a/odmtools/view/WizardVariableView.py b/odmtools/view/WizardVariableView.py index 3b6ec1d..df9a8bf 100644 --- a/odmtools/view/WizardVariableView.py +++ b/odmtools/view/WizardVariableView.py @@ -14,7 +14,7 @@ def __init__(self, parent): self.current_variable_radio = wx.RadioButton(self, label="Use current variable") self.variable_table = CustomListCtrl(table_panel) self.existing_variable_radio = wx.RadioButton(self, label="Select an existing variable") - + self.create_variable_radio = wx.RadioButton(self, label="Create new variable") required_static_box_sizer = wx.StaticBoxSizer(box=wx.StaticBox(self, label="Required Fields"), orient=wx.VERTICAL) variable_code_text = wx.StaticText(self, label="Variable Code") self.variable_code_text_ctrl = wx.TextCtrl(self) @@ -38,6 +38,7 @@ def __init__(self, parent): self.speciation_combo.SetSelection(0) self.variable_type_combo.SetSelection(0) self.SetupScrolling() + self.current_variable_radio.SetValue(True) # Add components to sizer table_sizer = wx.BoxSizer() @@ -80,6 +81,7 @@ def __init__(self, parent): main_sizer.Add(self.current_variable_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(self.existing_variable_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(table_panel, 0, wx.EXPAND | wx.TOP, 10) + main_sizer.Add(self.create_variable_radio, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(required_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) main_sizer.Add(optional_static_box_sizer, 0, wx.EXPAND | wx.TOP, 10) self.SetSizer(main_sizer) From ba4cfd5fdf9cd79163b286e909bc4961287ab398 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Wed, 12 Oct 2016 12:16:58 -0600 Subject: [PATCH 13/19] creating variable --- odmtools/controller/WizardVariableController.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/odmtools/controller/WizardVariableController.py b/odmtools/controller/WizardVariableController.py index f15f345..2038b5f 100644 --- a/odmtools/controller/WizardVariableController.py +++ b/odmtools/controller/WizardVariableController.py @@ -93,7 +93,18 @@ def get_variable(self): elif self.variable_view.create_variable_radio.GetValue(): # v = self.createdVar - pass + v = self.get_new_variable() + + return v + + def get_new_variable(self): + v = Variable() + v.code = self.variable_view.variable_code_text_ctrl.GetValue() if self.variable_view.variable_code_text_ctrl.GetValue() <> "" else None + v.name = self.variable_view.variable_name_combo.GetValue() if self.variable_view.variable_name_combo.GetValue() <> "" else None + v.speciation = self.variable_view.speciation_combo.GetValue() if self.variable_view.speciation_combo.GetValue() <> "" else None + v.variable_unit = self.service_manager.get_series_service() + v.no_data_value = self.variable_view.no_data_value_text_ctrl.GetValue() if self.variable_view.no_data_value_text_ctrl.GetValue() <> "" else None + # Need unit name, time support but neither of them are in the form... return v From 1e16a6413142a5279ce176da6ee796fad257fa27 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Wed, 12 Oct 2016 12:58:06 -0600 Subject: [PATCH 14/19] Updating the cv service Rename to ReadService. Mapped the methods to use the ReadOMD2 class in OMD2api --- .../{cv_service.py => ReadService.py} | 56 +++++-------------- odmtools/odmservices/__init__.py | 4 +- odmtools/odmservices/service_manager.py | 4 +- tests/test_odmservices/test_cv_service.py | 4 +- 4 files changed, 20 insertions(+), 48 deletions(-) rename odmtools/odmservices/{cv_service.py => ReadService.py} (51%) diff --git a/odmtools/odmservices/cv_service.py b/odmtools/odmservices/ReadService.py similarity index 51% rename from odmtools/odmservices/cv_service.py rename to odmtools/odmservices/ReadService.py index 4b448d2..7b98d7b 100644 --- a/odmtools/odmservices/cv_service.py +++ b/odmtools/odmservices/ReadService.py @@ -1,25 +1,13 @@ -# CV imports from odmtools.odmdata import SessionFactory from odmtools.odmdata import VerticalDatumCV -from odmtools.odmdata import SiteTypeCV -from odmtools.odmdata import VariableNameCV -from odmtools.odmdata import SpeciationCV -from odmtools.odmdata import SampleMediumCV from odmtools.odmdata import ValueTypeCV -from odmtools.odmdata import DataTypeCV from odmtools.odmdata import GeneralCategoryCV -from odmtools.odmdata import CensorCodeCV -from odmtools.odmdata import TopicCategoryCV -from odmtools.odmdata import SampleTypeCV -from odmtools.odmdata import OffsetType -from odmtools.odmdata import Sample -from odmtools.odmdata import Qualifier from odmtools.odmdata import Unit from sqlalchemy import not_ from odm2api.ODM2.services.readService import ReadODM2 -class CVService(): # Rename to ReadService +class ReadService: # Accepts a string for creating a SessionFactory, default uses odmdata/connection.cfg def __init__(self, connection_string="", debug=False): self._session_factory = SessionFactory(connection_string, debug) @@ -27,62 +15,49 @@ def __init__(self, connection_string="", debug=False): self._debug = debug self.read_service = ReadODM2(self._session_factory, debug=self._debug) - # Controlled Vocabulary get methods - #return a list of all terms in the cv def get_vertical_datum_cvs(self): result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() return result def get_samples(self): - result = self._edit_session.query(Sample).order_by(Sample.lab_sample_code).all() - return result + return self.read_service.getSamplingFeatures(ids=None, codes=None, uuids=None, type=None, wkt=None) def get_site_type_cvs(self): - result = self._edit_session.query(SiteTypeCV).order_by(SiteTypeCV.term).all() - return result + return self.read_service.getCVs(type="Site Type") # OR return self.read_service.getCVs(type="Sampling Feature Type") def get_variable_name_cvs(self): - result = self._edit_session.query(VariableNameCV).order_by(VariableNameCV.term).all() - return result + return self.read_service.getCVs(type="Variable Name") def get_offset_type_cvs(self): - result = self._edit_session.query(OffsetType).order_by(OffsetType.id).all() - return result + return self.read_service.getCVs(type="Spatial Offset Type") def get_speciation_cvs(self): - result = self._edit_session.query(SpeciationCV).order_by(SpeciationCV.term).all() - return result - # return self.read_service.getCVs(type="Speciation") # Returns Error running Query, + return self.read_service.getCVs(type="Speciation") def get_sample_medium_cvs(self): - result = self._edit_session.query(SampleMediumCV).order_by(SampleMediumCV.term).all() - return result + return self.read_service.getCVs(type="Medium") def get_value_type_cvs(self): result = self._edit_session.query(ValueTypeCV).order_by(ValueTypeCV.term).all() return result def get_data_type_cvs(self): - result = self._edit_session.query(DataTypeCV).order_by(DataTypeCV.term).all() - return result + return self.read_service.getCVs(type="dataset type") def get_general_category_cvs(self): result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() return result def get_censor_code_cvs(self): - result = self._edit_session.query(CensorCodeCV).order_by(CensorCodeCV.term).all() - return result + return self.read_service.getCVs(type="censorcode") def get_sample_type_cvs(self): - result = self._edit_session.query(SampleTypeCV).order_by(SampleTypeCV.term).all() - return result + return self.read_service.getCVs(type="Sampling Feature Type") def get_units(self): - result = self._edit_session.query(Unit).all() - return result + return self.read_service.getUnits(ids=None, name=None, type=None) def get_units_not_uni(self): result = self._edit_session.query(Unit).filter(not_(Unit.name.contains('angstrom'))).all() @@ -94,16 +69,13 @@ def get_units_names(self): # return a single cv def get_unit_by_name(self, unit_name): - result = self._edit_session.query(Unit).filter_by(name=unit_name).first() - return result + return self.read_service.getUnits(name=unit_name) def get_unit_by_id(self, unit_id): - result = self._edit_session.query(Unit).filter_by(id=unit_id).first() - return result + return self.read_service.getUnits(ids=unit_id) def get_annotation_by_code(self, code): - self.read_service.getAnnotations(type=code) - return + return self.read_service.getAnnotations(type=code) def get_all_annotations(self): return self.read_service.getAnnotations(type=None) diff --git a/odmtools/odmservices/__init__.py b/odmtools/odmservices/__init__.py index 6d161f5..3dac41e 100644 --- a/odmtools/odmservices/__init__.py +++ b/odmtools/odmservices/__init__.py @@ -1,6 +1,6 @@ from service_manager import ServiceManager from series_service import SeriesService -from cv_service import CVService +from ReadService import ReadService from edit_service import EditService from export_service import ExportService @@ -11,7 +11,7 @@ __all__ = [ 'EditService', - 'CVService', + 'ReadService', 'SeriesService', 'ExportService', 'ServiceManager', diff --git a/odmtools/odmservices/service_manager.py b/odmtools/odmservices/service_manager.py index 142a5c0..df31fa5 100644 --- a/odmtools/odmservices/service_manager.py +++ b/odmtools/odmservices/service_manager.py @@ -8,7 +8,7 @@ from odmtools.common.logger import LoggerTool from series_service import SeriesService -from cv_service import CVService +from ReadService import ReadService from edit_service import EditService from odmtools.controller import EditTools from export_service import ExportService @@ -153,7 +153,7 @@ def get_series_service(self, conn_dict=""): def get_cv_service(self): conn_string = self._build_connection_string(self._current_conn_dict) - return CVService(conn_string, self.debug) + return ReadService(conn_string, self.debug) def get_edit_service(self, series_id, connection): diff --git a/tests/test_odmservices/test_cv_service.py b/tests/test_odmservices/test_cv_service.py index 61c8ef5..184b74a 100644 --- a/tests/test_odmservices/test_cv_service.py +++ b/tests/test_odmservices/test_cv_service.py @@ -1,4 +1,4 @@ -from odmtools.odmservices import CVService +from odmtools.odmservices import ReadService from tests import test_util @@ -8,7 +8,7 @@ class TestCVService: def setup(self): self.connection_string = "sqlite:///:memory:" - self.cv_service = CVService(self.connection_string, debug=False) + self.cv_service = ReadService(self.connection_string, debug=False) self.session = self.cv_service._session_factory.getSession() engine = self.cv_service._session_factory.engine test_util.build_db(engine) From 9fdffd47484a1bd616df4538f3101df01e560529 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Wed, 12 Oct 2016 13:21:46 -0600 Subject: [PATCH 15/19] #293 Mapping the create methods to the new series service in omd2 This broke a few things due to the update --- odmtools/odmservices/series_service.py | 78 +++++++++---------- tests/test_odmservices/test_cv_service.py | 2 +- tests/test_odmservices/test_series_service.py | 1 - 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 834014d..42e4c64 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -6,14 +6,16 @@ from odmtools.odmdata import SessionFactory from odmtools.odmdata import Site -from odmtools.odmdata import Variable +# from odmtools.odmdata import Variable +from odm2api.ODM2.models import Variables from odmtools.odmdata import Unit from odmtools.odmdata import Series from odmtools.odmdata import DataValue from odmtools.odmdata import Qualifier from odmtools.odmdata import OffsetType from odmtools.odmdata import Sample -from odmtools.odmdata import Method +# from odmtools.odmdata import Method +from odm2api.ODM2.models import Methods from odmtools.odmdata import QualityControlLevel from odmtools.odmdata import ODMVersion from odmtools.common.logger import LoggerTool @@ -107,7 +109,7 @@ def get_used_variables(self): #create list of variables from the list of ids for var_id in var_ids: - Variables.append(self._edit_session.query(Variable).filter_by(id=var_id).first()) + Variables.append(self._edit_session.query(Variables).filter_by(id=var_id).first()) return Variables @@ -116,7 +118,7 @@ def get_all_variables(self): :return: List[Variables] """ - return self._edit_session.query(Variable).all() + return self._edit_session.query(Variables).all() def get_variable_by_id(self, variable_id): """ @@ -125,7 +127,7 @@ def get_variable_by_id(self, variable_id): :return: Variables """ try: - return self._edit_session.query(Variable).filter_by(id=variable_id).first() + return self._edit_session.query(Variables).filter_by(id=variable_id).first() except: return None @@ -136,7 +138,7 @@ def get_variable_by_code(self, variable_code): :return: Variables """ try: - return self._edit_session.query(Variable).filter_by(code=variable_code).first() + return self._edit_session.query(Variables).filter_by(code=variable_code).first() except: return None @@ -154,7 +156,7 @@ def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeU variables = [] for var_id in var_ids: - variables.append(self._edit_session.query(Variable).filter_by(id=var_id).first()) + variables.append(self._edit_session.query(Variables).filter_by(id=var_id).first()) return variables @@ -234,18 +236,18 @@ def get_qcl_by_code(self, qcl_code): # Method methods def get_all_methods(self): - return self._edit_session.query(Method).all() + return self._edit_session.query(Methods).all() def get_method_by_id(self, method_id): try: - result = self._edit_session.query(Method).filter_by(id=method_id).first() + result = self._edit_session.query(Methods).filter_by(id=method_id).first() except: result = None return result def get_method_by_description(self, method_code): try: - result = self._edit_session.query(Method).filter_by(description=method_code).first() + result = self._edit_session.query(Methods).filter_by(description=method_code).first() except: result = None logger.error("method not found") @@ -566,21 +568,19 @@ def create_method(self, description, link): :param link: :return: """ - meth = Method() - meth.description = description + method = Methods() + method.MethodDescription = description if link is not None: - meth.link = link + method.MethodLink = link - self.create_service.createMethod(method=meth) - return meth + return self.create_service.createMethod(method=method) def create_variable_by_var(self, var): """ :param var: Variable Object :return: """ - self.create_service.createVariable(var=var) - return var + return self.create_service.createVariable(var=var) def create_variable( self, code, name, speciation, variable_unit_id, sample_medium, @@ -601,22 +601,23 @@ def create_variable( :param no_data_value: :return: """ - var = Variable() - var.code = code - var.name = name - var.speciation = speciation - var.variable_unit_id = variable_unit_id - var.sample_medium = sample_medium - var.value_type = value_type - var.is_regular = is_regular - var.time_support = time_support - var.time_unit_id = time_unit_id - var.data_type = data_type - var.general_category = general_category - var.no_data_value = no_data_value - - self.create_service.createVariable(var=var) - return var + # var = Variable() + variable = Variables() + variable.VariableCode = code + variable.VariableNameCV = name + variable.SpeciationCV = speciation + # Commented lines indicate that Variables does not have such attributes + # var.variable_unit_id = variable_unit_id + # var.sample_medium = sample_medium + # var.value_type = value_type + # var.is_regular = is_regular + # var.time_support = time_support + # var.time_unit_id = time_unit_id + # var.data_type = data_type + # var.general_category = general_category + variable.NoDataValue = no_data_value + + return self.create_service.createVariable(var=variable) def create_qcl(self, code, definition, explanation): """ @@ -630,13 +631,10 @@ def create_qcl(self, code, definition, explanation): qcl.code = code qcl.definition = definition qcl.explanation = explanation - self.create_service.createProcessingLevel(proclevel=qcl) - - return qcl + return self.create_service.createProcessingLevel(proclevel=qcl) def create_annotation_by_anno(self, annotation): - self.create_service.create(annotation) - return annotation + return self.create_service.create(annotation) def create_annotation(self, code, description): """ @@ -779,7 +777,7 @@ def method_exists(self, m): :return: """ try: - result = self._edit_session.query(Method).filter_by(description=m.description).one() + result = self._edit_session.query(Methods).filter_by(description=m.description).one() return True except: return False @@ -791,7 +789,7 @@ def variable_exists(self, v): :return: """ try: - result = self._edit_session.query(Variable).filter_by(code=v.code, + result = self._edit_session.query(Variables).filter_by(code=v.code, name=v.name, speciation=v.speciation, variable_unit_id=v.variable_unit_id, sample_medium=v.sample_medium, diff --git a/tests/test_odmservices/test_cv_service.py b/tests/test_odmservices/test_cv_service.py index 184b74a..e3a6ccd 100644 --- a/tests/test_odmservices/test_cv_service.py +++ b/tests/test_odmservices/test_cv_service.py @@ -5,7 +5,7 @@ session = None -class TestCVService: +class TestReadService: def setup(self): self.connection_string = "sqlite:///:memory:" self.cv_service = ReadService(self.connection_string, debug=False) diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index c73dfc0..512fa10 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -104,7 +104,6 @@ def test_get_all_sites(self): if isinstance(sites, list) and len(sites) > 0: assert site.code == sites[0].code - def test_get_site_by_id_fail(self): assert self.series_service.get_site_by_id(0) == None From 07516a97834cb827be9a237a5cab523f86f4eb83 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Mon, 17 Oct 2016 13:15:25 -0600 Subject: [PATCH 16/19] Updated the acl to process level, and vertical datum to elevation datum --- odmtools/controller/logicEditTools.py | 2 +- odmtools/gui/wizSave.py | 2 +- odmtools/odmservices/ReadService.py | 6 ++---- odmtools/odmservices/edit_service.py | 2 +- odmtools/odmservices/series_service.py | 17 ++++++++--------- tests/test_odmservices/test_series_service.py | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/odmtools/controller/logicEditTools.py b/odmtools/controller/logicEditTools.py index 2504bd8..8ee3814 100644 --- a/odmtools/controller/logicEditTools.py +++ b/odmtools/controller/logicEditTools.py @@ -379,7 +379,7 @@ def toggle_record(self, value): # Creates ################### def create_qcl(self, code, definition, explanation): - qcl = self._edit_service.create_qcl(code, definition, explanation) + qcl = self._edit_service.create_processing_level(code, definition, explanation) if self._record: self._script('new_qcl = series_service.get_qcl_by_id(%s)\n' % (qcl.id)) Publisher.sendMessage("scroll") diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index a8837d9..657afb4 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -380,7 +380,7 @@ def on_wizard_finished(self, event): else: QCL = self.record_service.get_qcl(QCL) else: - QCL = self.record_service.create_qcl(QCL.code, QCL.definition, QCL.explanation) + QCL = self.record_service.create_processing_level(QCL.code, QCL.definition, QCL.explanation) #if variable exists use its id if self.series_service.variable_exists(Variable): diff --git a/odmtools/odmservices/ReadService.py b/odmtools/odmservices/ReadService.py index 7b98d7b..e57dc2e 100644 --- a/odmtools/odmservices/ReadService.py +++ b/odmtools/odmservices/ReadService.py @@ -18,8 +18,7 @@ def __init__(self, connection_string="", debug=False): # Controlled Vocabulary get methods #return a list of all terms in the cv def get_vertical_datum_cvs(self): - result = self._edit_session.query(VerticalDatumCV).order_by(VerticalDatumCV.term).all() - return result + return self.read_service.getCVs(type="Elevation Datum") def get_samples(self): return self.read_service.getSamplingFeatures(ids=None, codes=None, uuids=None, type=None, wkt=None) @@ -47,8 +46,7 @@ def get_data_type_cvs(self): return self.read_service.getCVs(type="dataset type") def get_general_category_cvs(self): - result = self._edit_session.query(GeneralCategoryCV).order_by(GeneralCategoryCV.term).all() - return result + return self.read_service.getAnnotations(type="categoricalresultvalue") def get_censor_code_cvs(self): return self.read_service.getCVs(type="censorcode") diff --git a/odmtools/odmservices/edit_service.py b/odmtools/odmservices/edit_service.py index 567b0dc..eb66c2b 100644 --- a/odmtools/odmservices/edit_service.py +++ b/odmtools/odmservices/edit_service.py @@ -594,7 +594,7 @@ def save_existing(self, var=None, method=None, qcl=None): return False def create_qcl(self, code, definition, explanation): - return self.memDB.series_service.create_qcl(code, definition, explanation) + return self.memDB.series_service.create_processing_level(code, definition, explanation) def create_method(self, description, link): return self.memDB.series_service.create_method(description, link) diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 42e4c64..429bf2f 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -7,7 +7,7 @@ from odmtools.odmdata import SessionFactory from odmtools.odmdata import Site # from odmtools.odmdata import Variable -from odm2api.ODM2.models import Variables +from odm2api.ODM2.models import * from odmtools.odmdata import Unit from odmtools.odmdata import Series from odmtools.odmdata import DataValue @@ -22,7 +22,7 @@ import pandas as pd from odm2api.ODM2.services.createService import CreateODM2 from odm2api.ODM2.models import Annotations -from odm2api.ODM2.services.readService import ReadODM2 +from odm2api.ODM2.services.readService import * # tool = LoggerTool() @@ -619,7 +619,7 @@ def create_variable( return self.create_service.createVariable(var=variable) - def create_qcl(self, code, definition, explanation): + def create_processing_level(self, code, definition, explanation): """ qcl -> Processing Level in ODM2 :param code: @@ -627,18 +627,17 @@ def create_qcl(self, code, definition, explanation): :param explanation: :return: """ - qcl = QualityControlLevel() - qcl.code = code - qcl.definition = definition - qcl.explanation = explanation - return self.create_service.createProcessingLevel(proclevel=qcl) + procLevel = ProcessingLevels() + procLevel.ProcessingLevelCode = code + procLevel.Definition = definition + procLevel.Explanation = explanation + return self.create_service.createProcessingLevel(proclevel=procLevel) def create_annotation_by_anno(self, annotation): return self.create_service.create(annotation) def create_annotation(self, code, description): """ - :param code: :param description: :return: diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index 512fa10..05efa88 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -359,7 +359,7 @@ def test_create_variable(self): assert variable.variable_unit_id == unit.id def test_create_qcl(self): - qcl = self.series_service.create_qcl("Code", "Definition", "Explanation") + qcl = self.series_service.create_processing_level("Code", "Definition", "Explanation") assert qcl.id != None assert qcl.code == "Code" From 919afc1efff78137fdb312bea6f469b354a2bd9a Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Mon, 17 Oct 2016 14:11:43 -0600 Subject: [PATCH 17/19] Converted most of the get functions in the series service to connect to omd2 Series, values, plot, and dataframes need to be converted --- odmtools/controller/WizardMethodController.py | 4 +- .../WizardProcessLevelController.py | 4 +- odmtools/gui/pageQCL.py | 2 +- odmtools/gui/wizSave.py | 2 +- odmtools/odmservices/edit_service.py | 2 +- odmtools/odmservices/series_service.py | 106 ++++++------------ tests/test_odmservices/test_series_service.py | 8 +- 7 files changed, 46 insertions(+), 82 deletions(-) diff --git a/odmtools/controller/WizardMethodController.py b/odmtools/controller/WizardMethodController.py index e0c245d..9272987 100644 --- a/odmtools/controller/WizardMethodController.py +++ b/odmtools/controller/WizardMethodController.py @@ -64,7 +64,7 @@ def getMethod(self): m = Method() if self.method_view.auto_method_radio.GetValue(): description = "Values derived from ODM Tools Python" - m = self.series_service.get_method_by_description(description) + m = self.series_service.get_method_by_code(description) if m is None: m = Method() m.description = description @@ -72,7 +72,7 @@ def getMethod(self): index = self.method_view.existing_method_table.GetFirstSelected() desc = self.method_view.existing_method_table.GetItem(index, 0).GetText() - m = self.series_service.get_method_by_description(desc) + m = self.series_service.get_method_by_code(desc) elif self.method_view.create_method_radio.GetValue(): m.description = self.method_view.description_text_ctrl.GetValue() diff --git a/odmtools/controller/WizardProcessLevelController.py b/odmtools/controller/WizardProcessLevelController.py index 76f3aed..f172453 100644 --- a/odmtools/controller/WizardProcessLevelController.py +++ b/odmtools/controller/WizardProcessLevelController.py @@ -39,7 +39,7 @@ def __set_create_proces_section(self, active): def __fetch_data(self): series_service = self.service_manager.get_series_service() - processes = series_service.get_all_qcls() + processes = series_service.get_all_processing_levels() data = [] for proc in processes: @@ -62,6 +62,6 @@ def getQCL(self): elif self.processing_level_view.existing_process_radio.GetValue(): selected_row = self.processing_level_view.existing_process_table.get_selected_row() code = selected_row[0] - q = self.service_manager.get_series_service().get_qcl_by_code(qcl_code=code) + q = self.service_manager.get_series_service().get_processing_level_by_code(proc_level_code=code) return q diff --git a/odmtools/gui/pageQCL.py b/odmtools/gui/pageQCL.py index 57f4970..044671c 100644 --- a/odmtools/gui/pageQCL.py +++ b/odmtools/gui/pageQCL.py @@ -122,7 +122,7 @@ def getQCL(self): logger.debug("lstQCL: %s" %(self.lstQCL)) code= self.lstQCL.GetItem(index, 0).GetText() logger.debug(code) - q= self.series_service.get_qcl_by_code(code) + q= self.series_service.get_processing_level_by_code(code) ## q.id = self.lstQCL.GetItem(index,3).GetText() ## q.code = self.lstQCL.GetItem(index, 0).GetText() diff --git a/odmtools/gui/wizSave.py b/odmtools/gui/wizSave.py index 657afb4..d88d3af 100644 --- a/odmtools/gui/wizSave.py +++ b/odmtools/gui/wizSave.py @@ -48,7 +48,7 @@ def __init__(self, parent, title, series_service, qcl): self._init_data(self.panel.series_service) def _init_data(self, series): - qcl = series.get_all_qcls() + qcl = series.get_all_processing_levels() index = 0 for q, i in zip(qcl, range(len(qcl))): num_items = self.panel.lstQCL.GetItemCount() diff --git a/odmtools/odmservices/edit_service.py b/odmtools/odmservices/edit_service.py index eb66c2b..bcf52cf 100644 --- a/odmtools/odmservices/edit_service.py +++ b/odmtools/odmservices/edit_service.py @@ -284,7 +284,7 @@ def get_filter_list(self): return self._filter_list def get_qcl(self, qcl_id): - return self.memDB.series_service.get_qcl_by_id(qcl_id) + return self.memDB.series_service.get_processing_level_by_id(qcl_id) def get_method(self, method_id): return self.memDB.series_service.get_method_by_id(method_id) diff --git a/odmtools/odmservices/series_service.py b/odmtools/odmservices/series_service.py index 429bf2f..30ce67d 100644 --- a/odmtools/odmservices/series_service.py +++ b/odmtools/odmservices/series_service.py @@ -1,27 +1,20 @@ import logging - - from sqlalchemy import distinct, func - - from odmtools.odmdata import SessionFactory from odmtools.odmdata import Site # from odmtools.odmdata import Variable from odm2api.ODM2.models import * -from odmtools.odmdata import Unit from odmtools.odmdata import Series from odmtools.odmdata import DataValue from odmtools.odmdata import Qualifier from odmtools.odmdata import OffsetType from odmtools.odmdata import Sample # from odmtools.odmdata import Method -from odm2api.ODM2.models import Methods from odmtools.odmdata import QualityControlLevel from odmtools.odmdata import ODMVersion from odmtools.common.logger import LoggerTool import pandas as pd from odm2api.ODM2.services.createService import CreateODM2 -from odm2api.ODM2.models import Annotations from odm2api.ODM2.services.readService import * @@ -51,12 +44,13 @@ def get_db_version(self): ##################### # Site methods - def get_all_sites(self): + def get_all_sites(self): # Site -> Sampling Feature in ODM2 """ :return: List[Sites] """ - return self._edit_session.query(Site).order_by(Site.code).all() + # return self._edit_session.query(Site).order_by(Site.code).all() + return self.read_service.getSamplingFeatures(ids=None, codes=None, uuids=None,type=None, wkt=None) def get_used_sites(self): @@ -65,10 +59,12 @@ def get_used_sites(self): :return: List[Sites] """ try: - site_ids = [x[0] for x in self._edit_session.query(distinct(Series.site_id)).all()] + site_ids = [site_ids[0] for site_ids in self._edit_session.query(distinct(Results.FeatureActionID)).all()] except: site_ids = None + site_ids = self._edit_session.query(FeatureActions.SamplingFeatureID).filter(FeatureActions.FeatureActionID.in_(site_ids)).all() + if not site_ids: return None @@ -85,10 +81,11 @@ def get_site_by_id(self, site_id): :param site_id: integer- the identification number of the site :return: Sites """ - try: - return self._edit_session.query(Site).filter_by(id=site_id).first() - except: - return None + # try: + # return self._edit_session.query(Site).filter_by(id=site_id).first() + # except: + # return None + return self.read_service.getSamplingFeatures(ids=site_id) # Variables methods def get_used_variables(self): @@ -98,10 +95,12 @@ def get_used_variables(self): """ try: - var_ids = [x[0] for x in self._edit_session.query(distinct(Series.variable_id)).all()] + # var_ids = [x[0] for x in self._edit_session.query(distinct(Series.variable_id)).all()] + var_ids = [x[0] for x in self._edit_session.query(distinct(Results.VariableID)).all()] except: var_ids = None + var_ids = self._edit_session.query(Results.VariableID).filter(Results.VariableID.in_(var_ids)).all() if not var_ids: return None @@ -115,10 +114,9 @@ def get_used_variables(self): def get_all_variables(self): """ - :return: List[Variables] """ - return self._edit_session.query(Variables).all() + return self.read_service.getVariables(ids=None, codes=None) def get_variable_by_id(self, variable_id): """ @@ -126,10 +124,7 @@ def get_variable_by_id(self, variable_id): :param variable_id: int :return: Variables """ - try: - return self._edit_session.query(Variables).filter_by(id=variable_id).first() - except: - return None + return self.read_service.getVariables(ids=variable_id) def get_variable_by_code(self, variable_code): """ @@ -137,10 +132,7 @@ def get_variable_by_code(self, variable_code): :param variable_code: str :return: Variables """ - try: - return self._edit_session.query(Variables).filter_by(code=variable_code).first() - except: - return None + return self.read_service.getVariables(codes=variable_code) def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeUnits """ @@ -163,50 +155,39 @@ def get_variables_by_site_code(self, site_code): # covers NoDV, VarUnits, TimeU # Unit methods def get_all_units(self): """ - :return: List[Units] """ - return self._edit_session.query(Unit).all() + return self.read_service.getUnits(ids=None, name=None, type=None) def get_unit_by_name(self, unit_name): """ - :param unit_name: str :return: Units """ - try: - return self._edit_session.query(Unit).filter_by(name=unit_name).first() - except: - return None + return self.read_service.getUnits(name=unit_name) def get_unit_by_id(self, unit_id): """ - :param unit_id: int :return: Units """ - try: - return self._edit_session.query(Unit).filter_by(id=unit_id).first() - except: - return None + self.read_service.getUnits(ids=unit_id) - - def get_all_qualifiers(self): + def get_all_qualifiers(self): # Rename to annotations """ - :return: List[Qualifiers] """ # result = self._edit_session.query(Annotations).order_by(Annotations.AnnotationCode).all() # return result return self.read_service.getAnnotations(None) - def get_qualifier_by_code(self, code): + def get_qualifier_by_code(self, code): # Rename to get_annotations_by_type """ - :return: Qualifiers """ - result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() - return result + # result = self._edit_session.query(Qualifier).filter(Qualifier.code==code).first() + # return result + return self.read_service.getAnnotations(type=code) def get_qualifiers_by_series_id(self, series_id): """ @@ -218,40 +199,23 @@ def get_qualifiers_by_series_id(self, series_id): Series.data_values).filter(Series.id == series_id, DataValue.qualifier_id != None).distinct().subquery() return self._edit_session.query(Qualifier).join(subquery).distinct().all() - #QCL methods - def get_all_qcls(self): - return self._edit_session.query(QualityControlLevel).all() + def get_all_processing_levels(self): + return self.read_service.getProcessingLevels(ids=None, codes=None) - def get_qcl_by_id(self, qcl_id): - try: - return self._edit_session.query(QualityControlLevel).filter_by(id=qcl_id).first() - except: - return None + def get_processing_level_by_id(self, proc_level_id): + return self.read_service.getProcessingLevels(ids=proc_level_id) - def get_qcl_by_code(self, qcl_code): - try: - return self._edit_session.query(QualityControlLevel).filter_by(code=qcl_code).first() - except: - return None + def get_processing_level_by_code(self, proc_level_code): + return self.read_service.getProcessingLevels(codes=proc_level_code) - # Method methods def get_all_methods(self): - return self._edit_session.query(Methods).all() + return self.read_service.getMethods(ids=None, codes=None, type=None) def get_method_by_id(self, method_id): - try: - result = self._edit_session.query(Methods).filter_by(id=method_id).first() - except: - result = None - return result + return self.read_service.getMethods(ids=method_id) - def get_method_by_description(self, method_code): - try: - result = self._edit_session.query(Methods).filter_by(description=method_code).first() - except: - result = None - logger.error("method not found") - return result + def get_method_by_code(self, method_code): + return self.read_service.getMethods(codes=method_code) def get_offset_types_by_series_id(self, series_id): """ diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index 05efa88..d14c569 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -248,17 +248,17 @@ def test_get_data_value_by_id(self): assert dv.data_value == db_dv.data_value def test_get_qcl_by_id(self): - assert self.series_service.get_qcl_by_id(10) == None + assert self.series_service.get_processing_level_by_id(10) == None qcl = test_util.add_qcl(self.session) - db_qcl = self.series_service.get_qcl_by_id(qcl.id) + db_qcl = self.series_service.get_processing_level_by_id(qcl.id) assert qcl.code == db_qcl.code def test_get_all_qcls(self): - assert self.series_service.get_all_qcls() == [] + assert self.series_service.get_all_processing_levels() == [] qcl = test_util.add_qcl(self.session) - all_qcls = self.series_service.get_all_qcls() + all_qcls = self.series_service.get_all_processing_levels() assert len(all_qcls) == 1 assert qcl.id == all_qcls[0].id From 6794dba71fcdde5497efb679cd1355667b0d295c Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Thu, 20 Oct 2016 12:08:39 -0600 Subject: [PATCH 18/19] #293 Upadated test_util.py --- tests/test_odmservices/test_series_service.py | 8 +- tests/test_util.py | 140 ++++++++---------- 2 files changed, 62 insertions(+), 86 deletions(-) diff --git a/tests/test_odmservices/test_series_service.py b/tests/test_odmservices/test_series_service.py index d14c569..9abaf1e 100644 --- a/tests/test_odmservices/test_series_service.py +++ b/tests/test_odmservices/test_series_service.py @@ -250,14 +250,14 @@ def test_get_data_value_by_id(self): def test_get_qcl_by_id(self): assert self.series_service.get_processing_level_by_id(10) == None - qcl = test_util.add_qcl(self.session) + qcl = test_util.add_process_level(self.session) db_qcl = self.series_service.get_processing_level_by_id(qcl.id) assert qcl.code == db_qcl.code def test_get_all_qcls(self): assert self.series_service.get_all_processing_levels() == [] - qcl = test_util.add_qcl(self.session) + qcl = test_util.add_process_level(self.session) all_qcls = self.series_service.get_all_processing_levels() assert len(all_qcls) == 1 @@ -307,7 +307,7 @@ def test_create_new_series(self): variable = test_util.add_variable(self.session) method = test_util.add_method(self.session) source = test_util.add_source(self.session) - qcl = test_util.add_qcl(self.session) + qcl = test_util.add_process_level(self.session) dvs = [] for val in range(10): @@ -382,7 +382,7 @@ def test_delete_values(self): def test_qcl_exists(self): - qcl = test_util.add_qcl(self.session) + qcl = test_util.add_process_level(self.session) assert self.series_service.qcl_exists(qcl) == True qcl.code = "00000" diff --git a/tests/test_util.py b/tests/test_util.py index 444fb7a..77dfdd8 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,6 +3,8 @@ from odmtools.odmdata import * import os import sys +from odm2api.ODM2.models import * +from odm2api.ODM2.services.readService import DetailedResult def build_db(engine): @@ -40,10 +42,11 @@ def add_bulk_data_values(session, series, dvs_size): session.commit() return df + def add_series_bulk_data(session, dvs_size=50): site = add_site(session) var = add_variable(session) - qcl = add_qcl(session) + qcl = add_process_level(session) method = add_method(session) source = add_source(session) @@ -73,33 +76,18 @@ def add_series_bulk_data(session, dvs_size=50): session.commit() return series + # Create Series objects def add_series(session): - site = add_site(session) + result = Results() var = add_variable(session) - qcl = add_qcl(session) - method = add_method(session) - source = add_source(session) - - series = Series() - series.site = site - series.site_code = site.code - series.variable = var - series.variable_code = var.code - series.method = method - series.source = source - series.quality_control_level_id = qcl.id - - dvs = add_data_values(session, series) - series.begin_date_time = dvs[0].local_date_time - series.end_date_time = dvs[-1].local_date_time - series.begin_date_time_utc = dvs[0].date_time_utc - series.end_date_time_utc = dvs[-1].date_time_utc - series.value_count = len(dvs) - - session.add(series) + qcl = add_process_level(session) + result.VariableObj = var + result.ProcessingLevelObj = qcl + result.ProcessingLevelID = qcl.ProcessingLevelID + session.add(result) session.commit() - return series + return result def add_data_values(session, series): @@ -126,14 +114,14 @@ def add_data_values(session, series): def add_site(session): spatial_ref = add_spatial_reference(session) - site = Site("ABC123", "Test Site") - site.latitude = 10.0 - site.longitude = 10.0 - site.lat_long_datum_id = spatial_ref.id - site.local_projection_id = spatial_ref.id - site.elevation_m = 1000 - site.local_x = 10.0 - site.local_y = 10.0 + site = Sites("ABC123", "Test Site") + site.Latitude = 10.0 + site.Longitude = 10.0 + site.lat_long_datum_id = spatial_ref.SpatialReferenceID + site.local_projection_id = spatial_ref.SpatialReferenceID + site.Elevation_m = 1000 + # site.local_x = 10.0 + # site.local_y = 10.0 session.add(site) session.commit() return site @@ -141,60 +129,48 @@ def add_site(session): def add_variable(session): unit = add_unit(session) - variable = Variable() - variable.code = "ABC123" - variable.name = "Test Variable" - variable.speciation = "Test" - variable.variable_unit_id = unit.id - variable.sample_medium = "Test Medium" - variable.value_type = "Test Val Type" - variable.is_regular = True - variable.time_support = 3.14 - variable.time_unit_id = unit.id - variable.data_type = "Test Data Type" - variable.general_category = "Test Category" - variable.no_data_value = -2000.0 + variable = Variables() + variable.VariableCode = "ABC123" + variable.VariableNameCV = "Test Variable" + variable.SpeciationCV = "Test" + variable.VariableID = unit.id + variable.NoDataValue = -2000.0 session.add(variable) session.commit() return variable def add_method(session): - method = Method() - method.description = "This is a test" + method = Methods() + method.MethodDescription = "This is a test" session.add(method) session.commit() return method -def add_qcl(session): - qcl = QualityControlLevel() - qcl.code = "ABC123" - qcl.definition = "This is a test" - qcl.explanation = "A test is a thing that tests code" - session.add(qcl) +def add_process_level(session): + proc_level = ProcessingLevels() + proc_level.ProcessingLevelCode = "ABC123" + proc_level.Definition = "This is a test" + proc_level.Explanation = "A test is a thing that tests code" + session.add(proc_level) session.commit() - return qcl + return proc_level def add_source(session): - source = Source() - source.organization = "Test Organization" - source.description = "This is a test" - source.contact_name = "Test Name" - source.phone = "555-1234" - source.email = "source@example.com" - source.address = "123 Test Street" - source.city = "Metropolis" - source.state = "NY" - source.zip_code = "12345" - source.citation = "Test Citation" - - iso = add_iso_metadata(session) - source.iso_metadata_id = iso.id - session.add(source) + organization = Organizations() + affiliation = Affiliations() + organization.OrganizationName = "Test Organization" + organization.OrganizationDescription = "This is a test" + affiliation.PersonLink = "Test Name" + affiliation.PrimaryPhone = "555-1234" + affiliation.PrimaryEmail = "source@example.com" + affiliation.PrimaryAddress = "123 Test Street" + affiliation.OrganizationObj = organization + session.add(affiliation) session.commit() - return source + return affiliation def add_iso_metadata(session): @@ -209,8 +185,8 @@ def add_iso_metadata(session): def add_spatial_reference(session): - spatial_ref = SpatialReference() - spatial_ref.srs_name = "This is a test" + spatial_ref = SpatialReferences() + spatial_ref.SRSName = "This is a test" session.add(spatial_ref) session.commit() return spatial_ref @@ -258,19 +234,19 @@ def add_site_type_cv(session): def add_variable_name_cv(session): - var_name_cv = VariableNameCV() - var_name_cv.term = "Test" - var_name_cv.definition = "This is a test" + var_name_cv = CVVariableName() + var_name_cv.Term = "Test" + var_name_cv.Definition = "This is a test" session.add(var_name_cv) session.commit() return var_name_cv def add_unit(session): - unit = Unit() - unit.name = "Test" - unit.type = "Test" - unit.abbreviation = "T" + unit = Units() + unit.UnitsName = "Test" + unit.UnitsTypeCV = "Test" + unit.UnitsAbbreviation = "T" session.add(unit) session.commit() return unit @@ -286,9 +262,9 @@ def add_offset_type_cv(session, unit_id): def add_speciation_cv(session): - spec = SpeciationCV() - spec.term = "Test" - spec.definition = "This is a test" + spec = CVSpeciation() + spec.Term = "Test" + spec.Definition = "This is a test" session.add(spec) session.commit() return spec From 70eac3c6a5cdb16f51b9c1f5f996be262acdc2d9 Mon Sep 17 00:00:00 2001 From: Francisco Arrieta Date: Thu, 20 Oct 2016 12:47:30 -0600 Subject: [PATCH 19/19] #293 Finished updating test_util.py --- tests/test_util.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index 77dfdd8..6827b9f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -25,18 +25,13 @@ def add_bulk_data_values(session, series, dvs_size): df['DateTimeUTC'] = pd.to_datetime(df['DateTimeUTC']).astype(datetime.datetime) dvs = [] for record in df.to_dict('records')[:dvs_size]: - dv = DataValue() - dv.data_value = record['DataValue'] - dv.local_date_time = record['LocalDateTime'] - dv.utc_offset = record['UTCOffset'] - dv.date_time_utc = record['DateTimeUTC'] - dv.site_id = series.site_id - dv.variable_id = series.variable_id - dv.censor_code = record['CensorCode'] - dv.method_id = series.method_id - dv.source_id = series.source_id - dv.quality_control_level_id = series.quality_control_level_id - dvs.append(dv) + timeseries_result_value = TimeSeriesResultValues() + timeseries_result_value.DataValue = record["DataValue"] + timeseries_result_value.ValueDateTimeUTCOffset = record["UTCOffset"] + timeseries_result_value.ValueDateTime = record["DateTimeUTC"] + timeseries_result_value.CensorCodeCV = record["CensorCode"] + timeseries_result_value.QualityCodeCV = series.quality_control_level_code + dvs.append(timeseries_result_value) series.data_values = dvs session.add_all(dvs) session.commit() @@ -93,18 +88,14 @@ def add_series(session): def add_data_values(session, series): dvs = [] for i in range(10): - dv = DataValue() - dv.data_value = i - dv.local_date_time = datetime.datetime.now() - datetime.timedelta(days=i) - dv.utc_offset = 0 - dv.date_time_utc = dv.local_date_time - dv.site_id = series.site_id - dv.variable_id = series.variable_id - dv.censor_code = "NC" - dv.method_id = series.method_id - dv.source_id = series.source_id - dv.quality_control_level_id = series.quality_control_level_id - dvs.append(dv) + timeseries = TimeSeriesResultValues() + timeseries.DataValue = i + timeseries.TimeAggregationInterval = datetime.datetime.now() - datetime.timedelta(days=i) + timeseries.ValueDateTimeUTCOffset = 0 + timeseries.ValueDateTime = timeseries.TimeAggregationInterval + timeseries.CensorCodeCV = "NC" + timeseries.QualityCodeCV = series.quality_control_level + dvs.append(timeseries) series.data_values = dvs session.add_all(dvs)