Skip to content

Commit eb4e1d8

Browse files
committed
Log name|summary changes in more detail (#23725)
* Log name|summary changes in more detail * fix tests for nondeterministic ordering * store removed, added strings in arguments instead
1 parent e58db64 commit eb4e1d8

File tree

7 files changed

+168
-22
lines changed

7 files changed

+168
-22
lines changed

src/olympia/addons/serializers.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import copy
2+
import json
23
import re
34

45
from django.core.exceptions import ValidationError as DjangoValidationError
@@ -80,6 +81,7 @@
8081
from .tasks import resize_icon, resize_preview
8182
from .utils import (
8283
fetch_translations_from_addon,
84+
get_translation_differences,
8385
validate_version_number_is_gt_latest_signed_listed_version,
8486
)
8587
from .validators import (
@@ -1298,7 +1300,7 @@ def _save_icon(self, uploaded_icon):
12981300
remove_icons(self.instance)
12991301
self.instance.update(icon_type='')
13001302

1301-
def log(self, instance, validated_data):
1303+
def log(self, instance, validated_data, changes):
13021304
validated_data = {**validated_data} # we want to modify it, so take a copy
13031305
user = self.context['request'].user
13041306

@@ -1319,6 +1321,15 @@ def log(self, instance, validated_data):
13191321
# version is always a new object, and not a property either
13201322
validated_data.pop('version')
13211323

1324+
for field, addedremoved in changes.items():
1325+
ActivityLog.objects.create(
1326+
amo.LOG.EDIT_ADDON_PROPERTY,
1327+
instance,
1328+
field,
1329+
json.dumps(addedremoved),
1330+
user=user,
1331+
)
1332+
validated_data.pop(field)
13221333
if validated_data:
13231334
ActivityLog.objects.create(
13241335
amo.LOG.EDIT_PROPERTIES,
@@ -1356,17 +1367,19 @@ def update(self, instance, validated_data):
13561367
if instance.has_listed_versions()
13571368
else None
13581369
)
1370+
changes = {}
13591371

13601372
old_slug = instance.slug
13611373

13621374
if 'icon' in validated_data:
13631375
self._save_icon(validated_data['icon'])
13641376
instance = super().update(instance, validated_data)
13651377

1366-
if old_metadata is not None and old_metadata != fetch_translations_from_addon(
1367-
instance, fields_to_review
1378+
if old_metadata is not None and old_metadata != (
1379+
new_metadata := fetch_translations_from_addon(instance, fields_to_review)
13681380
):
13691381
statsd.incr('addons.submission.metadata_content_review_triggered')
1382+
changes = get_translation_differences(old_metadata, new_metadata)
13701383
AddonApprovalsCounter.reset_content_for_addon(addon=instance)
13711384
if 'all_categories' in validated_data:
13721385
del instance.all_categories # super.update will have set it.
@@ -1386,7 +1399,7 @@ def update(self, instance, validated_data):
13861399
amo.LOG.ADDON_SLUG_CHANGED, instance, old_slug, instance.slug
13871400
)
13881401

1389-
self.log(instance, validated_data)
1402+
self.log(instance, validated_data, changes)
13901403
return instance
13911404

13921405

src/olympia/addons/tests/test_views.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,7 @@ def setUp(self):
19611961
self.statsd_incr_mock = self.patch('olympia.addons.serializers.statsd.incr')
19621962

19631963
def test_basic(self):
1964+
original_summary = str(self.addon.summary)
19641965
response = self.request(data={'summary': {'en-US': 'summary update!'}})
19651966
self.addon.reload()
19661967
assert response.status_code == 200, response.content
@@ -1990,8 +1991,12 @@ def test_basic(self):
19901991
)
19911992
).get()
19921993
assert alog.user == self.user
1993-
assert alog.action == amo.LOG.EDIT_PROPERTIES.id
1994-
assert alog.details == ['summary']
1994+
assert alog.action == amo.LOG.EDIT_ADDON_PROPERTY.id
1995+
assert alog.arguments == [
1996+
self.addon,
1997+
'summary',
1998+
json.dumps({'removed': [original_summary], 'added': ['summary update!']}),
1999+
]
19952000
return data
19962001

