Skip to content

Conversation

@MyPyDavid
Copy link
Member

@MyPyDavid MyPyDavid commented Sep 30, 2025

Description

  • adds a rdmo.config app
    • with a Plugin model
    • the Plugin behaves similar to any other element (Catalog, Taks, View)
  • added a new setting PLUGINS = [ .. ] which replaces the legacy settings (e.g. PROJECT_IMPORT).

Deprecations and legacy

  • removed rdmo.core.plugins
    • moved Plugin base class to rdmo.config.plugins and renamed to PluginBase

Related issue: #1413

@MyPyDavid MyPyDavid self-assigned this Sep 30, 2025
Copy link
Member

@jochenklar jochenklar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Lets discuss in the meeting.

help_text=_('Designates whether this plugin is generally available for projects.')
)

class PluginType(models.TextChoices):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do without this, the Plugin(-class) should know this.

Copy link
Member Author

@MyPyDavid MyPyDavid Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now something like this as a utils.py function, to detect the plugin type from the class inheritance.

PLUGIN_BASES = {
    'optionset_provider': OptionsetProvider,
    'export': Export,
    'import': Import,
    'issue_provider': IssueProvider,
}

def detect_plugin_type(plugin_class):
    if not issubclass(plugin_class, Plugin):
        return "not_an_rdmo_plugin"

    for type_name, base_cls in PLUGIN_BASES.items():
        if issubclass(plugin_class, base_cls):
            return type_name
    return 'rdmo_plugin_unknown_type'

I kept it as a property in the Plugin model:

    @property
    def plugin_type(self) -> str:
        try:
            plugin_class = self.get_class()
        except Exception as e:
            return e.__class__.__qualname__.lower()
        return detect_plugin_type(plugin_class)

verbose_name=_("Python path"),
)
plugin_type = models.CharField(max_length=32, choices=PluginType.choices)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what we need to add is plugin_settings which is used to set the settings for this plugin.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin_settings field is there now:

    plugin_settings = models.JSONField(
        blank=True, default=dict,
        verbose_name=_("Plugin settings"),
        help_text=_("Contains the settings for this plugin in JSON format."),
    )

@MyPyDavid MyPyDavid changed the title Draft: add config app for Plugin model Draft: add rdmo.config app for Plugin model Oct 9, 2025
@MyPyDavid
Copy link
Member Author

MyPyDavid commented Oct 15, 2025

I'm getting some strange DB errors for mysql in ci(and sqlite in local testing).
In my local pytest with sqlite:

django.db.utils.OperationalError: foreign key mismatch - "questions_question" referencing "domain_attribute"

From the ci logs:

# at
query = b'ALTER TABLE `domain_attribute` DROP COLUMN `attributeentity_ptr_id`'
 django.db.utils.OperationalError:
  (1829, "Cannot drop column 'attributeentity_ptr_id': 
 needed in a foreign key constraint 'questions_questionse_attribute_id_30ee3ea9_fk_domain_at' of table 'questions_questionset'")

The problem can be resolved by removing the app 'rdmo.config' from the installed apps again..
Trying to see it is a bug somewhere in the migrations (0037_remove_attribute.py) but can't really find it so far..

@MyPyDavid
Copy link
Member Author

I'm getting some strange DB errors for mysql in ci(and sqlite in local testing). In my local pytest with sqlite:

django.db.utils.OperationalError: foreign key mismatch - "questions_question" referencing "domain_attribute"

Think that I found the bug with help of GPT and could fix it by adding a single dependency to the ('domain', '0038_rename_attributeentity_to_attribute') in the questions.migrations.0032_meta.py file.

GPT:

You’ve got a classic multi-table-inheritance gotcha: the questions app points a FK at the child table’s primary key just before the domain app drops that child PK. SQLite complains with a vague “foreign key mismatch,” while MySQL is blunt: you can’t drop attributeentity_ptr_id on domain_attribute because it’s referenced by questions_questionset / questions_question.

Why this happens (in your tree)

  • In domain 0037, the child model Attribute (multi-table inheritance) is dismantled: it first adds a dummy column, then drops the attributeentity_ptr field (which is the PK of the child table!), and finally deletes the model.
  • Immediately after, domain 0038 renames the base class AttributeEntity → Attribute, so from here on “Attribute” refers to the base table (with a normal id PK), not the child.
  • In the questions app, migration 0031 renames the field attribute_entity → attribute, and 0032 makes that FK point to to='domain.Attribute'. At the time 0032 runs (without extra dependencies), “domain.Attribute” still means the child table (pre-rename), so the FK targets the child table’s PK (attributeentity_ptr_id).
  • Then domain 0037 tries to drop attributeentity_ptr on domain_attribute, but the FK from questions still points to that column → MySQL error 1829; SQLite “FK mismatch”.
    You also noticed removing rdmo.config “fixes” it. That’s consistent: the extra app changes the topological order so domain 0037 sneaks in before questions flips its FKs to the renamed/base model, exposing the ordering bug.

@MyPyDavid MyPyDavid changed the title Draft: add rdmo.config app for Plugin model Draft: add rdmo.config app for Plugin model Oct 21, 2025
@jochenklar
Copy link
Member

Ok got it! Good explanation by ChatGPT. This is why you don't do multi-table inheritance and why past-Jochen dismantled it.

@MyPyDavid MyPyDavid linked an issue Oct 27, 2025 that may be closed by this pull request
@jochenklar jochenklar modified the milestones: RDMO 2.4.0, RDMO 2.5.0 Oct 30, 2025
@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from 107f379 to f5eae22 Compare November 7, 2025 13:08
@MyPyDavid
Copy link
Member Author

MyPyDavid commented Nov 14, 2025

  • need to rebase to 2.4.0 now
    PS it ran without any conflicts

@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from 7031a4b to 6875f9a Compare November 14, 2025 14:52
@MyPyDavid MyPyDavid changed the base branch from 2.3.3 to 2.4.0 November 14, 2025 14:52
@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from 6875f9a to 9791229 Compare November 19, 2025 08:16
@MyPyDavid MyPyDavid changed the base branch from 2.4.0 to 2.5.0 November 19, 2025 08:17
@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from dceca19 to ae887db Compare November 24, 2025 14:45
return self.filter(Q(groups=None) | Q(groups__in=groups))


class ForSiteQuerySetMixin:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could implement this already in #1488, see #1488 (comment)

@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from 27516be to 4026f9a Compare December 5, 2025 16:12
@MyPyDavid MyPyDavid changed the title Draft: add rdmo.config app for Plugin model Draft: add rdmo.config app for Plugin model (plugin managament) Dec 9, 2025
@MyPyDavid MyPyDavid marked this pull request as ready for review December 9, 2025 12:23
@MyPyDavid MyPyDavid requested a review from jochenklar December 9, 2025 12:23
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
@MyPyDavid MyPyDavid force-pushed the add-config-app-for-plugin-model branch from e9b3b51 to 0ffb48a Compare December 22, 2025 10:02
@MyPyDavid MyPyDavid changed the base branch from 2.5.0 to main December 22, 2025 10:02
@MyPyDavid
Copy link
Member Author

rebased to main , in expectation of new 2.5.0 Release PR

@coveralls
Copy link

Coverage Status

coverage: 94.693% (-0.1%) from 94.814%
when pulling 0ffb48a on add-config-app-for-plugin-model
into cda25ed on main.

Copy link
Member

@jochenklar jochenklar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the work! I like the implementation, but I have some requests, in particular with the management scripts.

Additionally, the integration into the old front-end seems to be missing.

I have not tested yet with real plugins, I will do so in the next iteration.

plugin_type=PluginType.PROJECT_ISSUE_PROVIDER.value,
format=provider_key
)
provider = plugins.first().initialize_class() if plugins else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be try: ... except DoesNotExist:


