updated the skill settings endpoint in the public API handle more use cases

pull/187/head
Chris Veilleux 2019-06-25 18:12:34 -05:00
parent e411332667
commit 12dc2175d2
35 changed files with 928 additions and 605 deletions

View File

@ -4,7 +4,7 @@ from http import HTTPStatus
from typing import List
from selene.api import SeleneEndpoint
from selene.data.device import DeviceSkill, DeviceSkillRepository
from selene.data.device import DeviceSkillRepository, ManifestSkill
from selene.util.auth import AuthenticationError
VALID_STATUS_VALUES = (
@ -36,7 +36,7 @@ class SkillInstallStatusEndpoint(SeleneEndpoint):
def _get_installed_skills(self):
skill_repo = DeviceSkillRepository(self.db)
installed_skills = skill_repo.get_device_skills_for_account(
installed_skills = skill_repo.get_skill_manifest_for_account(
self.account.id
)
for skill in installed_skills:
@ -65,9 +65,9 @@ class SkillInstallStatusEndpoint(SeleneEndpoint):
class SkillManifestAggregator(object):
"""Base class containing functionality shared by summary and detail"""
def __init__(self, installed_skills: List[DeviceSkill]):
def __init__(self, installed_skills: List[ManifestSkill]):
self.installed_skills = installed_skills
self.aggregate_skill = DeviceSkill(**asdict(installed_skills[0]))
self.aggregate_skill = ManifestSkill(**asdict(installed_skills[0]))
def aggregate_skill_status(self):
"""Aggregate skill data on all devices into a single skill.

View File

@ -18,7 +18,7 @@ from .endpoints.device_refresh_token import DeviceRefreshTokenEndpoint
from .endpoints.device_setting import DeviceSettingEndpoint
from .endpoints.device_skill import DeviceSkillEndpoint
from .endpoints.device_skill_manifest import DeviceSkillManifestEndpoint
from .endpoints.device_skills import DeviceSkillsEndpoint
from .endpoints.device_skill_settings import DeviceSkillsEndpoint
from .endpoints.device_subscription import DeviceSubscriptionEndpoint
from .endpoints.google_stt import GoogleSTTEndpoint
from .endpoints.oauth_callback import OauthCallbackEndpoint

View File

@ -0,0 +1,342 @@
import json
from http import HTTPStatus
from flask import Response
from schematics import Model
from schematics.exceptions import ValidationError
from schematics.types import StringType, BooleanType, ListType, ModelType
from selene.api import PublicEndpoint
from selene.data.account import AccountRepository
from selene.data.device import DeviceSkillRepository
from selene.data.skill import (
SettingsDisplay,
SettingsDisplayRepository,
SkillRepository,
SkillSettingRepository
)
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
# matches <submodule_name>|<branch>
GLOBAL_ID_PATTERN = '^([^\|@]+)\|([^\|]+$)'
# matches @<device_id>|<submodule_name>|<branch>
GLOBAL_ID_DIRTY_PATTERN = '^@(.*)\|(.*)\|(.*)$'
# matches @<device_id>|<folder_name>
GLOBAL_ID_NON_MSM_PATTERN = '^@([^\|]+)\|([^\|]+$)'
GLOBAL_ID_ANY_PATTERN = '(?:{})|(?:{})|(?:{})'.format(
GLOBAL_ID_PATTERN,
GLOBAL_ID_DIRTY_PATTERN,
GLOBAL_ID_NON_MSM_PATTERN
)
class SkillSettingUpdater(object):
"""Update the settings data for all devices with a skill
Skills and their settings are global across devices. While the PUT
request specifies a single device to update, all devices with
the same skill must be updated as well.
"""
_device_skill_repo = None
_settings_display_repo = None
def __init__(self, db, device_id, display_data: dict):
self.db = db
self.device_id = device_id
self.display_data = display_data
self.settings_values = None
self.skill = None
@property
def device_skill_repo(self):
if self._device_skill_repo is None:
self._device_skill_repo = DeviceSkillRepository(self.db)
return self._device_skill_repo
@property
def settings_display_repo(self):
if self._settings_display_repo is None:
self._settings_display_repo = SettingsDisplayRepository(self.db)
return self._settings_display_repo
def update(self):
self._extract_settings_values()
self._get_skill_id()
self._ensure_settings_display_exists()
self._update_device_skill()
def _extract_settings_values(self):
"""Extract the settings values from the skillMetadata
The device applies the settings values in settings.json to the
settings_meta.json file before sending the result to this API. The
settings values are stored separately from the metadata in the database.
"""
settings_definition = self.display_data.get('skillMetadata')
if settings_definition is not None:
self.settings_values = dict()
sections_without_values = []
for section in settings_definition['sections']:
section_without_values = dict(**section)
for field in section_without_values['fields']:
field_name = field.get('name')
field_value = field.get('value')
if field_name is not None and field_value is not None:
self.settings_values[field_name] = field_value
del(field['value'])
sections_without_values.append(section_without_values)
settings_definition['sections'] = sections_without_values
def _get_skill_id(self):
"""Get the id of the skill in the request"""
skill_global_id = (
self.display_data['skill_gid'] or
self.display_data['identifier']
)
skill_repo = SkillRepository(self.db)
self.skill = skill_repo.get_skill_by_global_id(skill_global_id)
def _ensure_settings_display_exists(self) -> bool:
"""If the settings display changed, a new row needs to be added."""
new_settings_display = False
self.settings_display = SettingsDisplay(
self.skill.id,
self.display_data
)
self.settings_display.id = (
self.settings_display_repo.get_settings_display_id(
self.settings_display
)
)
if self.settings_display.id is None:
self.settings_display.id = self.settings_display_repo.add(
self.settings_display
)
new_settings_display = True
return new_settings_display
def _update_device_skill(self):
"""Update the account's devices with the skill to have new settings"""
account_repo = AccountRepository(self.db)
account = account_repo.get_account_by_device_id(self.device_id)
device_skill_settings = (
self.device_skill_repo.get_device_skill_settings_for_account(
account.id
)
)
for device_skill in device_skill_settings:
if device_skill.skill_id == self.skill.id:
if device_skill.install_method in ('voice', 'cli'):
device_ids = [self.device_id]
else:
device_ids = device_skill.device_ids
self.device_skill_repo.update_device_skill_settings(
device_ids,
self.settings_display,
self.settings_values if self.settings_values else None
)
class SkillField(Model):
name = StringType()
type = StringType()
label = StringType()
hint = StringType()
placeholder = StringType()
hide = BooleanType()
value = StringType()
options = StringType()
class SkillSection(Model):
name = StringType(required=True)
fields = ListType(ModelType(SkillField))
class SkillMetadata(Model):
sections = ListType(ModelType(SkillSection))
class SkillIcon(Model):
color = StringType()
icon = StringType()
class Skill(Model):
name = StringType()
skill_gid = StringType(regex=GLOBAL_ID_ANY_PATTERN)
skillMetadata = ModelType(SkillMetadata)
icon_img = StringType()
icon = ModelType(SkillIcon)
display_name = StringType()
color = StringType()
identifier = StringType()
def validate_skill_gid(self, data, value):
if data['skill_gid'] is None and data['identifier'] is None:
raise ValidationError(
'skill should have either skill_gid or identifier defined'
)
return value
class DeviceSkillsEndpoint(PublicEndpoint):
"""Fetch all skills associated with a device using the API v1 format"""
_device_skill_repo = None
_skill_repo = None
_skill_setting_repo = None
_settings_display_repo = None
@property
def device_skill_repo(self):
if self._device_skill_repo is None:
self._device_skill_repo = DeviceSkillRepository(self.db)
return self._device_skill_repo
@property
def settings_display_repo(self):
if self._settings_display_repo is None:
self._settings_display_repo = SettingsDisplayRepository(self.db)
return self._settings_display_repo
@property
def skill_repo(self):
if self._skill_repo is None:
self._skill_repo = SkillRepository(self.db)
return self._skill_repo
@property
def skill_setting_repo(self):
if self._skill_setting_repo is None:
self._skill_setting_repo = SkillSettingRepository(self.db)
return self._skill_setting_repo
def get(self, device_id):
"""
Retrieve skills installed on device from the database.
:raises NotModifiedException: when etag in request matches cache
"""
self._authenticate(device_id)
self._validate_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
device_skills = self.skill_setting_repo.get_skill_settings_for_device(
device_id
)
if device_skills:
response_data = self._build_response_data(device_skills)
response = Response(
json.dumps(response_data),
status=HTTPStatus.OK,
content_type='application/json'
)
self._add_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
else:
response = Response(
'',
status=HTTPStatus.NO_CONTENT,
content_type='application/json'
)
return response
def _build_response_data(self, device_skills):
response_data = []
for skill in device_skills:
response_skill = dict(uuid=skill.skill_id)
settings_definition = skill.settings_display.get('skillMetadata')
if settings_definition:
settings_sections = self._apply_settings_values(
settings_definition, skill.settings_values
)
if settings_sections:
response_skill.update(
skillMetadata=dict(sections=settings_sections)
)
skill_gid = skill.settings_display.get('skill_gid')
if skill_gid is not None:
response_skill.update(skill_gid=skill_gid)
identifier = skill.settings_display.get('identifier')
if identifier is not None:
response_skill.update(identifier=identifier)
response_data.append(response_skill)
return response_data
@staticmethod
def _apply_settings_values(settings_definition, settings_values):
"""Build a copy of the settings sections populated with values."""
sections_with_values = []
for section in settings_definition['sections']:
section_with_values = dict(**section)
for field in section_with_values['fields']:
field_name = field.get('name')
if field_name is not None and field_name in settings_values:
field.update(value=settings_values[field_name])
sections_with_values.append(section_with_values)
return sections_with_values
def put(self, device_id):
self._authenticate(device_id)
self._validate_put_request()
skill_id = self._update_skill_settings(device_id)
self.etag_manager.expire(
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
)
return dict(uuid=skill_id), HTTPStatus.OK
def _validate_put_request(self):
skill = Skill(self.request.json)
skill.validate()
def _update_skill_settings(self, device_id):
skill_setting_updater = SkillSettingUpdater(
self.db,
device_id,
self.request.json
)
skill_setting_updater.update()
self._delete_orphaned_settings_display(
skill_setting_updater.settings_display.id
)
return skill_setting_updater.skill.id
def delete(self, device_id, skill_id):
self._authenticate(device_id)
settings_display_id = self._delete_skill_from_device(
device_id,
skill_id
)
self._delete_orphaned_settings_display(settings_display_id)
return '', HTTPStatus.OK
def _delete_skill_from_device(self, device_id, skill_id):
settings_display_id = None
device_skills = (
self.device_skill_repo.get_device_skill_settings_for_device(
device_id
)
)
for device_skill in device_skills:
if device_skill.skill_id == skill_id:
self.device_skill_repo.remove(device_id, skill_id)
settings_display_id = device_skill.settings_display_id
return settings_display_id
def _delete_orphaned_settings_display(self, settings_display_id):
skill_count = self.device_skill_repo.get_settings_display_usage(
settings_display_id
)
if not skill_count:
self.settings_display_repo.remove(settings_display_id)

View File

@ -1,109 +0,0 @@
import json
from http import HTTPStatus
from flask import Response
from schematics import Model
from schematics.exceptions import ValidationError
from schematics.types import StringType, BooleanType, ListType, ModelType
from selene.api import PublicEndpoint
from selene.data.skill import SkillRepository
from selene.data.skill.repository.device_skill import DeviceSkillRepository
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
global_id_pattern = '^([^\|@]+)\|([^\|]+$)' # matches <submodule_name>|<branch>
global_id_dirt_pattern = '^@(.*)\|(.*)\|(.*)$' # matches @<device_id>|<submodule_name>|<branch>
global_id_non_msm_pattern = '^@([^\|]+)\|([^\|]+$)' # matches @<device_id>|<folder_name>
global_id_any_pattern = '(?:{})|(?:{})|(?:{})'.format(
global_id_pattern,
global_id_dirt_pattern,
global_id_non_msm_pattern
)
class SkillField(Model):
name = StringType()
type = StringType()
label = StringType()
hint = StringType()
placeholder = StringType()
hide = BooleanType()
value = StringType()
options = StringType()
class SkillSection(Model):
name = StringType(required=True)
fields = ListType(ModelType(SkillField))
class SkillMetadata(Model):
sections = ListType(ModelType(SkillSection))
class SkillIcon(Model):
color = StringType()
icon = StringType()
class Skill(Model):
name = StringType()
skill_gid = StringType(regex=global_id_any_pattern)
skillMetadata = ModelType(SkillMetadata)
icon_img = StringType()
icon = ModelType(SkillIcon)
display_name = StringType()
color = StringType()
identifier = StringType()
def validate_skill_gid(self, data, value):
if data['skill_gid'] is None and data['identifier'] is None:
raise ValidationError(
'skill should have either skill_gid or identifier define'
)
return value
class DeviceSkillsEndpoint(PublicEndpoint):
"""Fetch all skills associated with a device using the API v1 format"""
_skill_repo = None
@property
def skill_repo(self):
if self._skill_repo is None:
self._skill_repo = SkillRepository(self.db)
return self._skill_repo
def get(self, device_id):
self._authenticate(device_id)
self._validate_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
skills = self.skill_repo.get_skill_settings_by_device_id(device_id)
if skills is not None:
response = Response(
json.dumps(skills),
status=HTTPStatus.OK,
content_type='application_json'
)
self._add_etag(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
else:
response = Response(
'',
status=HTTPStatus.NO_CONTENT,
content_type='application_json'
)
return response
def put(self, device_id):
self._authenticate(device_id)
payload = json.loads(self.request.data)
skill = Skill(payload)
skill.validate()
skill_id = SkillRepository(self.db).add(device_id, payload)
return {'uuid': skill_id}, HTTPStatus.OK
def delete(self, device_id, skill_id):
self._authenticate(device_id)
DeviceSkillRepository(self.db).delete(device_id, skill_id)
return '', HTTPStatus.OK

View File

@ -0,0 +1,54 @@
Feature: Upload and fetch skills and their settings
Test all endpoints related to upload and fetch skill settings
Scenario: A device requests the settings for its skills
Given an authorized device
When a device requests the settings for its skills
Then the request will be successful
And the settings are returned
And an E-tag is generated for these settings
And device last contact timestamp is updated
# This scenario uses the ETag generated by the first scenario
Scenario: Device requests skill settings that have not changed since last they were requested
Given an authorized device
And a valid device skill E-tag
When a device requests the settings for its skills
Then the request will succeed with a "not modified" return code
And device last contact timestamp is updated
# This scenario uses the ETag generated by the first scenario
Scenario: Device requests skill settings that have changed since last they were requested
Given an authorized device
And an expired device skill E-tag
When a device requests the settings for its skills
Then the request will be successful
And an E-tag is generated for these settings
And device last contact timestamp is updated
Scenario: A device uploads a change to a single skill setting value
Given an authorized device
And a valid device skill E-tag
And skill settings with a new value
When the device sends a request to update the skill settings
Then the request will be successful
And the skill settings are updated with the new value
And the device skill E-tag is expired
And device last contact timestamp is updated
Scenario: A device uploads skill settings with a field deleted from the settings
Given an authorized device
And a valid device skill E-tag
And skill settings with a deleted field
When the device sends a request to update the skill settings
Then the request will be successful
And the field is no longer in the skill settings
And the device skill E-tag is expired
And device last contact timestamp is updated
Scenario: A device requests a skill to be deleted
Given an authorized device
When the device requests a skill to be deleted
Then the request will be successful
And the skill will be removed from the device skill list
And device last contact timestamp is updated

View File

@ -1,33 +0,0 @@
Feature: Upload and fetch skills
Test all endpoints related to upload and fetch skill settings
Scenario: A skill is successfully uploaded and retrieved
Given a device with skill settings
When the skill settings are updated
And the skill settings is fetched
Then the skill settings should be retrieved with the new values
And device last contact timestamp is updated
Scenario: Get a 304 when try to get the device's skills using a valid etag
Given a device with skill settings
When the skill settings are fetched using a valid etag
Then the skill setting endpoint should return 304
And device last contact timestamp is updated
Scenario: Get a new etag when try to fetch the skill settings using an expired etag
Given a device with skill settings
When the skill settings are fetched using an expired etag
Then the skill settings endpoint should return a new etag
And device last contact timestamp is updated
Scenario: Upload a skill with empty settings
When a skill with empty settings is uploaded
Then the endpoint to retrieve the skill should return 200
And device last contact timestamp is updated
Scenario: A skill setting is successfully deleted
Given a device with skill settings
When the skill settings is deleted
And the skill settings is fetched
Then the endpoint to delete the skills settings should return 200
And device last contact timestamp is updated

View File

@ -9,10 +9,16 @@ from selene.testing.account_geography import add_account_geography
from selene.testing.account_preference import add_account_preference
from selene.testing.device import add_device
from selene.testing.device_skill import (
add_skill_to_manifest,
remove_manifest_skill
add_device_skill,
add_device_skill_settings,
remove_device_skill
)
from selene.testing.skill import (
add_skill,
build_label_field,
build_text_field,
remove_skill
)
from selene.testing.skill import add_skill, remove_skill
from selene.testing.text_to_speech import (
add_text_to_speech,
remove_text_to_speech
@ -50,24 +56,10 @@ def after_all(context):
def before_scenario(context, _):
context.etag_manager = ETagManager(context.cache, context.client_config)
try:
context.account = add_account(context.db)
add_account_preference(context.db, context.account.id)
context.geography_id = add_account_geography(
context.db,
context.account
)
context.wake_word = add_wake_word(context.db)
context.voice = add_text_to_speech(context.db)
_add_account(context)
_add_skills(context)
_add_device(context)
context.skill = add_skill(
context.db,
skill_global_id='selene-test-skill|19.02'
)
context.manifest_skill = add_skill_to_manifest(
context.db,
context.device_id,
context.skill
)
_add_device_skills(context)
except:
import traceback
print(traceback.print_exc())
@ -77,10 +69,22 @@ def after_scenario(context, _):
remove_account(context.db, context.account)
remove_wake_word(context.db, context.wake_word)
remove_text_to_speech(context.db, context.voice)
remove_skill(context.db, skill_global_id=context.skill.skill_gid)
for skill in context.skills.values():
remove_skill(context.db, skill[0])
def _add_account(context):
context.account = add_account(context.db)
add_account_preference(context.db, context.account.id)
context.geography_id = add_account_geography(
context.db,
context.account
)
def _add_device(context):
context.wake_word = add_wake_word(context.db)
context.voice = add_text_to_speech(context.db)
device_id = add_device(context.db, context.account.id, context.geography_id)
context.device_id = device_id
context.device_name = 'Selene Test Device'
@ -88,26 +92,66 @@ def _add_device(context):
context.access_token = context.device_login['accessToken']
def _add_skills(context):
foo_skill, foo_settings_display = add_skill(
context.db,
skill_global_id='foo-skill|19.02',
)
bar_skill, bar_settings_display = add_skill(
context.db,
skill_global_id='bar-skill|19.02',
settings_fields=[build_label_field(), build_text_field()]
)
context.skills = dict(
foo=(foo_skill, foo_settings_display),
bar=(bar_skill, bar_settings_display)
)
def _add_device_skills(context):
for value in context.skills.values():
skill, settings_display = value
context.manifest_skill = add_device_skill(
context.db,
context.device_id,
skill
)
settings_values = None
if skill.skill_gid.startswith('bar'):
settings_values = dict(textfield='Device text value')
add_device_skill_settings(
context.db,
context.device_id,
settings_display,
settings_values=settings_values
)
def before_tag(context, tag):
if tag == 'device_specific_skill':
_add_device_specific_skil(context)
_add_device_specific_skill(context)
def _add_device_specific_skil(context):
context.device_specific_skill = add_skill(
def _add_device_specific_skill(context):
dirty_skill, dirty_skill_settings = add_skill(
context.db,
skill_global_id='@{device_id}|device-specific-skill|19.02'.format(
device_id=context.device_id
)
)
context.device_specific_manifest = add_skill_to_manifest(
context.skills.update(dirty=(dirty_skill, dirty_skill_settings))
context.device_specific_manifest = add_device_skill(
context.db,
context.device_id,
context.skill
dirty_skill
)
def after_tag(context, tag):
if tag == 'new_skill':
remove_manifest_skill(context.db, context.new_manifest_skill)
remove_skill(context.db, context.new_skill.skill_gid)
_delete_new_skill(context)
def _delete_new_skill(context):
remove_device_skill(context.db, context.new_manifest_skill)
remove_skill(context.db, context.new_skill)

View File

@ -1,268 +0,0 @@
import json
from http import HTTPStatus
from behave import when, then, given
from hamcrest import assert_that, equal_to, not_none, is_not, has_key
from selene.api.etag import ETagManager
from selene.data.skill import AccountSkillSetting, SkillSettingRepository
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
from selene.util.db import connect_to_db
skill = {
'skill_gid': 'wolfram-alpha|19.02',
'identifier': 'wolfram-alpha-123456',
"skillMetadata": {
"sections": [
{
"name": "Test-456",
"fields": [
{
"type": "label",
"label": "label-test"
},
{
"name": "user",
"type": "text",
"label": "Username",
"value": "name test",
"placeholder": "this is a test"
},
{
"name": "password",
"type": "password",
"label": "Password",
"value": "123"
}
]
}
]
}
}
skill_empty_settings = {
'skill_gid': 'mycroft-alarm|19.02',
'identifier': 'mycroft-alarm|19.02'
}
new_settings = {
'user': 'this name is a test',
'password': 'this is a new password'
}
skill_updated = {
'skill_gid': 'wolfram-alpha|19.02',
"skillMetadata": {
"sections": [
{
"name": "Test-456",
"fields": [
{
"type": "label",
"label": "label-test"
},
{
"name": "user",
"type": "text",
"label": "Username",
"value": "this name is a test",
"placeholder": "this is a test"
},
{
"name": "password",
"type": "password",
"label": "Password",
"value": "this is a new password"
}
]
}
]
}
}
@given('a device with skill settings')
def create_skill_settings(context):
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
context.upload_device_response = context.client.put(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
data=json.dumps(skill),
content_type='application_json',
headers=headers
)
context.get_skill_response = context.client.get(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
headers=headers
)
@when('the skill settings are updated')
def update_skill(context):
response = json.loads(context.upload_device_response.data)
update_settings = AccountSkillSetting(
settings_display={},
settings_values=new_settings,
device_names=[context.device_name]
)
skill_ids = [response['uuid']]
db = connect_to_db(context.client_config['DB_CONNECTION_CONFIG'])
skill_setting_repo = SkillSettingRepository(db, context.account.id)
skill_setting_repo.update_skill_settings(update_settings, skill_ids)
@when('the skill settings is fetched')
def retrieve_skill_updated(context):
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers=dict(Authorization='Bearer {token}'.format(token=access_token))
context.get_skill_updated_response = context.client.get(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
headers=headers
)
@then('the skill settings should be retrieved with the new values')
def validate_get_skill_updated_response(context):
# First we validate the skill uploading
response = context.upload_device_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
# Then we validate if the skill we fetch is the same that was uploaded
response = context.get_skill_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
skills_response = json.loads(response.data)
assert_that(len(skills_response), equal_to(1))
response = skills_response[0]
assert_that(response, has_key('uuid'))
assert_that(response['skill_gid'], equal_to(skill['skill_gid']))
assert_that(response['identifier'], equal_to(skill['identifier']))
assert_that(response['skillMetadata'], equal_to(skill['skillMetadata']))
# Then we validate if the skill was properly updated
response = context.get_skill_updated_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
response_data = json.loads(response.data)
assert_that(len(response_data), equal_to(1))
response_data = response_data[0]
assert_that(response_data['skill_gid'], equal_to(skill_updated['skill_gid']))
assert_that(response_data['skillMetadata'], equal_to(skill_updated['skillMetadata']))
@when('the skill settings are fetched using a valid etag')
def get_skills_etag(context):
etag_manager: ETagManager = context.etag_manager
login = context.device_login
device_id = login['uuid']
skill_etag = etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
)
access_token = login['accessToken']
headers = {
'Authorization': 'Bearer {token}'.format(token=access_token),
'If-None-Match': skill_etag
}
context.get_skill_response = context.client.get(
'/v1/device/{device_id}/skill'.format(device_id=device_id),
headers=headers
)
@then('the skill setting endpoint should return 304')
def validate_etag(context):
response = context.get_skill_response
assert_that(response.status_code, HTTPStatus.NOT_MODIFIED)
@when('the skill settings are fetched using an expired etag')
def get_skills_expired_etag(context):
etag_manager: ETagManager = context.etag_manager
login = context.device_login
device_id = login['uuid']
context.old_skill_etag = etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=device_id)
)
etag_manager.expire_skill_etag_by_device_id(device_id)
access_token = login['accessToken']
headers = {
'Authorization': 'Bearer {token}'.format(token=access_token),
'If-None-Match': context.old_skill_etag
}
context.get_skill_response = context.client.get(
'/v1/device/{device_id}/skill'.format(device_id=device_id),
headers=headers
)
@then('the skill settings endpoint should return a new etag')
def validate_expired_etag(context):
response = context.get_skill_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
new_etag = response.headers.get('ETag')
assert_that(new_etag, not_none())
assert_that(context.old_skill_etag, is_not(new_etag))
@when('a skill with empty settings is uploaded')
def upload_empty_skill(context):
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
context.upload_device_response = context.client.put(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
data=json.dumps(skill_empty_settings),
content_type='application_json',
headers=headers
)
context.get_skill_response = context.client.get(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
headers=headers
)
@then('the endpoint to retrieve the skill should return 200')
def validate_empty_skill_uploading(context):
response = context.upload_device_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
response = context.get_skill_response
assert_that(response.status_code, equal_to(HTTPStatus.OK))
new_etag = response.headers.get('ETag')
assert_that(new_etag, not_none())
retrieved_skill = json.loads(context.get_skill_response.data)[0]
assert_that(skill_empty_settings['skill_gid'], retrieved_skill['skill_gid'])
assert_that(skill_empty_settings['identifier'], retrieved_skill['identifier'])
@when('the skill settings is deleted')
def delete_skill(context):
skills = json.loads(context.get_skill_response.data)
skill_fetched = skills[0]
skill_uuid = skill_fetched['uuid']
login = context.device_login
device_id = login['uuid']
access_token = login['accessToken']
headers = dict(Authorization='Bearer {token}'.format(token=access_token))
context.delete_skill_response = context.client.delete(
'/v1/device/{device_uuid}/skill/{skill_uuid}'.format(device_uuid=device_id, skill_uuid=skill_uuid),
headers=headers
)
context.get_skill_after_delete_response = context.client.get(
'/v1/device/{uuid}/skill'.format(uuid=device_id),
headers=headers
)
@then('the endpoint to delete the skills settings should return 200')
def validate_delete_skill(context):
# Validating that the deletion happened successfully
response = context.delete_skill_response
assert_that(response.status_code, HTTPStatus.OK)
# Validating that the skill is not listed after we fetch the device's skills
response = context.get_skill_after_delete_response
assert_that(response.status_code, equal_to(HTTPStatus.NO_CONTENT))

View File

@ -0,0 +1,172 @@
import json
from behave import when, then, given
from hamcrest import assert_that, equal_to, is_not, is_in
from selene.api.etag import ETAG_REQUEST_HEADER_KEY
from selene.data.device import DeviceSkillRepository
from selene.data.skill import SkillSettingRepository
from selene.util.cache import DEVICE_SKILL_ETAG_KEY
@given('skill settings with a new value')
def change_skill_setting_value(context):
_, bar_settings_display = context.skills['bar']
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
field_with_value = section['fields'][1]
field_with_value['value'] = 'New device text value'
@given('skill settings with a deleted field')
def delete_field_from_settings(context):
_, bar_settings_display = context.skills['bar']
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
context.removed_field = section['fields'].pop(1)
@given('a valid device skill E-tag')
def set_skill_setting_etag(context):
context.device_skill_etag = context.etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
)
@given('an expired device skill E-tag')
def expire_skill_setting_etag(context):
valid_device_skill_etag = context.etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
)
context.device_skill_etag = context.etag_manager.expire(
valid_device_skill_etag
)
@when('a device requests the settings for its skills')
def get_device_skill_settings(context):
if hasattr(context, 'device_skill_etag'):
context.request_header[ETAG_REQUEST_HEADER_KEY] = (
context.device_skill_etag
)
context.response = context.client.get(
'/v1/device/{device_id}/skill'.format(device_id=context.device_id),
content_type='application/json',
headers=context.request_header
)
@when('the device sends a request to update the skill settings')
def update_skill_settings(context):
_, bar_settings_display = context.skills['bar']
context.response = context.client.put(
'/v1/device/{device_id}/skill'.format(device_id=context.device_id),
data=json.dumps(bar_settings_display.display_data),
content_type='application/json',
headers=context.request_header
)
@when('the device requests a skill to be deleted')
def delete_skill(context):
foo_skill, _ = context.skills['foo']
context.response = context.client.delete(
'/v1/device/{device_id}/skill/{skill_id}'.format(
device_id=context.device_id,
skill_id=foo_skill.id
),
headers=context.request_header
)
@then('the settings are returned')
def validate_response(context):
response = context.response.json
assert_that(len(response), equal_to(2))
foo_skill, foo_settings_display = context.skills['foo']
foo_skill_expected_result = dict(
uuid=foo_skill.id,
skill_gid=foo_skill.skill_gid,
identifier=foo_settings_display.display_data['identifier']
)
assert_that(foo_skill_expected_result, is_in(response))
bar_skill, bar_settings_display = context.skills['bar']
section = bar_settings_display.display_data['skillMetadata']['sections'][0]
field_with_value = section['fields'][1]
field_with_value['value'] = 'Device text value'
bar_skill_expected_result = dict(
uuid=bar_skill.id,
skill_gid=bar_skill.skill_gid,
identifier=bar_settings_display.display_data['identifier'],
skillMetadata=bar_settings_display.display_data['skillMetadata']
)
assert_that(bar_skill_expected_result, is_in(response))
@then('the device skill E-tag is expired')
def check_for_expired_etag(context):
"""An E-tag is expired by changing its value."""
expired_device_skill_etag = context.etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
)
assert_that(
expired_device_skill_etag.decode(),
is_not(equal_to(context.device_skill_etag))
)
@then('the skill settings are updated with the new value')
def validate_updated_skill_setting_value(context):
settings_repo = SkillSettingRepository(context.db)
device_skill_settings = settings_repo.get_skill_settings_for_device(
context.device_id
)
device_settings_values = [
dss.settings_values for dss in device_skill_settings
]
assert_that(len(device_skill_settings), equal_to(2))
expected_settings_values = dict(textfield='New device text value')
assert_that(
expected_settings_values,
is_in(device_settings_values)
)
@then('an E-tag is generated for these settings')
def get_skills_etag(context):
response_headers = context.response.headers
response_etag = response_headers['ETag']
skill_etag = context.etag_manager.get(
DEVICE_SKILL_ETAG_KEY.format(device_id=context.device_id)
)
assert_that(skill_etag.decode(), equal_to(response_etag))
@then('the field is no longer in the skill settings')
def validate_skill_setting_field_removed(context):
settings_repo = SkillSettingRepository(context.db)
device_skill_settings = settings_repo.get_skill_settings_for_device(
context.device_id
)
device_settings_values = [
dss.settings_values for dss in device_skill_settings
]
assert_that(len(device_skill_settings), equal_to(2))
assert_that([None, None], equal_to(device_settings_values))
new_section = dict(fields=None)
for device_skill_setting in device_skill_settings:
skill_gid = device_skill_setting.settings_display['skill_gid']
if skill_gid.startswith('bar'):
new_settings_display = device_skill_setting.settings_display
new_skill_definition = new_settings_display['skillMetadata']
new_section = new_skill_definition['sections'][0]
assert_that(context.removed_field, not is_in(new_section['fields']))
@then('the skill will be removed from the device skill list')
def validate_delete_skill(context):
device_skill_repo = DeviceSkillRepository(context.db)
device_skills = device_skill_repo.get_device_skill_settings_for_device(
context.device_id
)
assert_that(len(device_skills), equal_to(1))

