Django: assign foreignkey field with object? - django

models.py
class Order(TimeStampedModel):
name = models.CharField(max_length=50)
class Payment(TimeStampedModel):
order = models.ForeignKey(
'orders.Order',
null=True,
blank=True,
unique=True,
)
What I want to do is to create Payment first and add it to Order.
Let say Order is already created without payment field.
<1>
In [1]: order = Order.objects.first()
In [2]: payment = Payment.objects.create()
In [3]: order.payment = payment
In [4]: order.save()
<2>
In [7]: order.payment_set.add(payment)
I'd like to know what is difference between <1> and <2>. Which one is a right way?

Your first example can be rewritten to reduce DB usage (as it is currently presented, it will make INSERT with payment=NULL, followed by an UPDATE). This should be better:
order = Order.objects.first()
payment = Payment.objects.create(order=order)
Now, to the question. Both approaches are almost identical. However, there are differences if you are using django >= 1.9. .add now performs bulk inserts by default instead of calling save on each instance. This means that if you have any signals like post_save or post_create connected to your Payment model, you won't have them executed unless you explicitly specify bulk=False:
order.payment_set.add(payment, bulk=False)

I think you may have your relation the wrong way around your first scenario; lines 3 and 4 should be:
payment.order = order
payment.save()
1. Setting FK on an instance.
The save method of your Payment is called and you are associating a single Payment with a single Order. Payment.save is called, firing save related signals.
2: Using the RelatedManager.add method.
You can actually add multiple associations at once using this method. So for example you could associate multiple Payments with the one Order using:
order.payment_set.add(payment_1, payment_2, ... payment_N)
Importantly, by default since Django 1.9, the Payments are created without their save() method being called, so you will not get save related signals firing.
On the importance of save signals firing:
It's very common to use signals in Django applications. They provide a powerful way to add functionality to your application without cluttering up your model logic. If your application is using them, bewaring adding associations using the RelatedManager.add approach, as the save related signals will not fire.

Related

Django - adding gamification features

