Example #1
0
def build_dependency_tree() -> Dict[str, Set[str]]:
    """
    Build a dependency tree of all the TrackedModel subclasses mapped by record
    code.

    The return value is a dictionary, mapped by record code, where the mapped values
    are sets listing all the other record codes the mapped record code depends on.

    A dependency is defined as any foreign key relationship to another record code.
    An example output is given below.

    .. code:: python

        {
            "220": {"215", "210"},
        }
    """
    dependency_map = {}
    record_codes = {
        subclass.record_code
        for subclass in TrackedModel.__subclasses__()
    }
    for subclass in TrackedModel.__subclasses__():
        if subclass.record_code not in dependency_map:
            dependency_map[subclass.record_code] = set()
        for _, relation in subclass.get_relations():
            if (relation.record_code != subclass.record_code
                    and relation.record_code in record_codes):
                dependency_map[subclass.record_code].add(relation.record_code)

    return dependency_map
Example #2
0
 def _after_postgeneration(cls,
                           instance: TrackedModel,
                           create,
                           results=None):
     """Save again the instance if creating and at least one hook ran."""
     if create and results:
         # Some post-generation hooks ran, and may have modified us.
         instance.save(force_write=True)
Example #3
0
    def use(object: TrackedModel, new_data: Callable[[TrackedModel], dict[str, Any]]):
        model = type(object)
        versions = set(
            model.objects.filter(**object.get_identifying_fields()).values_list(
                "pk",
                flat=True,
            ),
        )

        # Visit the edit page and ensure it is a success
        edit_url = object.get_url("edit")
        assert edit_url, f"No edit page found for {object}"
        response = valid_user_api_client.get(edit_url)
        assert response.status_code == 200

        # Get the data out of the edit page
        # and override it with any data that has been passed in
        data = get_form_data(response.context_data["form"])

        # Submit the edited data and if we expect success ensure we are redirected
        realised_data = new_data(object)
        assert set(realised_data.keys()).issubset(data.keys())
        data.update(realised_data)
        response = valid_user_api_client.post(edit_url, data)

        # Check that if we expect failure that the new data was not persisted
        if response.status_code not in (301, 302):
            assert (
                set(
                    model.objects.filter(**object.get_identifying_fields()).values_list(
                        "pk",
                        flat=True,
                    ),
                )
                == versions
            )
            raise ValidationError(
                f"Update form contained errors: {response.context_data['form'].errors}",
            )

        # Check that what we asked to be changed has been persisted
        response = valid_user_api_client.get(edit_url)
        assert response.status_code == 200
        data = get_form_data(response.context_data["form"])
        for key in realised_data:
            assert data[key] == realised_data[key]

        # Check that if success was expected that the new version was persisted
        new_version = model.objects.exclude(pk=object.pk).get(
            version_group=object.version_group,
        )
        assert new_version != object

        # Check that the new version is an update and is not approved yet
        assert new_version.update_type == UpdateType.UPDATE
        assert new_version.transaction != object.transaction
        assert not new_version.transaction.workbasket.approved
        return new_version
Example #4
0
    def use(object: TrackedModel):
        model = type(object)
        versions = set(
            model.objects.filter(**object.get_identifying_fields()).values_list(
                "pk",
                flat=True,
            ),
        )

        # Visit the delete page and ensure it is a success
        delete_url = object.get_url("delete")
        assert delete_url, f"No delete page found for {object}"
        response = valid_user_api_client.get(delete_url)
        assert response.status_code == 200

        # Get the data out of the delete page
        data = get_form_data(response.context_data["form"])
        response = valid_user_api_client.post(delete_url, data)

        # Check that if we expect failure that the new data was not persisted
        if response.status_code not in (301, 302):
            assert (
                set(
                    model.objects.filter(**object.get_identifying_fields()).values_list(
                        "pk",
                        flat=True,
                    ),
                )
                == versions
            )
            raise ValidationError(
                f"Delete form contained errors: {response.context_data['form'].errors}",
            )

        # Check that the delete persisted and we can't delete again
        response = valid_user_api_client.get(delete_url)
        assert response.status_code == 404

        # Check that if success was expected that the new version was persisted
        new_version = model.objects.exclude(pk=object.pk).get(
            version_group=object.version_group,
        )
        assert new_version != object

        # Check that the new version is a delete and is not approved yet
        assert new_version.update_type == UpdateType.DELETE
        assert new_version.transaction != object.transaction
        assert not new_version.transaction.workbasket.approved
        return new_version
