Catch model objects unchanged in admin interface - python

I have models Car and Seat, with Seat having a foreign key to Car.
In models.py:
class Car(models.Model):
# ...
class Seat(models.Model):
car = models.ForeignKey(Car)
# ...
In the admin interface, seats can be added to a car when adding/changing a car (using inline fields).
In admin.py:
class CarAdmin(admin.ModelAdmin):
# ...
inlines = [SeatInline]
class SeatInline(admin.StackedInline):
model = Seat
extra = 1
When a user adds/changes/deletes a car or adds/changes/delete seats via the CarAdmin, I need to log what he did and therefore needs to compare in particular all the seats before and after the change.
The question is:
How to get two lists of seat objects seats_before and seats_after in order to compare them? This needs to be done somewhere where I can access request.user.
I tried to do that in save_formset(), but it did not work. One of the problems: To get seats_after, it is apparently only possible to obtain the list of seats that have been modified, i.e. there is no way to make the difference between an unchanged seat and a deleted seat.
def save_formset(self, request, form, formset, change):
# ...
instances = formset.save(commit=False)
seats_after = []
for instance in instances:
if isinstance(instance, Seat):
seats_after.append(instance) # Unchanged seats are not added here
# ...
I also have trouble to get seats_before in this function.
EDIT:
Following defuz' suggestion, I tried this piece of code:
def save_related(self, request, form, formsets, change):
car_before = form.save(commit=False)
seats_before = car_before.seats_set.all()
form.save_m2m()
seats_after = []
for formset in formsets:
instances = formset.save()
for instance in instances:
if isinstance(instance, Seat):
seats_after.append(instance)
In this example there are two problems:
seats_before seems to contain the new seats instead of the old ones, like if the related objects were saved when the form was saved.
seats_after contains only changed/added seats. Unchanged and deleted seats do not appear (and I would like to have the unchanged seats included).

Use ModelAdmin.save_related: method.

Django signals might be what you're looking for.
https://docs.djangoproject.com/en/dev/ref/signals/

Related

Django REST API query on related field

I have 3 models, Run, RunParameter, RunValue:
class Run(models.Model):
start_time = models.DateTimeField(db_index=True)
end_time = models.DateTimeField()
class RunParameter(models.Model):
parameter = models.ForeignKey(Parameter, on_delete=models.CASCADE)
class RunValue(models.Model):
run = models.ForeignKey(Run, on_delete=models.CASCADE)
run_parameter = models.ForeignKey(RunParameter, on_delete=models.CASCADE)
value = models.FloatField(default=0)
class Meta:
unique_together=(('run','run_parameter'),)
A Run can have a RunValue, which is a float value with the value's name coming from RunParameter (which is basically a table containing names), for example:
A RunValue could be AverageTime, or MaximumTemperature
A Run could then have RunValue = RunParameter:AverageTime with value X.
Another Run instance could have RunValue = RunParameter:MaximumTemperature with value Y, etc.
I created an endpoint to query my API, but I only have the RunParameter ID (because of the way you can select which parameter you want to graph), not the RunValue ID directly. I basically show a list of all RunParameter and a list of all Run instances, because if I showed all instances of RunValue the list would be too long and confusing, as instead of seeing "Maximum Temperature" you would see:
"Maximum Temperature for Run X"
"Maximum Temperature for Run Y"
"Maximum Temperature for Run Z", etc. (repeat 50+ times).
My API view looks like this:
class RunValuesDetailAPIView(RetrieveAPIView):
queryset = RunValue.objects.all()
serializer_class = RunValuesDetailSerializer
permission_classes = [IsOwnerOrReadOnly]]
And the serializer for that looks like this:
class RunValuesDetailSerializer(ModelSerializer):
run = SerializerMethodField()
class Meta:
model = RunValue
fields = [
'id',
'run',
'run_parameter',
'value'
]
def get_run(self, obj):
return str(obj.run)
And the URL just in case it's relevant:
url(r'^run-values/(?P<pk>\d+)/$', RunValuesDetailAPIView.as_view(), name='values_list_detail'),
Since I'm new to REST API, so far I've only dealt with having the ID of the model API view I am querying directly, but never an ID of a related field. I'm not sure where to modify my queryset to pass it an ID to get the appropriate model instance from a related field.
At the point I make the API query, I have the Run instance ID and the RunParameter ID. I would need the queryset to be:
run_value = RunValue.objects.get(run=run_id, run_parameter_id=param_id)
While so far I've only ever had to do something like:
run_value = RunValue.objects.get(id=value_id) # I don't have this ID
If I understand correctly, you're trying to get an instance of RunValue with only the Run id and the RunParameter id, i.e. query based on related fields.
The queryset can be achieved with the following:
run_value = RunValue.objects.get(
run__id=run_id,
run_parameter__id=run_parameter_id
)
Providing that a RunValue instance only ever has 1 related Run and RunParameter, this will return the instance of RunValue you're after.
Let me know if that's not what you mean.
The double underscore allows you to access those related instance fields in your query.
Well its pretty simple, all you have to do is override the get_object method, for example(copy pasted from documentation):
# view
from django.shortcuts import get_object_or_404
class RunValuesDetailAPIView(RetrieveAPIView):
queryset = RunValue.objects.all()
serializer_class = RunValuesDetailSerializer
permission_classes = [IsOwnerOrReadOnly]]
lookup_fields = ["run_id", "run_parameter_id"]
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs[field]: # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
# url
url(r'^run-values/(?P<run_id>\d+)/(?P<run_parameter_id>\d+)/$', RunValuesDetailAPIView.as_view(), name='values_list_detail'),
But one big thing you need to be careful, is not to have duplicate entries with same run_id and run_parameter_id, then it will throw errors. To avoid it, either use unique_together=['run', 'run_parameter'] or you can use queryset.filter(**filter).first() instead of get_object_or_404 in the view. But second option will produce wrong results when duplicate entries are created.

