I have two models Project and Shift with a Many-to-one relationship. And I want to collect statistics for all Shift objects.
How this should happen:
The User sends a GET request with a parameter that specifies which Project to calculate statistics for all its Shift, the function is called which calculates statistics and puts it all into the list and returns it, the list should be serialized and sent to the User. But I get an error when I send it.
[AttributeError: 'list' object has no attribute 'user_project']
If I needed to serialize a model or QuerySet I probably wouldn't have any problems, but here I have a regular list. I wrote a separate serializer specifically for values from this list, but nothing works. Most likely I wrote it wrong.
If there is a better way to do it, please advise me.
models.py
class Project(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE)
task = models.CharField(max_length=128)
technical_requirement = models.TextField()
customer = models.CharField(max_length=64, blank=True)
customer_email = models.EmailField(blank=True)
start_of_the_project = models.DateField()
salary_per_hour = models.FloatField()
project_cost = models.FloatField(blank=True, default=0)
project_duration = models.DurationField(blank=True, default=datetime.timedelta(0))
class Shift(models.Model):
user_project = models.ForeignKey('Project', on_delete=models.CASCADE)
shift_start_time = models.DateTimeField()
shift_end_time = models.DateTimeField()
shift_duration_time = models.DurationField(blank=True, null=True)
salary_per_shift = models.FloatField(blank=True, null=True)
serializers.py
class ShiftStatisticSerializer(serializers.Serializer):
user_project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all(), many=True)
number_of_shifts = serializers.IntegerField()
number_of_hours = serializers.FloatField()
duration_mean = serializers.FloatField()
salary_mean = serializers.FloatField()
project_cost = serializers.FloatField()
views.py
class ShiftStatisticView(APIView):
queryset = Shift.objects.all()
authentication_classes = [authentication.JWTAuthentication]
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
serializer = ShiftStatisticSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
valid = serializer.validated_data.pop('user_project')
query = self.get_queryset()
statistic = project_services.get_shift_statistics(validated_data=valid[0], queryset=query)
serializer = ShiftStatisticSerializer(statistic)
return Response(serializer.data)
def get_queryset(self):
return self.queryset.filter(user_project_id=self.request.data['user_project'])
project_services.py
def get_shift_statistics(validated_data, queryset):
df = pd.DataFrame(list(queryset.values()))
number_of_shifts = df["id"].count()
number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h')
duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h')
salary_mean = df["salary_per_shift"].mean()
statistics = [validated_data,
number_of_shifts,
number_of_hours,
duration_mean,
salary_mean,
validated_data.project_cost,]
return statistics
Traceback:
Traceback (most recent call last):
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 84, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 83, in get
return Response(serializer.data)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 555, in data
ret = super().data
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 253, in data
self._data = self.to_representation(self.instance)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 509, in to_representation
attribute = field.get_attribute(instance)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 538, in get_attribute
relationship = get_attribute(instance, self.source_attrs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/fields.py", line 97, in get_attribute
instance = getattr(instance, attr)
AttributeError: 'list' object has no attribute 'user_project'
When I remove many=True, queryset like in David Lu's answer.
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 1009, in _bootstrap_inner
self.run()
File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 946, in run
self._target(*self._args, **self._kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/autoreload.py", line 64, in wrapper
fn(*args, **kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/commands/runserver.py", line 134, in inner_run
self.check(display_num_errors=True)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/base.py", line 487, in check
all_issues = checks.run_checks(
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/registry.py", line 88, in run_checks
new_errors = check(app_configs=app_configs, databases=databases)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 14, in check_url_config
return check_resolver(resolver)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 24, in check_resolver
return check_method()
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 480, in check
for pattern in self.url_patterns:
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 696, in url_patterns
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__
res = instance.__dict__[self.name] = self.func(instance)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 689, in urlconf_module
return import_module(self.urlconf_name)
File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 883, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/Users/Tokio/Developer/ShiftBot/ShiftBot/urls.py", line 7, in <module>
path('shift/', include('shift.urls')),
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/conf.py", line 38, in include
urlconf_module = import_module(urlconf_module)
File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 883, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/Users/Tokio/Developer/ShiftBot/shift/urls.py", line 2, in <module>
from shift import views
File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 10, in <module>
from shift.serializers import ProjectSerializer, ShiftSerializer, ShiftStatisticSerializer
File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 31, in <module>
class ShiftStatisticSerializer(serializers.Serializer):
File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 32, in ShiftStatisticSerializer
user_project = serializers.PrimaryKeyRelatedField()
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 252, in __init__
super().__init__(**kwargs)
File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 107, in __init__
assert self.queryset is not None or kwargs.get('read_only'), (
AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
CodePudding user response:
I think user_project
field is the foreign key field and the attribute should not be the list type. You need to remove the many=True
, queryset
attribute in the PrimaryKeyRelatedField
.
class ShiftStatisticSerializer(serializers.Serializer):
user_project = serializers.IntegerField()
...
CodePudding user response:
The entire purpose of serializing data is to take python object(s) and put them in a format that can be parsed into a json response OR to take a bunch of external data and validate it before doing something with - generally creating or updating a model. Here, you seem to be expecting only an id from your request and want to generate data to pass back. You seem to be basically doing that in your get_shift_statistics
function. If you just want to return a list with no keys, I would just return the result of that function wrapped in a Django Rest Framework (DRF) Response
object.
Your get request would then look something like this:
def get(self, request):
statistic = project_services.get_shift_statistics(self.get_queryset())
return Response(statistic)
and you could rewrite your get_shift_statistics
function to remove the validated_data
bit. You are not validating data that's passed in, you're calculating it.
def get_shift_statistics(queryset):
df = pd.DataFrame(list(queryset.values()))
number_of_shifts = df["id"].count()
number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h')
duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h')
salary_mean = df["salary_per_shift"].mean()
statistics = [number_of_shifts,
number_of_hours,
duration_mean,
salary_mean,
queryset.first().user_project.project_cost,]
return statistics
As to why you are getting the error you are - the serializer expects data in a particular format: either a Django model or a key-value pair dictionary. If you pass it a flat list, it doesn't know how to process it. Also the output of a DRF serializer is always going to be a dictionary, where the keys are the listed attributes and the values are the values the serializer is able to determine from the data you pass it. So a serializer used like this is not really appropriate if you want to return a flat list.
That being said, you CAN pass a list into a DRF serializer, but you have to include the many=True
flag, and it will interpret said list as a list of objects to serialize and will then serialize each individually and return back a list of serialized objects.
Is there any reason you require a list on the front end as opposed to a dictionary? I've found that in most cases returning a properly-keyed json object from the backend is often the superior way of passing data. This way you can serialize the data more naturally and use the DRF more in the way it is intended. You can still iterate over the values in the dictionary and have the added benefit of being able to grab any single value by key rather than having to know which index it is located at in a list.