2

I have a table of PartyOrganiser(s), and a table of one's Contact(s) and a table of one's organised Party(s).

PartyOrganiser to Party is one-to-many.
PartyOrganiser to Contact is one-to-many.
Party to Contact is many-to-many, with an association table.

class PartyOrganiser(db.Model):
    __tablename__ = 'party_organiser'
    id   = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    parties  = db.relationship("Party",    back_populates='party_organiser')
    contacts = db.relationship("Contacts", back_populates='party_organiser')

contact_party_ass_tab = db.Table('contact_party', db.Model.metadata,
    db.Column('party_id', db.Integer, db.ForeignKey('party.id')),
    db.Column('contact_id', db.Integer, db.ForeignKey('contact.id')))

class Party(db.Model):
    __tablename__ = 'party'
    id      = db.Column(db.Integer, primary_key=True)
    details = db.Column(db.String)
    party_organiser_id = db.Column(db.Integer, db.ForeignKey('party_organiser.id'), nullable=False)
    party_organiser = db.relationship("PartyOrganiser", back_populates="parties", uselist=False)
    attendees = db.relationship("Contact", secondary=contact_party_ass_tab, back_populates='intended_parties')

class Contact(db.Model):
    __tablename__ = 'contact'
    id   = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    party_organiser_id = db.Column(db.Integer, db.ForeignKey('party_organiser.id'), nullable=False)
    party_organiser = db.relationship("PartyOrganiser", back_populates="parties", uselist=False)
    intended_parties = db.relationship("Contact", secondary=contact_party_ass_tab, back_populates='attendees')

Main Question:

Grammatically, for a specific party, I want to acquire a list of those contacts associated with the party's organiser that are not already attending the party. I.e. calling them potential_attendees, I would like the following as an SQLalchemy query style solution:

class Party(db.model):
    ...
    @property
    def potential_attendees(self):
        # get all contacts for the right party_organiser
        sub_query = Contact.query.filter(Contact.party_organiser_id == self.party_organiser_id)
        # then eliminate those that are already attendees to this party..
        return sub_query.difference(self.attendees) # <- pseudocode

Sub question:

This configuration has an inherent 3 way constraint between PartyOrganiser, Party and Contact: parties and attendees can only be associated if they share a party_organiser. I.e. None of PartyOrganiser1's contacts can be attendees to Party2 organised by PartyOrganiser2. It is not obvious to me that this is constrained as required in the above format. In fact I believe it isn't. How would I implement this constraint?

Attack68
  • 4,437
  • 1
  • 20
  • 40

1 Answers1

1

You can query for the items which are excluded in a relationship by using the NOT EXISTS construct on a joined table.

@property
def potential_attendees(self):
    sq = db.session.query(contact_party_ass_tab.columns.contact_id).subquery()
    return db.session.query(Contact).filter(
            Contact.party_organiser_id==self.party_organiser_id,
            ~exists().where(sq.c.contact_id==Contact.id)
        ).all()

As far as your other question is concerned, you can impose that constraint on the ORM level by adding attribute level validators for Party.attendees and Contact.intended_parties and ensuring that any new item added to those lists has a matching party_organiser_id. Here is the full code

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy import exists

from sqlalchemy.orm import validates


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
db = SQLAlchemy(app)

class PartyOrganiser(db.Model):
    __tablename__ = 'party_organiser'
    id   = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    parties  = db.relationship("Party",    back_populates='party_organiser')
    contacts = db.relationship("Contact", back_populates='party_organiser')

    def __repr__(self):
        return self.name


contact_party_ass_tab = db.Table('contact_party', db.Model.metadata,
    db.Column('party_id', db.Integer, db.ForeignKey('party.id')),
    db.Column('contact_id', db.Integer, db.ForeignKey('contact.id')))

class Party(db.Model):
    __tablename__ = 'party'
    id      = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    details = db.Column(db.String)
    party_organiser_id = db.Column(db.Integer, db.ForeignKey('party_organiser.id'), nullable=False)
    party_organiser = db.relationship("PartyOrganiser", back_populates="parties", uselist=False)
    attendees = db.relationship("Contact", secondary=contact_party_ass_tab, back_populates='intended_parties')

    def __repr__(self):
        return self.name

    @property
    def potential_attendees(self):
        sq = db.session.query(contact_party_ass_tab.columns.contact_id).subquery()
        return db.session.query(Contact).filter(
                Contact.party_organiser_id==self.party_organiser_id,
                ~exists().where(sq.c.contact_id==Contact.id)
            ).all()

    @validates('attendees')
    def validate_attendee(self, key, attendee):
        assert attendee.party_organiser_id == self.party_organiser_id
        return attendee

class Contact(db.Model):
    __tablename__ = 'contact'
    id   = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    party_organiser_id = db.Column(db.Integer, db.ForeignKey('party_organiser.id'), nullable=False)
    party_organiser = db.relationship("PartyOrganiser", back_populates="contacts", uselist=False)
    intended_parties = db.relationship("Party", secondary=contact_party_ass_tab, back_populates='attendees')

    def __repr__(self):
        return self.name

    @validates('intended_parties')
    def validate_party(self, key, party):
        assert party.party_organiser_id == self.party_organiser_id
        return party

db.create_all()

organiser1 = PartyOrganiser(name="organiser1")
organiser2 = PartyOrganiser(name="organiser2")

db.session.add_all([organiser1, organiser2])
db.session.commit()


org1_party1 = Party(name="Organiser1's Party1", party_organiser_id=organiser1.id)
org1_party2 = Party(name="Organiser1's Party2", party_organiser_id=organiser1.id)

org2_party1 = Party(name="Organiser2's Party1", party_organiser_id=organiser2.id)
org2_party2 = Party(name="Organiser2's Party2", party_organiser_id=organiser2.id)

db.session.add_all([org1_party1, org1_party2, org2_party1, org2_party2])
db.session.commit()

org1_contact1 = Contact(name="Organiser1's contact 1", party_organiser_id=organiser1.id)
org1_contact2 = Contact(name="Organiser1's contact 2", party_organiser_id=organiser1.id)
org1_contact3 = Contact(name="Organiser1's contact 3", party_organiser_id=organiser1.id)
org1_contact4 = Contact(name="Organiser1's contact 4", party_organiser_id=organiser1.id)

org2_contact1 = Contact(name="Organiser2's contact 1", party_organiser_id=organiser2.id)
org2_contact2 = Contact(name="Organiser2's contact 2", party_organiser_id=organiser2.id)
org2_contact3 = Contact(name="Organiser2's contact 3", party_organiser_id=organiser2.id)
org2_contact4 = Contact(name="Organiser2's contact 4", party_organiser_id=organiser2.id)

db.session.add_all([org1_contact1, org1_contact2, org1_contact3, org1_contact4, org2_contact1, org2_contact2, org2_contact3, org2_contact4])
db.session.commit()

org1_party1.attendees.append(org1_contact1)
db.session.commit()

print "Potential attendees of org1_party1 ", org1_party1.potential_attendees

print "Attempting to add a contact of a different organiser. Will throw exception"

org1_party1.attendees.append(org2_contact1)

Output (Check the exception at the end which is thrown by the last line in above code):

In [1]: from exclusion_query import *
/home/surya/Envs/inkmonk/local/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
Potential attendees of org1_party1  [Organiser1's contact 2, Organiser1's contact 3, Organiser1's contact 4]
Attempting to add a contact of a different organiser. Will throw exception
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-1-4380704ace46> in <module>()
----> 1 from exclusion_query import *
suryasankar
  • 4,821
  • 2
  • 14
  • 13