convert example & homeassistant specific configs to a generic with all optional fields

This commit is contained in:
Mikhail Epifanov 2024-01-17 22:25:02 +01:00
parent 245787b89e
commit 409c0295ec
No known key found for this signature in database
20 changed files with 159 additions and 316 deletions

View File

@ -16,7 +16,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingre
ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog,
TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace, TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, HomeAssistantConfig, ExampleConfig) ViewLog, ConnectorConfig)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
@ -95,20 +95,12 @@ class StorageAdmin(admin.ModelAdmin):
admin.site.register(Storage, StorageAdmin) admin.site.register(Storage, StorageAdmin)
class HomeAssistantConfigAdmin(admin.ModelAdmin): class ConnectorConfigAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'enabled', 'url') list_display = ('id', 'name', 'type', 'enabled', 'url')
search_fields = ('name', 'url') search_fields = ('name', 'url')
admin.site.register(HomeAssistantConfig, HomeAssistantConfigAdmin) admin.site.register(ConnectorConfig, ConnectorConfigAdmin)
class ExampleConfigAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'enabled', 'feed_url')
search_fields = ('name',)
admin.site.register(ExampleConfig, ExampleConfigAdmin)
class SyncAdmin(admin.ModelAdmin): class SyncAdmin(admin.ModelAdmin):

View File

@ -1,9 +1,13 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from cookbook.models import ShoppingListEntry, Space from cookbook.models import ShoppingListEntry, Space, ConnectorConfig
class Connector(ABC): class Connector(ABC):
@abstractmethod
def __init__(self, config: ConnectorConfig):
pass
@abstractmethod @abstractmethod
async def on_shopping_list_entry_created(self, space: Space, instance: ShoppingListEntry) -> None: async def on_shopping_list_entry_created(self, space: Space, instance: ShoppingListEntry) -> None:
pass pass
@ -20,5 +24,4 @@ class Connector(ABC):
async def close(self) -> None: async def close(self) -> None:
pass pass
# TODO: Maybe add an 'IsEnabled(self) -> Bool' to here
# TODO: Add Recipes & possibly Meal Place listeners/hooks (And maybe more?) # TODO: Add Recipes & possibly Meal Place listeners/hooks (And maybe more?)

View File

@ -12,15 +12,13 @@ from typing import List, Any, Dict, Optional
from django_scopes import scope from django_scopes import scope
from cookbook.connectors.connector import Connector from cookbook.connectors.connector import Connector
from cookbook.connectors.example import Example
from cookbook.connectors.homeassistant import HomeAssistant from cookbook.connectors.homeassistant import HomeAssistant
from cookbook.models import ShoppingListEntry, Recipe, MealPlan, Space, HomeAssistantConfig, ExampleConfig from cookbook.models import ShoppingListEntry, Recipe, MealPlan, Space, ConnectorConfig
multiprocessing.set_start_method('fork') # https://code.djangoproject.com/ticket/31169 multiprocessing.set_start_method('fork') # https://code.djangoproject.com/ticket/31169
QUEUE_MAX_SIZE = 25 QUEUE_MAX_SIZE = 25
REGISTERED_CLASSES: UnionType = ShoppingListEntry | Recipe | MealPlan REGISTERED_CLASSES: UnionType = ShoppingListEntry | Recipe | MealPlan
CONNECTOR_UPDATE_CLASSES: UnionType = HomeAssistantConfig | ExampleConfig
class ActionType(Enum): class ActionType(Enum):
@ -37,7 +35,7 @@ class Work:
class ConnectorManager: class ConnectorManager:
_queue: JoinableQueue _queue: JoinableQueue
_listening_to_classes = REGISTERED_CLASSES | CONNECTOR_UPDATE_CLASSES _listening_to_classes = REGISTERED_CLASSES | ConnectorConfig
def __init__(self): def __init__(self):
self._queue = multiprocessing.JoinableQueue(maxsize=QUEUE_MAX_SIZE) self._queue = multiprocessing.JoinableQueue(maxsize=QUEUE_MAX_SIZE)
@ -88,7 +86,7 @@ class ConnectorManager:
break break
# If a Connector was changed/updated, refresh connector from the database for said space # If a Connector was changed/updated, refresh connector from the database for said space
refresh_connector_cache = isinstance(item.instance, CONNECTOR_UPDATE_CLASSES) refresh_connector_cache = isinstance(item.instance, ConnectorConfig)
space: Space = item.instance.space space: Space = item.instance.space
connectors: Optional[List[Connector]] = _connectors.get(space.name) connectors: Optional[List[Connector]] = _connectors.get(space.name)
@ -98,10 +96,11 @@ class ConnectorManager:
loop.run_until_complete(close_connectors(connectors)) loop.run_until_complete(close_connectors(connectors))
with scope(space=space): with scope(space=space):
connectors: List[Connector] = [ connectors: List[Connector] = list(
*(HomeAssistant(config) for config in space.homeassistantconfig_set.all() if config.enabled), filter(
*(Example(config) for config in space.exampleconfig_set.all() if config.enabled) lambda x: x is not None,
] [ConnectorManager.get_connected_for_config(config) for config in space.connectorconfig_set.all() if config.enabled],
))
_connectors[space.name] = connectors _connectors[space.name] = connectors
if len(connectors) == 0 or refresh_connector_cache: if len(connectors) == 0 or refresh_connector_cache:
@ -113,6 +112,14 @@ class ConnectorManager:
loop.close() loop.close()
@staticmethod
def get_connected_for_config(config: ConnectorConfig) -> Optional[Connector]:
match config.type:
case ConnectorConfig.HOMEASSISTANT:
return HomeAssistant(config)
case _:
return None
async def close_connectors(connectors: List[Connector]): async def close_connectors(connectors: List[Connector]):
tasks: List[Task] = [asyncio.create_task(connector.close()) for connector in connectors] tasks: List[Task] = [asyncio.create_task(connector.close()) for connector in connectors]

