mirror of
https://github.com/wger-project/wger.git
synced 2026-02-18 00:17:51 +01:00
Correctly process weigh entry data as sent by powersync
This commit is contained in:
@@ -10,15 +10,14 @@ USER root
|
||||
|
||||
WORKDIR /home/wger/src
|
||||
RUN wget -O- https://deb.nodesource.com/setup_22.x | bash - \
|
||||
apt update && \
|
||||
apt install --no-install-recommends -y \
|
||||
&& apt update \
|
||||
&& apt install --no-install-recommends -y \
|
||||
git \
|
||||
vim \
|
||||
nodejs \
|
||||
sassc \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ln -s /usr/bin/sassc /usr/bin/sass
|
||||
&& npm install -g sass
|
||||
|
||||
USER wger
|
||||
COPY --chown=wger:wger . /home/wger/src
|
||||
|
||||
54
uv.lock
generated
54
uv.lock
generated
@@ -783,6 +783,9 @@ wheels = [
|
||||
crypto = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
python-jose = [
|
||||
{ name = "python-jose" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "drf-spectacular"
|
||||
@@ -831,6 +834,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/90/be137d0592501ec08d4a00c8430121e95dfe6c8114a1d4436b5cdd6b9803/easy_thumbnails-2.10.1-py3-none-any.whl", hash = "sha256:24462d63dd31543ef1585538b2bfefe0db96d3409bb431c70b81548fb2cfc5be", size = 79695, upload-time = "2025-08-17T20:49:09.119Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "37.8.0"
|
||||
@@ -1224,6 +1239,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
@@ -1365,6 +1389,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/bd/ccd7416fdb30f104ddf6cfd8ee9f699441c7d9880a26f9b3089438adee05/python_ipware-3.0.0-py3-none-any.whl", hash = "sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60", size = 10761, upload-time = "2024-04-19T20:00:57.171Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-jose"
|
||||
version = "3.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ecdsa" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/19/b2c86504116dc5f0635d29f802da858404d77d930a25633d2e86a64a35b3/python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a", size = 129068, upload-time = "2021-06-05T03:30:40.895Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a", size = 33530, upload-time = "2021-06-05T03:30:38.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
@@ -1659,6 +1697,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.2"
|
||||
@@ -1912,7 +1962,7 @@ dependencies = [
|
||||
{ name = "django-sortedm2m" },
|
||||
{ name = "django-storages" },
|
||||
{ name = "djangorestframework" },
|
||||
{ name = "djangorestframework-simplejwt", extra = ["crypto"] },
|
||||
{ name = "djangorestframework-simplejwt", extra = ["crypto", "python-jose"] },
|
||||
{ name = "drf-spectacular", extra = ["sidecar"] },
|
||||
{ name = "easy-thumbnails" },
|
||||
{ name = "flower" },
|
||||
@@ -1969,7 +2019,7 @@ requires-dist = [
|
||||
{ name = "django-sortedm2m", specifier = "==4.0.0" },
|
||||
{ name = "django-storages", specifier = "==1.14.6" },
|
||||
{ name = "djangorestframework", specifier = "==3.16.1" },
|
||||
{ name = "djangorestframework-simplejwt", extras = ["crypto"], specifier = "==5.5.1" },
|
||||
{ name = "djangorestframework-simplejwt", extras = ["crypto", "python-jose"], specifier = "==5.5.1" },
|
||||
{ name = "drf-spectacular", extras = ["sidecar"], specifier = "==0.28.0" },
|
||||
{ name = "easy-thumbnails", specifier = "==2.10.1" },
|
||||
{ name = "flower", specifier = "==2.0.1" },
|
||||
|
||||
@@ -63,6 +63,7 @@ from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.tokens import AccessToken
|
||||
|
||||
# wger
|
||||
import wger.weight.powersync as ps_weight
|
||||
from wger.core.api.serializers import (
|
||||
LanguageCheckSerializer,
|
||||
LanguageSerializer,
|
||||
@@ -484,13 +485,33 @@ def get_powersync_keys(request):
|
||||
)
|
||||
|
||||
|
||||
@api_view()
|
||||
@api_view(['PUT', 'PATCH', 'DELETE'])
|
||||
def upload_powersync_data(request):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
logger.debug(request.POST)
|
||||
user_id = request.user.id
|
||||
data = request.data
|
||||
|
||||
# get the http verb
|
||||
http_verb = request.method
|
||||
|
||||
logger.info(f'Received PowerSync data: {data} via {http_verb} for user {user_id}')
|
||||
match data['table']:
|
||||
case 'weight_weightentry':
|
||||
if http_verb == 'PUT':
|
||||
ps_weight.handle_create(payload=data['data'], user_id=user_id)
|
||||
|
||||
elif http_verb == 'PATCH':
|
||||
ps_weight.handle_update(payload=data['data'], user_id=user_id)
|
||||
|
||||
elif http_verb == 'DELETE':
|
||||
ps_weight.handle_delete(payload=data['data'], user_id=user_id)
|
||||
case _:
|
||||
logger.warning('Received unknown PowerSync table')
|
||||
raise ValueError('Unknown PowerSync table')
|
||||
|
||||
return JsonResponse(
|
||||
{'ok!'},
|
||||
{'status': 'ok!'},
|
||||
status=200,
|
||||
)
|
||||
|
||||
37
wger/weight/migrations/0005_add_uuid.py
Normal file
37
wger/weight/migrations/0005_add_uuid.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-21 10:11
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def gen_uuids(apps, schema_editor):
|
||||
WeightEntry = apps.get_model('weight', 'WeightEntry')
|
||||
|
||||
for entry in WeightEntry.objects.all():
|
||||
entry.uuid = uuid.uuid4()
|
||||
entry.save(update_fields=['uuid'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('weight', '0004_multiple_weight_entries_per_day'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='weightentry',
|
||||
name='uuid',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, null=True),
|
||||
),
|
||||
# Generate UUIDs
|
||||
migrations.RunPython(
|
||||
gen_uuids,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
# Set uuid fields to non-nullable
|
||||
migrations.AlterField(
|
||||
model_name='weightentry',
|
||||
name='uuid',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, null=False),
|
||||
),
|
||||
]
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
# Standard Library
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
|
||||
# Django
|
||||
from django.contrib.auth.models import User
|
||||
@@ -27,34 +28,27 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# This file is part of wger Workout Manager.
|
||||
#
|
||||
# wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# wger Workout Manager is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class WeightEntry(models.Model):
|
||||
"""
|
||||
Model for a weight point
|
||||
"""
|
||||
|
||||
date = models.DateTimeField(verbose_name=_('Date'))
|
||||
|
||||
uuid = models.UUIDField(
|
||||
default=uuid4,
|
||||
editable=False,
|
||||
null=True,
|
||||
unique=False,
|
||||
)
|
||||
|
||||
weight = models.DecimalField(
|
||||
verbose_name=_('Weight'),
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
validators=[MinValueValidator(Decimal(30)), MaxValueValidator(Decimal(600))],
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
verbose_name=_('User'),
|
||||
|
||||
69
wger/weight/powersync.py
Normal file
69
wger/weight/powersync.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# This file is part of wger Workout Manager.
|
||||
#
|
||||
# wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# wger Workout Manager is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Workout Manager. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Standard Library
|
||||
import logging
|
||||
|
||||
# wger
|
||||
from wger.weight.api.serializers import WeightEntrySerializer
|
||||
from wger.weight.models import WeightEntry
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_update(payload: dict[str, any], user_id: int) -> None:
|
||||
"""Handle a push event from PowerSync"""
|
||||
logger.debug(
|
||||
f'Received PowerSync payload for update: {payload}',
|
||||
)
|
||||
entry = WeightEntry.objects.get(uuid=payload['id'], user_id=user_id)
|
||||
|
||||
if not entry:
|
||||
logger.warning(
|
||||
f'WeightEntry with UUID {payload["id"]} and user {user_id} not found for update.'
|
||||
)
|
||||
return
|
||||
|
||||
serializer = WeightEntrySerializer(entry, data=payload, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
logger.info(f'Updated WeightEntry {entry.pk} (uuid={entry.uuid}) for user {user_id}')
|
||||
else:
|
||||
logger.warning(f'PowerSync update validation failed: {serializer.errors}')
|
||||
|
||||
|
||||
def handle_create(payload: dict[str, any], user_id: int) -> None:
|
||||
"""Handle a create event from PowerSync"""
|
||||
logger.debug(
|
||||
f'Received PowerSync payload for create: {payload}',
|
||||
)
|
||||
serializer = WeightEntrySerializer(data=payload)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
else:
|
||||
logger.warning(f'PowerSync create validation failed: {serializer.errors}')
|
||||
|
||||
|
||||
def handle_delete(payload: dict[str, any], user_id: int) -> None:
|
||||
"""Handle a delete event from PowerSync"""
|
||||
logger.debug(
|
||||
f'Received PowerSync payload for delete: {payload}',
|
||||
)
|
||||
entry = WeightEntry.objects.get(uuid=payload['id'], user_id=user_id)
|
||||
if not entry:
|
||||
logger.warning(f'WeightEntry with UUID {payload["uuid"]} not found for delete.')
|
||||
return
|
||||
entry.delete()
|
||||
Reference in New Issue
Block a user