Skip to content

Commit 614caf1

Browse files
sujalshah-bittimabbott
authored andcommitted
user_groups: Add creator and date_created field in user groups.
This commit introduced 'creator' and 'date_created' fields in user groups, allowing users to view who created the groups and when. Both fields can be null for groups without creator data.
1 parent 3b69f2e commit 614caf1

20 files changed

+251
-6
lines changed

api_docs/changelog.md

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
2020

2121
## Changes in Zulip 10.0
2222

23+
**Feature level 292**
24+
25+
* [`POST /register`](/api/register-queue), [`GET
26+
/events`](/api/get-events), [`GET
27+
/user_groups`](/api/get-user-groups): Added `creator_id` and
28+
`date_created` fields to user groups objects.
29+
2330
**Feature level 291**
2431

2532
* `PATCH /realm`, [`GET /events`](/api/get-events),

version.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@
3434
# new level means in api_docs/changelog.md, as well as "**Changes**"
3535
# entries in the endpoint's documentation in `zulip.yaml`.
3636

37-
38-
API_FEATURE_LEVEL = 291 # Last bumped for can_delete_own_message_group
37+
API_FEATURE_LEVEL = 292 # Last bumped for `namedusergroup_creator_date_created`.
3938

4039
# Bump the minor PROVISION_VERSION to indicate that folks should provision
4140
# only when going from an old version of the code to a newer version. Bump

web/src/state_data.ts

