Django: Making a Notification Engine - python

I've been building a email/sms notification engine. A person or group can be subscribed to an object and if the object gets updated, the people/groups will be notified of the change by email/sms.
Currently, I've implemented it as below:
models.py
class Subscription(models.Model):
# subscribers
people = models.ManyToManyField(Person)
groups = models.ManyToManyField(Group)
# mandatory fields for generic relation
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
mixins.py
class NotificationMixin(object):
def perform_update(self, serializer):
model_name = str.lower(serializer.Meta.model)
old_obj = model.objects.get(id=serializer.data['id'])
obj = serializer.save()
self.notify(model_name, old_obj, obj)
def notify(self, model_name, old_obj, obj):
# All models have a GenericRelation field for reverse searching
subscriptions = Subscription.objects.filter(**{ model_name: obj })
// *rest of logic to iterate over subscriptions and email people/groups
Using django's ContentType generic relations, I can subscribe a person/group to any object.
I want to add the capability to create global subscriptions using the same Subscription model so that they are all stored in the same table. A global subscription will not have an object that it is tracking, but when any object of a specific model is triggered, emails will be sent.
I'm having trouble generalizing my subscription model to be able to accept a model instance or the model for triggering a response.
The functionality I want:
Global Subscriptions
People/Groups updated by if any object of the model X is changed
Object Level Subscriptions
People/Groups updated if specific object is updated
Is the current model/architecture that I have a good way to go about this problem, or should I approach this differently?
Note The frontend is in AngularJs, so this is exclusively interacting with our django api.

For anybody who may want to find a solution to this, I ended up doing:
class Subscription(models.Model):
"""
Model for subscribing to object changes.
Can be subscribed to any object or any model type.
Subcriptions:
model - if any object changes of this type that belongs to the company, update
object - if that specific object changes, update
To create:
Either give a content_object or a content_type. Content object is a model instance. Content type is a ContentType (i.e. Study, Product, etc.)
If it is created wrong, will just be lost in database forever (unless you know it's there, then delete it.)
"""
people = models.ManyToManyField(Person)
groups = models.ManyToManyField(Group)
trigger = models.CharField(max_length=50)
APP_LABELS = [apps to limit the available choices]
# for object subscription
# mandatory fields for generic relation
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(null=True)
content_object = GenericForeignKey('content_type', 'object_id')
def save(self, *args, **kwargs):
'''
Save logic to validate the Subscription model. This is run after the native create() method.
'''
# check if no content_object, then content_type must be defined by user
# probably not necessary since it will fail at creation if content_type isn't an instance of a ContentType, but good to double check
if not self.content_object:
if self.content_type.__class__ != ContentType:
if type(self.content_type) == str:
# if content_type is a string designating a model
self.content_type = ContentType.objects.get(model=self.content_type)
else:
# if content_type is a model class
self.content_type = ContentType.objects.get_for_model(Study)
apps = ', '.join(map(str, self.APP_LABELS))
# check if content_type in our defined apps
if self.content_type.app_label not in apps:
raise ValidationError('Please select a content_object or content_type in apps: {0}'.format(apps))
super(Subscription, self).save(*args, **kwargs)

Related

How to auto populate a read-only serializer field in django rest framework?

