streams: refactor stream creation code path.

Refactor list_to_streams and create_streams_if_needed to take a list
of dictionaries, instead of a list of stream names.  This is
preparation for being able to pass additional arguments into the
stream creation process.

An important note: This removes a set of validation code from the
start of add_subscriptions_backend; doing so is correct because
list_to_streams has that same validation code already.

[with some tweaks by tabbott for clarity]
This commit is contained in:
Calvin Lee
2016-11-20 15:55:50 -05:00
committed by Tim Abbott
parent 308069d828
commit 8461cc411e
3 changed files with 41 additions and 26 deletions

View File

@@ -976,13 +976,15 @@ def create_stream_if_needed(realm, stream_name, invite_only=False):
send_event(event, active_user_ids(realm)) send_event(event, active_user_ids(realm))
return stream, created return stream, created
def create_streams_if_needed(realm, stream_names, invite_only): def create_streams_if_needed(realm, stream_dicts, invite_only):
# type: (Realm, List[text_type], bool) -> Tuple[List[Stream], List[Stream]] # type: (Realm, List[Mapping[str, text_type]], bool) -> Tuple[List[Stream], List[Stream]]
"""Note that stream_dict["name"] is assumed to already be stripped of
whitespace"""
added_streams = [] # type: List[Stream] added_streams = [] # type: List[Stream]
existing_streams = [] # type: List[Stream] existing_streams = [] # type: List[Stream]
for stream_name in stream_names: for stream_dict in stream_dicts:
stream, created = create_stream_if_needed(realm, stream, created = create_stream_if_needed(realm,
stream_name, stream_dict["name"],
invite_only=invite_only) invite_only=invite_only)
if created: if created:
added_streams.append(stream) added_streams.append(stream)

View File

@@ -63,7 +63,7 @@ class TestCreateStreams(ZulipTestCase):
new_streams, existing_streams = create_streams_if_needed( new_streams, existing_streams = create_streams_if_needed(
realm, realm,
stream_names, [{"name": stream_name} for stream_name in stream_names],
invite_only=True) invite_only=True)
self.assertEqual(len(new_streams), 3) self.assertEqual(len(new_streams), 3)
self.assertEqual(len(existing_streams), 0) self.assertEqual(len(existing_streams), 0)
@@ -73,7 +73,7 @@ class TestCreateStreams(ZulipTestCase):
new_streams, existing_streams = create_streams_if_needed( new_streams, existing_streams = create_streams_if_needed(
realm, realm,
stream_names, [{"name": stream_name} for stream_name in stream_names],
invite_only=True) invite_only=True)
self.assertEqual(len(new_streams), 0) self.assertEqual(len(new_streams), 0)
self.assertEqual(len(existing_streams), 3) self.assertEqual(len(existing_streams), 3)

View File

