forked from public/pyceo
Add group lookup functionality (#88)
note: **I am unaware of best practices** but I tried my best to keep changes consistent with the codebase feedback would be much appreciated notable changes: **new api endpoint**: `/groups/search` -- I moved searching into the api so it could be used in tui and cli, also seemed like a good idea to keep the json response as small as possible **tui searching** -- at first I wanted to make this realtime interactable, but the work required seemed inappropriate to a feature I am assuming will only be used sparingly Co-authored-by: Daniel Sun <dandancool@github.com> Co-authored-by: Daniel Sun <d6sun@uwaterloo.ca> Reviewed-on: public/pyceo#88 Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca> Co-authored-by: Daniel Sun <d6sun@csclub.uwaterloo.ca> Co-committed-by: Daniel Sun <d6sun@csclub.uwaterloo.ca>
This commit is contained in:
parent
234ab62f27
commit
010937ea17
|
@ -149,3 +149,15 @@ def delete(group_name):
|
||||||
click.confirm(f"Are you sure you want to delete {group_name}?", abort=True)
|
click.confirm(f"Are you sure you want to delete {group_name}?", abort=True)
|
||||||
resp = http_delete(f'/api/groups/{group_name}')
|
resp = http_delete(f'/api/groups/{group_name}')
|
||||||
handle_stream_response(resp, DeleteGroupTransaction.operations)
|
handle_stream_response(resp, DeleteGroupTransaction.operations)
|
||||||
|
|
||||||
|
|
||||||
|
@groups.command(short_help='Search for groups')
|
||||||
|
@click.argument('query')
|
||||||
|
@click.option('--count', default=10, help='number of results to show')
|
||||||
|
def search(query, count):
|
||||||
|
check_if_in_development()
|
||||||
|
resp = http_get(f'/api/groups/search/{query}/{count}')
|
||||||
|
result = handle_sync_response(resp)
|
||||||
|
for cn in result:
|
||||||
|
if cn != "":
|
||||||
|
click.echo(cn)
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
from ceo.utils import http_get
|
||||||
|
from .Controller import Controller
|
||||||
|
from .SyncRequestController import SyncRequestController
|
||||||
|
from ceo.tui.views import SearchGroupResponseView, GetGroupResponseView
|
||||||
|
|
||||||
|
|
||||||
|
# this is a little bit bad because it relies on zero coupling between
|
||||||
|
# the GetGroupResponseView and the GetGroupController
|
||||||
|
# coupling is also introduced between this controller and the
|
||||||
|
# SearchGroupResponseView as it requires this class's callback
|
||||||
|
class SearchGroupController(SyncRequestController):
|
||||||
|
def __init__(self, model, app):
|
||||||
|
super().__init__(model, app)
|
||||||
|
|
||||||
|
def get_resp(self):
|
||||||
|
if self.model.want_info:
|
||||||
|
return http_get(f'/api/groups/{self.model.name}')
|
||||||
|
else:
|
||||||
|
return http_get(f'/api/groups/search/{self.model.name}/{self.model.count}')
|
||||||
|
|
||||||
|
def get_response_view(self):
|
||||||
|
if self.model.want_info:
|
||||||
|
return GetGroupResponseView(self.model, self, self.app)
|
||||||
|
else:
|
||||||
|
return SearchGroupResponseView(self.model, self, self.app)
|
||||||
|
|
||||||
|
def group_info_callback(self, button, cn):
|
||||||
|
self.model.name = cn
|
||||||
|
self.model.want_info = True
|
||||||
|
self.request_in_progress = False
|
||||||
|
self.on_next_button_pressed(button)
|
||||||
|
|
||||||
|
def on_next_button_pressed(self, button):
|
||||||
|
try:
|
||||||
|
if not self.model.want_info:
|
||||||
|
self.model.name = self.get_username_from_view()
|
||||||
|
self.model.count = 10
|
||||||
|
except Controller.InvalidInput:
|
||||||
|
return
|
||||||
|
self.on_confirmation_button_pressed(button)
|
|
@ -8,6 +8,7 @@ from .ResetPasswordController import ResetPasswordController
|
||||||
from .ChangeLoginShellController import ChangeLoginShellController
|
from .ChangeLoginShellController import ChangeLoginShellController
|
||||||
from .AddGroupController import AddGroupController
|
from .AddGroupController import AddGroupController
|
||||||
from .GetGroupController import GetGroupController
|
from .GetGroupController import GetGroupController
|
||||||
|
from .SearchGroupController import SearchGroupController
|
||||||
from .AddMemberToGroupController import AddMemberToGroupController
|
from .AddMemberToGroupController import AddMemberToGroupController
|
||||||
from .RemoveMemberFromGroupController import RemoveMemberFromGroupController
|
from .RemoveMemberFromGroupController import RemoveMemberFromGroupController
|
||||||
from .CreateDatabaseController import CreateDatabaseController
|
from .CreateDatabaseController import CreateDatabaseController
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
class SearchGroupModel:
|
||||||
|
name = 'SearchGroup'
|
||||||
|
title = 'Search groups'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ''
|
||||||
|
self.resp_json = None
|
||||||
|
self.count = 10
|
||||||
|
self.want_info = False
|
|
@ -5,6 +5,7 @@ from .ResetPasswordModel import ResetPasswordModel
|
||||||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||||
from .AddGroupModel import AddGroupModel
|
from .AddGroupModel import AddGroupModel
|
||||||
from .GetGroupModel import GetGroupModel
|
from .GetGroupModel import GetGroupModel
|
||||||
|
from .SearchGroupModel import SearchGroupModel
|
||||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||||
from .CreateDatabaseModel import CreateDatabaseModel
|
from .CreateDatabaseModel import CreateDatabaseModel
|
||||||
|
@ -29,6 +30,7 @@ class WelcomeModel:
|
||||||
'Groups': [
|
'Groups': [
|
||||||
AddGroupModel,
|
AddGroupModel,
|
||||||
GetGroupModel,
|
GetGroupModel,
|
||||||
|
SearchGroupModel,
|
||||||
AddMemberToGroupModel,
|
AddMemberToGroupModel,
|
||||||
RemoveMemberFromGroupModel,
|
RemoveMemberFromGroupModel,
|
||||||
],
|
],
|
||||||
|
|
|
@ -6,6 +6,7 @@ from .ResetPasswordModel import ResetPasswordModel
|
||||||
from .ChangeLoginShellModel import ChangeLoginShellModel
|
from .ChangeLoginShellModel import ChangeLoginShellModel
|
||||||
from .AddGroupModel import AddGroupModel
|
from .AddGroupModel import AddGroupModel
|
||||||
from .GetGroupModel import GetGroupModel
|
from .GetGroupModel import GetGroupModel
|
||||||
|
from .SearchGroupModel import SearchGroupModel
|
||||||
from .AddMemberToGroupModel import AddMemberToGroupModel
|
from .AddMemberToGroupModel import AddMemberToGroupModel
|
||||||
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
from .RemoveMemberFromGroupModel import RemoveMemberFromGroupModel
|
||||||
from .CreateDatabaseModel import CreateDatabaseModel
|
from .CreateDatabaseModel import CreateDatabaseModel
|
||||||
|
|
|
@ -25,6 +25,7 @@ def handle_sync_response(resp, controller):
|
||||||
raise Controller.RequestFailed()
|
raise Controller.RequestFailed()
|
||||||
|
|
||||||
|
|
||||||
|
# this can probably be simplified with getattr or something
|
||||||
def get_mvc(app, name):
|
def get_mvc(app, name):
|
||||||
if name == WelcomeModel.name:
|
if name == WelcomeModel.name:
|
||||||
model = WelcomeModel()
|
model = WelcomeModel()
|
||||||
|
@ -58,6 +59,10 @@ def get_mvc(app, name):
|
||||||
model = GetGroupModel()
|
model = GetGroupModel()
|
||||||
controller = GetGroupController(model, app)
|
controller = GetGroupController(model, app)
|
||||||
view = GetGroupView(model, controller, app)
|
view = GetGroupView(model, controller, app)
|
||||||
|
elif name == SearchGroupModel.name:
|
||||||
|
model = SearchGroupModel()
|
||||||
|
controller = SearchGroupController(model, app)
|
||||||
|
view = SearchGroupView(model, controller, app)
|
||||||
elif name == AddMemberToGroupModel.name:
|
elif name == AddMemberToGroupModel.name:
|
||||||
model = AddMemberToGroupModel()
|
model = AddMemberToGroupModel()
|
||||||
controller = AddMemberToGroupController(model, app)
|
controller = AddMemberToGroupController(model, app)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
from .ColumnResponseView import ColumnResponseView
|
||||||
|
from .utils import decorate_button
|
||||||
|
|
||||||
|
|
||||||
|
class SearchGroupResponseView(ColumnResponseView):
|
||||||
|
def __init__(self, model, controller, app):
|
||||||
|
super().__init__(model, controller, app)
|
||||||
|
matches = self.model.resp_json.copy()
|
||||||
|
|
||||||
|
rows = [(urwid.Text(resp),
|
||||||
|
decorate_button(urwid.Button('more info', on_press=self.create_callback(resp))))
|
||||||
|
for resp in matches if resp != '']
|
||||||
|
|
||||||
|
self.set_rows(rows, on_next=self.controller.get_next_menu_callback('Welcome'))
|
||||||
|
|
||||||
|
def create_callback(self, cn):
|
||||||
|
def callback(button):
|
||||||
|
self.controller.group_info_callback(button, cn)
|
||||||
|
return callback
|
|
@ -0,0 +1,17 @@
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
from .ColumnView import ColumnView
|
||||||
|
|
||||||
|
|
||||||
|
class SearchGroupView(ColumnView):
|
||||||
|
def __init__(self, model, controller, app):
|
||||||
|
super().__init__(model, controller, app)
|
||||||
|
# used for query, consider combining Controller.user_name_from_view and Controller.get_group_name_from_view
|
||||||
|
self.username_edit = urwid.Edit()
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
urwid.Text('Query:', align='right'),
|
||||||
|
self.username_edit
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.set_rows(rows)
|
|
@ -16,6 +16,8 @@ from .AddGroupView import AddGroupView
|
||||||
from .AddGroupConfirmationView import AddGroupConfirmationView
|
from .AddGroupConfirmationView import AddGroupConfirmationView
|
||||||
from .GetGroupView import GetGroupView
|
from .GetGroupView import GetGroupView
|
||||||
from .GetGroupResponseView import GetGroupResponseView
|
from .GetGroupResponseView import GetGroupResponseView
|
||||||
|
from .SearchGroupView import SearchGroupView
|
||||||
|
from .SearchGroupResponseView import SearchGroupResponseView
|
||||||
from .AddMemberToGroupView import AddMemberToGroupView
|
from .AddMemberToGroupView import AddMemberToGroupView
|
||||||
from .AddMemberToGroupConfirmationView import AddMemberToGroupConfirmationView
|
from .AddMemberToGroupConfirmationView import AddMemberToGroupConfirmationView
|
||||||
from .RemoveMemberFromGroupView import RemoveMemberFromGroupView
|
from .RemoveMemberFromGroupView import RemoveMemberFromGroupView
|
||||||
|
|
|
@ -1,6 +1,52 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class fuzzy_result:
|
||||||
|
def __init__(self, string, score):
|
||||||
|
self.string = string
|
||||||
|
self.score = score
|
||||||
|
|
||||||
|
# consider a score worse if the edit distance is larger
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.score > other.score
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self.score < other.score
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self.score >= other.score
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.score <= other.score
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.score == other.score
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return self.score != other.score
|
||||||
|
|
||||||
|
|
||||||
|
# compute levenshtein edit distance, adapted from rosetta code
|
||||||
|
def fuzzy_match(s1, s2):
|
||||||
|
if len(s1) == 0:
|
||||||
|
return len(s2)
|
||||||
|
if len(s2) == 0:
|
||||||
|
return len(s1)
|
||||||
|
edits = [i for i in range(len(s2) + 1)]
|
||||||
|
for i in range(len(s1)):
|
||||||
|
corner = i
|
||||||
|
edits[0] = i + 1
|
||||||
|
for j in range(len(s2)):
|
||||||
|
upper = edits[j + 1]
|
||||||
|
if s1[i] == s2[j]:
|
||||||
|
edits[j + 1] = corner
|
||||||
|
else:
|
||||||
|
m = min(corner, upper, edits[j])
|
||||||
|
edits[j + 1] = m + 1
|
||||||
|
corner = upper
|
||||||
|
return edits[-1]
|
||||||
|
|
||||||
|
|
||||||
def get_current_datetime() -> datetime.datetime:
|
def get_current_datetime() -> datetime.datetime:
|
||||||
# We place this in a separate function so that we can mock it out
|
# We place this in a separate function so that we can mock it out
|
||||||
# in our unit tests.
|
# in our unit tests.
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
|
from flask.json import jsonify
|
||||||
from zope import component
|
from zope import component
|
||||||
|
|
||||||
from .utils import authz_restrict_to_syscom, is_truthy, \
|
from .utils import authz_restrict_to_syscom, is_truthy, \
|
||||||
create_streaming_response, development_only
|
create_streaming_response, development_only
|
||||||
from ceo_common.interfaces import ILDAPService
|
from ceo_common.interfaces import ILDAPService
|
||||||
|
from ceo_common.utils import fuzzy_result, fuzzy_match
|
||||||
from ceod.transactions.groups import (
|
from ceod.transactions.groups import (
|
||||||
AddGroupTransaction,
|
AddGroupTransaction,
|
||||||
AddMemberToGroupTransaction,
|
AddMemberToGroupTransaction,
|
||||||
|
@ -11,6 +13,8 @@ from ceod.transactions.groups import (
|
||||||
DeleteGroupTransaction,
|
DeleteGroupTransaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from heapq import heappushpop, nlargest
|
||||||
|
|
||||||
bp = Blueprint('groups', __name__)
|
bp = Blueprint('groups', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +36,22 @@ def get_group(group_name):
|
||||||
return group.to_dict()
|
return group.to_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/search/<query>/<count>')
|
||||||
|
def search_group(query, count):
|
||||||
|
query = str(query)
|
||||||
|
count = int(count)
|
||||||
|
ldap_srv = component.getUtility(ILDAPService)
|
||||||
|
clubs = ldap_srv.get_clubs()
|
||||||
|
scores = [fuzzy_result("", 99999) for _ in range(count)]
|
||||||
|
for club in clubs:
|
||||||
|
score = fuzzy_match(query, str(club.cn))
|
||||||
|
result = fuzzy_result(str(club.cn), score)
|
||||||
|
heappushpop(scores, result)
|
||||||
|
|
||||||
|
result = [score.string for score in nlargest(count, scores)]
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<group_name>/members/<username>', methods=['POST'])
|
@bp.route('/<group_name>/members/<username>', methods=['POST'])
|
||||||
@authz_restrict_to_syscom
|
@authz_restrict_to_syscom
|
||||||
def add_member_to_group(group_name, username):
|
def add_member_to_group(group_name, username):
|
||||||
|
|
|
@ -360,7 +360,11 @@ class LDAPService:
|
||||||
return users_to_change
|
return users_to_change
|
||||||
|
|
||||||
def _get_club_uids(self, conn: ldap3.Connection) -> List[str]:
|
def _get_club_uids(self, conn: ldap3.Connection) -> List[str]:
|
||||||
conn.search(self.ldap_users_base, '(objectClass=club)', attributes=['uid'])
|
conn.extend.standard.paged_search(self.ldap_users_base,
|
||||||
|
'(objectClass=club)',
|
||||||
|
attributes=['uid'],
|
||||||
|
paged_size=50,
|
||||||
|
generator=False)
|
||||||
return [entry.uid.value for entry in conn.entries]
|
return [entry.uid.value for entry in conn.entries]
|
||||||
|
|
||||||
def get_clubs(self) -> List[IGroup]:
|
def get_clubs(self) -> List[IGroup]:
|
||||||
|
|
|
@ -284,6 +284,26 @@ paths:
|
||||||
$ref: "#/components/schemas/UID"
|
$ref: "#/components/schemas/UID"
|
||||||
"404":
|
"404":
|
||||||
$ref: "#/components/responses/GroupNotFoundErrorResponse"
|
$ref: "#/components/responses/GroupNotFoundErrorResponse"
|
||||||
|
/groups/{query}/{count}:
|
||||||
|
get:
|
||||||
|
tags: ['groups']
|
||||||
|
summary: fuzzy search groups
|
||||||
|
description: >-
|
||||||
|
search count number of groups, returns a list of names sorted by levenshtein edit
|
||||||
|
distance.
|
||||||
|
parameters:
|
||||||
|
- name: query
|
||||||
|
in: path
|
||||||
|
description: query or string to search for
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: count
|
||||||
|
in: path
|
||||||
|
description: number of results to return, returns empty strings if necessary
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: int
|
||||||
/groups/{group_name}/members/{username}:
|
/groups/{group_name}/members/{username}:
|
||||||
post:
|
post:
|
||||||
tags: ['groups']
|
tags: ['groups']
|
||||||
|
|
|
@ -77,6 +77,44 @@ def test_groups(cli_setup, ldap_user):
|
||||||
result = runner.invoke(cli, ['groups', 'delete', 'test_group_1'], input='y\n')
|
result = runner.invoke(cli, ['groups', 'delete', 'test_group_1'], input='y\n')
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
group_names = [
|
||||||
|
"touch",
|
||||||
|
"error",
|
||||||
|
"happy",
|
||||||
|
"moon",
|
||||||
|
"decisive",
|
||||||
|
"exciting",
|
||||||
|
"super",
|
||||||
|
"ambitious",
|
||||||
|
"acidic",
|
||||||
|
"addition",
|
||||||
|
"blue-eyed",
|
||||||
|
"grate",
|
||||||
|
"replace",
|
||||||
|
"natural",
|
||||||
|
"explode",
|
||||||
|
"decorous",
|
||||||
|
"wide",
|
||||||
|
"hang",
|
||||||
|
"tomatoes",
|
||||||
|
"thirsty",
|
||||||
|
]
|
||||||
|
runner = CliRunner()
|
||||||
|
for name in group_names:
|
||||||
|
result = runner.invoke(cli, [
|
||||||
|
'groups', 'add', name, '-d', 'searchable group',
|
||||||
|
], input='y\n')
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
for name in group_names:
|
||||||
|
result = runner.invoke(cli, ['groups', 'search', '--count=1', name])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.find(name) != -1
|
||||||
|
|
||||||
|
for name in group_names:
|
||||||
|
result = runner.invoke(cli, ['groups', 'delete', name], input='y\n')
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
def create_group(group_name, desc):
|
def create_group(group_name, desc):
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
|
@ -111,6 +111,206 @@ def test_api_add_member_to_group(client, create_group_result, ldap_user):
|
||||||
assert data['members'] == []
|
assert data['members'] == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def create_random_names():
|
||||||
|
# 150 random names generated with https://www.randomlists.com/random-words
|
||||||
|
random_names = [
|
||||||
|
"intelligent",
|
||||||
|
"skin",
|
||||||
|
"shivering",
|
||||||
|
"hapless",
|
||||||
|
"abstracted",
|
||||||
|
"kiss",
|
||||||
|
"decision",
|
||||||
|
"van",
|
||||||
|
"advise",
|
||||||
|
"parcel",
|
||||||
|
"disillusioned",
|
||||||
|
"print",
|
||||||
|
"skate",
|
||||||
|
"robin",
|
||||||
|
"explode",
|
||||||
|
"fearless",
|
||||||
|
"feeling",
|
||||||
|
"chemical",
|
||||||
|
"identify",
|
||||||
|
"baseball",
|
||||||
|
"room",
|
||||||
|
"contain",
|
||||||
|
"smooth",
|
||||||
|
"play",
|
||||||
|
"fierce",
|
||||||
|
"north",
|
||||||
|
"secretive",
|
||||||
|
"plug",
|
||||||
|
"rely",
|
||||||
|
"home",
|
||||||
|
"push",
|
||||||
|
"guard",
|
||||||
|
"allow",
|
||||||
|
"depressed",
|
||||||
|
"evasive",
|
||||||
|
"slap",
|
||||||
|
"delicate",
|
||||||
|
"concern",
|
||||||
|
"consider",
|
||||||
|
"fang",
|
||||||
|
"roll",
|
||||||
|
"bait",
|
||||||
|
"rabbits",
|
||||||
|
"guarded",
|
||||||
|
"abnormal",
|
||||||
|
"loutish",
|
||||||
|
"voracious",
|
||||||
|
"chase",
|
||||||
|
"army",
|
||||||
|
"harsh",
|
||||||
|
"grieving",
|
||||||
|
"tacky",
|
||||||
|
"far",
|
||||||
|
"wise",
|
||||||
|
"street",
|
||||||
|
"price",
|
||||||
|
"bikes",
|
||||||
|
"post",
|
||||||
|
"afternoon",
|
||||||
|
"deranged",
|
||||||
|
"cart",
|
||||||
|
"evanescent",
|
||||||
|
"shrill",
|
||||||
|
"uppity",
|
||||||
|
"adhoc",
|
||||||
|
"alleged",
|
||||||
|
"round",
|
||||||
|
"smart",
|
||||||
|
"support",
|
||||||
|
"plantation",
|
||||||
|
"flap",
|
||||||
|
"pretty",
|
||||||
|
"radiate",
|
||||||
|
"excite",
|
||||||
|
"memorize",
|
||||||
|
"whisper",
|
||||||
|
"thoughtless",
|
||||||
|
"substantial",
|
||||||
|
"upset",
|
||||||
|
"pathetic",
|
||||||
|
"flow",
|
||||||
|
"shake",
|
||||||
|
"wail",
|
||||||
|
"share",
|
||||||
|
"songs",
|
||||||
|
"scream",
|
||||||
|
"aspiring",
|
||||||
|
"overwrought",
|
||||||
|
"mass",
|
||||||
|
"romantic",
|
||||||
|
"deliver",
|
||||||
|
"anxious",
|
||||||
|
"laborer",
|
||||||
|
"angry",
|
||||||
|
"faded",
|
||||||
|
"wish",
|
||||||
|
"homeless",
|
||||||
|
"salty",
|
||||||
|
"start",
|
||||||
|
"crooked",
|
||||||
|
"tremble",
|
||||||
|
"enjoy",
|
||||||
|
"chivalrous",
|
||||||
|
"useless",
|
||||||
|
"womanly",
|
||||||
|
"brake",
|
||||||
|
"wandering",
|
||||||
|
"please",
|
||||||
|
"cow",
|
||||||
|
"reason",
|
||||||
|
"expert",
|
||||||
|
"null",
|
||||||
|
"basket",
|
||||||
|
"early",
|
||||||
|
"river",
|
||||||
|
"prevent",
|
||||||
|
"sticks",
|
||||||
|
"vacation",
|
||||||
|
"eggnog",
|
||||||
|
"receive",
|
||||||
|
"memory",
|
||||||
|
"exchange",
|
||||||
|
"burly",
|
||||||
|
"agreement",
|
||||||
|
"flock",
|
||||||
|
"subdued",
|
||||||
|
"clap",
|
||||||
|
"simplistic",
|
||||||
|
"tiger",
|
||||||
|
"responsible",
|
||||||
|
"knock",
|
||||||
|
"camera",
|
||||||
|
"nifty",
|
||||||
|
"capable",
|
||||||
|
"disappear",
|
||||||
|
"afterthought",
|
||||||
|
"obese",
|
||||||
|
"harass",
|
||||||
|
"delicious",
|
||||||
|
"badge",
|
||||||
|
"dam",
|
||||||
|
"plate",
|
||||||
|
"acrid",
|
||||||
|
"voiceless",
|
||||||
|
"mate",
|
||||||
|
"juice",
|
||||||
|
"food",
|
||||||
|
"town",
|
||||||
|
"giraffe",
|
||||||
|
"decorate"
|
||||||
|
]
|
||||||
|
yield random_names
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def create_searchable_groups(client, create_random_names):
|
||||||
|
random_names = create_random_names
|
||||||
|
for name in random_names:
|
||||||
|
status, data = client.post('/api/groups', json={
|
||||||
|
'cn': name,
|
||||||
|
'description': 'Groups with distinct names for testing searching',
|
||||||
|
})
|
||||||
|
assert status == 200
|
||||||
|
assert data[-1]['status'] == 'completed'
|
||||||
|
yield random_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_group_search(client, create_searchable_groups):
|
||||||
|
cns = create_searchable_groups
|
||||||
|
# pairs of cn indices as well as amount of results that should be returned
|
||||||
|
random_numbers = [
|
||||||
|
(88, 68),
|
||||||
|
(117, 54),
|
||||||
|
(63, 97),
|
||||||
|
(64, 19),
|
||||||
|
(114, 98),
|
||||||
|
(45, 146),
|
||||||
|
(58, 12),
|
||||||
|
(42, 126),
|
||||||
|
(66, 137),
|
||||||
|
(39, 135),
|
||||||
|
]
|
||||||
|
|
||||||
|
for tup in random_numbers:
|
||||||
|
cn = cns[tup[0]]
|
||||||
|
status, data = client.get(f'/api/groups/search/{cn}/{tup[1]}')
|
||||||
|
assert status == 200
|
||||||
|
assert len(data) == tup[1]
|
||||||
|
assert data[0] == cn
|
||||||
|
|
||||||
|
for cn in cns:
|
||||||
|
status, data = client.delete(f'/api/groups/{cn}')
|
||||||
|
assert status == 200
|
||||||
|
assert data[-1]['status'] == 'completed'
|
||||||
|
|
||||||
|
|
||||||
def test_api_group_auxiliary(cfg, client, ldap_user, g_admin_ctx):
|
def test_api_group_auxiliary(cfg, client, ldap_user, g_admin_ctx):
|
||||||
# Make sure that syscom has auxiliary mailing lists and groups
|
# Make sure that syscom has auxiliary mailing lists and groups
|
||||||
# defined in ceod_test_local.ini.
|
# defined in ceod_test_local.ini.
|
||||||
|
|
Loading…
Reference in New Issue