How to update another model field from updateview

How can I use the current value of a field in update view to update another field?
I have a model employee. It has a field day_absent and amount_absent which shows the total deduction amount based on how many day_absent.
In update view the can set the day_absent.
When I can back to DetailView, say the day_absent is 1. but the deduction is still 0. I understand that this is because prior to save(), day_absent was still 0 and was changed to 1. So the question is how can i compute whatever is value entered in day_absent before it gets saved?
class PayrollTransactionUpdate(LoginRequiredMixin,UpdateView):
model = t_pay
template_name = 'payroll/transaction/update.html'
fields = ['day_absent']
def post(self,request,pk):
emp = t_pay.objects.get(pk=pk)
emp.amt_absent = emp.day_absent * emp.amt_rate
emp.save()
return super().post(request)
The UpdateView saves the object in the form_valid() method. Look at this invaluable site when you're using Django class-based views.
So you should override the form_valid() method, not the post() method:
def form_valid(self, form):
emp = form.save(commit=False)
emp.amt_absent = emp.day_absent * emp.amt_rate
emp.save()
self.object = amp
return super().form_valid(form)
Note that saving a field to Employee that can easily be calculated from two other fields is not recommended, as it could lead to inconsistent/corrupt data. Since amt_absent is just the multiplication of two other fields, why do you need to save it?
Alternatively, looking at what you're actually doing, this does not seem the responsibility of the view. If this is supposed to happen every time an Employee is saved, you could do it on the model itself:
class Employee(Model):
... # fields go here
def save(self, **kwargs):
self.amt_absent = self._get_amt_absent()
super().save(**kwargs)
# update other models here as well
In this situation, you don't need to override anything on the UpdateView.

ManyToMany on Self Model: How to add new instances to this relation

I have a Profile class, which extends the User class, and I would like to allow every profile to be able to bookmark other user profiles.
I've set up the following:
class Profile(models.Model):
# some fields e.g. name, email
bookmarked_profiles = models.ManyToManyField("self")
Now, I'm a bit confused, for example, how would I 'bookmark' new profiles for a given profile instance?
E.g. something like
# add new bookmark for profile instance
p = Profile.objects.get(pk=1)
profiles_to_bookmark = Profile.objects.all()
p.bookmarked_profiles = profiles_to_bookmark
I would also like to know the number of bookmarked profiles e.g.
p.bookmarked_profiles.count()
Check in django docs, there is everything that you need there
for profile in profiles_to_bookmark:
p.bookmarked_profiles.add(profile) # WIll add New objects
p.bookmarked_profiles.all() # Will return all objects
p.bookmarked_profiles.all().count() # Will return count of objects
https://docs.djangoproject.com/en/2.0/topics/db/examples/many_to_many/strong text
You can do so by adding the queryset via add() available in ManyToMnayField()
p = Profile.objects.get(pk=1)
profiles_to_bookmark = Profile.objects.all()
p.bookmarked_profiles.add(*profiles_to_bookmark) #Note the star *
And with all() you can set all bookmarked profiles, and count() will return the total
p.bookmarked_profiles.all()
p.bookmarked_profiles.count()

