├── .editorconfig ├── .env.example ├── .flake8 ├── .gitattributes ├── .github ├── FUNDING.yml ├── disabled-workflows │ └── deploy_staging.yml └── workflows │ ├── backport.yml │ ├── build_and_push.yml │ ├── codeql.yml │ ├── stale.yml │ ├── sync_translations.yml │ └── test.yml ├── .gitignore ├── .mypy.ini ├── .pre-commit-config.yaml ├── .tx └── config ├── LICENSE ├── README.md ├── SECURITY.md ├── conf └── nginx │ ├── certs │ └── README.md │ └── dhparams │ └── ssl-dhparams.pem ├── docker-app ├── .coveragerc ├── Dockerfile ├── entrypoint.sh ├── manage.py ├── qfieldcloud │ ├── __init__.py │ ├── authentication │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── auth_backends.py │ │ ├── authentication.py │ │ ├── conf.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_authtoken_client_type.py │ │ │ ├── 0003_authtoken_authenticat_created_ee68f9_idx.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── sso │ │ │ ├── __init__.py │ │ │ └── provider_styles.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_authentication.py │ │ │ └── test_list_auth_providers.py │ │ ├── utils.py │ │ └── views.py │ ├── core │ │ ├── __init__.py │ │ ├── adapters.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── converters.py │ │ ├── cron.py │ │ ├── deltafile_01.json │ │ ├── drf_utils.py │ │ ├── exceptions.py │ │ ├── fields.py │ │ ├── geodb_utils.py │ │ ├── invitations_utils.py │ │ ├── logging │ │ │ └── formatters.py │ │ ├── management │ │ │ └── commands │ │ │ │ ├── calcprojectstorage.py │ │ │ │ ├── createsuperuser.py │ │ │ │ ├── createuser.py │ │ │ │ ├── createuseraccounts.py │ │ │ │ ├── deleteorphanedfiles.py │ │ │ │ ├── dequeue.py │ │ │ │ ├── extracts3data.py │ │ │ │ ├── inviteusers.py │ │ │ │ ├── listfiles.py │ │ │ │ └── status.py │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ ├── qgis_auth.py │ │ │ ├── requests.py │ │ │ ├── test.py │ │ │ └── timezone.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_deltafile.py │ │ │ ├── 0003_auto_20201127_1256.py │ │ │ ├── 0004_auto_20201201_1209.py │ │ │ ├── 0005_auto_20201203_1037.py │ │ │ ├── 0006_auto_20201214_1642.py │ │ │ ├── 0007_project_overwrite_conflicts.py │ │ │ ├── 0008_update_site_name.py │ │ │ ├── 0009_geodb.py │ │ │ ├── 0010_auto_20210106_1543.py │ │ │ ├── 0011_export.py │ │ │ ├── 0012_auto_20210106_0930.py │ │ │ ├── 0013_remove_exportation_output.py │ │ │ ├── 0014_exportation_output.py │ │ │ ├── 0015_auto_20210123_0116.py │ │ │ ├── 0016_generate_user_account.py │ │ │ ├── 0017_auto_20210226_1939.py │ │ │ ├── 0018_auto_20210308_0034.py │ │ │ ├── 0019_auto_20210316_2056.py │ │ │ ├── 0020_auto_20210321_1749.py │ │ │ ├── 0021_auto_20210330_2209.py │ │ │ ├── 0022_auto_20210331_0143.py │ │ │ ├── 0023_auto_20210407_1247.py │ │ │ ├── 0024_auto_20210407_2002.py │ │ │ ├── 0025_auto_20210407_2306.py │ │ │ ├── 0026_auto_20210409_1339.py │ │ │ ├── 0027_auto_20210408_0138.py │ │ │ ├── 0028_auto_20210414_1545.py │ │ │ ├── 0029_auto_20210415_1420.py │ │ │ ├── 0030_refactor_roles.py │ │ │ ├── 0031_auto_20210415_1535.py │ │ │ ├── 0032_auto_20210419_1011.py │ │ │ ├── 0033_auto_20210419_1908.py │ │ │ ├── 0034_auto_20210508_1030.py │ │ │ ├── 0035_auto_20210510_0635.py │ │ │ ├── 0036_auto_20210426_1210.py │ │ │ ├── 0037_alter_project_owner_help_text.py │ │ │ ├── 0038_rename_workplace_useraccount_company.py │ │ │ ├── 0039_auto_20210630_1210.py │ │ │ ├── 0040_auto_20210630_1212.py │ │ │ ├── 0041_auto_20210705_0837.py │ │ │ ├── 0042_auto_20211001_2217.py │ │ │ ├── 0043_update_site_name.py │ │ │ ├── 0044_alter_user_username.py │ │ │ ├── 0045_auto_20211012_2234.py │ │ │ ├── 0046_auto_20211013_2304.py │ │ │ ├── 0047_useraccount_timezone.py │ │ │ ├── 0048_useraccount_notifs_frequency.py │ │ │ ├── 0049_auto_20211117_1843.py │ │ │ ├── 0050_auto_20211118_1150.py │ │ │ ├── 0051_auto_20211125_0444.py │ │ │ ├── 0052_secret.py │ │ │ ├── 0053_auto_20220505_1948.py │ │ │ ├── 0054_project_last_package.py │ │ │ ├── 0055_auto_20220204_1654.py │ │ │ ├── 0056_projectpermissionsview.py │ │ │ ├── 0057_auto_20220701_2140.py │ │ │ ├── 0058_auto_20220914_2049.py │ │ │ ├── 0059_auto_20221028_1806.py │ │ │ ├── 0060_alter_project_storage_size_mb.py │ │ │ ├── 0061_add_incognito_and_audit_to_collaborators.py │ │ │ ├── 0062_auto_20230216_1137.py │ │ │ ├── 0063_auto_20230305_1551.py │ │ │ ├── 0064_auto_20230328_2017.py │ │ │ ├── 0065_auto_20230422_1101.py │ │ │ ├── 0066_delta_client_id.py │ │ │ ├── 0067_auto_20230515_1320.py │ │ │ ├── 0068_job_container_id.py │ │ │ ├── 0069_auto_20230616_0827.py │ │ │ ├── 0070_alter_project_is_public.py │ │ │ ├── 0071_auto_20230717_2243.py │ │ │ ├── 0072_person_remaining_trial_organizations.py │ │ │ ├── 0073_project_packaging_offliner.py │ │ │ ├── 0074_auto_20240314_1805.py │ │ │ ├── 0075_auto_20240323_1419.py │ │ │ ├── 0076_project_restrict_project_modification.py │ │ │ ├── 0077_alter_user_username.py │ │ │ ├── 0078_alter_project_packaging_offliner.py │ │ │ ├── 0079_organizationmember_created_at_and_more.py │ │ │ ├── 0080_rename_project_filename_project_the_qgis_file_name.py │ │ │ ├── 0081_file_storage_project_and_more.py │ │ │ ├── 0082_project_attachments_file_storage.py │ │ │ ├── 0083_project_is_sticky.py │ │ │ ├── 0084_project_is_attachment_download_on_demand.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── pagination.py │ │ ├── paginators.py │ │ ├── permission_check.py │ │ ├── permissions_utils.py │ │ ├── querysets_utils.py │ │ ├── rest_utils.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── sql_config.py │ │ ├── staticfiles │ │ │ ├── css │ │ │ │ ├── admin.css │ │ │ │ └── sso.css │ │ │ ├── favicon.ico │ │ │ ├── img │ │ │ │ └── opengis_powering_qfc.png │ │ │ ├── logo.svg │ │ │ ├── logo_notext.svg │ │ │ ├── logo_sidetext.svg │ │ │ ├── logo_sidetext_white.svg │ │ │ ├── logo_undertext.svg │ │ │ ├── sad_nyuki.svg │ │ │ └── sso │ │ │ │ ├── github-dark.svg │ │ │ │ ├── github-light.svg │ │ │ │ ├── google.svg │ │ │ │ └── keycloak.svg │ │ ├── templates │ │ │ ├── account │ │ │ │ ├── password_reset_from_key.html │ │ │ │ └── password_reset_from_key_done.html │ │ │ ├── admin │ │ │ │ ├── account │ │ │ │ │ └── emailaddress │ │ │ │ │ │ └── change_list.html │ │ │ │ ├── base_site.html │ │ │ │ ├── constance │ │ │ │ │ └── change_list.html │ │ │ │ ├── delta_change_form.html │ │ │ │ ├── edit_inline │ │ │ │ │ ├── tabular_customized.html │ │ │ │ │ └── tabular_extended.html │ │ │ │ ├── job_change_form.html │ │ │ │ ├── login.html │ │ │ │ ├── password_reset_url.html │ │ │ │ ├── person_change_form.html │ │ │ │ ├── project_change_form.html │ │ │ │ └── project_files_widget.html │ │ │ ├── allauth │ │ │ │ └── elements │ │ │ │ │ └── provider.html │ │ │ └── socialaccount │ │ │ │ └── snippets │ │ │ │ └── provider_list.html │ │ ├── templatetags │ │ │ └── filters.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── mixins.py │ │ │ ├── test_admin.py │ │ │ ├── test_api.py │ │ │ ├── test_commands.py │ │ │ ├── test_deleteorphanedfiles.py │ │ │ ├── test_delta.py │ │ │ ├── test_file_upload_policy.py │ │ │ ├── test_geodb.py │ │ │ ├── test_jobs.py │ │ │ ├── test_localized_projects.py │ │ │ ├── test_organization.py │ │ │ ├── test_packages.py │ │ │ ├── test_permission.py │ │ │ ├── test_project.py │ │ │ ├── test_qgis_file.py │ │ │ ├── test_queryset.py │ │ │ ├── test_sentry.py │ │ │ ├── test_team.py │ │ │ ├── test_user.py │ │ │ ├── test_useraccount.py │ │ │ ├── test_username_generation.py │ │ │ ├── test_username_uniqueness.py │ │ │ ├── testdata │ │ │ │ ├── 01_deltas.yml │ │ │ │ ├── DCIM │ │ │ │ │ ├── 1.jpg │ │ │ │ │ └── 2.jpg │ │ │ │ ├── bumblebees.gpkg │ │ │ │ ├── delta │ │ │ │ │ ├── broken.qgs │ │ │ │ │ ├── deltas │ │ │ │ │ │ ├── multilayer_multidelta.json │ │ │ │ │ │ ├── multistage_p1_c1_create.json │ │ │ │ │ │ ├── multistage_p2_c2_create.json │ │ │ │ │ │ ├── multistage_p3_c1_patch.json │ │ │ │ │ │ ├── multistage_p4_c2_patch.json │ │ │ │ │ │ ├── multistage_p5_c1_delete.json │ │ │ │ │ │ ├── multistage_p6_c2_delete.json │ │ │ │ │ │ ├── nonspatial.json │ │ │ │ │ │ ├── nonspatial_geom_empty_str.json │ │ │ │ │ │ ├── not_schema_valid.json │ │ │ │ │ │ ├── singlelayer_multidelta.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xy_for_xyz_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xy_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyz_for_xyz_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyz_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyz_nan_for_xyz_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyz_nan_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyzm_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyzm_nan_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_delta_with_xyzm_nannan_for_xyzm_layer.json │ │ │ │ │ │ ├── singlelayer_multidelta_patch_create.json │ │ │ │ │ │ ├── singlelayer_singledelta.json │ │ │ │ │ │ ├── singlelayer_singledelta2.json │ │ │ │ │ │ ├── singlelayer_singledelta3.json │ │ │ │ │ │ ├── singlelayer_singledelta4.json │ │ │ │ │ │ ├── singlelayer_singledelta5.json │ │ │ │ │ │ ├── singlelayer_singledelta6.json │ │ │ │ │ │ ├── singlelayer_singledelta_conflict.json │ │ │ │ │ │ ├── singlelayer_singledelta_conflict2.json │ │ │ │ │ │ ├── singlelayer_singledelta_diff_content.json │ │ │ │ │ │ ├── singlelayer_singledelta_empty_source_layer_id.json │ │ │ │ │ │ ├── singlelayer_singledelta_null.json │ │ │ │ │ │ ├── singlelayer_singledelta_project2.json │ │ │ │ │ │ ├── special_data_types.json │ │ │ │ │ │ └── with_errors.json │ │ │ │ │ ├── nonspatial.csv │ │ │ │ │ ├── points.geojson │ │ │ │ │ ├── polygons.geojson │ │ │ │ │ ├── project.qgs │ │ │ │ │ ├── project2.qgs │ │ │ │ │ ├── project_broken_datasource.qgs │ │ │ │ │ ├── project_pgservice.qgs │ │ │ │ │ └── testdata.gpkg │ │ │ │ ├── file.txt │ │ │ │ ├── file2.txt │ │ │ │ ├── simple_bee_farming │ │ │ │ │ └── real_files │ │ │ │ │ │ ├── bees.gpkg │ │ │ │ │ │ ├── bumblebees.gpkg │ │ │ │ │ │ └── simple_bee_farming.qgs │ │ │ │ ├── simple_bumblebees.qgs │ │ │ │ ├── simple_bumblebees_correct_localized.qgs │ │ │ │ └── simple_bumblebees_wrong_localized.qgs │ │ │ └── utils.py │ │ ├── urls.py │ │ ├── utils.py │ │ ├── utils2 │ │ │ ├── __init__.py │ │ │ ├── audit.py │ │ │ ├── delta_utils.py │ │ │ ├── jobs.py │ │ │ ├── pg_service_file.py │ │ │ ├── projects.py │ │ │ ├── sentry.py │ │ │ └── storage.py │ │ ├── validators.py │ │ └── views │ │ │ ├── accounts_views.py │ │ │ ├── collaborators_views.py │ │ │ ├── deltas_views.py │ │ │ ├── files_views.py │ │ │ ├── jobs_views.py │ │ │ ├── members_views.py │ │ │ ├── package_views.py │ │ │ ├── projects_views.py │ │ │ ├── redirect_views.py │ │ │ ├── status_views.py │ │ │ ├── teams_views.py │ │ │ └── users_views.py │ ├── filestorage │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── backend.py │ │ ├── helpers.py │ │ ├── management │ │ │ └── commands │ │ │ │ ├── migrateavatars.py │ │ │ │ └── migratefilestorage.py │ │ ├── migrate_project_storage.py │ │ ├── migrations │ │ │ ├── 0001_initial │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_file_file_storage.py │ │ │ ├── 0003_alter_file_unique_together.py │ │ │ ├── 0004_alter_fileversion_size.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── signals.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_attachments.py │ │ │ ├── test_files_api.py │ │ │ ├── test_range.py │ │ │ ├── test_utils.py │ │ │ └── test_webdav.py │ │ ├── urls.py │ │ ├── utils.py │ │ ├── view_helpers.py │ │ └── views.py │ ├── locale │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── django.po │ │ └── es │ │ │ └── LC_MESSAGES │ │ │ └── django.po │ ├── notifs │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── cron.py │ │ ├── signals.py │ │ ├── templates │ │ │ └── notifs │ │ │ │ ├── notification_email_body.html │ │ │ │ ├── notification_email_body.txt │ │ │ │ └── notification_email_subject.txt │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_notifs.py │ ├── settings.py │ ├── settings_utils.py │ ├── subscription │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── exceptions.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_populate_plans.py │ │ │ ├── 0003_auto_20221028_1901.py │ │ │ ├── 0004_auto_20220923_1602.py │ │ │ ├── 0005_auto_20230113_0806.py │ │ │ ├── 0006_auto_20230426_2222.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── sql_config.py │ │ ├── templates │ │ │ └── admin │ │ │ │ └── subscription_change_form.html │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── data │ │ │ ├── project_pgservice.qgs │ │ │ ├── project_pgservice_delta_1.json │ │ │ └── project_pgservice_delta_2.json │ │ │ ├── test_external_db.py │ │ │ ├── test_job_creation.py │ │ │ ├── test_organization.py │ │ │ ├── test_package.py │ │ │ └── test_subscription.py │ ├── testing.py │ ├── urls.py │ └── wsgi.py ├── requirements │ ├── requirements.in │ ├── requirements.txt │ ├── requirements_runtime.in │ ├── requirements_runtime.txt │ ├── requirements_test.in │ ├── requirements_test.txt │ ├── requirements_worker_wrapper.in │ └── requirements_worker_wrapper.txt ├── wait_for_services.py └── worker_wrapper │ ├── __init__.py │ └── wrapper.py ├── docker-compose.override.local.yml ├── docker-compose.override.prod.yml ├── docker-compose.override.staging.yml ├── docker-compose.override.standalone.yml ├── docker-compose.override.test.yml ├── docker-compose.yml ├── docker-createbuckets ├── Dockerfile └── createbuckets.py ├── docker-nginx ├── 99-autoreload.sh ├── Dockerfile ├── options-ssl-nginx.conf ├── pages │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── favicon.ico │ ├── loading.html │ ├── logo_notext.svg │ └── sad_nyuki.svg └── templates │ └── default.conf.template ├── docker-qgis ├── Dockerfile ├── entrypoint.py ├── pyproject.toml ├── qfc_worker │ ├── __init__.py │ ├── apply_deltas.py │ ├── process_projectfile.py │ └── utils.py ├── requirements.in ├── requirements.txt ├── requirements_libqfieldsync.txt ├── schemas │ └── deltafile_01.json └── tests │ ├── test_apply_deltas.sh │ ├── test_qgis.py │ └── testdata │ ├── bees.gpkg │ ├── bumblebees.gpkg │ ├── project2apply │ ├── deltas │ │ ├── multilayer_multidelta.json │ │ ├── singlelayer_multidelta.json │ │ └── singlelayer_singledelta.json │ ├── points.geojson │ ├── polygons.geojson │ ├── project.qgs │ └── testdata.gpkg │ ├── simple_bee_farming.qgs │ ├── simple_bee_farming.qgs~ │ └── simple_project │ ├── curved_polys.gpkg │ ├── france_parts_shape.cpg │ ├── france_parts_shape.dbf │ ├── france_parts_shape.prj │ ├── france_parts_shape.qpj │ ├── france_parts_shape.shp │ ├── france_parts_shape.shx │ ├── project.qgs │ ├── project.qgs~ │ └── spatialite.db ├── ruff.toml └── scripts ├── check_envvars.py ├── check_envvars.sh ├── debug.sql └── init_letsencrypt.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | 9 | [*.py] 10 | indent_style = space 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | __pycache__ 5 | select = CLB100 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle line endings automatically for files detected as text 2 | # and leave all files detected as binary untouched. 3 | * text=auto 4 | 5 | # 6 | # The above will handle all files NOT found below 7 | # 8 | # These files are text and should be normalized (Convert crlf => lf) 9 | *.bash text eol=lf 10 | *.sh text eol=lf 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [opengisch] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://qfield.cloud"] 13 | -------------------------------------------------------------------------------- /.github/disabled-workflows/deploy_staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on staging.qfield.cloud 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy on staging.qfield.cloud 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Deploy 13 | uses: appleboy/ssh-action@master 14 | env: 15 | DEV_PASSWORD: ${{ secrets.DEV_PASSWORD }} 16 | REPO_USERNAME: ${{ secrets.REPO_USERNAME }} 17 | REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | host: staging.qfield.cloud 20 | username: ${{ secrets.DEV_USERNAME }} 21 | password: ${{ secrets.DEV_PASSWORD }} 22 | envs: DEV_PASSWORD,REPO_TOKEN,REPO_USERNAME 23 | script_stop: true 24 | script: | 25 | cd /opt/qfieldcloud 26 | docker compose -f docker-compose.staging.yml stop 27 | echo "$DEV_PASSWORD" | sudo -S git pull https://"$REPO_USERNAME":"$REPO_TOKEN"@github.com/opengisch/qfieldcloud 28 | docker compose -f docker-compose.staging.yml up -d --build 29 | docker compose -f docker-compose.staging.yml exec -T web python manage.py collectstatic --no-input --clear 30 | docker compose -f docker-compose.staging.yml exec -T web python manage.py migrate --noinput 31 | 32 | status: 33 | name: Check staging.qfield.cloud status 34 | runs-on: ubuntu-22.04 35 | needs: deploy 36 | steps: 37 | - name: Check 38 | run: curl -f https://staging.qfield.cloud/api/v1/status/ 39 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | jobs: 9 | backport: 10 | runs-on: ubuntu-24.04 11 | name: Backport 12 | steps: 13 | - name: Backport 14 | uses: m-kuhn/backport@v1.2.7 15 | with: 16 | github_token: ${{ secrets.NYUKI_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "master" ] 9 | schedule: 10 | - cron: '44 18 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-24.04 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'python' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v2 38 | with: 39 | category: "/language:${{matrix.language}}" 40 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 👓 Close stale issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-24.04 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | repo-token: ${{ secrets.NYUKI_TOKEN }} 13 | stale-issue-message: | 14 | The QFieldCloud project highly values your report and would love to see it addressed. However, this issue has been left in feedback mode for the last 14 days and is being automatically marked as "stale". If you would like to continue with this issue, please provide any missing information or answer any open questions. If you could resolve the issue yourself meanwhile, please leave a note for future readers with the same problem and close the issue. 15 | In case you should have any uncertainty, please leave a comment and we will be happy to help you proceed with this issue. 16 | If there is no further activity on this issue, it will be closed in a week. 17 | stale-issue-label: 'stale' 18 | only-labels: 'feedback' 19 | days-before-stale: 14 20 | days-before-close: 7 21 | -------------------------------------------------------------------------------- /.github/workflows/sync_translations.yml: -------------------------------------------------------------------------------- 1 | name: 🌎 Synchronize translations with Transifex 2 | 3 | on: 4 | schedule: 5 | - cron: "0 2 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | sync_translations: 11 | 12 | name: Synchronize Transifex translations 13 | runs-on: ubuntu-latest 14 | 15 | env: 16 | TX_TOKEN: ${{ secrets.TX_TOKEN }} 17 | 18 | steps: 19 | 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | token: ${{ secrets.NYUKI_TOKEN }} 24 | 25 | - name: Install Transifex CLI 26 | run: | 27 | curl -OL https://github.com/transifex/cli/releases/download/v1.6.17/tx-linux-amd64.tar.gz 28 | tar -xvzf tx-linux-amd64.tar.gz 29 | 30 | - name: Copy .env file 31 | run: | 32 | cp .env.example .env 33 | 34 | - name: Perform docker pre operations 35 | run: | 36 | docker compose pull 37 | docker compose build 38 | 39 | - name: Generate Django translation po files 40 | run: | 41 | docker compose run --user root app python manage.py makemessages -l es 42 | 43 | - name: Push translation files to Transifex 44 | run: ./tx push --source 45 | 46 | - name: Pull from Transifex 47 | run: ./tx pull --all --minimum-perc 0 --force 48 | 49 | - name: Add and commit new translations 50 | uses: EndBug/add-and-commit@v9 51 | with: 52 | message: Synchronize translations 53 | author_name: Translation update 💬 54 | author_email: info@opengis.ch 55 | add: '["docker-app/qfieldcloud/locale"]' 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.log 3 | *.orig 4 | .htmlcov/ 5 | .coverage 6 | .vscode 7 | .env 8 | docker-compose.override.yml 9 | client/projects 10 | conf/mkcert/* 11 | conf/certbot/* 12 | conf/nginx/certs/*.pem 13 | conf/nginx/config.d/*.conf 14 | conf/nginx/dhparams/*.pem 15 | Pipfile* 16 | **/site-packages 17 | docker-qgis/libqfieldsync 18 | docker-qgis/qfieldcloud-sdk-python 19 | 20 | # compiled translations 21 | docker-app/qfieldcloud/locale/**/*.mo 22 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | no_implicit_optional = False 3 | disable_error_code = var-annotated 4 | 5 | # Turn off mypy for all django migration packages via naming convention. 6 | [mypy-*.migrations.*] 7 | ignore_errors: True 8 | 9 | # Turn off mypy for unit tests 10 | [mypy-*.tests.*] 11 | ignore_errors: True 12 | 13 | plugins = 14 | mypy_drf_plugin.main 15 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:opengisch:p:qfieldcloud:r:qfieldcloud] 5 | file_filter = docker-app/qfieldcloud/locale//LC_MESSAGES/django.po 6 | source_file = docker-app/qfieldcloud/locale/en/LC_MESSAGES/django.po 7 | type = PO 8 | resource_name = QFieldCloud 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OPENGIS.ch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | On https://qfield.cloud we always run the latest stable release. 6 | 7 | 8 | ## Reporting a Vulnerability 9 | 10 | At OPENGIS.ch we take security very seriously, if you found a vulnerability, please get in touch with security@qfield.org. 11 | 12 | We'll get back at you as soon as possible after analising your report. 13 | -------------------------------------------------------------------------------- /conf/nginx/certs/README.md: -------------------------------------------------------------------------------- 1 | This directory will contain the self-signed certificates automatically created by `mkcert`. 2 | 3 | You can also place your custom certificates. 4 | 5 | To make use of the any of the certificates in this directory, make sure you adjust the values of `QFIELDCLOUD_TLS_CERT` and `QFIELDCLOUD_TLS_KEY` environment variables. 6 | This directory is accessible in the `nginx` container at `/etc/nginx/certs/`. 7 | -------------------------------------------------------------------------------- /conf/nginx/dhparams/ssl-dhparams.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN DH PARAMETERS----- 2 | MIICCAKCAgEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb 3 | IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft 4 | awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT 5 | mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh 6 | fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq 7 | 5RXSJhiY+gUQFXKOWoqqxC2tMxcNBFB6M6hVIavfHLpk7PuFBFjb7wqK6nFXXQYM 8 | fbOXD4Wm4eTHq/WujNsJM9cejJTgSiVhnc7j0iYa0u5r8S/6BtmKCGTYdgJzPshq 9 | ZFIfKxgXeyAMu+EXV3phXWx3CYjAutlG4gjiT6B05asxQ9tb/OD9EI5LgtEgqSEI 10 | ARpyPBKnh+bXiHGaEL26WyaZwycYavTiPBqUaDS2FQvaJYPpyirUTOjbu8LbBN6O 11 | +S6O/BQfvsqmKHxZR05rwF2ZspZPoJDDoiM7oYZRW+ftH2EpcM7i16+4G912IXBI 12 | HNAGkSfVsFqpk7TqmI2P3cGG/7fckKbAj030Nck0BjGZ//////////8CAQI= 13 | -----END DH PARAMETERS----- 14 | -------------------------------------------------------------------------------- /docker-app/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | qfieldcloud 5 | omit = 6 | **migrations* 7 | **tests* 8 | **/admin.py 9 | **/wsgi.py 10 | 11 | [report] 12 | precision = 2 13 | exclude_lines = 14 | pragma: no cover 15 | def __repr__ 16 | if self.debug: 17 | if settings.DEBUG 18 | raise AssertionError 19 | raise NotImplementedError 20 | if 0: 21 | if __name__ == .__main__.: 22 | 23 | [html] 24 | directory = .htmlcov 25 | -------------------------------------------------------------------------------- /docker-app/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function waitForServices() { 4 | python wait_for_services.py 5 | } 6 | 7 | waitForServices 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /docker-app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | 4 | import sys 5 | 6 | 7 | def main(): 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError as exc: 11 | raise ImportError( 12 | "Couldn't import Django. Are you sure it's installed and " 13 | "available on your PYTHONPATH environment variable? Did you " 14 | "forget to activate a virtual environment?" 15 | ) from exc 16 | 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/authentication/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | name = "qfieldcloud.authentication" 6 | initialized = False 7 | 8 | @classmethod 9 | def initialize(cls): 10 | """ 11 | Initialize authentication. 12 | This method is re-entrant and can be called multiple times. 13 | """ 14 | 15 | if cls.initialized: 16 | return 17 | 18 | cls.initialized = True 19 | 20 | from .conf import settings # noqa 21 | 22 | def ready(self): 23 | self.initialize() 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/auth_backends.py: -------------------------------------------------------------------------------- 1 | from allauth.account.auth_backends import ( 2 | AuthenticationBackend as AllAuthAuthenticationBackend, 3 | ) 4 | from django.contrib.auth import get_user_model 5 | 6 | 7 | class AuthenticationBackend(AllAuthAuthenticationBackend): 8 | """Extend the original `allauth` authentication backend to limit user types who can sign in. 9 | 10 | Sign in via team or organization should be forbidden. 11 | """ 12 | 13 | def _authenticate_by_username(self, **credentials): 14 | user = super()._authenticate_by_username(**credentials) 15 | 16 | if user and user.is_person: 17 | return user 18 | 19 | return None 20 | 21 | def _authenticate_by_email(self, **credentials): 22 | user = super()._authenticate_by_email(**credentials) 23 | 24 | if user and user.is_person: 25 | return user 26 | 27 | return None 28 | 29 | def get_user(self, user_id): 30 | """Almost the same as `contrib.auth.backends.ModelBackend`, but not using the default manager, but the normal `objects` manager 31 | 32 | Returns: 33 | In theory it can return any of `Person` | `Organization` | `Team` | `None` types, however it will always be a `Person` or `None` 34 | """ 35 | UserModel = get_user_model() 36 | 37 | try: 38 | user = UserModel.objects.get(pk=user_id) 39 | except UserModel.DoesNotExist: 40 | return None 41 | 42 | return user if self.user_can_authenticate(user) else None 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | settings.QFIELDCLOUD_LOGIN_SERIALIZER = getattr( 4 | settings, 5 | "QFIELDCLOUD_LOGIN_SERIALIZER", 6 | "qfieldcloud.authentication.serializers.LoginSerializer", 7 | ) 8 | 9 | settings.QFIELDCLOUD_TOKEN_SERIALIZER = getattr( 10 | settings, 11 | "QFIELDCLOUD_TOKEN_SERIALIZER", 12 | "qfieldcloud.authentication.serializers.TokenSerializer", 13 | ) 14 | 15 | settings.QFIELDCLOUD_USER_SERIALIZER = getattr( 16 | settings, 17 | "QFIELDCLOUD_USER_SERIALIZER", 18 | "qfieldcloud.authentication.serializers.UserSerializer", 19 | ) 20 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/migrations/0002_alter_authtoken_client_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-03-09 15:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("authentication", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="authtoken", 14 | name="client_type", 15 | field=models.CharField( 16 | choices=[ 17 | ("browser", "Browser"), 18 | ("cli", "Command line interface"), 19 | ("sdk", "SDK"), 20 | ("qfield", "QField"), 21 | ("qfieldsync", "QFieldSync"), 22 | ("worker", "Worker"), 23 | ("unknown", "Unknown"), 24 | ], 25 | default="unknown", 26 | max_length=32, 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/migrations/0003_authtoken_authenticat_created_ee68f9_idx.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-12-29 12:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("authentication", "0002_alter_authtoken_client_type"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddIndex( 13 | model_name="authtoken", 14 | index=models.Index( 15 | fields=["created_at"], name="authenticat_created_ee68f9_idx" 16 | ), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/sso/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/authentication/sso/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/sso/provider_styles.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from django.conf import settings 4 | from django.http import HttpRequest 5 | from django.templatetags.static import static 6 | 7 | 8 | class SSOProviderStyles: 9 | """Helper class to get the style settings for a provider.""" 10 | 11 | def __init__(self, request: HttpRequest): 12 | self.request = request 13 | 14 | def get(self, provider_id: str) -> dict: 15 | """Get the style settings for a (sub)provider.""" 16 | style_settings = settings.QFIELDCLOUD_SSO_PROVIDER_STYLES.get(provider_id, {}) 17 | styles = deepcopy(style_settings) 18 | 19 | for theme_id, theme in styles.items(): 20 | if not isinstance(theme, dict): 21 | continue 22 | 23 | logo = theme.get("logo") 24 | if logo and not logo.startswith("http"): 25 | # Assume a relative path - create an absolute URL 26 | theme["logo"] = self.request.build_absolute_uri(static(logo)) 27 | 28 | return styles 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/authentication/tests/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/authentication/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils.module_loading import import_string 2 | 3 | 4 | def load_module(name): 5 | try: 6 | backend = import_string(name) 7 | return backend 8 | except ModuleNotFoundError as e: 9 | raise ModuleNotFoundError(f'Can not find module path defined "{name}"') from e 10 | except ImportError as e: 11 | raise ImportError(f'Can not import backend class defined in "{name}"') from e 12 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "qfieldcloud.core" 6 | 7 | def ready(self): 8 | from qfieldcloud.core import signals # noqa 9 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/converters.py: -------------------------------------------------------------------------------- 1 | from django.urls.converters import StringConverter 2 | 3 | 4 | class IStringConverter(StringConverter): 5 | def to_python(self, value): 6 | return value.lower() 7 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/management/commands/calcprojectstorage.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.core.management.base import BaseCommand 4 | from qfieldcloud.core.models import Project 5 | 6 | 7 | class Command(BaseCommand): 8 | """ 9 | Recalculate projects storage size 10 | """ 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument("project_id", type=uuid.UUID, nargs="?") 14 | parser.add_argument("--force-recalculate", action="store_true") 15 | 16 | def handle(self, *args, **options): 17 | project_id = options.get("project_id") 18 | force_recalculate = options.get("force_recalculate") 19 | 20 | extra_filters = {} 21 | if project_id: 22 | extra_filters["id"] = project_id 23 | 24 | if not project_id and not force_recalculate: 25 | extra_filters["file_storage_bytes"] = 0 26 | 27 | projects_qs = Project.objects.filter( 28 | the_qgis_filename__isnull=False, 29 | **extra_filters, 30 | ).order_by("-updated_at") 31 | total_count = projects_qs.count() 32 | 33 | for idx, project in enumerate(projects_qs): 34 | print( 35 | f'Calculating project files storage size for "{project.id}" {idx}/{total_count}...' 36 | ) 37 | project.save(recompute_storage=True) 38 | print( 39 | f'Project files storage size for "{project.id}" is {project.file_storage_bytes} bytes.' 40 | ) 41 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/management/commands/createsuperuser.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.management.commands.createsuperuser import ( 2 | Command as SuperUserCommand, 3 | ) 4 | from qfieldcloud.core.models import Person 5 | 6 | 7 | class Command(SuperUserCommand): 8 | """ 9 | We overwrite the django createsuperuser command because it uses the wrong User model 10 | """ 11 | 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | self.UserModel = Person 15 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/management/commands/createuser.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from qfieldcloud.core.models import Person 3 | 4 | 5 | class Command(BaseCommand): 6 | """ 7 | Creates a normal or super user using the CLI. 8 | Unlike the Django's createsuperuser command, here we can pass the password as an argument. 9 | This is a utility function that is expected to be used only for testing purposes. 10 | """ 11 | 12 | help = """ 13 | Create a user with given username, email and password 14 | Usage: python manage.py createuser --username=test --email=test@test.com --password=test --superuser 15 | """ 16 | 17 | def add_arguments(self, parser): 18 | parser.add_argument("--username", type=str, required=True) 19 | parser.add_argument("--password", type=str, required=True) 20 | parser.add_argument("--email", type=str, required=True) 21 | parser.add_argument("--superuser", action="store_true") 22 | 23 | def handle(self, *args, **options): 24 | username = options.get("username") 25 | password = options.get("password") 26 | email = options.get("email") 27 | is_superuser = options.get("superuser") 28 | try: 29 | if not Person.objects.filter(username=username).exists(): 30 | Person.objects.create_user( 31 | username=username, 32 | email=email, 33 | password=password, 34 | is_superuser=is_superuser, 35 | ) 36 | print(f"User {username} has been successfully created\n") 37 | else: 38 | print(f"User {username} already exists\n") 39 | except Exception as e: 40 | print("ERROR: Unable to create user\n%s\n" % e) 41 | exit(1) 42 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/management/commands/createuseraccounts.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from qfieldcloud.core.models import User, UserAccount 3 | 4 | 5 | class Command(BaseCommand): 6 | """ 7 | Creates user accounts for all users that are missing a user account. 8 | """ 9 | 10 | def handle(self, *args, **options): 11 | for user in User.objects.filter( 12 | useraccount=None, 13 | type__in=[User.Type.PERSON, User.Type.ORGANIZATION], 14 | ): 15 | print( 16 | f'Creating user account for user "{user.username}" email "{user.email}"...' 17 | ) 18 | UserAccount.objects.create(user=user) 19 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/management/commands/status.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from qfieldcloud.core import utils 3 | 4 | 5 | class Command(BaseCommand): 6 | help = "Check qfieldcloud status" 7 | 8 | def handle(self, *args, **options): 9 | results = {} 10 | results["storage"] = "ok" 11 | # Check if bucket exists (i.e. the connection works) 12 | try: 13 | utils.get_s3_bucket() 14 | except Exception: 15 | results["storage"] = "error" 16 | 17 | self.stdout.write( 18 | self.style.SUCCESS(f"Everything seems to work properly: {results}") 19 | ) 20 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/middleware/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/middleware/requests.py: -------------------------------------------------------------------------------- 1 | import io 2 | import logging 3 | import shutil 4 | 5 | from constance import config 6 | from django.conf import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def attach_keys(get_response): 12 | """ 13 | QF-2540 14 | Annotate request with: 15 | - a `str` representation of relevant fields, so as to obtain a diff by comparing with the post-serialized request later in the callstack; 16 | - a byte-for-byte, non stealing copy of the raw body to inspect multipart boundaries. 17 | """ 18 | 19 | def middleware(request): 20 | # add a copy of the request body to the request 21 | if ( 22 | settings.SENTRY_DSN 23 | and request.method == "POST" 24 | and "Content-Length" in request.headers 25 | and ( 26 | int(request.headers["Content-Length"]) 27 | < int(config.SENTRY_REQUEST_MAX_SIZE_TO_SEND) 28 | ) 29 | ): 30 | logger.info("Making a temporary copy for request body.") 31 | 32 | input_stream = io.BytesIO(request.body) 33 | output_stream = io.BytesIO() 34 | shutil.copyfileobj(input_stream, output_stream) 35 | request.body_stream = output_stream 36 | 37 | request_attributes = { 38 | "file_key": str(request.FILES.keys()), 39 | "meta": str(request.META), 40 | "files": request.FILES.getlist("file"), 41 | } 42 | request.attached_keys = str(request_attributes) 43 | response = get_response(request) 44 | return response 45 | 46 | return middleware 47 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/middleware/test.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.core.exceptions import MiddlewareNotUsed 4 | 5 | 6 | class TestMiddleware: 7 | def __init__(self, get_response): 8 | self.get_response = get_response 9 | 10 | if settings.ENVIRONMENT != "test": 11 | raise MiddlewareNotUsed() 12 | 13 | def __call__(self, request): 14 | ContentType.objects.clear_cache() 15 | return self.get_response(request) 16 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/middleware/timezone.py: -------------------------------------------------------------------------------- 1 | import zoneinfo 2 | 3 | from django.conf import settings 4 | from django.utils import timezone 5 | 6 | 7 | class TimezoneMiddleware: 8 | def __init__(self, get_response): 9 | self.get_response = get_response 10 | 11 | def __call__(self, request): 12 | if request.user.is_authenticated and hasattr( 13 | request.user.useraccount, "useraccount" 14 | ): 15 | user_tz = request.user.useraccount.timezone 16 | elif settings.TIME_ZONE: 17 | user_tz = zoneinfo.ZoneInfo(settings.TIME_ZONE) 18 | else: 19 | user_tz = None 20 | 21 | if user_tz: 22 | timezone.activate(user_tz) 23 | else: 24 | timezone.deactivate() 25 | 26 | return self.get_response(request) 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0005_auto_20201203_1037.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-03 10:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0004_auto_20201201_1209"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="delta", 14 | name="status", 15 | field=models.PositiveSmallIntegerField( 16 | choices=[ 17 | (1, "STATUS_PENDING"), 18 | (2, "STATUS_BUSY"), 19 | (3, "STATUS_APPLIED"), 20 | (4, "STATUS_CONFLICT"), 21 | (5, "STATUS_NOT_APPLIED"), 22 | (6, "STATUS_ERROR"), 23 | ], 24 | default=1, 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0006_auto_20201214_1642.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-14 16:42 2 | 3 | import json 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations 7 | 8 | 9 | def jsonify_output_field(apps, schema_editor): 10 | # Old values in output field of delta table where string instead of json 11 | Delta = apps.get_model("core", "Delta") 12 | for delta in Delta.objects.all(): 13 | delta.output = json.dumps([{"msg": delta.output}]) 14 | delta.save() 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ("core", "0005_auto_20201203_1037"), 20 | ] 21 | 22 | operations = [ 23 | migrations.RunPython(jsonify_output_field), 24 | migrations.AlterField( 25 | model_name="delta", 26 | name="output", 27 | field=django.contrib.postgres.fields.jsonb.JSONField(null=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0007_project_overwrite_conflicts.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-21 19:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0006_auto_20201214_1642"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="overwrite_conflicts", 15 | field=models.BooleanField(default=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0008_update_site_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2020-12-31 15:04 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | def update_site_name(apps, schema_editor): 8 | site_model = apps.get_model("sites", "Site") 9 | domain = "qfield.cloud" 10 | name = "QFieldCloud" 11 | 12 | site_model.objects.update_or_create( 13 | pk=settings.SITE_ID, defaults={"domain": domain, "name": name} 14 | ) 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ("core", "0007_project_overwrite_conflicts"), 20 | ("sites", "0002_alter_domain_unique"), 21 | ] 22 | 23 | operations = [ 24 | migrations.RunPython(update_site_name), 25 | ] 26 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0010_auto_20210106_1543.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-06 15:43 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | import qfieldcloud.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("core", "0009_geodb"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="project", 17 | name="name", 18 | field=models.CharField( 19 | help_text="Project name", 20 | max_length=255, 21 | validators=[ 22 | django.core.validators.RegexValidator( 23 | "^[-a-zA-Z0-9_]+$", 24 | "Only letters, numbers, underscores or hyphens are allowed.", 25 | ), 26 | django.core.validators.RegexValidator( 27 | "^.{3,}$", "The name must be at least 3 characters long." 28 | ), 29 | django.core.validators.RegexValidator( 30 | "^[a-zA-Z].*$", "The name must begin with a letter." 31 | ), 32 | qfieldcloud.core.validators.reserved_words_validator, 33 | ], 34 | ), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0012_auto_20210106_0930.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-06 09:30 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0011_export"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameModel( 13 | old_name="Export", 14 | new_name="Exportation", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0013_remove_exportation_output.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-12 09:47 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0012_auto_20210106_0930"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="exportation", 14 | name="output", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0014_exportation_output.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-18 16:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0013_remove_exportation_output"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="exportation", 14 | name="output", 15 | field=models.TextField(null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0015_auto_20210123_0116.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-01-23 01:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0014_exportation_output"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="useraccount", 14 | name="bio", 15 | field=models.CharField(default="", max_length=255), 16 | ), 17 | migrations.AddField( 18 | model_name="useraccount", 19 | name="is_email_public", 20 | field=models.BooleanField(default=False), 21 | ), 22 | migrations.AddField( 23 | model_name="useraccount", 24 | name="location", 25 | field=models.CharField(default="", max_length=255), 26 | ), 27 | migrations.AddField( 28 | model_name="useraccount", 29 | name="twitter", 30 | field=models.CharField(default="", max_length=255), 31 | ), 32 | migrations.AddField( 33 | model_name="useraccount", 34 | name="workplace", 35 | field=models.CharField(default="", max_length=255), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0016_generate_user_account.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def generate_user_account(apps, schema_editor): 5 | User = apps.get_model("core", "User") 6 | UserAccount = apps.get_model("core", "UserAccount") 7 | 8 | for user in User.objects.filter(useraccount=None): 9 | UserAccount.objects.create(user=user) 10 | 11 | 12 | class Migration(migrations.Migration): 13 | dependencies = [ 14 | ("core", "0015_auto_20210123_0116"), 15 | ] 16 | 17 | operations = [ 18 | migrations.RunPython( 19 | generate_user_account, reverse_code=migrations.RunPython.noop 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0018_auto_20210308_0034.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-03-08 00:34 2 | 3 | from django.db import migrations, models 4 | from django.db.models.aggregates import Count 5 | 6 | 7 | class Migration(migrations.Migration): 8 | def forwards_func(apps, schema_editor): 9 | Project = apps.get_model("core", "Project") 10 | 11 | nonunique_projects_data = ( 12 | Project.objects.values("name", "owner") 13 | .annotate(c=Count("id")) 14 | .filter(c__gt=1) 15 | ) 16 | 17 | for project_data in nonunique_projects_data: 18 | count = 0 19 | 20 | for project in Project.objects.filter( 21 | name=project_data.get("name"), owner_id=project_data.get("owner") 22 | ): 23 | count += 1 24 | 25 | if count == 1: 26 | continue 27 | 28 | project.name += f"_{count}" 29 | project.save() 30 | 31 | dependencies = [ 32 | ("core", "0017_auto_20210226_1939"), 33 | ] 34 | 35 | operations = [ 36 | migrations.RunPython(forwards_func, migrations.RunPython.noop), 37 | migrations.AddConstraint( 38 | model_name="project", 39 | constraint=models.UniqueConstraint( 40 | fields=("owner", "name"), name="project_owner_name_uniq" 41 | ), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0019_auto_20210316_2056.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-03-16 20:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0018_auto_20210308_0034"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="delta", 14 | name="status", 15 | field=models.PositiveSmallIntegerField( 16 | choices=[ 17 | (1, "STATUS_PENDING"), 18 | (2, "STATUS_BUSY"), 19 | (3, "STATUS_APPLIED"), 20 | (4, "STATUS_CONFLICT"), 21 | (5, "STATUS_NOT_APPLIED"), 22 | (6, "STATUS_ERROR"), 23 | (7, "STATUS_IGNORED"), 24 | ], 25 | default=1, 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0020_auto_20210321_1749.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.17 on 2021-03-21 17:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0019_auto_20210316_2056"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="delta", 14 | name="status", 15 | field=models.PositiveSmallIntegerField( 16 | choices=[ 17 | (1, "STATUS_PENDING"), 18 | (2, "STATUS_BUSY"), 19 | (3, "STATUS_APPLIED"), 20 | (4, "STATUS_CONFLICT"), 21 | (5, "STATUS_NOT_APPLIED"), 22 | (6, "STATUS_ERROR"), 23 | (7, "STATUS_IGNORED"), 24 | (8, "STATUS_UNPERMITTED"), 25 | ], 26 | default=1, 27 | ), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0022_auto_20210331_0143.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-03-31 01:43 2 | 3 | from django.db import migrations, models 4 | from django.db.models.aggregates import Count 5 | 6 | 7 | class Migration(migrations.Migration): 8 | def forwards_func(apps, schema_editor): 9 | OrganizationMember = apps.get_model("core", "OrganizationMember") 10 | 11 | duplicated_members_data = ( 12 | OrganizationMember.objects.values("organization", "member") 13 | .annotate(c=Count("id")) 14 | .filter(c__gt=1) 15 | ) 16 | 17 | for member_data in duplicated_members_data: 18 | queryset = OrganizationMember.objects.filter( 19 | organization=member_data["organization"], 20 | member=member_data["member"], 21 | ).order_by("role") 22 | 23 | queryset.exclude(pk=queryset.first().pk).delete() 24 | queryset.save() 25 | 26 | for organization_member in OrganizationMember.objects.all(): 27 | member = organization_member.member 28 | organization = organization_member.organization 29 | 30 | if member == organization.organization_owner: 31 | organization_member.delete() 32 | 33 | dependencies = [ 34 | ("core", "0021_auto_20210330_2209"), 35 | ] 36 | 37 | operations = [ 38 | migrations.RunPython(forwards_func, migrations.RunPython.noop), 39 | migrations.AddConstraint( 40 | model_name="organizationmember", 41 | constraint=models.UniqueConstraint( 42 | fields=("organization", "member"), 43 | name="organization_organization_member_uniq", 44 | ), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0023_auto_20210407_1247.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-04-07 12:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0022_auto_20210331_0143"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="remaining_invitations", 15 | field=models.PositiveIntegerField(default=3), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0024_auto_20210407_2002.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.19 on 2021-04-07 20:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0023_auto_20210407_1247"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="is_geodb_enabled", 15 | field=models.BooleanField( 16 | default=True, 17 | help_text="Whether the account has the option to create a GeoDB.", 18 | ), 19 | ), 20 | migrations.AlterField( 21 | model_name="user", 22 | name="is_geodb_enabled", 23 | field=models.BooleanField( 24 | default=False, 25 | help_text="Whether the account has the option to create a GeoDB.", 26 | ), 27 | ), 28 | migrations.AlterField( 29 | model_name="user", 30 | name="remaining_invitations", 31 | field=models.PositiveIntegerField( 32 | default=3, 33 | help_text="Remaining invitations that can be sent by the user himself.", 34 | ), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0025_auto_20210407_2306.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.8 on 2021-04-07 23:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0024_auto_20210407_2002"), 9 | ("auth", "0012_alter_user_first_name_max_length"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="delta", 15 | name="content", 16 | field=models.JSONField(), 17 | ), 18 | migrations.AlterField( 19 | model_name="delta", 20 | name="output", 21 | field=models.JSONField(null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name="exportation", 25 | name="exportlog", 26 | field=models.JSONField(null=True), 27 | ), 28 | migrations.AlterField( 29 | model_name="user", 30 | name="first_name", 31 | field=models.CharField( 32 | blank=True, max_length=150, verbose_name="first name" 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0026_auto_20210409_1339.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-09 13:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | def generate_user_account(apps, schema_editor): 8 | User = apps.get_model("core", "User") 9 | UserAccount = apps.get_model("core", "UserAccount") 10 | 11 | for user in User.objects.filter(useraccount=None): 12 | UserAccount.objects.create(user=user) 13 | 14 | def transfer_is_geodb_enabled_forwards(apps, schema_editor): 15 | User = apps.get_model("core", "User") 16 | 17 | for user in User.objects.all(): 18 | user.useraccount.is_geodb_enabled = user.is_geodb_enabled 19 | 20 | def transfer_is_geodb_enabled_backwards(apps, schema_editor): 21 | User = apps.get_model("core", "User") 22 | 23 | for user in User.objects.all(): 24 | user.is_geodb_enabled = user.useraccount.is_geodb_enabled 25 | 26 | dependencies = [ 27 | ("core", "0025_auto_20210407_2306"), 28 | ] 29 | 30 | operations = [ 31 | migrations.AddField( 32 | model_name="useraccount", 33 | name="is_geodb_enabled", 34 | field=models.BooleanField( 35 | default=False, 36 | help_text="Whether the account has the option to create a GeoDB.", 37 | ), 38 | ), 39 | migrations.RunPython( 40 | generate_user_account, reverse_code=migrations.RunPython.noop 41 | ), 42 | migrations.RunPython( 43 | transfer_is_geodb_enabled_forwards, 44 | reverse_code=transfer_is_geodb_enabled_backwards, 45 | ), 46 | migrations.RemoveField( 47 | model_name="user", 48 | name="is_geodb_enabled", 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0027_auto_20210408_0138.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-08 01:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0026_auto_20210409_1339"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="useraccount", 14 | name="avatar_uri", 15 | field=models.CharField( 16 | blank=True, 17 | max_length=255, 18 | verbose_name="Profile Picture URI", 19 | ), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0028_auto_20210414_1545.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-14 15:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0027_auto_20210408_0138"), 9 | ] 10 | 11 | def set_is_public(apps, schema_editor): 12 | Project = apps.get_model("core", "Project") 13 | 14 | for project in Project.objects.all(): 15 | project.is_public = not project.private 16 | project.save() 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name="project", 21 | name="is_public", 22 | field=models.BooleanField( 23 | default=False, 24 | help_text="Projects that are marked as public would be visible and editable to anyone.", 25 | ), 26 | ), 27 | migrations.RunPython(set_is_public, reverse_code=migrations.RunPython.noop), 28 | migrations.RemoveField( 29 | model_name="project", 30 | name="private", 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0029_auto_20210415_1420.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-15 14:20 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("core", "0028_auto_20210414_1545"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name="project", 16 | options={"ordering": ["owner__username", "name"]}, 17 | ), 18 | migrations.AlterField( 19 | model_name="project", 20 | name="owner", 21 | field=models.ForeignKey( 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="projects", 24 | to=settings.AUTH_USER_MODEL, 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0032_auto_20210419_1011.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-19 10:11 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | import qfieldcloud.core.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | dependencies = [ 12 | ("core", "0031_auto_20210415_1535"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelManagers( 17 | name="organization", 18 | managers=[ 19 | ("objects", qfieldcloud.core.models.OrganizationManager()), 20 | ], 21 | ), 22 | migrations.AddField( 23 | model_name="organizationmember", 24 | name="is_public", 25 | field=models.BooleanField(default=False), 26 | ), 27 | migrations.AlterField( 28 | model_name="project", 29 | name="owner", 30 | field=models.ForeignKey( 31 | limit_choices_to=models.Q(user_type__in=[1, 2]), 32 | on_delete=django.db.models.deletion.CASCADE, 33 | related_name="projects", 34 | to=settings.AUTH_USER_MODEL, 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0033_auto_20210419_1908.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-19 19:08 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | def add_created_by(apps, schema_editor): 9 | Delta = apps.get_model("core", "Delta") 10 | 11 | for delta in Delta.objects.all(): 12 | delta.created_by = delta.project.owner 13 | delta.save() 14 | 15 | dependencies = [ 16 | ("core", "0032_auto_20210419_1011"), 17 | ] 18 | 19 | operations = [ 20 | migrations.AddField( 21 | model_name="delta", 22 | name="created_by", 23 | field=models.ForeignKey( 24 | on_delete=django.db.models.deletion.CASCADE, 25 | related_name="uploaded_deltas", 26 | to="core.user", 27 | null=True, 28 | ), 29 | ), 30 | migrations.RunPython(add_created_by, migrations.RunPython.noop), 31 | migrations.AlterField( 32 | model_name="delta", 33 | name="created_by", 34 | field=models.ForeignKey( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | related_name="uploaded_deltas", 37 | to="core.user", 38 | ), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0034_auto_20210508_1030.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-05-08 10:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0033_auto_20210419_1908"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="has_accepted_tos", 15 | field=models.BooleanField(default=False), 16 | ), 17 | migrations.AddField( 18 | model_name="user", 19 | name="has_newsletter_subscription", 20 | field=models.BooleanField(default=False), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0035_auto_20210510_0635.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.2 on 2021-05-10 06:35 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0034_auto_20210508_1030"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="project", 15 | name="name", 16 | field=models.CharField( 17 | help_text="Only letters, numbers, underscores, hyphens and dots are allowed.", 18 | max_length=255, 19 | validators=[ 20 | django.core.validators.RegexValidator( 21 | "^[a-zA-Z0-9-_\\.]+$", 22 | "Only letters, numbers, underscores, hyphens and dots are allowed.", 23 | ) 24 | ], 25 | ), 26 | ), 27 | migrations.AlterField( 28 | model_name="useraccount", 29 | name="bio", 30 | field=models.CharField(blank=True, default="", max_length=255), 31 | ), 32 | migrations.AlterField( 33 | model_name="useraccount", 34 | name="location", 35 | field=models.CharField(blank=True, default="", max_length=255), 36 | ), 37 | migrations.AlterField( 38 | model_name="useraccount", 39 | name="twitter", 40 | field=models.CharField(blank=True, default="", max_length=255), 41 | ), 42 | migrations.AlterField( 43 | model_name="useraccount", 44 | name="workplace", 45 | field=models.CharField(blank=True, default="", max_length=255), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0037_alter_project_owner_help_text.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.2 on 2021-05-17 09:36 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("core", "0036_auto_20210426_1210"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="project", 16 | name="owner", 17 | field=models.ForeignKey( 18 | help_text="The project owner can be either you or any of the organization you are member of.", 19 | limit_choices_to=models.Q(("user_type__in", [1, 2])), 20 | on_delete=django.db.models.deletion.CASCADE, 21 | related_name="projects", 22 | to=settings.AUTH_USER_MODEL, 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0038_rename_workplace_useraccount_company.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.2 on 2021-05-17 10:57 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0037_alter_project_owner_help_text"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="useraccount", 14 | old_name="workplace", 15 | new_name="company", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0039_auto_20210630_1210.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-30 12:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0038_rename_workplace_useraccount_company"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="applyjob", 14 | options={ 15 | "verbose_name": "Job: apply", 16 | "verbose_name_plural": "Jobs: apply", 17 | }, 18 | ), 19 | migrations.AlterModelOptions( 20 | name="exportjob", 21 | options={ 22 | "verbose_name": "Job: export", 23 | "verbose_name_plural": "Jobs: export", 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0040_auto_20210630_1212.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-30 12:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0039_auto_20210630_1210"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="delta", 14 | name="last_modified_pk", 15 | field=models.TextField(null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0042_auto_20211001_2217.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-01 22:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0041_auto_20210705_0837"), 9 | ] 10 | 11 | operations = [ 12 | # There used to be some operations, which does not affect the database storage, but got outdated. 13 | # Therefore this migrations become empty. 14 | ] 15 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0043_update_site_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-05 22:17 2 | 3 | 4 | from django.conf import settings 5 | from django.db import migrations 6 | 7 | 8 | def update_site_name(apps, schema_editor): 9 | site_model = apps.get_model("sites", "Site") 10 | domain = settings.QFIELDCLOUD_HOST 11 | name = "QFieldCloud" 12 | 13 | site_model.objects.update_or_create( 14 | pk=settings.SITE_ID, defaults={"domain": domain, "name": name} 15 | ) 16 | 17 | 18 | class Migration(migrations.Migration): 19 | dependencies = [ 20 | ("core", "0042_auto_20211001_2217"), 21 | ("sites", "0002_alter_domain_unique"), 22 | ] 23 | 24 | operations = [ 25 | migrations.RunPython(update_site_name), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0044_alter_user_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-06 13:38 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | import qfieldcloud.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("core", "0043_update_site_name"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="user", 17 | name="username", 18 | field=models.CharField( 19 | error_messages={"unique": "A user with that username already exists."}, 20 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 21 | max_length=150, 22 | unique=True, 23 | validators=[ 24 | django.core.validators.RegexValidator( 25 | "^[-a-zA-Z0-9_]+$", 26 | "Only letters, numbers, underscores or hyphens are allowed.", 27 | ), 28 | django.core.validators.RegexValidator( 29 | "^[a-zA-Z].*$", "The name must begin with a letter." 30 | ), 31 | django.core.validators.RegexValidator( 32 | "^.{3,}$", "The name must be at least 3 characters long." 33 | ), 34 | qfieldcloud.core.validators.reserved_words_validator, 35 | ], 36 | verbose_name="username", 37 | ), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0045_auto_20211012_2234.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-12 22:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def fill_in_datetime_fields(apps, schema_editor): 7 | # Old values in output field of delta table where string instead of json 8 | Job = apps.get_model("core", "Job") 9 | Job.objects.update( 10 | started_at=models.F("created_at"), finished_at=models.F("updated_at") 11 | ) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | dependencies = [ 16 | ("core", "0044_alter_user_username"), 17 | ] 18 | 19 | operations = [ 20 | migrations.AddField( 21 | model_name="job", 22 | name="finished_at", 23 | field=models.DateTimeField(blank=True, null=True, editable=False), 24 | ), 25 | migrations.AddField( 26 | model_name="job", 27 | name="started_at", 28 | field=models.DateTimeField(blank=True, null=True, editable=False), 29 | ), 30 | migrations.RunPython(fill_in_datetime_fields, migrations.RunPython.noop), 31 | ] 32 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0046_auto_20211013_2304.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-13 23:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def set_project_details(apps, schema_editor): 7 | # Old values in output field of delta table where string instead of json 8 | Project = apps.get_model("core", "Project") 9 | ProcessProjectfileJob = apps.get_model("core", "ProcessProjectfileJob") 10 | 11 | for project in Project.objects.filter(project_filename__isnull=False): 12 | ProcessProjectfileJob.objects.create( 13 | project=project, 14 | created_by=project.owner, 15 | type="process_projectfile", 16 | ) 17 | 18 | 19 | class Migration(migrations.Migration): 20 | dependencies = [ 21 | ("core", "0045_auto_20211012_2234"), 22 | ] 23 | 24 | operations = [ 25 | migrations.AddField( 26 | model_name="project", 27 | name="project_details", 28 | field=models.JSONField(blank=True, null=True), 29 | ), 30 | migrations.RunPython(set_project_details, migrations.RunPython.noop), 31 | ] 32 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0047_useraccount_timezone.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-20 21:35 2 | 3 | import timezone_field.fields 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0046_auto_20211013_2304"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="useraccount", 15 | name="timezone", 16 | field=timezone_field.fields.TimeZoneField( 17 | choices_display="WITH_GMT_OFFSET", default="Europe/Zurich" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0048_useraccount_notifs_frequency.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-18 09:52 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("core", "0047_useraccount_timezone"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="useraccount", 16 | name="notifs_frequency", 17 | field=models.DurationField( 18 | blank=True, 19 | choices=[ 20 | (datetime.timedelta(0), "Immediately"), 21 | (datetime.timedelta(seconds=3600), "Hourly"), 22 | (datetime.timedelta(days=1), "Daily"), 23 | (datetime.timedelta(days=7), "Weekly"), 24 | (None, "Disabled"), 25 | ], 26 | default=None, 27 | null=True, 28 | verbose_name="Email frequency for notifications", 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0049_auto_20211117_1843.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-17 17:43 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | def fill_in_last_apply_attempt_at(apps, schema_editor): 9 | # Old values in output field of delta table where string instead of json 10 | Delta = apps.get_model("core", "Delta") 11 | ApplyJobDelta = apps.get_model("core", "ApplyJobDelta") 12 | 13 | for delta in Delta.objects.all(): 14 | jobs_qs = ApplyJobDelta.objects.filter(delta=delta) 15 | 16 | if jobs_qs.count(): 17 | job_delta = jobs_qs.latest("apply_job__started_at") 18 | delta.last_apply_attempt_at = job_delta.apply_job.started_at 19 | delta.last_apply_attempt_by = job_delta.apply_job.created_by 20 | delta.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | dependencies = [ 25 | ("core", "0048_useraccount_notifs_frequency"), 26 | ] 27 | 28 | operations = [ 29 | migrations.AddField( 30 | model_name="delta", 31 | name="last_apply_attempt_at", 32 | field=models.DateTimeField(null=True), 33 | ), 34 | migrations.AddField( 35 | model_name="delta", 36 | name="last_apply_attempt_by", 37 | field=models.ForeignKey( 38 | null=True, 39 | on_delete=django.db.models.deletion.CASCADE, 40 | to=settings.AUTH_USER_MODEL, 41 | ), 42 | ), 43 | migrations.RunPython(fill_in_last_apply_attempt_at, migrations.RunPython.noop), 44 | ] 45 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0050_auto_20211118_1150.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.8 on 2021-10-27 09:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def rename_export_to_package(apps, schema_editor): 7 | Job = apps.get_model("core", "Job") 8 | Job.objects.filter(type="export").update(type="package") 9 | 10 | 11 | def rename_package_to_export(apps, schema_editor): 12 | Job = apps.get_model("core", "Job") 13 | Job.objects.filter(type="package").update(type="export") 14 | 15 | 16 | class Migration(migrations.Migration): 17 | dependencies = [ 18 | ("core", "0049_auto_20211117_1843"), 19 | ] 20 | 21 | operations = [ 22 | migrations.AddField( 23 | model_name="project", 24 | name="data_last_packaged_at", 25 | field=models.DateTimeField(blank=True, null=True), 26 | ), 27 | migrations.AddField( 28 | model_name="project", 29 | name="data_last_updated_at", 30 | field=models.DateTimeField(blank=True, null=True), 31 | ), 32 | migrations.RenameModel( 33 | old_name="ExportJob", 34 | new_name="PackageJob", 35 | ), 36 | migrations.AlterField( 37 | model_name="job", 38 | name="type", 39 | field=models.CharField( 40 | choices=[ 41 | ("package", "Package"), 42 | ("delta_apply", "Delta Apply"), 43 | ("process_projectfile", "Process QGIS Project File"), 44 | ], 45 | max_length=32, 46 | ), 47 | ), 48 | migrations.RunPython(rename_export_to_package, rename_package_to_export), 49 | ] 50 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0053_auto_20220505_1948.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-05-05 17:48 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0052_secret"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="secret", 15 | options={"ordering": ["project", "name"]}, 16 | ), 17 | migrations.AlterField( 18 | model_name="secret", 19 | name="name", 20 | field=models.TextField( 21 | help_text="Must start with a capital letter and followed by capital letters, numbers or underscores.", 22 | max_length=255, 23 | validators=[ 24 | django.core.validators.RegexValidator( 25 | "^[A-Z]+[A-Z0-9_]+$", 26 | "Must start with a capital letter and followed by capital letters, numbers or underscores.", 27 | ) 28 | ], 29 | ), 30 | ), 31 | migrations.AddConstraint( 32 | model_name="secret", 33 | constraint=models.UniqueConstraint( 34 | fields=("project", "name"), name="secret_project_name_uniq" 35 | ), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0054_project_last_package.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-03-09 15:58 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0053_auto_20220505_1948"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="project", 15 | name="last_package_job", 16 | field=models.ForeignKey( 17 | blank=True, 18 | null=True, 19 | on_delete=django.db.models.deletion.SET_NULL, 20 | related_name="last_job_of", 21 | to="core.packagejob", 22 | ), 23 | ), 24 | migrations.AddField( 25 | model_name="delta", 26 | name="jobs_to_apply", 27 | field=models.ManyToManyField( 28 | through="core.ApplyJobDelta", to="core.ApplyJob" 29 | ), 30 | ), 31 | migrations.RunSQL( 32 | sql=r'CREATE UNIQUE INDEX "core_user_username_uppercase" ON "core_user" (UPPER("username"));', 33 | reverse_sql=r'DROP INDEX IF EXISTS "core_user_username_uppercase";', 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0060_alter_project_storage_size_mb.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.17 on 2023-02-10 00:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0059_auto_20221028_1806"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="project", 14 | old_name="storage_size_mb", 15 | new_name="file_storage_bytes", 16 | ), 17 | migrations.RunSQL( 18 | """ 19 | UPDATE core_project 20 | SET 21 | file_storage_bytes = file_storage_bytes * 1000 * 1000 22 | """, 23 | """ 24 | UPDATE core_project 25 | SET 26 | file_storage_bytes = file_storage_bytes / 1000 / 1000 27 | """, 28 | ), 29 | migrations.AlterField( 30 | model_name="project", 31 | name="file_storage_bytes", 32 | field=models.PositiveBigIntegerField(default=0), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0063_auto_20230305_1551.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.17 on 2023-03-05 14:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0062_auto_20230216_1137"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="delta", 14 | name="created_at", 15 | field=models.DateTimeField(auto_now_add=True, db_index=True), 16 | ), 17 | migrations.AlterField( 18 | model_name="delta", 19 | name="deltafile_id", 20 | field=models.UUIDField(db_index=True), 21 | ), 22 | migrations.AlterField( 23 | model_name="delta", 24 | name="last_status", 25 | field=models.CharField( 26 | choices=[ 27 | ("pending", "Pending"), 28 | ("started", "Started"), 29 | ("applied", "Applied"), 30 | ("conflict", "Conflict"), 31 | ("not_applied", "Not_applied"), 32 | ("error", "Error"), 33 | ("ignored", "Ignored"), 34 | ("unpermitted", "Unpermitted"), 35 | ], 36 | db_index=True, 37 | default="pending", 38 | max_length=32, 39 | ), 40 | ), 41 | migrations.AlterField( 42 | model_name="delta", 43 | name="updated_at", 44 | field=models.DateTimeField(auto_now=True, db_index=True), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0064_auto_20230328_2017.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-03-28 18:17 2 | 3 | import migrate_sql.operations 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0063_auto_20230305_1551"), 10 | ] 11 | 12 | operations = [ 13 | migrate_sql.operations.ReverseAlterSQL( 14 | name="core_user_email_partial_uniq", 15 | sql="\n DROP INDEX IF EXISTS core_user_email_partial_uniq\n ", 16 | reverse_sql="\n CREATE UNIQUE INDEX IF NOT EXISTS core_user_email_partial_uniq ON core_user (email)\n WHERE type = 1 AND email IS NOT NULL AND email != ''\n ", 17 | ), 18 | migrate_sql.operations.AlterSQL( 19 | name="core_user_email_partial_uniq", 20 | sql="\n CREATE UNIQUE INDEX IF NOT EXISTS core_user_email_partial_uniq ON core_user (UPPER(email))\n WHERE type = 1 AND email IS NOT NULL AND email != ''\n ", 21 | reverse_sql="\n DROP INDEX IF EXISTS core_user_email_partial_uniq\n ", 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0065_auto_20230422_1101.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-22 09:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0064_auto_20230328_2017"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="job", 14 | name="created_at", 15 | field=models.DateTimeField(auto_now_add=True, db_index=True), 16 | ), 17 | migrations.AlterField( 18 | model_name="job", 19 | name="status", 20 | field=models.CharField( 21 | choices=[ 22 | ("pending", "Pending"), 23 | ("queued", "Queued"), 24 | ("started", "Started"), 25 | ("finished", "Finished"), 26 | ("stopped", "Stopped"), 27 | ("failed", "Failed"), 28 | ], 29 | db_index=True, 30 | default="pending", 31 | max_length=32, 32 | ), 33 | ), 34 | migrations.AlterField( 35 | model_name="job", 36 | name="updated_at", 37 | field=models.DateTimeField(auto_now=True, db_index=True), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0066_delta_client_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-09 21:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0065_auto_20230422_1101"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="delta", 14 | name="client_id", 15 | field=models.UUIDField(db_index=True, null=True, editable=False), 16 | ), 17 | migrations.RunSQL( 18 | "UPDATE core_delta SET client_id = COALESCE((content->>'clientId')::uuid, deltafile_id)", 19 | migrations.RunSQL.noop, 20 | ), 21 | migrations.AlterField( 22 | model_name="delta", 23 | name="client_id", 24 | field=models.UUIDField(db_index=True, null=False, editable=False), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0067_auto_20230515_1320.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-15 11:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0066_delta_client_id"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="job", 14 | name="docker_finished_at", 15 | field=models.DateTimeField(blank=True, editable=False, null=True), 16 | ), 17 | migrations.AddField( 18 | model_name="job", 19 | name="docker_started_at", 20 | field=models.DateTimeField(blank=True, editable=False, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0068_job_container_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-05-17 13:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0067_auto_20230515_1320"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="job", 14 | name="container_id", 15 | field=models.CharField( 16 | blank=True, db_index=True, default="", max_length=64 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-06-16 07:58 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0068_job_container_id"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="project", 15 | name="storage_keep_versions", 16 | field=models.PositiveIntegerField( 17 | verbose_name="File versions to keep", 18 | help_text=( 19 | "Use this value to limit the maximum number of file versions. If empty, your current plan's default will be used. Available to Premium users only." 20 | ), 21 | validators=[ 22 | django.core.validators.MinValueValidator(1), 23 | django.core.validators.MaxValueValidator(100), 24 | ], 25 | null=True, 26 | blank=True, 27 | ), 28 | ) 29 | ] 30 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-06-30 08:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0069_auto_20230616_0827"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="project", 14 | name="is_public", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="Projects marked as public are visible to (but not editable by) anyone.", 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0071_auto_20230717_2243.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-07-17 20:43 2 | 3 | import migrate_sql.operations 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("core", "0070_alter_project_is_public"), 10 | ] 11 | 12 | operations = [ 13 | migrate_sql.operations.DeleteSQL( 14 | name="core_delta_geom_insert_trigger", 15 | sql="\n DROP TRIGGER IF EXISTS core_delta_geom_insert_trigger ON core_delta\n ", 16 | reverse_sql="\n CREATE TRIGGER core_delta_geom_insert_trigger BEFORE INSERT ON core_delta\n FOR EACH ROW\n EXECUTE FUNCTION core_delta_geom_trigger_func()\n ", 17 | ), 18 | migrate_sql.operations.CreateSQL( 19 | name="core_delta_geom_insert_trigger", 20 | sql="\n CREATE TRIGGER core_delta_geom_insert_trigger BEFORE INSERT ON core_delta\n FOR EACH ROW\n EXECUTE FUNCTION core_delta_geom_trigger_func()\n ", 21 | reverse_sql="\n DROP TRIGGER IF EXISTS core_delta_geom_insert_trigger ON core_delta\n ", 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0072_person_remaining_trial_organizations.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-07-19 12:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0071_auto_20230717_2243"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="person", 14 | name="remaining_trial_organizations", 15 | field=models.PositiveIntegerField( 16 | default=0, 17 | help_text="Remaining trial organizations the user can create.", 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0073_project_packaging_offliner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.23 on 2024-01-09 22:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0072_person_remaining_trial_organizations"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="packaging_offliner", 15 | field=models.CharField( 16 | choices=[ 17 | ("qgiscore", "QGIS Core Offline Editing (deprecated)"), 18 | ("pythonmini", "Optimized Packager"), 19 | ], 20 | default="qgiscore", 21 | help_text='The Packaging Offliner packages data for offline use with QField. The new "Optimized Packager" should be preferred over the deprecated "QGIS Core Offline Editing" for new projects.', 22 | max_length=100, 23 | verbose_name="Packaging Offliner", 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0076_project_restrict_project_modification.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-05-25 10:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0075_auto_20240323_1419"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="has_restricted_projectfiles", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="Restrict modifications of QGIS/QField projectfiles to managers and administrators.", 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0077_alter_user_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-07-10 14:25 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | import qfieldcloud.core.validators 7 | 8 | 9 | class Migration(migrations.Migration): 10 | dependencies = [ 11 | ("core", "0076_project_restrict_project_modification"), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="user", 17 | name="username", 18 | field=models.CharField( 19 | error_messages={"unique": "A user with that username already exists."}, 20 | help_text="Between 3 and 150 characters. Letters, digits, underscores '_' or hyphens '-' only. Must begin with a letter.", 21 | max_length=150, 22 | unique=True, 23 | validators=[ 24 | django.core.validators.RegexValidator( 25 | "^[-a-zA-Z0-9_]+$", 26 | "Only letters, numbers, underscores '_' or hyphens '-' are allowed.", 27 | ), 28 | django.core.validators.RegexValidator( 29 | "^[a-zA-Z].*$", "Must begin with a letter." 30 | ), 31 | django.core.validators.RegexValidator( 32 | "^.{3,}$", "Must be at least 3 characters long." 33 | ), 34 | qfieldcloud.core.validators.reserved_words_validator, 35 | ], 36 | verbose_name="username", 37 | ), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0078_alter_project_packaging_offliner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-09-03 16:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0077_alter_user_username"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="project", 14 | name="packaging_offliner", 15 | field=models.CharField( 16 | choices=[ 17 | ("qgiscore", "QGIS Core Offline Editing (deprecated)"), 18 | ("pythonmini", "Optimized Packager"), 19 | ], 20 | default="pythonmini", 21 | help_text='The Packaging Offliner packages data for offline use with QField. The new "Optimized Packager" should be preferred over the deprecated "QGIS Core Offline Editing" for new projects.', 22 | max_length=100, 23 | verbose_name="Packaging Offliner", 24 | ), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0080_rename_project_filename_project_the_qgis_file_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2025-01-23 19:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0079_organizationmember_created_at_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="project", 14 | old_name="project_filename", 15 | new_name="the_qgis_file_name", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0082_project_attachments_file_storage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-05-06 14:42 2 | 3 | from django.db import migrations, models 4 | 5 | import qfieldcloud.core.models 6 | import qfieldcloud.core.validators 7 | 8 | 9 | def set_project_attachments_file_storage(apps, schema_editor): 10 | Project = apps.get_model("core", "Project") 11 | 12 | Project.objects.update( 13 | attachments_file_storage=models.F("file_storage"), 14 | ) 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ("core", "0081_file_storage_project_and_more"), 20 | ] 21 | 22 | operations = [ 23 | migrations.AddField( 24 | model_name="project", 25 | name="attachments_file_storage", 26 | field=models.CharField( 27 | default=qfieldcloud.core.models.get_project_file_storage_default, 28 | help_text="Which file storage provider should be used for storing the project attachments files.", 29 | max_length=100, 30 | validators=[qfieldcloud.core.validators.file_storage_name_validator], 31 | verbose_name="Attachments file storage", 32 | ), 33 | ), 34 | migrations.RunPython( 35 | set_project_attachments_file_storage, migrations.RunPython.noop 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0083_project_is_sticky.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-05-25 19:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0082_project_attachments_file_storage"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="is_featured", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="If set to true, the project will always appear on top of the project list, no matter the sorting. If multiple projects are featured, they will be sorted by the user defined sorting.", 18 | verbose_name="Is sticky", 19 | ), 20 | ), 21 | migrations.AlterModelOptions( 22 | name="project", 23 | options={"ordering": ["-is_featured", "owner__username", "name"]}, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/0084_project_is_attachment_download_on_demand.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-05-28 09:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0083_project_is_sticky"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="project", 14 | name="is_attachment_download_on_demand", 15 | field=models.BooleanField( 16 | default=False, 17 | help_text="If enabled, it indicates to the client (e.g. QField) that the attachments may be downloaded on demand.", 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/migrations/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/paginators.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from django.conf import settings 4 | from django.core.paginator import Paginator 5 | from django.db import connection 6 | from django.utils.functional import cached_property 7 | from django.utils.inspect import method_has_no_args 8 | 9 | 10 | class LargeTablePaginator(Paginator): 11 | """ 12 | Only for Postgres: 13 | Overrides the count method to get an estimate instead of actual count when not filtered 14 | inspired by: https://djangosnippets.org/snippets/2855/ 15 | """ 16 | 17 | @cached_property 18 | def count(self): 19 | """ 20 | Returns the total number of objects, across all pages. 21 | Changed to use an estimate if the estimate is greater than QFIELDCLOUD_ADMIN_EXACT_COUNT_LIMIT 22 | """ 23 | c = getattr(self.object_list, "count", None) 24 | if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c): 25 | estimate = 0 26 | if not self.object_list.query.where: 27 | cursor = connection.cursor() 28 | cursor.execute( 29 | "SELECT reltuples::int FROM pg_class WHERE relname = %s", 30 | [self.object_list.query.model._meta.db_table], 31 | ) 32 | estimate = cursor.fetchone()[0] 33 | 34 | if estimate < settings.QFIELDCLOUD_ADMIN_EXACT_COUNT_LIMIT: 35 | return c() 36 | else: 37 | return estimate 38 | 39 | return len(self.object_list) 40 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/permission_check.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable 2 | 3 | from django.http.request import HttpRequest 4 | from django.http.response import HttpResponse, HttpResponseForbidden 5 | 6 | from qfieldcloud.core import permissions_utils 7 | 8 | 9 | def permission_check(perm: str, check_args: list[str | Callable] = []) -> Callable: 10 | perm_check = getattr(permissions_utils, perm) 11 | 12 | def decorator_wrapper(func: Callable): 13 | def decorator(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: 14 | parsed_check_args = [] 15 | 16 | for arg in check_args: 17 | if callable(arg): 18 | parsed_check_args.append(arg(request)) 19 | else: 20 | parsed_check_args.append(getattr(request, arg)) 21 | 22 | is_permitted = perm_check(request.user, *parsed_check_args) 23 | 24 | if is_permitted: 25 | return func(request, *args, **kwargs) 26 | else: 27 | return HttpResponseForbidden() 28 | 29 | return decorator 30 | 31 | return decorator_wrapper 32 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/signals.py: -------------------------------------------------------------------------------- 1 | from axes.signals import user_locked_out 2 | from django.dispatch import receiver 3 | 4 | from qfieldcloud.core.exceptions import TooManyLoginAttemptsError 5 | 6 | 7 | @receiver(user_locked_out) 8 | def raise_permission_denied(*args, **kwargs): 9 | raise TooManyLoginAttemptsError() 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/css/admin.css: -------------------------------------------------------------------------------- 1 | .login-logo img { 2 | width: 100%; 3 | } 4 | 5 | .object-tools{ 6 | margin-bottom: 1rem; 7 | } 8 | 9 | #jazzy-logo img{ 10 | box-shadow: none!important; 11 | } 12 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/css/sso.css: -------------------------------------------------------------------------------- 1 | /* SSO providers buttons */ 2 | .provider-button-parent { 3 | list-style: none; 4 | margin: 0 1em 1em 0; 5 | padding: 0; 6 | } 7 | 8 | .provider-button { 9 | color: grey; 10 | background-color: white; 11 | border: 1px solid grey; 12 | border-radius: .25rem; 13 | padding: .5rem; 14 | } 15 | 16 | .provider-button img { 17 | margin-right: 1em; 18 | width: 50px; 19 | height: 50px; 20 | } 21 | 22 | .provider-button span { 23 | font-size: larger; 24 | margin-bottom: 0; 25 | } 26 | /* /SSO providers buttons */ 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/staticfiles/favicon.ico -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/img/opengis_powering_qfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/staticfiles/img/opengis_powering_qfc.png -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/sso/github-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/sso/github-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/staticfiles/sso/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | {% load i18n static %} 3 | {% load bootstrap4 %} 4 | 5 | {% block title %} 6 | {% trans 'Change Password' %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 | 13 |
14 | {% if token_fail %} 15 | {% trans 'Bad Token' %} 16 | {% else %} 17 | {% trans 'Change Password' %} 18 | {% endif %} 19 |
20 | 21 | {% if token_fail %} 22 | 23 |

