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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/tests/integration/devices/create.tavern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ stages:
locale: en-US
mobile_app:
package_name: test_package_name
package_version: test_package_version
package_version: "1.2.3+4567"
response:
status_code: 201
json:
Expand All @@ -74,7 +74,7 @@ stages:
locale: "en-US"
mobile_app:
package_name: "test_package_name"
package_version: "test_package_version"
package_version: "1.2.3+4567"
user_uuid: "{app_user.pk}"
last_login: !anything
created_at: !re_fullmatch \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z
Expand Down
4 changes: 2 additions & 2 deletions api/tests/integration/devices/update.tavern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ stages:
locale: es-ES
mobile_app:
package_name: new_test_package_name
package_version: new_test_package_version
package_version: "2.3.4+5678"
response:
status_code: 200
strict:
Expand All @@ -59,7 +59,7 @@ stages:
locale: es-ES
mobile_app:
package_name: new_test_package_name
package_version: new_test_package_version
package_version: "2.3.4+5678"
# Now simulate another user trying to access the first user data
- id: signup
type: ref
Expand Down
4 changes: 2 additions & 2 deletions api/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_package_is_set_on_report_create(self, app_user, data_create_request):
device_id='unique_device_id',
mobile_app=MobileApp.objects.create(
package_name='testapp',
package_version=1234
package_version="1.2.3+4567"
)
)
)
Expand All @@ -94,7 +94,7 @@ def test_package_is_set_on_report_create(self, app_user, data_create_request):
report = self.queryset.get(pk=response.data.get('uuid'))

assert report.package_name == 'testapp'
assert report.package_version == 1234
assert report.package_version == 102 # NOTE: only major and minor version are considered.
assert report.app_language == 'es'

def test_device_is_set_on_report_create(self, app_user, data_create_request):
Expand Down
3 changes: 2 additions & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ def perform_create(self, serializer):
if device.mobile_app:
kwargs['mobile_app'] = device.mobile_app
kwargs['package_name'] = device.mobile_app.package_name
kwargs['package_version'] = device.mobile_app.package_version
v = device.mobile_app.package_version
kwargs['package_version'] = int(f"{v.major}{v.minor:02d}")
serializer.save(**kwargs)

def perform_update(self, serializer):
Expand Down
1 change: 1 addition & 0 deletions requirements/prod_20_04.pip
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ python3-memcached==1.51
rawpy==0.17.1
redis==2.10.3
requests==2.22.0
semantic-version==2.10.0
scikit-learn==1.5.2
scipy==1.13.1
six==1.16.0
Expand Down
54 changes: 54 additions & 0 deletions tigaserver_app/migrations/0084_alter_mobileapp_package_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 3.2.25 on 2025-08-22 08:18

from django.db import migrations
import django.core.validators
import re
import semantic_version
import semantic_version.django_fields
from semantic_version import Version


def force_semantic_version(apps, schema_editor):
MobileApp = apps.get_model('tigaserver_app', 'MobileApp')

obj_to_update = []
for mobile_app in MobileApp.objects.iterator():
if not semantic_version.validate(mobile_app.package_version):
if '.' in mobile_app.package_version:
mobile_app.package_version = Version.coerce(mobile_app.package_version)
else:
# NOTE: this must be the same as in Report.save when doing the MobileApp.objects.get_or_create
mobile_app.package_version = Version(
major=0,
minor=int(mobile_app.package_version),
patch=0,
build=('legacy',)
)
obj_to_update.append(mobile_app)

MobileApp.objects.bulk_update(obj_to_update, ['package_version'], batch_size=2000)

def undo_force_semantic_version(apps, schema_editor):
MobileApp = apps.get_model('tigaserver_app', 'MobileApp')

obj_to_update = []
for mobile_app in MobileApp.objects.filter(package_version__contains='legacy').iterator():
mobile_app.package_version = str(Version.coerce(mobile_app.package_version).minor)
obj_to_update.append(mobile_app)

MobileApp.objects.bulk_update(obj_to_update, ['package_version'], batch_size=2000)

class Migration(migrations.Migration):

dependencies = [
('tigaserver_app', '0083_europecountry_reports_can_be_published'),
]

operations = [
migrations.RunPython(force_semantic_version, undo_force_semantic_version),
migrations.AlterField(
model_name='mobileapp',
name='package_version',
field=semantic_version.django_fields.VersionField(coerce=False, max_length=32, partial=False, validators=[django.core.validators.RegexValidator(code='invalid_version', regex=re.compile('^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([0-9a-zA-Z.-]+))?(?:\\+([0-9a-zA-Z.-]+))?$'))]),
),
]
27 changes: 23 additions & 4 deletions tigaserver_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from django.contrib.gis.db.models.functions import Distance as DistanceFunction
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.core.validators import RegexValidator
from django.db import transaction
from django.db.models import Count, Q
from django.db.models.signals import post_save
Expand All @@ -36,6 +37,8 @@
from fcm_django.models import AbstractFCMDevice, DeviceType
from imagekit.processors import ResizeToFit
from langcodes import Language, closest_supported_match, standardize_tag as standarize_language_tag, tag_is_valid as language_tag_is_valid
from semantic_version import Version
from semantic_version.django_fields import VersionField
from simple_history.models import HistoricalRecords
from timezone_field import TimeZoneField
from taggit.managers import TaggableManager
Expand Down Expand Up @@ -333,8 +336,16 @@ class Meta:


class MobileApp(models.Model):
# NOTE: At some point we should adjust the package_version which 'build' value is 'legacy'
# since this version were creating from Report.package_version (which is an IntegerField)
# and it's a number which is not related with the Mobile App pubspeck.yaml package version.
package_name = models.CharField(max_length=128)
package_version = models.CharField(max_length=32)
package_version = VersionField(max_length=32, validators=[
RegexValidator(
regex=Version.version_re,
code='invalid_version'
)
])
Copy link

Copilot AI Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RegexValidator is redundant since VersionField already validates semantic version format internally. This adds unnecessary validation overhead.

Suggested change
])
package_version = VersionField(max_length=32)

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not exactly true... It has an python validator but not a django validator, which is interesting to, for example, raise ValidationError correctly in the API (compatible with DRF)


created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Expand Down Expand Up @@ -1961,11 +1972,19 @@ def save(self, *args, **kwargs):
# in order to avoid publishing on bulk_create
self.published_at = timezone.now()

# Set mobile_app
if self.package_name and self.package_version:
# Set mobile_app (case legacy API)
if not self.mobile_app and self.package_name and self.package_version:
# NOTE: changing this will require a migration that matches the logic.
self.mobile_app, _ = MobileApp.objects.get_or_create(
package_name=self.package_name,
package_version=self.package_version
package_version=str(
Version(
major=0,
minor=int(self.package_version),
patch=0,
build=('legacy',)
)
)
)
# Update device according to the information provided in the report.
with transaction.atomic():
Expand Down
28 changes: 18 additions & 10 deletions tigaserver_app/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import tempfile
from django.core.files.uploadedfile import SimpleUploadedFile
import time_machine
import semantic_version

import io
import piexif
Expand Down Expand Up @@ -537,7 +538,7 @@ def test_user_locale_is_updated_according_to_app_language(self):

def test_mobile_app_fk_is_created_if_not_exist(self):
self.assertEqual(
MobileApp.objects.filter(package_name='testapp', package_version='100').count(),
MobileApp.objects.filter(package_name='testapp', package_version='0.100.0+legacy').count(),
0
)
response = self.client.post(
Expand All @@ -552,20 +553,19 @@ def test_mobile_app_fk_is_created_if_not_exist(self):
format="json"
)
self.assertEqual(response.status_code, 201)

self.assertEqual(
MobileApp.objects.filter(package_name='testapp', package_version='100').count(),
MobileApp.objects.filter(package_name='testapp', package_version='0.100.0+legacy').count(),
1
)
mobile_app = MobileApp.objects.get(package_name='testapp', package_version='100')
mobile_app = MobileApp.objects.get(package_name='testapp', package_version='0.100.0+legacy')
self.assertEqual(mobile_app.package_name, 'testapp')
self.assertEqual(mobile_app.package_version, '100')
self.assertEqual(mobile_app.package_version, semantic_version.Version(major=0, minor=100, patch=0, build=('legacy',)))

report = Report.objects.get(version_UUID=self.simple_payload["version_UUID"])
self.assertEqual(report.mobile_app, mobile_app)

def test_mobile_app_fk_is_set_correctly_if_exist(self):
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='100')
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='0.100.0+legacy')
response = self.client.post(
"/api/reports/",
{
Expand Down Expand Up @@ -635,7 +635,7 @@ def test_device_with_model_null_is_updated_on_new_report(self):
last_login=timezone.now()-timedelta(days=1)
)
self.assertIsNone(device.model)
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='100')
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='0.100.0+legacy')

response = self.client.post(
"/api/reports/",
Expand Down Expand Up @@ -680,7 +680,7 @@ def test_device_with_model_is_updated_on_new_report(self):
last_login=timezone.now()-timedelta(days=1)
)
self.assertIsNone(device.type)
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='100')
mobile_app = MobileApp.objects.create(package_name='testapp', package_version='0.100.0+legacy')

response = self.client.post(
"/api/reports/",
Expand Down Expand Up @@ -1905,7 +1905,15 @@ def test_mobile_app_is_deleted_on_save_failure(self):
)
self.assertEqual(MobileApp.objects.all().count(), 1)

mobile_app = MobileApp.objects.get(package_name=report.package_name, package_version=report.package_version)
mobile_app = MobileApp.objects.get(
package_name=report.package_name,
package_version=semantic_version.Version(
major=0,
minor=int(report.package_version),
patch=0,
build=('legacy',)
)
)

with self.assertRaises(IntegrityError) as context:
# Trying to create a new report with the same PK, which will raise.
Expand All @@ -1925,7 +1933,7 @@ def test_mobile_app_is_deleted_on_save_failure(self):
)

mobile_app.refresh_from_db()
self.assertEqual(mobile_app.package_version, "100")
self.assertEqual(str(mobile_app.package_version), "0.100.0+legacy")

def test_device_is_updated_if_previous_model_exist_and_new_model_None_also(self):
with time_machine.travel("2024-01-01 00:00:00", tick=False) as traveller:
Expand Down