Skip to content

Commit 1ea5a5f

Browse files
committedJan 5, 2018
[FEATURE] Reporting framework
Reports are based on the new layouts engine. They consist of multiple nested sections. Each individual section (and the report itself) can have an optional header and footer (which are themselves layouts, and can consist of multiple pages!). Two different types of sections are implemented so far: - a standard section, which has a single, static body layout. This can be used to embed static layouts mid way through a report - a "field group" section, which repeats its body layout for every feature in a layer. The features are sorted by the selected grouping feature (with an option for ascending/descending sort). If a field group section has child sections (e.g. another field group section with a different field, then only features with unique values for the group feature are iterated over. This allows nested reports, e.g. Report - Country: Australia - State: NSW - Town: Sydney - Town: Woolongong - State: QLD - Town: Beerburrum - Town: Brisbane - Town: Emerald - Country: NZ - State: ... etc In this example country, state or town groups can have their own headers and footers which will be inserted in the report. Reports are configured through a new panel in the layout designer dialog, which is shown when editing a report (created through the Layout Manager Dialog). The organizer allows for adding (and removing) sections to the report, and for selecting which layout (e.g. headers, footers, bodies) to edit within the layout designer.
1 parent 811145e commit 1ea5a5f

File tree

7 files changed

+870
-0
lines changed

7 files changed

+870
-0
lines changed
 

‎python/core/core_auto.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
%Include composer/qgscomposertexttable.sip
163163
%Include composer/qgspaperitem.sip
164164
%Include layout/qgsabstractlayoutiterator.sip
165+
%Include layout/qgsabstractreportsection.sip
165166
%Include layout/qgslayoutaligner.sip
166167
%Include layout/qgslayoutexporter.sip
167168
%Include layout/qgslayoutgridsettings.sip
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/************************************************************************
2+
* This file has been generated automatically from *
3+
* *
4+
* src/core/layout/qgsabstractreportsection.h *
5+
* *
6+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
7+
************************************************************************/
8+
9+
10+
class QgsAbstractReportSection : QgsAbstractLayoutIterator
11+
{
12+
%Docstring
13+
An abstract base class for QgsReport subsections.
14+
15+
.. versionadded:: 3.0
16+
%End
17+
18+
%TypeHeaderCode
19+
#include "qgsabstractreportsection.h"
20+
%End
21+
public:
22+
23+
QgsAbstractReportSection();
24+
%Docstring
25+
Constructor for QgsAbstractReportSection
26+
%End
27+
28+
~QgsAbstractReportSection();
29+
30+
31+
32+
virtual QgsAbstractReportSection *clone() const = 0 /Factory/;
33+
%Docstring
34+
Clones the report section. Ownership of the returned section is
35+
transferred to the caller.
36+
37+
Subclasses should call copyCommonProperties() in their clone()
38+
implementations.
39+
%End
40+
41+
42+
virtual QgsLayout *layout();
43+
44+
virtual bool beginRender();
45+
46+
virtual bool next();
47+
48+
virtual bool endRender();
49+
50+
51+
bool headerEnabled() const;
52+
%Docstring
53+
Returns true if the header for the section is enabled.
54+
55+
.. seealso:: :py:func:`setHeaderEnabled()`
56+
57+
.. seealso:: :py:func:`header()`
58+
59+
.. seealso:: :py:func:`setHeader()`
60+
%End
61+
62+
void setHeaderEnabled( bool enabled );
63+
%Docstring
64+
Sets whether the header for the section is ``enabled``.
65+
66+
.. seealso:: :py:func:`headerEnabled()`
67+
68+
.. seealso:: :py:func:`header()`
69+
70+
.. seealso:: :py:func:`setHeader()`
71+
%End
72+
73+
QgsLayout *header();
74+
%Docstring
75+
Returns the header for the section. Note that the header is only
76+
included if headerEnabled() is true.
77+
78+
.. seealso:: :py:func:`setHeaderEnabled()`
79+
80+
.. seealso:: :py:func:`headerEnabled()`
81+
82+
.. seealso:: :py:func:`setHeader()`
83+
%End
84+
85+
void setHeader( QgsLayout *header /Transfer/ );
86+
%Docstring
87+
Sets the ``header`` for the section. Note that the header is only
88+
included if headerEnabled() is true. Ownership of ``header``
89+
is transferred to the report section.
90+
91+
.. seealso:: :py:func:`setHeaderEnabled()`
92+
93+
.. seealso:: :py:func:`headerEnabled()`
94+
95+
.. seealso:: :py:func:`header()`
96+
%End
97+
98+
bool footerEnabled() const;
99+
%Docstring
100+
Returns true if the footer for the section is enabled.
101+
102+
.. seealso:: :py:func:`setFooterEnabled()`
103+
104+
.. seealso:: :py:func:`footer()`
105+
106+
.. seealso:: :py:func:`setFooter()`
107+
%End
108+
109+
void setFooterEnabled( bool enabled );
110+
%Docstring
111+
Sets whether the footer for the section is ``enabled``.
112+
113+
.. seealso:: :py:func:`footerEnabled()`
114+
115+
.. seealso:: :py:func:`footer()`
116+
117+
.. seealso:: :py:func:`setFooter()`
118+
%End
119+
120+
QgsLayout *footer();
121+
%Docstring
122+
Returns the footer for the section. Note that the footer is only
123+
included if footerEnabled() is true.
124+
125+
.. seealso:: :py:func:`setFooterEnabled()`
126+
127+
.. seealso:: :py:func:`footerEnabled()`
128+
129+
.. seealso:: :py:func:`setFooter()`
130+
%End
131+
132+
void setFooter( QgsLayout *footer /Transfer/ );
133+
%Docstring
134+
Sets the ``footer`` for the section. Note that the footer is only
135+
included if footerEnabled() is true. Ownership of ``footer``
136+
is transferred to the report section.
137+
138+
.. seealso:: :py:func:`setFooterEnabled()`
139+
140+
.. seealso:: :py:func:`footerEnabled()`
141+
142+
.. seealso:: :py:func:`footer()`
143+
%End
144+
145+
int childCount() const;
146+
%Docstring
147+
Return the number of child sections for this report section. The child
148+
sections form the body of the report section.
149+
150+
.. seealso:: :py:func:`children()`
151+
%End
152+
153+
QList< QgsAbstractReportSection * > children();
154+
%Docstring
155+
Return all child sections for this report section. The child
156+
sections form the body of the report section.
157+
158+
.. seealso:: :py:func:`childCount()`
159+
160+
.. seealso:: :py:func:`child()`
161+
162+
.. seealso:: :py:func:`appendChild()`
163+
164+
.. seealso:: :py:func:`insertChild()`
165+
166+
.. seealso:: :py:func:`removeChild()`
167+
%End
168+
169+
QgsAbstractReportSection *child( int index );
170+
%Docstring
171+
Returns the child section at the specified ``index``.
172+
173+
.. seealso:: :py:func:`children()`
174+
%End
175+
176+
void appendChild( QgsAbstractReportSection *section /Transfer/ );
177+
%Docstring
178+
Adds a child ``section``, transferring ownership of the section to this section.
179+
180+
.. seealso:: :py:func:`children()`
181+
182+
.. seealso:: :py:func:`insertChild()`
183+
%End
184+
185+
void insertChild( int index, QgsAbstractReportSection *section /Transfer/ );
186+
%Docstring
187+
Inserts a child ``section`` at the specified ``index``, transferring ownership of the section to this section.
188+
189+
.. seealso:: :py:func:`children()`
190+
191+
.. seealso:: :py:func:`appendChild()`
192+
%End
193+
194+
void removeChild( QgsAbstractReportSection *section );
195+
%Docstring
196+
Removes a child ``section``, deleting it.
197+
198+
.. seealso:: :py:func:`children()`
199+
%End
200+
201+
void removeChildAt( int index );
202+
%Docstring
203+
Removes the child section at the specified ``index``, deleting it.
204+
205+
.. seealso:: :py:func:`children()`
206+
%End
207+
208+
protected:
209+
210+
enum SubSection
211+
{
212+
Header,
213+
Body,
214+
Footer,
215+
End,
216+
};
217+
218+
void copyCommonProperties( QgsAbstractReportSection *destination ) const;
219+
%Docstring
220+
Copies the common properties of a report section to a ``destination`` section.
221+
This method should be called from clone() implementations.
222+
%End
223+
224+
private:
225+
QgsAbstractReportSection( const QgsAbstractReportSection &other );
226+
};
227+
228+
229+
class QgsReport : QgsAbstractReportSection
230+
{
231+
%Docstring
232+
Represents a report for use with the QgsLayout engine.
233+
234+
Reports consist of multiple sections, represented by QgsAbstractReportSection
235+
subclasses.
236+
237+
.. versionadded:: 3.0
238+
%End
239+
240+
%TypeHeaderCode
241+
#include "qgsabstractreportsection.h"
242+
%End
243+
public:
244+
245+
QgsReport();
246+
%Docstring
247+
Constructor for QgsReport.
248+
%End
249+
250+
virtual QgsReport *clone() const;
251+
252+
253+
virtual int count();
254+
virtual bool beginRender();
255+
256+
virtual bool next();
257+
258+
259+
virtual QString filePath( const QString &baseFilePath, const QString &extension );
260+
261+
262+
};
263+
264+
/************************************************************************
265+
* This file has been generated automatically from *
266+
* *
267+
* src/core/layout/qgsabstractreportsection.h *
268+
* *
269+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
270+
************************************************************************/

‎src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ SET(QGIS_CORE_SRCS
364364
dxf/qgsdxfpaintengine.cpp
365365
dxf/qgsdxfpallabeling.cpp
366366

367+
layout/qgsabstractreportsection.cpp
367368
layout/qgslayout.cpp
368369
layout/qgslayoutaligner.cpp
369370
layout/qgslayoutatlas.cpp
@@ -1029,6 +1030,7 @@ SET(QGIS_CORE_HDRS
10291030
composer/qgspaperitem.h
10301031

10311032
layout/qgsabstractlayoutiterator.h
1033+
layout/qgsabstractreportsection.h
10321034
layout/qgslayoutaligner.h
10331035
layout/qgslayoutexporter.h
10341036
layout/qgslayoutgridsettings.h
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/***************************************************************************
2+
qgsabstractreportsection.cpp
3+
--------------------
4+
begin : December 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
/***************************************************************************
9+
* *
10+
* This program is free software; you can redistribute it and/or modify *
11+
* it under the terms of the GNU General Public License as published by *
12+
* the Free Software Foundation; either version 2 of the License, or *
13+
* (at your option) any later version. *
14+
* *
15+
***************************************************************************/
16+
17+
#include "qgsabstractreportsection.h"
18+
#include "qgslayout.h"
19+
20+
QgsAbstractReportSection::~QgsAbstractReportSection()
21+
{
22+
qDeleteAll( mChildren );
23+
}
24+
25+
QgsLayout *QgsAbstractReportSection::layout()
26+
{
27+
return mCurrentLayout;
28+
}
29+
30+
bool QgsAbstractReportSection::beginRender()
31+
{
32+
// reset this section
33+
mCurrentLayout = nullptr;
34+
mNextChild = 0;
35+
mNextSection = Header;
36+
37+
// and all children too
38+
bool result = true;
39+
for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) )
40+
{
41+
result = result && child->beginRender();
42+
}
43+
return result;
44+
}
45+
46+
bool QgsAbstractReportSection::next()
47+
{
48+
switch ( mNextSection )
49+
{
50+
case Header:
51+
{
52+
// regardless of whether we have a header or not, the next section will be the body
53+
mNextSection = Body;
54+
55+
// if we have a header, then the current section will be the header
56+
if ( mHeaderEnabled && mHeader )
57+
{
58+
mCurrentLayout = mHeader.get();
59+
return true;
60+
}
61+
62+
// but if not, then the current section is the body
63+
FALLTHROUGH;
64+
}
65+
66+
case Body:
67+
{
68+
69+
// we iterate through all the section's children...
70+
while ( mNextChild < mChildren.count() )
71+
{
72+
// ... staying on the current child only while it still has content for us
73+
if ( mChildren.at( mNextChild )->next() )
74+
{
75+
mCurrentLayout = mChildren.at( mNextChild )->layout();
76+
return true;
77+
}
78+
else
79+
{
80+
// no more content for this child, so move to next child
81+
mNextChild++;
82+
}
83+
}
84+
85+
// all children have spent their content, so move to the footer
86+
mNextSection = Footer;
87+
FALLTHROUGH;
88+
}
89+
90+
case Footer:
91+
{
92+
// regardless of whether we have a footer or not, this is the last section
93+
mNextSection = End;
94+
95+
// if we have a footer, then the current section will be the footer
96+
if ( mFooterEnabled && mFooter )
97+
{
98+
mCurrentLayout = mFooter.get();
99+
return true;
100+
}
101+
102+
// if not, then we're all done
103+
FALLTHROUGH;
104+
}
105+
106+
case End:
107+
break;
108+
}
109+
110+
mCurrentLayout = nullptr;
111+
return false;
112+
}
113+
114+
bool QgsAbstractReportSection::endRender()
115+
{
116+
// reset this section
117+
mCurrentLayout = nullptr;
118+
mNextChild = 0;
119+
mNextSection = Header;
120+
121+
// and all children too
122+
bool result = true;
123+
for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) )
124+
{
125+
result = result && child->endRender();
126+
}
127+
return result;
128+
}
129+
130+
QgsAbstractReportSection *QgsAbstractReportSection::child( int index )
131+
{
132+
return mChildren.value( index );
133+
}
134+
135+
void QgsAbstractReportSection::appendChild( QgsAbstractReportSection *section )
136+
{
137+
mChildren.append( section );
138+
}
139+
140+
void QgsAbstractReportSection::insertChild( int index, QgsAbstractReportSection *section )
141+
{
142+
index = std::max( 0, index );
143+
index = std::min( index, mChildren.count() );
144+
mChildren.insert( index, section );
145+
}
146+
147+
void QgsAbstractReportSection::removeChild( QgsAbstractReportSection *section )
148+
{
149+
mChildren.removeAll( section );
150+
delete section;
151+
}
152+
153+
void QgsAbstractReportSection::removeChildAt( int index )
154+
{
155+
if ( index < 0 || index >= mChildren.count() )
156+
return;
157+
158+
QgsAbstractReportSection *section = mChildren.at( index );
159+
removeChild( section );
160+
}
161+
162+
void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *destination ) const
163+
{
164+
destination->mHeaderEnabled = mHeaderEnabled;
165+
if ( mHeader )
166+
destination->mHeader.reset( mHeader->clone() );
167+
else
168+
destination->mHeader.reset();
169+
170+
destination->mFooterEnabled = mFooterEnabled;
171+
if ( mFooter )
172+
destination->mFooter.reset( mFooter->clone() );
173+
else
174+
destination->mFooter.reset();
175+
176+
qDeleteAll( destination->mChildren );
177+
destination->mChildren.clear();
178+
179+
for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) )
180+
{
181+
destination->mChildren.append( child->clone() );
182+
}
183+
}
184+
185+
186+
// QgsReport
187+
188+
QgsReport *QgsReport::clone() const
189+
{
190+
std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >();
191+
copyCommonProperties( copy.get() );
192+
return copy.release();
193+
}
194+
195+
bool QgsReport::beginRender()
196+
{
197+
mSectionNumber = 0;
198+
return QgsAbstractReportSection::beginRender();
199+
}
200+
201+
bool QgsReport::next()
202+
{
203+
mSectionNumber++;
204+
return QgsAbstractReportSection::next();
205+
}
206+
207+
QString QgsReport::filePath( const QString &baseFilePath, const QString &extension )
208+
{
209+
QString base = QDir( baseFilePath ).filePath( "report_" ) + QString::number( mSectionNumber );
210+
if ( !extension.startsWith( '.' ) )
211+
base += '.';
212+
base += extension;
213+
return base;
214+
215+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/***************************************************************************
2+
qgsabstractreportsection.h
3+
---------------------------
4+
begin : December 2017
5+
copyright : (C) 2017 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
/***************************************************************************
9+
* *
10+
* This program is free software; you can redistribute it and/or modify *
11+
* it under the terms of the GNU General Public License as published by *
12+
* the Free Software Foundation; either version 2 of the License, or *
13+
* (at your option) any later version. *
14+
* *
15+
***************************************************************************/
16+
#ifndef QGSABSTRACTREPORTSECTION_H
17+
#define QGSABSTRACTREPORTSECTION_H
18+
19+
#include "qgis_core.h"
20+
#include "qgsabstractlayoutiterator.h"
21+
#include "qgslayoutreportcontext.h"
22+
23+
/**
24+
* \ingroup core
25+
* \class QgsAbstractReportSection
26+
* \brief An abstract base class for QgsReport subsections.
27+
* \since QGIS 3.0
28+
*/
29+
class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator
30+
{
31+
32+
public:
33+
34+
//! Constructor for QgsAbstractReportSection
35+
QgsAbstractReportSection() = default;
36+
37+
~QgsAbstractReportSection() override;
38+
39+
//! QgsAbstractReportSection cannot be copied
40+
QgsAbstractReportSection( const QgsAbstractReportSection &other ) = delete;
41+
42+
//! QgsAbstractReportSection cannot be copied
43+
QgsAbstractReportSection &operator=( const QgsAbstractReportSection &other ) = delete;
44+
45+
/**
46+
* Clones the report section. Ownership of the returned section is
47+
* transferred to the caller.
48+
*
49+
* Subclasses should call copyCommonProperties() in their clone()
50+
* implementations.
51+
*/
52+
virtual QgsAbstractReportSection *clone() const = 0 SIP_FACTORY;
53+
54+
#if 0 //TODO
55+
virtual void setContext( const QgsLayoutReportContext &context ) = 0;
56+
#endif
57+
58+
QgsLayout *layout() override;
59+
bool beginRender() override;
60+
bool next() override;
61+
bool endRender() override;
62+
63+
/**
64+
* Returns true if the header for the section is enabled.
65+
* \see setHeaderEnabled()
66+
* \see header()
67+
* \see setHeader()
68+
*/
69+
bool headerEnabled() const { return mHeaderEnabled; }
70+
71+
/**
72+
* Sets whether the header for the section is \a enabled.
73+
* \see headerEnabled()
74+
* \see header()
75+
* \see setHeader()
76+
*/
77+
void setHeaderEnabled( bool enabled ) { mHeaderEnabled = enabled; }
78+
79+
/**
80+
* Returns the header for the section. Note that the header is only
81+
* included if headerEnabled() is true.
82+
* \see setHeaderEnabled()
83+
* \see headerEnabled()
84+
* \see setHeader()
85+
*/
86+
QgsLayout *header() { return mHeader.get(); }
87+
88+
/**
89+
* Sets the \a header for the section. Note that the header is only
90+
* included if headerEnabled() is true. Ownership of \a header
91+
* is transferred to the report section.
92+
* \see setHeaderEnabled()
93+
* \see headerEnabled()
94+
* \see header()
95+
*/
96+
void setHeader( QgsLayout *header SIP_TRANSFER ) { mHeader.reset( header ); }
97+
98+
/**
99+
* Returns true if the footer for the section is enabled.
100+
* \see setFooterEnabled()
101+
* \see footer()
102+
* \see setFooter()
103+
*/
104+
bool footerEnabled() const { return mFooterEnabled; }
105+
106+
/**
107+
* Sets whether the footer for the section is \a enabled.
108+
* \see footerEnabled()
109+
* \see footer()
110+
* \see setFooter()
111+
*/
112+
void setFooterEnabled( bool enabled ) { mFooterEnabled = enabled; }
113+
114+
/**
115+
* Returns the footer for the section. Note that the footer is only
116+
* included if footerEnabled() is true.
117+
* \see setFooterEnabled()
118+
* \see footerEnabled()
119+
* \see setFooter()
120+
*/
121+
QgsLayout *footer() { return mFooter.get(); }
122+
123+
/**
124+
* Sets the \a footer for the section. Note that the footer is only
125+
* included if footerEnabled() is true. Ownership of \a footer
126+
* is transferred to the report section.
127+
* \see setFooterEnabled()
128+
* \see footerEnabled()
129+
* \see footer()
130+
*/
131+
void setFooter( QgsLayout *footer SIP_TRANSFER ) { mFooter.reset( footer ); }
132+
133+
/**
134+
* Return the number of child sections for this report section. The child
135+
* sections form the body of the report section.
136+
* \see children()
137+
*/
138+
int childCount() const { return mChildren.count(); }
139+
140+
/**
141+
* Return all child sections for this report section. The child
142+
* sections form the body of the report section.
143+
* \see childCount()
144+
* \see child()
145+
* \see appendChild()
146+
* \see insertChild()
147+
* \see removeChild()
148+
*/
149+
QList< QgsAbstractReportSection * > children() { return mChildren; }
150+
151+
/**
152+
* Returns the child section at the specified \a index.
153+
* \see children()
154+
*/
155+
QgsAbstractReportSection *child( int index );
156+
157+
/**
158+
* Adds a child \a section, transferring ownership of the section to this section.
159+
* \see children()
160+
* \see insertChild()
161+
*/
162+
void appendChild( QgsAbstractReportSection *section SIP_TRANSFER );
163+
164+
/**
165+
* Inserts a child \a section at the specified \a index, transferring ownership of the section to this section.
166+
* \see children()
167+
* \see appendChild()
168+
*/
169+
void insertChild( int index, QgsAbstractReportSection *section SIP_TRANSFER );
170+
171+
/**
172+
* Removes a child \a section, deleting it.
173+
* \see children()
174+
*/
175+
void removeChild( QgsAbstractReportSection *section );
176+
177+
/**
178+
* Removes the child section at the specified \a index, deleting it.
179+
* \see children()
180+
*/
181+
void removeChildAt( int index );
182+
183+
protected:
184+
185+
//! Report sub-sections
186+
enum SubSection
187+
{
188+
Header, //!< Header for section
189+
Body, //!< Body of section
190+
Footer, //!< Footer for section
191+
End, //!< End of section (i.e. past all available content)
192+
};
193+
194+
/**
195+
* Copies the common properties of a report section to a \a destination section.
196+
* This method should be called from clone() implementations.
197+
*/
198+
void copyCommonProperties( QgsAbstractReportSection *destination ) const;
199+
200+
private:
201+
202+
SubSection mNextSection = Header;
203+
int mNextChild = 0;
204+
QgsLayout *mCurrentLayout = nullptr;
205+
206+
bool mHeaderEnabled = false;
207+
bool mFooterEnabled = false;
208+
std::unique_ptr< QgsLayout > mHeader;
209+
std::unique_ptr< QgsLayout > mFooter;
210+
211+
QList< QgsAbstractReportSection * > mChildren;
212+
213+
#ifdef SIP_RUN
214+
QgsAbstractReportSection( const QgsAbstractReportSection &other );
215+
#endif
216+
};
217+
218+
219+
/**
220+
* \ingroup core
221+
* \class QgsReport
222+
* \brief Represents a report for use with the QgsLayout engine.
223+
*
224+
* Reports consist of multiple sections, represented by QgsAbstractReportSection
225+
* subclasses.
226+
*
227+
* \since QGIS 3.0
228+
*/
229+
class CORE_EXPORT QgsReport : public QgsAbstractReportSection
230+
{
231+
232+
public:
233+
234+
//! Constructor for QgsReport.
235+
QgsReport() = default;
236+
237+
QgsReport *clone() const override;
238+
239+
// TODO - how to handle this?
240+
int count() override { return -1; }
241+
bool beginRender() override;
242+
bool next() override;
243+
244+
//TODO - baseFilePath should be a filename, not directory
245+
QString filePath( const QString &baseFilePath, const QString &extension ) override;
246+
247+
private:
248+
249+
int mSectionNumber = 0;
250+
251+
};
252+
253+
#endif //QGSABSTRACTREPORTSECTION_H

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ ADD_PYTHON_TEST(PyQgsRelation test_qgsrelation.py)
154154
ADD_PYTHON_TEST(PyQgsRelationManager test_qgsrelationmanager.py)
155155
ADD_PYTHON_TEST(PyQgsRenderContext test_qgsrendercontext.py)
156156
ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py)
157+
ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py)
157158
ADD_PYTHON_TEST(PyQgsRulebasedRenderer test_qgsrulebasedrenderer.py)
158159
ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py)
159160
ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py)

