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
21 changes: 18 additions & 3 deletions .github/workflows/auto_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ name: Auto Tests
on: push

jobs:
server_tests:
tests:
runs-on: ubuntu-24.04

strategy:
fail-fast: false
matrix:
include:
- suite: server
pytest_args: "-v --cov=mergin --cov-report=lcov mergin/tests"
- suite: migration
pytest_args: "-v mergin/test_migrations"

services:
postgres:
image: postgres:14
Expand All @@ -15,13 +24,18 @@ jobs:
POSTGRES_USER: postgres
ports:
- 5435:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DB_USER: postgres
DB_PASSWORD: postgres
DB_HOST: localhost
DB_PORT: 5435

steps:
- name: Check out repository
uses: actions/checkout@v3
Expand All @@ -36,9 +50,10 @@ jobs:
- name: Run tests
run: |
cd server
pipenv run pytest -v --cov=mergin --cov-report=lcov mergin/tests
pipenv run pytest ${{ matrix.pytest_args }}

- name: Coveralls
if: matrix.suite == 'server'
uses: coverallsapp/github-action@v2
with:
base-path: server
Expand Down
15 changes: 9 additions & 6 deletions server/mergin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,13 +389,16 @@ def handle_exception(e):
def log_bad_request(response):
"""Log bad requests for easier debugging"""
if response.status_code == 400:
json_body = response.get_json(silent=True)
if json_body and json_body.get("detail"):
# default response from connexion (check against swagger.yaml)
logging.warning(f'HTTP 400: {json_body["detail"]}')
if "xml" in response.content_type:
pass # QGIS proxy errors are already logged at the source
else:
# either WTF form validation error or custom validation with abort(400)
logging.warning(f"HTTP 400: {response.data}")
json_body = response.get_json(silent=True)
if json_body and json_body.get("detail"):
# default response from connexion (check against swagger.yaml)
logging.warning(f'HTTP 400: {json_body["detail"]}')
else:
# either WTF form validation error or custom validation with abort(400)
logging.warning(f"HTTP 400: {response.data}")
elif response.status_code == 409:
# request which would result in conflict, e.g. creating the same project again
logging.warning(f"HTTP 409: {response.data}")
Expand Down
6 changes: 6 additions & 0 deletions server/mergin/sync/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ def get_by_name(self, name):
"""
pass

def get_by_names(self, names):
"""
Return list of workspaces whose names are in the given collection.
"""
pass

@abstractmethod
def get_by_project(self, project):
"""
Expand Down
48 changes: 45 additions & 3 deletions server/mergin/sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import os
import re
import threading
import time
import uuid
Expand All @@ -18,7 +19,9 @@
from blinker import signal
from flask_login import current_user
from pygeodiff import GeoDiff
from functools import cached_property
from sqlalchemy import text, null, desc, nullslast, tuple_
from sqlalchemy.orm import contains_eager, joinedload, load_only
from sqlalchemy.dialects.postgresql import ARRAY, BIGINT, UUID, JSONB, ENUM, insert
from sqlalchemy.types import String
from sqlalchemy.ext.hybrid import hybrid_property
Expand Down Expand Up @@ -136,6 +139,12 @@ def workspace(self):
project_workspace = current_app.ws_handler.get(self.workspace_id)
return project_workspace

@cached_property
def _has_conflict(self) -> bool:
"""True if any current project file matches a known conflict-copy pattern."""
pattern = r"(\.gpkg|\.qgs|.qgz)(.*conflict.*)|( \(.*conflict.*)"
return any(re.search(pattern, f.path) for f in self.files)

def get_latest_files_cache(self) -> List[int]:
"""Get latest file history ids either from cached table or calculate them on the fly"""
if self.latest_project_files.file_history_ids is not None:
Expand Down Expand Up @@ -658,7 +667,7 @@ def __init__(
def path(self) -> str:
return self.file.path

@property
@cached_property
def diff(self) -> Optional[FileDiff]:
"""Diff file pushed with UPDATE_DIFF change type.

Expand Down Expand Up @@ -713,9 +722,37 @@ def changes(
if not (is_versioned_file(file) and since is not None and to is not None):
return []

history = []
# when since=1 the range spans the entire project history; narrow it to
# the most recent CREATE/DELETE so we don't load records from previous
# file lifecycles that the Python break would discard anyway
if since == 1:
boundary = (
FileHistory.query.join(ProjectFilePath)
.filter(
ProjectFilePath.project_id == project_id,
ProjectFilePath.path == file,
FileHistory.project_version_name <= to,
FileHistory.change.in_(
[PushChangeType.CREATE.value, PushChangeType.DELETE.value]
),
)
.order_by(desc(FileHistory.project_version_name))
.with_entities(FileHistory.project_version_name)
.first()
)
since = boundary[0] if boundary else since

full_history = (
FileHistory.query.join(ProjectFilePath)
.join(FileHistory.version)
.join(ProjectVersion.project)
.options(
contains_eager(FileHistory.file).load_only(ProjectFilePath.path),
contains_eager(FileHistory.version)
.load_only(ProjectVersion.name, ProjectVersion.project_id)
.contains_eager(ProjectVersion.project)
.load_only(Project.storage_params),
)
.filter(
ProjectFilePath.project_id == project_id,
FileHistory.project_version_name <= to,
Expand All @@ -726,6 +763,7 @@ def changes(
.all()
)

history = []
for item in full_history:
history.append(item)

Expand Down Expand Up @@ -1781,11 +1819,15 @@ def diff_summary(self):

def changes_count(self) -> Dict:
"""Return number of changes by type"""
query = f"SELECT change, COUNT(change) FROM file_history WHERE version_id = :version_id GROUP BY change;"
query = "SELECT change, COUNT(change) FROM file_history WHERE version_id = :version_id GROUP BY change;"
params = {"version_id": self.id}
result = db.session.execute(text(query), params).fetchall()
return {row[0]: row[1] for row in result}

@cached_property
def _changes_count(self) -> Dict:
return self.changes_count()

@property
def zip_path(self):
return os.path.join(
Expand Down
5 changes: 0 additions & 5 deletions server/mergin/sync/public_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1124,11 +1124,6 @@ components:
- added
- updated
- removed
expiration:
nullable: true
type: string
format: date-time
example: 2019-02-26T08:47:58.636074Z
UploadFileInfo:
allOf:
- $ref: "#/components/schemas/FileInfo"
Expand Down
Loading
Loading