I have a question regarding django rest framework.
Most of the time, I have a serializer which has some read-only fields. For example, consider this simple model below:
class PersonalMessage(models.Model):
sender = models.ForeignKey(User, related_name="sent_messages", ...)
recipient = models.ForeignKey(User, related_name="recieved_messages", ...)
text = models.CharField(...)
def __str__(self) -> str:
return f"{self.text} (sender={self.sender})"
In this model, the value of sender and recipient should be automatically provided by the application itself and the user shouldn't be able to edit those fields. Alright, now take a look at this serializer:
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
It perfectly prevents users from setting an arbitrary value on the sender and recipient fields. But the problem is, when these fields are marked as read-only in the serializer, the serializer will completely ignore all the values that are passed into the constructor for these fields. So when I try to create a model, no values would be set for these fields:
PersonalMessageSerializer(data={**request.data, 'sender': ..., 'recipient': ...) # Won't work
What's the best way to prevent users from setting an arbitrary value and at the same time auto-populate those restricted fields in django rest framework?
Depending on how you get those two objects, you can use the serializer's save method to pass them, and they will automatically be applied to the object you are saving:
sender = User.objects.first()
recipient = User.objects.last()
serializer = PersonalMessageSerializer(data=request.data)
message = serializer.save(sender=sender, recipient=recipient)
The kwargs should match the field names in your model for this to work. For reference, have a look here
You able to override the serializer context like this;
PersonalMessageSerializer(data={**request.data, context={'sender': sender, 'recipent': recipent})
and catch the context inside serializer.
class PersonalMessageSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')
def validate(self, attrs):
attrs = super().validate(attrs)
attrs['sender'] = self.context['sender']
attrs['recipent'] = self.context['recipent']
return attrs
now serializer.validated_data it must returns sender and recipent.
From the question it is not possible to understand what field(s) of the relationship with sender and recipient you want to interact with, but a general answer can be found in the Serializer relations section of Django REST documentation.
Long story short, if you want to interact with one field only, you can use SlugRelatedField, which lets you interact with the target of the relationship using only one of its fields.
If it just the id, you can use PrimaryKeyRelatedField.
If you want to interact with more than one field, the way to go is Nested Relationships. Here you can specify a custom serializer for the target relationship, but you will have to override the create() method in your PersonalMessageSerializer to create the object from your relationship, as nested serializers are read-only by default.
So this is how you can make set a default on create but read only after in DRF. Although in this solution it wont actually be readonly, it's writable, but you now have explicit control on what the logged in user can write, which is the ultimate goal
Given the model
class PersonalMessage(models.Model):
sender = models.ForeignKey(User,...)
recipient = models.ForeignKey(User,..)
text = models.CharField(...)
You would first create your own custom default (I will show an example for only one field)
# Note DRF already has a CurrentUserDefault you can also use
class CurrentSenderDefault:
requires_context = True
def __call__(self, serializer_field):
return serializer_field.context['request'].user
def __repr__(self):
return '%s()' % self.__class__.__name__
Next you make your own field, that knows whats up with the filter.
This queryset prevents people from setting a value they are not allowed to. which is exactly what you want
class SenderField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
if user:
queryset = User.objects.filter(id=user.id)
else:
queryset = User.objects.none()
return queryset
Finally on the serialiser you go
class PersonalMessageSerializer(serializers.ModelSerializer):
sender = SenderField(default=CurrentSenderDefault())
recipient = ...
class Meta:
model = PersonalMessage
fields = '__all__'
read_only_fields = ('sender', 'recipient')

Django auto save m2m relationship in form using through table

I have a m2m relationship between Servers and Products in Django with a through table called ServerProducts.
class ServerProduct(TimeStampedModel):
# Additional fields may be required in the future
server = models.ForeignKey('Server', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
class Server(TimeStampedModel):
name = models.CharField(max_length=35)
# ...
products = models.ManyToManyField('Product', through='ServerProduct',
related_name='products', blank=True)
class Product(TimeStampedModel):
name = models.CharField(max_length=45, unique=True)
# ...
servers = models.ManyToManyField(
'Server', through='ServerProduct', related_name='servers')
In my view I have a form which allows users to create a Server and select from a list of all products for the Server to be associted with.
In order to create the ServerProduct objects (for the through table) on each save I have to write the following code inside save().
class ServerForm(forms.ModelForm):
class Meta:
model = Server
fields = '__all__'
def save(self, commit=True):
instance = super(ServerForm, self).save(commit=False)
instance.save()
if instance.products.count():
instance.products.clear()
for product in self.cleaned_data['products']:
ServerProduct.objects.create(server=instance, product=product)
return instance
I want to be able to reuse the form for both Create and Update views. Hence why I have to check if the current Server is associated with any products, and then do instance.products.clear(). To make sure it removes any previous products if they get deselected by a user.
This entire process feels unecessary, especially when I've read a lot about Django's built-in form.save_m2m() method. My question is, is there a simpler way do achieve what I'm doing using Django built-in's?

Casting objects in django model

I am making a django project with models.py having following code:
class Record(models.Model):
id = models.AutoField(primary_key=True)
class TTAMRecord(Record):
client = models.ForeignKey(TTAMClient)
class TTAMClient(models.Model):
...
class Account(models.Model):
records = models.ManyToManyField(Record)
...
I also have following code to insert a TTAMRecord into the records of an Account:
account = Account.objects.get(...)
client = TTAMClient.objects.create(...)
record = TTAMRecord.objects.create(..., client = client, ...)
account.records.add(record)
What I want to do(but can't) is to call the client within the record object of an account; e.g.:
account = Account.objects.get(...)
for record in account.records.all():
client = record.client
...
However, if I am not allowed to do this, since record here is stored as a Record (which doesn't have client) type instead of TTAMRecord(which has client) type...
Any idea how to cast the object?
I want to use the more generic Record instead of TTAMRecord for some purposes not stated here...
As Record is not abstract model, it has its own table and life cycle as other models. However, you can access the corresponding client object as record.ttamclient, so you can change your line to
account = Account.objects.get(...)
for record in account.records.all():
client = record.ttamclient
...
However, this is little cumbersome if you have multiple derived classes. In such cases you would have to know which derived class you are referring to and use the corresponding attribute name.
If I understood your concept of "cast" it will not work in the way you described.
However, to get model inheritance to work you need to use abstract models (see docs)
class Record(models.Model):
# Some generic fields
class Meta:
abstract = True
class TTAMRecord(Record):
client = models.ForeignKey(TTAMClient)
If you need both Record and TTAMRecord to be stored in Account you will need to use polymorphic relationships, that in Django it's called Generic Relations (see docs)
You will need an intermediary model to store these generic relations. So, basically you will have a AccountRecord and a Account model:
class AccountRecord(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
class Account(models.Model):
records = models.ManyToManyField(AccountRecord)
And so you can do:
account = Account.objects.get(...)
for record in account.records.all():
record_content = record.content_object
if isinstance(record_content, TTAMRecord):
client = record_content.client
else:
# No client available

tastypie - disable nested objects creation

When i'm creating a new resource with a foreign relation, specified as, i.e., {"pk": 20}, i get a new unwanted FK-item created.
I have Order model class with a relations to the Language model, so when creating an Order instance, i may have to specify the order's languages. Language list should be constant, and the users must not have an ability to modify existant or to create the new languages.
Order resource:
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True)
dst_lang = fields.ForeignKey(LanguageResource, 'dst_lang', null=True, full=True)
def obj_create(self, bundle, request=None, **kwargs):
return super(OrderResource, self).obj_create(bundle, request, user=request.user)
class Meta:
resource_name = 'orders'
queryset = Order.objects.all()
serializer = Serializer(['json'])
authentication = MultiAuthentication(SessionAuthentication(), ApiKeyAuthentication())
authorization = ResourceAuthorization()
And here is a Language resource:
class Language(models.Model):
name = models.CharField(max_length=100)
code = models.CharField(max_length=100)
class LanguageResource(ModelResource):
class Meta:
resource_name = 'languages'
queryset = Language.objects.all()
allowed_methods = ['get']
authorization = ReadOnlyAuthorization()
serializer = Serializer(['json'])
I'm trying to create a new Order with jQuery:
var data = JSON.stringify({
"comment": "Something random",
"src_lang": {"pk": "20"},
"dst_lang": "/api/v2/languages/72/"
});
$.ajax({
type: 'POST',
url: '/api/v2/orders/',
data: data,
dataType: "json",
contentType: "application/json"
});
Instead of setting the pk:20 to src_lang_id field, it creates a new Language with empty fields for src_lang and sets a correct value for dst_lang. But empty fields are restricted with the Language model definition. How it saves it?
Also it's enough strange because i've straightly specified readonly access for language model, and only get method for accessing the supported language list.
If i declare language fields of OrderResource class as, i.e.: src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True, readonly=True), it creates nothing, but also does not set any values for the foreign keys.
So, i just need to specify an existant language, i don't need to create it.
UPDATE
ResourceAuthorization:
class ResourceAuthorization(Authorization):
def is_authorized(self, request, object=None):
user = getattr(request, 'user', None)
if not user:
return False
return user.is_authenticated()
def apply_limits(self, request, object_list):
if request and hasattr(request, 'user'):
if request.user.is_superuser:
return object_list
return object_list.filter(user=request.user)
return object_list.none()
UPDATE 2
I found nothing more clever making fields read only and overriding obj_create method:
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(LanguageResource, 'src_lang', null=True, full=True, blank=True, readonly=True)
dst_lang = fields.ForeignKey(LanguageResource, 'dst_lang', null=True, full=True, blank=True, readonly=True)
def obj_create(self, bundle, request=None, **kwargs):
src_lang_id, dst_lang_id = bundle.data.get('src_lang', None), bundle.data.get('dst_lang', None)
if not all([src_lang_id, dst_lang_id]):
raise BadRequest('You should specify both source and destination language codes')
src_lang, dst_lang = Language.objects.guess(src_lang_id), Language.objects.guess(dst_lang_id)
if not all([src_lang, dst_lang]):
raise BadRequest('You should specify both source and destination language codes')
return super(OrderResource, self).obj_create(
bundle, request, user=request.user, src_lang=src_lang, dst_lang=dst_lang
)
class Meta:
resource_name = 'orders'
queryset = Order.objects.all()
serializer = Serializer(['json'])
authentication = MultiAuthentication(SessionAuthentication(), ApiKeyAuthentication())
authorization = ResourceAuthorization()
As outlined in this answer to your question, src_lang should correspond to a resource, not to some other value. I suspect that when the POST occurs and it doesn't find a resource pk=20, it creates a new Language object and calls save without Django model validation, allowing a blank field to exist in the created Language.
One way of forcing a read-only type resource is to create a resource which doesn't allow obj_create.
class ReadOnlyLanguageResource(ModelResource):
# All the meta stuff here.
def obj_create(self):
# This should probably raise some kind of http error exception relating
# to permission denied rather than Exception.
raise Exception("Permission denied, cannot create new language resource")
This resource is then referenced from your Order resource, overriding just the src_lang field to point to your read only resource.
class OrderResource(ModelResource):
user = fields.ForeignKey(UserResource, 'user', null=True, full=True)
src_lang = fields.ForeignKey(ReadOnlyLanguageResource, 'src_lang')
dst_lang = fields.ForeignKey(ReadOnlyLanguageResource, 'dst_lang')
Any request that references an existing resource will complete as per normal (but you'll need to reference the resource correctly, not using pk=20). Any request the references an unknown language will fail as a new Language object cannot be created.
You should specify the src_lang in the format /api/v2/languages/72/ corresponding the pk=20.
Secondly what exactly is ResourceAuthorization? The documentation lists ReadOnlyAuthorization which might be useful to you.
Also the authorization applies the resource, not the underlying model. When creating a new object for fk, it does not use the REST Api but django.db.models and its permissions. So the authorizations might not apply via the foreign key constraint.

Django: assigning a foreignKey object a value of current user

I'm trying to override the save() method in the admin so when my publisher-users are creating their customer-users, the publisher field is automatically assigned a value of current user.
ValueError: Cannot assign "User: foo": "SimpleSubscriber.publisher" must be a "Publisher" instance.
I used this tutorial to get started: here.
def save_model(self, request, obj, form, change):
if not change:
obj.publisher = request.user
obj.save()
this is the save method override. The only users who can access the admin are publishers, and both the Publisher and SimpleSubscriber models are user models:
class Publisher(User):
def __unicode__(self):
return self.get_full_name()
class SimpleSubscriber(User):
publisher = models.ForeignKey(Publisher)
address = models.CharField(max_length=200)
city = models.CharField(max_length=100)
state = USStateField()
zipcode = models.CharField(max_length=9)
phone = models.CharField(max_length=10)
date_created = models.DateField(null=True)
sub_type = models.ForeignKey(Product)
sub_startdate = models.DateField()
def __unicode__(self):
return self.last_name
What can I replace request.user with in order to assign each new SimpleSubscriber to the current publisher user?
You must replace request.user with an instance of Publisher.
One way to do that would be:
Publisher.objects.get(**{Publisher._meta.get_ancestor_link(User).name: request.user})
Of course, that will do a lookup every time you invoke it, so you might like to design your application to do this on every request.
Another way to do this is to (slightly) abuse the django model system - where one model inherits from another, the corresponding parent and child model instances have the same id (by default);
Publisher.objects.get(id = request.user.id)

Categories

Resources