Skip to content

Commit 7e4fa13

Browse files
committed
Add rule edit endpoint
1 parent 5812022 commit 7e4fa13

File tree

3 files changed

+216
-3
lines changed

3 files changed

+216
-3
lines changed

src/sentry/api/endpoints/project_rule_details.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,95 @@
11
from __future__ import absolute_import
22

3+
from rest_framework import serializers, status
34
from rest_framework.response import Response
45

56
from sentry.api.bases.project import ProjectEndpoint
67
from sentry.api.serializers import serialize
78
from sentry.models import Rule
9+
from sentry.rules import rules
10+
11+
12+
ValidationError = serializers.ValidationError
13+
14+
15+
class RuleNodeField(serializers.WritableField):
16+
def __init__(self, type):
17+
super(RuleNodeField, self).__init__()
18+
self.type_name = type
19+
20+
def to_native(self, obj):
21+
return obj
22+
23+
def from_native(self, data):
24+
if not isinstance(data, dict):
25+
msg = 'Incorrect type. Expected a mapping, but got %s'
26+
raise ValidationError(msg % type(data).__name__)
27+
28+
if 'id' not in data:
29+
raise ValidationError("Missing attribute 'id'")
30+
31+
cls = rules.get(data['id'], self.type_name)
32+
if cls is None:
33+
msg = "Invalid node. Could not find '%s'"
34+
raise ValidationError(msg % data['id'])
35+
36+
if not cls(self.context['project'], data).validate_form():
37+
raise ValidationError('Node did not pass validation')
38+
39+
return data
40+
41+
42+
class ListField(serializers.WritableField):
43+
def __init__(self, child):
44+
self.child = child
45+
super(ListField, self).__init__()
46+
47+
def initialize(self, **kwargs):
48+
super(ListField, self).initialize(**kwargs)
49+
self.child.initialize(**kwargs)
50+
51+
def to_native(self, obj):
52+
return obj
53+
54+
def from_native(self, data):
55+
if not isinstance(data, list):
56+
msg = 'Incorrect type. Expected a mapping, but got %s'
57+
raise ValidationError(msg % type(data).__name__)
58+
59+
return map(self.child.from_native, data)
60+
61+
62+
class RuleSerializer(serializers.Serializer):
63+
name = serializers.CharField(max_length=64)
64+
actionMatch = serializers.ChoiceField(choices=(
65+
('all', 'all'),
66+
('any', 'any'),
67+
('none', 'none'),
68+
))
69+
actions = ListField(
70+
child=RuleNodeField(type='action/event'),
71+
)
72+
conditions = ListField(
73+
child=RuleNodeField(type='condition/event'),
74+
)
75+
76+
def save(self, rule):
77+
if self.data.get('name'):
78+
rule.label = self.data['name']
79+
if self.data.get('actionMatch'):
80+
rule.data['action_match'] = self.data['actionMatch']
81+
if self.data.get('actions') is not None:
82+
rule.data['actions'] = self.data['actions']
83+
if self.data.get('conditions') is not None:
84+
rule.data['conditions'] = self.data['conditions']
85+
rule.save()
86+
return rule
887

988

