in src/olympia/activity/models.py [0:0]
def arguments_builder(cls, activities):
def handle_renames(value):
# Cope with renames of key models (use the original model name like
# it was in the ActivityLog as the key so that we can find it
# later)
return 'ratings.rating' if value == 'reviews.review' else value
# We need to do 2 passes on each log:
# - The first time, gather the references to every instance we need
# - The second time, we built querysets for all instances of the same
# type, pick data from that queryset.
#
# Because it relies on in_bulk(), this method needs the pks to be of a
# consistent type, which doesn't appear to be guaranteed in our
# existing data. For this reason, it forces a conversion to int. If we
# ever want to store ActivityLog items pointing to models using a non
# integer PK field, we'll need to make this a little smarter.
instances_to_load = defaultdict(list)
instances = {}
for activity in activities:
try:
# `arguments_data` will be a list of dicts like:
# `[{'addons.addon':12}, {'addons.addon':1}, ... ]`
activity.arguments_data = json.loads(activity._arguments)
except Exception as e:
log.info('unserializing data from activity_log failed: %s', activity.id)
log.info(e)
activity.arguments_data = []
for item in activity.arguments_data:
# Each 'item' should have one key and one value only.
name, pk = list(item.items())[0]
if name not in ('str', 'int', 'null') and pk:
# Convert pk to int to have consistent data for when we
# call .in_bulk() later.
name = handle_renames(name)
instances_to_load[name].append(int(pk))
# At this point, instances_to_load is a dict of "names" that
# each have a bunch of pks we want to load.
for name, pks in instances_to_load.items():
(app_label, model_name) = name.split('.')
model = apps.get_model(app_label, model_name)
# Load the instances, avoiding transformers other than translations
# and coping with soft-deleted models and unlisted add-ons.
qs = model.get_unfiltered_manager().all()
if hasattr(qs, 'only_translations'):
qs = qs.only_translations()
instances[name] = qs.in_bulk(pks)
# instances is now a dict of "model names" that each have a dict of
# {pk: instance}. We do our second pass on the logs to build the
# "arguments" property from that data, which is a list of the instances
# that each particular log has, in the correct order.
for activity in activities:
objs = []
# We preloaded that property earlier
for item in activity.arguments_data:
# As above, each 'item' should have one key and one value only.
name, pk = list(item.items())[0]
if name in ('str', 'int', 'null'):
# It's not actually a model reference, just return the
# value directly.
objs.append(pk)
elif pk:
# Fetch the instance from the cache we built.
name = handle_renames(name)
obj = instances[name].get(int(pk))
# Most of the time, we're eventually going to call
# to_string() on each ActivityLog that we're processing
# here. For some of the models, that will result in a call
# to <model>.get_absolute_url(), which in turn can cause an
# extra SQL query because some parent model is needed to
# build the URL.
# It's difficult to predict what we'll need as ActivitLog
# is fairly generic, but we know Addon is going to be
# needed in some cases for sure (Version, Rating) so if
# we're dealing with objects that have an `addon_id`
# property, and we have already fetched the corresponding
# Addon instance, set the `addon` property on the object
# to the Addon instance we already have to avoid the extra
# SQL query.
addon_id = getattr(obj, 'addon_id', None)
if addon := instances.get('addons.addon', {}).get(addon_id):
obj.addon = addon
objs.append(obj)
# Override the arguments cached_property with what we got.
activity.arguments = objs