@@ -38,8 +38,8 @@ def is_active_subscriber(user_profile, recipient):
active=True).exists() active=True).exists()
def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=False): def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=False):
# type: (Iterable[text_type], UserProfile, Optional[bool], Optional[bool]) -> Tuple[List[Stream], List[Stream]] # type: (Iterable[Mapping[str, text_type]], UserProfile, Optional[bool], Optional[bool]) -> Tuple[List[Stream], List[Stream]]
"""Converts plaintext stream names to a list of Streams, validating input in the process """Converts list of dicts to a list of Streams, validating input in the process
For each stream name, we validate it to ensure it meets our For each stream name, we validate it to ensure it meets our
requirements for a proper stream name: that is, that it is shorter requirements for a proper stream name: that is, that it is shorter
@@ -49,33 +49,39 @@ def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=Fal
This function in autocreate mode should be atomic: either an exception will be raised This function in autocreate mode should be atomic: either an exception will be raised
during a precheck, or all the streams specified will have been created if applicable. during a precheck, or all the streams specified will have been created if applicable.
@param streams_raw The list of stream names to process @param streams_raw The list of stream names to process; should
already be stripped of whitespace by the caller.
@param user_profile The user for whom we are retreiving the streams @param user_profile The user for whom we are retreiving the streams
@param autocreate Whether we should create streams if they don't already exist @param autocreate Whether we should create streams if they don't already exist
@param invite_only Whether newly created streams should have the invite_only bit set @param invite_only Whether newly created streams should have the invite_only bit set
""" """
# Validate all streams, getting extant ones, then get-or-creating the rest. # Validate all streams, getting extant ones, then get-or-creating the rest.
stream_set = set(stream_name.strip() for stream_name in streams_raw) stream_set = set(stream_dict["name"] for stream_dict in streams_raw)
for stream_name in stream_set: for stream_name in stream_set:
# Stream names should already have been stripped by the
# caller, but it makes sense to verify anyway.
assert stream_name == stream_name.strip()
if len(stream_name) > Stream.MAX_NAME_LENGTH: if len(stream_name) > Stream.MAX_NAME_LENGTH:
raise JsonableError(_("Stream name (%s) too long.") % (stream_name,)) raise JsonableError(_("Stream name (%s) too long.") % (stream_name,))
if not valid_stream_name(stream_name): if not valid_stream_name(stream_name):
raise JsonableError(_("Invalid stream name (%s).") % (stream_name,)) raise JsonableError(_("Invalid stream name (%s).") % (stream_name,))
existing_streams = [] # type: List[Stream] existing_streams = [] # type: List[Stream]
missing_stream_names = [] # type: List[text_type] missing_stream_dicts = [] # type: List[Mapping[str, text_type]]
existing_stream_map = bulk_get_streams(user_profile.realm, stream_set) existing_stream_map = bulk_get_streams(user_profile.realm, stream_set)
for stream_name in stream_set: for stream_dict in streams_raw:
stream_name = stream_dict["name"]
stream = existing_stream_map.get(stream_name.lower()) stream = existing_stream_map.get(stream_name.lower())
if stream is None: if stream is None:
missing_stream_names.append(stream_name) missing_stream_dicts.append(stream_dict)
else: else:
existing_streams.append(stream) existing_streams.append(stream)
if not missing_stream_names: if len(missing_stream_dicts) == 0:
# This is the happy path for callers who expected all of these # This is the happy path for callers who expected all of these
# streams to exist already. # streams to exist already.
created_streams = [] # type: List[Stream] created_streams = [] # type: List[Stream]
@@ -84,7 +90,8 @@ def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=Fal
if not user_profile.can_create_streams(): if not user_profile.can_create_streams():
raise JsonableError(_('User cannot create streams.')) raise JsonableError(_('User cannot create streams.'))
elif not autocreate: elif not autocreate:
raise JsonableError(_("Stream(s) (%s) do not exist") % ", ".join(missing_stream_names)) raise JsonableError(_("Stream(s) (%s) do not exist") % ", ".join(
stream_dict["name"] for stream_dict in missing_stream_dicts))
# We already filtered out existing streams, so dup_streams # We already filtered out existing streams, so dup_streams
# will normally be an empty list below, but we protect against somebody # will normally be an empty list below, but we protect against somebody
@@ -92,7 +99,7 @@ def list_to_streams(streams_raw, user_profile, autocreate=False, invite_only=Fal
# paranoid approach, since often on Zulip two people will discuss # paranoid approach, since often on Zulip two people will discuss
# creating a new stream, and both people eagerly do it.) # creating a new stream, and both people eagerly do it.)
created_streams, dup_streams = create_streams_if_needed(realm=user_profile.realm, created_streams, dup_streams = create_streams_if_needed(realm=user_profile.realm,
stream_names=missing_stream_names, stream_dicts=missing_stream_dicts,
invite_only=invite_only) invite_only=invite_only)
existing_streams += dup_streams existing_streams += dup_streams
@@ -241,7 +248,11 @@ def remove_subscriptions_backend(request, user_profile,
# admin. # admin.
return json_error(_("This action requires administrative rights")) return json_error(_("This action requires administrative rights"))
streams, __ = list_to_streams(streams_raw, user_profile) streams_as_dict = []
for stream_name in streams_raw:
streams_as_dict.append({"name": stream_name.strip()})
streams, __ = list_to_streams(streams_as_dict, user_profile)
for stream in streams: for stream in streams:
if removing_someone_else and stream.invite_only and \ if removing_someone_else and stream.invite_only and \
@@ -311,18 +322,20 @@ def add_subscriptions_backend(request, user_profile,
principals = REQ(validator=check_list(check_string), default=None), principals = REQ(validator=check_list(check_string), default=None),
authorization_errors_fatal = REQ(validator=check_bool, default=True)): authorization_errors_fatal = REQ(validator=check_bool, default=True)):
# type: (HttpRequest, UserProfile, Iterable[Mapping[str, text_type]], bool, bool, Optional[List[text_type]], bool) -> HttpResponse # type: (HttpRequest, UserProfile, Iterable[Mapping[str, text_type]], bool, bool, Optional[List[text_type]], bool) -> HttpResponse
stream_names = [] stream_dicts = []
for stream_dict in streams_raw: for stream_dict in streams_raw:
stream_name = stream_dict["name"].strip() stream_dict_copy = {} # type: Dict[str, Any]
if len(stream_name) > Stream.MAX_NAME_LENGTH: for field in stream_dict:
return json_error(_("Stream name (%s) too long.") % (stream_name,)) stream_dict_copy[field] = stream_dict[field]
if not valid_stream_name(stream_name): # Strip the stream name here.
return json_error(_("Invalid stream name (%s).") % (stream_name,)) stream_dict_copy['name'] = stream_dict_copy['name'].strip()
stream_names.append(stream_name) stream_dicts.append(stream_dict_copy)
# Enforcement of can_create_streams policy is inside list_to_streams. # Validation of the streams arguments, including enforcement of
# can_create_streams policy and valid_stream_name policy is inside
# list_to_streams.
existing_streams, created_streams = \ existing_streams, created_streams = \
list_to_streams(stream_names, user_profile, autocreate=True, invite_only=invite_only) list_to_streams(stream_dicts, user_profile, autocreate=True, invite_only=invite_only)
authorized_streams, unauthorized_streams = \ authorized_streams, unauthorized_streams = \
filter_stream_authorization(user_profile, existing_streams) filter_stream_authorization(user_profile, existing_streams)
if len(unauthorized_streams) > 0 and authorization_errors_fatal: if len(unauthorized_streams) > 0 and authorization_errors_fatal: