From f5c2591fd4daab3e341e4400e3fea724dfd25509 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 4 May 2023 01:02:48 +0200 Subject: [PATCH] Add 'Tag' management (#4367) * 'Tag' management Fixes #83 * Add for ManufacturerPart, SupplierPart * Add tags for StockLocation, StockItem * fix serializer definition * add migrations * update pre-commit * bump dependencies * revert updates * set version for bugbear * remove bugbear * readd bugbear remove isort * and remove bugbear again * remove bugbear * make tag fields not required * add ruleset * Merge migrations * fix migrations * add unittest for detail * test tag add * order api * reduce database access * add tag modification test * use overriden serializer to ensuer the manager is always available * fix typo * fix serializer * increae query thershold by 1 * move tag serializer * fix migrations * content_types are changing between tests - removing them * remove unneeded fixture * Add basic docs * bump API version * add api access to the docs * add python code * Add tags to search and filters for all models --- InvenTree/InvenTree/api_version.py | 8 ++- InvenTree/InvenTree/serializers.py | 23 +++++++ InvenTree/InvenTree/settings.py | 1 + InvenTree/company/api.py | 13 +++- .../migrations/0057_auto_20230427_2033.py | 25 ++++++++ InvenTree/company/models.py | 4 ++ InvenTree/company/serializers.py | 17 +++++- InvenTree/part/api.py | 6 ++ InvenTree/part/migrations/0106_part_tags.py | 20 ++++++ InvenTree/part/models.py | 3 + InvenTree/part/serializers.py | 14 +++-- InvenTree/part/test_api.py | 10 +++ InvenTree/part/test_category.py | 2 +- InvenTree/stock/api.py | 15 ++++- .../migrations/0098_auto_20230427_2033.py | 25 ++++++++ InvenTree/stock/models.py | 5 ++ InvenTree/stock/serializers.py | 13 +++- InvenTree/users/models.py | 2 + docs/docs/extend/plugins/tags.md | 61 +++++++++++++++++++ docs/mkdocs.yml | 1 + requirements.in | 1 + requirements.txt | 3 + 22 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 InvenTree/company/migrations/0057_auto_20230427_2033.py create mode 100644 InvenTree/part/migrations/0106_part_tags.py create mode 100644 InvenTree/stock/migrations/0098_auto_20230427_2033.py create mode 100644 docs/docs/extend/plugins/tags.md diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index b39d3f1e43..a084d4dd1f 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,17 @@ # InvenTree API version -INVENTREE_API_VERSION = 110 +INVENTREE_API_VERSION = 111 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v111 -> 2023-05-02 : https://github.com/inventree/InvenTree/pull/4367 + - Adds tags to the Part serializer + - Adds tags to the SupplierPart serializer + - Adds tags to the ManufacturerPart serializer + - Adds tags to the StockItem serializer + - Adds tags to the StockLocation serializer v110 -> 2023-04-26 : https://github.com/inventree/InvenTree/pull/4698 - Adds 'order_currency' field for PurchaseOrder / SalesOrder endpoints diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 26d9ea7528..9d86af03fb 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -19,6 +19,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.fields import empty from rest_framework.serializers import DecimalField from rest_framework.utils import model_meta +from taggit.serializers import TaggitSerializer from common.models import InvenTreeSetting from common.settings import currency_code_default, currency_code_mappings @@ -264,6 +265,28 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data +class InvenTreeTaggitSerializer(TaggitSerializer): + """Updated from https://github.com/glemmaPaul/django-taggit-serializer.""" + + def update(self, instance, validated_data): + """Overriden update method to readd the tagmanager.""" + to_be_tagged, validated_data = self._pop_tags(validated_data) + + tag_object = super().update(instance, validated_data) + + for key in to_be_tagged.keys(): + # readd the tagmanager + new_tagobject = tag_object.__class__.objects.get(id=tag_object.id) + setattr(tag_object, key, getattr(new_tagobject, key)) + + return self._save_tags(tag_object, to_be_tagged) + + +class InvenTreeTagModelSerializer(InvenTreeTaggitSerializer, InvenTreeModelSerializer): + """Combination of InvenTreeTaggitSerializer and InvenTreeModelSerializer.""" + pass + + class UserSerializer(InvenTreeModelSerializer): """Serializer for a User.""" diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 38a921c778..0632f78245 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -225,6 +225,7 @@ INSTALLED_APPS = [ 'django_q', 'formtools', # Form wizard tools 'dbbackup', # Backups - django-dbbackup + 'taggit', # Tagging 'allauth', # Base app for SSO 'allauth.account', # Extend user with accounts diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index a0c4e8c34d..bd35fcc4ef 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -146,6 +146,8 @@ class ManufacturerPartFilter(rest_filters.FilterSet): 'manufacturer', 'MPN', 'part', + 'tags__name', + 'tags__slug', ] # Filter by 'active' status of linked part @@ -163,6 +165,7 @@ class ManufacturerPartList(ListCreateDestroyAPIView): 'part', 'manufacturer', 'supplier_parts', + 'tags', ) serializer_class = ManufacturerPartSerializer @@ -193,6 +196,8 @@ class ManufacturerPartList(ListCreateDestroyAPIView): 'part__IPN', 'part__name', 'part__description', + 'tags__name', + 'tags__slug', ] @@ -303,6 +308,8 @@ class SupplierPartFilter(rest_filters.FilterSet): 'part', 'manufacturer_part', 'SKU', + 'tags__name', + 'tags__slug', ] # Filter by 'active' status of linked part @@ -323,7 +330,9 @@ class SupplierPartList(ListCreateDestroyAPIView): - POST: Create a new SupplierPart object """ - queryset = SupplierPart.objects.all() + queryset = SupplierPart.objects.all().prefetch_related( + 'tags', + ) filterset_class = SupplierPartFilter def get_queryset(self, *args, **kwargs): @@ -403,6 +412,8 @@ class SupplierPartList(ListCreateDestroyAPIView): 'part__name', 'part__description', 'part__keywords', + 'tags__name', + 'tags__slug', ] diff --git a/InvenTree/company/migrations/0057_auto_20230427_2033.py b/InvenTree/company/migrations/0057_auto_20230427_2033.py new file mode 100644 index 0000000000..874cc484d5 --- /dev/null +++ b/InvenTree/company/migrations/0057_auto_20230427_2033.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.18 on 2023-04-27 20:33 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0005_auto_20220424_2025'), + ('company', '0056_alter_company_notes'), + ] + + operations = [ + migrations.AddField( + model_name='manufacturerpart', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='supplierpart', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 91818095ac..3170657f58 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -15,6 +15,7 @@ from django.utils.translation import gettext_lazy as _ from moneyed import CURRENCIES from stdimage.models import StdImageField +from taggit.managers import TaggableManager import common.models import common.settings @@ -310,6 +311,8 @@ class ManufacturerPart(MetadataMixin, models.Model): help_text=_('Manufacturer part description') ) + tags = TaggableManager() + @classmethod def create(cls, part, manufacturer, mpn, description, link=None): """Check if ManufacturerPart instance does not already exist then create it.""" @@ -445,6 +448,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin db_table = 'part_supplierpart' objects = SupplierPartManager() + tags = TaggableManager() @staticmethod def get_api_url(): diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index bad595d85f..3835371947 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from sql_util.utils import SubqueryCount +from taggit.serializers import TagListSerializerField import part.filters from InvenTree.serializers import (InvenTreeAttachmentSerializer, @@ -14,7 +15,9 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializer, InvenTreeDecimalField, InvenTreeImageSerializerField, InvenTreeModelSerializer, - InvenTreeMoneySerializer, RemoteImageMixin) + InvenTreeMoneySerializer, + InvenTreeTagModelSerializer, + RemoteImageMixin) from part.serializers import PartBriefSerializer from .models import (Company, CompanyAttachment, Contact, ManufacturerPart, @@ -149,7 +152,7 @@ class ContactSerializer(InvenTreeModelSerializer): ] -class ManufacturerPartSerializer(InvenTreeModelSerializer): +class ManufacturerPartSerializer(InvenTreeTagModelSerializer): """Serializer for ManufacturerPart object.""" class Meta: @@ -166,8 +169,12 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer): 'description', 'MPN', 'link', + + 'tags', ] + tags = TagListSerializerField(required=False) + def __init__(self, *args, **kwargs): """Initialize this serializer with extra detail fields as required""" part_detail = kwargs.pop('part_detail', True) @@ -236,7 +243,7 @@ class ManufacturerPartParameterSerializer(InvenTreeModelSerializer): manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', many=False, read_only=True) -class SupplierPartSerializer(InvenTreeModelSerializer): +class SupplierPartSerializer(InvenTreeTagModelSerializer): """Serializer for SupplierPart object.""" class Meta: @@ -267,6 +274,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'supplier_detail', 'url', 'updated', + + 'tags', ] read_only_fields = [ @@ -274,6 +283,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer): 'barcode_hash', ] + tags = TagListSerializerField(required=False) + def __init__(self, *args, **kwargs): """Initialize this serializer with extra detail fields as required""" diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 3b0cc3000b..c2afd23fbe 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -970,6 +970,10 @@ class PartFilter(rest_filters.FilterSet): virtual = rest_filters.BooleanFilter() + tags_name = rest_filters.CharFilter(field_name='tags__name', lookup_expr='iexact') + + tags_slug = rest_filters.CharFilter(field_name='tags__slug', lookup_expr='iexact') + class PartMixin: """Mixin class for Part API endpoints""" @@ -1240,6 +1244,8 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI): 'category__name', 'manufacturer_parts__MPN', 'supplier_parts__SKU', + 'tags__name', + 'tags__slug', ] diff --git a/InvenTree/part/migrations/0106_part_tags.py b/InvenTree/part/migrations/0106_part_tags.py new file mode 100644 index 0000000000..b4a6badea2 --- /dev/null +++ b/InvenTree/part/migrations/0106_part_tags.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.18 on 2023-04-27 20:33 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0005_auto_20220424_2025'), + ('part', '0105_alter_part_notes'), + ] + + operations = [ + migrations.AddField( + model_name='part', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6a714f49d4..2c0ca74a6e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -31,6 +31,7 @@ from mptt.exceptions import InvalidMove from mptt.managers import TreeManager from mptt.models import MPTTModel, TreeForeignKey from stdimage.models import StdImageField +from taggit.managers import TaggableManager import common.models import common.settings @@ -336,6 +337,7 @@ class PartManager(TreeManager): 'category__parent', 'stock_items', 'builds', + 'tags', ) @@ -378,6 +380,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel) """ objects = PartManager() + tags = TaggableManager() class Meta: """Metaclass defines extra model properties""" diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 4a07afc16e..4aee89f050 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -15,6 +15,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from sql_util.utils import SubqueryCount, SubquerySum +from taggit.serializers import TagListSerializerField import common.models import company.models @@ -31,8 +32,9 @@ from InvenTree.serializers import (DataFileExtractSerializer, InvenTreeDecimalField, InvenTreeImageSerializerField, InvenTreeModelSerializer, - InvenTreeMoneySerializer, RemoteImageMixin, - UserSerializer) + InvenTreeMoneySerializer, + InvenTreeTagModelSerializer, + RemoteImageMixin, UserSerializer) from InvenTree.status_codes import BuildStatus from InvenTree.tasks import offload_task @@ -403,7 +405,7 @@ class InitialSupplierSerializer(serializers.Serializer): return data -class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer): +class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): """Serializer for complete detail information of a part. Used when displaying all details of a single component. @@ -464,13 +466,17 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer): 'duplicate', 'initial_stock', 'initial_supplier', - 'copy_category_parameters' + 'copy_category_parameters', + + 'tags', ] read_only_fields = [ 'barcode_hash', ] + tags = TagListSerializerField(required=False) + def __init__(self, *args, **kwargs): """Custom initialization method for PartSerializer: diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 935ae5baf3..af15ad5de7 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1425,6 +1425,7 @@ class PartDetailTests(PartAPITestBase): 'name': 'my test api part', 'description': 'a part created with the API', 'category': 1, + 'tags': '["tag1", "tag2"]', } ) @@ -1438,6 +1439,8 @@ class PartDetailTests(PartAPITestBase): part = Part.objects.get(pk=pk) self.assertEqual(part.name, 'my test api part') + self.assertEqual(part.tags.count(), 2) + self.assertEqual([a.name for a in part.tags.order_by('slug')], ['tag1', 'tag2']) # Edit the part url = reverse('api-part-detail', kwargs={'pk': pk}) @@ -1468,6 +1471,13 @@ class PartDetailTests(PartAPITestBase): self.assertEqual(response.status_code, 200) + # Try to remove a tag + response = self.patch(url, { + 'tags': ['tag1',], + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['tags'], ['tag1']) + # Try to remove the part response = self.delete(url) diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index f484314415..f164e56443 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -152,7 +152,7 @@ class CategoryTest(TestCase): def test_parameters(self): """Test that the Category parameters are correctly fetched.""" # Check number of SQL queries to iterate other parameters - with self.assertNumQueries(8): + with self.assertNumQueries(9): # Prefetch: 3 queries (parts, parameters and parameters_template) fasteners = self.fasteners.prefetch_parts_parameters() # Iterate through all parts and parameters diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9e8ac14fc8..39c25ac23c 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -214,7 +214,9 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI): - POST: Create a new StockLocation """ - queryset = StockLocation.objects.all() + queryset = StockLocation.objects.all().prefetch_related( + 'tags', + ) serializer_class = StockSerializers.LocationSerializer def download_queryset(self, queryset, export_format): @@ -300,11 +302,15 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI): 'name', 'structural', 'external', + 'tags__name', + 'tags__slug', ] search_fields = [ 'name', 'description', + 'tags__name', + 'tags__slug', ] ordering_fields = [ @@ -351,6 +357,8 @@ class StockFilter(rest_filters.FilterSet): 'customer', 'sales_order', 'purchase_order', + 'tags__name', + 'tags__slug', ] # Relationship filters @@ -811,7 +819,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): queryset = queryset.prefetch_related( 'part', 'part__category', - 'location' + 'location', + 'tags', ) return queryset @@ -1035,6 +1044,8 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView): 'part__IPN', 'part__description', 'location__name', + 'tags__name', + 'tags__slug', ] diff --git a/InvenTree/stock/migrations/0098_auto_20230427_2033.py b/InvenTree/stock/migrations/0098_auto_20230427_2033.py new file mode 100644 index 0000000000..097190b6d9 --- /dev/null +++ b/InvenTree/stock/migrations/0098_auto_20230427_2033.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.18 on 2023-04-27 20:33 + +from django.db import migrations +import taggit.managers + + +class Migration(migrations.Migration): + + dependencies = [ + ('taggit', '0005_auto_20220424_2025'), + ('stock', '0097_alter_stockitem_notes'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='stocklocation', + name='tags', + field=taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 65406d3023..a79bb2febc 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -21,6 +21,7 @@ from django.utils.translation import gettext_lazy as _ from jinja2 import Template from mptt.managers import TreeManager from mptt.models import MPTTModel, TreeForeignKey +from taggit.managers import TaggableManager import common.models import InvenTree.helpers @@ -53,6 +54,8 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree): verbose_name = _('Stock Location') verbose_name_plural = _('Stock Locations') + tags = TaggableManager() + def delete_recursive(self, *args, **kwargs): """This function handles the recursive deletion of sub-locations depending on kwargs contents""" delete_stock_items = kwargs.get('delete_stock_items', False) @@ -321,6 +324,8 @@ class StockItem(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, commo } } + tags = TaggableManager() + # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( quantity__gt=0, diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 1d93f900bd..6775c53dbf 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -12,6 +12,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.serializers import ValidationError from sql_util.utils import SubqueryCount, SubquerySum +from taggit.serializers import TagListSerializerField import common.models import company.models @@ -76,7 +77,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer): return value -class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): +class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): """Serializer for a StockItem. - Includes serialization for the linked part @@ -123,6 +124,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): 'updated', 'purchase_price', 'purchase_price_currency', + + 'tags', ] """ @@ -236,6 +239,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True) sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True) + tags = TagListSerializerField(required=False) + def __init__(self, *args, **kwargs): """Add detail fields.""" part_detail = kwargs.pop('part_detail', False) @@ -566,7 +571,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer): ] -class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer): +class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): """Detailed information about a stock location.""" class Meta: @@ -587,6 +592,8 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer): 'icon', 'structural', 'external', + + 'tags', ] read_only_fields = [ @@ -610,6 +617,8 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer): level = serializers.IntegerField(read_only=True) + tags = TagListSerializerField(required=False) + class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer): """Serializer for StockItemAttachment model.""" diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 9f08f1a892..93f530598b 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -79,6 +79,8 @@ class RuleSet(models.Model): 'plugin_pluginsetting', 'plugin_notificationusersetting', 'common_newsfeedentry', + 'taggit_tag', + 'taggit_taggeditem', ], 'part_category': [ 'part_partcategory', diff --git a/docs/docs/extend/plugins/tags.md b/docs/docs/extend/plugins/tags.md new file mode 100644 index 0000000000..2ecc16aecd --- /dev/null +++ b/docs/docs/extend/plugins/tags.md @@ -0,0 +1,61 @@ +--- +title: Item Tags +--- + +## Tags + +Several models in InvenTree can be tagged with arbitrary tags. Tags are useful for grouping items together. This can be used to mark items with a plugin or to group items together for a particular theme. Tags are meant to be used by programms and are not visible to the end user. +Tags are shared between all models that can be tagged. + +The following models can be tagged: +- [Parts](../../part/part.md) and [Supplier Parts](../../order/company#supplier-parts)/[Manufacturer Part](../../order/company#manufacturer-parts) +- [Stock Items](../../stock/stock.md#stock-item) / [Stock Location](../../stock/stock.md#stock-location) + + +## Accessing Tags + +### Plugin Access + +The `tags` field can be accessed and updated directly from custom plugin code, as follows: + +```python +from part.models import Part + +# Show tags for a particular Part instance +part = Part.objects.get(pk=100) +print(part.tags) + +> {['Tag1', 'Another Tag']} + +# Tags can also be accessed via tags.all() +print(part.tags.all()) + +> {['Tag1', 'Another Tag']} + +# Add tag +part.tags.add('Tag 2') +print(part.tags) + +> {['Tag1', 'Tag 2', 'Another Tag']} + +# Remove tag +part.tags.remove('Tag1') +print(part.tags) + +> {['Tag 2', 'Another Tag']} + +# Filter by tags +Part.objects.filter(tags__name__in=["Tag1", "Tag 2"]).distinct() +``` + +### API Access + +For models which provide tags, access is also provided via the API. The tags are exposed via the detail endpoint for the models starting from version 111. + +Tags can be cached via PATCH or POST requests. The tags are provided as a json formatted list of strings. The tags are note case sensitive and must be unique across the instance - else the exsisting tag gets assigned. The tags are not sorted and the order is not guaranteed. + +```json +{ + "tags": '["foo", "bar"]' +} +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index aabd0aabf3..ec06820193 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -166,6 +166,7 @@ nav: - Installation: extend/plugins/install.md - Developing a Plugin: extend/how_to_plugin.md - Model Meatadata: extend/plugins/metadata.md + - Tags: extend/plugins/tags.md - Plugin Mixins: - Action Mixin: extend/plugins/action.md - API Mixin: extend/plugins/api.md diff --git a/requirements.in b/requirements.in index 5ab78a112a..dec51e3454 100644 --- a/requirements.in +++ b/requirements.in @@ -23,6 +23,7 @@ django-q-sentry # sentry.io integration for django-q django-sql-utils # Advanced query annotation / aggregation django-sslserver # Secure HTTP development server django-stdimage<6.0.0 # Advanced ImageField management # FIXED 2022-06-29 6.0.0 breaks serialization for django-q +django-taggit # Tagging support django-user-sessions # user sessions in DB django-weasyprint # django weasyprint integration djangorestframework # DRF framework diff --git a/requirements.txt b/requirements.txt index 0fbc0ade90..4d6d258a85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,6 +68,7 @@ django==3.2.18 # django-sql-utils # django-sslserver # django-stdimage + # django-taggit # django-user-sessions # django-weasyprint # django-xforwardedfor-middleware @@ -125,6 +126,8 @@ django-sslserver==0.22 # via -r requirements.in django-stdimage==5.3.0 # via -r requirements.in +django-taggit==3.1.0 + # via -r requirements.in django-user-sessions==2.0.0 # via -r requirements.in django-weasyprint==2.2.0