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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ jobs:
steps:

- name: Install Python
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.12'

- name: Check out source repository
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Install packages
run: |
pip install -r requirements/testing.txt
pip install pylint pycodestyle

- name: flake8 Lint
uses: py-actions/flake8@v1
uses: py-actions/flake8@v2

- name: Pycodestyle
run: make pycodestyle
20 changes: 9 additions & 11 deletions .github/workflows/test_plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
env:
# plugin name/directory where the code for the plugin is stored
PLUGIN_NAME: felt
# python notation to test running inside plugin
TESTS_RUN_FUNCTION: felt.test_suite.test_package
# Docker settings
DOCKER_IMAGE: qgis/qgis

Expand All @@ -24,26 +22,26 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
docker_tags: [release-3_22, release-3_28, release-3_34, release-3_36]
# oldest supported version, recent LTRs, and latest QGIS 4 (Qt6)
docker_tags: [release-3_22, release-3_34, "3.40", "3.44", "4.0"]

steps:

- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Docker pull and create qgis-testing-environment
run: |
docker pull "$DOCKER_IMAGE":${{ matrix.docker_tags }}
docker run -d --name qgis-testing-environment -v "$GITHUB_WORKSPACE":/tests_directory -e DISPLAY=:99 "$DOCKER_IMAGE":${{ matrix.docker_tags }}
# -t keeps the newer images' shell entrypoint alive
docker run -d -t --name qgis-testing-environment -v "$GITHUB_WORKSPACE":/tests_directory -e QT_QPA_PLATFORM=offscreen "$DOCKER_IMAGE":${{ matrix.docker_tags }}

- name: Docker set up QGIS
- name: Docker install test requirements
run: |
docker exec qgis-testing-environment sh -c "qgis_setup.sh $PLUGIN_NAME"
docker exec qgis-testing-environment sh -c "rm -f /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME"
docker exec qgis-testing-environment sh -c "ln -s /tests_directory/$PLUGIN_NAME /root/.local/share/QGIS/QGIS3/profiles/default/python/plugins/$PLUGIN_NAME"
docker exec qgis-testing-environment sh -c "pip3 install -r /tests_directory/requirements/testing.txt"
docker exec qgis-testing-environment sh -c "pip3 install --break-system-packages -r /tests_directory/requirements/testing.txt || pip3 install -r /tests_directory/requirements/testing.txt"

- name: Docker run plugin tests
run: |
docker exec qgis-testing-environment sh -c "qgis_testrunner.sh $TESTS_RUN_FUNCTION"
docker exec qgis-testing-environment sh -c "cd /tests_directory && python3 -c \"from felt.test_suite import run_tests_and_exit; run_tests_and_exit()\""
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

## [1.1.0] - 2026-06-12

- Add support for QGIS 4.x (Qt6-based) releases, while remaining
compatible with QGIS 3.22 and later

## [1.0.0] - 2023-06-21

- Initial release
Expand Down
52 changes: 27 additions & 25 deletions felt/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,35 +342,37 @@ def create_upload_file_request(self,
b"Content-Type",
b"multipart/form-data; boundary=QGISFormBoundary2XCkqVRLJ5XMxfw5")

form_content = QByteArray()
# build the form content as bytes: PyQt6 does not permit appending
# strings to QByteArray
form_content = b''
for name, value in parameters.to_form_fields().items():
form_content.append("--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n")
form_content.append("Content-Disposition: form-data; ")
form_content.append(f"name=\"{name}\"")
form_content.append("\r\n")
form_content.append("\r\n")
form_content.append(value)
form_content.append("\r\n")

form_content.append("--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n")
form_content.append("Content-Disposition: ")
form_content.append(
f"form-data; name=\"file\"; filename=\"{filename}\"\r\n")
form_content.append(
"Content-Type: application/octet-stream\r\n")
form_content.append("\r\n")

form_content.append(content)

form_content.append("\r\n")
form_content.append("--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n")

content_length = form_content.length()
form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n"
form_content += b"Content-Disposition: form-data; "
form_content += f"name=\"{name}\"".encode()
form_content += b"\r\n"
form_content += b"\r\n"
form_content += str(value).encode()
form_content += b"\r\n"

form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n"
form_content += b"Content-Disposition: "
form_content += \
f"form-data; name=\"file\"; filename=\"{filename}\"\r\n".encode()
form_content += b"Content-Type: application/octet-stream\r\n"
form_content += b"\r\n"

form_content += content

form_content += b"\r\n"
form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n"

form_data = QByteArray(form_content)
content_length = form_data.length()
network_request.setRawHeader(
b"Content-Length",
str(content_length).encode()
)
return network_request, form_content
return network_request, form_data

