happeninghq / happening

Open Source Event/Community management
MIT License
2 stars 1 forks source link

Happening universal filtering language #66

Closed jscott1989 closed 9 years ago

jscott1989 commented 9 years ago

For targeting emails, setting who can purchase tickets, etc. it would be useful to have a single way of filtering users.

This could be done with a sparql like interface, or something more SQL-like.

It needs to be powerful enough to do filters such as "users who cancelled their ticket before X date", "users who have a no-show rate of higher than 40%", "users who attended both X and Y events", etc.

It's most likely this will be easiest to accomplish using a cross-compiler from our DSL to SQL.

jscott1989 commented 9 years ago

This needs to be completed as part of #5

First we need to think of a suitable syntax that is easy to parse, possible for plugins to add to, and can represent our requirements.

We can put a nicer interface on top of this for common queries - but the power needs to be there for plugins to exploit.

The current idea for syntax is:

(Member tickets__has:(Ticket event: (Event id:1) cancelled:True cancelled_datetime__lt:"01/01/2015 19:00:00"))

(Member no_show_percentage__gt:40)

(Member ticketshas:(Ticket event: (Event id:1)) ticketshas:(Ticket event: (Event id:2)))

Ideally each attribute (e.g. "tickets" "id" "cancelled" etc.) will be provided by using a method on the model with a decorator (@query_attribute) which takes in a queryset returns a modified queryset.

We'd parse it by moving inside out. e.g:

(Member tickets__has:(Ticket event: (Event id:1) cancelled:True cancelled_datetime__lt:"01/01/2015 19:00:00"))

First, parse (Event id:1)

Start with Event.objects.all()

call Event.query_attribute_id which adds q.filter(id=1)

We then call .get() on the final query set which returns a single Event object.

We then parse (Ticket event: event cancelled:True cancelled_datetime__lt:"01/01/2015 19:00:00")

Begin with Ticket.objects.all().

Call Ticket.query_attribute_event which adds q.filter(event=event)

Ticket.query_attribute_cancelled adds q.filter(cancelled=True)

Ticket.query_attribute_cancelled_datetime shows that these methods will need to take an extra parameter to give the comparison criteria (default "equals", in this case "lt"). It will add q.filter(cancelled_datetime__lt=dt).

We've finished constructing the query and now call .get() which returns 0 or more Ticket objects.

Finally, we call Member.query_attribute_tickets passing the results of the query and the comparison criteria "has". I'm still figuring out how exactly this will modify the query.

This feels like it should work and fulfils the requirements listed above. I don't really like using the double underscore but that keeps it consistent with django.

Another option would be to just make a light DSL around the django query interface that maps one-to-one with django. This would be less powerful in places (Embedded queries such as the above would be difficult) and more powerful than we'd like in other places (it'd be difficult to restrict it from touching attributes we'd rather it didn't).

An advantage of using methods for the query attributes is that we can use docstrings for automatic documentation of the query language.

I'll have more of a think about how best to implement this. If there's a way to do it without writing a parser for a DSL that'd be awesome but I don't think there is. Any suggestions are welcome

jscott1989 commented 9 years ago

https://github.com/django/django/blob/master/tests/or_lookups/tests.py The "Q" objects will be useful for this

jscott1989 commented 9 years ago

Implemented this differently - decided as staff will be the only current intended use we can be a bit more liberal with what we allow. The syntax is:

ticketshas:(eventid:1 cancelled:True) ticketshas:(eventid:1) ticketshas:(eventid:2) ticketscountgt:2

This doesn't currently support everything I wanted (custom properties such as no_show_percentage, datetimes, etc.) it's not so easy for plugins to add to, and allowing the public to use this language would potentially be a huge security flaw.

But the parser is easy and it runs fine. This will do for beta 1. It needs some thought for 1.0.

(Right now this is going to be used for the new email functionality - long term I'd like it to be used to set eligibility criteria for tickets/membership/etc.)

jscott1989 commented 9 years ago

This is in a usable state for beta 1

The queries are difficult to write - this is to be improved in #76

The language needs some documentation written though. Once written this issue can be closed.