From 409c0295ec7e2b2c51812773f7e1c0f5a3ca9bbf Mon Sep 17 00:00:00 2001 From: Mikhail Epifanov Date: Wed, 17 Jan 2024 22:25:02 +0100 Subject: [PATCH] convert example & homeassistant specific configs to a generic with all optional fields --- cookbook/admin.py | 16 ++---- cookbook/connectors/connector.py | 7 ++- cookbook/connectors/connector_manager.py | 25 ++++++--- cookbook/connectors/example.py | 27 --------- cookbook/connectors/homeassistant.py | 9 ++- cookbook/forms.py | 41 +++----------- cookbook/migrations/0208_connectorconfig.py | 36 ++++++++++++ .../0208_homeassistantconfig_exampleconfig.py | 56 ------------------- cookbook/models.py | 25 ++++----- cookbook/serializer.py | 6 +- cookbook/tables.py | 49 ++-------------- cookbook/templates/base.html | 2 +- .../api/test_api_home_assistant_config.py | 12 ++-- ...nfig.py => test_edits_connector_config.py} | 19 ++++--- cookbook/urls.py | 8 +-- cookbook/views/api.py | 11 ++-- cookbook/views/delete.py | 22 ++------ cookbook/views/edit.py | 35 +++--------- cookbook/views/lists.py | 20 ++++++- cookbook/views/new.py | 49 +++------------- 20 files changed, 159 insertions(+), 316 deletions(-) delete mode 100644 cookbook/connectors/example.py create mode 100644 cookbook/migrations/0208_connectorconfig.py delete mode 100644 cookbook/migrations/0208_homeassistantconfig_exampleconfig.py rename cookbook/tests/edits/{test_edits_home_assistant_config.py => test_edits_connector_config.py} (77%) diff --git a/cookbook/admin.py b/cookbook/admin.py index 64a03173..fcc007bc 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -16,7 +16,7 @@ from .models import (BookmarkletImport, Comment, CookLog, Food, ImportLog, Ingre ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, TelegramBot, Unit, UnitConversion, UserFile, UserPreference, UserSpace, - ViewLog, HomeAssistantConfig, ExampleConfig) + ViewLog, ConnectorConfig) class CustomUserAdmin(UserAdmin): @@ -95,20 +95,12 @@ class StorageAdmin(admin.ModelAdmin): admin.site.register(Storage, StorageAdmin) -class HomeAssistantConfigAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'enabled', 'url') +class ConnectorConfigAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'type', 'enabled', 'url') search_fields = ('name', 'url') -admin.site.register(HomeAssistantConfig, HomeAssistantConfigAdmin) - - -class ExampleConfigAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'enabled', 'feed_url') - search_fields = ('name',) - - -admin.site.register(ExampleConfig, ExampleConfigAdmin) +admin.site.register(ConnectorConfig, ConnectorConfigAdmin) class SyncAdmin(admin.ModelAdmin): diff --git a/cookbook/connectors/connector.py b/cookbook/connectors/connector.py index 4ed61350..3647dc71 100644 --- a/cookbook/connectors/connector.py +++ b/cookbook/connectors/connector.py @@ -1,9 +1,13 @@ from abc import ABC, abstractmethod -from cookbook.models import ShoppingListEntry, Space +from cookbook.models import ShoppingListEntry, Space, ConnectorConfig class Connector(ABC): + @abstractmethod + def __init__(self, config: ConnectorConfig): + pass + @abstractmethod async def on_shopping_list_entry_created(self, space: Space, instance: ShoppingListEntry) -> None: pass @@ -20,5 +24,4 @@ class Connector(ABC): async def close(self) -> None: pass - # TODO: Maybe add an 'IsEnabled(self) -> Bool' to here # TODO: Add Recipes & possibly Meal Place listeners/hooks (And maybe more?) diff --git a/cookbook/connectors/connector_manager.py b/cookbook/connectors/connector_manager.py index 178c8efb..86a60b04 100644 --- a/cookbook/connectors/connector_manager.py +++ b/cookbook/connectors/connector_manager.py @@ -12,15 +12,13 @@ from typing import List, Any, Dict, Optional from django_scopes import scope from cookbook.connectors.connector import Connector -from cookbook.connectors.example import Example 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 QUEUE_MAX_SIZE = 25 REGISTERED_CLASSES: UnionType = ShoppingListEntry | Recipe | MealPlan -CONNECTOR_UPDATE_CLASSES: UnionType = HomeAssistantConfig | ExampleConfig class ActionType(Enum): @@ -37,7 +35,7 @@ class Work: class ConnectorManager: _queue: JoinableQueue - _listening_to_classes = REGISTERED_CLASSES | CONNECTOR_UPDATE_CLASSES + _listening_to_classes = REGISTERED_CLASSES | ConnectorConfig def __init__(self): self._queue = multiprocessing.JoinableQueue(maxsize=QUEUE_MAX_SIZE) @@ -88,7 +86,7 @@ class ConnectorManager: break # 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 connectors: Optional[List[Connector]] = _connectors.get(space.name) @@ -98,10 +96,11 @@ class ConnectorManager: loop.run_until_complete(close_connectors(connectors)) with scope(space=space): - connectors: List[Connector] = [ - *(HomeAssistant(config) for config in space.homeassistantconfig_set.all() if config.enabled), - *(Example(config) for config in space.exampleconfig_set.all() if config.enabled) - ] + connectors: List[Connector] = list( + filter( + 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 if len(connectors) == 0 or refresh_connector_cache: @@ -113,6 +112,14 @@ class ConnectorManager: 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]): tasks: List[Task] = [asyncio.create_task(connector.close()) for connector in connectors] diff --git a/cookbook/connectors/example.py b/cookbook/connectors/example.py deleted file mode 100644 index ce55f476..00000000 --- a/cookbook/connectors/example.py +++ /dev/null @@ -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 diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index e394e974..e653c3f3 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -4,16 +4,19 @@ from logging import Logger from homeassistant_api import Client, HomeassistantAPIError, Domain from cookbook.connectors.connector import Connector -from cookbook.models import ShoppingListEntry, HomeAssistantConfig, Space +from cookbook.models import ShoppingListEntry, ConnectorConfig, Space class HomeAssistant(Connector): _domains_cache: dict[str, Domain] - _config: HomeAssistantConfig + _config: ConnectorConfig _logger: Logger _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._config = config self._logger = logging.getLogger("connector.HomeAssistant") diff --git a/cookbook/forms.py b/cookbook/forms.py index 18867958..2244acad 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -10,7 +10,7 @@ from django_scopes.forms import SafeModelChoiceField, SafeModelMultipleChoiceFie from hcaptcha.fields import hCaptchaField 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): @@ -209,11 +209,6 @@ class ConnectorConfigForm(forms.ModelForm): 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( widget=forms.TextInput(attrs={'autocomplete': 'update-token', 'type': 'password'}), required=False, @@ -221,45 +216,23 @@ class HomeAssistantConfigForm(ConnectorConfigForm): ) url = forms.URLField( - required=True, + required=False, 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: - 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 = { '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 class RecipeBookEntryForm(forms.ModelForm): prefix = 'bookmark' diff --git a/cookbook/migrations/0208_connectorconfig.py b/cookbook/migrations/0208_connectorconfig.py new file mode 100644 index 00000000..c29c0253 --- /dev/null +++ b/cookbook/migrations/0208_connectorconfig.py @@ -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), + ), + ] diff --git a/cookbook/migrations/0208_homeassistantconfig_exampleconfig.py b/cookbook/migrations/0208_homeassistantconfig_exampleconfig.py deleted file mode 100644 index 789094c1..00000000 --- a/cookbook/migrations/0208_homeassistantconfig_exampleconfig.py +++ /dev/null @@ -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), - ), - ] diff --git a/cookbook/models.py b/cookbook/models.py index a41dc37f..f0bc1af6 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -339,8 +339,7 @@ class Space(ExportModelOperationsMixin('space'), models.Model): SyncLog.objects.filter(sync__space=self).delete() Sync.objects.filter(space=self).delete() Storage.objects.filter(space=self).delete() - HomeAssistantConfig.objects.filter(space=self).delete() - ExampleConfig.objects.filter(space=self).delete() + ConnectorConfig.objects.filter(space=self).delete() ShoppingListEntry.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): + HOMEASSISTANT = 'HomeAssistant' + CONNECTER_TYPE = ((HOMEASSISTANT, 'HomeAssistant'),) + 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") 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(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) space = models.ForeignKey(Space, on_delete=models.CASCADE) 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): diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 53fc8eeb..e102c46f 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -34,7 +34,7 @@ from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Cu ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, - UserFile, UserPreference, UserSpace, ViewLog, HomeAssistantConfig) + UserFile, UserPreference, UserSpace, ViewLog, ConnectorConfig) from cookbook.templatetags.custom_tags import markdown 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): validated_data['created_by'] = self.context['request'].user return super().create(validated_data) class Meta: - model = HomeAssistantConfig + model = ConnectorConfig fields = ( 'id', 'name', 'url', 'token', 'todo_entity', 'enabled', 'on_shopping_list_entry_created_enabled', 'on_shopping_list_entry_updated_enabled', diff --git a/cookbook/tables.py b/cookbook/tables.py index ba14fbbe..3fcec9c3 100644 --- a/cookbook/tables.py +++ b/cookbook/tables.py @@ -1,14 +1,9 @@ -from typing import Any, Dict - import django_tables2 as tables from django.utils.html import format_html 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 .helper.permission_helper import GroupRequiredMixin -from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog, HomeAssistantConfig, ExampleConfig +from .models import CookLog, InviteLink, RecipeImport, Storage, Sync, SyncLog, ViewLog, ConnectorConfig class StorageTable(tables.Table): @@ -20,47 +15,13 @@ class StorageTable(tables.Table): fields = ('id', 'name', 'method') -class ExampleConfigTable(tables.Table): - id = tables.LinkColumn('edit_example_config', args=[A('id')]) +class ConnectorConfigTable(tables.Table): + id = tables.LinkColumn('edit_connector_config', args=[A('id')]) class Meta: - model = ExampleConfig + model = ConnectorConfig template_name = 'generic/table_template.html' - fields = ('id', 'name', 'enabled', 'feed_url') - 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) - ] + fields = ('id', 'name', 'type', 'enabled') class ImportLogTable(tables.Table): diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 7fbb14db..6505b405 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -336,7 +336,7 @@ class="fas fa-server fa-fw"> {% trans 'Space Settings' %} {% endif %} {% if request.user == request.space.created_by or user.is_superuser %} - {% trans 'External Connectors' %} {% endif %} {% if user.is_superuser %} diff --git a/cookbook/tests/api/test_api_home_assistant_config.py b/cookbook/tests/api/test_api_home_assistant_config.py index a3de0f45..79f496bf 100644 --- a/cookbook/tests/api/test_api_home_assistant_config.py +++ b/cookbook/tests/api/test_api_home_assistant_config.py @@ -5,21 +5,21 @@ from django.contrib import auth from django.urls import reverse from django_scopes import scopes_disabled -from cookbook.models import HomeAssistantConfig +from cookbook.models import ConnectorConfig -LIST_URL = 'api:homeassistantconfig-list' -DETAIL_URL = 'api:homeassistantconfig-detail' +LIST_URL = 'api:connectorconfig-list' +DETAIL_URL = 'api:connectorconfig-detail' @pytest.fixture() 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, ) @pytest.fixture 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, ) @@ -123,4 +123,4 @@ def test_delete(a1_s1, a1_s2, obj_1): assert r.status_code == 204 with scopes_disabled(): - assert HomeAssistantConfig.objects.count() == 0 + assert ConnectorConfig.objects.count() == 0 diff --git a/cookbook/tests/edits/test_edits_home_assistant_config.py b/cookbook/tests/edits/test_edits_connector_config.py similarity index 77% rename from cookbook/tests/edits/test_edits_home_assistant_config.py rename to cookbook/tests/edits/test_edits_connector_config.py index 0df507de..aad00f8d 100644 --- a/cookbook/tests/edits/test_edits_home_assistant_config.py +++ b/cookbook/tests/edits/test_edits_connector_config.py @@ -3,16 +3,18 @@ from django.contrib import auth from django.contrib import messages from django.contrib.messages import get_messages 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 def home_assistant_config_obj(a1_s1, space_1): - return HomeAssistantConfig.objects.create( + return ConnectorConfig.objects.create( name='HomeAssistant 1', + type=ConnectorConfig.HOMEASSISTANT, token='token', url='http://localhost:8123/api', 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' r = a1_s1.post( reverse(EDIT_VIEW_NAME, args={home_assistant_config_obj.pk}), { 'name': home_assistant_config_obj.name, + 'type': home_assistant_config_obj.type, 'url': home_assistant_config_obj.url, - 'token': new_token, + 'update_token': new_token, 'todo_entity': home_assistant_config_obj.todo_entity, '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)] 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}), { 'name': home_assistant_config_obj.name, + 'type': home_assistant_config_obj.type, 'url': home_assistant_config_obj.url, 'todo_entity': home_assistant_config_obj.todo_entity, - 'token': new_token, + 'update_token': new_token, 'enabled': home_assistant_config_obj.enabled, } ) diff --git a/cookbook/urls.py b/cookbook/urls.py index e85cfbaf..54d29ae4 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -12,8 +12,7 @@ from recipes.settings import DEBUG, PLUGINS from .models import (Automation, Comment, CustomFilter, Food, InviteLink, Keyword, PropertyType, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Space, Step, Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, UnitConversion, - UserFile, UserSpace, get_model_name, HomeAssistantConfig, ExampleConfig) -from .tables import ConnectorConfigTable + UserFile, UserSpace, get_model_name, ConnectorConfig) from .views import api, data, delete, edit, import_export, lists, new, telegram, views 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'step', api.StepViewSet) 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-category', api.SupermarketCategoryViewSet) router.register(r'supermarket-category-relation', api.SupermarketCategoryRelationViewSet) @@ -116,7 +115,6 @@ urlpatterns = [ path('edit/recipe/convert//', edit.convert_recipe, name='edit_convert_recipe'), path('edit/storage//', edit.edit_storage, name='edit_storage'), - path('list/connectors', ConnectorConfigTable.as_view(), name='list_connectors'), path('delete/recipe-source//', delete.delete_recipe_source, name='delete_recipe_source'), @@ -169,7 +167,7 @@ urlpatterns = [ ] generic_models = ( - Recipe, RecipeImport, Storage, HomeAssistantConfig, ExampleConfig, RecipeBook, SyncLog, Sync, + Recipe, RecipeImport, Storage, ConnectorConfig, RecipeBook, SyncLog, Sync, Comment, RecipeBookEntry, ShoppingList, InviteLink, UserSpace, Space ) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index d2cf65f8..bffc3ee4 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -76,7 +76,7 @@ from cookbook.models import (Automation, BookmarkletImport, CookLog, CustomFilte ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UnitConversion, UserFile, UserPreference, UserSpace, - ViewLog, HomeAssistantConfig) + ViewLog, ConnectorConfig) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -102,7 +102,7 @@ from cookbook.serializer import (AccessTokenSerializer, AutomationSerializer, SupermarketCategorySerializer, SupermarketSerializer, SyncLogSerializer, SyncSerializer, UnitConversionSerializer, UnitSerializer, UserFileSerializer, UserPreferenceSerializer, - UserSerializer, UserSpaceSerializer, ViewLogSerializer, HomeAssistantConfigSerializer) + UserSerializer, UserSpaceSerializer, ViewLogSerializer, ConnectorConfigConfigSerializer) from cookbook.views.import_export import get_integration from recipes import settings 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) -class HomeAssistantConfigViewSet(viewsets.ModelViewSet): - # TODO handle delete protect error and adjust test - queryset = HomeAssistantConfig.objects - serializer_class = HomeAssistantConfigSerializer +class ConnectorConfigConfigViewSet(viewsets.ModelViewSet): + queryset = ConnectorConfig.objects + serializer_class = ConnectorConfigConfigSerializer permission_classes = [CustomIsAdmin & CustomTokenHasReadWriteScope] def get_queryset(self): diff --git a/cookbook/views/delete.py b/cookbook/views/delete.py index bbdc98ee..f37d8a55 100644 --- a/cookbook/views/delete.py +++ b/cookbook/views/delete.py @@ -9,7 +9,7 @@ from django.views.generic import DeleteView from cookbook.helper.permission_helper import GroupRequiredMixin, OwnerRequiredMixin, group_required 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.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -122,27 +122,15 @@ class StorageDelete(GroupRequiredMixin, DeleteView): return HttpResponseRedirect(reverse('list_storage')) -class HomeAssistantConfigDelete(GroupRequiredMixin, DeleteView): +class ConnectorConfigDelete(GroupRequiredMixin, DeleteView): groups_required = ['admin'] template_name = "generic/delete_template.html" - model = HomeAssistantConfig - success_url = reverse_lazy('list_connectors') + model = ConnectorConfig + success_url = reverse_lazy('list_connector_config') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['title'] = _("HomeAssistant 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") + context['title'] = _("Connectors Config Backend") return context diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index 2f99216b..356191ec 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -9,10 +9,10 @@ from django.utils.translation import gettext as _ from django.views.generic import UpdateView 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, 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.local import Local 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'] template_name = "generic/edit_template.html" - model = HomeAssistantConfig - form_class = HomeAssistantConfigForm + model = ConnectorConfig + form_class = ConnectorConfigForm def get_form_kwargs(self): 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'] != "": form.instance.token = form.cleaned_data['update_token'] 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): - 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): context = super().get_context_data(**kwargs) - context['title'] = _("HomeAssistantConfig") - 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") + context['title'] = _("ConnectorConfig") return context diff --git a/cookbook/views/lists.py b/cookbook/views/lists.py index ed43c143..53b1ae79 100644 --- a/cookbook/views/lists.py +++ b/cookbook/views/lists.py @@ -6,8 +6,8 @@ from django.utils.translation import gettext as _ from django_tables2 import RequestConfig from cookbook.helper.permission_helper import group_required -from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile -from cookbook.tables import ImportLogTable, InviteLinkTable, RecipeImportTable, StorageTable +from cookbook.models import InviteLink, RecipeImport, Storage, SyncLog, UserFile, ConnectorConfig +from cookbook.tables import ImportLogTable, InviteLinkTable, RecipeImportTable, StorageTable, ConnectorConfigTable @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') def invite_link(request): table = InviteLinkTable( diff --git a/cookbook/views/new.py b/cookbook/views/new.py index 22bfbc84..93ad9996 100644 --- a/cookbook/views/new.py +++ b/cookbook/views/new.py @@ -5,9 +5,9 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ 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.models import Recipe, RecipeImport, ShareLink, Step, HomeAssistantConfig, ExampleConfig +from cookbook.models import Recipe, RecipeImport, ShareLink, Step, ConnectorConfig from recipes import settings @@ -70,21 +70,12 @@ class StorageCreate(GroupRequiredMixin, CreateView): return context -class HomeAssistantConfigCreate(GroupRequiredMixin, CreateView): +class ConnectorConfigCreate(GroupRequiredMixin, CreateView): groups_required = ['admin'] template_name = "generic/new_template.html" - model = HomeAssistantConfig - form_class = HomeAssistantConfigForm - success_url = reverse_lazy('list_home_assistant_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 + model = ConnectorConfig + form_class = ConnectorConfigForm + success_url = reverse_lazy('list_connector_config') def form_valid(self, form): if self.request.space.demo or settings.HOSTED: @@ -96,35 +87,11 @@ class HomeAssistantConfigCreate(GroupRequiredMixin, CreateView): obj.created_by = self.request.user obj.space = self.request.space 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): context = super().get_context_data(**kwargs) - context['title'] = _("HomeAssistant 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") + context['title'] = _("Connector Config Backend") return context