Fetching the Last Login Time of a User in Plone via restapi
By Érico Andrei

Fetching the Last Login Time of a User in Plone via restapi

Recently, a client approached me with a specific requirement: they wanted to ascertain the last time a user logged into their Plone site using the restapi. While I recalled that Plone does maintain this information in a member property, it struck me that this particular data isn't displayed anywhere within the user interface.

Upon inspecting the response from the @users endpoint, I observed that it primarily fetches member properties present in the user schema. It doesn't, however, retrieve the older properties that reside in the portal_memberdata. This necessitated a modification in the endpoint response.

My Initial Approach

Initially, I considered overriding the existing endpoint. I considered registering a new service named @users to the project's browser layer (ericof.interfaces.IERICOFCOMLayer). Here's a brief look at what I attempted:

<configure
    xmlns="http://namespaces.zope.org/zope"
    xmlns:plone="http://namespaces.plone.org/plone"
    >

  <plone:service
      method="GET"
      factory=".users.UsersGet"
      for="Products.CMFPlone.interfaces.IPloneSiteRoot"
      layer="ericof.interfaces.IERICOFCOMLayer"
      permission="zope2.View"
      name="@users"
      />

</configure>

While this method yielded the desired results, it wasn't the most efficient. The primary drawback was the need to replicate a portion of the original class's codebase.

A better and cleaner approach

However, as I delved deeper to minimize this code redundancy, I stumbled upon a more elegant solution. Plone already has a serializer for MemberData objects. All I needed to do was override this serializer.

To achieve this, I crafted a serializers/user.py file that enhances the original, by adding the login dates:

from datetime import datetime
from ericof.interfaces import IERICOFCOMLayer
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.serializer.user import SerializeUserToJson as BaseSerializer
from Products.CMFCore.interfaces._tools import IMemberData
from zope.component import adapter
from zope.interface import implementer


@implementer(ISerializeToJson)
@adapter(IMemberData, IERICOFCOMLayer)
class SerializeUserToJson(BaseSerializer):
    def __call__(self):
        data = super().__call__()
        now = datetime.now()
        user = self.context
        # Get current login date
        login = user.getProperty("login_time", now)
        data["login_current"] = json_compatible(login)
        # Get previous login_date
        previous_login = user.getProperty("last_login_time")
        data["login_previous"] = json_compatible(previous_login)
        return data

Subsequently, I registered it within the serializers/configure.zcml:

<configure xmlns="http://namespaces.zope.org/zope">

  <adapter factory=".user.SerializeUserToJson" />

</configure>

Concluding Thoughts

Plone, with its rich legacy spanning over two decades, is a testament to the evolution of CMS platforms. Over time, innovative solutions like the editable UserSchema have emerged, phasing out older implementations like Memberdata. Yet, the beauty of Plone lies in its versatility – the older systems remain intact, ready to be harnessed when needed.

Furthermore, the plone.restapi stands out as a meticulously crafted code. It offers the flexibility to augment its functionalities without the hassle of extensive boilerplate code and duplication.

Quoting "The Zen of Python":

  • Simple is better than complex
  • There should be one-- and preferably only one --obvious way to do it.
  • Although that way may not be obvious at first unless you're Dutch.