Example #5
0
def tracked_model_to_activity_stream_item(obj: TrackedModel):
    """
    Convert a TrackedModel into a data object suitable for consumption by
    Activity Stream as outlined in https://www.w3.org/TR/activitystreams-core/

    Instead of providing the full nested data for every related object the
    relations are removed and provided as their equivalent ActivityStream IDs.
    """

    item_type = get_activity_stream_item_type(obj)
    item_id = get_activity_stream_item_id(obj)
    published = obj.transaction.updated_at.isoformat()

    # Find all the relations to remove and replace with activity stream IDs.
    exclusions = []
    extra_data = {}
    for relation, _ in obj.get_relations():
        exclusions.append(relation.name)
        relation_obj = getattr(obj, relation.name, None)
        if relation_obj:
            extra_data[
                f"{item_type}:{relation.name}"] = get_activity_stream_item_id(
                    relation_obj, )

    obj_data = {
        f"{item_type}:{key}": value
        for key, value in TrackedModelSerializer(
            obj,
            read_only=True,
            context={
                "format": "json"
            },
            child_kwargs={
                "omit": exclusions
            },
        ).data.items()
    }

    if f"{item_type}:valid_between" in obj_data:
        # DITs Elastic system has these as keywords:
        # https://github.com/uktrade/activity-stream/blob/e6fc63f/core/app/app_outgoing_elasticsearch.py#L335
        extra_data["startTime"] = obj_data[f"{item_type}:valid_between"].get(
            "lower")
        extra_data["endTime"] = obj_data[f"{item_type}:valid_between"].get(
            "upper")

    obj_data.update(**extra_data)

    data = {
        "id": item_id,
        "published": published,
        "object": {
            "id": item_id,
            "type": item_type,
            "name": str(obj),
            **obj_data,
        },
    }

    return data
Example #6
0
    def cache_object(self, obj: TrackedModel):
        """
        Caches an objects primary key and model name in the cache.

        Key is generated based on the model name and the identifying fields used
        to find it.
        """
        model = obj.__class__
        link_fields = self.get_handler_link_fields(model)

        for identifying_fields in link_fields:
            cache_key = self.generate_cache_key(
                model,
                identifying_fields,
                obj.get_identifying_fields(identifying_fields),
            )
            cache.set(cache_key, (obj.pk, model.__name__), timeout=None)
Example #7
0
    def remove_object_from_cache(self, obj: TrackedModel):
        """
        Removes an object from the importer cache. If an object has to be
        deleted (generally done in dev only) then it is problematic to keep the
        ID in the cache as well.

        Key is generated based on the model name and the identifying fields used
        to find it.
        """
        model = obj.__class__
        link_fields = self.get_handler_link_fields(model)

        for identifying_fields in link_fields:
            cache_key = self.generate_cache_key(
                model,
                identifying_fields,
                obj.get_identifying_fields(identifying_fields),
            )
            cache.delete(cache_key)
Example #8
0
    def check(
        model: Union[TrackedModel, Type[DjangoModelFactory]],
        serializer: Type[TrackedModelSerializer],
        parent_model: TrackedModel = None,
        dependencies: Dict[str, Union[TrackedModel, Type[DjangoModelFactory]]] = None,
        kwargs: Dict[str, Any] = None,
        validity=(date_ranges.normal, date_ranges.adjacent_no_end),
    ):
        update_type = request.param
        if isinstance(model, type) and issubclass(model, DjangoModelFactory):
            if parent_model:
                raise ValueError("Can't have parent_model and a factory defined")

            # Build kwargs and dependencies needed to make a complete model.
            # This can't rely on the factory itself as the dependencies need
            # to be in the database and .build does not save anything.
            kwargs = kwargs or {}
            for name, dependency_model in (dependencies or {}).items():
                if isinstance(dependency_model, type) and issubclass(
                    dependency_model,
                    DjangoModelFactory,
                ):
                    kwargs[name] = dependency_model.create()
                else:
                    kwargs[name] = dependency_model

            if validity:
                kwargs["valid_between"] = validity[0]

            parent_model = model.create(**kwargs)

            kwargs.update(parent_model.get_identifying_fields())
            if validity:
                kwargs["valid_between"] = validity[1]

            model = model.build(
                update_type=update_type,
                **kwargs,
            )
        elif not parent_model:
            raise ValueError("parent_model must be defined if an instance is provided")

        try:
            updated_model = imported_fields_match(
                model,
                serializer,
            )
        except model.__class__.DoesNotExist:
            if update_type == UpdateType.UPDATE:
                raise
            updated_model = model.__class__.objects.get(
                update_type=UpdateType.DELETE, **model.get_identifying_fields()
            )

        version_group = parent_model.version_group
        version_group.refresh_from_db()
        assert version_group.versions.count() == 2
        assert version_group == updated_model.version_group
        assert version_group.current_version == updated_model
        assert version_group.current_version.update_type == update_type
        return updated_model