dependencies = [
('questions', '0031_rename_attribute_entity_to_attribute'),
('domain', '0038_rename_attributeentity_to_attribute'), # post hoc fix for mysql/sqlite
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we fix this before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, before in this PR #1436 (comment)

queryset = settings.OPTIONSET_PROVIDERS

def get_queryset(self):
return getattr(settings, 'OPTIONSET_PROVIDERS', [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings in RDMO should have a default so that we don't need those checks. But I guess OPTIONSET_PROVIDERS is now a queryset anyway so this might be an oversight.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's an oversight, it needs to query the Plugin objects.

verbose_name=_('Conditions'),
help_text=_('The list of conditions evaluated for this option set.')
)
plugins = models.ManyToManyField( # can not import Plugin due to circular import
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, we now have multiple plugins for an optionset, lets see what crasy things people will do with it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from django.utils.dateparse import parse_date
from django.utils.encoding import force_str
from django.utils.formats import get_format
from django.utils.module_loading import import_string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this exists since Django 1.7? 🤯

<Tabs id="#plugin-tabs" defaultActiveKey={0} animation={false}>
{
config.settings && config.settings.languages.map(([lang_code, lang], index) => (
<Tab className="pt-10" key={index} eventKey={index} title={lang}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

help is missing here.

const [value, setValue] = useState(formatValue(element[field]))
const [parseError, setParseError] = useState(false)

useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is buggy, if I type something in the field it (almost) instantly runs the validation, which is different from the other fields. I would prefer that the indenting happens only after pressing save.

verbose_name_plural = _('Plugins')

def __str__(self):
return f"({self.python_path}, {self.plugin_type}) {self.uri}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be just the URI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add that f-string to a __repr__ right?!

models.QuerySet):

def filter_for_project(self, project):
return (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to check for availability. Right now, I can export using an unavailable Plugin.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to look into this, I though I'm using only the for_context query (it has a check for availability) for the Plugins and this filter_for_project is used inside of it.

@jochenklar
Copy link
Member

The test plugins can be used in the rdmo-app by adding:

import importlib.util
import sys
from pathlib import Path

rdmo_path = Path(importlib.util.find_spec('rdmo').origin).parent.parent
sys.path.append(str(rdmo_path / 'testing'))

INSTALLED_APPS += ['plugins']

to the settings.

Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
@MyPyDavid MyPyDavid changed the base branch from main to 2.5.0/release December 22, 2025 16:41
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
format=self.provider_key)
)
if plugins.exists():
return plugins.first().initialize_class()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this will "arbitrarily" only return the first plugin instance as provider. At some other places in the code I also simply return plugins.first().initialize_class(), this was a temporary hack to make a working version.
How should it be handled when there are multiple plugin instances available as providers? Should a new property like providers be added?

Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Signed-off-by: David Wallace <david.wallace@tu-darmstadt.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

config app: Allow managers to configure Plugin settings via GUI

4 participants