Problem Passing Parameters to the REST API
Hello Barry,
I had the same (roundabout) problem as my peer in regards to passing parameters to the REST API. For the Systers-customized version of Mailman, I wanted to add an 'essay' field to the member, such that someone trying to subscribe to our mailing list would write an essay that could be stored. I made whatever changes to the code I thought were necessary and then attempted to subscribe a member myself.
This Google Doc highlights some code and errors that I ran into.
https://docs.google.com/document/d/1CbqZ_uONS-uEzVXoV8B198LmQoQ8vUqRspcJTdBh...
My main question is: How deep do I need to go into the subscription workflow to add a single field?
Thanks, Nafisa Shazia
Nafisa Shazia writes:
Hello Barry,
I had the same (roundabout) problem as my peer in regards to passing parameters to the REST API.
Khushboo's solution is going to be different, although most of the following general discussion should apply.
My main question is: How deep do I need to go into the subscription workflow to add a single field?
The short answer is: *you* don't. In Mailman 2, there was no user concept, only subscribers (also called members in that context). A subscription was a relationship between an email address and a mailing list. This was implemented as a one-many attribute of the list. Also, in general it was not possible to identify whether two addresses correspond to the same person.
In Mailman 3, there *is* a user model, and you can be a user without subscribing to any lists (eg, a moderator), but every subscription is a relationship between a user and a mailing list. This is implemented as a many-many relationship:
def __init__(self, essay, role, list_id, subscriber):
self._member_id = uid_factory.new_uid()
self.role = role
self.essay = essay
self.list_id = list_id
[Note: at least in Chrome, Google docs does *not* format your code as Python code, nor does copy-paste give valid Python code.]
Unfortunately you omitted the rest of the function. So your Google doc is pretty, but it makes people trying to help you do a lot of extra work. :-( Here's the rest of that function:
if IAddress.providedBy(subscriber):
self._address = subscriber
# Look this up dynamically.
self._user = None
elif IUser.providedBy(subscriber):
self._user = subscriber
# Look this up dynamically.
self._address = None
else:
raise ValueError('subscriber must be a user or address')
if role in (MemberRole.owner, MemberRole.moderator):
self.moderation_action = Action.accept
elif role is MemberRole.member:
self.moderation_action =
getUtility(IListManager).get_by_list_id(
list_id).default_member_action
else:
assert role is MemberRole.nonmember, (
'Invalid MemberRole: {0}'.format(role))
self.moderation_action =
getUtility(IListManager).get_by_list_id(
list_id).default_nonmember_action
This makes it clear that it's possible to subscribe just an address. However, under the hood Mailman provides a dummy user. (This is implied by the comment "Look this up dynamically.")
Now, in the Systers application, you don't want address-only anonymous subscribers, you want users who have "personalities", ie, user objects with the appropriate attributes. So you should be adding your "essay" attribute to the *users*, not to (list) members. I'm not sure about the UI, there are a couple of possibilities. You have to take account of the fact that there are non-Systers like me who subscribe to the lists, although that could be done by list administrators if preferred.
(IMHO, the term "member" is very confusing, because there's a natural tendency to think of a member as being a person and assuming it has personal attributes, but that's not the case at all in Mailman 2, and in Mailman 3 it might not be the case.)
The long answer is actually shorter<wink />. Most likely you should not need to change the subscription workflow at all. If you do, it's going to require deep surgery and a lot of work. Consider the signature of on_post. It is the same for *all* classes that implement it. (This is what Pythonistas call "duck-typing" or "protocol".) To change it for one class, you need to change it for all.
Instead, you want to find the *object* (typically only one) that needs to change and change its model. This typically requires changing an interface (under interfaces) as well as the model itself (under model), and often other code used in the implementation. In the case of adding the essay, you need to add the attribute in interfaces/user.py, and initialize it in model/user.py. For some reason there's no rest/user.py, but probably in rest/users.py you will want to add code in an appropriate class's on_post method to retrieve the "essay" parameter from the request and attach it to the appropriate AUser object.
Note that an __init__ method is *not* a public protocol (you never see code that invokes it as x.__init__(arg1, arg2, ...)), so it can (and does) vary a lot from class to class. The only requirement is that the first argument refer to the object itself (conventionally "self", but some programmers use "s" or "me" or "this") so that it can be called using object.method() syntax. The on_* methods on the other hand are invoked by various workflows in exactly the same way for any object they receive, regardless of type. (That's implicitly the definition of "protocol" -- "do it this way and it just works".)
Regards, Steve
On Jun 12, 2015, at 08:00 PM, Nafisa Shazia wrote:
My main question is: How deep do I need to go into the subscription workflow to add a single field?
Steve provided some good background and answers, and I'll just add a few more thoughts.
The subscription workflow stuff was added fairly late in the 3.0 cycle and I had to give up on a few things to make this work. Most importantly was the idea of attaching additional information to the subscription request, e.g. the preferred delivery mode (regular or digest).
The subscription workflow is interesting because there are steps which need to be persistent. This happens when a confirmation is required from the user, or an approval is required from the list moderator. In those cases, we need to store the state of the workflow in the database, keyed off a token, and then reconstitute the workflow when that token is presented, either in an email or through the REST API. In order to simplify, we only persist the the most important bits of the subscription request, and things like the delivery mode didn't make the cut.
In 3.1, I would like to add back the ability to attach other pieces of information to the subscription request, but that will probably require some additional changes to the database schema. I might be able to reuse the Pending database, which at its heart is a rather simple key/value store. Anything we add here would also have to be exposed to the REST API, and I'd rather not let that accept arbitrary parameters.
It's possible that an essay string could be attached to the subscription request. There are implementation details such as making sure the essay gets deleted if the request gets denied or times out, and there are issues of the model as Steve points out. E.g. if a new address is getting subscribed for an existing user, and that user already has an essay, does the new one overwrite the old one, or does it get ignored?
On Jun 13, 2015, at 03:13 PM, Stephen J. Turnbull wrote:
In Mailman 3, there *is* a user model, and you can be a user without subscribing to any lists (eg, a moderator), but every subscription is a relationship between a user and a mailing list.
Right; the model works like this:
Addresses represent an email address. The internal terminology always describes IAddress instances as "address" and string typed email addresses as "email". Addresses have certain properties in addition to the email, as you can see in the IAddress interface.
Users represent people. An IUser can control multiple addresses but an address can only be linked to (zero or) one user. A user can have a "preferred address" which is used in memberships when no explicit address is given. Users can have multiple memberships.
Members represents the relationship between an address or a user (collectively termed "subscriber" internally) and a mailing list. Members also have roles, so this relationship triplet is what defines a membership. E.g. Anne subscribed to the Ant mailing list as a member is a different IMember instance than Anne subscribed to the Ant mailing list as an owner.
A roster is a collection of members, but it's important to remember that there are no roster objects in the database. A roster is a query, so all kinds of rosters can exist. E.g. the roster of all digest members of mailing list Bee, or the roster of all of Bart's memberships in all mailing lists, regardless of role.
This makes it clear that it's possible to subscribe just an address. However, under the hood Mailman provides a dummy user. (This is implied by the comment "Look this up dynamically.")
Right, while the model does support subscribing bare (i.e. unlinked to a user) addresses to mailing lists, this generally isn't exposed externally.
The other important constraint is that users can only be subscribed to mailing lists if they have a preferred address, since ultimately we have to deliver messages to this member, and if we don't know what address to use, we can't send the user messages.
So you should be adding your "essay" attribute to the *users*, not to (list) members.
+1
(IMHO, the term "member" is very confusing, because there's a natural tendency to think of a member as being a person and assuming it has personal attributes, but that's not the case at all in Mailman 2, and in Mailman 3 it might not be the case.)
IMembers (the interface that describes members) indeed doesn't have much personality. The closest thing is perhaps that members can have different preferences than the underlying subscriber (user or address).
The preference system (such as whether a person wants to receive a list copy if they're explicitly CC'd on a message) is based on a hierarchy. Attributes are looked up in this order, with the first one found winning:
- The member's preferences
- The subscribed address's [1] preferences
- The subscribed user's preferences
- The system default preferences
(It's arguable that mailing lists should have preferences, in which case the hierarchy should probably be extended to look up list, followed by domain preferences before the system defaults.)
[1] "The subscribed address" means the IAddress if that was used as the subscriber explicitly, or the IUser's preferred address if the user was subscribed.
I hope that provides some additional useful information. Be sure to read the rest of Steve's response; there's lots of good stuff in there. :)
Cheers, -Barry
participants (3)
-
Barry Warsaw
-
Nafisa Shazia
-
Stephen J. Turnbull