def arguments_builder()

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