def upload_file(self,
filename: str,
Expand Down Expand Up @@ -517,7 +519,7 @@ def create_layer_groups(self,
json.dumps(group_post_data).encode()
)

if reply.error() == QNetworkReply.ContentAccessDenied:
if reply.error() == QNetworkReply.NetworkError.ContentAccessDenied:
raise PaidPlanRequiredError("Upload requires a paid plan")

return [
Expand Down
5 changes: 3 additions & 2 deletions felt/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ def do_GET(self):
token_body = urllib.parse.urlencode(body).encode()

network_request = QNetworkRequest(QUrl(TOKEN_URL))
network_request.setHeader(QNetworkRequest.ContentTypeHeader,
'application/x-www-form-urlencoded')
network_request.setHeader(
QNetworkRequest.KnownHeaders.ContentTypeHeader,
'application/x-www-form-urlencoded')

result_code = request.post(network_request,
data=token_body,
Expand Down
43 changes: 22 additions & 21 deletions felt/core/fsl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,9 +1004,9 @@ def convert_cap_style(style: Qt.PenCapStyle) -> str:
Convert a Qt cap style to FSL
"""
return {
Qt.RoundCap: 'round',
Qt.SquareCap: 'square',
Qt.FlatCap: 'butt',
Qt.PenCapStyle.RoundCap: 'round',
Qt.PenCapStyle.SquareCap: 'square',
Qt.PenCapStyle.FlatCap: 'butt',
}[style]

@staticmethod
Expand All @@ -1015,10 +1015,10 @@ def convert_join_style(style: Qt.PenJoinStyle) -> str:
Convert a Qt join style to FSL
"""
return {
Qt.RoundJoin: 'round',
Qt.BevelJoin: 'bevel',
Qt.MiterJoin: 'miter',
Qt.SvgMiterJoin: 'miter',
Qt.PenJoinStyle.RoundJoin: 'round',
Qt.PenJoinStyle.BevelJoin: 'bevel',
Qt.PenJoinStyle.MiterJoin: 'miter',
Qt.PenJoinStyle.SvgMiterJoin: 'miter',
}[style]

@staticmethod
Expand All @@ -1027,12 +1027,12 @@ def convert_pen_style(style: Qt.PenStyle) -> List[float]:
Converts a Qt pen style to an array of dash/space lengths
"""
return {
Qt.NoPen: [],
Qt.SolidLine: [],
Qt.DashLine: [2.5, 2],
Qt.DotLine: [0.5, 1.3],
Qt.DashDotLine: [0.5, 1.3, 2.5, 1.3],
Qt.DashDotDotLine: [0.5, 1.3, 0.5, 1.3, 2.5, 1.3]
Qt.PenStyle.NoPen: [],
Qt.PenStyle.SolidLine: [],
Qt.PenStyle.DashLine: [2.5, 2],
Qt.PenStyle.DotLine: [0.5, 1.3],
Qt.PenStyle.DashDotLine: [0.5, 1.3, 2.5, 1.3],
Qt.PenStyle.DashDotDotLine: [0.5, 1.3, 0.5, 1.3, 2.5, 1.3]
}[style]

@staticmethod
Expand All @@ -1043,7 +1043,7 @@ def simple_line_to_fsl(
"""
Converts a QGIS simple line symbol layer to FSL
"""
if (layer.penStyle() == Qt.NoPen or
if (layer.penStyle() == Qt.PenStyle.NoPen or
not layer.color().isValid() or
layer.color().alphaF() == 0):
return []
Expand All @@ -1070,7 +1070,7 @@ def simple_line_to_fsl(
part,
layer.customDashPatternUnit(), context, round_size=False) for
part in layer.customDashVector()]
elif layer.penStyle() != Qt.SolidLine:
elif layer.penStyle() != Qt.PenStyle.SolidLine:
res['dashArray'] = FslConverter.convert_pen_style(layer.penStyle())

# not supported:
Expand Down Expand Up @@ -1264,10 +1264,10 @@ def simple_fill_to_fsl(
"""
Converts a QGIS simple fill symbol layer to FSL
"""
has_invisible_fill = (layer.brushStyle() == Qt.NoBrush or
has_invisible_fill = (layer.brushStyle() == Qt.BrushStyle.NoBrush or
not layer.color().isValid() or
layer.color().alphaF() == 0)
has_invisible_stroke = (layer.strokeStyle() == Qt.NoPen or
has_invisible_stroke = (layer.strokeStyle() == Qt.PenStyle.NoPen or
not layer.strokeColor().isValid() or
layer.strokeColor().alphaF() == 0)
if has_invisible_fill and has_invisible_stroke:
Expand All @@ -1291,7 +1291,7 @@ def simple_fill_to_fsl(
res['lineJoin'] = FslConverter.convert_join_style(
layer.penJoinStyle())

if layer.strokeStyle() != Qt.SolidLine:
if layer.strokeStyle() != Qt.PenStyle.SolidLine:
res['dashArray'] = FslConverter.convert_pen_style(
layer.strokeStyle())
else:
Expand All @@ -1301,7 +1301,8 @@ def simple_fill_to_fsl(
# - fill offset
# - fill style

if layer.brushStyle() not in (Qt.SolidPattern, Qt.NoBrush):
if layer.brushStyle() not in (Qt.BrushStyle.SolidPattern,
Qt.BrushStyle.NoBrush):
context.push_warning(
'Fill patterns are not supported, converting to a solid fill',
LogLevel.Warning,
Expand All @@ -1323,7 +1324,7 @@ def simple_marker_to_fsl(
"""
has_fill = layer.color().isValid() and layer.color().alphaF() > 0
has_stroke = (layer.strokeColor().alphaF() > 0 and
layer.strokeStyle() != Qt.NoPen)
layer.strokeStyle() != Qt.PenStyle.NoPen)
if not has_fill and not has_stroke:
return []

Expand Down Expand Up @@ -1378,7 +1379,7 @@ def ellipse_marker_to_fsl(
"""
has_fill = layer.color().isValid() and layer.color().alphaF() > 0
has_stroke = (layer.strokeColor().alphaF() > 0 and
layer.strokeStyle() != Qt.NoPen)
layer.strokeStyle() != Qt.PenStyle.NoPen)
if not has_fill and not has_stroke:
return []

Expand Down
8 changes: 4 additions & 4 deletions felt/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def log_message(self, message: str):
QMetaObject.invokeMethod(
self,
"_submit_usage",
Qt.QueuedConnection,
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, message),
Q_ARG(str, UsageType.Info.to_string()))

Expand All @@ -137,7 +137,7 @@ def log_message_json(self, message: Dict):
QMetaObject.invokeMethod(
self,
"_submit_usage",
Qt.QueuedConnection,
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, message_str),
Q_ARG(str, UsageType.Info.to_string()))

Expand All @@ -152,7 +152,7 @@ def log_error(self, error: str):
QMetaObject.invokeMethod(
self,
"_submit_usage",
Qt.QueuedConnection,
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, message),
Q_ARG(str, UsageType.Error.to_string()))

Expand All @@ -167,7 +167,7 @@ def log_error_json(self, error: Dict):
QMetaObject.invokeMethod(
self,
"_submit_usage",
Qt.QueuedConnection,
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, json.dumps(error)),
Q_ARG(str, UsageType.Error.to_string()))

Expand Down
2 changes: 1 addition & 1 deletion felt/core/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def from_json(jsons: Union[str, Dict]) -> 'Map':
last_visited_string = res.get('attributes', {}).get('visited_at')
if last_visited_string:
last_visited = QDateTime.fromString(
last_visited_string, Qt.ISODate
last_visited_string, Qt.DateFormat.ISODate
)
else:
last_visited = None
Expand Down
27 changes: 17 additions & 10 deletions felt/core/map_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,9 @@ def run(self):
feedback=self.feedback
)

if reply.error() != QNetworkReply.NoError:
if reply.error() == QNetworkReply.ContentAccessDenied:
if reply.error() != QNetworkReply.NetworkError.NoError:
if (reply.error() ==
QNetworkReply.NetworkError.ContentAccessDenied):
self.paid_plan_error = True
self.error_string = reply.errorString()
Logger.instance().log_error_json(
Expand Down Expand Up @@ -647,7 +648,8 @@ def run(self):
)

if reply.attribute(
QNetworkRequest.HttpStatusCodeAttribute) == 429:
QNetworkRequest.Attribute.HttpStatusCodeAttribute
) == 429:
rate_limit_counter += 1
if rate_limit_counter > 3:
self.error_string = \
Expand All @@ -669,8 +671,9 @@ def run(self):
QThread.sleep(5)
continue

if reply.error() != QNetworkReply.NoError:
if reply.error() == QNetworkReply.ContentAccessDenied:
if reply.error() != QNetworkReply.NetworkError.NoError:
if (reply.error() ==
QNetworkReply.NetworkError.ContentAccessDenied):
self.paid_plan_error = True
self.error_string = reply.errorString()
Logger.instance().log_error_json(
Expand Down Expand Up @@ -734,7 +737,9 @@ def _upload_progress(sent, total):
form_content,
feedback=self.feedback)

if blocking_request.reply().error() != QNetworkReply.NoError:
if (blocking_request.reply().error() !=

QNetworkReply.NetworkError.NoError):
self.error_string = blocking_request.reply().errorString()
Logger.instance().log_error_json(
{
Expand Down Expand Up @@ -784,8 +789,9 @@ def _upload_progress(sent, total):
ordering_key=details.ordering_key,
)

if reply and reply.error() != QNetworkReply.NoError:
if reply.error() == QNetworkReply.ContentAccessDenied:
if reply and reply.error() != QNetworkReply.NetworkError.NoError:
if (reply.error() ==
QNetworkReply.NetworkError.ContentAccessDenied):
self.paid_plan_error = True
self.error_string = reply.errorString()
Logger.instance().log_error_json(
Expand Down Expand Up @@ -823,8 +829,9 @@ def _upload_progress(sent, total):
ordering_key=details.ordering_key,
)

if reply and reply.error() != QNetworkReply.NoError:
if reply.error() == QNetworkReply.ContentAccessDenied:
if reply and reply.error() != QNetworkReply.NetworkError.NoError:
if (reply.error() ==
QNetworkReply.NetworkError.ContentAccessDenied):
self.paid_plan_error = True
self.error_string = reply.errorString()
Logger.instance().log_error_json(
Expand Down
Loading
Loading