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
147 changes: 147 additions & 0 deletions app/management/commands/create_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import time

import pandas as pd
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand

from app.services import AppService
from metric.models import Metric
from polling.services.polling_schedule_service import PollingScheduleService
from source.models import Source


class Command(BaseCommand):
help = "Crea les Sources i Metric bàsiques"

def handle(self, *args, **kwargs):
average_rating = Metric.objects.create(
code="average_rating",
name="Average Rating",
value_type="float",
description="Average user rating of the app.",
)
total_reviews = Metric.objects.create(
code="total_reviews",
name="Total Reviews",
value_type="integer",
description="Total number of user reviews.",
)
daily_news_blog_mentions = Metric.objects.create(
code="daily_news_blog_mentions",
name="Daily News Blog Mentions",
value_type="integer",
description="Number of daily mentions in news blogs.",
)
daily_social_network_mentions = Metric.objects.create(
code="daily_social_network_mentions",
name="Daily Social Network Mentions",
value_type="integer",
description="Number of daily mentions on social networks.",
)
total_downloads = Metric.objects.create(
code="total_downloads",
name="Total Downloads",
value_type="integer",
description="Total number of app downloads.",
)
last_update_date = Metric.objects.create(
code="last_update_date",
name="Last Update Date",
value_type="date",
description="Date of the app’s last update.",
)
Metric.objects.create(
code="bug_rate",
name="Bug Rate",
value_type="float",
description="Proportion of reviews that mention errors"
" or bugs relative to the total number of reviews.",
is_derived=True,
)
Metric.objects.create(
code="positive_rate",
name="Positive Rate",
value_type="float",
description="Proportion of reviews with positive sentiment"
" relative to the total number of reviews.",
is_derived=True,
)
Metric.objects.create(
code="update_changed",
name="Update Changed",
value_type="integer",
description="Indicates whether the last update date has changed"
" compared to the previous day.\nValue is 1 if it changed, 0 otherwise.",
id_derived=True,
)
Source.objects.create(
code="itunes",
name="App Store",
type="api",
url="https://itunes.apple.com",
).metrics.set([average_rating, total_reviews])
Source.objects.create(
code="google_play",
name="Google Play Scraper",
type="scraper",
url="https://play.google.com",
).metrics.set([average_rating, total_reviews, total_downloads, last_update_date])
Source.objects.create(
code="news",
name="News API",
type="api",
url="https://newsapi.org/v2",
).metrics.set([daily_news_blog_mentions])
Source.objects.create(
code="reddit",
name="Reddit API",
type="api",
).metrics.set([daily_social_network_mentions])

user, created = User.objects.get_or_create(
username="Anyer",
defaults={
"email": "anyer@example.com",
"is_superuser": True,
"is_staff": True,
},
)
if created:
user.set_password("Anyer123")
user.save()

service = AppService()
poll_service = PollingScheduleService()

df_page1 = pd.read_excel("apps_updated_filtered.xlsx", sheet_name="Sheet1")
created_count = 0

for index, row in df_page1.iterrows():
try:
validated_data = {
"name": row["name"],
"appstore_id": (
str(int(row["apple_store_id"]))
if not pd.isna(row["apple_store_id"])
else None
),
"playstore_id": str(row["google_play_id"]),
}
app = service.create_app(validated_data, user)
polling_schedule_metrics = poll_service.get_polling_schedule(
app.id, "metrics", [app.id]
)
polling_schedule_reviews = poll_service.get_polling_schedule(
app.id, "reviews", [app.id]
)
poll_service.activate_polling_schedule(polling_schedule_metrics)
poll_service.activate_polling_schedule(polling_schedule_reviews)

created_count = created_count + 1
time.sleep(0.1)
except Exception as e:
print(f"❌ Error en la fila {index}: {e}")

self.stdout.write(
self.style.SUCCESS(f"✔ {created_count} apps creadas y polling activado correctamente.")
)
27 changes: 0 additions & 27 deletions app/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from rest_framework import serializers

from metric.serializers import MetricDashboardSerializer

from .models import App


Expand Down Expand Up @@ -36,28 +34,3 @@ class AppCreateSerializer(serializers.ModelSerializer):
class Meta:
model = App
fields = ["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)
45 changes: 1 addition & 44 deletions app/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.exceptions import NotFound

from metric.services import MetricService, MetricValueService
from polling.services.polling_schedule_service import PollingScheduleService
from source.adapters.google_play_scraper import GooglePlayScraperAdapter
from source.adapters.itunes import ItunesSearchAPIAdapter
Expand All @@ -12,10 +11,8 @@


class AppService:
def __init__(self, google_play_adapter=None):
def __init__(self):
self.repo = AppRepository()
self.metric_service = MetricService()
self.metric_value_service = MetricValueService()
self.polling_schedule_service = PollingScheduleService()
self.itunes_adapter = ItunesSearchAPIAdapter()
self.google_play_adapter = GooglePlayScraperAdapter()
Expand Down Expand Up @@ -101,43 +98,3 @@ def update_app(self, instance, validated_data):

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

def get_metric_dashboard(self, app_id: str, metric_id: str) -> dict:
metric = self.metric_service.get_metric(metric_id)
if not metric.is_derived:
metric_values = self.metric_value_service.get_metric_values_by_app_and_metric(
app_id, metric_id
)
values = [
{
"retrieved_at": metric_value.retrieved_at.date(),
"value": metric_value.value,
"source": metric_value.source.name if metric_value.source else "Internal",
}
for metric_value in metric_values
]
else:
values = self.metric_value_service.get_derived_metric_values_by_app_and_metric(
app_id, metric.code
)
for value in values:
if "source" not in value:
value["source"] = "Internal"

sources_data = {}
for value in values:
source_name = value["source"]
sources_data.setdefault(source_name, {"source": source_name, "history": []})
sources_data[source_name]["history"].append(
{"date": value["retrieved_at"], "value": value["value"]}
)

return {
"metric": {
"code": metric.code,
"name": metric.name,
"description": metric.description,
"value_type": metric.value_type,
},
"sources": list(sources_data.values()),
}
37 changes: 28 additions & 9 deletions app/tests/test_create_apps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

import pytest
from rest_framework import status
from rest_framework.test import APIClient
Expand All @@ -6,29 +8,27 @@


@pytest.mark.django_db
@pytest.mark.usefixtures("dummy_app")
@pytest.mark.usefixtures("dummy_user")
class TestAppCreateViewSet:
def test_create_app_success(self, dummy_app, create_default_sources_and_metrics):
def test_create_app_success(self, dummy_user):
client = APIClient()
client.force_authenticate(user=dummy_app.user)
client.force_authenticate(user=dummy_user)

payload = {
"name": "Test App",
"code": "test_app",
"appstore_id": "123456789",
"playstore_id": "com.example.app",
}

response = client.post("/api/apps/", data=payload, format="json")

assert response.status_code == status.HTTP_201_CREATED
assert App.objects.filter(name="Test App", user=dummy_app.user).exists()
assert App.objects.filter(name="Test App", user=dummy_user).exists()

def test_create_app_unauthenticated(self):
client = APIClient()
payload = {
"name": "Test App",
"code": "test_app",
"appstore_id": "123456789",
"playstore_id": "com.example.app",
}
Expand All @@ -37,13 +37,13 @@ def test_create_app_unauthenticated(self):

assert response.status_code == status.HTTP_401_UNAUTHORIZED

def test_create_app_missing_fields(self, dummy_app):
def test_create_app_missing_fields(self, dummy_user):
client = APIClient()
client.force_authenticate(user=dummy_app.user)
client.force_authenticate(user=dummy_user)

payload = {
# Falta 'name' i altres
"code": "test_app"
"appstore_id": "123456789"
}

response = client.post("/api/apps/", data=payload, format="json")
Expand All @@ -52,3 +52,22 @@ def test_create_app_missing_fields(self, dummy_app):
assert (
"name" in response.json()["errors"][0].lower() or "name" in str(response.json()).lower()
)

def test_create_app_with_real_values(self, dummy_user):
client = APIClient()
client.force_authenticate(user=dummy_user)

payload = {
"name": "Discord",
"appstore_id": "985746746",
"playstore_id": "com.discord",
}

response = client.post("/api/apps/", data=payload, format="json")

assert response.status_code == status.HTTP_201_CREATED
app = App.objects.get(name="Discord", user=dummy_user)
assert app.developer == "Discord, Inc."
assert app.release_date == datetime.date(2015, 5, 21)
assert app.available_on_ios is True
assert app.available_on_android is True
32 changes: 1 addition & 31 deletions app/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from schemas.responses import (
Expand All @@ -9,7 +8,7 @@
not_found_response,
)

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


Expand Down Expand Up @@ -118,32 +117,3 @@ def destroy(self, request, pk=None):
app = self.service.get_app_by_user(pk, user=request.user)
self.service.delete_app(app)
return Response(status=status.HTTP_204_NO_CONTENT)

@extend_schema(
summary="Get app metric dashboard",
description=(
"Retrieves the historical dashboard for a specific metric of a given app.\n\n"
"If the metric is a direct (raw) metric, this endpoint returns its historical values "
"grouped by data source (e.g. App Store, Google Play, or internal sources).\n"
"If the metric is a derived one, it is computed dynamically "
"and presented similarly.\n\n"
"Each source includes a list of historical records, each with a date and value. "
"Derived metrics are labeled as coming from the 'Internal' source."
),
parameters=[
OpenApiParameter(name="id", required=True, type=int, location="path"),
OpenApiParameter(name="metric_id", required=True, type=int, location="path"),
],
responses={
200: MetricResponseSerializer,
401: UNAUTHORIZED_RESPONSE,
404: not_found_response("app", True),
},
tags=["Apps"],
)
@action(detail=True, methods=["get"], url_path="metrics/(?P<metric_id>[^/.]+)")
def get_app_metric(self, request, pk=None, metric_id=None):
app = self.service.get_app_by_user(pk, user=request.user)
response_data = self.service.get_metric_dashboard(app_id=app.id, metric_id=metric_id)
serializer = MetricResponseSerializer(response_data)
return Response(serializer.data)
Binary file added apps_updated_filtered.xlsx
Binary file not shown.
4 changes: 1 addition & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ def dummy_review(db, dummy_app, dummy_source):
)


@pytest.fixture
@pytest.fixture(autouse=True)
def create_default_sources_and_metrics(db):
"""Crea sources i mètriques per defecte disponibles a tots els tests."""
itunes = Source.objects.create(
code="itunes", name="App Store", type="api", url="https://itunes.apple.com"
)
Expand All @@ -112,7 +111,6 @@ def create_default_sources_and_metrics(db):
MetricCode.AVERAGE_RATING,
MetricCode.TOTAL_REVIEWS,
MetricCode.TOTAL_DOWNLOADS,
MetricCode.LAST_UPDATE_DATE,
MetricCode.DAILY_SOCIAL_NETWORK_MENTIONS,
MetricCode.DAILY_NEWS_BLOG_MENTIONS,
]:
Expand Down
Loading
Loading