Creating a display-only (non-editable) Django admin field

Is it possible to build a custom model field/widget combination which displays a value but never writes anything back to the database? I would use this widget exclusively in the admin's forms.
I wrote my own field, which overwrites the formfield() method to declare its own widget class. It displays just fine, but as soon as the 'Save' button is clicked in the admin, I'm getting a validation error:
This field is required.
That makes sense, considering that my widget didn't render out a form field. However, what I'd like to do is basically remove this field from the update process: whenever used in the admin, it just shouldn't be mentioned in the SQL UPDATE at all.
Is that possible?
Here's a sketch of the code I have so far:
class MyWidget(Widget):
def render(self, name, value, attrs=None):
if value is None:
value = ""
else:
# pretty print the contents of value here
return '<table>' + ''.join(rows) + '</table>'
class MyField(JSONField):
def __init__(self, *args, **kwargs):
kwargs['null'] = False
kwargs['default'] = list
super(MyField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {
'form_class': JSONFormField,
'widget': MyWidget,
}
defaults.update(**kwargs)
return super(MyField, self).formfield(**defaults)
UPDATE 1: The use case is that the field represents an audit log. Internally, it will be written to regularly. The admin however never needs to write to it, it only has to render it out in a very readable format.
I'm not using any other ModelForms in the application, so the admin is the only form-user. I don't want to implement the behavior on the admin classes themselves, because this field will be reused across various models and is always supposed to behave the same way.
There are multiple ways to create a read-only field in the admin pages. Your requirements on the database storage are a bit fuzzy so I go through the options.
You have to register an AdminModel first in admin.py:
from django.contrib import admin
from yourapp.models import YourModel
class YourAdmin(admin.ModelAdmin):
pass
admin.site.register(YourModel, YourAdmin)
Now you can add different behavior to it. For example you can add the list of fields shown in the edit/add page:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
This can be names of the model fields, model properties or model methods. Methods are displayed read-only.
If you want to have one field read-only explicitly add this:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
readonly_fields = ['field2']
Then you have the option to overwrite the display of the field completely by adding a method with the same name. You will not even need a model field/method with that name, then:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
readonly_fields = ['field2']
def field2(self, obj):
return '*** CLASSIFIED *** {}'.format(obj.field2)
With django.utils.safestring.mark_safe you can return HTML code as well.
All other options of the Admin are available, except the widget configuration as it applies to the writable fields only.
I might be a little confused as to what you want but you might want to look into model properties. Here is an example for my current project.
Code inside your model:
class Textbook(models.Model):
#other fields
#property
def NumWishes(self):
return self.wishlist_set.count()
Then you can just display it on the admin page.
class Textbook_table(admin.ModelAdmin):
fields = ["""attributes that are saved in the model"""]
list_display = ("""attributes that are saved in the model""", 'NumWishes'')
So now I can display NumWishes in the admin page but it doesn't need to be created with the model.
Hello in the class admin modify the permission method
#admin.register(my_model)
class My_modelAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False

Is there a way to automatically add manager methods or class level methods to admin page in Django?

Let's say I have a class Person, which has bunch of manager methods and class level methods. I have the Person enabled in the admin page, but I am trying to find a way so that all these manager/class level methods automatically show on the admin page too.
class PersonManager(models.Manager):
def get_status(self, *args, **kwargs):
# code to get the status of selected persons
class Person(models.Model):
objects = PersonManager()
name = models.CharField(max_length=100)
def email_person(self, email):
#code to email the specific person
So for example, the email_person method would show up when I goto the detailed view on the admin page after selecting an instance. Or one of the manager methods can be invoked in the page of the admin that lists all the instances and I can select which instances I want the manager method to run on. Is there any way like that? I found an article that manually shows how to do so, but I was wondering if there is an existing solution/app that does that by default? http://patrick.arminio.info/additional-admin-views/
You can add admin actions to the list view by specifying them in your ModelAdmin:
class PersonAdmin(admin.ModelAdmin):
actions = ['some_action']
def some_action(self, queryset, request):
queryset.some_action()
some_action.short_description = "Do some action on selected persons"
Adding it to the detail view requires more work, but I've found that this method is generally sufficient.

Categories