19972002
@override_settings(API_THROTTLING=False)
@@ -2212,6 +2217,10 @@ def test_set_slug_log(self):
22122217
def test_set_extra_data(self):
22132218
self.addon.description = 'Existing description'
22142219
self.addon.save()
2220+
original_data = {
2221+
'name': str(self.addon.name),
2222+
'summary': str(self.addon.summary),
2223+
}
22152224
patch_data = {
22162225
'developer_comments': {'en-US': 'comments'},
22172226
'homepage': {'en-US': 'https://my.home.page/'},
@@ -2251,17 +2260,48 @@ def test_set_extra_data(self):
22512260
assert addon.support_email == '[email protected]'
22522261
assert data['support_url']['url'] == {'en-US': 'https://my.home.page/support/'}
22532262
assert addon.support_url == 'https://my.home.page/support/'
2263+
2264+
summ_log, name_log = list(
2265+
ActivityLog.objects.filter(action=amo.LOG.EDIT_ADDON_PROPERTY.id)
2266+
)
2267+
if name_log.arguments[1] != 'name':
2268+
# The order isn't deterministic, it doesn't matter, so just switch em.
2269+
name_log, summ_log = summ_log, name_log
2270+
assert name_log.arguments == [
2271+
self.addon,
2272+
'name',
2273+
json.dumps(
2274+
{
2275+
'removed': [original_data['name']],
2276+
'added': [patch_data['name']['en-US']],
2277+
}
2278+
),
2279+
]
2280+
assert summ_log.arguments == [
2281+
self.addon,
2282+
'summary',
2283+
json.dumps(
2284+
{
2285+
'removed': [original_data['summary']],
2286+
'added': [patch_data['summary']['en-US']],
2287+
}
2288+
),
2289+
]
2290+
22542291
alog = ActivityLog.objects.exclude(
22552292
action__in=(
22562293
amo.LOG.ADD_VERSION.id,
22572294
amo.LOG.LOG_IN.id,
22582295
amo.LOG.LOG_IN_API_TOKEN.id,
22592296
amo.LOG.ADDON_SLUG_CHANGED.id,
2297+
amo.LOG.EDIT_ADDON_PROPERTY.id,
22602298
)
22612299
).get()
22622300
assert alog.user == self.user
22632301
assert alog.action == amo.LOG.EDIT_PROPERTIES.id
2264-
assert alog.details == list(patch_data.keys())
2302+
assert alog.details == [
2303+
prop for prop in patch_data if prop not in ('name', 'summary')
2304+
]
22652305

22662306
def test_set_disabled(self):
22672307
response = self.request(
@@ -2500,7 +2540,7 @@ def test_delete_icon_formdata(self):
25002540

25012541
def _test_metadata_content_review(self):
25022542
response = self.request(
2503-
data={'name': {'en-US': 'new name'}, 'summary': {'en-US': 'new summary'}},
2543+
data={'name': {'en-US': 'new name'}, 'summary': {'fr': 'summary nouveau'}},
25042544
)
25052545
assert response.status_code == 200
25062546

@@ -2526,6 +2566,7 @@ def test_metadata_content_review_unlisted(self, fetch_mock):
25262566
)
25272567

25282568
def test_metadata_change_triggers_content_review(self):
2569+
old_name = str(self.addon.name)
25292570
AddonApprovalsCounter.approve_content_for_addon(addon=self.addon)
25302571
assert AddonApprovalsCounter.objects.get(addon=self.addon).last_content_review
25312572

@@ -2539,6 +2580,26 @@ def test_metadata_change_triggers_content_review(self):
25392580
self.statsd_incr_mock.assert_any_call(
25402581
'addons.submission.metadata_content_review_triggered'
25412582
)
2583+
assert (
2584+
ActivityLog.objects.filter(action=amo.LOG.EDIT_ADDON_PROPERTY.id).count()
2585+
== 2
2586+
)
2587+
summ_log, name_log = list(
2588+
ActivityLog.objects.filter(action=amo.LOG.EDIT_ADDON_PROPERTY.id)
2589+
)
2590+
if name_log.arguments[1] != 'name':
2591+
# The order isn't deterministic, it doesn't matter, so just switch em.
2592+
name_log, summ_log = summ_log, name_log
2593+
assert name_log.arguments == [
2594+
self.addon,
2595+
'name',
2596+
json.dumps({'removed': [old_name], 'added': ['new name']}),
2597+
]
2598+
assert summ_log.arguments == [
2599+
self.addon,
2600+
'summary',
2601+
json.dumps({'removed': [], 'added': ['summary nouveau']}),
2602+
]
25422603

25432604
def test_metadata_change_same_content(self):
25442605
AddonApprovalsCounter.approve_content_for_addon(addon=self.addon)
@@ -2547,7 +2608,7 @@ def test_metadata_change_same_content(self):
25472608
).last_content_review
25482609
assert old_content_review
25492610
self.addon.name = {'en-US': 'new name'}
2550-
self.addon.summary = {'en-US': 'new summary'}
2611+
self.addon.summary = {'en-US': str(self.addon.summary), 'fr': 'summary nouveau'}
25512612
self.addon.save()
25522613