View File

@ -112,7 +112,7 @@ for db_setup_file in DB_CREATE_FILES:
postgres_db.close_db()
template_db = PostgresDB(db_name='mycroft_template')
template_db = PostgresDB(db_name='mycroft_template', user='mycroft')
print('Creating the extensions')
template_db.execute_sql(

View File

@ -5,6 +5,8 @@ from selene.data.device import DeviceRepository
from selene.util.cache import SeleneCache, DEVICE_SKILL_ETAG_KEY
from selene.util.db import connect_to_db
ETAG_REQUEST_HEADER_KEY = 'If-None-Match'
def device_etag_key(device_id: str):
return 'device.etag:{uuid}'.format(uuid=device_id)
@ -37,7 +39,7 @@ class ETagManager(object):
self.cache.set(key, etag)
return etag
def _expire(self, key):
def expire(self, key):
"""Expires an existent etag
:param key: key where the etag is stored"""
etag = ''.join(random.choice(self.etag_chars) for _ in range(32))
@ -46,12 +48,12 @@ class ETagManager(object):
def expire_device_etag_by_device_id(self, device_id: str):
"""Expire the etag associated with a device entity
:param device_id: device uuid"""
self._expire(device_etag_key(device_id))
self.expire(device_etag_key(device_id))
def expire_device_setting_etag_by_device_id(self, device_id: str):
"""Expire the etag associated with a device's settings entity
:param device_id: device uuid"""
self._expire(device_setting_etag_key(device_id))
self.expire(device_setting_etag_key(device_id))
def expire_device_setting_etag_by_account_id(self, account_id: str):
"""Expire the settings' etags for all devices from a given account. Used when the settings are updated
@ -64,7 +66,7 @@ class ETagManager(object):
def expire_device_location_etag_by_device_id(self, device_id: str):
"""Expire the etag associate with the device's location entity
:param device_id: device uuid"""
self._expire(device_location_etag_key(device_id))
self.expire(device_location_etag_key(device_id))
def expire_device_location_etag_by_account_id(self, account_id: str):
"""Expire the locations' etag fpr açç device for a given acccount
@ -79,7 +81,7 @@ class ETagManager(object):
:param device_id: device uuid
"""
self._expire(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
self.expire(DEVICE_SKILL_ETAG_KEY.format(device_id=device_id))
def expire_skill_etag_by_account_id(self, account_id):
db = connect_to_db(self.db_connection_config)

View File

@ -1,4 +1,4 @@
from .entity.device_skill import DeviceSkill, ManifestSkill
from .entity.device_skill import ManifestSkill, DeviceSkillSettings
from .entity.geography import Geography
from .entity.preference import AccountPreferences
from .entity.text_to_speech import TextToSpeech

View File

@ -1,21 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
@dataclass
class DeviceSkill(object):
id: str
device_id: str
install_method: str
install_status: str
skill_id: str
skill_gid: str
device_name: str = None
install_failure_reason: str = None
install_ts: datetime = None
update_ts: datetime = None
skill_settings: dict = None
skill_settings_display_id: str = None
from typing import List
@dataclass
@ -26,5 +11,15 @@ class ManifestSkill(object):
skill_gid: str
install_failure_reason: str = None
install_ts: datetime = None
skill_id: str = None
update_ts: datetime = None
id: str = None
@dataclass
class DeviceSkillSettings(object):
device_ids: List[str]
install_method: str
skill_id: str
settings_values: dict = None
settings_display_id: str = None

View File

@ -1,8 +1,10 @@
"""Data repository code for the skills on a device"""
import json
from dataclasses import asdict
from typing import List
from ..entity.device_skill import DeviceSkill, ManifestSkill
from selene.data.skill import SettingsDisplay
from ..entity.device_skill import ManifestSkill, DeviceSkillSettings
from ...repository_base import RepositoryBase
@ -10,15 +12,24 @@ class DeviceSkillRepository(RepositoryBase):
def __init__(self, db):
super(DeviceSkillRepository, self).__init__(db, __file__)
def get_installed_skills_for_account(
def get_device_skill_settings_for_account(
self, account_id: str
) -> List[DeviceSkill]:
) -> List[DeviceSkillSettings]:
return self._select_all_into_dataclass(
dataclass=DeviceSkill,
sql_file_name='get_device_skills_for_account.sql',
DeviceSkillSettings,
sql_file_name='get_device_skill_settings_for_account.sql',
args=dict(account_id=account_id)
)
def get_device_skill_settings_for_device(
self, device_id: str
) -> List[DeviceSkillSettings]:
return self._select_all_into_dataclass(
DeviceSkillSettings,
sql_file_name='get_device_skill_settings_for_device.sql',
args=dict(device_id=device_id)
)
def update_skill_settings(
self, account_id: str, device_names: tuple, skill_name: str
):
@ -32,13 +43,41 @@ class DeviceSkillRepository(RepositoryBase):
)
self.cursor.update(db_request)
def get_skill_manifest_for_device(self, device_id: str):
def update_device_skill_settings(
self,
device_ids: List[str],
settings_display: SettingsDisplay,
settings_values: str,
):
db_request = self._build_db_request(
sql_file_name='update_device_skill_settings.sql',
args=dict(
device_ids=tuple(device_ids),
skill_id=settings_display.skill_id,
settings_values=json.dumps(settings_values),
settings_display_id=settings_display.id
)
)
self.cursor.update(db_request)
def get_skill_manifest_for_device(
self, device_id: str
) -> List[ManifestSkill]:
return self._select_all_into_dataclass(
dataclass=ManifestSkill,
sql_file_name='get_device_skill_manifest.sql',
args=dict(device_id=device_id)
)
def get_skill_manifest_for_account(
self, account_id: str
) -> List[ManifestSkill]:
return self._select_all_into_dataclass(
dataclass=ManifestSkill,
sql_file_name='get_skill_manifest_for_account.sql',
args=dict(account_id=account_id)
)
def update_manifest_skill(self, manifest_skill: ManifestSkill):
db_request = self._build_db_request(
sql_file_name='update_skill_manifest.sql',
@ -47,12 +86,10 @@ class DeviceSkillRepository(RepositoryBase):
self.cursor.update(db_request)
def add_manifest_skill(self, skill_id: str, manifest_skill: ManifestSkill):
db_request_args = dict(skill_id=skill_id)
db_request_args.update(asdict(manifest_skill))
def add_manifest_skill(self, manifest_skill: ManifestSkill):
db_request = self._build_db_request(
sql_file_name='add_manifest_skill.sql',
args=db_request_args
args=asdict(manifest_skill)
)
db_result = self.cursor.insert_returning(db_request)
@ -67,3 +104,22 @@ class DeviceSkillRepository(RepositoryBase):
)
)
self.cursor.delete(db_request)
def get_settings_display_usage(self, settings_display_id: str) -> int:
db_request = self._build_db_request(
sql_file_name='get_settings_display_usage.sql',
args=dict(settings_display_id=settings_display_id)
)
db_result = self.cursor.select_one(db_request)
return db_result['usage']
def remove(self, device_id, skill_id):
db_request = self._build_db_request(
sql_file_name='delete_device_skill.sql',
args=dict(
device_id=device_id,
skill_id=skill_id
)
)
self.cursor.delete(db_request)

View File

@ -0,0 +1,5 @@
DELETE FROM
device.device_skill
WHERE
device_id = %(device_id)s
AND skill_id = %(skill_id)s

View File

@ -5,6 +5,7 @@ SELECT
ds.install_method,
ds.install_status,
ds.install_ts,
ds.skill_id,
ds.update_ts,
s.skill_gid
FROM

View File

@ -0,0 +1,16 @@
SELECT
dds.skill_settings_display_id AS settings_display_id,
dds.settings::jsonb AS settings_values,
dds.install_method,
dds.skill_id,
array_agg(dds.device_id::text) AS device_ids
FROM
device.device dd
INNER JOIN device.device_skill dds ON dd.id = dds.device_id
WHERE
dd.account_id = %(account_id)s
GROUP BY
dds.skill_settings_display_id,
dds.settings::jsonb,
dds.install_method,
dds.skill_id

View File

@ -0,0 +1,15 @@
SELECT
skill_settings_display_id AS settings_display_id,
settings::jsonb AS settings_values,
install_method,
skill_id,
array_agg(device_id::text) AS device_ids
FROM
device.device_skill
WHERE
device_id = %(device_id)s
GROUP BY
skill_settings_display_id,
settings::jsonb,
install_method,
skill_id

View File

@ -0,0 +1,6 @@
SELECT
count(*) AS usage
FROM
device.device_skill
WHERE
skill_settings_display_id = %(settings_display_id)s

View File

@ -1,14 +1,10 @@
SELECT
d.name AS device_name,
ds.id,
ds.device_id,
ds.install_failure_reason,
ds.install_method,
ds.install_status,
ds.install_ts,
ds.settings,
ds.skill_id,
ds.skill_settings_display_id,
ds.update_ts,
s.skill_gid
FROM

View File

@ -0,0 +1,8 @@
UPDATE
device.device_skill
SET
skill_settings_display_id = %(settings_display_id)s,
settings = %(settings_values)s
WHERE
skill_id = %(skill_id)s
AND device_id IN %(device_ids)s

View File

@ -1,6 +1,11 @@
from .entity.display import SkillDisplay
from .entity.skill import Skill
from .entity.skill_setting import AccountSkillSetting
from .entity.skill_setting import (
AccountSkillSetting,
DeviceSkillSetting,
SettingsDisplay
)
from .repository.display import SkillDisplayRepository
from .repository.setting import SkillSettingRepository
from .repository.settings_display import SettingsDisplayRepository
from .repository.skill import SkillRepository

View File

@ -7,3 +7,17 @@ class AccountSkillSetting(object):
settings_display: dict
settings_values: dict
device_names: List[str]
@dataclass
class DeviceSkillSetting(object):
settings_display: dict
settings_values: dict
skill_id: str
@dataclass
class SettingsDisplay(object):
skill_id: str
display_data: dict
id: str = None

View File

@ -77,6 +77,6 @@ class SkillSettingRepository(RepositoryBase):
"""Return all skills and their settings for a given device id"""
return self._select_all_into_dataclass(
DeviceSkillSetting,
sql_file_name='get_skill_setting_by_device_id.sql',
sql_file_name='get_skill_setting_by_device.sql',
args=dict(device_id=device_id)
)

View File

@ -1,4 +1,7 @@
from selene.data.repository_base import RepositoryBase
import json
from ...repository_base import RepositoryBase
from ..entity.skill_setting import SettingsDisplay
class SettingsDisplayRepository(RepositoryBase):
@ -6,16 +9,33 @@ class SettingsDisplayRepository(RepositoryBase):
def __init__(self, db):
super(SettingsDisplayRepository, self).__init__(db, __file__)
def add(self, skill_id: str, settings_display: str) -> str:
def add(self, settings_display: SettingsDisplay) -> str:
db_request = self._build_db_request(
sql_file_name='get_settings_display.sql',
args=dict(skill_id=skill_id, settings_display=settings_display)
sql_file_name='add_settings_display.sql',
args=dict(
skill_id=settings_display.skill_id,
display_data=json.dumps(settings_display.display_data)
)
)
result = self.cursor.insert_returning(db_request)
return result['id']
def get_settings_display_id(self, settings_display: SettingsDisplay):
db_request = self._build_db_request(
sql_file_name='get_settings_display_id.sql',
args=dict(
skill_id=settings_display.skill_id,
display_data=json.dumps(settings_display.display_data)
)
)
result = self.cursor.select_one(db_request)
if result is None:
db_request = self._build_db_request(
sql_file_name='add_settings_display.sql',
args=dict(skill_id=skill_id, settings_display=settings_display)
)
result = self.cursor.insert_returning(db_request)
return result['id']
return None if result is None else result['id']
def remove(self, settings_display_id: str):
db_request = self._build_db_request(
sql_file_name='delete_settings_display.sql',
args=dict(settings_display_id=settings_display_id)
)
self.cursor.delete(db_request)

View File

@ -23,69 +23,6 @@ class SkillRepository(RepositoryBase):
self.db = db
super(SkillRepository, self).__init__(db, __file__)
def get_skill_settings_by_device_id(self, device_id):
"""Return all skill settings from a given device id
:param device_id: device uuid
:return list of skills using the format from the API v1"""
query = self._build_db_request(
'get_skill_setting_by_device_id.sql',
args=dict(device_id=device_id)
)
sql_results = self.cursor.select_all(query)
if sql_results:
skills = []
for result in sql_results:
sections = self._fill_setting_with_values(result['settings'], result['settings_display'])
skill = {'uuid': result['id']}
if sections:
skill['skillMetadata'] = {'sections': sections}
display = result['settings_display']
skill_gid = display.get('skill_gid')
if skill_gid:
skill['skill_gid'] = skill_gid
identifier = display.get('identifier')
if identifier:
skill['identifier'] = identifier
skills.append(skill)
return skills
def get_skill_settings_by_device_id_and_version_hash(self, device_id, version_hash):
"""Return a skill setting for a given device id and skill version hash
:param device_id: device uuid
:param version_hash: skill setting version hash
:return skill setting using the format from the API v1
"""
query = self._build_db_request(
'get_skill_setting_by_device_id_and_version_hash.sql',
args=dict(device_id=device_id, version_hash=version_hash)
)
sql_results = self.cursor.select_one(query)
if sql_results:
sections = self._fill_setting_with_values(sql_results['settings'], sql_results['settings_display'])
skill = {
'skill_gid': sql_results['settings_display']['skill_gid'],
'skillMetadata': {
'sections': sections
}
}
return skill
def _fill_setting_with_values(self, settings: dict, setting_meta: dict):
skill_metadata = setting_meta.get('skillMetadata')
if skill_metadata:
sections = skill_metadata['sections']
if settings:
for section in sections:
section_fields = section['fields']
for field in section_fields:
if 'name' in field:
name = field['name']
if name in settings:
field['value'] = settings[field['name']]
return sections
def get_skills_for_account(self, account_id) -> List[SkillFamily]:
skills = []
db_request = self._build_db_request(

View File

@ -1,6 +1,6 @@
INSERT INTO
skill.settings_display (skill_id, settings_display)
VALUES
(%(skill_id)s, %(settings_display)s)
(%(skill_id)s, %(display_data)s)
RETURNING
id
id

View File

@ -0,0 +1,4 @@
DELETE FROM
skill.settings_display
WHERE
id = %(settings_display_id)s

View File

@ -1,6 +0,0 @@
SELECT
id
FROM
skill.settings_display
WHERE
skill_id = %(skill_id)s AND settings_display = %(settings_display)s

View File

@ -0,0 +1,7 @@
SELECT
id
FROM
skill.settings_display
WHERE
skill_id = %(skill_id)s
AND settings_display = %(display_data)s

View File

@ -0,0 +1,10 @@
SELECT
ss.id AS skill_id,
dds.settings AS settings_values,
ssd.settings_display
FROM
device.device_skill dds
INNER JOIN skill.skill ss ON dds.skill_id = ss.id
INNER JOIN skill.settings_display ssd ON dds.skill_settings_display_id = ssd.id
WHERE
dds.device_id = %(device_id)s

View File

@ -1,14 +0,0 @@
SELECT
skill.id,
dev_skill.settings,
display.settings_display
FROM
device.device dev
INNER JOIN
device.device_skill dev_skill ON dev.id = dev_skill.device_id
INNER JOIN
skill.skill skill ON dev_skill.skill_id = skill.id
INNER JOIN
skill.settings_display display ON dev_skill.skill_settings_display_id = display.id
WHERE
dev.id = %(device_id)s

View File

@ -1,14 +0,0 @@
SELECT
skill.id,
dev_skill.settings,
display.settings_display
FROM
device.device dev
INNER JOIN
device.device_skill dev_skill ON dev.id = dev_skill.device_id
INNER JOIN
skill.skill skill ON dev_skill.skill_id = skill.id
INNER JOIN
skill.settings_display display ON dev_skill.skill_settings_display_id = display.id
WHERE
dev.id = %(device_id)s AND display.settings_display->>'identifier' = %(version_hash)s

View File

@ -1,25 +1,34 @@
import json
from datetime import datetime
from selene.data.device import ManifestSkill, DeviceSkillRepository
from selene.data.device import DeviceSkillRepository, ManifestSkill
def add_skill_to_manifest(db, device_id, skill):
def add_device_skill(db, device_id, skill):
manifest_skill = ManifestSkill(
device_id=device_id,
install_method='test_install_method',
install_status='test_install_status',
skill_id=skill.id,
skill_gid=skill.skill_gid,
install_ts=datetime.utcnow(),
update_ts=datetime.utcnow()
)
device_skill_repo = DeviceSkillRepository(db)
manifest_skill.id = device_skill_repo.add_manifest_skill(
skill.id,
manifest_skill
)
manifest_skill.id = device_skill_repo.add_manifest_skill(manifest_skill)
return manifest_skill
def remove_manifest_skill(db, manifest_skill):
def add_device_skill_settings(db, device_id, settings_display, settings_values):
device_skill_repo = DeviceSkillRepository(db)
device_skill_repo.update_device_skill_settings(
[device_id],
settings_display,
settings_values
)
def remove_device_skill(db, manifest_skill):
device_skill_repo = DeviceSkillRepository(db)
device_skill_repo.remove_manifest_skill(manifest_skill)

View File

@ -1,13 +1,62 @@
from selene.data.skill import Skill, SkillRepository
from selene.data.skill import (
SettingsDisplay,
SettingsDisplayRepository,
Skill,
SkillRepository
)
def add_skill(db, skill_global_id):
def build_text_field():
return dict(
name='textfield',
type='text',
label='Text Field',
placeholder='Text Placeholder'
)
def build_label_field():
return dict(
type='label',
label='This is a section label.'
)
def _build_display_data(skill_gid, fields):
gid_parts = skill_gid.split('|')
if len(gid_parts) == 3:
skill_name = gid_parts[1]
else:
skill_name = gid_parts[0]
skill_identifier = skill_name + '-123456'
settings_display = dict(
skill_gid=skill_gid,
identifier=skill_identifier,
display_name=skill_name,
)
if fields is not None:
settings_display.update(
skillMetadata=dict(
sections=[dict(name='Section Name', fields=fields)]
)
)
return settings_display
def add_skill(db, skill_global_id, settings_fields=None):
display_data = _build_display_data(skill_global_id, settings_fields)
skill_repo = SkillRepository(db)
skill_id = skill_repo.ensure_skill_exists(skill_global_id=skill_global_id)
skill_id = skill_repo.ensure_skill_exists(skill_global_id)
skill = Skill(skill_global_id, skill_id)
settings_display = SettingsDisplay(skill_id, display_data)
settings_display_repo = SettingsDisplayRepository(db)
settings_display.id = settings_display_repo.add(settings_display)
return Skill(skill_global_id, skill_id)
return skill, settings_display
def remove_skill(db, skill_global_id):
def remove_skill(db, skill):
skill_repo = SkillRepository(db)
skill_repo.remove_by_gid(skill_gid=skill_global_id)
skill_repo.remove_by_gid(skill_gid=skill.skill_gid)