Validating embedded document in Mongoid based on embedded attribute - mongoid

I've a class Subscriber which has embeds_many Subscriptions. Subscription has an attribute status. I want to add a validation on status such that only one Subscription can have status 'active' per subscriber. The subscriber can have multiple subscription with status 'purchased' or 'expired' .

This should do it:
class Subscriber
include Mongoid::Document
embeds_many :subscriptions
validate :active_subscriptions
def active_subscriptions
self.errors.add(:base, 'too many active subscriptions') if
subscriptions.where(status: 'active').count > 1
end
end
class Subscription
include Mongoid::Document
embedded_in :subscriber
field :status, class: 'String'
end
s = Subscriber.create
s.subscriptions.build(status: 'active')
s.save # fires validations on subscriber
s.subscriptions.build(status: 'active')
s.save # wouldn't save
But make sure that you always call save on subscriber, otherwise the validations will not fire on subscriber and you will land in an inconsistent state. In an inconsistent state you might see failing validations later
s = Subscriber.create
s.subscriptions.create(status: 'active') # fires validations on subscription only
s.subscriptions.build(status: 'active').save # fires validations on subscription only
If you need to also validate subscription, when saving subscriber, you cascade callbacks:
embeds_many :subscriptions, cascade_callbacks: true

Related

Reverse One-to-Many-Relationship in Django

My app links invoices, contracts and services with Many-to-One-Relationships:
class Invoice(models.Model):
contract = models.ForeignKey(Contract, on_delete=models.CASCADE)
class Contract(models.Model):
service = models.ForeignKey(Service, on_delete=models.CASCADE)
Whenever a new invoice is registered, it can be linked to a service and split/billed internally. Unfortunately, some contracts/invoices need to be linked to more than one service according to a fixed split (e.g. 30/70).
For this to work on the surface, I could to reverse the relationship between contracts and services –
class Service(models.Model):
contract = models.ForeignKey(Contract, on_delete=models.CASCADE)
– or change the ForeignKey field on the Contract class to a ManyToManyField.
But in both cases, I will not be able to get back from the invoice to the service easily anymore, as with the following statement:
invoices = Invoice.objects.filter(models.Q(contract__service__building=self.tenant.unit.building), models.Q(begin__lte=self.begin, end__gt=self.begin) | models.Q(begin__gt=self.begin, begin__lt=self.end))
Is it wise to insert an intermediate helper model (ContractService) with two ForeignKey fields to keep the current app logic and add the option to link a contract to more than one service?
Ok just to clarify one example:
Contract is "Cleaning of House"
Services are "Cleaning of first floor" and "Cleaning of second floor"
Invoices are "Invoice1", "Invoice2", ...
You want a relationship that "Invoice1" can be linked to "Cleaning of first floor" AND "Cleaning of second floor".
models.py
class Contract(models.Model):
"""Can hold multiple Services"""
pass
class Service(models.Model):
"""Is linked to one specific Contract"""
contract = models.ForeignKey(Contract, on_delete=models.CASCADE)
class Invoice(models.Model):
"""Can hold multiple Services and one Service can hold multiple Invoices"""
service = models.ManyToManyField(Service)
Now your question is:
"I can easily get the contract when I have the Service object, but how can I get the Service when I have the Contract object?
con = Contract.objects.all().first()
queryset = con.service_set.all() # gives you all related Services for that specific Contract
Read more about ManytoOne
And:
"How can I get the Invoice when I have the Service object?"
ser = Service.objects.all().first()
queryset = ser.invoice_set.all() # gives you all related Invoices for that specific Service
Read more about ManyToMany
Let me know how it goes
Thanks to #Tarquinius for your help – I found a solution based on his suggestion. The three models in question are connected as follows:
class Service(models.Model):
pass
class Contract(models.Model):
services = models.ManyToManyField(Service)
class Invoice(models.Model):
contract = models.ForeignKey(Contract, on_delete=models.CASCADE)
The query quoted above did not even need to be modified (apart from the different fieldname) to reflect the possibility of more than one service per contract, I just had to add the distinct() function:
invoices = Invoice.objects.filter(models.Q(contract__services__building=self.tenancy_agreement.unit.building), models.Q(begin__lte=self.begin, end__gt=self.begin) | models.Q(begin__gt=self.begin, begin__lt=self.end)).distinct()
In hindsight, this is quite obvious (and simple).

How to ignore an unknown subclass with single collection inheritance in Mongoid?

The default inheritance method for Mongoid is creating a single collection for all subclasses. For instance:
class User
include Mongoid::Document
end
class Admin < User
end
class Guest < User
end
Internally, Mongoid adds a _type field to each document with the class name, which is used to automatically map each instance to the right class.
The problem is, if I have a document in this collection with an unknown _type value, I get an exception:
NameError: uninitialized constant UnknownClass
This can happen if you create a new subclass of User, in the example above, and a migration that creates a new instance of this new subclass. Until you restart your servers, every query to this collection (like User.all.to_a). Is there a safe way to avoid this error?
The only solution I came up is rescuing NameError exception and querying by all known subclasses:
class User
def self.some_query(params)
self.where(params).to_a
rescue NameError => e
Rails.logger.error "Unknown subclass: #{e.message}"
subtypes = self.descendants.map(&:to_s)
self.where(params.merge(:_type.in => subtypes)).to_a
end
end

Simple datastore entity with 2 fields that are also unique

All I am trying to produce is an entity that holds a unique username, and a unique device ID, and the ability to return an error if either of these conditions are not met on submission.
The only way I can see is to perform a query within a transaction, then filter the results. This however requires an ancestor (which seems unnecessary for a single simple entity).
What is the best method to go about doing this?
Here is an example that does what you want.
I put 2 entities to show you also how to make relationships
class Person(ndb.Expando):
registration_date = ndb.DateTimeProperty(auto_now_add=True)
#property
def info(self):
info = PersonInfo.query(ancestor=self.key).get()
return info
class PersonInfo(ndb.Expando):
email = ndb.StringProperty()
nick_name = ndb.StringProperty()
edit_date = ndb.DateTimeProperty(auto_now=True)
Later in the controller for register:
class RegisterPersonHandler(webapp2.RequestHandler):
def get(self):
user = users.get_current_user() #Stub here
if not user:
self.redirect(users.create_login_url(self.request.uri), abort=True)
return
person = Person.get_or_insert(user.user_id())
if not self._register(person, user):
# more logging is needed
logging.warning('Warning registration failed')
return
#ndb.transactional()
def _register(self, person, user):
''' Registration process happens here
'''
# check if the person has info and if not create it
info = PersonInfo.query(ancestor=person.key).get()
if not info:
info = PersonInfo(id=user.user_id(), parent=person.key)
info.nick_name = user.nickname()
info.email = user.email()
info.put()
return True
To answer also the comment question:
How can you programatically tell whether the returned entity is a new
or existing one though?
Try checking against a property that is default. Eg creation_date etc.
Though you can also check on something you need or on another entity's existence like I do because I expect the data to be consistent, and if not then create the bond.

Mongoid unique model references

I'm using Mongoid 3. I have a simple class Tour and references multiple Itineraries. Is there a way that I can validate that for each tour, the itineraries' dates are unique, i.e. I can't have 2 itineraries of the same date for a single tour.
class Tour
has_many :itineraries
end
class Itinerary
field :date, :type => Date
validates :date, :presence => true
index({date: 1})
belongs_to :tour
end
I'm not sure how to set up the validation.
You can create custom validations :
class Tour
has_many :itineraries
validates :check_uniqueness_of_date # This line
# And this part
private
def check_uniqueness_of_date
# Check validation here
end
end
Another Stackoverflow Question
Rails Guides

Only persist document if it has embedded documents with Mongoid?

I have a 2 level nested form (much like this) with the following classes. The problem I have is that when I don't add any intervals (the deepest embedded document) I don't want the second deepest document to be persisted either. In the owner I added a reject statement to check if there's any intervals being passed down, this works.
However, when the schedule originally had intervals but they where destroyed in the form (by passing _destroy: true) the schedule also needs to be destroyed. What would be the best way to do this? I would like to avoid a callback on the schedule that destroys the document after it is persisted.
class Owner
include Mongoid::Document
embeds_many :schedules
attr_accessible :schedules_attributes
accepts_nested_attributes_for :schedules, allow_destroy: true, reject_if: :no_intervals?
def no_intervals?(attributes)
attributes['intervals_attributes'].nil?
end
end
class Schedule
include Mongoid::Document
embeds_many :intervals
embedded_in :owner
attr_accessible :days, :intervals_attributes
accepts_nested_attributes_for :intervals,
allow_destroy: true,
reject_if: :all_blank
end
class Interval
include Mongoid::Document
embedded_in :schedule
end
Update: Maybe this is best done in the form itself? If all intervals is marked with _destroy: true, also mark the schedule with _destroy: true. But Ideally the solution would be client agnostic.
How about adding this to the Owner class:
before_update do
schedules.each |schedule|
schedule.destroy if schedule.intervals.empty?
end
end

Resources