‎tests/src/python/test_qgsreport.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsReport
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '29/12/2017'
11+
__copyright__ = 'Copyright 2017, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # NOQA
16+
17+
from qgis.core import (QgsProject,
18+
QgsLayout,
19+
QgsReport)
20+
from qgis.testing import start_app, unittest
21+
22+
start_app()
23+
24+
25+
class TestQgsReport(unittest.TestCase):
26+
27+
def testGettersSetters(self):
28+
p = QgsProject()
29+
r = QgsReport()
30+
31+
r.setHeaderEnabled(True)
32+
self.assertTrue(r.headerEnabled())
33+
34+
header = QgsLayout(p)
35+
r.setHeader(header)
36+
self.assertEqual(r.header(), header)
37+
38+
r.setFooterEnabled(True)
39+
self.assertTrue(r.footerEnabled())
40+
41+
footer = QgsLayout(p)
42+
r.setFooter(footer)
43+
self.assertEqual(r.footer(), footer)
44+
45+
def testChildren(self):
46+
p = QgsProject()
47+
r = QgsReport()
48+
self.assertEqual(r.childCount(), 0)
49+
self.assertEqual(r.children(), [])
50+
self.assertIsNone(r.child(-1))
51+
self.assertIsNone(r.child(1))
52+
self.assertIsNone(r.child(0))
53+
54+
# try deleting non-existant children
55+
r.removeChildAt(-1)
56+
r.removeChildAt(0)
57+
r.removeChildAt(100)
58+
r.removeChild(None)
59+
60+
# append child
61+
child1 = QgsReport()
62+
r.appendChild(child1)
63+
self.assertEqual(r.childCount(), 1)
64+
self.assertEqual(r.children(), [child1])
65+
self.assertEqual(r.child(0), child1)
66+
child2 = QgsReport()
67+
r.appendChild(child2)
68+
self.assertEqual(r.childCount(), 2)
69+
self.assertEqual(r.children(), [child1, child2])
70+
self.assertEqual(r.child(1), child2)
71+
72+
def testInsertChild(self):
73+
p = QgsProject()
74+
r = QgsReport()
75+
76+
child1 = QgsReport()
77+
r.insertChild(11, child1)
78+
self.assertEqual(r.childCount(), 1)
79+
self.assertEqual(r.children(), [child1])
80+
child2 = QgsReport()
81+
r.insertChild(-1, child2)
82+
self.assertEqual(r.childCount(), 2)
83+
self.assertEqual(r.children(), [child2, child1])
84+
85+
def testRemoveChild(self):
86+
p = QgsProject()
87+
r = QgsReport()
88+
89+
child1 = QgsReport()
90+
r.appendChild(child1)
91+
child2 = QgsReport()
92+
r.appendChild(child2)
93+
94+
r.removeChildAt(-1)
95+
r.removeChildAt(100)
96+
r.removeChild(None)
97+
self.assertEqual(r.childCount(), 2)
98+
self.assertEqual(r.children(), [child1, child2])
99+
100+
r.removeChildAt(1)
101+
self.assertEqual(r.childCount(), 1)
102+
self.assertEqual(r.children(), [child1])
103+
104+
r.removeChild(child1)
105+
self.assertEqual(r.childCount(), 0)
106+
self.assertEqual(r.children(), [])
107+
108+
def testClone(self):
109+
p = QgsProject()
110+
r = QgsReport()
111+
112+
child1 = QgsReport()
113+
child1.setHeaderEnabled(True)
114+
r.appendChild(child1)
115+
child2 = QgsReport()
116+
child2.setFooterEnabled(True)
117+
r.appendChild(child2)
118+
119+
cloned = r.clone()
120+
self.assertEqual(cloned.childCount(), 2)
121+
self.assertTrue(cloned.child(0).headerEnabled())
122+
self.assertFalse(cloned.child(0).footerEnabled())
123+
self.assertFalse(cloned.child(1).headerEnabled())
124+
self.assertTrue(cloned.child(1).footerEnabled())
125+
126+
127+
if __name__ == '__main__':
128+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.