25532614
self._test_metadata_content_review()

src/olympia/addons/utils.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import uuid
2+
from collections import defaultdict
23

34
from django import forms
45
from django.core.files.storage import default_storage as storage
@@ -89,10 +90,25 @@ def compute_last_updated(addon):
8990

9091

9192
def fetch_translations_from_addon(addon, properties):
92-
translation_ids_gen = (getattr(addon, prop + '_id', None) for prop in properties)
93-
translation_ids = [id_ for id_ in translation_ids_gen if id_]
93+
translation_ids_gen = (
94+
(prop, getattr(addon, prop + '_id', None)) for prop in properties
95+
)
96+
translation_ids = {id_: prop for prop, id_ in translation_ids_gen if id_}
9497
# Just get all the values together to make it simplier
95-
return {str(value) for value in Translation.objects.filter(id__in=translation_ids)}
98+
out = defaultdict(set)
99+
for trans in Translation.objects.filter(id__in=translation_ids):
100+
out[translation_ids.get(trans.id, '_')].add(str(trans))
101+
return out
102+
103+
104+
def get_translation_differences(old_, new_):
105+
out = {}
106+
for field in list({*old_, *new_}):
107+
removed = list(old_[field] - new_[field])
108+
added = list(new_[field] - old_[field])
109+
if removed or added:
110+
out[field] = {'removed': removed, 'added': added}
111+
return out
96112

97113

98114
class DeleteTokenSigner(TimestampSigner):

src/olympia/constants/activity.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CREATE_ADDON(_LOG):
2828

2929

3030
class EDIT_PROPERTIES(_LOG):
31-
"""Expects: addon"""
31+
"""Expects: addon. Consider using EDIT_ADDON_PROPERTY instead"""
3232

3333
id = 2
3434
action_class = 'edit'
@@ -1238,6 +1238,15 @@ class DISABLE_AUTO_APPROVAL(_LOG):
12381238
hide_developer = True
12391239

12401240

1241+
class EDIT_ADDON_PROPERTY(_LOG):
1242+
"""Expects: addon, field. 3rd arg is a json blob."""
1243+
1244+
id = 208
1245+
action_class = 'edit'
1246+
format = _('{addon} {0} property edited.')
1247+
show_user_to_developer = True
1248+
1249+
12411250
LOGS = [x for x in vars().values() if isclass(x) and issubclass(x, _LOG) and x != _LOG]
12421251
# Make sure there's no duplicate IDs.
12431252
assert len(LOGS) == len({log.id for log in LOGS})

