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
23 changes: 23 additions & 0 deletions app/migrations/0004_app_icon_url_app_size_in_bytes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.1.7 on 2025-05-15 16:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("app", "0003_alter_app_developer"),
]

operations = [
migrations.AddField(
model_name="app",
name="icon_url",
field=models.URLField(null=True),
),
migrations.AddField(
model_name="app",
name="size_in_bytes",
field=models.BigIntegerField(null=True),
),
]
2 changes: 2 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class App(models.Model):
pegi_rating = models.CharField(max_length=10, null=True) # PEGI rating
release_date = models.DateField(null=True)
min_ios_version = models.CharField(max_length=10, null=True) # Minimum iOS version
icon_url = models.URLField(null=True)
size_in_bytes = models.BigIntegerField(null=True)

def __str__(self):
return self.name
36 changes: 36 additions & 0 deletions app/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from rest_framework import serializers

from metric.serializers import MetricDashboardSerializer

from .models import App


class AppSerializer(serializers.ModelSerializer):
sizeMB = serializers.SerializerMethodField()

class Meta:
model = App
fields = [
Expand All @@ -19,10 +23,42 @@ class Meta:
"pegi_rating",
"release_date",
"min_ios_version",
"icon_url",
"sizeMB",
]

def get_sizeMB(self, obj):
if obj.size_in_bytes is None:
return None
return round(obj.size_in_bytes / (1024 * 1024), 2)


class AppCreateSerializer(serializers.ModelSerializer):
class Meta:
model = App
fields = ["code", "name", "description", "appstore_id", "playstore_id"]


class MetricHistorySerializer(serializers.Serializer):
date = serializers.DateField()
value = serializers.SerializerMethodField()

def get_value(self, obj):
value_type = self.context.get("value_type", "string")

if value_type == "float":
return float(obj["value"])
elif value_type == "int":
return int(obj["value"])
else:
return obj["value"]


class SourceHistorySerializer(serializers.Serializer):
source = serializers.CharField()
history = MetricHistorySerializer(many=True)


class MetricResponseSerializer(serializers.Serializer):
metric = MetricDashboardSerializer()
sources = SourceHistorySerializer(many=True)
11 changes: 10 additions & 1 deletion app/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,15 @@ def _fetch_appstore_data(self, validated_data):
validated_data["developer"] = result.get("artistName")
validated_data["pegi_rating"] = result.get("contentAdvisoryRating")
validated_data["min_ios_version"] = result.get("minimumOsVersion")

validated_data["icon_url"] = (
result.get("artworkUrl512") or result.get("artworkUrl100") or result.get("artworkUrl60")
)
file_size_str = result.get("fileSizeBytes")
if file_size_str:
try:
validated_data["size_in_bytes"] = int(file_size_str)
except ValueError:
print(f"Error al convertir fileSizeBytes: {file_size_str}")
released_str = result.get("releaseDate")
if released_str:
try:
Expand All @@ -77,6 +85,7 @@ def _fetch_playstore_data(self, validated_data):
validated_data["pegi_rating"] = validated_data.get("pegi_rating") or result.get(
"contentRating"
)
validated_data["icon_url"] = validated_data.get("icon_url") or result.get("icon")

released_str = result.get("released")
if released_str and not validated_data.get("release_date"):
Expand Down
50 changes: 48 additions & 2 deletions app/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from .serializers import AppCreateSerializer, AppSerializer
from metric.services import MetricService, MetricValueService

from .serializers import AppCreateSerializer, AppSerializer, MetricResponseSerializer
from .services import AppService


class AppViewSet(viewsets.ViewSet):
service = AppService()
metric_service = MetricService()
metric_value_service = MetricValueService()

