def get_linked_models( cls: Type[BusinessRule], model: TrackedModel, transaction, ) -> Iterator[TrackedModel]: """ Returns latest approved model instances that have relations to the passed ``model`` and have this business rule listed in their ``business_rules`` attribute. :param model TrackedModel: Get models linked to this model instance :param transaction Transaction: Get latest approved versions of linked models as of this transaction :rtype Iterator[TrackedModel]: The linked models """ for field, related_model in get_relations(type(model)).items(): business_rules = getattr(related_model, "business_rules", []) if cls in business_rules: if field.one_to_many or field.many_to_many: related_instances = getattr(model, field.get_accessor_name()).all() else: related_instances = [getattr(model, field.name)] for instance in related_instances: try: yield instance.version_at(transaction) except TrackedModel.DoesNotExist: # `related_instances` will contain all instances, even # deleted ones, and `version_at` will return # `DoesNotExist` if the item has been deleted as of a # certain transaction. That's ok, because we can just # skip running business rules against deleted things. continue
def in_use(self, transaction=None, *relations: str) -> bool: """ Returns True if there are any models that are using this one as of the specified transaction. This can be any model this model is related to, but ignoring any subrecords (because e.g. a footnote is not considered "in use by" its own description) and then filtering for only things that link _to_ this model. The list of relations can be filtered by passing in the name of a relation. If a name is passed in that does not refer to a relation on this model, ``ValueError`` will be raised. """ # Get the list of models that use models of this type. class_ = self.__class__ using_models = set( relation.name for relation in (get_relations(class_).keys() - get_subrecord_relations(class_) - get_models_linked_to(class_).keys())) # If the user has specified names, check that they are sane # and then filter the relations to them, if relations: bad_names = set(relations) - set(using_models) if any(bad_names): raise ValueError( f"{bad_names} are unknown relations; use one of {using_models}", ) using_models = { relation for relation in using_models if relation in relations } # If this model doesn't have any using relations, it cannot be in use. if not any(using_models): return False # If we find any objects for any relation, then the model is in use. for relation_name in using_models: relation_queryset = self.in_use_by(relation_name, transaction) if relation_queryset.exists(): return True return False
def get_descriptions(self, transaction=None, request=None) -> TrackedModelQuerySet: """ Get the latest descriptions related to this instance of the Tracked Model. If there is no Description relation existing a `NoDescriptionError` is raised. If a transaction is provided then all latest descriptions that are either approved or in the workbasket of the transaction up to the transaction will be provided. """ try: descriptions_model = self.descriptions.model except AttributeError as e: raise NoDescriptionError( f"Model {self.__class__.__name__} has no descriptions relation.", ) from e for field, model in get_relations(descriptions_model).items(): if isinstance(self, model): field_name = field.name break else: raise NoDescriptionError( f"No foreign key back to model {self.__class__.__name__} " f"found on description model {descriptions_model.__name__}.", ) filter_kwargs = { f"{field_name}__{key}": value for key, value in self.get_identifying_fields().items() } query = descriptions_model.objects.filter(**filter_kwargs).order_by( *descriptions_model._meta.ordering) if transaction: return query.approved_up_to_transaction(transaction) # if a global transaction variable is available, filter objects approved up to this if get_current_transaction(): return query.current() return query.latest_approved()
def in_use_by(self, via_relation: str, transaction=None) -> QuerySet[TrackedModel]: """ Returns all of the models that are referencing this one via the specified relation and exist as of the passed transaction. ``via_relation`` should be the name of a relation, and a ``KeyError`` will be raised if the relation name is not valid for this model. Relations are accessible via get_relations helper method. """ relation = {r.name: r for r in get_relations(self.__class__).keys() }[via_relation] remote_model = relation.remote_field.model remote_field_name = get_accessor(relation.remote_field) return remote_model.objects.filter( **{ f"{remote_field_name}__version_group": self.version_group }).approved_up_to_transaction(transaction)
def build_dependency_tree( use_subrecord_codes: bool = False) -> 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 = { code for subclass in TrackedModel.__subclasses__() for code in get_record_codes(subclass) } for subclass in TrackedModel.__subclasses__(): for record_code in get_record_codes(subclass): if record_code not in dependency_map: dependency_map[record_code] = set() for relation in get_relations(subclass).values(): relation_codes = get_record_codes(relation) for relation_code in relation_codes: if relation_code != record_code and relation_code in record_codes: dependency_map[record_code].add(relation_code) return dependency_map
def described_object_field(cls) -> Field: for rel in get_relations(cls).keys(): if rel.name.startswith("described_"): return rel raise TypeError(f"{cls} should have a described field.")