Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
ea17151
feat(backend): add guides / tipps endpoint
matmair Oct 29, 2025
f5a093a
fix api typing
matmair Oct 29, 2025
24c1000
fix pr ref
matmair Oct 29, 2025
bdfe92a
add missing ruleset
matmair Oct 29, 2025
a4d696e
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Oct 30, 2025
d6678cd
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Oct 30, 2025
65c8789
more validation
matmair Oct 30, 2025
1e5d22a
add initial types
matmair Oct 30, 2025
397dc4c
add collection mechanisms
matmair Oct 30, 2025
4b22594
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Oct 30, 2025
f002b8e
add frontend component scafold
matmair Oct 30, 2025
5964d9f
fix default
matmair Oct 30, 2025
6f90c19
fix missing migration
matmair Oct 30, 2025
7008568
fix name conflict
matmair Oct 30, 2025
82dca39
make logging more reliable
matmair Oct 30, 2025
ba92372
switch to real api rendering
matmair Oct 30, 2025
6efed1c
add a first
matmair Oct 30, 2025
d485d37
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Oct 30, 2025
4b6e28c
rename data field
matmair Oct 30, 2025
6a33a05
fix discovery
matmair Oct 30, 2025
bfd394d
move around pieces
matmair Oct 30, 2025
8497db8
merge migrations
matmair Oct 31, 2025
4f7cf0a
make more robust
matmair Oct 31, 2025
4dce1ee
Merge branch 'master' into add-tipps-model
matmair Nov 1, 2025
a10c222
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 5, 2025
c84e102
Merge branch 'add-tipps-model' of https://github.com/matmair/InvenTre…
matmair Nov 5, 2025
7f266e1
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 5, 2025
4be4dad
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 6, 2025
5d86ebc
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 10, 2025
913d269
add way to dismiss tests
matmair Nov 10, 2025
3ed5e56
Merge branch 'master' into add-tipps-model
matmair Nov 11, 2025
fcfc817
add dismissal diag
matmair Nov 14, 2025
4335e00
Merge branch 'add-tipps-model' of https://github.com/matmair/InvenTre…
matmair Nov 14, 2025
eb30bf1
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 14, 2025
87e9755
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 16, 2025
4815cf9
add filter for all
matmair Nov 19, 2025
107b38b
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 19, 2025
09d02aa
fix style
matmair Nov 19, 2025
a72c74a
Merge branch 'master' into add-tipps-model
matmair Nov 19, 2025
b48fd05
fix install issues
matmair Nov 19, 2025
ffaa249
fix bool
matmair Nov 19, 2025
b90059c
Merge branch 'master' into add-tipps-model
matmair Nov 20, 2025
c44ceca
Merge branch 'master' into add-tipps-model
matmair Nov 22, 2025
e2556b1
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 23, 2025
41015ab
simplify models
matmair Nov 23, 2025
a00d14c
fix serializer
matmair Nov 23, 2025
c7993dc
respect is_applicable
matmair Nov 24, 2025
6798169
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 24, 2025
be08fea
update migrations
matmair Nov 24, 2025
9ba95b6
make migration less dependenat
matmair Nov 24, 2025
5e149e9
refactor is_applicable
matmair Nov 24, 2025
29e3bce
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Nov 24, 2025
aa61b12
fix import
matmair Nov 24, 2025
b017120
move import
matmair Nov 24, 2025
b6850e6
add optional typing
matmair Nov 24, 2025
417a708
add tests
matmair Nov 24, 2025
18491da
move typing
matmair Nov 24, 2025
134c0b1
make message more clear
matmair Nov 24, 2025
aca70cc
ignore code paths that do not require coverage
matmair Nov 24, 2025
2303872
extend test to ensure model str works
matmair Nov 24, 2025
3c53351
extend testing
matmair Nov 24, 2025
e748e4c
add ftue
matmair Nov 24, 2025
4b603b2
ensure links can have nice texts too
matmair Nov 24, 2025
eddeb89
fix dismissal logic to respect user
matmair Nov 24, 2025
5d33a4c
fix test
matmair Nov 24, 2025
e6e3b55
add key to actiongrid
matmair Nov 24, 2025
a76bb05
add links to tipps
matmair Nov 24, 2025
afc3869
fix firstuse check
matmair Nov 24, 2025
9b11f98
fix assertation
matmair Nov 24, 2025
5136af8
adjust assertation
matmair Nov 25, 2025
8501d54
Merge branch 'master' into add-tipps-model
matmair Nov 26, 2025
371fe03
Merge branch 'master' into add-tipps-model
matmair Nov 27, 2025
f734ba0
Merge branch 'master' into add-tipps-model
matmair Nov 30, 2025
8cbe841
Merge branch 'master' into add-tipps-model
matmair Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 430
INVENTREE_API_VERSION = 431
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """

v431 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10715
- Adds GuideDefinition and GuideExecution models and API endpoints to provide tipps and guides within InvenTree's web frontend.

v430 -> 2025-12-04 : https://github.com/inventree/InvenTree/pull/10699
- Removed the "PartParameter" and "PartParameterTemplate" API endpoints
- Removed the "ManufacturerPartParameter" API endpoint
Expand Down
10 changes: 10 additions & 0 deletions src/backend/InvenTree/InvenTree/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Provides helper functions used throughout the InvenTree project."""

import datetime
import gc
import hashlib
import inspect
import io
Expand Down Expand Up @@ -1147,6 +1148,15 @@ def inheritors(
return subcls


def instances_of(cls: type[Inheritors_T]) -> list[Inheritors_T]:
"""Return instances of a class.

Args:
cls: The class of which type instances should be searched
"""
return [k for k in gc.get_referrers(cls) if k.__class__ is cls]


def pui_url(subpath: str) -> str:
"""Return the URL for a web subpath."""
if not subpath.startswith('/'):
Expand Down
4 changes: 4 additions & 0 deletions src/backend/InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ class FilterableIntegerField(FilterableSerializerField, serializers.IntegerField
"""Custom IntegerField which allows filtering."""


class FilterableJSONField(FilterableSerializerField, serializers.JSONField):
"""Custom JSONField which allows filtering."""


# endregion


Expand Down
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@
'machine.apps.MachineConfig',
'data_exporter.apps.DataExporterConfig',
'importer.apps.ImporterConfig',
'web',
'web.apps.WebConfig',
'generic',
'InvenTree.apps.InvenTreeConfig', # InvenTree app runs last
# Core django modules
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/InvenTree/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import report.api
import stock.api
import users.api
import web.api
from plugin.urls import get_plugin_urls
from web.urls import cui_compatibility_urls
from web.urls import urlpatterns as platform_urls
Expand Down Expand Up @@ -80,6 +81,7 @@
]),
),
path('user/', include(users.api.user_urls)),
path('web/', include(web.api.web_urls)),
# Plugin endpoints
path('', include(plugin.api.plugin_api_urls)),
# Common endpoints endpoint
Expand Down
3 changes: 3 additions & 0 deletions src/backend/InvenTree/users/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ def get_ruleset_ignore() -> list[str]:
'common_selectionlist',
'users_owner',
'users_userprofile', # User profile is handled in the serializer - only own user can change
# Web
'web_guidedefinition',
'web_guideexecution',
# Third-party tables
'error_report_error',
'exchange_rate',
Expand Down
23 changes: 23 additions & 0 deletions src/backend/InvenTree/web/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Admin classes for the 'web' app."""

from django.contrib import admin

from web.models import GuideDefinition, GuideExecution


class GuideExecutionInline(admin.TabularInline):
"""Inline for guide executions."""

model = GuideExecution
extra = 1


@admin.register(GuideDefinition)
class GuideDefinitionAdmin(admin.ModelAdmin):
"""Admin class for the GuideDefinition model."""

list_display = ('guide_type', 'name', 'slug')
list_filter = ('guide_type',)
inlines = [GuideExecutionInline]
fields = ('guide_type', 'name', 'slug', 'description', 'guide_data', 'metadata')
prepopulated_fields = {'slug': ('name',)}
122 changes: 122 additions & 0 deletions src/backend/InvenTree/web/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""DRF API definition for the 'web' app."""

from datetime import datetime

from django.urls import include, path

import django_filters.rest_framework.filters as rest_filters
import structlog

import InvenTree.permissions
from InvenTree.fields import InvenTreeOutputOption, OutputConfiguration
from InvenTree.filters import SEARCH_ORDER_FILTER
from InvenTree.helpers import str2bool
from InvenTree.mixins import (
ListAPI,
OutputOptionsMixin,
RetrieveAPI,
SerializerContextMixin,
UpdateAPI,
)
from web.models import GuideDefinition
from web.serializers import EmptySerializer, GuideDefinitionSerializer

logger = structlog.get_logger('inventree')


class GuideDefinitionFilter:
"""Filter class for GuideDefinition objects."""

all = rest_filters.BooleanFilter(
label='Show all definitions', method='filter_all', default=False
)

def filter_all(self, queryset, name, value):
"""Filter to show all definitions."""
if str2bool(value):
return queryset
return queryset.filter(executions__done=False)


class GuideDefinitionMixin(SerializerContextMixin):
"""Mixin for GuideDefinition detail views."""

queryset = GuideDefinition.objects.all()
serializer_class = GuideDefinitionSerializer
permission_classes = [InvenTree.permissions.IsStaffOrReadOnlyScope]
filter_backends = SEARCH_ORDER_FILTER
filterclass = GuideDefinitionFilter

def get_queryset(self):
"""Return queryset for this endpoint."""
return super().get_queryset().prefetch_related('executions')


class GuideDefinitionDetailOptions(OutputConfiguration):
"""Holds all available output options for Group views."""

OPTIONS = [
InvenTreeOutputOption(
'description',
description='Include description field (optional, may be large)',
),
InvenTreeOutputOption(
'guide_data',
description='Include data field (optional, may be large JSON object and is only meant for machines)',
),
]


class GuideDefinitionDetail(GuideDefinitionMixin, OutputOptionsMixin, RetrieveAPI):
"""Detail for a particular guide definition."""

output_options = GuideDefinitionDetailOptions


class GuideDefinitionList(GuideDefinitionMixin, OutputOptionsMixin, ListAPI):
"""List of guide definitions."""

output_options = GuideDefinitionDetailOptions
filter_backends = SEARCH_ORDER_FILTER
search_fields = ['name', 'slug', 'description']
ordering_fields = ['guide_type', 'slug', 'name']


class GuideDismissal(UpdateAPI):
"""Dismissing a guide for the current user."""

queryset = GuideDefinition.objects.all()
serializer_class = EmptySerializer
permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope]
lookup_field = 'slug'

def get_serializer_context(self):
"""Provide context for the serializer."""
context = super().get_serializer_context()
context['user'] = self.request.user
return context

def perform_update(self, serializer):
"""Override to dismiss the guide for the user."""
obj: GuideDefinition = serializer.instance
user = self.request.user
items = obj.executions.filter(user__pk=user.pk, done=True)
if len(items) == 0:
obj.executions.create(user=user, done=True, completed_at=datetime.now())
logger.info('Guide dismissed', guide=obj.slug, user=user.username)


web_urls = [
path(
'guides/',
include([
path(
'<str:slug>/dismiss/',
GuideDismissal.as_view(),
name='api-guide-dismiss',
),
path('<int:pk>/', GuideDefinitionDetail.as_view(), name='api-guide-detail'),
path('', GuideDefinitionList.as_view(), name='api-guide-list'),
]),
)
]
23 changes: 23 additions & 0 deletions src/backend/InvenTree/web/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""App config for "web" app."""

from django.apps import AppConfig

import structlog

import InvenTree.ready

logger = structlog.get_logger('inventree')


class WebConfig(AppConfig):
"""AppConfig for web app."""

name = 'web'

def ready(self):
"""Initialize restart flag clearance on startup."""
if not InvenTree.ready.canAppAccessDatabase(): # pragma: no cover
return
from web.models import collect_guides # pragma: no cover

collect_guides(create=True) # Preload guide definitions # pragma: no cover
53 changes: 53 additions & 0 deletions src/backend/InvenTree/web/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 5.2.8 on 2025-11-24 11:39

import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
from web.models import GuideDefinition

class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='GuideDefinition',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('uid', models.CharField(default=uuid.uuid4, editable=False, help_text='Unique uuid4 identifier for this guide definition', max_length=255, verbose_name='Endpoint')),
('name', models.CharField(help_text='Name of the guide', max_length=100, unique=True, verbose_name='Name')),
('slug', models.SlugField(help_text='URL-friendly unique identifier for the guide', max_length=100, unique=True, verbose_name='Slug')),
('description', models.TextField(blank=True, help_text='Optional description of the guide', verbose_name='Description')),
('guide_type', models.CharField(choices=GuideDefinition.GuideType.choices, help_text='Type of the guide', max_length=20, verbose_name='Guide Type')),
('guide_data', models.JSONField(blank=True, help_text='JSON data field for storing extra information', null=True, verbose_name='Data')),
],
options={
'verbose_name': 'Guide Definition',
'verbose_name_plural': 'Guide Definitions',
},
),
migrations.CreateModel(
name='GuideExecution',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('uid', models.CharField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this guide execution', max_length=255, verbose_name='UID')),
('started_at', models.DateTimeField(auto_now_add=True, help_text='Timestamp when the guide execution started', verbose_name='Started At')),
('completed_at', models.DateTimeField(blank=True, help_text='Timestamp when the guide execution was completed', null=True, verbose_name='Completed At')),
('progres_data', models.JSONField(blank=True, help_text='JSON field to track progress of the guide execution', null=True, verbose_name='Progress')),
('done', models.BooleanField(default=False, help_text='Indicates whether the guide execution is completed', verbose_name='Done')),
('guide', models.ForeignKey(help_text='The guide definition associated with this execution', on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='web.guidedefinition', verbose_name='Guide')),
('user', models.ForeignKey(help_text='The user who is executing the guide', on_delete=django.db.models.deletion.CASCADE, related_name='guide_executions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Guide Execution',
'verbose_name_plural': 'Guide Executions',
},
),
]
Empty file.
Loading
Loading