I would like to execute Python code upon the successful download of a specific File.
As far as I understand, I could define a corresponding event subscriber to achieve this. However, zope.lifecycleevent does not seem to list an interface corresponding to an object download. What would be the best approach to handle this?
Specifically, I need to save the date and time of the last successful File download from a specific area of the website, store it in the user metadata, and display it on the user information page. The last two steps are clear to me, but I am unsure how to implement the first one.
In any case, you need to overwrite the the implementation of the @@download view for File. This can be accomplished using a custom implementation of the related view and configured through the overrides.zcml mechanism or using a monkey-patch of the download method for File. If you implement the particular logic directly or using a custom event (defined by yourself), is an implementation detail.
Many thanks @zopyx and @jaroel. I have implemented Roel's suggestion, and it works perfectly even when the @@download view is not used. The code below demonstrates how I achieved this, in case anyone is interested. Of course, there is room for improvement, but it serves its purpose.
Please don’t laugh at how I’m checking the object class in the if statement - I couldn’t get the standard isinstance(obj, Download) to work because the Download class isn’t defined in metaconfigure module (returned by obj.__class__.__module__), and I couldn’t determine where it’s actually defined. Hints are warmly welcome!
from zope.component import adapter
from ZPublisher.interfaces import IPubSuccess
from datetime import datetime
from plone import api
import transaction
import logging
logger = logging.getLogger(__name__)
@adapter(IPubSuccess)
def track_file_download(event):
"""
Track if a file download was successful.
"""
request = event.request
response = request.response
obj = request.get('PUBLISHED', None)
if obj.__class__.__name__ == "Download" and obj.__class__.__module__ == "Products.Five.browser.metaconfigure":
url = request.getURL()
download_path_marker = '/download/'
if download_path_marker in url: # we are in the Downloads section
is_download = '/@@download' in url or 'Content-Disposition' in response.headers
if is_download:
user = api.user.get_current()
username = user.getUserName()
status_code = response.getStatus()
if status_code == 200:
logger.info(f"Successful file download: {url} by {username}")
if not api.user.is_anonymous():
user.setProperties(download_timestamp=datetime.now(), download_url=url)
transaction.commit()
else:
logger.warning(f"Failed file download (status {status_code}): {url} by {username}")
Looking at what you actually did, I think Andreas' approach is beter. It'll be easier to maintain as long as you keep using the @@download view. I feel I sent you down a rabbit hole, my bad.
You don't need to check to status_code 200 - the fact that you got to the view should be fine, but add a few automated tests to show all this works correctly.