@extend_schema(
responses=AppSerializer(many=True),
Expand Down Expand Up @@ -44,7 +49,11 @@ def retrieve(self, request, pk=None):
def create(self, request):
print("REQUEST DATA:", request.data)
serializer = AppCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
serializer.is_valid(raise_exception=True)
except Exception as e:
print("❌ VALIDATION ERROR:", serializer.errors)
raise e # sigue dejando que DRF devuelva el 400
app = self.service.create_app(serializer.validated_data)
return Response(AppSerializer(app).data, status=status.HTTP_201_CREATED)

Expand All @@ -69,3 +78,40 @@ def destroy(self, request, pk=None):
app = self.service.get_app(pk)
self.service.delete_app(app)
return Response(status=status.HTTP_204_NO_CONTENT)

@extend_schema(
parameters=[
OpenApiParameter(name="id", required=True, type=int, location="path"),
OpenApiParameter(name="metric_id", required=True, type=int, location="path"),
],
responses=MetricResponseSerializer,
tags=["Apps"],
)
@action(detail=True, methods=["get"], url_path="metrics/(?P<metric_id>[^/.]+)")
def get_app_metric(self, request, pk=None, metric_id=None):
metric = self.metric_service.get_metric(metric_id)
metric_values = self.metric_value_service.get_metric_values_by_app_and_metric(
app_id=pk, metric_id=metric_id
)

sources_data = {}
for value in metric_values:
source_name = value.source.name if value.source else "Internal"
if source_name not in sources_data:
sources_data[source_name] = {"source": source_name, "history": []}
sources_data[source_name]["history"].append(
{"date": value.retrieved_at.date(), "value": value.value}
)

response_data = {
"metric": {
"code": metric.code,
"name": metric.name,
"description": metric.description,
"value_type": metric.value_type,
},
"sources": list(sources_data.values()),
}

serializer = MetricResponseSerializer(response_data)
return Response(serializer.data)
15 changes: 15 additions & 0 deletions config/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from rest_framework.views import exception_handler


def custom_exception_handler(exc, context):
response = exception_handler(exc, context)

if response is not None and isinstance(response.data, dict):
formatted_errors = []
for field, errors in response.data.items():
for error in errors:
formatted_errors.append(f"{field.capitalize()}: {error}")

response.data = {"errors": formatted_errors}

return response
1 change: 1 addition & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@

REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"EXCEPTION_HANDLER": "config.exceptions.custom_exception_handler",
}

CELERY_BROKER_URL = "redis://redis:6379/0"
Expand Down
17 changes: 17 additions & 0 deletions metric/migrations/0006_remove_metric_is_internal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.7 on 2025-05-15 16:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("metric", "0005_add_is_internal"),
]

operations = [
migrations.RemoveField(
model_name="metric",
name="is_internal",
),
]
1 change: 0 additions & 1 deletion metric/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class Metric(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
value_type = models.CharField(max_length=10, choices=MetricValueType.choices)
is_internal = models.BooleanField(default=False)

def __str__(self):
return self.name
Expand Down
41 changes: 17 additions & 24 deletions metric/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,55 @@


class MetricRepository:
@staticmethod
def get_all():
def get_all(self):
return Metric.objects.all()

@staticmethod
def get_by_id(pk):
def get_by_id(self, pk):
return Metric.objects.get(id=pk)

@staticmethod
def create(data):
def create(self, data):
return Metric.objects.create(**data)

@staticmethod
def update(instance, data):
def update(self, instance, data):
for attr, value in data.items():
setattr(instance, attr, value)
instance.save()
return instance

@staticmethod
def delete(instance):
def delete(self, instance):
instance.delete()

@staticmethod
def add_sources(instance, sources_ids):
def add_sources(self, instance, sources_ids):
instance.sources.add(*sources_ids)
instance.save()
return instance

@staticmethod
def remove_sources(instance, sources_ids):
def remove_sources(self, instance, sources_ids):
instance.sources.remove(*sources_ids)
instance.save()
return instance


class MetricValueRepository:
@staticmethod
def get_all():
def get_all(self):
return MetricValue.objects.all()

@staticmethod
def get_by_id(pk):
def get_by_id(self, pk):
return MetricValue.objects.get(id=pk)

@staticmethod
def create(data):
def create(self, data):
return MetricValue.objects.create(**data)

@staticmethod
def update(instance, data):
def update(self, instance, data):
for attr, value in data.items():
setattr(instance, attr, value)
instance.save()
return instance

@staticmethod
def delete(instance):
def delete(self, instance):
instance.delete()

def get_by_app_and_metric(self, app_id, metric_id):
return MetricValue.objects.filter(app_id=app_id, metric_id=metric_id).select_related(
"source"
)
8 changes: 8 additions & 0 deletions metric/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ class LinkMetricsSerializer(serializers.Serializer):
metrics = serializers.ListField(
child=serializers.IntegerField(), help_text="List of metric IDs to link to the source."
)


class MetricDashboardSerializer(serializers.ModelSerializer):
value_type = serializers.ChoiceField(choices=MetricValueType.choices)

class Meta:
model = Metric
fields = ["code", "name", "description", "value_type"]
3 changes: 3 additions & 0 deletions metric/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ def update_metric_value(self, instance, validated_data):

def delete_metric_value(self, instance):
return self.repo.delete(instance)

def get_metric_values_by_app_and_metric(self, app_id, metric_id):
return self.repo.get_by_app_and_metric(app_id, metric_id)
Loading