+2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export const realm_emoji_map_schema = z.record(server_emoji_schema);
129129
export const user_group_schema = z.object({
130130
description: z.string(),
131131
id: z.number(),
132+
creator_id: z.number().nullable(),
133+
date_created: z.number().nullable(),
132134
name: z.string(),
133135
members: z.array(z.number()),
134136
is_system_group: z.boolean(),

web/src/user_group_edit.js

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import * as settings_components from "./settings_components";
2424
import * as settings_data from "./settings_data";
2525
import * as settings_org from "./settings_org";
2626
import {current_user, realm} from "./state_data";
27+
import * as stream_data from "./stream_data";
28+
import * as timerender from "./timerender";
2729
import * as ui_report from "./ui_report";
2830
import * as user_group_components from "./user_group_components";
2931
import * as user_group_create from "./user_group_create";
@@ -333,6 +335,13 @@ function update_toggler_for_group_setting() {
333335
export function show_settings_for(group) {
334336
const html = render_user_group_settings({
335337
group,
338+
// We get timestamp in seconds from the API but timerender needs milliseconds.
339+
date_created_string: timerender.get_localized_date_or_time_for_format(
340+
new Date(group.date_created * 1000),
341+
"dayofyear_year",
342+
),
343+
creator: stream_data.maybe_get_creator_details(group.creator_id),
344+
is_creator: group.creator_id === current_user.user_id,
336345
is_member: user_groups.is_direct_member_of(people.my_current_user_id(), group.id),
337346
});
338347

web/src/user_groups.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function add(user_group_raw: UserGroupRaw): void {
4545
description: user_group_raw.description,
4646
id: user_group_raw.id,
4747
name: user_group_raw.name,
48+
creator_id: user_group_raw.creator_id,
49+
date_created: user_group_raw.date_created,
4850
members: new Set(user_group_raw.members),
4951
is_system_group: user_group_raw.is_system_group,
5052
direct_subgroup_ids: new Set(user_group_raw.direct_subgroup_ids),

web/styles/subscriptions.css

+4-2
Original file line numberDiff line numberDiff line change
@@ -950,8 +950,10 @@ h4.user_group_setting_subsection_title {
950950
}
951951
}
952952

953-
.stream_details_box {
954-
> .stream_details_subsection {
953+
.stream_details_box,
954+
.group_detail_box {
955+
> .stream_details_subsection,
956+
> .group_details_box_subsection {
955957
margin: 0 0 10px; /* mimic paragraph spacing */
956958
}
957959

web/templates/user_group_settings/user_group_settings.hbs

+15
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@
4343

4444
{{> group_permissions can_mention_group_widget_name="can_mention_group" can_manage_group_widget_name="can_manage_group"}}
4545
</div>
46+
47+
<div class="group_detail_box">
48+
<div class="user_group_details_box_header">
49+
<h3 class="user_group_setting_subsection_title">
50+
{{t "User group details" }}
51+
</h3>
52+
</div>
53+
<div class="creator_details group_details_box_subsection">
54+
{{> ../creator_details }}
55+
</div>
56+
<div class="group_details_box_subsection">
57+
{{t "User group ID"}}<br/>
58+
{{group.id}}
59+
</div>
60+
</div>
4661
</div>
4762

4863
<div class="group_member_settings group_setting_section" data-group-section="members">

web/tests/composebox_typeahead.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ const harry_item = user_item(harry);
424424
const hamletcharacters = user_group_item({
425425
name: "hamletcharacters",
426426
id: 1,
427+
creator_id: null,
428+
date_created: 1596710000,
427429
description: "Characters of Hamlet",
428430
members: new Set([100, 104]),
429431
is_system_group: false,
@@ -435,6 +437,8 @@ const hamletcharacters = user_group_item({
435437
const backend = user_group_item({
436438
name: "Backend",
437439
id: 2,
440+
creator_id: null,
441+
date_created: 1596710000,
438442
description: "Backend team",
439443
members: new Set([101]),
440444
is_system_group: false,
@@ -446,6 +450,8 @@ const backend = user_group_item({
446450
const call_center = user_group_item({
447451
name: "Call Center",
448452
id: 3,
453+
creator_id: null,
454+
date_created: 1596710000,
449455
description: "folks working in support",
450456
members: new Set([102]),
451457
is_system_group: false,

web/tests/lib/events.js

+2
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,8 @@ exports.fixtures = {
809809
group: {
810810
id: 555,
811811
name: "Mobile",
812+
creator_id: null,
813+
date_created: fake_now,
812814
description: "mobile folks",
813815
members: [1],
814816
is_system_group: false,

web/tests/user_groups.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ run_test("user_groups", () => {
1313
const students = {
1414
description: "Students group",
1515
name: "Students",
16+
creator_id: null,
17+
date_created: 1596710000,
1618
id: 0,
1719
members: new Set([1, 2]),
1820
is_system_group: false,
@@ -32,6 +34,8 @@ run_test("user_groups", () => {
3234
const admins = {
3335
name: "Admins",
3436
description: "foo",
37+
creator_id: null,
38+
date_created: 1596710000,
3539
id: 1,
3640
members: new Set([3]),
3741
is_system_group: false,

zerver/actions/user_groups.py

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils.translation import gettext as _
99

1010
from zerver.lib.exceptions import JsonableError
11+
from zerver.lib.timestamp import datetime_to_timestamp
1112
from zerver.lib.user_groups import (
1213
AnonymousSettingGroupDict,
1314
get_group_setting_value_for_api,
@@ -53,6 +54,7 @@ def create_user_group_in_database(
5354
description=description,
5455
is_system_group=is_system_group,
5556
realm_for_sharding=realm,
57+
creator=acting_user,
5658
)
5759

5860
for setting_name, setting_value in group_settings_map.items():
@@ -170,11 +172,17 @@ def do_send_create_user_group_event(
170172
members: list[UserProfile],
171173
direct_subgroups: Sequence[UserGroup] = [],
172174
) -> None:
175+
creator_id = user_group.creator_id
176+
assert user_group.date_created is not None
177+
date_created = datetime_to_timestamp(user_group.date_created)
178+
173179
event = dict(
174180
type="user_group",
175181
op="add",
176182
group=dict(
177183
name=user_group.name,
184+
creator_id=creator_id,
185+
date_created=date_created,
178186
members=[member.id for member in members],
179187
description=user_group.description,
180188
id=user_group.id,

zerver/lib/event_schema.py

+2
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,8 @@ def check_update_message(
18121812
required_keys=[
18131813
("id", int),
18141814
("name", str),
1815+
("creator_id", OptionalType(int)),
1816+
("date_created", OptionalType(int)),
18151817
("members", ListType(int)),
18161818
("direct_subgroup_ids", ListType(int)),
18171819
("description", str),

zerver/lib/export.py

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ class MessagePartial(TypedDict):
301301
"zerver_realm": ["date_created"],
302302
"zerver_scheduledmessage": ["scheduled_timestamp"],
303303
"zerver_stream": ["date_created"],
304+
"zerver_namedusergroup": ["date_created"],
304305
"zerver_useractivityinterval": ["start", "end"],
305306
"zerver_useractivity": ["last_visit"],
306307
"zerver_onboardingstep": ["timestamp"],

zerver/lib/import_realm.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -754,13 +754,15 @@ def bulk_import_named_user_groups(data: TableData) -> None:
754754
group["can_manage_group_id"],
755755
group["can_mention_group_id"],
756756
group["deactivated"],
757+
group["creator_id"],
758+
group["date_created"],
757759
)
758760
for group in data["zerver_namedusergroup"]
759761
]
760762

761763
query = SQL(
762764
"""
763-
INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_manage_group_id, can_mention_group_id, deactivated)
765+
INSERT INTO zerver_namedusergroup (usergroup_ptr_id, realm_id, name, description, is_system_group, can_manage_group_id, can_mention_group_id, deactivated, creator_id, date_created)
764766
VALUES %s
765767
"""
766768
)
@@ -1197,6 +1199,10 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
11971199
bulk_import_model(data, UserGroup)
11981200

11991201
if "zerver_namedusergroup" in data:
1202+
re_map_foreign_keys(
1203+
data, "zerver_namedusergroup", "creator", related_table="user_profile"
1204+
)
1205+
fix_datetime_fields(data, "zerver_namedusergroup")
12001206
re_map_foreign_keys(
12011207
data, "zerver_namedusergroup", "usergroup_ptr", related_table="usergroup"
12021208
)

zerver/lib/user_groups.py

+13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
PreviousSettingValueMismatchedError,
1818
SystemGroupRequiredError,
1919
)
20+
from zerver.lib.timestamp import datetime_to_timestamp
2021
from zerver.lib.types import GroupPermissionSetting, ServerSupportedPermissionSettings
2122
from zerver.models import (
2223
GroupGroupMembership,
@@ -50,6 +51,8 @@ class UserGroupDict(TypedDict):
5051
description: str
5152
members: list[int]
5253
direct_subgroup_ids: list[int]
54+
creator_id: int | None
55+
date_created: int | None
5356
is_system_group: bool
5457
can_manage_group: int | AnonymousSettingGroupDict
5558
can_mention_group: int | AnonymousSettingGroupDict
@@ -516,9 +519,19 @@ def user_groups_in_realm_serialized(
516519
if user_group.id in group_subgroups:
517520
direct_subgroup_ids = group_subgroups[user_group.id]
518521

522+
creator_id = user_group.creator_id
523+
524+
date_created = (
525+
datetime_to_timestamp(user_group.date_created)
526+
if user_group.date_created is not None
527+
else None
528+
)
529+
519530
group_dicts[user_group.id] = dict(
520531
id=user_group.id,
521532
name=user_group.name,
533+
creator_id=creator_id,
534+
date_created=date_created,
522535
description=user_group.description,
523536
members=direct_member_ids,
524537
direct_subgroup_ids=direct_subgroup_ids,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 5.0.8 on 2024-08-31 08:09
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
dependencies = [
11+
("zerver", "0582_remove_realm_delete_own_message_policy"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="namedusergroup",
17+
name="creator",
18+
field=models.ForeignKey(
19+
db_column="creator_id",
20+
null=True,
21+
on_delete=django.db.models.deletion.SET_NULL,
22+
related_name="+",
23+
to=settings.AUTH_USER_MODEL,
24+
),
25+
),
26+
migrations.AddField(
27+
model_name="namedusergroup",
28+
name="date_created",
29+
field=models.DateTimeField(default=django.utils.timezone.now, null=True),
30+
),
31+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 5.0.8 on 2024-08-31 08:09
2+
3+
from django.db import migrations
4+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
5+
from django.db.migrations.state import StateApps
6+
7+
8+
def backfill_creator_id_and_date_created_from_realm_audit_log(
9+
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
10+
) -> None:
11+
RealmAuditLog = apps.get_model("zerver", "RealmAuditLog")
12+
RealmAuditLog.USER_GROUP_CREATED = 701
13+
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
14+
15+
user_group_creator_updates = []
16+
for audit_log_entry in RealmAuditLog.objects.select_related("modified_user_group").filter(
17+
event_type=RealmAuditLog.USER_GROUP_CREATED,
18+
acting_user_id__isnull=False,
19+
):
20+
assert audit_log_entry.modified_user_group is not None
21+
user_group = audit_log_entry.modified_user_group
22+
user_group.creator_id = audit_log_entry.acting_user_id
23+
user_group_creator_updates.append(user_group)
24+
25+
NamedUserGroup.objects.bulk_update(user_group_creator_updates, ["creator_id"], batch_size=1000)
26+
27+
user_group_date_created_updates = []
28+
for audit_log_entry in RealmAuditLog.objects.select_related("modified_user_group").filter(
29+
event_type=RealmAuditLog.USER_GROUP_CREATED,
30+
event_time__isnull=False,
31+
):
32+
assert audit_log_entry.modified_user_group is not None
33+
user_group = audit_log_entry.modified_user_group
34+
user_group.date_created = audit_log_entry.event_time
35+
user_group_date_created_updates.append(user_group)
36+
37+
NamedUserGroup.objects.bulk_update(
38+
user_group_date_created_updates, ["date_created"], batch_size=1000
39+
)
40+
41+
42+
class Migration(migrations.Migration):
43+
dependencies = [
44+
("zerver", "0583_namedusergroup_creator_namedusergroup_date_created"),
45+
]
46+
47+
operations = [
48+
migrations.RunPython(
49+
backfill_creator_id_and_date_created_from_realm_audit_log,
50+
reverse_code=migrations.RunPython.noop,
51+
elidable=True,
52+
),
53+
]

zerver/models/groups.py

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.db import models
22
from django.db.models import CASCADE
3+
from django.utils.timezone import now as timezone_now
34
from django_cte import CTEManager
45

56
from zerver.lib.types import GroupPermissionSetting
@@ -52,6 +53,10 @@ class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # djang
5253
)
5354
name = models.CharField(max_length=MAX_NAME_LENGTH, db_column="name")
5455
description = models.TextField(default="", db_column="description")
56+
date_created = models.DateTimeField(default=timezone_now, null=True)
57+
creator = models.ForeignKey(
58+
UserProfile, null=True, on_delete=models.SET_NULL, related_name="+", db_column="creator_id"
59+
)
5560
is_system_group = models.BooleanField(default=False, db_column="is_system_group")
5661

5762
can_manage_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT, related_name="+")

0 commit comments

Comments
 (0)