View File

@ -1,27 +0,0 @@
from cookbook.connectors.connector import Connector
from cookbook.models import ExampleConfig, Space, ShoppingListEntry
class Example(Connector):
_config: ExampleConfig
def __init__(self, config: ExampleConfig):
self._config = config
async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_created_enabled:
return
pass
async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_updated_enabled:
return
pass
async def on_shopping_list_entry_deleted(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_deleted_enabled:
return
pass
async def close(self) -> None:
pass

View File

@ -4,16 +4,19 @@ from logging import Logger
from homeassistant_api import Client, HomeassistantAPIError, Domain from homeassistant_api import Client, HomeassistantAPIError, Domain
from cookbook.connectors.connector import Connector from cookbook.connectors.connector import Connector
from cookbook.models import ShoppingListEntry, HomeAssistantConfig, Space from cookbook.models import ShoppingListEntry, ConnectorConfig, Space
class HomeAssistant(Connector): class HomeAssistant(Connector):
_domains_cache: dict[str, Domain] _domains_cache: dict[str, Domain]
_config: HomeAssistantConfig _config: ConnectorConfig
_logger: Logger _logger: Logger
_client: Client _client: Client
def __init__(self, config: HomeAssistantConfig): def __init__(self, config: ConnectorConfig):
if not config.token or not config.url or not config.todo_entity:
raise ValueError("config for HomeAssistantConnector in incomplete")
self._domains_cache = dict() self._domains_cache = dict()
self._config = config self._config = config
self._logger = logging.getLogger("connector.HomeAssistant") self._logger = logging.getLogger("connector.HomeAssistant")

View File

@ -10,7 +10,7 @@ from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceFie
from hcaptcha.fields import hCaptchaField from hcaptcha.fields import hCaptchaField
from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry, from .models import (Comment, Food, InviteLink, Keyword, Recipe, RecipeBook, RecipeBookEntry,
SearchPreference, Space, Storage, Sync, User, UserPreference, HomeAssistantConfig, ExampleConfig) SearchPreference, Space, Storage, Sync, User, UserPreference, ConnectorConfig)
class SelectWidget(widgets.Select): class SelectWidget(widgets.Select):
@ -209,11 +209,6 @@ class ConnectorConfigForm(forms.ModelForm):
required=False, required=False,
) )
class Meta:
fields = ('name', 'enabled', 'on_shopping_list_entry_created_enabled', 'on_shopping_list_entry_updated_enabled', 'on_shopping_list_entry_deleted_enabled')
class HomeAssistantConfigForm(ConnectorConfigForm):
update_token = forms.CharField( update_token = forms.CharField(
widget=forms.TextInput(attrs={'autocomplete': 'update-token', 'type': 'password'}), widget=forms.TextInput(attrs={'autocomplete': 'update-token', 'type': 'password'}),
required=False, required=False,
@ -221,45 +216,23 @@ class HomeAssistantConfigForm(ConnectorConfigForm):
) )
url = forms.URLField( url = forms.URLField(
required=True, required=False,
help_text=_('Something like http://homeassistant.local:8123/api'), help_text=_('Something like http://homeassistant.local:8123/api'),
) )
on_shopping_list_entry_created_enabled = forms.BooleanField(
help_text="Enable syncing ShoppingListEntry to Homeassistant Todo List -- Warning: Might have negative performance impact",
required=False,
)
on_shopping_list_entry_updated_enabled = forms.BooleanField(
help_text="PLACEHOLDER",
required=False,
)
on_shopping_list_entry_deleted_enabled = forms.BooleanField(
help_text="Enable syncing ShoppingListEntry deletion to Homeassistant Todo List -- Warning: Might have negative performance impact",
required=False,
)
class Meta: class Meta:
model = HomeAssistantConfig model = ConnectorConfig
fields = ConnectorConfigForm.Meta.fields + ('url', 'todo_entity') fields = (
'name', 'type', 'enabled', 'on_shopping_list_entry_created_enabled', 'on_shopping_list_entry_updated_enabled',
'on_shopping_list_entry_deleted_enabled', 'url', 'todo_entity',
)
help_texts = { help_texts = {
'url': _('http://homeassistant.local:8123/api for example'), 'url': _('http://homeassistant.local:8123/api for example'),
} }
class ExampleConfigForm(ConnectorConfigForm):
feed_url = forms.URLField(
required=False,
)
class Meta:
model = ExampleConfig
fields = ConnectorConfigForm.Meta.fields + ('feed_url',)
# TODO: Deprecate # TODO: Deprecate
class RecipeBookEntryForm(forms.ModelForm): class RecipeBookEntryForm(forms.ModelForm):
prefix = 'bookmark' prefix = 'bookmark'

View File

@ -0,0 +1,36 @@
# Generated by Django 4.2.7 on 2024-01-17 21:12
import cookbook.models
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0207_space_logo_color_128_space_logo_color_144_and_more'),
]
operations = [
migrations.CreateModel(
name='ConnectorConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)])),
('type', models.CharField(choices=[('HomeAssistant', 'HomeAssistant')], default='HomeAssistant', max_length=128)),
('enabled', models.BooleanField(default=True, help_text='Is Connector Enabled')),
('on_shopping_list_entry_created_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_updated_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_deleted_enabled', models.BooleanField(default=False)),
('url', models.URLField(blank=True, null=True)),
('token', models.CharField(blank=True, max_length=512, null=True)),
('todo_entity', models.CharField(blank=True, max_length=128, null=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@ -1,56 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-14 16:00
import cookbook.models
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0207_space_logo_color_128_space_logo_color_144_and_more'),
]
operations = [
migrations.CreateModel(
name='HomeAssistantConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)])),
('enabled', models.BooleanField(default=True, help_text='Is Connector Enabled')),
('on_shopping_list_entry_created_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_updated_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_deleted_enabled', models.BooleanField(default=False)),
('url', models.URLField(blank=True)),
('token', models.CharField(blank=True, max_length=512)),
('todo_entity', models.CharField(default='todo.shopping_list', max_length=128)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
options={
'abstract': False,
},
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
migrations.CreateModel(
name='ExampleConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128, validators=[django.core.validators.MinLengthValidator(1)])),
('enabled', models.BooleanField(default=True, help_text='Is Connector Enabled')),
('on_shopping_list_entry_created_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_updated_enabled', models.BooleanField(default=False)),
('on_shopping_list_entry_deleted_enabled', models.BooleanField(default=False)),
('feed_url', models.URLField(blank=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')),
],
options={
'abstract': False,
},
bases=(models.Model, cookbook.models.PermissionModelMixin),
),
]

View File

@ -339,8 +339,7 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
SyncLog.objects.filter(sync__space=self).delete() SyncLog.objects.filter(sync__space=self).delete()
Sync.objects.filter(space=self).delete() Sync.objects.filter(space=self).delete()
Storage.objects.filter(space=self).delete() Storage.objects.filter(space=self).delete()
HomeAssistantConfig.objects.filter(space=self).delete() ConnectorConfig.objects.filter(space=self).delete()
ExampleConfig.objects.filter(space=self).delete()
ShoppingListEntry.objects.filter(shoppinglist__space=self).delete() ShoppingListEntry.objects.filter(shoppinglist__space=self).delete()
ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete() ShoppingListRecipe.objects.filter(shoppinglist__space=self).delete()
@ -366,30 +365,28 @@ class Space(ExportModelOperationsMixin('space'), models.Model):
class ConnectorConfig(models.Model, PermissionModelMixin): class ConnectorConfig(models.Model, PermissionModelMixin):
HOMEASSISTANT = 'HomeAssistant'
CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),)
name = models.CharField(max_length=128, validators=[MinLengthValidator(1)]) name = models.CharField(max_length=128, validators=[MinLengthValidator(1)])
type = models.CharField(
choices=CONNECTER_TYPE, max_length=128, default=HOMEASSISTANT
)
enabled = models.BooleanField(default=True, help_text="Is Connector Enabled") enabled = models.BooleanField(default=True, help_text="Is Connector Enabled")
on_shopping_list_entry_created_enabled = models.BooleanField(default=False) on_shopping_list_entry_created_enabled = models.BooleanField(default=False)
on_shopping_list_entry_updated_enabled = models.BooleanField(default=False) on_shopping_list_entry_updated_enabled = models.BooleanField(default=False)
on_shopping_list_entry_deleted_enabled = models.BooleanField(default=False) on_shopping_list_entry_deleted_enabled = models.BooleanField(default=False)
url = models.URLField(blank=True, null=True)
token = models.CharField(max_length=512, blank=True, null=True)
todo_entity = models.CharField(max_length=128, blank=True, null=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT) created_by = models.ForeignKey(User, on_delete=models.PROTECT)
space = models.ForeignKey(Space, on_delete=models.CASCADE) space = models.ForeignKey(Space, on_delete=models.CASCADE)
objects = ScopedManager(space='space') objects = ScopedManager(space='space')
class Meta:
abstract = True
class HomeAssistantConfig(ConnectorConfig):
url = models.URLField(blank=True)
token = models.CharField(max_length=512, blank=True)
todo_entity = models.CharField(max_length=128, default='todo.shopping_list')
class ExampleConfig(ConnectorConfig):
feed_url = models.URLField(blank=True)
class UserPreference(models.Model, PermissionModelMixin): class UserPreference(models.Model, PermissionModelMixin):

View File

@ -34,7 +34,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space,
Step, Storage, Supermarket, SupermarketCategory, Step, Storage, Supermarket, SupermarketCategory,
SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserPreference, UserSpace, ViewLog, HomeAssistantConfig) UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig)
from cookbook.templatetags.custom_tags import markdown from cookbook.templatetags.custom_tags import markdown
from recipes.settings import AWS_ENABLED, MEDIA_URL from recipes.settings import AWS_ENABLED, MEDIA_URL
@ -413,14 +413,14 @@ class StorageSerializer(SpacedModelSerializer):
} }
class HomeAssistantConfigSerializer(SpacedModelSerializer): class ConnectorConfigConfigSerializer(SpacedModelSerializer):
def create(self, validated_data): def create(self, validated_data):
validated_data['created_by'] = self.context['request'].user validated_data['created_by'] = self.context['request'].user
return super().create(validated_data) return super().create(validated_data)
class Meta: class Meta:
model = HomeAssistantConfig model = ConnectorConfig
fields = ( fields = (
'id', 'name', 'url', 'token', 'todo_entity', 'enabled', 'id', 'name', 'url', 'token', 'todo_entity', 'enabled',
'on_shopping_list_entry_created_enabled', 'on_shopping_list_entry_updated_enabled', 'on_shopping_list_entry_created_enabled', 'on_shopping_list_entry_updated_enabled',

View File

@ -1,14 +1,9 @@
from typing import Any, Dict
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from django_tables2 import MultiTableMixin
from django_tables2.utils import A from django_tables2.utils import A
from .helper.permission_helper import GroupRequiredMixin from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog, ConnectorConfig
from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog, HomeAssistantConfig, ExampleConfig
class StorageTable(tables.Table): class StorageTable(tables.Table):
@ -20,47 +15,13 @@ class StorageTable(tables.Table):
fields = ('id', 'name', 'method') fields = ('id', 'name', 'method')
class ExampleConfigTable(tables.Table): class ConnectorConfigTable(tables.Table):
id = tables.LinkColumn('edit_example_config', args=[A('id')]) id = tables.LinkColumn('edit_connector_config', args=[A('id')])
class Meta: class Meta:
model = ExampleConfig model = ConnectorConfig
template_name = 'generic/table_template.html' template_name = 'generic/table_template.html'
fields = ('id', 'name', 'enabled', 'feed_url') fields = ('id', 'name', 'type', 'enabled')
attrs = {'table_name': "Example Configs", 'create_url': 'new_example_config'}
class HomeAssistantConfigTable(tables.Table):
id = tables.LinkColumn('edit_home_assistant_config', args=[A('id')])
class Meta:
model = HomeAssistantConfig
template_name = 'generic/table_template.html'
fields = ('id', 'name', 'enabled', 'url')
attrs = {'table_name': "HomeAssistant Configs", 'create_url': 'new_home_assistant_config'}
class ConnectorConfigTable(GroupRequiredMixin, MultiTableMixin, TemplateView):
groups_required = ['admin']
template_name = "list_connectors.html"
table_pagination = {
"per_page": 25
}
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
kwargs['title'] = _("Connectors")
return kwargs
def get_tables(self):
example_configs = ExampleConfig.objects.filter(space=self.request.space).all()
home_assistant_configs = HomeAssistantConfig.objects.filter(space=self.request.space).all()
return [
ExampleConfigTable(example_configs),
HomeAssistantConfigTable(home_assistant_configs)
]
class ImportLogTable(tables.Table): class ImportLogTable(tables.Table):

View File

@ -336,7 +336,7 @@
class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a> class="fas fa-server fa-fw"></i> {% trans 'Space Settings' %}</a>
{% endif %} {% endif %}
{% if request.user == request.space.created_by or user.is_superuser %} {% if request.user == request.space.created_by or user.is_superuser %}
<a class="dropdown-item" href="{% url 'list_connectors' %}"><i <a class="dropdown-item" href="{% url 'list_connector_config' %}"><i
class="fas fa-sync-alt fa-fw"></i> {% trans 'External Connectors' %}</a> class="fas fa-sync-alt fa-fw"></i> {% trans 'External Connectors' %}</a>
{% endif %} {% endif %}
{% if user.is_superuser %} {% if user.is_superuser %}

View File

@ -5,21 +5,21 @@ from django.contrib import auth
from django.urls import reverse from django.urls import reverse
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
from cookbook.models import HomeAssistantConfig from cookbook.models import ConnectorConfig
LIST_URL = 'api:homeassistantconfig-list' LIST_URL = 'api:connectorconfig-list'
DETAIL_URL = 'api:homeassistantconfig-detail' DETAIL_URL = 'api:connectorconfig-detail'
@pytest.fixture() @pytest.fixture()
def obj_1(space_1, u1_s1): def obj_1(space_1, u1_s1):
return HomeAssistantConfig.objects.create( return ConnectorConfig.objects.create(
name='HomeAssistant 1', token='token', url='url', todo_entity='todo.shopping_list', enabled=True, created_by=auth.get_user(u1_s1), space=space_1, ) name='HomeAssistant 1', token='token', url='url', todo_entity='todo.shopping_list', enabled=True, created_by=auth.get_user(u1_s1), space=space_1, )
@pytest.fixture @pytest.fixture
def obj_2(space_1, u1_s1): def obj_2(space_1, u1_s1):
return HomeAssistantConfig.objects.create( return ConnectorConfig.objects.create(
name='HomeAssistant 2', token='token', url='url', todo_entity='todo.shopping_list', enabled=True, created_by=auth.get_user(u1_s1), space=space_1, ) name='HomeAssistant 2', token='token', url='url', todo_entity='todo.shopping_list', enabled=True, created_by=auth.get_user(u1_s1), space=space_1, )
@ -123,4 +123,4 @@ def test_delete(a1_s1, a1_s2, obj_1):
assert r.status_code == 204 assert r.status_code == 204
with scopes_disabled(): with scopes_disabled():
assert HomeAssistantConfig.objects.count() == 0 assert ConnectorConfig.objects.count() == 0

View File

@ -3,16 +3,18 @@ from django.contrib import auth
from django.contrib import messages from django.contrib import messages
from django.contrib.messages import get_messages from django.contrib.messages import get_messages
from django.urls import reverse from django.urls import reverse
from pytest_django.asserts import assertTemplateUsed
from cookbook.models import HomeAssistantConfig from cookbook.models import ConnectorConfig
EDIT_VIEW_NAME = 'edit_home_assistant_config' EDIT_VIEW_NAME = 'edit_connector_config'
@pytest.fixture @pytest.fixture
def home_assistant_config_obj(a1_s1, space_1): def home_assistant_config_obj(a1_s1, space_1):
return HomeAssistantConfig.objects.create( return ConnectorConfig.objects.create(
name='HomeAssistant 1', name='HomeAssistant 1',
type=ConnectorConfig.HOMEASSISTANT,
token='token', token='token',
url='http://localhost:8123/api', url='http://localhost:8123/api',
todo_entity='todo.shopping_list', todo_entity='todo.shopping_list',
@ -22,20 +24,22 @@ def home_assistant_config_obj(a1_s1, space_1):
) )
def test_edit_home_assistant_config(home_assistant_config_obj: HomeAssistantConfig, a1_s1, a1_s2): def test_edit_connector_config_homeassistant(home_assistant_config_obj: ConnectorConfig, a1_s1, a1_s2):
new_token = '1234_token' new_token = '1234_token'
r = a1_s1.post( r = a1_s1.post(
reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}), reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}),
{ {
'name': home_assistant_config_obj.name, 'name': home_assistant_config_obj.name,
'type': home_assistant_config_obj.type,
'url': home_assistant_config_obj.url, 'url': home_assistant_config_obj.url,
'token': new_token, 'update_token': new_token,
'todo_entity': home_assistant_config_obj.todo_entity, 'todo_entity': home_assistant_config_obj.todo_entity,
'enabled': home_assistant_config_obj.enabled, 'enabled': home_assistant_config_obj.enabled,
} }
) )
assert r.status_code == 200 assert r.status_code == 302
r_messages = [m for m in get_messages(r.wsgi_request)] r_messages = [m for m in get_messages(r.wsgi_request)]
assert not any(m.level > messages.SUCCESS for m in r_messages) assert not any(m.level > messages.SUCCESS for m in r_messages)
@ -46,9 +50,10 @@ def test_edit_home_assistant_config(home_assistant_config_obj: HomeAssistantConf
reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}), reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}),
{ {
'name': home_assistant_config_obj.name, 'name': home_assistant_config_obj.name,
'type': home_assistant_config_obj.type,
'url': home_assistant_config_obj.url, 'url': home_assistant_config_obj.url,
'todo_entity': home_assistant_config_obj.todo_entity, 'todo_entity': home_assistant_config_obj.todo_entity,
'token': new_token, 'update_token': new_token,
'enabled': home_assistant_config_obj.enabled, 'enabled': home_assistant_config_obj.enabled,
} }
) )

View File

@ -12,8 +12,7 @@ from recipes.settings import DEBUG, PLUGINS
from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, PropertyType, from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, PropertyType,
Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Space, Step, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Space, Step,
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UnitConversion, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UnitConversion,
UserFile, UserSpace, get_model_name, HomeAssistantConfig, ExampleConfig) UserFile, UserSpace, get_model_name, ConnectorConfig)
from .tables import ConnectorConfigTable
from .views import api, data, delete, edit, import_export, lists, new, telegram, views from .views import api, data, delete, edit, import_export, lists, new, telegram, views
from .views.api import CustomAuthToken, ImportOpenData from .views.api import CustomAuthToken, ImportOpenData
@ -52,7 +51,7 @@ router.register(r'shopping-list-recipe', api.ShoppingListRecipeViewSet)
router.register(r'space', api.SpaceViewSet) router.register(r'space', api.SpaceViewSet)
router.register(r'step', api.StepViewSet) router.register(r'step', api.StepViewSet)
router.register(r'storage', api.StorageViewSet) router.register(r'storage', api.StorageViewSet)
router.register(r'home-assistant-config', api.HomeAssistantConfigViewSet) router.register(r'home-assistant-config', api.ConnectorConfigConfigViewSet)
router.register(r'supermarket', api.SupermarketViewSet) router.register(r'supermarket', api.SupermarketViewSet)
router.register(r'supermarket-category', api.SupermarketCategoryViewSet) router.register(r'supermarket-category', api.SupermarketCategoryViewSet)
router.register(r'supermarket-category-relation', api.SupermarketCategoryRelationViewSet) router.register(r'supermarket-category-relation', api.SupermarketCategoryRelationViewSet)
@ -116,7 +115,6 @@ urlpatterns = [
path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'), path('edit/recipe/convert/<int:pk>/', edit.convert_recipe, name='edit_convert_recipe'),
path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'), path('edit/storage/<int:pk>/', edit.edit_storage, name='edit_storage'),
path('list/connectors', ConnectorConfigTable.as_view(), name='list_connectors'),
path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'), path('delete/recipe-source/<int:pk>/', delete.delete_recipe_source, name='delete_recipe_source'),
@ -169,7 +167,7 @@ urlpatterns = [
] ]
generic_models = ( generic_models = (
Recipe, RecipeImport, Storage, HomeAssistantConfig, ExampleConfig, RecipeBook, SyncLog, Sync, Recipe, RecipeImport, Storage, ConnectorConfig, RecipeBook, SyncLog, Sync,
Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space
) )

View File

@ -76,7 +76,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte
ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage,
Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync,
SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace, SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace,
ViewLog, HomeAssistantConfig) ViewLog, ConnectorConfig)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -102,7 +102,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer,
SupermarketCategorySerializer, SupermarketSerializer, SupermarketCategorySerializer, SupermarketSerializer,
SyncLogSerializer, SyncSerializer, UnitConversionSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer,
UnitSerializer, UserFileSerializer, UserPreferenceSerializer, UnitSerializer, UserFileSerializer, UserPreferenceSerializer,
UserSerializer, UserSpaceSerializer, ViewLogSerializer, HomeAssistantConfigSerializer) UserSerializer, UserSpaceSerializer, ViewLogSerializer, ConnectorConfigConfigSerializer)
from cookbook.views.import_export import get_integration from cookbook.views.import_export import get_integration
from recipes import settings from recipes import settings
from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT from recipes.settings import FDC_API_KEY, DRF_THROTTLE_RECIPE_URL_IMPORT
@ -464,10 +464,9 @@ class StorageViewSet(viewsets.ModelViewSet):
return self.queryset.filter(space=self.request.space) return self.queryset.filter(space=self.request.space)
class HomeAssistantConfigViewSet(viewsets.ModelViewSet): class ConnectorConfigConfigViewSet(viewsets.ModelViewSet):
# TODO handle delete protect error and adjust test queryset = ConnectorConfig.objects
queryset = HomeAssistantConfig.objects serializer_class = ConnectorConfigConfigSerializer
serializer_class = HomeAssistantConfigSerializer
permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope]
def get_queryset(self): def get_queryset(self):

View File

@ -9,7 +9,7 @@ from django.views.generic import DeleteView
from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required
from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry, from cookbook.models import (Comment, InviteLink, MealPlan, Recipe, RecipeBook, RecipeBookEntry,
RecipeImport, Space, Storage, Sync, UserSpace, HomeAssistantConfig, ExampleConfig) RecipeImport, Space, Storage, Sync, UserSpace, ConnectorConfig)
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -122,27 +122,15 @@ class StorageDelete(GroupRequiredMixin, DeleteView):
return HttpResponseRedirect(reverse('list_storage')) return HttpResponseRedirect(reverse('list_storage'))
class HomeAssistantConfigDelete(GroupRequiredMixin, DeleteView): class ConnectorConfigDelete(GroupRequiredMixin, DeleteView):
groups_required = ['admin'] groups_required = ['admin']
template_name = "generic/delete_template.html" template_name = "generic/delete_template.html"
model = HomeAssistantConfig model = ConnectorConfig
success_url = reverse_lazy('list_connectors') success_url = reverse_lazy('list_connector_config')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _("HomeAssistant Config Backend") context['title'] = _("Connectors Config Backend")
return context
class ExampleConfigDelete(GroupRequiredMixin, DeleteView):
groups_required = ['admin']
template_name = "generic/delete_template.html"
model = ExampleConfig
success_url = reverse_lazy('list_connectors')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Example Config Backend")
return context return context

View File

@ -9,10 +9,10 @@ from django.utils.translation import gettext as _
from django.views.generic import UpdateView from django.views.generic import UpdateView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from cookbook.forms import CommentForm, ExternalRecipeForm, StorageForm, SyncForm, HomeAssistantConfigForm, ExampleConfigForm from cookbook.forms import CommentForm, ExternalRecipeForm, StorageForm, SyncForm, ConnectorConfigForm
from cookbook.helper.permission_helper import (GroupRequiredMixin, OwnerRequiredMixin, from cookbook.helper.permission_helper import (GroupRequiredMixin, OwnerRequiredMixin,
above_space_limit, group_required) above_space_limit, group_required)
from cookbook.models import Comment, Recipe, RecipeImport, Storage, Sync, HomeAssistantConfig, ExampleConfig from cookbook.models import Comment, Recipe, RecipeImport, Storage, Sync, ConnectorConfig
from cookbook.provider.dropbox import Dropbox from cookbook.provider.dropbox import Dropbox
from cookbook.provider.local import Local from cookbook.provider.local import Local
from cookbook.provider.nextcloud import Nextcloud from cookbook.provider.nextcloud import Nextcloud
@ -128,11 +128,11 @@ def edit_storage(request, pk):
) )
class HomeAssistantConfigUpdate(GroupRequiredMixin, UpdateView): class ConnectorConfigUpdate(GroupRequiredMixin, UpdateView):
groups_required = ['admin'] groups_required = ['admin']
template_name = "generic/edit_template.html" template_name = "generic/edit_template.html"
model = HomeAssistantConfig model = ConnectorConfig
form_class = HomeAssistantConfigForm form_class = ConnectorConfigForm
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@ -143,33 +143,14 @@ class HomeAssistantConfigUpdate(GroupRequiredMixin, UpdateView):
if form.cleaned_data['update_token'] != VALUE_NOT_CHANGED and form.cleaned_data['update_token'] != "": if form.cleaned_data['update_token'] != VALUE_NOT_CHANGED and form.cleaned_data['update_token'] != "":
form.instance.token = form.cleaned_data['update_token'] form.instance.token = form.cleaned_data['update_token']
messages.add_message(self.request, messages.SUCCESS, _('Config saved!')) messages.add_message(self.request, messages.SUCCESS, _('Config saved!'))
return super(HomeAssistantConfigUpdate, self).form_valid(form) return super(ConnectorConfigUpdate, self).form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse('edit_home_assistant_config', kwargs={'pk': self.object.pk}) return reverse('edit_connector_config', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _("HomeAssistantConfig") context['title'] = _("ConnectorConfig")
return context
class ExampleConfigUpdate(GroupRequiredMixin, UpdateView):
groups_required = ['admin']
template_name = "generic/edit_template.html"
model = ExampleConfig
form_class = ExampleConfigForm
def form_valid(self, form):
messages.add_message(self.request, messages.SUCCESS, _('Config saved!'))
return super().form_valid(form)
def get_success_url(self):
return reverse('edit_example_config', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("ExampleConfig")
return context return context

View File

@ -6,8 +6,8 @@ from django.utils.translation import gettext as _
from django_tables2 import RequestConfig from django_tables2 import RequestConfig
from cookbook.helper.permission_helper import group_required from cookbook.helper.permission_helper import group_required
from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile, ConnectorConfig
from cookbook.tables import ImportLogTable, InviteLinkTable, RecipeImportTable, StorageTable from cookbook.tables import ImportLogTable, InviteLinkTable, RecipeImportTable, StorageTable, ConnectorConfigTable
@group_required('admin') @group_required('admin')
@ -65,6 +65,22 @@ def storage(request):
) )
@group_required('admin')
def connector_config(request):
table = ConnectorConfigTable(ConnectorConfig.objects.filter(space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(
request,
'generic/list_template.html',
{
'title': _("Connector Config Backend"),
'table': table,
'create_url': 'new_connector_config'
}
)
@group_required('admin') @group_required('admin')
def invite_link(request): def invite_link(request):
table = InviteLinkTable( table = InviteLinkTable(

View File

@ -5,9 +5,9 @@ from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import CreateView from django.views.generic import CreateView
from cookbook.forms import ImportRecipeForm, Storage, StorageForm, HomeAssistantConfigForm, ExampleConfigForm from cookbook.forms import ImportRecipeForm, Storage, StorageForm, ConnectorConfigForm
from cookbook.helper.permission_helper import GroupRequiredMixin, above_space_limit, group_required from cookbook.helper.permission_helper import GroupRequiredMixin, above_space_limit, group_required
from cookbook.models import Recipe, RecipeImport, ShareLink, Step, HomeAssistantConfig, ExampleConfig from cookbook.models import Recipe, RecipeImport, ShareLink, Step, ConnectorConfig
from recipes import settings from recipes import settings
@ -70,21 +70,12 @@ class StorageCreate(GroupRequiredMixin, CreateView):
return context return context
class HomeAssistantConfigCreate(GroupRequiredMixin, CreateView): class ConnectorConfigCreate(GroupRequiredMixin, CreateView):
groups_required = ['admin'] groups_required = ['admin']
template_name = "generic/new_template.html" template_name = "generic/new_template.html"
model = HomeAssistantConfig model = ConnectorConfig
form_class = HomeAssistantConfigForm form_class = ConnectorConfigForm
success_url = reverse_lazy('list_home_assistant_config') success_url = reverse_lazy('list_connector_config')
def get_form_class(self):
form_class = super().get_form_class()
if self.request.method == 'GET':
update_token_field = form_class.base_fields['update_token']
update_token_field.required = True
return form_class
def form_valid(self, form): def form_valid(self, form):
if self.request.space.demo or settings.HOSTED: if self.request.space.demo or settings.HOSTED:
@ -96,35 +87,11 @@ class HomeAssistantConfigCreate(GroupRequiredMixin, CreateView):
obj.created_by = self.request.user obj.created_by = self.request.user
obj.space = self.request.space obj.space = self.request.space
obj.save() obj.save()
return HttpResponseRedirect(reverse('edit_home_assistant_config', kwargs={'pk': obj.pk})) return HttpResponseRedirect(reverse('edit_connector_config', kwargs={'pk': obj.pk}))
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _("HomeAssistant Config Backend") context['title'] = _("Connector Config Backend")
return context
class ExampleConfigCreate(GroupRequiredMixin, CreateView):
groups_required = ['admin']
template_name = "generic/new_template.html"
model = ExampleConfig
form_class = ExampleConfigForm
success_url = reverse_lazy('list_connectors')
def form_valid(self, form):
if self.request.space.demo or settings.HOSTED:
messages.add_message(self.request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
return redirect('index')
obj = form.save(commit=False)
obj.created_by = self.request.user
obj.space = self.request.space
obj.save()
return HttpResponseRedirect(reverse('edit_example_config', kwargs={'pk': obj.pk}))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Example Config Backend")
return context return context