src/olympia/devhub/forms.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
)
3636
from olympia.addons.utils import (
3737
fetch_translations_from_addon,
38+
get_translation_differences,
3839
remove_icons,
3940
validate_version_number_is_gt_latest_signed_listed_version,
4041
verify_mozilla_trademark,
@@ -134,15 +135,17 @@ def save(self, *args, **kwargs):
134135
else {}
135136
)
136137
obj = super().save(*args, **kwargs)
137-
if not metadata_content_review:
138-
return obj
139-
new_data = fetch_translations_from_addon(
140-
obj, self.fields_to_trigger_content_review
141-
)
142-
if existing_data != new_data:
143-
# flag for content review
144-
statsd.incr('devhub.metadata_content_review_triggered')
145-
AddonApprovalsCounter.reset_content_for_addon(addon=obj)
138+
if metadata_content_review:
139+
new_data = fetch_translations_from_addon(
140+
obj, self.fields_to_trigger_content_review
141+
)
142+
if existing_data != new_data:
143+
self.metadata_changes = get_translation_differences(
144+
existing_data, new_data
145+
)
146+
# flag for content review
147+
statsd.incr('devhub.metadata_content_review_triggered')
148+
AddonApprovalsCounter.reset_content_for_addon(addon=obj)
146149
return obj
147150

148151

src/olympia/devhub/tests/test_views_edit.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,37 @@ def test_metadata_change_triggers_content_review(self):
321321
assert str(addon.name) == data['name']
322322
assert str(addon.summary) == data['summary']
323323

324+
if self.listed:
325+
# check we logged the changes
326+
alogs = ActivityLog.objects.filter(action=amo.LOG.EDIT_ADDON_PROPERTY.id)
327+
assert alogs.count() == 2
328+
name_log, summ_log = list(
329+
ActivityLog.objects.filter(action=amo.LOG.EDIT_ADDON_PROPERTY.id)
330+
)
331+
if name_log.arguments[1] != 'name':
332+
# The order isn't deterministic, it doesn't matter, so just switch em.
333+
name_log, summ_log = summ_log, name_log
334+
assert name_log.arguments == [
335+
self.addon,
336+
'name',
337+
json.dumps({'removed': ['Delicious Bookmarks'], 'added': ['new name']}),
338+
]
339+
assert summ_log.arguments == [
340+
self.addon,
341+
'summary',
342+
json.dumps(
343+
{
344+
'removed': ['Delicious Bookmarks is the official'],
345+
'added': ['new summary'],
346+
}
347+
),
348+
]
349+
else:
350+
alogs = ActivityLog.objects.filter(action=amo.LOG.EDIT_PROPERTIES.id)
351+
assert alogs.count() == 1
352+
324353
# Now repeat, but we won't be changing either name or summary
354+
alogs.delete()
325355
AddonApprovalsCounter.approve_content_for_addon(addon=addon)
326356
assert AddonApprovalsCounter.objects.get(addon=addon).last_content_review
327357
data['description'] = 'its a totally new description!'
@@ -332,6 +362,10 @@ def test_metadata_change_triggers_content_review(self):
332362

333363
# Still keeps its date this time, so no new content review
334364
assert AddonApprovalsCounter.objects.get(addon=addon).last_content_review
365+
# no changes logged for name or summary this time
366+
assert not ActivityLog.objects.filter(
367+
action=amo.LOG.EDIT_ADDON_PROPERTY.id
368+
).exists()
335369
# And metadata was updated
336370
assert str(addon.description) == data['description']
337371

src/olympia/devhub/views.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import json
23
import os
34
import time
45
from copy import deepcopy
@@ -919,6 +920,15 @@ def addons_section(request, addon_id, addon, section, editable=False):
919920
if section == 'media':
920921
ActivityLog.objects.create(amo.LOG.CHANGE_MEDIA, addon)
921922
else:
923+
metadata_changes = getattr(main_form, 'metadata_changes', {})
924+
for field, addedremoved in metadata_changes.items():
925+
ActivityLog.objects.create(
926+
amo.LOG.EDIT_ADDON_PROPERTY,
927+
addon,
928+
field,
929+
json.dumps(addedremoved),
930+
)
931+
922932
ActivityLog.objects.create(amo.LOG.EDIT_PROPERTIES, addon)
923933

924934
if valid_slug != addon.slug:

0 commit comments

Comments
 (0)