Proposal for extra field support in API
toras
toras9000 at gmail.com
Mon Aug 7 11:49:01 UTC 2023
Hi.
This is a proposal to add extra field support in the API.
I am not sure if this fits the project's policy, as the content was changed for personal purposes.
If it doesn't fit, you can reject it.
There are two changes.
01-api update extra field by update_repo.patch
This is a change to accept updates to extra fields at the updat_repo endpoint.
I recently found that the model already supports updates.
However, the API interface was not accepted, so this is to add parameters for it.
A point for a little consideration is the method of specifying key names for extra fields.
For example, the get_repo API uses a flat hierarchy of properties with an 'ex_' prefix to retrieve information.
This proposed change is not symmetrical with it. However, I believe this format is more convenient.
02-api add endpoint for extra fields.patch
It adds endpoints for retrieving, creating, and deleting extra field definitions in the repository.
I am not sure if the way db is referenced here, etc., is appropriate for the policy.
In both of the above, I included methods for testing.
However, we do not know if this way of creating the test is also appropriate.
Thanks
--
toras9000
-------------- next part --------------
# HG changeset patch
# User toras9000 <toras9000 at example.com>
# Date 1691408559 -32400
# Mon Aug 07 20:42:39 2023 +0900
# Branch stable
# Node ID 6f59d9f02044092bac1993879dcea4eece45d049
# Parent 36a36ebdf4bbc4da77c41cabdbdf4a688e8fbeea
api: update extra field by update_repo
diff --git a/kallithea/controllers/api/api.py b/kallithea/controllers/api/api.py
--- a/kallithea/controllers/api/api.py
+++ b/kallithea/controllers/api/api.py
@@ -1017,7 +1017,8 @@
description=None, private=None,
clone_uri=None, landing_rev=None,
enable_statistics=None,
- enable_downloads=None):
+ enable_downloads=None,
+ extra_fields=None):
"""
Updates repo
"""
@@ -1037,6 +1038,11 @@
'Only Kallithea admin can specify `owner` param'
)
+ if extra_fields is not None:
+ ex_field_setting = db.Setting.get_by_name('repository_fields')
+ if (ex_field_setting is None) or (not ex_field_setting.app_settings_value):
+ raise JSONRPCError('Extra field setting is disabled.')
+
updates = {}
repo_group = group
if repo_group is not None:
@@ -1056,6 +1062,11 @@
store_update(updates, enable_statistics, 'repo_enable_statistics')
store_update(updates, enable_downloads, 'repo_enable_downloads')
+ if isinstance(extra_fields, dict):
+ for key, val in extra_fields.items():
+ if (key is not None) and (val is not None):
+ store_update(updates, val, db.RepositoryField.PREFIX + key)
+
RepoModel().update(repo, **updates)
meta.Session().commit()
return dict(
diff --git a/kallithea/tests/api/api_base.py b/kallithea/tests/api/api_base.py
--- a/kallithea/tests/api/api_base.py
+++ b/kallithea/tests/api/api_base.py
@@ -1243,6 +1243,72 @@
finally:
fixture.destroy_repo(repo_name)
+ def test_api_update_repo_extra_field_change(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1')
+ fixture.create_repo_extra_field(repo, field_key='testkey2', field_value='testval2')
+
+ expected = {
+ 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
+ 'repository': repo.get_api_data()
+ }
+ expected['repository']['ex_testkey1'] = 'testval1'
+ expected['repository']['ex_testkey2'] = 'changeval'
+
+ updates = { 'extra_fields': { 'testkey2': 'changeval', }, }
+ id_, params = _build_data(self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
+ response = api_call(self, params)
+
+ self._compare_ok(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_update_repo_extra_field_missing(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1')
+ fixture.create_repo_extra_field(repo, field_key='testkey2', field_value='testval2')
+
+ expected = {
+ 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
+ 'repository': repo.get_api_data()
+ }
+ expected['repository']['ex_testkey1'] = 'testval1'
+ expected['repository']['ex_testkey2'] = 'testval2'
+
+ updates = { 'extra_fields': { 'testkey3': 'otherval', }, }
+ id_, params = _build_data(self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
+ response = api_call(self, params)
+
+ self._compare_ok(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_update_repo_extra_field_disabled(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', False, 'bool') # extra_fields disabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1')
+ fixture.create_repo_extra_field(repo, field_key='testkey2', field_value='testval2')
+
+ updates = { 'extra_fields': { 'testkey2': 'changeval', }, }
+ id_, params = _build_data(self.apikey_regular, 'update_repo', repoid=repo_name, **updates)
+ response = api_call(self, params)
+
+ expected = 'Extra field setting is disabled.'
+ self._compare_error(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
def test_api_delete_repo(self):
repo_name = 'api_delete_me'
fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
diff --git a/kallithea/tests/fixture.py b/kallithea/tests/fixture.py
--- a/kallithea/tests/fixture.py
+++ b/kallithea/tests/fixture.py
@@ -192,6 +192,19 @@
RepoModel().delete(repo_name, **kwargs)
meta.Session().commit()
+ def create_repo_extra_field(self, repo, field_key, field_value, **kwargs):
+ field = db.RepositoryField()
+ field.repository = repo
+ field.field_type = 'str'
+ field.field_key = field_key
+ field.field_value = field_value
+ field.field_label = kwargs.get('field_label', '')
+ field.field_desc = kwargs.get('field_desc', '')
+ meta.Session().add(field)
+ meta.Session().commit()
+
+ return field
+
def create_repo_group(self, name, parent_group_id=None, cur_user=TEST_USER_ADMIN_LOGIN, **kwargs):
assert '/' not in name, (name, kwargs) # use group_parent_id to make nested groups
if 'skip_if_exists' in kwargs:
-------------- next part --------------
# HG changeset patch
# User toras9000 <toras9000 at example.com>
# Date 1691408559 -32400
# Mon Aug 07 20:42:39 2023 +0900
# Branch stable
# Node ID 20f782d5b6ce2e2228151d99bcb45762ac6f188a
# Parent 6f59d9f02044092bac1993879dcea4eece45d049
api: add endpoint for extra fields
diff --git a/kallithea/controllers/api/api.py b/kallithea/controllers/api/api.py
--- a/kallithea/controllers/api/api.py
+++ b/kallithea/controllers/api/api.py
@@ -1995,3 +1995,126 @@
# NOTE: no explicit check that removed reviewers were actually present.
'removed': [x.username for x in remove_objs],
}
+
+ # permission check inside
+ def get_repo_extra_fields(self, repoid):
+ """
+ OUTPUT::
+
+ id : <id_given_in_input>
+ result : [
+ {
+ "key" : "<field_key>",
+ "value" : "<field_value>",
+ "label" : "<field_label>",
+ "desc" : "<field_desc>",
+ "type" : "<field_type>"
+ },
+ …
+ ]
+ error : null
+ """
+ repo = get_repo_or_error(repoid)
+ if not HasRepoPermissionLevel('admin')(repo.repo_name):
+ raise JSONRPCError('Access denied to repo `%s`' % repo.repo_name)
+
+ # Even if the extra field setting is disabled, reading may still be allowed.
+
+ def field_api_data(field):
+ return {
+ 'key': db.RepositoryField.un_prefix_key(field.field_key),
+ 'value': field.field_value,
+ 'label': field.field_label,
+ 'desc': field.field_desc,
+ 'type': field.field_type,
+ }
+
+ data = [field_api_data(field) for field in repo.extra_fields]
+
+ return data
+
+ # permission check inside
+ def create_repo_extra_field(self, repoid, field_key, field_label=None, field_desc=None, field_value=None):
+ """
+ OUTPUT::
+
+ id : <id_given_in_input>
+ result : {
+ "msg": "created extra field key:<field_key>",
+ "extra_field": {
+ "key" : "<field_key>",
+ "value" : "<field_value>",
+ "label" : "<field_label>",
+ "desc" : "<field_desc>",
+ "type" : "<field_type>"
+ }
+ }
+ """
+ repo = get_repo_or_error(repoid)
+ if not HasRepoPermissionLevel('admin')(repo.repo_name):
+ raise JSONRPCError('Access denied to repo `%s`' % repo.repo_name)
+
+ ex_field_setting = db.Setting.get_by_name('repository_fields')
+ if (ex_field_setting is None) or (not ex_field_setting.app_settings_value):
+ raise JSONRPCError('Extra field setting is disabled.')
+
+ try:
+ field = db.RepositoryField()
+ field.repository = repo
+ field.field_type = 'str'
+ field.field_key = field_key
+ field.field_desc = field_desc if field_desc is not None else ''
+ field.field_label = field_label if field_label is not None else ''
+ field.field_value = field_value if field_value is not None else ''
+ meta.Session().add(field)
+ meta.Session().commit()
+
+ return {
+ 'msg': 'created extra field key:%s' % field_key,
+ 'extra_field':
+ {
+ 'type': field.field_type,
+ 'key': db.RepositoryField.un_prefix_key(field.field_key),
+ 'label': field.field_label,
+ 'desc': field.field_desc,
+ 'value': field.field_value,
+ },
+ }
+
+ except Exception:
+ log.error(traceback.format_exc())
+ raise JSONRPCError('failed to create extra field key:%s' % field_key)
+
+ # permission check inside
+ def delete_repo_extra_field(self, repoid, field_key):
+ """
+ OUTPUT::
+
+ id : <id_given_in_input>
+ result : {
+ "msg": "created extra field key:<field_key>"
+ }
+ """
+ repo = get_repo_or_error(repoid)
+ if not HasRepoPermissionLevel('admin')(repo.repo_name):
+ raise JSONRPCError('Access denied to repo `%s`' % repo.repo_name)
+
+ ex_field_setting = db.Setting.get_by_name('repository_fields')
+ if (ex_field_setting is None) or (not ex_field_setting.app_settings_value):
+ raise JSONRPCError('Extra field setting is disabled.')
+
+ field = db.RepositoryField.get_by_key_name(field_key, repo)
+ if field is None:
+ raise JSONRPCError('Extra field `%s` does not exist' % field_key)
+
+ try:
+ meta.Session().delete(field)
+ meta.Session().commit()
+
+ return {
+ 'msg': 'deleted extra field key:%s' % field_key,
+ }
+
+ except Exception:
+ log.error(traceback.format_exc())
+ raise JSONRPCError('failed to delete extra field key:%s' % field_ke)
diff --git a/kallithea/tests/api/api_base.py b/kallithea/tests/api/api_base.py
--- a/kallithea/tests/api/api_base.py
+++ b/kallithea/tests/api/api_base.py
@@ -3033,3 +3033,134 @@
self._compare_error(random_id, "Invalid request. Neither 'add' nor 'remove' is specified.", given=response.body)
assert ext_json.loads(response.body)['result'] is None
+
+ def test_api_get_repo_extra_fields(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1',
+ field_label='testlabel1', field_desc='testdesc1')
+ fixture.create_repo_extra_field(repo, field_key='testkey2', field_value='testval2',
+ field_label='testlabel2', field_desc='testdesc2')
+
+ id_, params = _build_data(self.apikey_regular, 'get_repo_extra_fields', repoid=repo_name)
+ response = api_call(self, params)
+
+ expected = [
+ { 'key': 'testkey1', 'value': 'testval1', 'label': 'testlabel1', 'desc': 'testdesc1', 'type': 'str', },
+ { 'key': 'testkey2', 'value': 'testval2', 'label': 'testlabel2', 'desc': 'testdesc2', 'type': 'str', },
+ ]
+
+ self._compare_ok(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_create_repo_extra_field(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+
+ field_key = 'testkey1'
+ assert db.RepositoryField.get_by_key_name(field_key, repo) is None
+
+ args = { 'field_key': field_key, 'field_label': 'testlabel1', 'field_desc': 'testdesc1', 'field_value': 'testval1', }
+ id_, params = _build_data(self.apikey_regular, 'create_repo_extra_field', repoid=repo_name, **args)
+ response = api_call(self, params)
+
+ expected = {
+ 'msg': 'created extra field key:%s' % field_key,
+ 'extra_field': { 'type': 'str', 'key': field_key, 'label': 'testlabel1', 'desc': 'testdesc1', 'value': 'testval1', },
+ }
+ self._compare_ok(id_, expected, given=response.body)
+
+ field = db.RepositoryField.get_by_key_name(field_key, repo)
+ assert field is not None
+ assert field.field_type == 'str'
+ assert field.field_key == field_key
+ assert field.field_label == 'testlabel1'
+ assert field.field_desc == 'testdesc1'
+ assert field.field_value == 'testval1'
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_create_repo_extra_field_minimum(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+
+ field_key = 'testkey1'
+ args = { 'repoid': repo_name, 'field_key': field_key, }
+ id_, params = _build_data(self.apikey_regular, 'create_repo_extra_field', **args)
+ response = api_call(self, params)
+
+ expected = {
+ 'msg': 'created extra field key:%s' % field_key,
+ 'extra_field': { 'type': 'str', 'key': field_key, 'label': '', 'desc': '', 'value': '', },
+ }
+ self._compare_ok(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_create_repo_extra_field_dpplicate(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1')
+
+ field_key = 'testkey1'
+ args = { 'repoid': repo_name, 'field_key': field_key, }
+ id_, params = _build_data(self.apikey_regular, 'create_repo_extra_field', **args)
+ response = api_call(self, params)
+
+ expected = 'failed to create extra field key:%s' % field_key
+ self._compare_error(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_create_repo_extra_field_disabled(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', False, 'bool') # extra_fields disabled
+
+ field_key = 'testkey'
+ args = { 'repoid': repo_name, 'field_key': field_key, }
+ id_, params = _build_data(self.apikey_regular, 'create_repo_extra_field', **args)
+ response = api_call(self, params)
+
+ expected = 'Extra field setting is disabled.'
+ self._compare_error(id_, expected, given=response.body)
+ finally:
+ fixture.destroy_repo(repo_name)
+
+ def test_api_delete_repo_extra_field(self):
+ repo_name = 'admin_owned'
+ repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
+ try:
+ RepoModel().grant_user_permission(repo=repo_name, user=self.TEST_USER_LOGIN, perm='repository.admin')
+ db.Setting.create_or_update('repository_fields', True, 'bool') # extra_fields enabled
+ fixture.create_repo_extra_field(repo, field_key='testkey1', field_value='testval1')
+ fixture.create_repo_extra_field(repo, field_key='testkey2', field_value='testval2')
+
+ args = { 'repoid': repo_name, 'field_key': 'testkey2', }
+ id_, params = _build_data(self.apikey_regular, 'delete_repo_extra_field', **args)
+ response = api_call(self, params)
+
+ expected = {
+ 'msg': 'deleted extra field key:testkey2',
+ }
+ self._compare_ok(id_, expected, given=response.body)
+
+ assert db.RepositoryField.get_by_key_name('testkey2', repo) is None
+ assert db.RepositoryField.get_by_key_name('testkey1', repo) is not None
+ finally:
+ fixture.destroy_repo(repo_name)
More information about the kallithea-general
mailing list