Skip to content
This repository was archived by the owner on Jun 1, 2022. It is now read-only.

Commit d82b66d

Browse files
committed
Merge pull request #256 from editorsnotes/action-api
Improve project serializer and API for getting action histories
2 parents 09d2d73 + 312322e commit d82b66d

File tree

11 files changed

+107
-71
lines changed

11 files changed

+107
-71
lines changed

editorsnotes/api/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from topics import TopicSerializer
33
from notes import NoteSerializer
44
from activity import ActivitySerializer
5+
from projects import ProjectSerializer

editorsnotes/api/serializers/activity.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class Meta:
2121
model = LogActivity
2222
fields = ('user', 'project', 'time', 'type', 'url', 'title', 'action',)
2323
def get_object_url(self, obj):
24-
return None if obj.action == DELETION else obj.content_object.get_absolute_url()
24+
if obj.action == DELETION or obj.content_object is None:
25+
return None
26+
else:
27+
return obj.content_object.get_absolute_url()
2528
def get_action_repr(self, version_obj):
2629
return VERSION_ACTIONS[version_obj.action]

editorsnotes/api/serializers/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def __init__(self, *args, **kwargs):
5151
def get_attribute(self, obj):
5252
return obj.get_affiliation()
5353
def to_representation(self, value):
54-
url = reverse('api:api-project-detail', args=(value.slug,),
54+
url = reverse('api:api-projects-detail', args=(value.slug,),
5555
request=self.context['request'])
5656
return { 'name': value.name, 'url': url }
5757

editorsnotes/api/serializers/documents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class CitationSerializer(serializers.ModelSerializer):
100100
document = HyperlinkedProjectItemField(view_name='api:api-documents-detail',
101101
queryset=Document.objects,
102102
required=True)
103-
document_description = serializers.SerializerMethodField('get_document_description')
103+
document_description = serializers.SerializerMethodField()
104104
class Meta:
105105
model = Citation
106106
fields = ('id', 'url', 'ordering', 'document', 'document_description', 'notes')

editorsnotes/api/serializers/projects.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,35 @@
22
from rest_framework.reverse import reverse
33

44
from editorsnotes.main.models import Project
5+
from editorsnotes.search import get_index
6+
7+
from .base import URLField
8+
9+
def count_for(project, doc_type):
10+
index = get_index('main')
11+
count = index.es.count(
12+
{ 'query': { 'term': { 'serialized.project.name': project.name }}},
13+
index=index.name,
14+
doc_type=doc_type)
15+
return count['count']
16+
517

618
class ProjectSerializer(serializers.ModelSerializer):
7-
url = serializers.SerializerMethodField('get_api_url')
8-
notes = serializers.SerializerMethodField('get_notes_url')
9-
topics = serializers.SerializerMethodField('get_topics_url')
10-
documents = serializers.SerializerMethodField('get_documents_url')
19+
url = URLField('api:api-projects-detail', ('slug',))
20+
notes = serializers.SerializerMethodField()
21+
notes_url = URLField('api:api-notes-list', ('slug',))
22+
topics = serializers.SerializerMethodField()
23+
topics_url = URLField('api:api-topics-list', ('slug',))
24+
documents = serializers.SerializerMethodField()
25+
documents_url = URLField('api:api-documents-list', ('slug',))
26+
activity_url = URLField('api:api-projects-activity', ('slug',))
1127
class Meta:
1228
model = Project
13-
fields = ('slug', 'name', 'url', 'notes', 'topics', 'documents',)
14-
def get_api_url(self, obj):
15-
return reverse('api:api-project-detail', args=(obj.slug,),
16-
request=self.context['request'])
17-
def get_notes_url(self, obj):
18-
return reverse('api:api-notes-list', args=(obj.slug,),
19-
request=self.context['request'])
20-
def get_topics_url(self, obj):
21-
return reverse('api:api-topics-list', args=(obj.slug,),
22-
request=self.context['request'])
23-
def get_documents_url(self, obj):
24-
return reverse('api:api-documents-list', args=(obj.slug,),
25-
request=self.context['request'])
29+
fields = ('slug', 'url', 'name', 'description', 'notes', 'notes_url',
30+
'topics', 'topics_url', 'documents', 'documents_url', 'activity_url')
31+
def get_notes(self, obj):
32+
return count_for(obj, 'note')
33+
def get_topics(self, obj):
34+
return count_for(obj, 'topic')
35+
def get_documents(self, obj):
36+
return count_for(obj, 'document')

editorsnotes/api/serializers/topics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def get_alternate_forms(self, obj):
3333
def get_project_value(self, obj):
3434
return [OrderedDict((
3535
('project_name', topic.project.name),
36-
('project_url', reverse('api:api-project-detail',
36+
('project_url', reverse('api:api-projects-detail',
3737
args=(topic.project.slug,),
3838
request=self.context['request'])),
3939
('preferred_name', topic.preferred_name),

editorsnotes/api/tests.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_topic_api_create(self):
138138
self.assertEqual(Revision.objects.count(), 1)
139139

140140
# Make sure an entry was added to the activity index
141-
activity_response = self.client.get(reverse('api:api-project-activity',
141+
activity_response = self.client.get(reverse('api:api-projects-activity',
142142
args=[self.project.slug]))
143143
self.assertEqual(activity_response.status_code, 200)
144144
self.assertEqual(len(activity_response.data['activity']), 1)
@@ -259,7 +259,7 @@ def test_topic_api_update(self):
259259
self.assertEqual(Revision.objects.count(), 1)
260260

261261
# Make sure an entry was added to the activity index
262-
activity_response = self.client.get(reverse('api:api-project-activity',
262+
activity_response = self.client.get(reverse('api:api-projects-activity',
263263
args=[self.project.slug]))
264264
self.assertEqual(activity_response.status_code, 200)
265265
activity_data = activity_response.data['activity'][0]
@@ -325,7 +325,7 @@ def test_topic_api_delete(self):
325325
self.assertEqual(main_models.TopicNode.objects.count(), 1)
326326

327327
# Make sure an entry was added to the activity index
328-
activity_response = self.client.get(reverse('api:api-project-activity',
328+
activity_response = self.client.get(reverse('api:api-projects-activity',
329329
args=[self.project.slug]))
330330
self.assertEqual(activity_response.status_code, 200)
331331
activity_data = activity_response.data['activity'][0]

editorsnotes/api/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
project_specific_patterns = patterns('',
77
### Project (general) ###
8-
url(r'^$', views.ProjectDetail.as_view(), name='api-project-detail'),
9-
url(r'^activity/$', views.ActivityView.as_view(), name='api-project-activity'),
8+
url(r'^$', views.ProjectDetail.as_view(), name='api-projects-detail'),
9+
url(r'^activity/$', views.ActivityView.as_view(), name='api-projects-activity'),
1010

1111
### Topics ###
1212
url(r'^topics/$', views.TopicList.as_view(), name='api-topics-list'),

editorsnotes/api/views/people.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,55 @@
1111
__all__ = ['ActivityView', 'ProjectList', 'ProjectDetail']
1212

1313
class ProjectList(ListAPIView):
14-
model = Project
14+
queryset = Project.objects.all()
1515
serializer_class = ProjectSerializer
1616

1717
class ProjectDetail(RetrieveAPIView):
18-
model = Project
18+
queryset = Project.objects.all()
1919
serializer_class = ProjectSerializer
2020
def get_object(self):
2121
qs = self.get_queryset()
2222
project = get_object_or_404(qs, slug=self.kwargs['project_slug'])
2323
return project
2424

25+
def parse_int(val, default=25, maximum=100):
26+
if not isinstance(val, int):
27+
try:
28+
val = int(val)
29+
except ValueError:
30+
val = default
31+
return val if val <= maximum else maximum
32+
2533
class ActivityView(GenericAPIView):
2634
"""
2735
Recent activity for a user or project.
2836
2937
Takes the following arguments:
3038
31-
* number (int)
39+
* count (int)
3240
* start (int)
3341
* type ("note", "topic", "document", "transcript", "footnote")
3442
* action ("add", "change", "delete")
3543
* order ('asc' or 'desc')
3644
"""
45+
TYPES = ['note', 'topic', 'document', 'transcript', 'footnote']
46+
ACTIONS = ['added', 'changed', 'deleted']
47+
def get_es_query(self):
48+
q = {'query': {'filtered': {'filter': {'bool': { 'must': []}}}}}
49+
params = self.request.QUERY_PARAMS
50+
if 'count' in params:
51+
q['size'] = parse_int(params['count'])
52+
if 'start' in params:
53+
q['from'] = parse_int(params['start'])
54+
if 'type' in params and params['type'] in self.TYPES:
55+
q['query']['filtered']['filter']['bool']['must'].append({
56+
'term': { 'data.type': params['type'] }
57+
})
58+
if 'action' in params and params['action'] in self.ACTIONS:
59+
q['query']['filtered']['filter']['bool']['must'].append({
60+
'term': { 'data.action': params['action'] }
61+
})
62+
return q
3763
def get_object(self, username=None, project_slug=None):
3864
if username is not None:
3965
obj = get_object_or_404(User, username=username)
@@ -44,7 +70,6 @@ def get_object(self, username=None, project_slug=None):
4470
return obj
4571
def get(self, request, format=None, **kwargs):
4672
obj = self.get_object(**kwargs)
47-
data = get_index('activity').get_activity_for(obj)
73+
es_query = self.get_es_query()
74+
data = get_index('activity').get_activity_for(obj, es_query)
4875
return Response({ 'activity': data })
49-
50-

editorsnotes/search/index.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,25 +158,36 @@ def search(self, query, highlight=False, **kwargs):
158158
class ActivityIndex(ElasticSearchIndex):
159159
def get_name(self):
160160
return settings.ELASTICSEARCH_PREFIX + '-activitylog'
161-
def get_activity_for(self, entity, size=25, **kwargs):
162-
query = {
163-
'query': {},
164-
'sort': {'data.time': { 'order': 'desc', 'ignore_unmapped': True }}
165-
}
161+
def get_activity_for(self, entity, es_query=None, size=25):
162+
query = es_query or {}
163+
if not 'size' in query:
164+
query['size'] = size
165+
if not 'sort' in query:
166+
query['sort'] = {
167+
'data.time': { 'order': 'desc', 'ignore_unmapped': True }
168+
}
169+
if not 'query' in query:
170+
query['query'] = {'filtered': {'filter': {'bool': { 'must': []}}}}
171+
166172
if isinstance(entity, User):
167-
query['query']['match'] = { 'data.user': entity.username }
173+
query['query']['filtered']['filter']['bool']['must'].append({
174+
'term': { 'data.user': entity.username }
175+
})
168176
elif isinstance(entity, Project):
169-
query['query']['match'] = { 'data.project': entity.slug }
177+
query['query']['filtered']['filter']['bool']['must'].append({
178+
'term': { 'data.project': entity.slug }
179+
})
170180
else:
171181
raise ValueError('Must pass either project or user')
172-
query.update(kwargs)
173-
search = self.es.search(query, index=self.name, size=size)
182+
183+
search = self.es.search(query, index=self.name)
174184
return [ hit['_source']['data'] for hit in search['hits']['hits'] ]
175185

176-
def handle_edit(self, instance):
186+
def handle_edit(self, instance, refresh=True):
177187
serializer = ActivitySerializer(instance)
178188
data = json.loads(JSONRenderer().render(serializer.data),
179189
object_pairs_hook=OrderedDict)
180190

181-
self.es.index(self.name, 'activity',{ 'id': instance.id, 'data': data },
182-
id='id', refresh=True)
191+
self.es.index(self.name, 'activity',
192+
{ 'id': instance.id, 'data': data },
193+
refresh=refresh)

0 commit comments

Comments
 (0)