1089
class ProjectRuleDetailsEndpoint(ProjectEndpoint):
1190
def get(self, request, project, rule_id):
1291
"""
13-
Retrieve a rules
92+
Retrieve a rule
1493
1594
Return details on an individual rule.
1695
@@ -22,3 +101,33 @@ def get(self, request, project, rule_id):
22101
id=rule_id,
23102
)
24103
return Response(serialize(rule, request.user))
104+
105+
def put(self, request, project, rule_id):
106+
"""
107+
Update a rule
108+
109+
Update various attributes for the given rule.
110+
111+
{method} {path}
112+
{{
113+
"name": "My rule name",
114+
"conditions": [],
115+
"actions": [],
116+
"actionMatch": "all"
117+
}}
118+
119+
"""
120+
rule = Rule.objects.get(
121+
project=project,
122+
id=rule_id,
123+
)
124+
serializer = RuleSerializer({
125+
'actionMatch': rule.data.get('action_match', 'all'),
126+
}, context={'project': project}, data=request.DATA, partial=True)
127+
128+
if serializer.is_valid():
129+
rule = serializer.save(rule=rule)
130+
131+
return Response(serialize(rule, request.user))
132+
133+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

src/sentry/rules/registry.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def __init__(self):
1616
self._rules = defaultdict(list)
1717
self._map = {}
1818

19+
def __contains__(self, rule_id):
20+
return rule_id in self._map
21+
1922
def __iter__(self):
2023
for rule_type, rule_list in self._rules.iteritems():
2124
for rule in rule_list:
@@ -25,5 +28,8 @@ def add(self, rule):
2528
self._map[rule.id] = rule
2629
self._rules[rule.rule_type].append(rule)
2730

28-
def get(self, rule_id):
29-
return self._map.get(rule_id)
31+
def get(self, rule_id, type=None):
32+
cls = self._map.get(rule_id)
33+
if type is not None and cls not in self._rules[type]:
34+
return
35+
return cls

tests/sentry/api/endpoints/test_project_rule_details.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.core.urlresolvers import reverse
44

5+
from sentry.models import Rule
56
from sentry.testutils import APITestCase
67

78

@@ -24,3 +25,100 @@ def test_simple(self):
2425

2526
assert response.status_code == 200, response.content
2627
assert response.data['id'] == str(rule.id)
28+
29+
30+
class UpdateProjectRuleTest(APITestCase):
31+
def test_simple(self):
32+
self.login_as(user=self.user)
33+
34+
project = self.create_project()
35+
36+
rule = Rule.objects.create(project=project, label='foo')
37+
38+
conditions = [{
39+
'id': 'sentry.rules.conditions.first_seen_event.FirstSeenEventCondition',
40+
'key': 'foo',
41+
'match': 'eq',
42+
'value': 'bar',
43+
}]
44+
45+
url = reverse('sentry-api-0-project-rule-details', kwargs={
46+
'organization_slug': project.organization.slug,
47+
'project_slug': project.slug,
48+
'rule_id': rule.id,
49+
})
50+
response = self.client.put(url, data={
51+
'name': 'hello world',
52+
'actionMatch': 'any',
53+
'actions': [{'id': 'sentry.rules.actions.notify_event.NotifyEventAction'}],
54+
'conditions': conditions,
55+
}, format='json')
56+
57+
assert response.status_code == 200, response.content
58+
assert response.data['id'] == str(rule.id)
59+
60+
rule = Rule.objects.get(id=rule.id)
61+
assert rule.label == 'hello world'
62+
assert rule.data['action_match'] == 'any'
63+
assert rule.data['actions'] == [{'id': 'sentry.rules.actions.notify_event.NotifyEventAction'}]
64+
assert rule.data['conditions'] == conditions
65+
66+
def test_invalid_rule_node_type(self):
67+
self.login_as(user=self.user)
68+
69+
project = self.create_project()
70+
71+
rule = Rule.objects.create(project=project, label='foo')
72+
73+
url = reverse('sentry-api-0-project-rule-details', kwargs={
74+
'organization_slug': project.organization.slug,
75+
'project_slug': project.slug,
76+
'rule_id': rule.id,
77+
})
78+
response = self.client.put(url, data={
79+
'name': 'hello world',
80+
'actionMatch': 'any',
81+
'conditions': [{'id': 'sentry.rules.actions.notify_event.NotifyEventAction'}],
82+
}, format='json')
83+
84+
assert response.status_code == 400, response.content
85+
86+
def test_invalid_rule_node(self):
87+
self.login_as(user=self.user)
88+
89+
project = self.create_project()
90+
91+
rule = Rule.objects.create(project=project, label='foo')
92+
93+
url = reverse('sentry-api-0-project-rule-details', kwargs={
94+
'organization_slug': project.organization.slug,
95+
'project_slug': project.slug,
96+
'rule_id': rule.id,
97+
})
98+
response = self.client.put(url, data={
99+
'name': 'hello world',
100+
'actionMatch': 'any',
101+
'actions': [{'id': 'foo'}],
102+
}, format='json')
103+
104+
assert response.status_code == 400, response.content
105+
106+
def test_rule_form_not_valid(self):
107+
self.login_as(user=self.user)
108+
109+
project = self.create_project()
110+
111+
rule = Rule.objects.create(project=project, label='foo')
112+
113+
url = reverse('sentry-api-0-project-rule-details', kwargs={
114+
'organization_slug': project.organization.slug,
115+
'project_slug': project.slug,
116+
'rule_id': rule.id,
117+
})
118+
response = self.client.put(url, data={
119+
'name': 'hello world',
120+
'actionMatch': 'any',
121+
'conditions': [{'id': 'sentry.rules.conditions.tagged_event.TaggedEventCondition'}],
122+
}, format='json')
123+
124+
assert response.status_code == 400, response.content

0 commit comments

Comments
 (0)