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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 5.1.7 on 2025-05-23 10:40

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("app", "0004_app_icon_url_app_size_in_bytes"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name="app",
name="user",
field=models.ForeignKey(
default=2,
on_delete=django.db.models.deletion.CASCADE,
related_name="apps",
to=settings.AUTH_USER_MODEL,
),
preserve_default=False,
),
migrations.AlterField(
model_name="app",
name="appstore_id",
field=models.CharField(max_length=100, null=True),
),
migrations.AlterField(
model_name="app",
name="code",
field=models.CharField(max_length=100),
),
migrations.AlterField(
model_name="app",
name="playstore_id",
field=models.CharField(max_length=100, null=True),
),
migrations.AddConstraint(
model_name="app",
constraint=models.UniqueConstraint(fields=("user", "code"), name="unique_user_code"),
),
migrations.AddConstraint(
model_name="app",
constraint=models.UniqueConstraint(
fields=("user", "appstore_id"), name="unique_user_appstore_id"
),
),
migrations.AddConstraint(
model_name="app",
constraint=models.UniqueConstraint(
fields=("user", "playstore_id"), name="unique_user_playstore_id"
),
),
]
18 changes: 18 additions & 0 deletions app/migrations/0006_alter_app_description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-26 17:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("app", "0005_app_user_alter_app_appstore_id_alter_app_code_and_more"),
]

operations = [
migrations.AlterField(
model_name="app",
name="description",
field=models.TextField(blank=True, null=True),
),
]
19 changes: 15 additions & 4 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from django.contrib.auth.models import User
from django.db import models


class App(models.Model):
code = models.CharField(max_length=100, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="apps")
code = models.CharField(max_length=100)
name = models.CharField(max_length=200)
description = models.TextField()
appstore_id = models.CharField(max_length=100, null=True, unique=True)
playstore_id = models.CharField(max_length=100, null=True, unique=True)
description = models.TextField(null=True, blank=True)
appstore_id = models.CharField(max_length=100, null=True)
playstore_id = models.CharField(max_length=100, null=True)
developer = models.CharField(max_length=100, null=True)
available_on_ios = models.BooleanField(default=False)
available_on_android = models.BooleanField(default=False)
Expand All @@ -16,5 +18,14 @@ class App(models.Model):
icon_url = models.URLField(null=True)
size_in_bytes = models.BigIntegerField(null=True)

class Meta:
constraints = [
models.UniqueConstraint(fields=["user", "code"], name="unique_user_code"),
models.UniqueConstraint(fields=["user", "appstore_id"], name="unique_user_appstore_id"),
models.UniqueConstraint(
fields=["user", "playstore_id"], name="unique_user_playstore_id"
),
]

def __str__(self):
return self.name
19 changes: 7 additions & 12 deletions app/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,20 @@


class AppRepository:
@staticmethod
def get_all():
return App.objects.all()
def get_all(self, user):
return App.objects.filter(user=user)

@staticmethod
def get_by_id(app_id):
def get_by_id(self, app_id):
return App.objects.get(id=app_id)

@staticmethod
def create(data):
return App.objects.create(**data)
def create(self, data, user):
return App.objects.create(user=user, **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()
8 changes: 4 additions & 4 deletions app/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ def google_play_adapter(self):
self._google_play_adapter = GooglePlayScraperAdapter()
return self._google_play_adapter

def list_apps(self):
return self.repo.get_all()
def list_apps(self, user):
return self.repo.get_all(user)

def get_app(self, app_id):
try:
return self.repo.get_by_id(app_id)
except ObjectDoesNotExist:
raise NotFound(f"The app with ID '{app_id}' is not registered.")

def create_app(self, validated_data):
def create_app(self, validated_data, user):
self._fetch_appstore_data(validated_data)
self._fetch_playstore_data(validated_data)
return self.repo.create(validated_data)
return self.repo.create(validated_data, user)

def _fetch_appstore_data(self, validated_data):
if not validated_data.get("appstore_id"):
Expand Down
32 changes: 20 additions & 12 deletions app/views.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

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


class AppViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
service = AppService()

def _get_user_app(self, pk):
app = self.service.get_app(pk)
if app.user != self.request.user:
raise PermissionDenied("No tens accés a aquesta aplicació.")
return app

@extend_schema(
responses=AppSerializer(many=True),
tags=["Apps"],
)
def list(self, request):
apps = self.service.list_apps()
print("👤 Usuari:", request.user)
print("🔐 Auth:", request.auth)

apps = self.service.list_apps(user=request.user)
serializer = AppSerializer(apps, many=True)
return Response(serializer.data)

Expand All @@ -25,7 +37,7 @@ def list(self, request):
tags=["Apps"],
)
def retrieve(self, request, pk=None):
app = self.service.get_app(pk)
app = self._get_user_app(pk)
serializer = AppSerializer(app)
return Response(serializer.data)