I have a medium size Django REST app that I'm looking to add gamification features to.
The application in question is a school webapp where students can create mockup quizzes, participate in exams that teachers publish, and write didactical content that gets voted by other students and teachers.
I want to add some gamification features to make the app more interesting and to incentivize participation and usage of the various features: for example, each student will have a personal "reputation" score, and gain points upon completing certain actions--a student may gain points when completing a quiz with a high score, when submitting some content, or when receiving upvotes to such content.
The tricky part is I want to be able to have this logic be as separate as possible from the existing codebase, for various reasons: separation of concerns, ability to plug the engine in/out if needed, ability to easily deactivate features for certain groups of users, etc.
What I'm looking for here is some software engineering advice that's also Django-specific. Here's a high level description of what I'm thinking of doing--I'd like some advice on the approach.
create a new gamification app. Here I will have models that describe a change in reputation for a user and possibly other related events. The app should also send notifications when gamification-related events occur
from the gamification app, expose a callback-based interface, which the other primary app can call into to dispatch events
use the django-lifecycle package to call the callbacks from gamification when triggers occur.
This way, my existing models would only get touched to register the triggers from django-lifecycle (similar to signals). For example, let's say I want to give students points when they turn in an assignment. Let's say I have an AssignmentSubmission model to handle assignment submissions. With the added lifecycle hook, it'd look like this:
class AssignmentSubmission(models.Model):
NOT_TURNED_IN = 0
TURNED_IN = 1
STATES = ((NOT_TURNED_IN, 'NOT_TURNED_IN'), (TURNED_IN, 'TURNED_IN'))
user = models.ForeignKey(user)
assignment = models.ForeignKey(assignment)
state = models.PositiveSmallIntegerField(choices=STATES, default=NOT_TURNED_IN)
#hook(AFTER_UPDATE, when="state", was=NOT_TURNED_IN, is_now=TURNED_IN)
def on_turn_in(self):
get_gamification_interface().on_assignment_turn_in(self.user)
The on_assignment_turn_in method might look something like:
def on_assignment_turn_in(user):
ReputationIncrease.objects.create(user, points=50)
notifications.notify(user, "You gained 50 points")
This is pretty much just a sketch to give an idea.
I am unsure how get_gamification_interface() would work. Should it return a singleton? Maybe instantiate an object? Or return a class with static methods? I think it'd be best to have a getter like this as opposed to manually importing methods from the gamification app, but maybe it could also create too much overhead.
What's a good way to handle adding "pluggable" features to a project that are inherently entangled with existing models and business logic while also touching those as little as possible?
The foreign key approach is fine. You can easily chain and link queries using information from existing tables and you could even avoid touching the original code by importing your models to the new app. You can use Django signals in your new app and ditch the django-lifecycle extension to avoid adding lines to your core models. I used the following approach to keep track of modified records in a table; take a TrackedModel with fields field_one, field_two, field_n... which will be tracked by one of your new app's model, namely RecordTrackingModel:
from parent_app.models import TrackedModel # The model you want to track from a parent app.
from django.db.models.signals import post_save # I'm choosing post_save just to illustrate.
from django.dispatch import receiver
from datetime import datetime
class RecordTrackingModel(models.Model):
record = models.ForeignKey(TrackedModel, verbose_name=("Tracked Model"), on_delete=models.CASCADE)
field_one = models.TextField(verbose_name=("Tracked Field One"), null=True, blank=True) # Use same field type as original
field_two = models.TextField(("Tracked Field Two"))
field_n = ...
notes = models.TextField(verbose_name=("notes"), null=True, blank=True)
created = models.DateTimeField(verbose_name=("Record creation date"), auto_now=False, auto_now_add=True)
#receiver(post_save, sender=TrackedModel) # Here, listen for the save signal coming from a saved or updated TrackedModel record.
def modified_records(instance, **kwargs):
record = instance
tracked_field_one = instance.field_one
tracked_field_two = instance.field_two
tracked_field_n = another_function(instance.field_n) #an external function that could take a field content as input.
...
note = 'Timestamp: ' + str(datetime.now().isoformat(timespec='seconds'))
track_record = RecordTrackingModel.objects.create(record=record, field_one=tracked_field_one, field_two=tracked_field_two, field_n=tracked_field_n, ..., notes=note)
return track_record
There's no need to add functions to your pre-existing models as the signal dispatcher triggers whenever a save or delete signal appears at TrackedModel. Then you could place "if" statements for wether or not to perform actions based on field values, i.e.: just pass if an AssignmentSubmission record has a "Not Turned In" status.
Check Django signals reference for more information about when they trigger.
Additionally, I would suggest to change the "state" field to boolean type and rename it to something more explicit like "is_turned_in" for ease of use. It will simplify your forms and code. Edit: For more than 2 choices (non-boolean), I prefer using ForeignKey instead. It will let you modify choices over time easily from the admin site.
Edit:
Another approach could be mirroring the original models in your gamification app and call for a mirror record update when a save method is used in the original model.
gamification_app/models.py:
from parent_app.models import OriginalModel # The model you want to track from a parent app.
from django.db.models.signals import post_save # I'm choosing post_save just to illustrate.
from django.dispatch import receiver
from datetime import datetime
def gamification_function(input, parameters):
output = *your gamification logic*
return output
class MirrorModel(models.Model):
original_model = (OriginalModel, verbose_name=("Original Model"), on_delete=models.CASCADE)
field_one = ... #Same type as the original
field_two = ...
field_n = ...
#hook(AFTER_UPDATE, when="field_n", was=OPTION_1, is_now=OPTION_2)
def on_turn_in(self):
gamification_function(self.field, self.other_field)
#receiver(post_save, sender=OriginalModel) # Here, listen for the save signal coming from the original app change record.
def mirror_model_update(instance, **kwargs):
pk = instance.pk
mirror_model = []
if MirrorModel.objects.get(original_model.pk=pk).exists():
mirror_model = MirrorModel.objects.get(original_model.pk=pk)
mirror_model.field_one = instance.field_one # Map field values
*other logic ...*
mirror_model.save() # This should trigger your hook
else:
mirror_model = MirrorModel(original_model = instance, field_one = instance.field_one, ...)
mirror_model.save() #This should trigger your hooks as well, but for a new record
This way you can decouple the gamification app and even choose not to keep a record of all or the same fields as the original model, but fields specific to your functionality needs.
Your idea was good.
In the gamification app add your views, protect it with LoginRequiredMixin, and extend it with a check if a related record in the AssignmentSubmission table exists and is turned on.
In this way you have a 100% separated gamification views, models, logic, ecc...
In the existing views you can add a link to a gamification view.

Prefetch queryset when related_name="+"

Is it possible without related name (related_name="+") to prefetch objects on the target instance? Sure I know it's not a problem with the related name, but I'm not really sure if it's possible without it.
Here is the example code:
from django.db import models
class Parent(models.Model):
name = models.CharField(max_length=50)
class Child(models.Model):
parent = models.ForeignKey(to=Parent, related_name="+", on_delete=models.CASCADE)
name = models.CharField(max_length=50)
Parent.objects.all().prefetch_related('child_set')
Maybe it's possible using the Prefetch(lookup, queryset=None, to_attr=None) object, because it takes the queryset in the argument list?
Looked through the code a bit and found this line:
rel_obj_descriptor = getattr(instance.__class__, through_attr, None)
Here instance is the model instance, and through_attr is the field name of related instance to be fetched. This line basically tries to get a related descriptor to perform the prefetch query. In your case rel_obj_descriptor would contain None.
To answer your question no it is not possible at least for a Foreign Key, there may be some hack for Many to Many relationships as Django appears to use some internal descriptors for them.
I would advice you to simply not set related_name="+" since you want to use the backwards relation here. You say "It's because of separation of concerns between multiple apps" but that does not make much sense. Don't we set a foreign key to the user model for various other models anyway and still use the related name? Does the point of separation of concerns arise there(the user model is in a separate app)?
try
parent = Parent.objects.get(id=pk)
parent.child_set.all()
I don't know if having related_name = '+' prevents this situation, but if you never define related_name, you can definitely use this method.

User model with static set of related models

Say I have a User model in django and I want to add some achievements to users. So I've created an Achieve model:
class Achive:
type = ....
value = ....
status = BooleanField(default=False)
I want all those achieves be a static set of models for every user (20 instances, for example) with ability to delete old and create new achieves. The problem is how to do it. Expected flow is:
1) user granted to use achievement system;
2) user got all those achieves (in admin panel shows like a table);
3) in admin panel per user I can change status of every achieve (affects only on edited user);
4) if new Achieve instance is created — add to all users who have achievements;
5) if existed Achieve instance has been deleted — remove from all users;
Solutions with I came up:
1) use Achieve model with jsonfield. store achieves in json like dictionary, use custom widget for admin panel to show checkboxes to change status). But where to store global set of achievements to create new/delete old ones? how to manage it?
2) use many to many field to Achieve and Achieve model without status. Why: if relation between User ← → Achieve exists, that means that user earn an achieve.
Both solutions I don't really like so hope for your advice.
P.S. sqlite is used as db and not allowed to use another (like mongo, etc.)
Thanks in advance!
What you want is a ManyToMany relationship between Achieve and User, but with the ability to store extra data on the relationship (the status for example).
With a normal ManyToManyField on a Model, Django actually creates an intermediate model to store the relationships in the database. By adding a through argument to your ManyToManyField, you can specify the intermediate model used for the relationship and store extra data with the relationship, as documented here:
class Goal(models.Model):
type = ...
value = ...
achievers = models.ManyToManyField(to=User, through='Achievement', related_name='goals')
class Achievement(models.Model):
status = models.BooleanField()
date_reached = models.DateField(null=True)
goal = models.ForeignKey(to=Goal, on_delete=models.CASCADE)
achiever = models.ForeignKey(to=User, on_delete=models.CASCADE)
then you can create and query the relationships like this, assuming you have a user and a goal:
achievement = Achievement.objects.create(status=True, date_reached=date(2018, 10, 12), achiever=user, goal=goal)
user.goals.filter(achievement__status=True) # gives the achieved goals of a user
goal.achievers.filter(achievement__status=True) # gives the users that achieved a goal

Combined forms in Django for attributes of parent model

Having a lot of models, I have started to factor out common blocks with the aim of my database reaching second normal form. As the application should be used by a sales team, most entries are some kind of orders. An excerpt from my models file looks like this:
class Order(models.Model):
dl = models.CharField(max_length=100)
cl = models.CharField(max_length=100)
(...)
class Setup(models.Model):
order = models.ForeignKey(Order) # could be OneToOneField()
name = models.CharField(max_length=200)
package = models.CharField(choices=(
('S', 'Small'),
('M', 'Medium'),
('L', 'Large'),
('XL', 'Extra large'),
('C', 'Custom')
), max_length=2)
server = models.ForeignKey(Webserver)
(...)
While it does not make any logical sense to keep the order details out of the model for a setup order, it helps to keep the project maintainable, since setup orders are not the only things coming in and this way you can change the order model and all other models/orders are updated too.
Here comes the problem. For attributes like Setup.server Django's default behaviour of creating a dropdown with all web servers the company can offer is totally fine. If the team decides to add another one, it can simply create another server option on another page. But for order, I would like that the OrderForm is included on the same page as the SetupForm, ideally as a separate Fieldset. After submitting the form, the new order should be added to the database and Setup.order is to be filled. I know that I can code it for this special case, but the application will contain numerous forms, so a generic solution would be better.
A possible solution could be to create a custom models.ForeignKey or models.OneToOneField with a custom option and a generic view that than renders a template with two forms and links the objects afterwards.
Did anyone have a similar problem? Does anyone know a simple, maybe even builtin solution to that?
EDIT 1
I have thought about inline formsets which were the solution to a similar question which used this example
class Contact(models.Model):
...
class Communication(models.Model):
contact = models.ForeignKey(Contact)
and by using a communication fieldset. However, this treats the class containing as the child, which is not the case in my example. But it still is an option, even if it would still have to be automated so it can be used quickly for all links between the other models.

Django pre_save signal called when commit=False

I am writing a Django application that will track changes to the models, in a similar way to the admin interface. For example, I will be able to display a list of changes to a model, that look something like Changed Status from Open to Closed.
I am using the pre_save signal to do this, comparing the relevant fields between the existing item in the database, and the "instance" which is being saved. To get the existing item, I have to do sender._default_manager.get(pk=sender.pk) which seems a bit messy, but that part works.
The problem is, the view for changing this model calls the save() method on the form twice (first with commit=False) - this means that 2 changes get logged in the database, as the pre_save signal is emitted twice.
Is there any way I can accomplish this? Maybe in a different way altogether, though I remember reading that the Django admin app uses signals to track changes that users make.
Looking through the Django source, it seems that pre_save signals are sent on every call to save, even if commit is false. I would suggest inserting on the first pre_save, but add a flag column to the changes table, e.g.
class FooChanges(models.Model):
foo = models.ForeignKey(Foo)
dt = models.DateTimeField(default=datetime.now)
field = models.CharField(max_length=50)
value = models.CharField(max_length=50) # Or whatever is appropriate here
finished = models.BooleanField(default=False)
Then, your presave can be:
def pre_save_handler(sender, instance):
foo_changes, created = FooChanges.objects.get_or_create(foo=instance, finished=False, field='Status', value=instance.status)
if not created:
foo_changes.finished = True
foo_changes.save()
So on the first pre_save, you actually insert the change. On the second pass, you retrieve it from the database, and set the flag to false to make sure you don't pick it up the next time Foo's status changes.
use dispatch_uid:
http://docs.djangoproject.com/en/1.2/topics/signals/#preventing-duplicate-signals
Django Audit Log
django-audit-log is a pluggable app that does exactly what you want with little effort. I've used it in a project and I'll surely use it in many more now that I know it.