diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index cae2a8fd2f7..e4ddebd6859 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -45,6 +45,14 @@ exposed to the Python bindings for unit testing purposes only. %TypeHeaderCode #include "qgsabstractreportsection.h" +%End +%ConvertToSubClassCode + if ( dynamic_cast< QgsReportSectionFieldGroup * >( sipCpp ) ) + sipType = sipType_QgsReportSectionFieldGroup; + else if ( dynamic_cast< QgsReportSectionLayout * >( sipCpp ) ) + sipType = sipType_QgsReportSectionLayout; + else + sipType = NULL; %End public: @@ -58,6 +66,11 @@ Note that ownership is not transferred to ``parent``. + virtual QString type() const = 0; +%Docstring +Returns the section subclass type. +%End + virtual QgsAbstractReportSection *clone() const = 0 /Factory/; %Docstring Clones the report section. Ownership of the returned section is @@ -272,6 +285,20 @@ Sets the current ``context`` for this section. Returns the current context for this section. .. seealso:: :py:func:`setContext()` +%End + + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; +%Docstring +Stores the section state in a DOM element. + +.. seealso:: :py:func:`readXml()` +%End + + bool readXml( const QDomElement §ionElement, const QDomDocument &document, const QgsReadWriteContext &context ); +%Docstring +Sets the item state from a DOM element. + +.. seealso:: :py:func:`writeXml()` %End protected: @@ -291,9 +318,27 @@ Copies the common properties of a report section to a ``destination`` section. This method should be called from clone() implementations. %End - void setParent( QgsAbstractReportSection *parent ); + virtual void setParentSection( QgsAbstractReportSection *parent ); %Docstring Sets the ``parent`` report section. +%End + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; +%Docstring +Stores section state within an XML DOM element. + +.. seealso:: :py:func:`writeXml()` + +.. seealso:: :py:func:`readPropertiesFromElement()` +%End + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); +%Docstring +Sets section state from a DOM element. + +.. seealso:: :py:func:`writePropertiesToElement()` + +.. seealso:: :py:func:`readXml()` %End private: diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index de44b514426..a4aeea50f6b 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -38,6 +38,7 @@ Constructor for QgsReport, associated with the specified Note that ownership is not transferred to ``project``. %End + virtual QString type() const; virtual QgsProject *layoutProject() const; virtual QgsReport *clone() const /Factory/; diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 977e65f95fe..972ffa1da45 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -34,6 +34,8 @@ Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` Note that ownership is not transferred to ``parent``. %End + virtual QString type() const; + QgsLayout *body(); %Docstring Returns the body layout for the section. @@ -85,6 +87,15 @@ Sets the ``field`` associated with this section. virtual void reset(); + virtual void setParentSection( QgsAbstractReportSection *parent ); + + + protected: + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + }; diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index 7bfdab56365..9d01b5740f1 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -33,6 +33,8 @@ Constructor for QgsReportSectionLayout, attached to the specified ``parent`` sec Note that ownership is not transferred to ``parent``. %End + virtual QString type() const; + QgsLayout *body(); %Docstring Returns the body layout for the section. @@ -55,6 +57,13 @@ is transferred to the report section. virtual QgsLayout *nextBody( bool &ok ); + protected: + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + + }; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index b68c61cf275..9a3356d154e 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -17,6 +17,8 @@ #include "qgsabstractreportsection.h" #include "qgslayout.h" #include "qgsreport.h" +#include "qgsreportsectionfieldgroup.h" +#include "qgsreportsectionlayout.h" ///@cond NOT_STABLE @@ -54,6 +56,95 @@ void QgsAbstractReportSection::setContext( const QgsReportSectionContext &contex } } +bool QgsAbstractReportSection::writeXml( QDomElement &parentElement, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement element = doc.createElement( QStringLiteral( "Section" ) ); + element.setAttribute( QStringLiteral( "type" ), type() ); + + element.setAttribute( QStringLiteral( "headerEnabled" ), mHeaderEnabled ? "1" : "0" ); + if ( mHeader ) + { + QDomElement headerElement = doc.createElement( QStringLiteral( "header" ) ); + headerElement.appendChild( mHeader->writeXml( doc, context ) ); + element.appendChild( headerElement ); + } + element.setAttribute( QStringLiteral( "footerEnabled" ), mFooterEnabled ? "1" : "0" ); + if ( mFooter ) + { + QDomElement footerElement = doc.createElement( QStringLiteral( "footer" ) ); + footerElement.appendChild( mFooter->writeXml( doc, context ) ); + element.appendChild( footerElement ); + } + + for ( QgsAbstractReportSection *section : mChildren ) + { + section->writeXml( element, doc, context ); + } + + writePropertiesToElement( element, doc, context ); + + parentElement.appendChild( element ); + return true; +} + +bool QgsAbstractReportSection::readXml( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + if ( element.nodeName() != QStringLiteral( "Section" ) ) + { + return false; + } + + mHeaderEnabled = element.attribute( QStringLiteral( "headerEnabled" ), "0" ).toInt(); + mFooterEnabled = element.attribute( QStringLiteral( "footerEnabled" ), "0" ).toInt(); + const QDomElement headerElement = element.firstChildElement( QStringLiteral( "header" ) ); + if ( !headerElement.isNull() ) + { + const QDomElement headerLayoutElem = headerElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( project() ); + header->readXml( headerLayoutElem, doc, context ); + mHeader = std::move( header ); + } + const QDomElement footerElement = element.firstChildElement( QStringLiteral( "footer" ) ); + if ( !footerElement.isNull() ) + { + const QDomElement footerLayoutElem = footerElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( project() ); + footer->readXml( footerLayoutElem, doc, context ); + mFooter = std::move( footer ); + } + + const QDomNodeList sectionItemList = element.childNodes(); + for ( int i = 0; i < sectionItemList.size(); ++i ) + { + const QDomElement currentSectionElem = sectionItemList.at( i ).toElement(); + if ( currentSectionElem.nodeName() != QStringLiteral( "Section" ) ) + continue; + + const QString sectionType = currentSectionElem.attribute( QStringLiteral( "type" ) ); + + //TODO - eventually move this to a registry when there's enough subclasses to warrant it + std::unique_ptr< QgsAbstractReportSection > section; + if ( sectionType == QLatin1String( "SectionFieldGroup" ) ) + { + section = qgis::make_unique< QgsReportSectionFieldGroup >(); + } + else if ( sectionType == QLatin1String( "SectionLayout" ) ) + { + section = qgis::make_unique< QgsReportSectionLayout >(); + } + + if ( section ) + { + appendChild( section.get() ); + section->readXml( currentSectionElem, doc, context ); + ( void )section.release(); //ownership was transferred already + } + } + + bool result = readPropertiesFromElement( element, doc, context ); + return result; +} + QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) { QString base = QStringLiteral( "%1_%2" ).arg( baseFilePath ).arg( mSectionNumber, 4, 10, QChar( '0' ) ); @@ -235,13 +326,13 @@ QgsAbstractReportSection *QgsAbstractReportSection::childSection( int index ) void QgsAbstractReportSection::appendChild( QgsAbstractReportSection *section ) { - section->setParent( this ); + section->setParentSection( this ); mChildren.append( section ); } void QgsAbstractReportSection::insertChild( int index, QgsAbstractReportSection *section ) { - section->setParent( this ); + section->setParentSection( this ); index = std::max( 0, index ); index = std::min( index, mChildren.count() ); mChildren.insert( index, section ); @@ -285,5 +376,15 @@ void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *d } } +bool QgsAbstractReportSection::writePropertiesToElement( QDomElement &, QDomDocument &, const QgsReadWriteContext & ) const +{ + return true; +} + +bool QgsAbstractReportSection::readPropertiesFromElement( const QDomElement &, const QDomDocument &, const QgsReadWriteContext & ) +{ + return true; +} + ///@endcond diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index cb732191340..3e51a49b68a 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -53,6 +53,17 @@ class CORE_EXPORT QgsReportSectionContext class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator { +#ifdef SIP_RUN + SIP_CONVERT_TO_SUBCLASS_CODE + if ( dynamic_cast< QgsReportSectionFieldGroup * >( sipCpp ) ) + sipType = sipType_QgsReportSectionFieldGroup; + else if ( dynamic_cast< QgsReportSectionLayout * >( sipCpp ) ) + sipType = sipType_QgsReportSectionLayout; + else + sipType = NULL; + SIP_END +#endif + public: /** @@ -69,6 +80,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator //! QgsAbstractReportSection cannot be copied QgsAbstractReportSection &operator=( const QgsAbstractReportSection &other ) = delete; + /** + * Returns the section subclass type. + */ + virtual QString type() const = 0; + /** * Clones the report section. Ownership of the returned section is * transferred to the caller. @@ -242,6 +258,18 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ const QgsReportSectionContext &context() const { return mContext; } + /** + * Stores the section state in a DOM element. + * \see readXml() + */ + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; + + /** + * Sets the item state from a DOM element. + * \see writeXml() + */ + bool readXml( const QDomElement §ionElement, const QDomDocument &document, const QgsReadWriteContext &context ); + protected: //! Report sub-sections @@ -263,7 +291,21 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator /** * Sets the \a parent report section. */ - void setParent( QgsAbstractReportSection *parent ) { mParent = parent; } + virtual void setParentSection( QgsAbstractReportSection *parent ) { mParent = parent; } + + /** + * Stores section state within an XML DOM element. + * \see writeXml() + * \see readPropertiesFromElement() + */ + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + /** + * Sets section state from a DOM element. + * \see writePropertiesToElement() + * \see readXml() + */ + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); private: diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp index 9051791e0b0..560952ce729 100644 --- a/src/core/layout/qgsreport.cpp +++ b/src/core/layout/qgsreport.cpp @@ -40,15 +40,20 @@ void QgsReport::setName( const QString &name ) QDomElement QgsReport::writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const { QDomElement element = document.createElement( QStringLiteral( "Report" ) ); - - - + writeXml( element, document, context ); + element.setAttribute( QStringLiteral( "name" ), mName ); return element; } bool QgsReport::readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) { - + const QDomNodeList sectionList = layoutElement.elementsByTagName( QStringLiteral( "Section" ) ); + if ( sectionList.count() > 0 ) + { + readXml( sectionList.at( 0 ).toElement(), document, context ); + } + setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); + return true; } ///@endcond diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index 3c957595e0a..0325b98d551 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -52,6 +52,7 @@ class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, p */ QgsReport( QgsProject *project ); + QString type() const override { return QStringLiteral( "SectionReport" ); } QgsProject *layoutProject() const override { return mProject; } QgsReport *clone() const override SIP_FACTORY; QString name() const override { return mName; } diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 09a6554c504..ce75358440c 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -98,6 +98,56 @@ void QgsReportSectionFieldGroup::reset() mEncounteredValues.clear(); } +void QgsReportSectionFieldGroup::setParentSection( QgsAbstractReportSection *parent ) +{ + QgsAbstractReportSection::setParentSection( parent ); + if ( !mCoverageLayer ) + mCoverageLayer.resolveWeakly( project() ); +} + +bool QgsReportSectionFieldGroup::writePropertiesToElement( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + element.setAttribute( QStringLiteral( "field" ), mField ); + + if ( mCoverageLayer ) + { + element.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId ); + element.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name ); + element.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source ); + element.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider ); + } + + if ( mBody ) + { + QDomElement bodyElement = doc.createElement( QStringLiteral( "body" ) ); + bodyElement.appendChild( mBody->writeXml( doc, context ) ); + element.appendChild( bodyElement ); + } + return true; +} + +bool QgsReportSectionFieldGroup::readPropertiesFromElement( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + mField = element.attribute( QStringLiteral( "field" ) ); + + QString layerId = element.attribute( QStringLiteral( "coverageLayer" ) ); + QString layerName = element.attribute( QStringLiteral( "coverageLayerName" ) ); + QString layerSource = element.attribute( QStringLiteral( "coverageLayerSource" ) ); + QString layerProvider = element.attribute( QStringLiteral( "coverageLayerProvider" ) ); + mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + mCoverageLayer.resolveWeakly( project() ); + + const QDomElement bodyElement = element.firstChildElement( QStringLiteral( "body" ) ); + if ( !bodyElement.isNull() ) + { + const QDomElement bodyLayoutElem = bodyElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( project() ); + body->readXml( bodyLayoutElem, doc, context ); + mBody = std::move( body ); + } + return true; +} + QgsFeatureRequest QgsReportSectionFieldGroup::buildFeatureRequest() const { QgsFeatureRequest request; diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index 6075376ac99..a6abbda26cd 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -44,6 +44,8 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection */ QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); + QString type() const override { return QStringLiteral( "SectionFieldGroup" ); } + /** * Returns the body layout for the section. * \see setBody() @@ -85,6 +87,12 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; void reset() override; + void setParentSection( QgsAbstractReportSection *parent ) override; + + protected: + + bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; private: diff --git a/src/core/layout/qgsreportsectionlayout.cpp b/src/core/layout/qgsreportsectionlayout.cpp index fa5d85615b5..97798156aed 100644 --- a/src/core/layout/qgsreportsectionlayout.cpp +++ b/src/core/layout/qgsreportsectionlayout.cpp @@ -59,5 +59,29 @@ QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) } } +bool QgsReportSectionLayout::writePropertiesToElement( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + if ( mBody ) + { + QDomElement bodyElement = doc.createElement( QStringLiteral( "body" ) ); + bodyElement.appendChild( mBody->writeXml( doc, context ) ); + element.appendChild( bodyElement ); + } + return true; +} + +bool QgsReportSectionLayout::readPropertiesFromElement( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + const QDomElement bodyElement = element.firstChildElement( QStringLiteral( "body" ) ); + if ( !bodyElement.isNull() ) + { + const QDomElement bodyLayoutElem = bodyElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( project() ); + body->readXml( bodyLayoutElem, doc, context ); + mBody = std::move( body ); + } + return true; +} + ///@endcond diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index 9ddd053c4ac..fcbf6c76958 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -41,6 +41,8 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection */ QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); + QString type() const override { return QStringLiteral( "SectionLayout" ); } + /** * Returns the body layout for the section. * \see setBody() @@ -58,6 +60,11 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; + protected: + + bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; + private: bool mExportedBody = false; diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 7d6b8d461dc..5e317aa4587 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -21,8 +21,11 @@ from qgis.core import (QgsProject, QgsReportSectionFieldGroup, QgsVectorLayer, QgsField, - QgsFeature) + QgsFeature, + QgsReadWriteContext, + QgsUnitTypes) from qgis.testing import start_app, unittest +from qgis.PyQt.QtXml import QDomDocument start_app() @@ -429,6 +432,65 @@ class TestQgsReport(unittest.TestCase): self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) self.assertFalse(r.next()) + def testReadWriteXml(self): + p = QgsProject() + ptLayer = QgsVectorLayer("Point?crs=epsg:4326&field=country:string(20)&field=state:string(20)&field=town:string(20)", "points", "memory") + p.addMapLayer(ptLayer) + + r = QgsReport(p) + r.setName('my report') + # add a header + r.setHeaderEnabled(True) + report_header = QgsLayout(p) + report_header.setUnits(QgsUnitTypes.LayoutInches) + r.setHeader(report_header) + # add a footer + r.setFooterEnabled(True) + report_footer = QgsLayout(p) + report_footer.setUnits(QgsUnitTypes.LayoutMeters) + r.setFooter(report_footer) + + # add some subsections + child1 = QgsReportSectionLayout() + child1_body = QgsLayout(p) + child1_body.setUnits(QgsUnitTypes.LayoutPoints) + child1.setBody(child1_body) + + child2 = QgsReportSectionLayout() + child2_body = QgsLayout(p) + child2_body.setUnits(QgsUnitTypes.LayoutPixels) + child2.setBody(child2_body) + child1.appendChild(child2) + + child2a = QgsReportSectionFieldGroup() + child2a_body = QgsLayout(p) + child2a_body.setUnits(QgsUnitTypes.LayoutInches) + child2a.setBody(child2a_body) + child2a.setField('my field') + child2a.setLayer(ptLayer) + child1.appendChild(child2a) + + r.appendChild(child1) + + doc = QDomDocument("testdoc") + elem = r.writeLayoutXml(doc, QgsReadWriteContext()) + + r2 = QgsReport(p) + self.assertTrue(r2.readLayoutXml(elem, doc, QgsReadWriteContext())) + self.assertEqual(r2.name(), 'my report') + self.assertTrue(r2.headerEnabled()) + self.assertEqual(r2.header().units(), QgsUnitTypes.LayoutInches) + self.assertTrue(r2.footerEnabled()) + self.assertEqual(r2.footer().units(), QgsUnitTypes.LayoutMeters) + + self.assertEqual(r2.childCount(), 1) + self.assertEqual(r2.childSection(0).body().units(), QgsUnitTypes.LayoutPoints) + self.assertEqual(r2.childSection(0).childCount(), 2) + self.assertEqual(r2.childSection(0).childSection(0).body().units(), QgsUnitTypes.LayoutPixels) + self.assertEqual(r2.childSection(0).childSection(1).body().units(), QgsUnitTypes.LayoutInches) + self.assertEqual(r2.childSection(0).childSection(1).field(), 'my field') + self.assertEqual(r2.childSection(0).childSection(1).layer(), ptLayer) + if __name__ == '__main__': unittest.main()