24 | {% url 'account_reset_password' as passwd_reset_url %} 25 | {% blocktrans %} 26 | The password reset link was invalid, possibly because it has already been used. Please request a new password reset. 27 | {% endblocktrans %} 28 |

29 | {% else %} 30 | {% if form %} 31 |

32 | {% url 'account_login' as login_url %} 33 | {% blocktrans with login_url=login_url %} 34 | Please enter the newly chosen secure password in the form below.
35 | If you happened to remember the old password, please go back to the sign-in form. 36 | {% endblocktrans %} 37 |

38 |
39 | {% csrf_token %} 40 | 41 | {% bootstrap_form form %} 42 | 43 | 44 |
45 | 46 | {% else %} 47 |

48 | {% trans 'Your password is now changed.' %} 49 |

50 | {% endif %} 51 | 52 | {% endif %} 53 |
54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | {% load i18n static %} 3 | 4 | {% block title %} 5 | {% trans 'Change Password Complete' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |
{% trans 'Change Password' %}
12 |

13 | {% url 'account_login' as login_url %} 14 | {% blocktrans with login_url=login_url %} 15 | The password is now changed. Use the password on the sign-in form. 16 | {% endblocktrans %} 17 |

18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/account/emailaddress/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | {% load i18n admin_list %} 3 | 4 | {% block object-tools-items %} 5 | {{ block.super }} 6 |
  • 7 | {% trans 'CSV export' %} 8 |
  • 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | 3 | {% block title %}{{ title }} | {{ site_title|default:_('QFieldCloud Admin') }}{% endblock %} 4 | 5 | {% block branding %} 6 |

    {{ site_header|default:_('QFieldCloud Admin') }}

    7 | {% endblock %} 8 | 9 | {% block nav-global %}{% endblock %} 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/constance/change_list.html: -------------------------------------------------------------------------------- 1 | {# extends https://github.com/jazzband/django-constance/blob/master/constance/templates/admin/constance/change_list.html #} 2 | {# Add some style for table readability. Mainly copied from bootstrap #} 3 | 4 | {% extends "admin/constance/change_list.html" %} 5 | {% load i18n %} 6 | 7 | {% block extrastyle %} 8 | {{ block.super }} 9 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/delta_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block submit_buttons_bottom %} 5 | {{ block.super }} 6 | 7 |
    8 | 14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_customized.html: -------------------------------------------------------------------------------- 1 | {% if qfc_admin_inline_included != 1 %} 2 | {% include "admin/edit_inline/tabular_extended.html" with qfc_admin_inline_included=1 %} 3 | {% endif %} 4 | 5 | {{ fieldset.opts.bottom_html }} 6 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/edit_inline/tabular_extended.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/edit_inline/tabular.html" %} 2 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/job_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block submit_buttons_bottom %} 5 | {{ block.super }} 6 | 7 | {% if original.type == 'delta_apply' %} 8 |
    9 | 10 | {% trans 'Download deltafile' %} 11 | 12 |
    13 | {% endif %} 14 |
    15 | {% if original %}{% trans 'Re-run job' %} 16 | {% endif %} 17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | 3 | {% block content %} 4 | {{ block.super }} 5 | {% comment %}TODO add a configuration based mechanism to render the social login instead of depending on query param {% endcomment %} 6 | {% if request.GET.sso %} 7 | {% include "socialaccount/snippets/login.html" %} 8 | {% endif %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/password_reset_url.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block content %} 5 |

    6 | {% blocktrans trimmed with username=user.username %} 7 | The following link can be used to reset the password of user 8 | {{ username }}: 9 | {% endblocktrans %} 10 |

    11 |

    {{ url }}

    12 | {% blocktrans trimmed with username=user.username %} 13 | This link can be sent directly to the user (e.g. by email). 14 | It's only usable once, and it expires in 15 | {{ timeout_days }} days. 16 | {% endblocktrans %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/person_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block submit_buttons_bottom %} 5 | {{ block.super }} 6 | 7 |
    8 | {% trans 'Generate reset password URL' %} 9 | | 10 | {% trans 'Owned projects' %} 11 | | 12 | {% trans 'Projects collaborations' %} 13 | | 14 | {% trans 'Owned organizations' %} 15 | | 16 | {% trans 'Organization memberships' %} 17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/admin/project_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block submit_buttons_bottom %} 5 | {{ block.super }} 6 | 7 |
    8 | {% trans 'Project jobs' %} 9 |
    10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/allauth/elements/provider.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |
    5 | 6 | {{ attrs.name }} 7 | 8 | {{ attrs.name }} 9 | 10 | 11 |
    12 |
    13 |
  • 14 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templates/socialaccount/snippets/provider_list.html: -------------------------------------------------------------------------------- 1 | {% load allauth socialaccount %} 2 | {% get_providers as socialaccount_providers %} 3 | 4 | {% load static %} 5 | 6 | 7 | {% if socialaccount_providers %} 8 | {% element provider_list %} 9 | {% for provider in socialaccount_providers %} 10 | {% if provider.id == "openid" %} 11 | {% for brand in provider.get_brands %} 12 | {% provider_login_url provider openid=brand.openid_url process=process as href %} 13 | {% element provider name=brand.name provider_id=provider.id href=href styles=provider.styles%} 14 | {% endelement %} 15 | {% endfor %} 16 | {% endif %} 17 | {% provider_login_url provider process=process scope=scope auth_params=auth_params as href %} 18 | {% element provider name=provider.name provider_id=provider.id href=href styles=provider.styles %} 19 | {% endelement %} 20 | {% endfor %} 21 | {% endelement %} 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/templatetags/filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils import formats 3 | from django.utils.html import avoid_wrapping 4 | from django.utils.translation import gettext as _ 5 | from django.utils.translation import ngettext 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.filter(is_safe=True) 11 | def filesizeformat10(bytes_) -> str: 12 | """ 13 | Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 14 | 102 bytes, etc.). 15 | 16 | Unlike Django's `filesizeformat` which uses powers of 2 (e.g. 1024KB==1MB), `filesizeformat10` uses powers of 10 (e.g. 1000KB==1MB) 17 | """ 18 | try: 19 | bytes_ = int(bytes_) 20 | except (TypeError, ValueError, UnicodeDecodeError): 21 | value = ngettext("%(size)d byte", "%(size)d bytes", 0) % {"size": 0} 22 | return avoid_wrapping(value) 23 | 24 | def filesize_number_format(value): 25 | return formats.number_format(round(value, 1), 1) 26 | 27 | KB = 10**3 28 | MB = 10**6 29 | GB = 10**9 30 | TB = 10**12 31 | PB = 10**15 32 | 33 | negative = bytes_ < 0 34 | if negative: 35 | bytes_ = -bytes_ # Allow formatting of negative numbers. 36 | 37 | if bytes_ < KB: 38 | value = ngettext("%(size)d byte", "%(size)d bytes", bytes_) % {"size": bytes_} 39 | elif bytes_ < MB: 40 | value = _("%s KB") % filesize_number_format(bytes_ / KB) 41 | elif bytes_ < GB: 42 | value = _("%s MB") % filesize_number_format(bytes_ / MB) 43 | elif bytes_ < TB: 44 | value = _("%s GB") % filesize_number_format(bytes_ / GB) 45 | elif bytes_ < PB: 46 | value = _("%s TB") % filesize_number_format(bytes_ / TB) 47 | else: 48 | value = _("%s PB") % filesize_number_format(bytes_ / PB) 49 | 50 | if negative: 51 | value = "-%s" % value 52 | 53 | return avoid_wrapping(value) 54 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/test_geodb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import psycopg2 5 | from django.conf import settings 6 | 7 | from qfieldcloud.core.models import Geodb, Person 8 | 9 | from .utils import setup_subscription_plans 10 | 11 | logging.disable(logging.CRITICAL) 12 | 13 | 14 | class QfcTestCase(unittest.TestCase): 15 | def setUp(self): 16 | setup_subscription_plans() 17 | 18 | def test_create_db(self): 19 | # Create a user 20 | user1 = Person.objects.create_user( 21 | username="user1", password="abc123", email="user1@pizza.it" 22 | ) 23 | 24 | # Create a geodb object 25 | 26 | geodb = Geodb.objects.create( 27 | user=user1, 28 | hostname=settings.GEODB_HOST, 29 | port=settings.GEODB_PORT, 30 | ) 31 | 32 | conn = psycopg2.connect( 33 | dbname=geodb.dbname, 34 | user=geodb.username, 35 | password=geodb.password, 36 | host=geodb.hostname, 37 | port=geodb.port, 38 | ) 39 | 40 | cur = conn.cursor() 41 | 42 | cur.execute( 43 | """ 44 | CREATE TABLE pizza ( 45 | code char(5) CONSTRAINT firstkey PRIMARY KEY, 46 | title varchar(40) NOT NULL 47 | ); 48 | """ 49 | ) 50 | 51 | conn.commit() 52 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/test_username_generation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from django.http import HttpRequest 5 | from django.test.testcases import TransactionTestCase 6 | 7 | from qfieldcloud.core.adapters import AccountAdapter 8 | from qfieldcloud.core.models import Person 9 | from qfieldcloud.core.tests.utils import setup_subscription_plans 10 | 11 | logging.disable(logging.CRITICAL) 12 | 13 | 14 | class QfcTestCase(TransactionTestCase): 15 | def setUp(self): 16 | setup_subscription_plans() 17 | 18 | def test_generated_usernames_are_normalized(self): 19 | random.seed(42) 20 | expectations = [ 21 | # Bases username on localpart of email plus random 4-digit suffix 22 | ("john@example.org", "john2824"), 23 | # Letters are lowercased 24 | ("JOHN@example.org", "john9928"), 25 | # Non-ASCII characters are transliterated 26 | ("fööbär@example.org", "foobar1711"), 27 | # Special characters are stripped 28 | ("john.doe@example.org", "johndoe8428"), 29 | # Almomst all of them... 30 | ("john.+*?%$/doe@example.org", "johndoe6168"), 31 | # Except underscores and dashes, which are preserved 32 | ("john-peter_doe@example.org", "john-peter_doe7543"), 33 | ] 34 | 35 | adapter = AccountAdapter() 36 | user = Person.objects.create_user(username="temp", password="abc123") 37 | 38 | for email, expected in expectations: 39 | user.username = "" 40 | user.email = email 41 | adapter.populate_username(HttpRequest(), user) 42 | self.assertEqual(user.username, expected) 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/test_username_uniqueness.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | 4 | from django.http import HttpRequest 5 | from django.test.testcases import TransactionTestCase 6 | 7 | from qfieldcloud.core.adapters import AccountAdapter 8 | from qfieldcloud.core.models import Person 9 | from qfieldcloud.core.tests.utils import setup_subscription_plans 10 | 11 | logging.disable(logging.CRITICAL) 12 | 13 | 14 | class QfcTestCase(TransactionTestCase): 15 | def setUp(self): 16 | setup_subscription_plans() 17 | self.existing_user = Person.objects.create_user( 18 | username="existing2824", password="abc123" 19 | ) 20 | 21 | def test_generated_usernames_avoid_collisions(self): 22 | random.seed(42) 23 | expectations = [ 24 | # Collisions with existing usernames are avoided 25 | ("existing@example.org", "existing28240"), 26 | ] 27 | 28 | adapter = AccountAdapter() 29 | user = Person.objects.create_user(username="temp", password="abc123") 30 | 31 | for email, expected in expectations: 32 | user.username = "" 33 | user.email = email 34 | adapter.populate_username(HttpRequest(), user) 35 | self.assertEqual(user.username, expected) 36 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/DCIM/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/DCIM/1.jpg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/DCIM/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/DCIM/2.jpg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/bumblebees.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/bumblebees.gpkg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p1_c1_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1000", 8 | "sourcePk": "", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "create", 12 | "new": { 13 | "attributes": { 14 | "fid": 1000, 15 | "int": 1000 16 | } 17 | } 18 | } 19 | ], 20 | "files": [], 21 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 22 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 23 | "version": "1.0" 24 | } 25 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p2_c2_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "608bbfb7-fb9c-49c4-818f-f636ee4ec20a", 5 | "clientId": "7cdbf420-6872-40c6-ad79-1423c6e5bd2a", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "2000", 8 | "sourcePk": "", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "create", 12 | "new": { 13 | "attributes": { 14 | "fid": 2000, 15 | "int": 2000 16 | } 17 | } 18 | } 19 | ], 20 | "files": [], 21 | "id": "afe134df-6b24-4d7c-af78-110b2614f20f", 22 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 23 | "version": "1.0" 24 | } 25 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p3_c1_patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "f11603e5-13b2-43a9-b27a-db722297773b", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1000", 8 | "sourcePk": "0", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 1001 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1000 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "ac625277-e7dd-4f74-a3db-dddd3f9ce831", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p4_c2_patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "582de6de-562f-4482-9350-5b5aaa25d822", 5 | "clientId": "7cdbf420-6872-40c6-ad79-1423c6e5bd2a", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "2000", 8 | "sourcePk": "0", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 2002 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 2000 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "4758ee40-cb14-4438-9668-f3e34076b987", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p5_c1_delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "b7a09a1d-9626-4da0-8456-61c2ff884611", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1000", 8 | "sourcePk": "0", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "delete", 12 | "old": { 13 | "attributes": { 14 | "int": 1001 15 | } 16 | } 17 | } 18 | ], 19 | "files": [], 20 | "id": "a5ec4bbd-3439-4827-a29b-cf1fe3f244cf", 21 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 22 | "version": "1.0" 23 | } 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/multistage_p6_c2_delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "7cb988e9-5de2-4bd7-af4b-c1d27a2d579f", 5 | "clientId": "7cdbf420-6872-40c6-ad79-1423c6e5bd2a", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "2000", 8 | "sourcePk": "0", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "delete", 12 | "old": { 13 | "attributes": { 14 | "int": 2002 15 | } 16 | } 17 | } 18 | ], 19 | "files": [], 20 | "id": "38482012-ad3f-459f-ad36-1bef526f75dc", 21 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 22 | "version": "1.0" 23 | } 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/nonspatial_geom_empty_str.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "1270b97d-6a28-49cc-83f3-b827ec574fee", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "nonspatial_5b9be5d0_6faa_4c14_825d_7889615c842c", 10 | "sourceLayerId": "nonspatial_5b9be5d0_6faa_4c14_825d_7889615c842c", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "", 14 | "attributes": { 15 | "col1": "new_value" 16 | } 17 | }, 18 | "old": { 19 | "geometry": null, 20 | "attributes": { 21 | "col1": "foo" 22 | } 23 | } 24 | } 25 | ], 26 | "files": [], 27 | "id": "2c98410f-e1b2-45e6-9026-97898bfee730", 28 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 29 | "version": "1.0" 30 | } 31 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/not_schema_valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-deltas-here": [ 3 | { 4 | "uuid": "cfed67a6-326c-4807-897b-57da7af7d33b", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "pippo", 10 | "sourceLayerId": "pluto", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "7c77388e-f902-43b9-8016-4e44c5394f66", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xy_for_xyz_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 10 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINT (666 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZ (1 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 26 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINT (777 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xy_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINT (666 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINT (777 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyz_for_xyz_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 10 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZ (666 0 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZ (1 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 26 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZ (777 0 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyz_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZ (666 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZ (777 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyz_nan_for_xyz_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 10 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZ (666 0 nan)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZ (1 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 26 | "sourceLayerId": "points_xyz_ff574332_1ff5_47e6_8d6a_15f68e0c7cd1", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZ (777 0 nan)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyz_nan_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZ (666 0 nan)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZ (777 0 nan)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyzm_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZM (666 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZM (777 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyzm_nan_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZM (666 0 nan 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZM (777 0 nan 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_delta_with_xyzm_nannan_for_xyzm_layer.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 10 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINTZM (666 0 nan nan)" 14 | }, 15 | "old": { 16 | "geometry": "POINTZM (1 0 0 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 26 | "sourceLayerId": "points_xyzm_1cb6363a_5a99_4090_aeaf_d88deec2a3d8", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINTZM (777 0 nan nan)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_multidelta_patch_create.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "736bf2c2-646a-41a2-8c55-28c26aecd68d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "geometry": "POINT (666 0)" 14 | }, 15 | "old": { 16 | "geometry": "POINT (1 0)" 17 | } 18 | }, 19 | { 20 | "uuid": "8adac0df-e1d3-473e-b150-f8c4a91b4781", 21 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 22 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 23 | "localPk": "2", 24 | "sourcePk": "", 25 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 26 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 27 | "method": "create", 28 | "new": { 29 | "geometry": "POINT (777 0)", 30 | "attributes": { 31 | "dbl": 0.666, 32 | "int": 666, 33 | "str": "str666" 34 | } 35 | } 36 | } 37 | ], 38 | "files": [], 39 | "id": "b7c17bc6-e5af-4fa7-b905-4b395729d782", 40 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 41 | "version": "1.0" 42 | } 43 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta2.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "c8c421cd-e39c-40a0-97d8-a319c245ba14", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "469e13a0-5a97-40cd-b39d-fa0368339caa", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta3.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "e4546ec2-6e01-43a1-ab30-a52db9469afd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "ab3e55a2-98cc-4c03-8069-8266fefd8124", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta4.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "802ae2ef-f360-440e-a816-8990d6a06667", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "4d027a9d-d31a-4e8f-acad-2f2d59caa48c", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta5.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "ad98634e-509f-4dff-9000-de79b09c5359", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "3aab7e58-ea27-4b7c-9bca-c772b6d94820", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta6.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "df6a19eb-7d61-4c64-9e3b-29bce0a8dfab", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "17f45c15-130a-412f-b031-dec792920458", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_conflict.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "8d185b67-f05e-40c6-9c9a-6ceca8100c39", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 134 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "816c0b2d-66b3-4ea5-b21c-5eb77a571b19", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_conflict2.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "bd507a3d-aa7b-42c4-bdb7-23ff34f65d5c", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 134 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "a27c06e6-afd8-4610-992b-bb4f27e8b27a", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_diff_content.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "e5b1ad46-e33f-4421-af90-a5172a64a77d", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 667 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_empty_source_layer_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_null.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "str": "\u0000" 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "str": "str1" 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/singlelayer_singledelta_project2.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "f2af4942-e4ab-446e-bd97-5aab17e7ccc1", 5 | "clientId": "e6bafe63-7d79-4970-b49b-5d4f13e6344b", 6 | "exportId": "d2706728-bf75-4220-b76e-e1f36aea31f5", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 10 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "dcfd00b1-e156-4a25-aa1c-a72584636e28", 26 | "project": "2f221069-59f6-40d2-b7d6-0f454380c2ed", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/deltas/with_errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "65b605b4-9832-4de0-9055-92e1dd94ebec", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1", 8 | "sourcePk": "1", 9 | "localLayerId": "pippo", 10 | "sourceLayerId": "pluto", 11 | "method": "patch", 12 | "new": { 13 | "attributes": { 14 | "int": 666 15 | } 16 | }, 17 | "old": { 18 | "attributes": { 19 | "int": 1 20 | } 21 | } 22 | } 23 | ], 24 | "files": [], 25 | "id": "7c77388e-f902-43b9-8016-4e44c5394f66", 26 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 27 | "version": "1.0" 28 | } 29 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/nonspatial.csv: -------------------------------------------------------------------------------- 1 | fid,col1 2 | "1",foo 3 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/points.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "points", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "id": 1, "properties": { "int": 1, "dbl": 0.1, "str": "str1", "fid": 1 }, "geometry": { "type": "Point", "coordinates": [ 1.0, 0.0 ] } }, 7 | { "type": "Feature", "id": 2, "properties": { "int": 2, "dbl": 0.2, "str": "str2", "fid": 2 }, "geometry": { "type": "Point", "coordinates": [ 5.0, 0.0 ] } }, 8 | { "type": "Feature", "id": 3, "properties": { "int": 3, "dbl": 0.3, "str": "str3", "fid": 3 }, "geometry": { "type": "Point", "coordinates": [ 9.0, 0.0 ] } } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/polygons.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "polygons", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "id": 8, "properties": { "int": 8, "dbl": 0.8, "str": "str8", "fid": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 5.0, 2.0 ], [ 5.0, 5.0 ], [ 2.0, 5.0 ], [ 2.0, 2.0 ] ] ] } }, 7 | { "type": "Feature", "id": 9, "properties": { "int": 9, "dbl": 0.9, "str": "str9", "fid": 1 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.0, 2.0 ], [ 9.0, 2.0 ], [ 9.0, 5.0 ], [ 6.0, 5.0 ], [ 6.0, 2.0 ] ] ] } } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/delta/testdata.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/delta/testdata.gpkg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/file.txt: -------------------------------------------------------------------------------- 1 | Hello, World 2 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/file2.txt: -------------------------------------------------------------------------------- 1 | Second file! 2 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/simple_bee_farming/real_files/bees.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/simple_bee_farming/real_files/bees.gpkg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/tests/testdata/simple_bee_farming/real_files/bumblebees.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/core/tests/testdata/simple_bee_farming/real_files/bumblebees.gpkg -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/utils2/__init__.py: -------------------------------------------------------------------------------- 1 | import qfieldcloud.core.utils2.audit as audit 2 | import qfieldcloud.core.utils2.jobs as jobs 3 | import qfieldcloud.core.utils2.storage as storage 4 | 5 | __all__ = ["audit", "jobs", "storage"] 6 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/utils2/audit.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from auditlog.models import LogEntry 4 | from django.contrib.auth.models import AnonymousUser, User 5 | from django_currentuser.middleware import get_current_authenticated_user 6 | 7 | 8 | def audit( 9 | instance, 10 | action: LogEntry.Action, 11 | changes: dict[str, Any] | list[Any] | str | None = None, 12 | actor: User | None = None, 13 | remote_addr: str | None = None, 14 | additional_data: Any | None = None, 15 | ): 16 | if actor is None: 17 | actor = get_current_authenticated_user() 18 | elif isinstance(actor, AnonymousUser): 19 | actor = None 20 | 21 | actor_id = actor.pk if actor else None 22 | 23 | return LogEntry.objects.log_create( 24 | instance, 25 | action=action, 26 | changes=changes, 27 | actor_id=actor_id, 28 | remote_addr=remote_addr, 29 | additional_data=additional_data, 30 | ) 31 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/utils2/delta_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable 2 | from uuid import UUID 3 | 4 | 5 | def generate_deltafile( 6 | deltas: Iterable[dict[str, Any]], 7 | project_id: UUID, 8 | id: UUID = UUID("111111111-1111-1111-1111-11111111111"), 9 | ) -> dict[str, Any]: 10 | """Returns a deltafile-structured dictionary with the given deltas. 11 | The given deltas must be an iterable with at least one element. 12 | 13 | Args: 14 | deltas: deltas in the deltafile 15 | project_id: project ID 16 | id: deltafile UUID 17 | """ 18 | deltas = list(deltas) 19 | deltafile = { 20 | "deltas": deltas, 21 | "files": [], 22 | "id": id, 23 | "project": project_id, 24 | "version": "1.0", 25 | } 26 | 27 | return deltafile 28 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/utils2/pg_service_file.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import io 3 | 4 | from django.core.exceptions import ValidationError 5 | from django.utils.translation import gettext as _ 6 | 7 | PGSERVICE_SECRET_NAME_PREFIX = "PG_SERVICE_" 8 | 9 | 10 | def validate_pg_service_conf(value: str) -> None: 11 | """Checks if a string is a valid `pg_service.conf` file contents, otherwise throws a `ValueError`""" 12 | try: 13 | buffer = io.StringIO(value) 14 | config = configparser.ConfigParser() 15 | config.read_file(buffer) 16 | 17 | if len(config.sections()) != 1: 18 | raise ValidationError( 19 | _("The `.pg_service.conf` must have exactly one service definition.") 20 | ) 21 | except ValidationError as err: 22 | raise err 23 | except Exception: 24 | raise ValidationError(_("Failed to parse the `.pg_service.conf` file.")) 25 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/validators.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ValidationError 3 | from django.core.validators import BaseValidator 4 | from django.utils.deconstruct import deconstructible 5 | from django.utils.translation import gettext as _ 6 | from django.utils.translation import ngettext_lazy 7 | 8 | 9 | def reserved_words_validator(value): 10 | reserved_words = [ 11 | "user", 12 | "users", 13 | "project", 14 | "projects", 15 | "owner", 16 | "push", 17 | "file", 18 | "files", 19 | "collaborator", 20 | "collaborators", 21 | "member", 22 | "members", 23 | "organization", 24 | "qfield", 25 | "qfieldcloud", 26 | "history", 27 | "version", 28 | "delta", 29 | "deltas", 30 | "deltafile", 31 | "auth", 32 | "qfield-files", 33 | "esri", 34 | ] 35 | if value.lower() in reserved_words: 36 | raise ValidationError(_('"{}" is a reserved word!').format(value)) 37 | 38 | 39 | def file_storage_name_validator(value): 40 | if value not in settings.STORAGES: 41 | raise ValidationError(_("Storage {} is not a valid option!".format(value))) 42 | 43 | 44 | @deconstructible 45 | class MaxBytesLengthValidator(BaseValidator): 46 | message = ngettext_lazy( 47 | "Ensure this value has at most %(limit_value)d byte (it has " 48 | "%(show_value)d).", 49 | "Ensure this value has at most %(limit_value)d bytes (it has " 50 | "%(show_value)d).", 51 | "limit_value", 52 | ) 53 | code = "max_length" 54 | 55 | def compare(self, a, b): 56 | return a > b 57 | 58 | def clean(self, x): 59 | return len(x.encode("utf-8")) 60 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/views/redirect_views.py: -------------------------------------------------------------------------------- 1 | from django.http.request import HttpRequest 2 | from django.http.response import ( 3 | Http404, 4 | HttpResponsePermanentRedirect, 5 | HttpResponseRedirect, 6 | ) 7 | from django.shortcuts import redirect 8 | from django.urls import reverse 9 | from qfieldcloud.core.models import Project 10 | 11 | 12 | def redirect_to_admin_project_view( 13 | request: HttpRequest, username: str, project_name: str 14 | ) -> HttpResponseRedirect | HttpResponsePermanentRedirect: 15 | try: 16 | project = Project.objects.only("id").get( 17 | name=project_name, 18 | owner__username=username, 19 | ) 20 | except Exception: 21 | raise Http404() 22 | 23 | return redirect(reverse("admin:core_project_change", args=(project.id,))) 24 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/core/views/status_views.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from drf_spectacular.utils import extend_schema, extend_schema_view 3 | from qfieldcloud.core import geodb_utils, utils 4 | from rest_framework import status, views 5 | from rest_framework.permissions import AllowAny 6 | from rest_framework.response import Response 7 | 8 | 9 | @extend_schema_view( 10 | get=extend_schema(description="Get the current status of the API"), 11 | ) 12 | class APIStatusView(views.APIView): 13 | permission_classes = [AllowAny] 14 | 15 | def get(self, request): 16 | # Try to get the status from the cache 17 | results = cache.get("status_results", {}) 18 | if not results: 19 | results["geodb"] = "ok" 20 | # Check geodb 21 | if not geodb_utils.geodb_is_running(): 22 | results["geodb"] = "error" 23 | 24 | results["storage"] = "ok" 25 | # Check if bucket exists (i.e. the connection works) 26 | try: 27 | utils.get_s3_bucket() 28 | except Exception: 29 | results["storage"] = "error" 30 | 31 | # Cache the result for 10 minutes 32 | cache.set("status_results", results, 600) 33 | 34 | return Response(results, status=status.HTTP_200_OK) 35 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/filestorage/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import File, FileVersion 4 | 5 | 6 | class FileAdmin(admin.ModelAdmin): 7 | list_display = [ 8 | "id", 9 | "project", 10 | "name", 11 | "latest_version", 12 | "latest_version_count", 13 | "uploaded_at", 14 | "uploaded_by", 15 | ] 16 | 17 | list_display_links = [ 18 | "project", 19 | "latest_version", 20 | "uploaded_by", 21 | ] 22 | 23 | 24 | class FileVersionAdmin(admin.ModelAdmin): 25 | list_display = [ 26 | "id", 27 | "file", 28 | "md5sum", 29 | "size", 30 | "uploaded_at", 31 | "uploaded_by", 32 | ] 33 | list_display_links = [ 34 | "file", 35 | "uploaded_by", 36 | ] 37 | 38 | 39 | admin.site.register(File, FileAdmin) 40 | admin.site.register(FileVersion, FileVersionAdmin) 41 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FileStorageConfig(AppConfig): 5 | name = "qfieldcloud.filestorage" 6 | verbose_name = "FileStorage" 7 | 8 | def ready(self): 9 | from . import signals # noqa 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import transaction 4 | 5 | from qfieldcloud.core.models import Project 6 | 7 | from .models import FileVersion 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def purge_old_file_versions(project: Project) -> None: 13 | """ 14 | Deletes old versions of all files in the given project. Will keep __3__ 15 | versions for COMMUNITY user accounts, and __10__ versions for PRO user 16 | accounts 17 | """ 18 | 19 | keep_count = project.owner_aware_storage_keep_versions 20 | 21 | logger.info(f"Cleaning up old files for {project} to {keep_count} versions") 22 | 23 | versions_to_delete_ids = [] 24 | versions_to_delete_size = 0 25 | 26 | for file in project.files.all(): 27 | versions_to_delete = file.versions.order_by("-created_at")[keep_count:] 28 | 29 | if not versions_to_delete: 30 | continue 31 | 32 | for file_version in versions_to_delete: 33 | versions_to_delete_ids.append(file_version.id) 34 | versions_to_delete_size += file_version.size 35 | 36 | if not versions_to_delete_ids: 37 | return 38 | 39 | with transaction.atomic(): 40 | FileVersion.objects.filter(id__in=versions_to_delete_ids).delete() 41 | 42 | project = Project.objects.select_for_update().get(id=project.id) 43 | project.file_storage_bytes -= versions_to_delete_size 44 | project.save(update_fields=["file_storage_bytes"]) 45 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/migrations/0003_alter_file_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-05-23 12:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0082_project_attachments_file_storage"), 9 | ("filestorage", "0002_file_file_storage"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name="file", 15 | unique_together={("project", "name", "file_type", "package_job")}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/migrations/0004_alter_fileversion_size.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-05-30 11:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("filestorage", "0003_alter_file_unique_together"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="fileversion", 14 | name="size", 15 | field=models.PositiveBigIntegerField(editable=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/filestorage/migrations/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import pre_delete 2 | from django.dispatch import receiver 3 | 4 | from .models import File, FileVersion 5 | 6 | 7 | @receiver(pre_delete, sender=FileVersion) 8 | def pre_delete_file_version(sender, instance, origin, **kwargs): 9 | """Responsible for setting the `latest_version` of the parent to the correct version.""" 10 | # Do nothing if we are deleting the whole `File` object, not only a single `FileVersion` 11 | if isinstance(origin, File): 12 | return 13 | 14 | file_version = instance 15 | 16 | # Do nothing if the file_version is not the latest version 17 | if file_version.file.latest_version != file_version: 18 | return 19 | 20 | file_version.file.latest_version = file_version.previous_version 21 | file_version.file.save(update_fields=["latest_version"]) 22 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/filestorage/tests/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/filestorage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ( 4 | AvatarFileReadView, 5 | compatibility_file_crud_view, 6 | compatibility_file_list_view, 7 | compatibility_project_meta_file_read_view, 8 | ) 9 | 10 | urlpatterns = [ 11 | path( 12 | "files//", 13 | compatibility_file_list_view, 14 | name="filestorage_list_files", 15 | ), 16 | path( 17 | "files///", 18 | compatibility_file_crud_view, 19 | name="filestorage_crud_file", 20 | ), 21 | path( 22 | "files/thumbnails//", 23 | compatibility_project_meta_file_read_view, 24 | name="filestorage_project_thumbnails", 25 | ), 26 | path( 27 | "files/avatars//", 28 | AvatarFileReadView.as_view(), 29 | name="filestorage_avatars", 30 | ), 31 | # NOTE The sole purpose of this URL is to keep backwards compatibility with QField/QFieldSync. They expect the URL path to end with filename with file extension. 32 | path( 33 | "files/avatars//", 34 | AvatarFileReadView.as_view(), 35 | name="filestorage_named_avatars", 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/notifs/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from notifications.admin import NotificationAdmin as NotificationAdminBase 3 | from notifications.models import Notification 4 | 5 | admin.site.unregister(Notification) 6 | 7 | 8 | class NotificationAdmin(NotificationAdminBase): 9 | list_display = ( 10 | "actor", 11 | "verb", 12 | "action_object", 13 | "target", 14 | "timestamp", 15 | "recipient", 16 | "unread", 17 | ) 18 | 19 | list_select_related = ("recipient",) 20 | 21 | list_per_page = 10 22 | 23 | search_fields = ("recipient__username__icontains",) 24 | 25 | 26 | admin.site.register(Notification, NotificationAdmin) 27 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class NotifsConfig(AppConfig): 5 | name = "qfieldcloud.notifs" 6 | verbose_name = "Notifs" 7 | 8 | def ready(self): 9 | from . import signals # noqa 10 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/templates/notifs/notification_email_body.html: -------------------------------------------------------------------------------- 1 |

    Here is what happened recently on QFieldCloud.

    2 | 3 | {% block body %} 4 |
      5 | {% for notif in notifs %} 6 |
    • {{notif}}
    • 7 | {% endfor %} 8 |
    9 | {% endblock body %} 10 | 11 | {% block footer %} 12 | {% comment %}You should include an unsubscribe link here in your overrides. You can use {{hostname}} and {{username}}.{% endcomment %} 13 | {% endblock footer %} 14 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/templates/notifs/notification_email_body.txt: -------------------------------------------------------------------------------- 1 | Here is what happened recently on QFieldCloud: 2 | 3 | {% block body %} 4 | {% for notif in notifs %} 5 | - {{notif}} 6 | {% endfor %} 7 | {% endblock body %} 8 | 9 | {% block footer %} 10 | {% comment %}You should include an unsubscribe link here in your overrides. You can use {{hostname}} and {{username}}.{% endcomment %} 11 | {% endblock footer %} 12 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/templates/notifs/notification_email_subject.txt: -------------------------------------------------------------------------------- 1 | New activity on QFieldCloud 2 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/notifs/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/notifs/tests/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/subscription/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "qfieldcloud.subscription" 6 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/subscription/migrations/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/sql_config.py: -------------------------------------------------------------------------------- 1 | from migrate_sql.config import SQLItem 2 | 3 | sql_items = [ 4 | SQLItem( 5 | "subscription_subscription_prevent_overlaps_idx", 6 | r""" 7 | ALTER TABLE subscription_subscription 8 | ADD CONSTRAINT subscription_subscription_prevent_overlaps 9 | EXCLUDE USING gist ( 10 | account_id WITH =, 11 | tstzrange(active_since, active_until) WITH && 12 | ) 13 | WHERE (active_since IS NOT NULL) 14 | """, 15 | r""" 16 | ALTER TABLE subscription_subscription DROP CONSTRAINT subscription_subscription_prevent_overlaps 17 | """, 18 | ), 19 | SQLItem( 20 | "subscription_package_prevent_overlaps_idx", 21 | r""" 22 | ALTER TABLE subscription_package 23 | ADD CONSTRAINT subscription_package_prevent_overlaps 24 | EXCLUDE USING gist ( 25 | subscription_id WITH =, 26 | tstzrange(active_since, active_until) WITH && 27 | ) 28 | WHERE (active_since IS NOT NULL) 29 | """, 30 | r""" 31 | ALTER TABLE subscription_package DROP CONSTRAINT subscription_package_prevent_overlaps 32 | """, 33 | ), 34 | SQLItem( 35 | "current_subscriptions_vw", 36 | r""" 37 | CREATE VIEW current_subscriptions_vw AS 38 | SELECT 39 | * 40 | FROM 41 | subscription_subscription 42 | WHERE 43 | active_since < now() 44 | AND ( 45 | active_until IS NULL 46 | OR active_until > now() 47 | ) 48 | """, 49 | r""" 50 | DROP VIEW current_subscriptions_vw 51 | """, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/templates/admin/subscription_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n %} 3 | 4 | {% block form_top %} 5 | {% if change %} 6 | NOTE: For changing Plan create a new Subscription. First cancel the current subscription by setting "Active until". 7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-app/qfieldcloud/subscription/tests/__init__.py -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/tests/data/project_pgservice_delta_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1000", 8 | "sourcePk": "", 9 | "localLayerId": "point_6ed9e32d_b677_466f_ae6b_c86d48412d22", 10 | "sourceLayerId": "point_6ed9e32d_b677_466f_ae6b_c86d48412d22", 11 | "method": "create", 12 | "new": { 13 | "attributes": { 14 | "id": 1000, 15 | "name": "hello-1000" 16 | } 17 | } 18 | } 19 | ], 20 | "files": [], 21 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 22 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 23 | "version": "1.0" 24 | } 25 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/subscription/tests/data/project_pgservice_delta_2.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "f15db6cf-1ed7-4a8c-9728-e13d4d96bdb5", 5 | "clientId": "cd517e24-a520-4021-8850-e5af70e3a612", 6 | "exportId": "f70c7286-fcec-4dbe-85b5-63d4735dac47", 7 | "localPk": "1001", 8 | "sourcePk": "", 9 | "localLayerId": "point_6ed9e32d_b677_466f_ae6b_c86d48412d22", 10 | "sourceLayerId": "point_6ed9e32d_b677_466f_ae6b_c86d48412d22", 11 | "method": "create", 12 | "new": { 13 | "attributes": { 14 | "id": 1001, 15 | "name": "hello-1001" 16 | } 17 | } 18 | } 19 | ], 20 | "files": [], 21 | "id": "c903cde7-f075-41ce-9c66-63e2b59e571a", 22 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 23 | "version": "1.0" 24 | } 25 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/testing.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test.runner import DiscoverRunner 3 | 4 | 5 | class QfcTestSuiteRunner(DiscoverRunner): 6 | def __init__(self, *args, **kwargs): 7 | settings.IN_TEST_SUITE = True 8 | super().__init__(*args, **kwargs) 9 | -------------------------------------------------------------------------------- /docker-app/qfieldcloud/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for qfieldcloud project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "qfieldcloud.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements.in: -------------------------------------------------------------------------------- 1 | boto3-stubs==1.35.90 2 | boto3==1.35.90 3 | deprecated==1.2.18 4 | django-allauth[socialaccount]==65.4.1 5 | django-auditlog==3.0.0 6 | django-axes==7.0.2 7 | django-bootstrap4==24.4 8 | django-classy-tags==4.1.0 9 | django-cleanup==9.0.0 10 | django-common-helpers==0.9.2 11 | django-constance[database]==4.3.2 12 | django-countries==7.6.1 13 | django-cron==0.6.0 14 | django-cryptography==1.1 15 | django-currentuser==0.8.0 16 | django-debug-toolbar==5.0.1 17 | django-extensions==3.2.3 18 | django-filter==25.1 19 | django-invitations==2.1.0 20 | django-jazzmin==3.0.1 21 | django-log-request-id==2.1.0 22 | django-migrate-sql-3==3.0.2 23 | django-model-utils==5.0.0 24 | django-nonrelated-inlines==0.2 25 | django-notifications-hq==1.8.3 26 | django-phonenumber-field==8.0.0 27 | django-sri==0.8.0 28 | django-storages==1.14.5 29 | django-tables2==2.7.5 30 | django-timezone-field==7.1 31 | django==4.2.19 32 | djangorestframework==3.15.2 33 | drf-spectacular==0.28.0 34 | json-log-formatter==1.1 35 | mypy-boto3-s3==1.35.81 36 | phonenumbers==8.13.55 37 | Pillow==11.1.0 38 | psycopg2==2.9.10 39 | pymemcache==4.0.0 40 | sentry-sdk==1.24.0 41 | stripe==4.2.0 42 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_runtime.in: -------------------------------------------------------------------------------- 1 | gunicorn==22.0.0 2 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_runtime.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=/requirements/requirements_runtime.txt /requirements/requirements_runtime.in 6 | # 7 | gunicorn==22.0.0 8 | # via -r /requirements/requirements_runtime.in 9 | packaging==24.1 10 | # via gunicorn 11 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_test.in: -------------------------------------------------------------------------------- 1 | fiona==1.* 2 | imageio==2.* 3 | requests-mock==1.* 4 | selenium==4.6.* 5 | shapely==2.* 6 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=/requirements/requirements_test.txt /requirements/requirements_test.in 6 | # 7 | attrs==24.2.0 8 | # via 9 | # fiona 10 | # outcome 11 | # trio 12 | certifi==2024.8.30 13 | # via 14 | # fiona 15 | # requests 16 | # selenium 17 | charset-normalizer==3.4.1 18 | # via requests 19 | click==8.1.7 20 | # via 21 | # click-plugins 22 | # cligj 23 | # fiona 24 | click-plugins==1.1.1 25 | # via fiona 26 | cligj==0.7.2 27 | # via fiona 28 | exceptiongroup==1.2.2 29 | # via 30 | # trio 31 | # trio-websocket 32 | fiona==1.9.6 33 | # via -r /requirements/requirements_test.in 34 | h11==0.14.0 35 | # via wsproto 36 | idna==3.8 37 | # via 38 | # requests 39 | # trio 40 | imageio==2.35.1 41 | # via -r /requirements/requirements_test.in 42 | numpy==2.1.0 43 | # via 44 | # imageio 45 | # shapely 46 | outcome==1.3.0.post0 47 | # via trio 48 | pillow==10.4.0 49 | # via imageio 50 | pysocks==1.7.1 51 | # via urllib3 52 | requests==2.32.3 53 | # via requests-mock 54 | requests-mock==1.12.1 55 | # via -r /requirements/requirements_test.in 56 | selenium==4.6.1 57 | # via -r /requirements/requirements_test.in 58 | shapely==2.0.6 59 | # via -r /requirements/requirements_test.in 60 | six==1.16.0 61 | # via fiona 62 | sniffio==1.3.1 63 | # via trio 64 | sortedcontainers==2.4.0 65 | # via trio 66 | trio==0.26.2 67 | # via 68 | # selenium 69 | # trio-websocket 70 | trio-websocket==0.11.1 71 | # via selenium 72 | urllib3[socks]==1.26.20 73 | # via 74 | # requests 75 | # selenium 76 | wsproto==1.2.0 77 | # via trio-websocket 78 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_worker_wrapper.in: -------------------------------------------------------------------------------- 1 | docker==7.1.0 2 | tenacity==8.3.0 3 | -------------------------------------------------------------------------------- /docker-app/requirements/requirements_worker_wrapper.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=/requirements/requirements_worker_wrapper.txt /requirements/requirements_worker_wrapper.in 6 | # 7 | certifi==2024.8.30 8 | # via requests 9 | charset-normalizer==3.3.2 10 | # via requests 11 | docker==7.1.0 12 | # via -r /requirements/requirements_worker_wrapper.in 13 | idna==3.8 14 | # via requests 15 | requests==2.32.3 16 | # via docker 17 | tenacity==8.3.0 18 | # via -r /requirements/requirements_worker_wrapper.in 19 | urllib3==2.2.2 20 | # via 21 | # docker 22 | # requests 23 | -------------------------------------------------------------------------------- /docker-app/wait_for_services.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from time import sleep, time 4 | 5 | import psycopg2 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | logger.addHandler(logging.StreamHandler()) 10 | 11 | TIMEOUT = 30 12 | INTERVAL = 2 13 | 14 | 15 | def wait_for_postgres(): 16 | logger.info("Waiting for postgres...") 17 | config = { 18 | "dbname": os.environ.get("POSTGRES_DB"), 19 | "user": os.environ.get("POSTGRES_USER"), 20 | "password": os.environ.get("POSTGRES_PASSWORD"), 21 | "host": os.environ.get("POSTGRES_HOST"), 22 | "port": os.environ.get("POSTGRES_PORT"), 23 | "sslmode": os.environ.get("POSTGRES_SSLMODE"), 24 | "connect_timeout": TIMEOUT, 25 | } 26 | start_time = time() 27 | while time() - start_time < TIMEOUT: 28 | try: 29 | conn = psycopg2.connect(**config) 30 | logger.info("Postgres is ready! ✨ 💅") 31 | conn.close() 32 | return True 33 | except psycopg2.OperationalError as error: 34 | if time() - start_time < TIMEOUT: 35 | logger.info( 36 | f"Postgres isn't ready.\npsycopg2 {type(error).__name__}\n{error}\nWaiting for {INTERVAL} second(s)..." 37 | ) 38 | sleep(INTERVAL) 39 | else: 40 | logger.error( 41 | f"Postgres never responded in {TIMEOUT} seconds.\npsycopg2 {type(error).__name__}\n{error}" 42 | ) 43 | 44 | logger.error(f"We could not connect to Postgres within {TIMEOUT} seconds.") 45 | 46 | return False 47 | 48 | 49 | wait_for_postgres() 50 | -------------------------------------------------------------------------------- /docker-app/worker_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | django.setup() 4 | -------------------------------------------------------------------------------- /docker-compose.override.prod.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | volumes: 4 | - static_volume:/var/www/html/staticfiles/ 5 | - media_volume:/var/www/html/mediafiles/ 6 | -------------------------------------------------------------------------------- /docker-compose.override.staging.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | app: 4 | build: 5 | args: 6 | DEBUG_BUILD: ${DEBUG} 7 | 8 | geodb: 9 | image: postgis/postgis:${POSTGIS_IMAGE_VERSION} 10 | restart: unless-stopped 11 | volumes: 12 | - geodb_data:/var/lib/postgresql 13 | environment: 14 | POSTGRES_DB: ${GEODB_DB} 15 | POSTGRES_USER: ${GEODB_USER} 16 | POSTGRES_PASSWORD: ${GEODB_PASSWORD} 17 | ports: 18 | - ${HOST_GEODB_PORT}:5432 19 | 20 | nginx: 21 | volumes: 22 | - static_volume:/var/www/html/staticfiles/ 23 | - media_volume:/var/www/html/mediafiles/ 24 | 25 | volumes: 26 | geodb_data: 27 | -------------------------------------------------------------------------------- /docker-compose.override.test.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | app: 4 | build: 5 | context: ./docker-app 6 | target: webserver_test 7 | environment: 8 | # we must use the same db for test and runserver 9 | POSTGRES_DB: test_${POSTGRES_DB} 10 | POSTGRES_DB_TEST: test_${POSTGRES_DB} 11 | worker_wrapper: 12 | environment: 13 | # we must use the same db for test and runserver 14 | POSTGRES_DB: test_${POSTGRES_DB} 15 | POSTGRES_DB_TEST: test_${POSTGRES_DB} 16 | scale: ${QFIELDCLOUD_WORKER_REPLICAS} 17 | 18 | db: 19 | environment: 20 | POSTGRES_DB: test_${POSTGRES_DB} 21 | 22 | networks: 23 | default: 24 | # Use a custom driver 25 | name: ${QFIELDCLOUD_DEFAULT_NETWORK:-${COMPOSE_PROJECT_NAME}_default} 26 | 27 | volumes: 28 | # We use a different volume, just so that the test_ database 29 | # gets created in the entrypoint. 30 | postgres_data: 31 | name: qfieldcloud_postgres_data_test 32 | -------------------------------------------------------------------------------- /docker-createbuckets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:noble 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | curl \ 6 | python3 \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | RUN curl https://dl.min.io/client/mc/release/linux-amd64/archive/mc.RELEASE.2025-03-12T17-29-24Z -o /usr/bin/mc 10 | RUN chmod +x /usr/bin/mc 11 | 12 | COPY ./createbuckets.py /createbuckets.py 13 | RUN chmod +x /createbuckets.py 14 | 15 | ENTRYPOINT /createbuckets.py 16 | -------------------------------------------------------------------------------- /docker-nginx/99-autoreload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | while :; do 3 | sleep 6h 4 | nginx -t && nginx -s reload 5 | done & 6 | -------------------------------------------------------------------------------- /docker-nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:stable 2 | 3 | COPY pages /var/www/html/pages/ 4 | COPY templates/ /etc/nginx/templates/ 5 | COPY options-ssl-nginx.conf /etc/nginx/options-ssl-nginx.conf 6 | COPY 99-autoreload.sh /docker-entrypoint.d/99-autoreload.sh 7 | 8 | RUN chmod 755 /docker-entrypoint.d/99-autoreload.sh 9 | -------------------------------------------------------------------------------- /docker-nginx/options-ssl-nginx.conf: -------------------------------------------------------------------------------- 1 | # This file contains important security parameters. If you modify this file 2 | # manually, Certbot will be unable to automatically provide future security 3 | # updates. Instead, Certbot will print and log an error message with a path to 4 | # the up-to-date file that you will need to refer to when manually updating 5 | # this file. Contents are based on https://ssl-config.mozilla.org 6 | 7 | ssl_session_cache shared:le_nginx_SSL:10m; 8 | ssl_session_timeout 1440m; 9 | ssl_session_tickets off; 10 | 11 | ssl_protocols TLSv1.2 TLSv1.3; 12 | ssl_prefer_server_ciphers off; 13 | 14 | ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; 15 | -------------------------------------------------------------------------------- /docker-nginx/pages/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-nginx/pages/favicon.ico -------------------------------------------------------------------------------- /docker-qgis/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=68.0", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.setuptools] 9 | packages = ["qfc_worker"] 10 | 11 | [tool.setuptools.dynamic] 12 | dependencies = { file = ["requirements.txt", "requirements_libqfieldsync.txt"] } 13 | 14 | [project] 15 | name = "qfc_worker" 16 | description = "the QFieldCloud worker library" 17 | version = "1.0" 18 | authors = [ 19 | { name = "OPENGIS.ch", email = "info@opengis.ch" } 20 | ] 21 | requires-python = ">=3.8" 22 | 23 | [project.urls] 24 | homepage = "https://github.com/opengisch/QFieldCloud" 25 | documentation = "https://docs.qfield.org/get-started/" 26 | repository = "https://github.com/opengisch/QFieldCloud" 27 | tracker = "https://github.com/opengisch/QFieldCloud/issues" 28 | 29 | [project.optional-dependencies] 30 | dev = ["pre-commit"] 31 | -------------------------------------------------------------------------------- /docker-qgis/qfc_worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/qfc_worker/__init__.py -------------------------------------------------------------------------------- /docker-qgis/requirements.in: -------------------------------------------------------------------------------- 1 | jsonschema>=3.2.0 2 | typing-extensions>=3 3 | tabulate>=v0.8.9 4 | sentry-sdk 5 | requests>=2.28.1 6 | qfieldcloud-sdk==0.8.4 7 | -------------------------------------------------------------------------------- /docker-qgis/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | attrs==24.2.0 8 | # via 9 | # jsonschema 10 | # referencing 11 | certifi==2024.8.30 12 | # via 13 | # qfieldcloud-sdk 14 | # requests 15 | # sentry-sdk 16 | charset-normalizer==3.3.2 17 | # via 18 | # qfieldcloud-sdk 19 | # requests 20 | click==8.1.7 21 | # via qfieldcloud-sdk 22 | idna==3.8 23 | # via 24 | # qfieldcloud-sdk 25 | # requests 26 | jsonschema==4.23.0 27 | # via -r requirements.in 28 | jsonschema-specifications==2023.12.1 29 | # via jsonschema 30 | qfieldcloud-sdk==0.8.4 31 | # via -r requirements.in 32 | referencing==0.35.1 33 | # via 34 | # jsonschema 35 | # jsonschema-specifications 36 | requests==2.32.3 37 | # via 38 | # -r requirements.in 39 | # qfieldcloud-sdk 40 | rpds-py==0.20.0 41 | # via 42 | # jsonschema 43 | # referencing 44 | sentry-sdk==2.13.0 45 | # via -r requirements.in 46 | tabulate==0.9.0 47 | # via -r requirements.in 48 | tqdm==4.66.5 49 | # via qfieldcloud-sdk 50 | typing-extensions==4.12.2 51 | # via -r requirements.in 52 | urllib3==2.2.2 53 | # via 54 | # qfieldcloud-sdk 55 | # requests 56 | # sentry-sdk 57 | -------------------------------------------------------------------------------- /docker-qgis/requirements_libqfieldsync.txt: -------------------------------------------------------------------------------- 1 | libqfieldsync @ git+https://github.com/opengisch/libqfieldsync@c670dcffb9aada9a7066591a7588188f024102a0 2 | -------------------------------------------------------------------------------- /docker-qgis/tests/test_apply_deltas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | 5 | echo "BEGIN TESTS" 6 | $DIR/../apply_deltas.py delta apply $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/singlelayer_singledelta.json 7 | $DIR/../apply_deltas.py delta apply --inverse $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/singlelayer_singledelta.json 8 | $DIR/../apply_deltas.py delta apply $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/singlelayer_multidelta.json 9 | $DIR/../apply_deltas.py delta apply --inverse $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/singlelayer_multidelta.json 10 | $DIR/../apply_deltas.py delta apply $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/multilayer_multidelta.json 11 | $DIR/../apply_deltas.py delta apply --inverse $DIR/testdata/project2apply/project.qgs $DIR/testdata/project2apply/deltas/multilayer_multidelta.json 12 | echo "END TESTS" 13 | -------------------------------------------------------------------------------- /docker-qgis/tests/test_qgis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import tempfile 5 | import unittest 6 | 7 | 8 | class QfcTestCase(unittest.TestCase): 9 | def test_package(self): 10 | project_directory = self.data_directory_path("simple_project") 11 | output_directory = tempfile.mkdtemp() 12 | 13 | command = [ 14 | "docker-compose", 15 | "run", 16 | "--rm", 17 | "-v", 18 | f"{project_directory}:/io/project/", 19 | "-v", 20 | f"{output_directory}:/io/output/", 21 | "qgis", 22 | "bash", 23 | "-c", 24 | "./entrypoint.sh package /io/project/project.qgs /io/output", 25 | ] 26 | 27 | subprocess.check_call( 28 | command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL 29 | ) 30 | 31 | files = os.listdir(output_directory) 32 | 33 | self.assertIn("project_qfield.qgs", files) 34 | self.assertIn("france_parts_shape.shp", files) 35 | self.assertIn("france_parts_shape.dbf", files) 36 | self.assertIn("curved_polys.gpkg", files) 37 | self.assertIn("spatialite.db", files) 38 | 39 | shutil.rmtree(output_directory) 40 | 41 | def data_directory_path(self, path): 42 | basepath = os.path.dirname(os.path.abspath(__file__)) 43 | return os.path.join(basepath, "testdata", path) 44 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/bees.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/bees.gpkg -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/bumblebees.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/bumblebees.gpkg -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/project2apply/deltas/singlelayer_singledelta.json: -------------------------------------------------------------------------------- 1 | { 2 | "deltas": [ 3 | { 4 | "uuid": "9311eb96-bff8-4d5b-ab36-c314a007cfcd", 5 | "localPk": "1", 6 | "sourcePk": "1", 7 | "localLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 8 | "sourceLayerId": "points_xy_897d5ed7_b810_4624_abe3_9f7c0a93d6a1", 9 | "method": "patch", 10 | "new": { 11 | "attributes": { 12 | "int": 666 13 | } 14 | }, 15 | "old": { 16 | "attributes": { 17 | "int": 1 18 | } 19 | } 20 | } 21 | ], 22 | "files": [], 23 | "id": "6f109cd3-f44c-41db-b134-5f38468b9fda", 24 | "project": "e02d02cc-af1b-414c-a14c-e2ed5dfee52f", 25 | "version": "1.0" 26 | } 27 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/project2apply/points.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "points", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "id": 1, "properties": { "fid": 1, "int": 1, "dbl": 0.1, "str": "str1" }, "geometry": { "type": "Point", "coordinates": [ 1.0, 0.0 ] } }, 7 | { "type": "Feature", "id": 2, "properties": { "fid": 2, "int": 2, "dbl": 0.2, "str": "str2" }, "geometry": { "type": "Point", "coordinates": [ 5.0, 0.0 ] } }, 8 | { "type": "Feature", "id": 3, "properties": { "fid": 3, "int": 3, "dbl": 0.3, "str": "str3" }, "geometry": { "type": "Point", "coordinates": [ 9.0, 0.0 ] } } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/project2apply/polygons.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "polygons", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "id": 8, "properties": { "fid": 8, "int": 8, "dbl": 0.8, "str": "str8" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 2.0, 2.0 ], [ 5.0, 2.0 ], [ 5.0, 5.0 ], [ 2.0, 5.0 ], [ 2.0, 2.0 ] ] ] } }, 7 | { "type": "Feature", "id": 9, "properties": { "fid": 9, "int": 9, "dbl": 0.9, "str": "str9" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 6.0, 2.0 ], [ 9.0, 2.0 ], [ 9.0, 5.0 ], [ 6.0, 5.0 ], [ 6.0, 2.0 ] ] ] } } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/project2apply/testdata.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/project2apply/testdata.gpkg -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/curved_polys.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/simple_project/curved_polys.gpkg -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.cpg: -------------------------------------------------------------------------------- 1 | UTF-8 2 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/simple_project/france_parts_shape.dbf -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] 2 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.qpj: -------------------------------------------------------------------------------- 1 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] 2 | -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/simple_project/france_parts_shape.shp -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/france_parts_shape.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/simple_project/france_parts_shape.shx -------------------------------------------------------------------------------- /docker-qgis/tests/testdata/simple_project/spatialite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opengisch/QFieldCloud/244ba21a6b736d06de6610544c161b6f9ffca004/docker-qgis/tests/testdata/simple_project/spatialite.db -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # See the default values at: https://docs.astral.sh/ruff/configuration/ 2 | 3 | # Exclude a variety of commonly ignored directories. 4 | exclude = [ 5 | ".git", 6 | ".git-rewrite", 7 | ".hg", 8 | ".ipynb_checkpoints", 9 | ".mypy_cache", 10 | ".ruff_cache", 11 | ".vscode", 12 | "__pypackages__", 13 | "site-packages", 14 | # Custom QFieldCloud dirs to ignore. 15 | "**/docker-qgis/libqfieldsync/", 16 | "**/docker-qgis/qfieldcloud-sdk-python/", 17 | ] 18 | 19 | # Support Python 3.10+. 20 | target-version = "py310" 21 | 22 | [lint] 23 | extend-select = ["I"] 24 | ignore = [] 25 | 26 | [lint.mccabe] 27 | max-complexity = 30 28 | -------------------------------------------------------------------------------- /scripts/check_envvars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | python3 scripts/check_envvars.py .env.example --docker-compose-dir . --ignored-varnames DEBUG_DEBUGPY_APP_PORT DEBUG_DEBUGPY_WORKER_WRAPPER_PORT 4 | -------------------------------------------------------------------------------- /scripts/init_letsencrypt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # read and export the variables from the .env file for the duration of this script 6 | set -o allexport 7 | source .env 8 | set +o allexport 9 | 10 | echo "### Requesting Let's Encrypt certificate for $QFIELDCLOUD_HOST ..." 11 | domain_args="-d ${QFIELDCLOUD_HOST}" 12 | 13 | # Enable staging mode if needed 14 | if [ $LETSENCRYPT_STAGING != "0" ]; then staging_arg="--staging"; fi 15 | 16 | docker compose run --rm --entrypoint "\ 17 | certbot certonly --webroot -w /var/www/certbot \ 18 | $staging_arg \ 19 | $domain_args \ 20 | --email $LETSENCRYPT_EMAIL \ 21 | --rsa-key-size $LETSENCRYPT_RSA_KEY_SIZE \ 22 | --agree-tos \ 23 | --force-renewal" certbot 24 | 25 | echo 26 | echo "### Reloading nginx ..." 27 | docker compose exec nginx nginx -s reload 28 | --------------------------------------------------------------------------------