Expand All @@ -43,14 +55,9 @@ def retrieve(self, request, pk=None):
tags=["Apps"],
)
def create(self, request):
print("REQUEST DATA:", request.data)
serializer = AppCreateSerializer(data=request.data)
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)
serializer.is_valid(raise_exception=True)
app = self.service.create_app(serializer.validated_data, user=request.user)
return Response(AppSerializer(app).data, status=status.HTTP_201_CREATED)

@extend_schema(
Expand All @@ -59,7 +66,7 @@ def create(self, request):
tags=["Apps"],
)
def update(self, request, pk=None):
app = self.service.get_app(pk)
app = self._get_user_app(pk)
serializer = AppSerializer(app, data=request.data)
serializer.is_valid(raise_exception=True)
updated_app = self.service.update_app(app, serializer.validated_data)
Expand All @@ -71,7 +78,7 @@ def update(self, request, pk=None):
tags=["Apps"],
)
def destroy(self, request, pk=None):
app = self.service.get_app(pk)
app = self._get_user_app(pk)
self.service.delete_app(app)
return Response(status=status.HTTP_204_NO_CONTENT)

Expand All @@ -85,6 +92,7 @@ def destroy(self, request, pk=None):
)
@action(detail=True, methods=["get"], url_path="metrics/(?P<metric_id>[^/.]+)")
def get_app_metric(self, request, pk=None, metric_id=None):
response_data = self.service.get_metric_dashboard(app_id=pk, metric_id=metric_id)
app = self._get_user_app(pk)
response_data = self.service.get_metric_dashboard(app_id=app.id, metric_id=metric_id)
serializer = MetricResponseSerializer(response_data)
return Response(serializer.data)
15 changes: 15 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import os
from datetime import timedelta
from pathlib import Path

from dotenv import load_dotenv
Expand Down Expand Up @@ -53,6 +54,9 @@
"drf_spectacular",
"django_celery_beat",
"corsheaders",
"rest_framework",
"rest_framework.authtoken",
"users",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -148,6 +152,17 @@
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"EXCEPTION_HANDLER": "config.exceptions.custom_exception_handler",
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}

SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
}

CELERY_BROKER_URL = "redis://redis:6379/0"
Expand Down
1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ def home(request):
path("api/", include("review.urls")),
path("api/", include("metric.urls")),
path("api/polling/", include("polling.urls")),
path("api/users/", include("users.urls")),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 5.1.7 on 2025-05-23 10:40

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("metric", "0007_alter_metricvalue_retrieved_at"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AddField(
model_name="metric",
name="user",
field=models.ForeignKey(
default=2,
on_delete=django.db.models.deletion.CASCADE,
related_name="metrics",
to=settings.AUTH_USER_MODEL,
),
preserve_default=False,
),
migrations.AddField(
model_name="metricvalue",
name="user",
field=models.ForeignKey(
default=2,
on_delete=django.db.models.deletion.CASCADE,
related_name="metrics_values",
to=settings.AUTH_USER_MODEL,
),
preserve_default=False,
),
migrations.AlterField(
model_name="metric",
name="code",
field=models.CharField(max_length=100),
),
migrations.AddConstraint(
model_name="metric",
constraint=models.UniqueConstraint(
fields=("user", "code"), name="unique_user_metric_code"
),
),
]
10 changes: 9 additions & 1 deletion metric/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone

Expand All @@ -6,16 +7,23 @@


class Metric(models.Model):
code = models.CharField(max_length=100, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="metrics")
code = models.CharField(max_length=100)
name = models.CharField(max_length=100)
description = models.TextField()
value_type = models.CharField(max_length=10, choices=MetricValueType.choices)

class Meta:
constraints = [
models.UniqueConstraint(fields=["user", "code"], name="unique_user_metric_code")
]

def __str__(self):
return self.name


class MetricValue(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="metrics_values")
app = models.ForeignKey(App, on_delete=models.CASCADE)
metric = models.ForeignKey(Metric, on_delete=models.CASCADE)
source = models.ForeignKey("source.Source", on_delete=models.CASCADE, null=True, blank=True)
Expand Down
3 changes: 1 addition & 2 deletions metric/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

class MetricRepository:
def get_all(self):
return Metric.objects.filter(id=2)
# return Metric.objects.all()
return Metric.objects.all()

def get_by_id(self, pk):
return Metric.objects.get(id=pk)
Expand Down
Loading