diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index a69d1cce15..ace7b7a7b7 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -251,20 +251,14 @@ class CmdCopy(ObjManipCommand): copy an object and its properties Usage: - copy[/reset] [= ][;alias;alias..] + copy [= ][;alias;alias..] [:] [, ...] - switch: - reset - make a 'clean' copy off the object, thus - removing any changes that might have been made to the original - since it was first created. - Create one or more copies of an object. If you don't supply any targets, one exact copy of the original object will be created with the name *_copy. """ key = "copy" - switch_options = ("reset",) locks = "cmd:perm(copy) or perm(Builder)" help_category = "Building" diff --git a/evennia/contrib/puzzles.py b/evennia/contrib/puzzles.py index 5eef1f77e2..924addfc32 100644 --- a/evennia/contrib/puzzles.py +++ b/evennia/contrib/puzzles.py @@ -588,15 +588,30 @@ def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients): class CmdUsePuzzleParts(MuxCommand): + """ + Use an object, or a group of objects at once. + + + Example: + You look around you and see a pole, a long string, and a needle. + + use pole, long string, needle + + Genius! You built a fishing pole. + + + Usage: + use [,obj2,...] + """ + + # Technical explanation """ Searches for all puzzles whose parts match the given set of objects. If there are matching puzzles, the result objects are spawned in their corresponding location if all parts have been passed in. - - Usage: - use ] """ + key = "use" aliases = "combine" locks = "cmd:pperm(use) or pperm(Player)" diff --git a/evennia/locks/tests.py b/evennia/locks/tests.py index dce0e28fff..c078b597ea 100644 --- a/evennia/locks/tests.py +++ b/evennia/locks/tests.py @@ -175,6 +175,16 @@ class TestLockfuncs(EvenniaTest): self.assertEqual(True, lockfuncs.objtag(None, self.obj2, "test2", "category1")) self.assertEqual(False, lockfuncs.objtag(None, self.obj2, "test2")) + def test_traverse_taglock(self): + self.obj2.tags.add("test1", "category1") + self.exit.locks.add("traverse:tag(test1,category1)") + self.assertEqual(self.exit.access(self.obj2, "traverse"), True) + + def test_traverse_taglock_fail(self): + self.obj2.tags.add("test1") # missing the category + self.exit.locks.add("traverse:tag(test1,category1)") + self.assertEqual(self.exit.access(self.obj2, "traverse"), False) + def test_inside_holds(self): self.assertEqual(True, lockfuncs.inside(self.char1, self.room1)) self.assertEqual(False, lockfuncs.inside(self.char1, self.room2)) diff --git a/evennia/typeclasses/managers.py b/evennia/typeclasses/managers.py index fe2ffef3b2..092a8eba6c 100644 --- a/evennia/typeclasses/managers.py +++ b/evennia/typeclasses/managers.py @@ -31,7 +31,14 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # Attribute manager methods def get_attribute( - self, key=None, category=None, value=None, strvalue=None, obj=None, attrtype=None, **kwargs + self, + key=None, + category=None, + value=None, + strvalue=None, + obj=None, + attrtype=None, + **kwargs ): """ Return Attribute objects by key, by category, by value, by @@ -75,9 +82,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # no reason to make strvalue/value mutually exclusive at this level query.append(("attribute__db_value", value)) return Attribute.objects.filter( - pk__in=self.model.db_attributes.through.objects.filter(**dict(query)).values_list( - "attribute_id", flat=True - ) + pk__in=self.model.db_attributes.through.objects.filter( + **dict(query) + ).values_list("attribute_id", flat=True) ) def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None): @@ -104,7 +111,13 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): ) def get_by_attribute( - self, key=None, category=None, value=None, strvalue=None, attrtype=None, **kwargs + self, + key=None, + category=None, + value=None, + strvalue=None, + attrtype=None, + **kwargs ): """ Return objects having attributes with the given key, category, @@ -132,7 +145,10 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): """ dbmodel = self.model.__dbclass__.__name__.lower() - query = [("db_attributes__db_attrtype", attrtype), ("db_attributes__db_model", dbmodel)] + query = [ + ("db_attributes__db_attrtype", attrtype), + ("db_attributes__db_model", dbmodel), + ] if key: query.append(("db_attributes__db_key", key)) if category: @@ -158,11 +174,15 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): obj (list): Objects having the matching Nicks. """ - return self.get_by_attribute(key=key, category=category, strvalue=nick, attrtype="nick") + return self.get_by_attribute( + key=key, category=category, strvalue=nick, attrtype="nick" + ) # Tag manager methods - def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False): + def get_tag( + self, key=None, category=None, obj=None, tagtype=None, global_search=False + ): """ Return Tag objects by key, by category, by object (it is stored on) or with a combination of those criteria. @@ -206,9 +226,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): if category: query.append(("tag__db_category", category)) return Tag.objects.filter( - pk__in=self.model.db_tags.through.objects.filter(**dict(query)).values_list( - "tag_id", flat=True - ) + pk__in=self.model.db_tags.through.objects.filter( + **dict(query) + ).values_list("tag_id", flat=True) ) def get_permission(self, key=None, category=None, obj=None): @@ -279,7 +299,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): if not _Tag: from evennia.typeclasses.models import Tag as _Tag - match = kwargs.get("match", "all").lower().strip() + anymatch = "any" == kwargs.get("match", "all").lower().strip() keys = make_iter(key) if key else [] categories = make_iter(category) if category else [] @@ -290,7 +310,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): dbmodel = self.model.__dbclass__.__name__.lower() query = ( - self.filter(db_tags__db_tagtype__iexact=tagtype, db_tags__db_model__iexact=dbmodel) + self.filter( + db_tags__db_tagtype__iexact=tagtype, db_tags__db_model__iexact=dbmodel + ) .distinct() .order_by("id") ) @@ -309,28 +331,30 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): ) clauses = Q() for ikey, key in enumerate(keys): - # Keep each key and category together, grouped by AND - clauses |= Q(db_key__iexact=key, db_category__iexact=categories[ikey]) - + # ANY mode; must match any one of the given tags/categories + clauses |= Q( + db_key__iexact=key, db_category__iexact=categories[ikey] + ) else: # only one or more categories given - # import evennia;evennia.set_trace() clauses = Q() + # ANY mode; must match any one of them for category in unique_categories: clauses |= Q(db_category__iexact=category) tags = _Tag.objects.filter(clauses) query = query.filter(db_tags__in=tags).annotate( - matches=Count("db_tags__pk", filter=Q(db_tags__in=tags), distinct=True) + matches=Count("db_tags__pk", filter=Q(db_tags__in=tags), + distinct=True) ) - # Default ALL: Match all of the tags and optionally more - if match == "all": - n_req_tags = tags.count() if n_keys > 0 else n_unique_categories - query = query.filter(matches__gte=n_req_tags) - # ANY: Match any single tag, ordered by weight - elif match == "any": + if anymatch: + # ANY: Match any single tag, ordered by weight query = query.order_by("-matches") + else: + # Default ALL: Match all of the tags and optionally more + n_req_tags = n_keys if n_keys > 0 else n_unique_categories + query = query.filter(matches__gte=n_req_tags) return query @@ -388,7 +412,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): # try to get old tag dbmodel = self.model.__dbclass__.__name__.lower() - tag = self.get_tag(key=key, category=category, tagtype=tagtype, global_search=True) + tag = self.get_tag( + key=key, category=category, tagtype=tagtype, global_search=True + ) if tag and data is not None: # get tag from list returned by get_tag tag = tag[0] @@ -402,7 +428,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): from evennia.typeclasses.models import Tag as _Tag tag = _Tag.objects.create( db_key=key.strip().lower() if key is not None else None, - db_category=category.strip().lower() if category and key is not None else None, + db_category=category.strip().lower() + if category and key is not None + else None, db_data=data, db_model=dbmodel, db_tagtype=tagtype.strip().lower() if tagtype is not None else None, @@ -511,7 +539,8 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): typeclass=F("db_typeclass_path"), # Calculate this class' percentage of total composition percent=ExpressionWrapper( - ((F("count") / float(self.count())) * 100.0), output_field=FloatField() + ((F("count") / float(self.count())) * 100.0), + output_field=FloatField(), ), ) .values("typeclass", "count", "percent") @@ -531,7 +560,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): stats = self.get_typeclass_totals().order_by("typeclass") return {x.get("typeclass"): x.get("count") for x in stats} - def typeclass_search(self, typeclass, include_children=False, include_parents=False): + def typeclass_search( + self, typeclass, include_children=False, include_parents=False + ): """ Searches through all objects returning those which has a certain typeclass. If location is set, limit search to objects @@ -806,7 +837,8 @@ class TypeclassManager(TypedObjectManager): """ paths = [self.model.path] + [ - "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) + "%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model) ] kwargs.update({"db_typeclass_path__in": paths}) return super().get(**kwargs) @@ -828,7 +860,8 @@ class TypeclassManager(TypedObjectManager): """ # query, including all subclasses paths = [self.model.path] + [ - "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) + "%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model) ] kwargs.update({"db_typeclass_path__in": paths}) return super().filter(*args, **kwargs) @@ -843,6 +876,7 @@ class TypeclassManager(TypedObjectManager): """ paths = [self.model.path] + [ - "%s.%s" % (cls.__module__, cls.__name__) for cls in self._get_subclasses(self.model) + "%s.%s" % (cls.__module__, cls.__name__) + for cls in self._get_subclasses(self.model) ] return super().all().filter(db_typeclass_path__in=paths) diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index eb2f8e45e1..93b100f87b 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -58,12 +58,16 @@ class TestTypedObjectManager(EvenniaTest): self.obj2.tags.add("tag4") self.obj2.tags.add("tag2c") self.assertEqual(self._manager("get_by_tag", "tag1"), [self.obj1]) - self.assertEqual(set(self._manager("get_by_tag", "tag2")), set([self.obj1, self.obj2])) + self.assertEqual( + set(self._manager("get_by_tag", "tag2")), set([self.obj1, self.obj2]) + ) self.assertEqual(self._manager("get_by_tag", "tag2a"), [self.obj2]) self.assertEqual(self._manager("get_by_tag", "tag3 with spaces"), [self.obj2]) self.assertEqual(self._manager("get_by_tag", ["tag2a", "tag2b"]), [self.obj2]) self.assertEqual(self._manager("get_by_tag", ["tag2a", "tag1"]), []) - self.assertEqual(self._manager("get_by_tag", ["tag2a", "tag4", "tag2c"]), [self.obj2]) + self.assertEqual( + self._manager("get_by_tag", ["tag2a", "tag4", "tag2c"]), [self.obj2] + ) def test_get_by_tag_and_category(self): self.obj1.tags.add("tag5", "category1") @@ -79,24 +83,66 @@ class TestTypedObjectManager(EvenniaTest): self.obj1.tags.add("tag8", "category6") self.obj2.tags.add("tag9", "category6") - self.assertEqual(self._manager("get_by_tag", "tag5", "category1"), [self.obj1, self.obj2]) + self.assertEqual( + self._manager("get_by_tag", "tag5", "category1"), [self.obj1, self.obj2] + ) self.assertEqual(self._manager("get_by_tag", "tag6", "category1"), []) - self.assertEqual(self._manager("get_by_tag", "tag6", "category3"), [self.obj1, self.obj2]) + self.assertEqual( + self._manager("get_by_tag", "tag6", "category3"), [self.obj1, self.obj2] + ) self.assertEqual( self._manager("get_by_tag", ["tag5", "tag6"], ["category1", "category3"]), [self.obj1, self.obj2], ) self.assertEqual( - self._manager("get_by_tag", ["tag5", "tag7"], "category1"), [self.obj1, self.obj2] + self._manager("get_by_tag", ["tag5", "tag7"], "category1"), + [self.obj1, self.obj2], + ) + self.assertEqual( + self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2] ) - self.assertEqual(self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2]) self.assertEqual(self._manager("get_by_tag", category="category2"), [self.obj2]) self.assertEqual( - self._manager("get_by_tag", category=["category1", "category3"]), [self.obj1, self.obj2] + self._manager("get_by_tag", category=["category1", "category3"]), + [self.obj1, self.obj2], ) self.assertEqual( - self._manager("get_by_tag", category=["category1", "category2"]), [self.obj1, self.obj2] + self._manager("get_by_tag", category=["category1", "category2"]), + [self.obj1, self.obj2], + ) + self.assertEqual( + self._manager("get_by_tag", category=["category5", "category4"]), [] + ) + self.assertEqual( + self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2] + ) + self.assertEqual( + self._manager("get_by_tag", category="category6"), [self.obj1, self.obj2] + ) + + def test_get_tag_with_all(self): + self.obj1.tags.add("tagA", "categoryA") + self.assertEqual( + self._manager( + "get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB"], match="all" + ), + [], + ) + + def test_get_tag_with_any(self): + self.obj1.tags.add("tagA", "categoryA") + self.assertEqual( + self._manager( + "get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB"], match="any" + ), + [self.obj1], + ) + + def test_get_tag_withnomatch(self): + self.obj1.tags.add("tagC", "categoryC") + self.assertEqual( + self._manager( + "get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB"], match="any" + ), + [], ) - self.assertEqual(self._manager("get_by_tag", category=["category5", "category4"]), []) - self.assertEqual(self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2]) - self.assertEqual(self._manager("get_by_tag", category="category6"), [self.obj1, self.obj2])